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..2665ad7e 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -15,40 +15,55 @@ 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 + 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_parallel/test_forward.py - - name: Covering parallel tests + source /home/olender/Firedrakes/newest3/firedrake/bin/activate + mpiexec -n 6 pytest test_3d/test_hexahedral_convergence.py + mpiexec -n 6 pytest test_parallel/test_forward.py + mpiexec -n 6 pytest test_parallel/test_fwi.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_parallel/test_forward.py - - name: Running parallel 3D forward test + 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: Covering parallel 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 test_3d/test_forward_3d.py - - name: Covering parallel 3D forward test + source /home/olender/Firedrakes/newest3/firedrake/bin/activate + mpiexec -n 6 pytest --cov-report=xml --cov-append --cov=spyro test_parallel/test_forward.py + - name: Covering parallel fwi 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_parallel/test_fwi.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/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..99d93959 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + // { + // "name": "Python Attach 0", + // "type": "python", + // "request": "attach", + // "port": 3000, + // "host": "localhost", + // }, + // { + // "name": "Python Attach 1", + // "type": "python", + // "request": "attach", + // "port": 3001, + // "host": "localhost" + // }, + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 3a4f1288..28ea9bb6 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ [![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) -spyro: Acoustic wave modeling in Firedrake +spyro: seismic parallel inversion and reconstruction optimization framework ============================================ +Wave modeling in Firedrake + spyro 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). +These wave propagators can be used to form complete full waveform inversion (FWI) applications. See the [notebooks](https://github.com/Olender/spyro-1/tree/main/notebook_tutorials). To implement these solvers, spyro 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 spyro, you'll need to have some knowledge of Python and some basic concepts in inverse modeling relevant to active-source 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 +53,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/demos/run_forward.py b/demos/run_forward.py deleted file mode 100644 index a2066dab..00000000 --- a/demos/run_forward.py +++ /dev/null @@ -1,62 +0,0 @@ -import spyro - -model = {} - -model["opts"] = { - "method": "KMV", # either CG or KMV - "quadratrue": "KMV", # Equi or KMV - "degree": 5, # p order - "dimension": 2, # dimension -} -model["parallelism"] = { - "type": "automatic", -} -model["mesh"] = { - "Lz": 3.5, # depth in km - always positive - "Lx": 17.0, # width in km - always positive - "Ly": 0.0, # thickness in km - always positive - "meshfile": "meshes/marmousi_exact.msh", - "initmodel": "not_used.hdf5", - "truemodel": "velocity_models/marmousi_exact.hdf5", -} -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.5, # maximum acoustic wave velocity in PML - km/s - "R": 1e-6, # theoretical reflection coefficient - "lz": 0.9, # thickness of the PML in the z-direction (km) - always positive - "lx": 0.9, # 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 -} -model["acquisition"] = { - "source_type": "Ricker", - "num_sources": 40, - "source_pos": spyro.create_transect((-0.01, 1.0), (-0.01, 15.0), 40), - "frequency": 5.0, - "delay": 1.0, - "num_receivers": 500, - "receiver_locations": spyro.create_transect((-0.10, 0.1), (-0.10, 17.0), 500), -} -model["timeaxis"] = { - "t0": 0.0, # Initial time for event - "tf": 6.00, # Final time for event - "dt": 0.001, - "amplitude": 1, # the Ricker has an amplitude of 1. - "nspool": 100, # how frequently to output solution to pvds - "fspool": 99999, # how frequently to save solution to RAM -} -comm = spyro.utils.mpi_init(model) -mesh, V = spyro.io.read_mesh(model, comm) -vp = spyro.io.interpolate(model, mesh, V, guess=False) -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"], -) -p, p_r = spyro.solvers.forward(model, mesh, comm, vp, sources, wavelet, receivers) -spyro.plots.plot_shots(model, comm, p_r, vmin=-1e-3, vmax=1e-3) -spyro.io.save_shots(model, comm, p_r) diff --git a/demos/run_fwi.py b/demos/run_fwi.py deleted file mode 100644 index a52bf09c..00000000 --- a/demos/run_fwi.py +++ /dev/null @@ -1,245 +0,0 @@ -from firedrake import * -import numpy as np -import finat -from ROL.firedrake_vector import FiredrakeVector as FeVector -import ROL -from mpi4py import MPI - -import spyro - -# import gc - -outdir = "fwi_p5/" - - -model = {} - -model["opts"] = { - "method": "KMV", # either CG or KMV - "quadratrue": "KMV", # Equi or KMV - "degree": 5, # p order - "dimension": 2, # dimension - "regularization": True, # regularization is on? - "gamma": 1.0e-6, # regularization parameter -} -model["parallelism"] = { - "type": "automatic", -} -model["mesh"] = { - "Lz": 3.5, # depth in km - always positive - "Lx": 17.0, # width in km - always positive - "Ly": 0.0, # thickness in km - always positive - "meshfile": "meshes/marmousi_guess.msh", - "initmodel": "velocity_models/marmousi_guess.hdf5", - "truemodel": "not_used.hdf5", -} -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.5, # maximum acoustic wave velocity in PML - km/s - "R": 1e-6, # theoretical reflection coefficient - "lz": 0.9, # thickness of the PML in the z-direction (km) - always positive - "lx": 0.9, # 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 -} -model["acquisition"] = { - "source_type": "Ricker", - "num_sources": 40, - "source_pos": spyro.create_transect((-0.01, 1.0), (-0.01, 15.0), 40), - "frequency": 5.0, - "delay": 1.0, - "num_receivers": 500, - "receiver_locations": spyro.create_transect((-0.10, 0.1), (-0.10, 17.0), 500), -} -model["timeaxis"] = { - "t0": 0.0, # Initial time for event - "tf": 6.00, # Final time for event - "dt": 0.001, - "amplitude": 1, # the Ricker has an amplitude of 1. - "nspool": 1000, # how frequently to output solution to pvds - "fspool": 10, # how frequently to save solution to RAM -} -comm = spyro.utils.mpi_init(model) -# if comm.comm.rank == 0 and comm.ensemble_comm.rank == 0: -# fil = open("FUNCTIONAL_FWI_P5.txt", "w") -mesh, V = spyro.io.read_mesh(model, comm) -vp = spyro.io.interpolate(model, mesh, V, guess=True) -if comm.ensemble_comm.rank == 0: - File("guess_velocity.pvd", comm=comm.comm).write(vp) -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"], -) - -if comm.ensemble_comm.rank == 0: - control_file = File(outdir + "control.pvd", comm=comm.comm) - grad_file = File(outdir + "grad.pvd", comm=comm.comm) - -quad_rule = finat.quadrature.make_quadrature( - V.finat_element.cell, V.ufl_element().degree(), "KMV" -) -dxlump = dx(scheme=quad_rule) - -water = np.where(vp.dat.data[:] < 1.51) - - -class L2Inner(object): - def __init__(self): - self.A = assemble( - TrialFunction(V) * TestFunction(V) * dxlump, mat_type="matfree" - ) - self.Ap = as_backend_type(self.A).mat() - - def eval(self, _u, _v): - upet = as_backend_type(_u).vec() - vpet = as_backend_type(_v).vec() - A_u = self.Ap.createVecLeft() - self.Ap.mult(upet, A_u) - return vpet.dot(A_u) - - -kount = 0 - - -def regularize_gradient(vp, dJ, gamma): - """Tikhonov regularization""" - m_u = TrialFunction(V) - m_v = TestFunction(V) - mgrad = m_u * m_v * dx(scheme=qr_x) - ffG = dot(grad(vp), grad(m_v)) * dx(scheme=qr_x) - G = mgrad - ffG - lhsG, rhsG = lhs(G), rhs(G) - gradreg = Function(V) - grad_prob = LinearVariationalProblem(lhsG, rhsG, gradreg) - grad_solver = LinearVariationalSolver( - grad_prob, - solver_parameters={ - "ksp_type": "preonly", - "pc_type": "jacobi", - "mat_type": "matfree", - }, - ) - grad_solver.solve() - dJ += gamma * gradreg - return dJ - - -class Objective(ROL.Objective): - def __init__(self, inner_product): - ROL.Objective.__init__(self) - self.inner_product = inner_product - self.p_guess = None - self.misfit = 0.0 - self.p_exact_recv = spyro.io.load_shots(model, comm) - - def value(self, x, tol): - """Compute the functional""" - J_total = np.zeros((1)) - self.p_guess, p_guess_recv = spyro.solvers.forward( - model, - mesh, - comm, - vp, - sources, - wavelet, - receivers, - ) - self.misfit = spyro.utils.evaluate_misfit( - model, p_guess_recv, self.p_exact_recv - ) - J_total[0] += spyro.utils.compute_functional(model, self.misfit, velocity=vp) - J_total = COMM_WORLD.allreduce(J_total, op=MPI.SUM) - J_total[0] /= comm.ensemble_comm.size - if comm.comm.size > 1: - J_total[0] /= comm.comm.size - return J_total[0] - - def gradient(self, g, x, tol): - """Compute the gradient of the functional""" - dJ = Function(V, name="gradient") - dJ_local = spyro.solvers.gradient( - model, - mesh, - comm, - vp, - receivers, - self.p_guess, - self.misfit, - ) - if comm.ensemble_comm.size > 1: - comm.allreduce(dJ_local, dJ) - else: - dJ = dJ_local - dJ /= comm.ensemble_comm.size - if comm.comm.size > 1: - dJ /= comm.comm.size - # regularize the gradient if asked. - if model["opts"]["regularization"]: - gamma = model["opts"]["gamma"] - dJ = regularize_gradient(vp, dJ, gamma) - # mask the water layer - dJ.dat.data[water] = 0.0 - # Visualize - if comm.ensemble_comm.rank == 0: - grad_file.write(dJ) - g.scale(0) - g.vec += dJ - - def update(self, x, flag, iteration): - vp.assign(Function(V, x.vec, name="velocity")) - # If iteration reduces functional, save it. - if iteration >= 0: - if comm.ensemble_comm.rank == 0: - control_file.write(vp) - - -paramsDict = { - "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": 100, - "Step Tolerance": 1.0e-16, - }, -} - -params = ROL.ParameterList(paramsDict, "Parameters") - -inner_product = L2Inner() - -obj = Objective(inner_product) - -u = Function(V, name="velocity").assign(vp) -opt = FeVector(u.vector(), inner_product) -# Add control bounds to the problem (uses more RAM) -xlo = Function(V) -xlo.interpolate(Constant(1.0)) -x_lo = FeVector(xlo.vector(), inner_product) - -xup = Function(V) -xup.interpolate(Constant(5.0)) -x_up = FeVector(xup.vector(), inner_product) - -bnd = ROL.Bounds(x_lo, x_up, 1.0) - -# Set up the line search -algo = ROL.Algorithm("Line Search", params) - -algo.run(opt, obj, bnd) - -if comm.ensemble_comm.rank == 0: - File("res.pvd", comm=comm.comm).write(obj.vp) - -# fil.close() diff --git a/notebook_tutorials/altering_time_integration.ipynb b/notebook_tutorials/altering_time_integration.ipynb new file mode 100644 index 00000000..e864bebc --- /dev/null +++ b/notebook_tutorials/altering_time_integration.ipynb @@ -0,0 +1,563 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Altering time discretization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial was prepared by Alexandre Olender. If you have any questions, please email: olender@usp.br\n", + "\n", + "This tutorial is specifically tailored for developers of Spyro, an open-source Python library for modeling waves. Spyro provides a user-friendly interface built on top of Firedrake, making working with complex mathematical models easier. Before you begin this tutorial, it is recommended that you familiarize yourself either with Firedrake (https://www.firedrakeproject.org/documentation.html) or FEniCS (which uses the same domain-specific language called Unified Form Language - UFL). Firedrake is an automated system for the solution of partial differential equations using the finite element method (FEM). In addition to the prerequisite knowledge of Firedrake or FEniCS, a solid understanding of Python programming and basic concepts of numerical methods will be beneficial. \n", + "\n", + "This tutorial, however, does not delve into the details of finite element methods.\n", + "\n", + "By the end of this tutorial, you will have the skills to start implementing explicit time integration schemes in Spyro." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "firedrake:WARNING OMP_NUM_THREADS is not set or is set to a value greater than 1, we suggest setting OMP_NUM_THREADS=1 to improve performance\n" + ] + } + ], + "source": [ + "# Code in this cell enables plotting in the notebook\n", + "%matplotlib inline\n", + "import spyro" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Wave class" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `Wave` class in spyro provides a base class for any wave propagator." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on class Wave in module spyro.solvers.wave:\n", + "\n", + "class Wave(spyro.io.model_parameters.Model_parameters)\n", + " | Wave(dictionary=None, comm=None)\n", + " | \n", + " | Base class for wave equation solvers.\n", + " | \n", + " | Attributes:\n", + " | -----------\n", + " | comm: MPI communicator\n", + " | \n", + " | initial_velocity_model: firedrake function\n", + " | Initial velocity model\n", + " | function_space: firedrake function space\n", + " | Function space for the wave equation\n", + " | current_time: float\n", + " | Current time of the simulation\n", + " | solver_parameters: Python object\n", + " | Contains solver parameters\n", + " | real_shot_record: firedrake function\n", + " | Real shot record\n", + " | wavelet: list of floats\n", + " | Values at timesteps of wavelet used in the simulation\n", + " | mesh: firedrake mesh\n", + " | Mesh used in the simulation (2D or 3D)\n", + " | mesh_z: symbolic coordinate z of the mesh object\n", + " | mesh_x: symbolic coordinate x of the mesh object\n", + " | mesh_y: symbolic coordinate y of the mesh object\n", + " | sources: Sources object\n", + " | Contains information about sources\n", + " | receivers: Receivers object\n", + " | Contains information about receivers\n", + " | \n", + " | Methods:\n", + " | --------\n", + " | set_mesh: sets or calculates new mesh\n", + " | set_solver_parameters: sets new or default solver parameters\n", + " | get_spatial_coordinates: returns spatial coordinates of mesh\n", + " | set_initial_velocity_model: sets initial velocity model\n", + " | get_and_set_maximum_dt: calculates and/or sets maximum dt\n", + " | get_mass_matrix_diagonal: returns diagonal of mass matrix\n", + " | set_last_solve_as_real_shot_record: sets last solve as real shot record\n", + " | \n", + " | Method resolution order:\n", + " | Wave\n", + " | spyro.io.model_parameters.Model_parameters\n", + " | builtins.object\n", + " | \n", + " | Methods defined here:\n", + " | \n", + " | __init__(self, dictionary=None, comm=None)\n", + " | Wave object solver. Contains both the forward solver\n", + " | and gradient calculator methods.\n", + " | \n", + " | Parameters:\n", + " | -----------\n", + " | comm: MPI communicator\n", + " | \n", + " | model_parameters: Python object\n", + " | Contains model parameters\n", + " | \n", + " | forward_solve(self)\n", + " | Solves the forward problem.\n", + " | \n", + " | get_and_set_maximum_dt(self, fraction=0.7, estimate_max_eigenvalue=False)\n", + " | Calculates and sets the maximum stable time step (dt) for the wave solver.\n", + " | \n", + " | Args:\n", + " | fraction (float, optional):\n", + " | Fraction of the estimated time step to use. Defaults to 0.7.\n", + " | estimate_max_eigenvalue (bool, optional):\n", + " | Whether to estimate the maximum eigenvalue. Defaults to False.\n", + " | \n", + " | Returns:\n", + " | float: The calculated maximum time step (dt).\n", + " | \n", + " | get_mass_matrix_diagonal(self)\n", + " | Builds a section of the mass matrix for debugging purposes.\n", + " | \n", + " | get_spatial_coordinates(self)\n", + " | \n", + " | matrix_building(self)\n", + " | Builds the matrix for the forward problem.\n", + " | \n", + " | set_initial_velocity_model(self, constant=None, conditional=None, velocity_model_function=None, expression=None, new_file=None, output=False, dg_velocity_model=True)\n", + " | Method to define new user velocity model or file. It is optional.\n", + " | \n", + " | Parameters:\n", + " | -----------\n", + " | conditional: (optional)\n", + " | Firedrake conditional object.\n", + " | velocity_model_function: Firedrake function (optional)\n", + " | Firedrake function to be used as the velocity model. Has to be in the same function space as the object.\n", + " | expression: str (optional)\n", + " | If you use an expression, you can use the following variables:\n", + " | x, y, z, pi, tanh, sqrt. Example: \"2.0 + 0.5*tanh((x-2.0)/0.1)\".\n", + " | It will be interpoalte into either the same function space as the object or a DG0 function space\n", + " | in the same mesh.\n", + " | new_file: str (optional)\n", + " | Name of the file containing the velocity model.\n", + " | output: bool (optional)\n", + " | If True, outputs the velocity model to a pvd file for visualization.\n", + " | \n", + " | set_last_solve_as_real_shot_record(self)\n", + " | \n", + " | set_mesh(self, user_mesh=None, mesh_parameters=None)\n", + " | Set the mesh for the solver.\n", + " | \n", + " | Args:\n", + " | user_mesh (optional): User-defined mesh. Defaults to None.\n", + " | mesh_parameters (optional): Parameters for generating a mesh. Defaults to None.\n", + " | \n", + " | set_solver_parameters(self, parameters=None)\n", + " | Set the solver parameters.\n", + " | \n", + " | Args:\n", + " | parameters (dict): A dictionary containing the solver parameters.\n", + " | \n", + " | Returns:\n", + " | None\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Methods inherited from spyro.io.model_parameters.Model_parameters:\n", + " | \n", + " | get_mesh(self)\n", + " | Reads in an external mesh and scatters it between cores.\n", + " | \n", + " | Returns\n", + " | -------\n", + " | mesh: Firedrake.Mesh object\n", + " | The distributed mesh across `ens_comm`\n", + " | \n", + " | get_wavelet(self)\n", + " | Returns a wavelet based on the source type.\n", + " | \n", + " | Returns\n", + " | -------\n", + " | wavelet : numpy.ndarray\n", + " | Wavelet values in each time step to be used in the simulation.\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Data descriptors inherited from spyro.io.model_parameters.Model_parameters:\n", + " | \n", + " | __dict__\n", + " | dictionary for instance variables (if defined)\n", + " | \n", + " | __weakref__\n", + " | list of weak references to the object (if defined)\n", + "\n" + ] + } + ], + "source": [ + "help(spyro.Wave)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is highly recommended that every wave solver inherit this class. Two of the methods present in it are abstract methods. This means that any concrete class derived from it needs to define them. They are the matrix_building and the forward_solve methods. The matrix_building method is not the subject of this tutorial; however, we have to consider its outputs. The name itself is counterintuitive since it does not necessarily have to build a matrix. However, we will momentarily deal with it as having matrices for teaching purposes. Spyro focuses on solving time-dependent wave equations. After doing the finite element-based spatial discretization, we should have a time-dependent equation with something analogous to the following:\n", + "\n", + "$A_1 \\ddot{Q} + A_2 \\dot{Q} + A_3 Q = F$\n", + "\n", + "For most cases in seismic imaging, we limit ourselves to explicit time integration schemes. Several papers have examined their advantages compared to implicit schemes when dealing with PDEs encountered for seismic imaging. However, this is not a rigorous rule. Studying new implicit schemes is an exciting area of research with various promising developments. We do limit our scope in this tutorial to only explicit schemes. In these schemes, we can discretize the above equation in time to make a result at a given timestep an explicit function of the previous ones. In other words, the result at a timestep n+1 can be written as:\n", + "\n", + "$Q_{n+1} = M^{-1}(B_0 Q_{n} + B_1 Q_{n-1} + B_2 Q_{n-2} + ... + B_n)$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Not every previous $Q$ solution at a timestep needs to be used, since every variable we store increases our memory storage cost. For this tutorial we will focus on an integration scheme that only saves $Q_n$ and $Q_{n-1}$, therefore our equation above has to take the form:\n", + "\n", + "$Q_{n+1} = M^{-1}(B_0 Q_{n} + B_1 Q_{n-1} + B_2)$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Therefore, our time marching scheme has to apply a solver operator, where the previous values for $Q$ are known and calculate $Q_{n+1}$. Afterward, $Q_n$ and $Q_{n-1}$ have to be updated. I have used $Q$ instead of $u$ or $p$ for the desired time-dependent variable we are calculating for, because our PDE can actually be a system of equations with various time-dependent auxiliariary variables. In those cases, we use a mixed-function space, and the pressure-related space has to be first." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us build an acoustic wave object. For simplicity, I will use a rectangle example (see premade useful examples tutorial)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parallelism type: automatic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/alexandre/Development/tutorials/spyro-1/spyro/solvers/wave.py:85: UserWarning: No mesh file, Firedrake mesh will be automatically generated.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "dictionary = {}\n", + "dictionary[\"mesh\"] = {\n", + " \"Lz\": 3.0, # depth in km - always positive\n", + " \"Lx\": 4.0, # width in km - always positive\n", + " \"h\": 0.1, # mesh size in km\n", + "}\n", + "dictionary[\"absorving_boundary_conditions\"] = {\n", + " \"status\": False,\n", + " \"pad_length\": 0.,\n", + "}\n", + "dictionary[\"acquisition\"] = {\n", + " \"source_locations\": [(-0.1, 2.0)],\n", + " \"receiver_locations\": spyro.create_transect((-1.0, 0.0), (-1.0, 4.0), 20),\n", + "}\n", + "Wave_obj = spyro.examples.Rectangle_acoustic(dictionary=dictionary)\n", + "layer_values = [1.5, 2.5, 3.5]\n", + "z_switches = [-1.0, -2.0]\n", + "Wave_obj.multiple_layer_velocity_model(z_switches, layer_values)\n", + "\n", + "# to visualize" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File name model.png\n" + ] + }, + { + "data": { + "image/png": "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P950LHTDiy++qBkzZqiurk4xMTGm48ClX/ziF1qyZIl2795tOgpcWLZsmW6//XaVlZWZjoIOnHvuuZo0aZIefPBBSZJt28rOztZ3vvMd/ehHPzKcDm5YlqXnn39eM2bMMB3llMaRExhVUlKim2++WY8//rj69OljOg5CcOTIET3xxBOaMmUKhYnPlJeXq3///qZjAL5XX1+vd999V9OmTWtZFggENG3aNBUUFBhMBvgPxQmMcRxHN954o7797W9r4sSJpuOgm374wx+qb9++GjBggAoLC/XCCy+YjoRu2Llzp37zm9/olltuMR0F8L1Dhw6pqalJGRkZQcszMjJUXFxsKBXgTxQn6HU/+tGPZFnWCS/bt2/Xb37zG1VWVmrevHmmI0Pun7fPfP/739emTZv02muvKSoqSjNnzhRniUZed583Sdq/f7+mT5+ua665RjfffLOh5Ke2UJ43ADgV0OcEve7gwYM6fPjwCdsMHz5c1157rf785z/LsqyW5U1NTYqKitINN9ygxx57LNxR0Yrb5y02Nrbd8k8//VTZ2dlav3698vLywhURHeju83bgwAFNnTpV5513npYtW6ZAgN+oTAjl740+J95VX1+vPn366E9/+lNQf4VZs2aprKyMI8s+QZ8Tb4g2HQAnn7S0NKWlpXXZ7te//rV++tOftvz/wIEDys/P19NPP61zzz03nBHRAbfPW0ds25bUPCQ0Iqs7z9v+/ft18cUX65xzztGjjz5KYWJQT/7e4D2xsbE655xztGbNmpYvtrZta82aNbrtttvMhgN8huIExpx22mlB/+/Xr58kacSIERoyZIiJSHDh7bff1saNG3XBBRcoNTVVu3bt0o9//GONGDGCoyYetn//fk2dOlVDhw7VL3/5Sx08eLBlXWZmpsFk6EphYaGOHDmiwsJCNTU1tcwFNXLkyJb3TZg3d+5czZo1SxMnTtTkyZN1//33q7q6Wt/85jdNR8MJVFVVaefOnS3/37NnjzZv3qz+/fu3+56CyKA4AdAtffr00YoVK7RgwQJVV1crKytL06dP15133qm4uDjT8dCJ1atXa+fOndq5c2e74p+ze71t/vz5Qae5TpgwQZL0+uuva+rUqYZSoa3rrrtOBw8e1Pz581VcXKzx48fr1VdfbddJHt7yzjvv6OKLL275/9y5cyU1n5K3bNkyQ6lObfQ5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CWX/Ss2zQLfed3v37g1az2s3tH27YsWKoP26bds2RUVFtXvPPdVfu0OGDNE999yjd999V++8844uueQSXXXVVfrggw86bM/7rXvd3be837rX3X0rRfi91kGXJDnPP//8Cdu8/vrrjiTn6NGjnbaZPHmyM2fOnJb/NzU1OYMGDXIWLVrUS0n9yc3+bev55593LMtyPvnkk5ZlCxYscHJzc3s3nM+VlpY6kpw33nij0zbXXnutc8UVVwQtO/fcc51bbrnFcRzHsW3byczMdH7xi1+0rC8rK3Pi4uKcJ598MjzBfcDNvm2rsbHRSUxMdB577LGWZbNmzXKuuuqqMCT0Nzf799FHH3WSk5M7Xc9rt2OhvHbvu+8+JzEx0amqqmpZxmu3Y6mpqc7vf//7DtfxftszJ9q3bfF+2z0n2reRfq/lyEkvGz9+vLKysnTppZfq73//e8vy+vp6vfvuu5o2bVrLskAgoGnTpqmgoMBEVF/7wx/+oGnTpmno0KFByz/++GMNGjRIw4cP1w033KDCwkJDCb2hvLxcktS/f/9O2xQUFAS9LiUpPz+/5XW5Z88eFRcXB7VJTk7Wueeee0q/dt3s27aOHTumhoaGdrdZu3at0tPTNXr0aN166606fPhwr2b1I7f7t6qqSkOHDlV2dna7X/547XYslNfuH/7wB331q19V3759g5bz2j2uqalJTz31lKqrq5WXl9dhG95vQ+Nm37bF+607bvdtJN9ro7t9C3QoKytLDz/8sCZOnKi6ujr9/ve/19SpU/X222/r7LPP1qFDh9TU1KSMjIyg22VkZGj79u2GUvvTgQMH9Morr2j58uVBy88991wtW7ZMo0ePVlFRkX7yk5/owgsv1LZt25SYmGgorTm2bev222/X+eefr7Fjx3barri4uMPX5Wf9pT7790RtTjVu921bP/zhDzVo0KCgN/Dp06fr6quv1umnn65du3bpjjvu0OWXX66CggJFRUWFI77nud2/o0eP1tKlSzVu3DiVl5frl7/8paZMmaIPPvhAQ4YM4bXbgVBeuxs2bNC2bdv0hz/8IWg5r91mW7duVV5enmpra9WvXz89//zzOuusszpsy/tt93Rn37bF++2JdWffRvq9luKkl4wePVqjR49u+f+UKVO0a9cu3XfffXr88ccNJjv5PPbYY0pJSdGMGTOCll9++eUt18eNG6dzzz1XQ4cO1TPPPKPZs2dHOKV5c+bM0bZt2065fjeREMq+veeee/TUU09p7dq1QR0Jv/rVr7Zcz8nJ0bhx4zRixAitXbtWX/jCF3o1t1+43b95eXlBv/RNmTJFZ555ph555BHdfffd4Y7pS6G8dv/whz8oJydHkydPDlrOa7fZ6NGjtXnzZpWXl+tPf/qTZs2apTfeeMP1l2h0LtR9y/tt17qzbyP9XstpXWE0efJk7dy5U5I0cOBARUVFqaSkJKhNSUmJMjMzTcTzJcdxtHTpUn3jG99QbGzsCdumpKToc5/7XMtzcCq57bbb9NJLL+n111/XkCFDTtg2MzPzhK/Lz/7ltdusO/v2M7/85S91zz336LXXXtO4ceNO2Hb48OEaOHDgKfm6lULbv5+JiYnRhAkTWvYdr91goezb6upqPfXUU65+4DlVX7uxsbEaOXKkzjnnHC1atEi5ubl64IEHOmzL+233dGfffob3W3dC2befCfd7LcVJGG3evFlZWVmSml8E55xzjtasWdOy3rZtrVmzxvX5k2gedWbnzp2uPiirqqq0a9eulufgVOA4jm677TY9//zz+tvf/qbTTz+9y9vk5eUFvS4lafXq1S2vy9NPP12ZmZlBbSoqKvT222+fUq/dUPat1DyCyd13361XX31VEydO7LL9p59+qsOHD59Sr1sp9P3bWlNTk7Zu3dqy73jtNuvJvn322WdVV1enr3/96122PVVfu23Ztq26uroO1/F+2zMn2rcS77c90dW+bS3s77Xd7kJ/iqisrHQ2bdrkbNq0yZHk/OpXv3I2bdrk7N2713Ecx/nRj37kfOMb32hpf9999zkrV650Pv74Y2fr1q3Od7/7XScQCDh//etfW9o89dRTTlxcnLNs2TLnww8/dP7jP/7DSUlJcYqLiyP++Ezr7v79zNe//nXn3HPP7XCb3/ve95y1a9c6e/bscf7+978706ZNcwYOHOiUlpaG9bF4ya233uokJyc7a9eudYqKiloux44da2nzjW98w/nRj37U8v+///3vTnR0tPPLX/7S+eijj5wFCxY4MTExztatW1va3HPPPU5KSorzwgsvOO+//75z1VVXOaeffrpTU1MT0cdnUij79p577nFiY2OdP/3pT0G3qaysdByn+e/gf/7nf5yCggJnz549zl//+lfn7LPPdkaNGuXU1tZG/DGaFMr+/clPfuKsWrXK2bVrl/Puu+86X/3qV534+Hjngw8+aGnDaze0ffuZCy64wLnuuuvaLee12+xHP/qR88Ybbzh79uxx3n//fedHP/qRY1mW89prrzmOw/ttT3R33/J+6153922k32spTjrx2dDAbS+zZs1yHKd5OLqLLrqopf3Pf/5zZ8SIEU58fLzTv39/Z+rUqc7f/va3dtv9zW9+45x22mlObGysM3nyZOcf//hHhB6Rt3R3/zpO87B0CQkJzm9/+9sOt3ndddc5WVlZTmxsrDN48GDnuuuuc3bu3BnmR+ItHe1TSc6jjz7a0uaiiy5q2c+feeaZZ5zPfe5zTmxsrDNmzBjnL3/5S9B627adH//4x05GRoYTFxfnfOELX3B27NgRgUfkHaHs26FDh3Z4mwULFjiO4zjHjh1zLrvsMictLc2JiYlxhg4d6tx8882n5A8Woezf22+/veX9NCMjw/niF7/ovPfee0Hb5bUb+vvC9u3bHUktX1ha47Xb7KabbnKGDh3qxMbGOmlpac4XvvCFoP3F+23ourtveb91r7v7NtLvtZbjOE73j7cAAAAAQO+izwkAAAAAT6A4AQAAAOAJFCcAAAAAPIHiBAAAAIAnUJwAAAAA8ASKEwAAAACeQHECAAAAwBMoTgAA3TZs2DDdf//9nt1eazt27FBmZqYqKyslScuWLVNKSkpY7sut+vp6DRs2TO+8847RHADgNRQnAHAKufLKKzV9+vQO17311luyLEvvv/9+hFNJGzdu1H/8x3+0/N+yLK1cubJXtj1v3jx95zvfUWJiYq9sryvf/OY3deedd56wTWxsrP7nf/5HP/zhDyOSCQD8guIEAE4hs2fP1urVq/Xpp5+2W/foo49q4sSJGjduXMRzpaWlqU+fPr2+3cLCQr300ku68cYbe33bHWlqatJLL72kf//3f++y7Q033KB169bpgw8+iEAyAPAHihMAOIX827/9m9LS0rRs2bKg5VVVVXr22Wc1e/ZsSdK6det04YUXKiEhQdnZ2fqv//ovVVdXd7rdwsJCXXXVVerXr5+SkpJ07bXXqqSkJKjNn//8Z02aNEnx8fEaOHCgvvSlL7Wsa31a17BhwyRJX/rSl2RZloYNG6ZPPvlEgUCg3WlQ999/v4YOHSrbtjvM9cwzzyg3N1eDBw/uNPvBgwc1ceJEfelLX1JdXZ3Wrl0ry7K0atUqTZgwQQkJCbrkkktUWlqqV155RWeeeaaSkpL0ta99TceOHQva1vr16xUTE6NJkyapvr5et912m7KyshQfH6+hQ4dq0aJFLW1TU1N1/vnn66mnnuo0GwCcaihOAOAUEh0drZkzZ2rZsmVyHKdl+bPPPqumpiZdf/312rVrl6ZPn64vf/nLev/99/X0009r3bp1uu222zrcpm3buuqqq3TkyBG98cYbWr16tXbv3q3rrruupc1f/vIXfelLX9IXv/hFbdq0SWvWrNHkyZM73N7GjRslNR/JKSoq0saNGzVs2DBNmzZNjz76aFDbRx99VDfeeKMCgY4/zt566y1NnDix0/2xb98+XXjhhRo7dqz+9Kc/KS4urmXdXXfdpQcffFDr16/Xvn37dO211+r+++/X8uXL9Ze//EWvvfaafvOb3wRt78UXX9SVV14py7L061//Wi+++KKeeeYZ7dixQ0888URL4fWZyZMn66233uo0HwCcchwAwCnlo48+ciQ5r7/+esuyCy+80Pn617/uOI7jzJ492/mP//iPoNu89dZbTiAQcGpqahzHcZyhQ4c69913n+M4jvPaa685UVFRTmFhYUv7Dz74wJHkbNiwwXEcx8nLy3NuuOGGTjO13p7jOI4k5/nnnw9q8/TTTzupqalObW2t4ziO8+677zqWZTl79uzpdLu5ubnOwoULg5Y9+uijTnJysrN9+3YnOzvb+a//+i/Htu2W9a+//rojyfnrX//asmzRokWOJGfXrl0ty2655RYnPz8/aNujRo1yXnrpJcdxHOc73/mOc8kllwRtu60HHnjAGTZsWKfrAeBUw5ETADjFnHHGGZoyZYqWLl0qSdq5c6feeuutllO6tmzZomXLlqlfv34tl/z8fNm2rT179rTb3kcffaTs7GxlZ2e3LDvrrLOUkpKijz76SJK0efNmfeELX+hR7hkzZigqKkrPP/+8pOZRty6++OJ2RyNaq6mpUXx8fIfLL7zwQl199dV64IEHZFlWuzat+95kZGSoT58+Gj58eNCy0tLSlv9/9NFHOnDgQMvjvPHGG7V582aNHj1a//Vf/6XXXnut3X0kJCS0OzUMAE5lFCcAcAqaPXu2nnvuOVVWVurRRx/ViBEjdNFFF0lq7n9yyy23aPPmzS2XLVu26OOPP9aIESNCur+EhIQeZ46NjdXMmTP16KOPqr6+XsuXL9dNN910wtsMHDhQR48ebbc8Li5O06ZN00svvaT9+/d3eNuYmJiW65ZlBf3/s2Wt+7q8+OKLuvTSS1uKobPPPlt79uzR3XffrZqaGl177bX6yle+ErSNI0eOKC0t7cQPHABOIRQnAHAKuvbaaxUIBLR8+XL93//9n2666aaWowdnn322PvzwQ40cObLdJTY2tt22zjzzTO3bt0/79u1rWfbhhx+qrKxMZ511lqTmoxBr1qxxnS8mJkZNTU3tln/rW9/SX//6Vz300ENqbGzU1VdffcLtTJgwQR9++GG75YFAQI8//rjOOeccXXzxxTpw4IDrbJ154YUXdNVVVwUtS0pK0nXXXaff/e53evrpp/Xcc8/pyJEjLeu3bdumCRMm9Pi+AeBkQXECAKegfv366brrrtO8efNUVFQUNNTuD3/4Q61fv1633XabNm/erI8//lgvvPBCpx3ip02bppycHN1www167733tGHDBs2cOVMXXXRRS2f0BQsW6Mknn9SCBQv00UcfaevWrfr5z3/eab5hw4ZpzZo1Ki4uDjryceaZZ+q8887TD3/4Q11//fVdHpHJz89XQUFBh4VOVFSUnnjiCeXm5uqSSy5RcXHxCbd1IqWlpXrnnXf0b//2by3LfvWrX+nJJ5/U9u3b9c9//lPPPvusMjMzgyaAfOutt3TZZZeFfL8AcLKhOAGAU9Ts2bN19OhR5efna9CgQS3Lx40bpzfeeEP//Oc/deGFF2rChAmaP39+UJvWLMvSCy+8oNTUVH3+85/XtGnTNHz4cD399NMtbaZOnapnn31WL774osaPH69LLrlEGzZs6DTbvffeq9WrVys7O7vdkYXZs2ervr6+y1O6JOnyyy9XdHS0/vrXv3a4Pjo6Wk8++aTGjBnTMlxwKP785z9r8uTJGjhwYMuyxMRE/e///q8mTpyoSZMm6ZNPPtHLL7/cMrJYQUGBysvL253qBQCnMstxWo0lCQCAx91999169tlnXc9kv3jxYr344otatWpV2DL9+7//uy644AL94Ac/cH2b6667Trm5ubrjjjvClgsA/CbadAAAANyoqqrSJ598ogcffFA//elPXd/ulltuUVlZmSorK5WYmBiWbBdccIGuv/561+3r6+uVk5Oj//7v/w5LHgDwK46cAAB84cYbb9STTz6pGTNmaPny5YqKijIdCQDQyyhOAAAAAHgCHeIBAAAAeALFCQAAAABPoDgBAAAA4AkUJwAAAAA8geIEAAAAgCdQnAAAAADwBIoTAAAAAJ5AcQIAAADAEyhOAAAAAHjC/w8YLZAe1QMAqgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spyro.plots.plot_model(Wave_obj, show=True, flip_axis=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before propagating our wave, we have to define the velocity model for the solver operator and construct it." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "Wave_obj._get_initial_velocity_model()\n", + "Wave_obj.c = Wave_obj.initial_velocity_model\n", + "Wave_obj.matrix_building()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `_get_initial_velocity_model` is not actually necessary for this specific case (where the velocity model is already created in the object) and does not do anything in this case. However, it needs to be called, because in larger cases the velocity model is only loaded, by this method, into our function space right before it is needed (to conserve memory). Spyro will build the solver object based on the velocity model that the `c` attribute points to. This can be diferent than the `initial_velocity_model`, especially in inversion problems." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before we propagate our wave, we can now calculate the maximum upper bound of a stable timestep based on the spectral radius of our mass matrix. The actual maximum stable timestep can vary depending on the discretization used. If you are using lumped elements you can estimate the spectral radius to reduce total runtime of this calculation. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Timstep used: 0.0014619883040935672\n" + ] + } + ], + "source": [ + "Wave_obj.get_and_set_maximum_dt(fraction=0.8, estimate_max_eigenvalue=True)\n", + "print(f\"Timstep used: {Wave_obj.dt}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now focus on the time discretization. The source_id variable illustrates which sources we will be propagating. Inside the forward_solve method there is a wrapper to distribute them while taking into account our parallelism strategy. If you are making a new method you only have to copy that wrapper and put source_id as an input to your method." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "import firedrake as fire\n", + "source_id = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we just get the variables we will use from the corresponding object." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "excitations = Wave_obj.sources\n", + "excitations.current_source = source_id\n", + "receivers = Wave_obj.receivers\n", + "\n", + "output_filename = \"forward_time_scheme_tutorial.pvd\"\n", + "output = fire.File(output_filename)\n", + "\n", + "final_time = Wave_obj.final_time\n", + "dt = Wave_obj.dt\n", + "t = Wave_obj.current_time" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next code is very simple, but a common source of error. Please calculate the number of timesteps based on final time and current time. Don't forget to add one when calculating the number of timesteps!" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "nt = int((final_time - t) / dt) + 1 # ANSWER" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Firedrake stores data on Function objects. This is where we will save our solutions. u_nm1 and u_n are also functions in the same function space, that were previously defined. If you want to use more previous timesteps you have to add them too. To save space we will save the whole solution field of our pressure variable only for the necessary steps used in gradient calculation, with u_sol." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "X = fire.Function(Wave_obj.function_space)\n", + "\n", + "u_nm1 = Wave_obj.u_nm1\n", + "u_n = Wave_obj.u_n\n", + "u_np1 = fire.Function(Wave_obj.function_space)\n", + "\n", + "usol = [\n", + " fire.Function(Wave_obj.function_space, name=\"pressure\")\n", + " for t in range(nt)\n", + " if t % Wave_obj.gradient_sampling_frequency == 0\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our system has a right hand that can be calculated (based on the previous time-steps). Here we save a function to hold those values (`Function` objects in Firedrake have values and are always initialized with zeros). The rhs is actually a symbolic object that calcutes our right hand side. Everytime we change a variable in it we have to reassemble it." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "rhs_forcing = fire.Function(Wave_obj.function_space)\n", + "usol_recv = []\n", + "save_step = 0\n", + "B = Wave_obj.B\n", + "rhs = Wave_obj.rhs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The apply_source method projects our time-dependent source into our function space and saves the values in the relevant degrees of freedom. The following code does not have the update of the rpevious time steps. Please add them." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "for step in range(nt):\n", + " rhs_forcing.assign(0.0)\n", + " B = fire.assemble(rhs, tensor=B)\n", + " f = excitations.apply_source(rhs_forcing, Wave_obj.wavelet[step])\n", + " B0 = B.sub(0)\n", + " B0 += f\n", + " Wave_obj.solver.solve(X, B)\n", + "\n", + " u_np1.assign(X)\n", + "\n", + " usol_recv.append(\n", + " Wave_obj.receivers.interpolate(u_np1.dat.data_ro_with_halos[:])\n", + " )\n", + "\n", + " if step % Wave_obj.gradient_sampling_frequency == 0:\n", + " usol[save_step].assign(u_np1)\n", + " save_step += 1\n", + "\n", + " if (step - 1) % Wave_obj.output_frequency == 0:\n", + " assert (\n", + " fire.norm(u_n) < 1\n", + " ), \"Numerical instability. Try reducing dt or building the \\\n", + " mesh differently\"\n", + " if Wave_obj.forward_output:\n", + " output.write(u_n, time=t, name=\"Pressure\")\n", + "\n", + " u_nm1.assign(u_n) # ANSWER\n", + " u_n.assign(u_np1) # ANSWER\n", + "\n", + " t = step * float(dt)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "Wave_obj.current_time = t\n", + "\n", + "Wave_obj.receivers_output = usol_recv\n", + "\n", + "Wave_obj.forward_solution = usol\n", + "Wave_obj.forward_solution_receivers = usol_recv" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "firedrake", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebook_tutorials/meshing.html b/notebook_tutorials/meshing.html new file mode 100644 index 00000000..fe948904 --- /dev/null +++ b/notebook_tutorials/meshing.html @@ -0,0 +1,8976 @@ + + + + + +meshing + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/notebook_tutorials/meshing.ipynb b/notebook_tutorials/meshing.ipynb new file mode 100644 index 00000000..9b4fa2f0 --- /dev/null +++ b/notebook_tutorials/meshing.ipynb @@ -0,0 +1,1154 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Meshing in Spyro" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial was prepared by Alexandre Olender. If you have any questions, please email: olender@usp.br\n", + "\n", + "Meshing is a complex problem frequently encountered in Seismic imaging. For seismic imaging based on higher-order finite element methods, such as Spectral Element Methods of higher-order mass lumped triangles, adequate meshing is a computational necessity. Using higher-order FEM without any specific mesh considerations will usually give results computationally significantly more expensive than finite difference-based wave solvers. This inherent meshing complexity tends to turn users away from FEM-based solvers. Spyro aims to treat most seismic imaging-based problems with little user input relative to generating meshes, removing the additional complexity encountered by an end-user when propagating waves. \n", + "\n", + "Wave solvers in Spyro can automatically create meshes based on the inputs of the dictionary parameters. For most users, this will be enough. For these automatic mesh capabilities, please see the **simple forward exercises** and the **simple forward with overthrust** tutorials. This tutorial is only geared for more advanced users who need to directly call on the `AutomaticMesh` class or develop inside it." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "firedrake:WARNING OMP_NUM_THREADS is not set or is set to a value greater than 1, we suggest setting OMP_NUM_THREADS=1 to improve performance\n" + ] + } + ], + "source": [ + "# Code in this cell enables plotting in the notebook\n", + "%matplotlib inline\n", + "\n", + "import spyro" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "In Spyro, we use the Firedrake meshing utilities and a Python package for simplex (2D triangles and 3D tetrahedral) mesh generation called SeismicMesh. A user can input Gmsh-generated meshes (see https://www.firedrakeproject.org/demos/immersed_fem.py.html) and also create meshes separately in SeismicMesh (https://doi.org/10.21105/joss.02687). Any mesh compatible with Firedrake is compatible with Spyro. Here, we will use Spyro's wrappers for Firedrake and SeismicMesh for mesh generation. This should be enough for any synthetic or complex real seismic imaging problem. However, if you are using Spyro for other cases, creating those meshes in either Gmsh or SeimicMesh might be necessary." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Automatic Mesh class" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the Automatic mesh class, we use Firedrake and SeismicMesh-based wrappers. Almost every option is based on the weird seismic orientation we found in segy files. The axis orders are z, x, and y for 3D and z, x for 2D, with z coordinates going in the negative direction. We can learn more about this class using Python's help method." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on class AutomaticMesh in module spyro.meshing.meshing_functions:\n", + "\n", + "class AutomaticMesh(builtins.object)\n", + " | AutomaticMesh(comm=None, mesh_parameters=None)\n", + " | \n", + " | Class for automatic meshing.\n", + " | \n", + " | Attributes\n", + " | ----------\n", + " | dimension : int\n", + " | Spatial dimension of the mesh.\n", + " | length_z : float\n", + " | Length of the domain in the z direction.\n", + " | length_x : float\n", + " | Length of the domain in the x direction.\n", + " | length_y : float\n", + " | Length of the domain in the y direction.\n", + " | dx : float\n", + " | Mesh size.\n", + " | quadrilateral : bool\n", + " | If True, the mesh is quadrilateral.\n", + " | periodic : bool\n", + " | If True, the mesh is periodic.\n", + " | comm : MPI communicator\n", + " | MPI communicator.\n", + " | mesh_type : str\n", + " | Type of the mesh.\n", + " | abc_pad : float\n", + " | Padding to be added to the domain.\n", + " | \n", + " | Methods\n", + " | -------\n", + " | set_mesh_size(length_z=None, length_x=None, length_y=None)\n", + " | Sets the mesh size.\n", + " | set_meshing_parameters(dx=None, cell_type=None, mesh_type=None)\n", + " | Sets the meshing parameters.\n", + " | set_seismicmesh_parameters(cpw=None, velocity_model=None, edge_length=None)\n", + " | Sets the SeismicMesh parameters.\n", + " | make_periodic()\n", + " | Sets the mesh boundaries periodic. Only works for firedrake_mesh.\n", + " | create_mesh()\n", + " | Creates the mesh.\n", + " | create_firedrake_mesh()\n", + " | Creates a mesh based on Firedrake meshing utilities.\n", + " | create_firedrake_2D_mesh()\n", + " | Creates a 2D mesh based on Firedrake meshing utilities.\n", + " | create_firedrake_3D_mesh()\n", + " | Creates a 3D mesh based on Firedrake meshing utilities.\n", + " | create_seismicmesh_mesh()\n", + " | Creates a mesh based on SeismicMesh meshing utilities.\n", + " | create_seimicmesh_2d_mesh()\n", + " | Creates a 2D mesh based on SeismicMesh meshing utilities.\n", + " | create_seismicmesh_2D_mesh_homogeneous()\n", + " | Creates a 2D mesh homogeneous velocity mesh based on SeismicMesh meshing utilities.\n", + " | \n", + " | Methods defined here:\n", + " | \n", + " | __init__(self, comm=None, mesh_parameters=None)\n", + " | Initialize the MeshingFunctions class.\n", + " | \n", + " | Parameters\n", + " | ----------\n", + " | comm : MPI communicator, optional\n", + " | MPI communicator. The default is None.\n", + " | mesh_parameters : dict, optional\n", + " | Dictionary containing the mesh parameters. The default is None.\n", + " | \n", + " | Raises\n", + " | ------\n", + " | ValueError\n", + " | If `abc_pad_length` is negative.\n", + " | \n", + " | Notes\n", + " | -----\n", + " | The `mesh_parameters` dictionary should contain the following keys:\n", + " | - 'dimension': int, optional. Dimension of the mesh. The default is 2.\n", + " | - 'length_z': float, optional. Length of the mesh in the z-direction.\n", + " | - 'length_x': float, optional. Length of the mesh in the x-direction.\n", + " | - 'length_y': float, optional. Length of the mesh in the y-direction.\n", + " | - 'cell_type': str, optional. Type of the mesh cells.\n", + " | - 'mesh_type': str, optional. Type of the mesh.\n", + " | \n", + " | For mesh with absorbing layer only:\n", + " | - 'abc_pad_length': float, optional. Length of the absorbing boundary condition padding.\n", + " | \n", + " | For Firedrake mesh only:\n", + " | - 'dx': float, optional. Mesh element size.\n", + " | - 'periodic': bool, optional. Whether the mesh is periodic.\n", + " | - 'edge_length': float, optional. Length of the mesh edges.\n", + " | \n", + " | For SeismicMesh only:\n", + " | - 'cells_per_wavelength': float, optional. Number of cells per wavelength.\n", + " | - 'source_frequency': float, optional. Frequency of the source.\n", + " | - 'minimum_velocity': float, optional. Minimum velocity.\n", + " | - 'velocity_model_file': str, optional. File containing the velocity model.\n", + " | - 'edge_length': float, optional. Length of the mesh edges.\n", + " | \n", + " | create_firedrake_2D_mesh(self)\n", + " | Creates a 2D mesh based on Firedrake meshing utilities.\n", + " | \n", + " | create_firedrake_3D_mesh(self)\n", + " | Creates a 3D mesh based on Firedrake meshing utilities.\n", + " | \n", + " | create_firedrake_mesh(self)\n", + " | Creates a mesh based on Firedrake meshing utilities.\n", + " | \n", + " | create_mesh(self)\n", + " | Creates the mesh.\n", + " | \n", + " | Returns\n", + " | -------\n", + " | mesh : Mesh\n", + " | Mesh\n", + " | \n", + " | create_seimicmesh_2d_mesh(self)\n", + " | Creates a 2D mesh based on SeismicMesh meshing utilities.\n", + " | \n", + " | create_seismicmesh_2D_mesh_homogeneous(self)\n", + " | Creates a 2D mesh based on SeismicMesh meshing utilities, with homogeneous velocity model.\n", + " | \n", + " | create_seismicmesh_2D_mesh_with_velocity_model(self)\n", + " | \n", + " | create_seismicmesh_mesh(self)\n", + " | Creates a mesh based on SeismicMesh meshing utilities.\n", + " | \n", + " | Returns\n", + " | -------\n", + " | mesh : Mesh\n", + " | Mesh\n", + " | \n", + " | make_periodic(self)\n", + " | Sets the mesh boundaries periodic.\n", + " | Only works for firedrake_mesh.\n", + " | \n", + " | set_mesh_size(self, length_z=None, length_x=None, length_y=None)\n", + " | Parameters\n", + " | ----------\n", + " | length_z : float, optional\n", + " | Length of the domain in the z direction. The default is None.\n", + " | length_x : float, optional\n", + " | Length of the domain in the x direction. The default is None.\n", + " | length_y : float, optional\n", + " | Length of the domain in the y direction. The default is None.\n", + " | \n", + " | Returns\n", + " | -------\n", + " | None\n", + " | \n", + " | set_meshing_parameters(self, dx=None, cell_type=None, mesh_type=None)\n", + " | Parameters\n", + " | ----------\n", + " | dx : float, optional\n", + " | Mesh size. The default is None.\n", + " | cell_type : str, optional\n", + " | Type of the cell. The default is None.\n", + " | mesh_type : str, optional\n", + " | Type of the mesh. The default is None.\n", + " | \n", + " | Returns\n", + " | -------\n", + " | None\n", + " | \n", + " | set_seismicmesh_parameters(self, cpw=None, velocity_model=None, edge_length=None, output_file_name=None)\n", + " | Parameters\n", + " | ----------\n", + " | cpw : float, optional\n", + " | Cells per wavelength parameter. The default is None.\n", + " | velocity_model : str, optional\n", + " | Velocity model. The default is None.\n", + " | edge_length : float, optional\n", + " | Edge length. The default is None.\n", + " | output_file_name : str, optional\n", + " | Output file name. The default is None.\n", + " | \n", + " | Returns\n", + " | -------\n", + " | None\n", + " | \n", + " | ----------------------------------------------------------------------\n", + " | Data descriptors defined here:\n", + " | \n", + " | __dict__\n", + " | dictionary for instance variables (if defined)\n", + " | \n", + " | __weakref__\n", + " | list of weak references to the object (if defined)\n", + "\n" + ] + } + ], + "source": [ + "help(spyro.meshing.AutomaticMesh)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As with everything in Python, help() provides information based on the documentation written inside the code. Let us highlight the notes on the init method:\n", + "\n", + "Notes\n", + "-----\n", + "The `mesh_parameters` dictionary should contain the following keys:\n", + " - 'dimension': int, optional. Dimension of the mesh. The default is 2.\n", + " - 'length_z': float, optional. Length of the mesh in the z-direction.\n", + " - 'length_x': float, optional. Length of the mesh in the x-direction.\n", + " - 'length_y': float, optional. Length of the mesh in the y-direction.\n", + " - 'cell_type': str, optional. Type of the mesh cells.\n", + " - 'mesh_type': str, optional. Type of the mesh.\n", + "\n", + "For mesh with absorbing layer only:\n", + " - 'abc_pad_length': float, optional. Length of the absorbing boundary condition padding.\n", + "\n", + "For Firedrake mesh only:\n", + " - 'dx': float, optional. Mesh element size.\n", + " - 'periodic': bool, optional. Whether the mesh is periodic.\n", + " - 'edge_length': float, optional. Length of the mesh edges.\n", + "\n", + " For SeismicMesh only:\n", + " - 'cells_per_wavelength': float, optional. Number of cells per wavelength.\n", + " - 'source_frequency': float, optional. Frequency of the source.\n", + " - 'minimum_velocity': float, optional. Minimum velocity.\n", + " - 'velocity_model_file': str, optional. File containing the velocity model.\n", + " - 'edge_length': float, optional. Length of the mesh edges." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Firedrake based meshes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can use the Firedrake-based meshes. These take the dictionary inputs and place them in the appropriate 2D or 3D wrappers of Firedrake functions, for 2D Firedrake provides a `RectangleMesh` object." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on cython_function_or_method in module firedrake.utility_meshes:\n", + "\n", + "RectangleMesh(nx, ny, Lx, Ly, quadrilateral=False, reorder=None, diagonal='left', distribution_parameters=None, comm=, name='firedrake_default', distribution_name=None, permutation_name=None)\n", + " Generate a rectangular mesh\n", + " \n", + " :arg nx: The number of cells in the x direction\n", + " :arg ny: The number of cells in the y direction\n", + " :arg Lx: The extent in the x direction\n", + " :arg Ly: The extent in the y direction\n", + " :kwarg quadrilateral: (optional), creates quadrilateral mesh, defaults to False\n", + " :kwarg reorder: (optional), should the mesh be reordered\n", + " :kwarg comm: Optional communicator to build the mesh on (defaults to\n", + " COMM_WORLD).\n", + " :kwarg diagonal: For triangular meshes, should the diagonal got\n", + " from bottom left to top right (``\"right\"``), or top left to\n", + " bottom right (``\"left\"``), or put in both diagonals (``\"crossed\"``).\n", + " :kwarg name: Optional name of the mesh.\n", + " :kwarg distribution_name: the name of parallel distribution used\n", + " when checkpointing; if `None`, the name is automatically\n", + " generated.\n", + " :kwarg permutation_name: the name of entity permutation (reordering) used\n", + " when checkpointing; if `None`, the name is automatically\n", + " generated.\n", + " \n", + " The boundary edges in this mesh are numbered as follows:\n", + " \n", + " * 1: plane x == 0\n", + " * 2: plane x == Lx\n", + " * 3: plane y == 0\n", + " * 4: plane y == Ly\n", + "\n" + ] + } + ], + "source": [ + "import firedrake as fire\n", + "help(fire.RectangleMesh)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Based on the above documentation, please create a 10-element by 20-element quadrilateral mesh with the first axis (representing Z) of length 1.5 km and the second axis (representing X) of length 3 km." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "nz = 10\n", + "nx = 20\n", + "length_z = 1.5\n", + "length_x = 3.0\n", + "\n", + "mesh = fire.RectangleMesh(nz, nx, length_z, length_x, quadrilateral=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us have a look at our mesh:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from firedrake import triplot\n", + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axes = plt.subplots()\n", + "triplot(mesh, axes=axes)\n", + "axes.set_aspect(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As shown above, see Z is still positive. We can alter the coordinates in mesh.coordinates.dat.data. Please try to do this below." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "mesh.coordinates.dat.data[:, 0] *= -1.0\n", + "\n", + "fig, axes = plt.subplots()\n", + "triplot(mesh, axes=axes)\n", + "axes.set_aspect(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our wrapper also adds a pad option and dislocates it appropriately (with zero starting in the domain of interest). Below, we show this wrapper (located in spyro.meshing.RectangleMesh) with the comm variable removed:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "def RectangleMesh(nx, ny, Lx, Ly, pad=None, quadrilateral=False):\n", + " \"\"\"Create a rectangle mesh based on the Firedrake mesh.\n", + " First axis is negative, second axis is positive. If there is a pad, both\n", + " axis are dislocated by the pad.\n", + "\n", + " Parameters\n", + " ----------\n", + " Lx : float\n", + " Length of the domain in the x direction.\n", + " Ly : float\n", + " Length of the domain in the y direction.\n", + " nx : int\n", + " Number of elements in the x direction.\n", + " ny : int\n", + " Number of elements in the y direction.\n", + " pad : float, optional\n", + " Padding to be added to the domain. The default is None.\n", + " comm : MPI communicator, optional\n", + " MPI communicator. The default is None.\n", + " quadrilateral : bool, optional\n", + " If True, the mesh is quadrilateral. The default is False.\n", + "\n", + " Returns\n", + " -------\n", + " mesh : Firedrake Mesh\n", + " Mesh\n", + " \"\"\"\n", + " if pad is not None:\n", + " Lx += pad\n", + " Ly += 2 * pad\n", + " else:\n", + " pad = 0\n", + " mesh = fire.RectangleMesh(nx, ny, Lx, Ly, quadrilateral=quadrilateral)\n", + " mesh.coordinates.dat.data[:, 0] *= -1.0\n", + " mesh.coordinates.dat.data[:, 1] -= pad\n", + "\n", + " return mesh\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us use it to alter the example below and add a 0.5km pad." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "nz = 10\n", + "nx = 20\n", + "length_z = 1.5\n", + "length_x = 3.0\n", + "pad = 0.5\n", + "quad = True\n", + "mesh = RectangleMesh(nz, nx, length_z, length_x, pad=0.5, quadrilateral=quad)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots()\n", + "triplot(mesh, axes=axes)\n", + "axes.invert_yaxis()\n", + "axes.set_xlabel(\"Z (km)\")\n", + "axes.set_ylabel(\"X (km)\", rotation=-90, labelpad=20)\n", + "plt.setp(axes.get_xticklabels(), rotation=-90, va=\"top\", ha=\"center\")\n", + "plt.setp(axes.get_yticklabels(), rotation=-90, va=\"center\", ha=\"left\")\n", + "axes.tick_params(axis='y', pad=20)\n", + "axes.set_aspect(\"equal\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We use similar wrappers for 3D hexahedral and tetrahedral meshes, shown below as:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def BoxMesh(nx, ny, nz, Lx, Ly, Lz, pad=None, quadrilateral=False):\n", + " if pad is not None:\n", + " Lx += pad\n", + " Ly += 2 * pad\n", + " Lz += 2 * pad\n", + " else:\n", + " pad = 0\n", + " if quadrilateral:\n", + " quad_mesh = fire.RectangleMesh(nx, ny, Lx, Ly, quadrilateral=quadrilateral)\n", + " quad_mesh.coordinates.dat.data[:, 0] *= -1.0\n", + " quad_mesh.coordinates.dat.data[:, 1] -= pad\n", + " layer_height = Lz / nz\n", + " mesh = fire.ExtrudedMesh(quad_mesh, nz, layer_height=layer_height)\n", + " else:\n", + " mesh = fire.BoxMesh(nx, ny, nz, Lx, Ly, Lz)\n", + " mesh.coordinates.dat.data[:, 0] *= -1.0\n", + "\n", + " return mesh" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You will notice that an extruded rectangle mesh is used for hexahedral elements. This is necessary for Firedrake to take advantage of sum factorization in spectral elements. The layer height can be adjusted and be different for every layer. The 2D mesh to be extruded can be replaced with an unstructured quadrilateral mesh." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## SeismicMesh" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Please contribute to SeismicMesh and collaborate with Dr. Keith Richards for any improvements in our meshing algorithm. SeismicMesh has extensive documentation and demos at https://seismicmesh.readthedocs.io/en/par3d/. The current 2D wrapper in use is located in spyro.meshing.AutomaticMesh.create_seismicmesh_2d_mesh_with_velocity_model, but we refer to the SeismicMesh repository for tutorials and demos. \n", + "\n", + "It is essential to understand the required mesh resolution for a given desired accuracy to apply higher-order mass-lumped methods with unstructured meshes effectively. Here, we will only focus on the cells-per-wavelength parameter. To better understand this parameter relative to acoustic waves with simplexes, please read the Spyro paper or ask at olender@usp.br. If you want to use different wave equations, calculating new parameters is necessary but straightforward with the cells_per_wavelength_calculator located inside the tools package. If you need any help using the previously mentioned tool, please contact the developer of this specific tool (Alexandre Olender) only after implementing and verifying your new wave equation.\n", + "\n", + "For acoustic waves, just follow the updated table below when calling SeismicMesh:\n", + "\n", + "| Element | CPW |\n", + "| ----------- | ----------- |\n", + "| mlt2tri | 7.20 |\n", + "| mlt3tri | 3.97 |\n", + "| mlt4tri | 2.67 |\n", + "| mlt5tri | 2.03 |\n", + "| mlt6tri | 1.50 |\n", + "| mlt2tet | 6.12 |\n", + "| mlt3tet | 3.72 |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us create meshes for mlt2tri and mlt6tri elements and compare them based on a cut Overthrust model. The dimensions are length_z = 2.8 km and length_x = 6.0 km. The Ricker source has a peak frequency of 5 Hz. A pad of abc_pad = 0.75 km was added. Please complete the code below:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "frequency = 5.0 # ANSWER\n", + "length_z = 2.8 # ANSWER\n", + "length_x = 6.0 # ANSWER\n", + "abc_pad = 0.75 # ANSWER\n", + "\n", + "cells_per_wavelength = 7.20 # ANSWER\n", + "\n", + "# SeismicMesh takes length parameters in meters, even though the velocity paramter can be in km/s\n", + "length_z *= 1000\n", + "length_x *= 1000\n", + "abc_pad *= 1000" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We also need to input the minimum element length. This occurs where velocity has the smallest value in our Overthurst model, representing the water layer, with 1.5 km/s." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "v_min = 1.5\n", + "lbda_min = v_min/frequency # ANSWER\n", + "h_min = lbda_min/cells_per_wavelength\n", + "\n", + "h_min *= 1000" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We need to build a box and domain with our corner coordinates. " + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "import SeismicMesh\n", + "\n", + "bbox = (-length_z, 0.0, 0.0, length_x)\n", + "domain = SeismicMesh.Rectangle(bbox)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Seismic mesh can now calculate the necessary element sizes across the domain using its `get_sizing_from_segy` method. If the file were a segy, the code below would run without errors." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "ename": "IndexError", + "evalue": "list index out of range", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[14], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m ef \u001b[38;5;241m=\u001b[39m \u001b[43mSeismicMesh\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget_sizing_function_from_segy\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 2\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mvelocity_models/cut_overthrust_binary.bin\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[43mbbox\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[43mhmin\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mh_min\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[43mwl\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mcells_per_wavelength\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 6\u001b[0m \u001b[43m \u001b[49m\u001b[43mfreq\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfrequency\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 7\u001b[0m \u001b[43m \u001b[49m\u001b[43mgrade\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m0.15\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 8\u001b[0m \u001b[43m \u001b[49m\u001b[43mdomain_pad\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mabc_pad\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9\u001b[0m \u001b[43m \u001b[49m\u001b[43mpad_style\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43medge\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 10\u001b[0m \u001b[43m \u001b[49m\u001b[43munits\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mkm/s\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[1;32m 11\u001b[0m \u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/firedrake/lib/python3.8/site-packages/SeismicMesh/sizing/mesh_size_function.py:132\u001b[0m, in \u001b[0;36mget_sizing_function_from_segy\u001b[0;34m(filename, bbox, comm, **kwargs)\u001b[0m\n\u001b[1;32m 129\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 130\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mDimension not supported\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m--> 132\u001b[0m vp, nz, nx, ny \u001b[38;5;241m=\u001b[39m \u001b[43m_read_velocity_model\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 133\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilename\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mfilename\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 134\u001b[0m \u001b[43m \u001b[49m\u001b[43mnz\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msz_opts\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mnz\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 135\u001b[0m \u001b[43m \u001b[49m\u001b[43mnx\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msz_opts\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mnx\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 136\u001b[0m \u001b[43m \u001b[49m\u001b[43mny\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msz_opts\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mny\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 137\u001b[0m \u001b[43m \u001b[49m\u001b[43mbyte_order\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msz_opts\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mbyte_order\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 138\u001b[0m \u001b[43m \u001b[49m\u001b[43maxes_order\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msz_opts\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43maxes_order\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 139\u001b[0m \u001b[43m \u001b[49m\u001b[43maxes_order_sort\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msz_opts\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43maxes_order_sort\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 140\u001b[0m \u001b[43m \u001b[49m\u001b[43mdtype\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43msz_opts\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mdtype\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 141\u001b[0m \u001b[43m \u001b[49m\u001b[43mdim\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mdim\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 142\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 144\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m sz_opts[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124munits\u001b[39m\u001b[38;5;124m\"\u001b[39m] \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mkm-s\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[1;32m 145\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mConverting from km-s to m-s...\u001b[39m\u001b[38;5;124m\"\u001b[39m, flush\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n", + "File \u001b[0;32m~/firedrake/lib/python3.8/site-packages/SeismicMesh/sizing/mesh_size_function.py:575\u001b[0m, in \u001b[0;36m_read_velocity_model\u001b[0;34m(filename, nz, nx, ny, byte_order, axes_order, axes_order_sort, dtype, dim)\u001b[0m\n\u001b[1;32m 573\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m _read_segy(filename)\n\u001b[1;32m 574\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m--> 575\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_read_bin\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 576\u001b[0m \u001b[43m \u001b[49m\u001b[43mfilename\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnz\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnx\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mny\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbyte_order\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxes_order\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxes_order_sort\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdtype\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdim\u001b[49m\n\u001b[1;32m 577\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/firedrake/lib/python3.8/site-packages/SeismicMesh/sizing/mesh_size_function.py:583\u001b[0m, in \u001b[0;36m_read_bin\u001b[0;34m(filename, nz, nx, ny, byte_order, axes_order, axes_order_sort, dtype, dim)\u001b[0m\n\u001b[1;32m 581\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Read a velocity model from a binary\"\"\"\u001b[39;00m\n\u001b[1;32m 582\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m dim \u001b[38;5;241m==\u001b[39m \u001b[38;5;241m2\u001b[39m:\n\u001b[0;32m--> 583\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_read_bin_2d\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfilename\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnz\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnx\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbyte_order\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxes_order\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43maxes_order_sort\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdtype\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 584\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m (nz \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;129;01mor\u001b[39;00m (nx \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;129;01mor\u001b[39;00m (ny \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[1;32m 585\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[1;32m 586\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mPlease specify the number of grid points in each dimension (e.g., `nz`, `nx`, `ny`)...\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 587\u001b[0m )\n", + "File \u001b[0;32m~/firedrake/lib/python3.8/site-packages/SeismicMesh/sizing/mesh_size_function.py:610\u001b[0m, in \u001b[0;36m_read_bin_2d\u001b[0;34m(filename, nz, nx, byte_order, axes_order, axes_order_sort, dtype)\u001b[0m\n\u001b[1;32m 608\u001b[0m axes \u001b[38;5;241m=\u001b[39m [nz, nx]\n\u001b[1;32m 609\u001b[0m ix \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39margsort(axes_order)\n\u001b[0;32m--> 610\u001b[0m axes \u001b[38;5;241m=\u001b[39m [axes[o] \u001b[38;5;28;01mfor\u001b[39;00m o \u001b[38;5;129;01min\u001b[39;00m ix]\n\u001b[1;32m 611\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(filename, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m file:\n\u001b[1;32m 612\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mReading binary file: \u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m+\u001b[39m filename)\n", + "File \u001b[0;32m~/firedrake/lib/python3.8/site-packages/SeismicMesh/sizing/mesh_size_function.py:610\u001b[0m, in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[1;32m 608\u001b[0m axes \u001b[38;5;241m=\u001b[39m [nz, nx]\n\u001b[1;32m 609\u001b[0m ix \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39margsort(axes_order)\n\u001b[0;32m--> 610\u001b[0m axes \u001b[38;5;241m=\u001b[39m [\u001b[43maxes\u001b[49m\u001b[43m[\u001b[49m\u001b[43mo\u001b[49m\u001b[43m]\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m o \u001b[38;5;129;01min\u001b[39;00m ix]\n\u001b[1;32m 611\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(filename, \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124m\"\u001b[39m) \u001b[38;5;28;01mas\u001b[39;00m file:\n\u001b[1;32m 612\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mReading binary file: \u001b[39m\u001b[38;5;124m\"\u001b[39m \u001b[38;5;241m+\u001b[39m filename)\n", + "\u001b[0;31mIndexError\u001b[0m: list index out of range" + ] + } + ], + "source": [ + "ef = SeismicMesh.get_sizing_function_from_segy(\n", + " \"velocity_models/cut_overthrust_binary.bin\",\n", + " bbox,\n", + " hmin=h_min,\n", + " wl=cells_per_wavelength,\n", + " freq=frequency,\n", + " grade=0.15,\n", + " domain_pad=abc_pad,\n", + " pad_style=\"edge\",\n", + " units='km/s',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Even though segy files are the norm in Seismic imaging, binary files are also common. This is a severe hindrance if you need to learn how the binary file is organized. However, most binaries used in seismic imaging to represent velocity files are organized similarly. To use these files in SeismicMesh, you have to pass the number of elements in each direction, axes order, byte order, and dtype. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reading binary file: velocity_models/cut_overthrust_binary.bin\n", + "Mesh sizes will be built to resolve an estimate of wavelength of a 5.0 hz wavelet with 7.2 vertices...\n", + "Enforcing minimum edge length of 41.666666666666664\n", + "Enforcing maximum edge length of 10000.0\n", + "Enforcing mesh size gradation of 0.15 decimal percent...\n", + "Including a 750.0 meter domain extension...\n", + "Using the pad_style: edge\n" + ] + } + ], + "source": [ + "nz, nx, ny = 140, 300, 0\n", + "\n", + "ef = SeismicMesh.get_sizing_function_from_segy(\n", + " \"velocity_models/cut_overthrust_binary.bin\",\n", + " bbox,\n", + " hmin=h_min,\n", + " wl=cells_per_wavelength,\n", + " freq=frequency,\n", + " grade=0.15,\n", + " domain_pad=abc_pad,\n", + " pad_style=\"edge\",\n", + " units='km/s',\n", + " nz=nz,\n", + " nx=nx,\n", + " ny=ny,\n", + " axes_order=(1, 0),\n", + " byte_order=\"little\",\n", + " dtype=\"int32\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now create our mesh" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "points, cells = SeismicMesh.generate_mesh(\n", + " domain=domain,\n", + " edge_length=ef,\n", + " verbose=0,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can save our mesh using meshio. If necessary, we can convert the coordinates back to km here. The vtk file is just for Paraview visualization." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Warning: Appending zeros to replace the missing physical tag data.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;33mWarning:\u001b[0m\u001b[33m Appending zeros to replace the missing physical tag data.\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Warning: Appending zeros to replace the missing geometrical tag data.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;33mWarning:\u001b[0m\u001b[33m Appending zeros to replace the missing geometrical tag data.\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Warning: VTK requires 3D points, but 2D points given. Appending 0 third component.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;33mWarning:\u001b[0m\u001b[33m VTK requires 3D points, but 2D points given. Appending \u001b[0m\u001b[1;33m0\u001b[0m\u001b[33m third component.\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import meshio\n", + "\n", + "meshio.write_points_cells(\n", + " \"meshing_tutorial_mesh.msh\",\n", + " points/1000.0,\n", + " [(\"triangle\", cells)],\n", + " file_format=\"gmsh22\",\n", + " binary=False\n", + ")\n", + "\n", + "meshio.write_points_cells(\n", + " \"meshing_tutorial_mesh.vtk\",\n", + " points/1000.0,\n", + " [(\"triangle\", cells)],\n", + " file_format=\"vtk\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us load our mesh into Firedrake so we can have a look at it" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "mesh = fire.Mesh(\n", + " 'meshing_tutorial_mesh.msh',\n", + " distribution_parameters={\n", + " \"overlap_type\": (fire.DistributedMeshOverlapType.NONE, 0)\n", + " },\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABE8AAALcCAYAAAAWgJ7AAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAACuZklEQVR4nOzdd5gV5f3//9fSFqR3UBBbIhYCAbuCxqAmJLbYokYTGyofREWjYo2a/JAYRMWoUZEYSywkRqKiIoqIisYEAQ12NlZEBXYpwrIwvz/4nuOWc+bMzJly3zPPx3XtpZw9e849d5/33HNPheM4jgAAAAAAAFBQs6QTAAAAAAAAYDKCJwAAAAAAAC4IngAAAAAAALggeAIAAAAAAOCC4AkAAAAAAIALgicAAAAAAAAuCJ4AAAAAAAC4IHgCAAAAAADgguAJAAAAAACAC4InAAAAFps3b56OP/54HXTQQUknBQCA1GqRdAIAAEDy6urqdP/99+uBBx7QunXr5DiOampqNHjwYF1wwQXaZZddkk4iCvjwww81d+5cPfTQQxo2bFjSyQEAILVYeQIAQMZ99tln2nffffWnP/1Jt912m1544QXNmTNHr7/+ugYNGqQhQ4Zo0qRJiaWvurpaEydOTOS7r7zyykS+16vttttOF154oXr06JF0UgAASDVWngAAkGE1NTUaPny4Vq5cqbfeekudO3fO/65FixYaM2aMVq5cqbFjx6pNmzY666yzYk/jtddeq3bt2sX+vY8//rhefPHF2L83iNatWyedBAAAUo2VJwAAZNi1116rxYsXa8yYMQ0CJ/WNHTtW7dq104UXXqilS5fGmr77778/kVUnb731lk466SQ5jhP7dwMAAPMQPAEAIKPq6up09913S5IOOeSQou9r166d9tlnH61Zs0YPPPCAXn31VXXq1EkVFRWqqKjQvvvum3/vrbfeqo4dO6pdu3Z68sknJUnffPONLr74Yo0YMULbbLONBg0apKefflqStHbtWj3wwAP66U9/qoMPPlj/+te/1L9/f/Xp00d/+tOf9Mc//lGS9Oc//1kHHHCAbrjhBs2bN09jxoxRr169tHjxYh133HFq166dbrnlFvXr108VFRU64IAD8mnab7/91KxZM1VUVKiqqir/+oMPPqihQ4dqr732UuvWrVVRUaHVq1dryZIluvjii/XNN9/ojTfe0AEHHKCTTz65YN7MmTNHo0aNUr9+/fTnP/9Z06ZN0+mnn67u3btrn3320Ycffqg33nhDo0eP1m677aa+fftq2rRpDT5j48aNuu666zRixAj1799fO+64o+6///4G7ymW1sYWLlyo888/X9tvv7123XVXLVy4sGi5AgAAHxwAAJBJixcvdiQ5kpyamhrX955zzjmOJOfYY491HMdxPvvsM6d79+5Ou3btnE2bNjV476mnnuo899xzjuM4zoYNG5z999/fuffeex3HcZx169Y5+++/v9OiRQvnzTffdJYtW+a88MILTmVlpbPTTjs5v/vd75y7777bOeigg5wlS5Y4S5YscSQ5V111Vf7z//WvfzmHHHKII8k5++yznWeeecY5/PDDnalTpzpr1651Onbs6Oy///4N0jRy5EhHkrNkyRLHcRzn7bffdnbYYQdnzZo1juM4zocffuj07NnTWbVqVf5v+vXr1+RzCrnnnnscSc6JJ57ofPDBB47jOM4bb7zhVFRUOEOGDHGmTZuWf++JJ57otG3b1qmurm7w2m9/+1vHcRxn06ZNzgknnOBIcp566ilfae3Tp48zffp0x3EcZ+3atU6/fv2cAw88sGT6AQBAaaw8AQAgo5YvX57//y222ML1vR06dJAkrVy5UpLUu3dvnXfeeVq9erXmzp2bf9+aNWv05Zdf6gc/+IGkzSsm1qxZo1/84heSpMrKSl144YWqq6vT+PHj1b17dw0bNkzdu3fXxo0bdckll+iUU07RM888o2222aZgWnbbbTftvvvukqQTTzxRBx10kP7xj3/oV7/6ldq0aaNOnTo1+ZvevXs3+PfChQtVXV2tNWvWSJK23XZbXXDBBaqoqHDNh0K22morSdLw4cO13XbbSZIGDhyoHj16qF27djrqqKPy7x06dKjWrFmjt99+W5I0d+5czZw5U5deeqkkqaKiQuPGjZO0+ZYqP2ndfvvtdeihh0qS2rRpoz322EOvv/667+MBAABNsWEsAAAZVX+Pk5qamqJ7nkibb72RpK5du+ZfO/vss/X//X//nyZOnKihQ4dKkqZOnapTTjkl/56nnnpKH330UYPbaNatW6d+/frlAzGS1Lx5c/Xu3VvNmnm7rtO8eXNJ3wYu/Bo6dKg2bdqkAQMG6KKLLtLIkSP161//OtBn5dLSWKFNXCsrKyV9m59PPfWU1q1blw82SZtvp+rXr1/+tpygaW3Tpk3BW3sAAIB/rDwBACCjvvOd76hjx46SpMWLF7u+93//+58kaZ999sm/1rlzZ5122mn65z//qXfffVeO4+jJJ5/U4Ycfnn/PF198oQEDBmj27Nn5n3nz5qmqqkqPP/54BEflTa9evfTvf/9bBxxwgC688EL17dtXN910U2zf7/y/jWi/+OILde/evUH+zJ07V1VVVXrjjTfKSmtFRYU2bdoU5WEAAJAZBE8AAMioFi1a5DdCdQtk1NbWas6cOerQoYOOP/74Br87//zzVVFRoUmTJumJJ57Qj3/84warRzp16qRXX31Vn3zySZPPXbBgQUhH0pDXW2/69eunBx98UPPnz9euu+6q8847L9YAirQ5fz788EPNnz+/ye/q548JaQUAIMsIngAAkGFXXnml+vXrp9tuu02fffZZwff88Y9/1JdffqmbbrqpwW07krTNNtvo6KOP1j333KPJkyc3uGVH2rwPyOrVq3XYYYc1CAY8/fTTTZ4o4xR4LHCpQEihv9liiy3yt8UUe//f/va3fHoGDhyoWbNmaeutt9asWbM8f3cYhg8fLsdxdNRRR2n27Nn5119//XXdfPPNntMKAACiRfAEAIAM69atm5555hn17NlTI0aM0DvvvJP/3YYNGzRx4kRdeuml+uMf/6hf/epXBT/j17/+tb755hsNHDhQ7dq1a/C7X/3qVxoyZIjmz5+vQYMGqUePHurZs6dOOukkjR07VtLmPVCqq6v1wQcfNAl6dOnSRRUVFfr0008lKb857dKlSyVJixYtapKe73//+1q8eLG++uqr/HueffZZSdLXX38taXMQ5fTTT9fnn38uafPqmk2bNunAAw/Mf07Xrl3z3/vKK68UvQXm448/bpAmafMjmKurq/XFF180+Lvce3L/PeSQQ/TTn/5US5Ys0Q9+8AN17txZW221lYYNG6bzzz/fU1q/+eYbff311/ryyy8bBJNyGwLn/g4AAJQhsef8AAAAY3zzzTfOjTfe6Oy7777Ovvvu6+y///7O97//fefMM8903nnnnZJ/f9BBBzkfffRRwd+tXLnSOeuss5xu3bo5lZWVzg9/+EPnzTffdBzHcV599VVnm222yT8yuU+fPs7s2bMb/P3VV1/tdOnSxTnjjDOc9957zzn44IOdZs2aOZKc1q1bOxdccEGD9//vf/9z9tprL6dv377OL3/5S+eRRx5xfvOb3zjbbrutc/nllzuLFi1yHnnkkfzf77777s7uu+/uXHfddQ0eu/zEE084vXr1cn7yk584zzzzTMFju+6665x27do5kpzKykrn5JNPdl5++WWnX79++WP67ne/68yfP9857rjjnJYtWzqSnLZt2zq///3vHcfZ/PjmcePGOVtuuaXTqlUrZ88993RefPHF/He4pXXhwoXO1ltvnf+u/v37OwsXLnR222031zwFAAD+VDhOgfWuAAAAAAAAkMRtOwAAAAAAAK5aJJ0AP5577jndfvvt6tGjh1q2bKnrr79eLVoUPoQVK1bonHPOUbdu3bR06VJdc801+u53vxtzigEAAAAAgO2suW1nwYIFOuSQQ/Tmm2+qW7duGjNmjJo3b65JkyY1ea/jOBo6dKhOPvlkjRw5Uv/5z3905JFH6s0331T79u0TSD0AAAAAALCVNcGTn/zkJ+rcubPuu+8+SdK8efO033776f3339c222zT4L2PPPKITjzxRFVXV6tNmzaSpP79++vEE0/UFVdcEXfSAQAAAACAxazY86SmpkbPPPOMBg8enH9t0KBBchxH06ZNa/L+adOmqX///vnAiSQNGTJEDz30UCzpBQAAAAAA6WFF8GT+/Pmqq6tT165d86+1bt1aHTp00Pz585u8/7XXXmvwXknq0aOHFi9erPXr10eeXgAAAAAAkB5WbBi7bNkySVKXLl0avN6+fXt99dVXBd9ff5VK7r2bNm3S8uXL1bt37yZ/s379+gaBldx7u3btqoqKijAOAwAAAAAAhMxxHK1atUpbbrmlmjWLZo2IFcGTXPCi/m04krRx40a1bNmy4PsLvVdSwfdL0vjx43X11VeHkVwAAAAAABCzjz/+WH369Inks60InuRWilRXVzd4ffXq1erevXvB9xd6b/PmzZusXskZN26cxo4dm/93dXW1tt56ax3Q9SS1aNaq3EMAAAAAAAARqNtUq9lf3xvp03WtCJ7stNNOatmyZf72HUlau3atampqtMceezR5/8CBA/Xxxx83eG3p0qUaMmRI0SU8lZWVqqysbPJ6i2atCJ4AAAAAAGC4KLfcsGLD2C5dumjEiBGaN29e/rVFixapVatWOvTQQ5u8/xe/+IUWLFjQYA+ThQsX6uijj44lvQAAAAAA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iooKVVRUqGvXrlq7dm3SyYJBnnnmGY0dO1a33Xab/vvf/yadHESMDWNhvAULFuiQQw7Rm2++qW7dumnMmDFq3ry5Jk2alHTSkLC6ujrde++9+sMf/qA1a9aoqqoq6STBACNGjNAee+yh73znO5o6dapmzZqlv/zlLzrppJOSThoSdv7552v48OHq3r27Lr74Ys2ZM0dvvvmmdtppp6STBgOcfvrpmjJlip5//nkdcMABSScHBrjjjju0ZMkS9ezZU5K044476sc//nHCqYIJHMfRhRdeqFWrVunWW29VixYtkk4SYkDwBMb7yU9+os6dO+u+++6TJM2bN0/77bef3n//fW2zzTbJJg5GuOSSS/Tggw8SPIHeeustLV68WEcffbSkzQG2XXfdVT169NCcOXMSTh2StGrVKi1dulTf+c53JElVVVXadttt9cgjj+TrC7LrxRdf1NSpUzV16lSCJ5Akbdq0SSeeeKL++te/Jp0UGOiyyy7TvHnz9Mwzz6h58+ZJJwcx4bYdGK2mpkbPPPOMBg8enH9t0KBBchxH06ZNSzBlMEnr1q2TTgIM0aVLFx111FH5f7do0UI//vGPtXz58gRTBRO0b98+HziRpK5du6pDhw6cJEO1tbW64YYbdPHFFyedFBjk0Ucf1WOPPaaDDjpIf//735NODgzy6quv6vrrr9fUqVMJnGQMwRMYbf78+aqrq1PXrl3zr7Vu3VodOnTQ/PnzE0wZABPl7k2vr66ujj1x0MQDDzyg++67T926dUs6KUjYhAkTdN5556mysjLppMAgn3zyiQ444AC99tprOuqoo3TSSSdp06ZNSScLBrj22mu1xx576M9//rOGDx+ufffdV88991zSyUIMCJ7AaMuWLZO0+Wpyfe3bt9dXX32VRJIAWOb555/X2LFjk04GDLFkyRJdeOGF+r//+z899NBDqq2tTTpJSNB7772npUuXav/99086KTDMueeeqyeffFKfffaZRo0apfvuu0833nhj0slCwtasWaOZM2dqyy231LnnnqsZM2Zoq6220ogRI7RkyZKkk4eIETyB0XJXkNu0adPg9Y0bN6ply5ZJJAmARR599FENHz5cO++8c9JJgSF69eql448/XkcccYTuv/9+XXvttUknCQm68sorqQNw1bZtW/3xj3/U8ccfr9tvvz3p5CBhH374oWpra3XcccepY8eOatmypX73u99p/fr1mjx5ctLJQ8QInsBovXv3lrT58ZL1rV69Wt27d08iSQAssWLFCt133336/e9/n3RSYJA2bdpoyJAhmjZtmvbbbz89/vjjSScJCbnnnnv0k5/8pMnqVqCQSy+9lI3podWrV0uS2rVrl3/tO9/5jvr27at33303qWQhJgRPYLSddtpJLVu2zN++I0lr165VTU2N9thjjwRTBsBkGzdu1EUXXaSbb75ZrVq1Sjo5MNRRRx1F/ciwP//5zzrppJNUUVGhiooKbbvttpKkH/zgB2wkjCa222479enTJ+lkIGG5OtB4I/qePXsSiM0AgicwWpcuXTRixAjNmzcv/9qiRYvUqlUrHXrooQmmDIDJxo0bp1GjRmmrrbbKv1Y/CAtI0rp16zR06NCkk4GE3HnnnZo/f37+54knnsi/ftdddyWcOpjm9ddf16mnnpp0MpCwvn376nvf+55eeeWVBq8vX75cP/zhDxNKFeJC8ATGu/zyyzVr1iytXbtW0uZltueeey7Rf+Rt2LBBdXV1SScDhrjsssu0adMmffHFF3rqqaf0+OOP66KLLtLrr7+edNKQoKVLl2rixIn68ssvJUmff/65pk+frnHjxiWcMiRlhx120KBBg/I/ub2RdthhB+2www4Jpw5JWrlypY488kg9++yzkqQPPvhAU6dO1UUXXZRwymCCCRMm6K9//Wv+4RWvv/662rRpoxNOOCHhlCFqFY7jOEknAijlH//4hx566CF169ZNHTt21DXXXKNmzYj9Qfrb3/6m3/zmN/rvf/+riRMn6oQTTlCPHj2SThYS8oc//EG//vWvm7zeuXNnffHFF2w0nWGLFi3S4YcfruXLl+vAAw/U1ltvrQsvvJBAPPKqqqq07bbb6vnnn+e2nYxbt26dDjvsMM2ZM0c777yzRowYoSuuuILHWSPv4Ycf1kMPPaRBgwbpo48+0lVXXcV4kgEETwAAAAAAAFxw6R4AAAAAAMAFwRMAAAAAAAAXBE8AAAAAAABcEDwBAAAAAABwQfAEAAAAAADABcETAAAAAAAAFwRPAAAAAAAAXBA8AQAAAAAAcEHwBAAAAAAAwAXBEwAAYJRnn31WRx55pCoqKlRRUaETTzxRS5YskSQtWbJEI0eOVEVFhX7xi1/ojTfecP2slStX6oYbblB1dbUmTpyo/v37a/bs2aGl9ZFHHtHLL78c2ucBAAAzETwBAABGGT58uB599FGNHDlSkrTXXntp2223lSRtu+22Gjx4sC655BLdd999GjRoUNHPWblypc477zydffbZatWqlXbaaSe98847oab1mGOO0cyZM/XMM8+E+rkAAMAsFY7jOEknAgAAoLFvvvlGgwcP1vLly/XOO++oU6dO+t///qfRo0frH//4h5o3b+7690cffbQuvPBC7bXXXpKkDz/8UNtvv72ef/55HXDAAaGlc/369Ro6dKj+/ve/q0+fPqF9LgAAMAcrTwAAgJHatGmjP/3pT1q2bJkuvfRS1dXVadSoUbrllltKBk5mz56txYsX5wMnktSsWTTTnsrKSh177LG6+uqrI/l8AACQPIInAADAWMOGDdMpp5yiP/3pTzrhhBN00kknqV+/fiX/bvLkyTrwwANd3/PQQw+pTZs2uuyyy/TGG2/o+eef11FHHaXTTz9dc+bM0aBBg9SpUyfdeuutWrVqlU466SS1bdtWBxxwgKqrqxt81gEHHKC//OUvWrFiRVnHCwAAzETwBAAAGO36669Xly5d9PLLL+uYY44p+f6NGzfqqaee0s4771z0PXV1dXrqqaf0zDPP6He/+50GDRqk7t27a968eXrrrbf0+eef69lnn9Vhhx2mCy64QDfeeKPGjRunefPm6fXXX9cdd9zR4PN23HFH1dbW6tlnny37eAEAgHkIngAAAKO1bNlSvXr10qeffqo//vGPJd///vvva+3aterVq1fB39fV1en000/XaaedpqFDh+Zf33XXXbXttttqxx131HHHHadu3brp+OOP17p163TSSSdp55131oABAzRgwAC9++67DT6zffv26tChQ8mn/wAAADsRPAEAAEYbM2aM7rrrLu2+++664oor9Pnnn7u+/+uvv5YktW3btuDvr7zySr322msaMmRIk9+1aNGiwb/btGnT5D2VlZXasGFDk9fbtm2rL774wjVtAADATgRPAACAse655x5997vf1Z577qnbbrtNq1ev1tixY13/JhfwqK2tLfj7M888U59//rnOO++8UNNaUVFRciNbAABgJ4InAADASO+9954efvhhXXLJJZKkIUOG6Oyzz9aDDz6omTNnFv27rbbaSpKabOqa069fP91xxx2644479Mgjj4SW3lWrVqlv376hfR4AADAH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RYVWr16tjz/+WB06dEg6OYhJTU2N+vbtS7lnDOWeTZR7NlHu2US5ZxPlnk2Uezblyr2iIrpVxQRPSujQoQONLoMo92yi3LOJcs8myj2bKPdsotyziXJH2NgwFgAAAAAAwAXBEwAAAAAAABcET4qorKzUVVddpcrKyqSTghhR7tlEuWcT5Z5NlHs2Ue7ZRLlnE+WeTXGUO0/bAQAAAAAAcMHKEwAAAAAAABcETwAAAAAAAFwQPAEAAAAAAHCRqeDJc889p2OPPVajR4/W+eefr7q6uqLvXbFihX7xi1/ovPPO089//nO9++67DX7/1ltvaejQoWrXrp122203/fvf/446+QgozHLPqaur0+23365x48Zp6tSp+uqrr6JKPgIKs9y//PJL/fKXv9To0aM1fPhw3XfffVEnHwH5KXdJmjNnjoYNG6bZs2c3+d1TTz2l733ve2rfvr1+8IMfaNGiRRGlGuUKs9wl6bHHHlNFRUX+56yzzoog1ShXmOX+3nvvaeTIkfrtb3+riy66SNdee602bdoUUcpRDj/lfv3112vrrbdW165dddJJJ+nrr79u8p4PP/xQv/zlL/Xb3/42ymSjTGGXe859992nbbbZJoIUIwxhlnv//v0bjO25H1/zOycj3njjDadnz57Ol19+6TiO45xzzjnOeeedV/C9mzZtcvbdd1/nT3/6k+M4jvPvf//b2XrrrZ2amhrHcRynpqbGOeWUU5xXXnnFefLJJ52+ffs6W221lbNx48Z4DgaehVnuOStWrHCGDx/uTJ06NdK0I7iwy32//fZzHnroIcdxHGfZsmVOt27dnH/+858RHwX88lPujuM4c+bMcU499VRHkvP88883+N3ixYud3Xbbzbn77rudm266yenWrZvTo0eP/GfDHGGWe87Pf/5zZ9KkSfmfqqqqKJKOMoRZ7rW1tc5OO+3kfPLJJ/nXRo4c6dxwww2RpB3B+Sn3KVOmOEcccYTzwAMPOBdffLHTokUL58ADD2zwnnfffde57rrrnIqKCueqq66KOvkIKOxyz/n666+dHj16OP369Ysq6ShDmOX+0ksvOT/84Q+dW2+91Zk6daozdepU57rrrvNd9pkJnowYMcI58cQT8/9+5ZVXnObNmztLlixp8t6HH37YadmypbN27dr8azvuuKNzzTXXOI7jOG+99ZbzzTff5H83depURxKTagOFWe6Os3mCdcABBzDAGi7Mcp85c6bTsmVLp66uLv/7c845x9lpp52iOwAE4qfccxYvXlzwZGrSpEnOypUr8/9+8cUXHUnOlClTwk42yhRmuTvO5rK+6aabIkgpwhRmuf/nP/9x2rVr16Cfv+2225yf/vSnYScbZfJT7r/5zW8a/Pvaa691JDkffPBBk/f27NmTuZ3Boir3UaNGOT//+c8JnhgqzHL/xz/+0WShwx133OGce+65vtKUidt2ampq9Mwzz2jw4MH51wYNGiTHcTRt2rQm7582bZr69++vNm3a5F8bMmSIHnroIUnSzjvvrNatW+d/17VrV+29997q1q1bhEcBv8Iud0m64YYbtHTpUl155ZXRJh6BhV3us2fPVqdOndS8efP87/fee28tXrxYS5YsifBI4Iffcs+p35fXd+yxx6pjx475f++3337q1KmTli9fHl6iUbawy12SJkyYoAkTJmjkyJF6//33Q00vwhF2uXft2lWrV6/W5MmT86+99NJLOuCAA0JLM8rnt9zPOOOMBv8+9NBDJalgP+7WJyBZUZX7nDlz1LNnT+24444RpBrlCrvcDz/8cDVr1jD08dhjj+mII47wla5MBE/mz5+vuro6de3aNf9a69at1aFDB82fP7/J+1977bUG75WkHj16aPHixVq/fn2T98+cOVP33ntv+AlHWcIu9w0bNmjChAkaNmyYzj//fO2+++46/PDDVVVVFfWhwIewy33FihVavny5Nm7cmP999+7dJUmffPJJREcBv/yWeylbbrllg387jqONGzdqr732KjutCE/Y5b5mzRp16tRJffr00ZQpUzRgwABNnz49zCQjBGGX+9Zbb61TTjlFY8eO1YQJEzRlyhS1bNlS559/fpjJRpn8lnvjfryurk7t2rXTrrvuGnlaEZ4oyr22tlY33nijLrnkkugSjrJE3d7XrFmjBQsWaOjQob7SlYngybJlyyRJXbp0afB6+/btC270uWzZsoLv3bRpU4Oo5bx583TYYYfpzjvv1D/+8Y/wE46yhF3us2fP1ooVK7TDDjvohhtu0BNPPKG3335bP/nJT0puUof4hF3u22+/vTZu3KiXXnop//u1a9dKkjp37hx28hGQ33L369VXX9Wuu+6q/fbbr+zPQnjCLve2bdvq3nvv1auvvqo33nhDO+ywg44//nh9/PHHoaQX4Yiivd9+++066qijdMkll+jOO+/UnXfe2eQqJZJVbrnPnDlTo0ePZpWJZaIo99///vc677zz1KpVq3ATi9BE3d6ffvppDR8+vMHKci8yMSpUVFRIUoNl+ZK0ceNGtWzZsuD7C71XUoP39+/fX6NGjdKOO+6oCy+8ULNmzQo76ShD2OX+9ttvS5JOPfVUNW/eXD169NCFF16o//73v3ryySejOAQEEHa5n3zyyerQoYOuvPJKrV27VtXV1XrwwQfVvHlzdmc3iN9y92vixIm66aabyv4chCvKch8wYIBmzpypyspKVpcaJopyX7t2rVq2bKlTTz1Vr776qo488kgujBimnHJfs2aNpk+fzkoDC4Vd7u+++66WLl2qYcOGhZ9YhCbq9v7YY4/pyCOP9J2uTARPevfuLUmqrq5u8Prq1avzy+8bv7/Qe5s3b94g+tWpUyf96Ec/0nPPPaf27dvr8ccfjyD1CCrscl+9erUkqV27dvnf//CHP5Skoo80RvzCLvdu3bpp9uzZatmypX7wgx9owoQJ+uyzzzRixIgGdQHJ8lvufjzyyCPaZ599tPvuu5f1OQhflOUuSb169dKpp57K7ZmGCbvcHcfRz372M51wwgmaMmWKxo8fr3/+85+6+uqrQ0kvwlFOuV9++eWaNGlSg72sYIewy/3KK6/UNddcE35CEaoo2/vGjRs1e/ZsHXTQQb7T1cL3X1hop512UsuWLfPLf6TNVxhqamq0xx57NHn/wIEDmyzRXbp0qYYMGVJwCWeXLl104IEHsvTLMGGXe58+fSRt3ngo16B79uwpqemSMiQnivb+/e9/XzNnzpQkff7557rxxhtZbWQYv+Xu1dtvv625c+ey6sRQUZV7fdttt10ot34hPGGX+5w5c/T6669rxIgRkqRLLrlEH330ke666y5de+21oaUb5Qla7n/96181cOBA7bnnnnEkEyELs9z/97//6aGHHmrwMIiciooKTZ06Vb/61a9CTT+CibK9z507V7vttluTVS1eZGLlSZcuXTRixAjNmzcv/9qiRYvUqlWr/E689f3iF7/QggULGmwOu3DhQh199NFFv2PdunW+N5xBtMIu94MOOkitWrXSK6+8kv/98uXL1bx5c3bkN0jU7X3s2LEaOXIkZW4Yv+Xuxeeff64//OEP+sMf/pB/raamRuvWrSs7vQhHFOXe2KJFi3TCCSeE8lkIR9jlXl1drZYtWza4QHb22Wdr06ZNoaQX4QhS7i+88II++OCDBifE9U/GYL4wy713796aP39+g58zzzwz//phhx0W9eHAoyjbe5Cn7OT5erCxxf71r385W221lbNmzRrHcRzn7LPPdn796187juM4M2fOdIYMGeIsXbrUcRzHqaurc4YMGeL87W9/cxzHcV599VVn2223dVavXu04juPMmzfPufPOO53169c7juM4s2fPdn7605/GfUjwIMxydxzHGTdunLP//vs7mzZtchzHca6//npn5MiRcR4SPAi73B3HcTZt2uRcccUVzujRo5s8Jx5m8FPuOe+++64jyZk5c2aD17/66itn6NChzoMPPujMmDHDmTFjhvPwww87P/vZzyh/w4RZ7v/4xz+c0047zfn8888dx3Gc+++/37n55ptjOAr4FWa519TUOL1793bmzJmTf+3BBx90LrzwwoiPAn75Kfd//etfzrBhw/J9+JNPPuncddddznnnndfkc7fcckvnsssui+9A4EtU5e44jnPVVVc5/fr1i+U44E9U5b7jjjs6y5cvD5SmTNy2I0m77babbrnlFp122mnq1q2bunTpkr/fbeXKlaqqqspfeW7evLmmT5+usWPHat68efr88881a9YstW3bVpL0zjvv6JJLLtE111yjoUOHqn///nrkkUcSOzYUF2a5S9Jvf/tbXXnllTr99NPVp08frV27VjfffHMix4biwiz3JUuWaO7cuXr77be11157hXY1G+HzU+7S5hVGt956q6TNT9po166d9tprL23YsEEHHXSQ5s+frxdffLHBd4wePZoncBgmrHKXNj9B69lnn9XDDz+sffbZR6NHj2bViaHCLPf27dtrxowZuuKKK/TMM8+oc+fOWr9+vX73u9/Ff2Bw5bXc33vvPR188MFasWKF5syZ0+Azpk2blv//pUuX6q9//auWLl2qxx57TEOGDAm0iSSiFXa5ww5RlPubb76pPn36BH5iZoXjOE6wwwEAAAAAAEg/Lp8BAAAAAAC4IHgCAAAAAADgguAJAAAAAACAC4InAAAAAAAALgieAAAAAAAAuCB4AgAAAAAA4ILgCQAAAAAAgAuCJwAAAAAAAC4IngAAAAAAALggeAIAQIocccQROu+885JOhm+vvfaaunTpok8++STwZ7z77rsaM2aMvve974WYMn8++ugjjRs3Tt27d8+/tm7dOm211Vb629/+Ful3z507Vw8//LAWLlyoM888UwMGDAjts2tqajRx4kRVV1eH9pkAANiE4AkAACkydOhQ7bbbbkknw7fevXvrRz/6kTp16hT4M1q3bq1PP/1UNTU14SXMp5YtW2rdunX66quv8q9VVlbqRz/6kbbbbrvIvnfGjBl6/fXXdeyxx2rrrbfWmjVrtGrVqtA+v0OHDjr99NM1cuTIUD8XAABbVDiO4ySdCAAAgDBcccUVuvfee1VVVZVYGu6++26ddtppimuK9emnn+roo4/WSy+9pGbNNl8Xu/LKK/WXv/wl9HyYOXOm7rrrLj300EOhfi4AAKZj5QkAAEiN5s2bJ52EfAAjLpdddpl+9rOfNfjeqNJw0EEH6a233tLLL78cyecDAGAqgicAAFikrq5OV1xxhc455xwdccQROu644/L7UMyZM0fHHHOMTj/9dEmb9wA599xzNWjQIL3wwgvq3bu3zj77bEnSsmXLdPbZZ2vMmDEaNmyYrrrqKknSs88+q1122UXbbLONJOmLL77QmDFjVFFRodmzZ2vNmjW69dZbtcsuu+j555/X1Vdfre7du2uXXXbRu+++q3nz5mngwIFq166dJkyY0CDt//73v/WrX/1K55xzjvbYY4/8HiArVqzQ9ddfr2233VZVVVWqra3V/fffr3322Ud33XWXTjjhBHXr1k3vvPOOJOmBBx7QqaeeqjPOOEP77befFi1a5Jpn06ZN09ixY3XIIYdozz331CuvvFLwfbNnz9YxxxyjM844Q5MmTVKHDh10//33a926dTrrrLN08cUX66ijjtLw4cMb7M2ydu1ajR49WqNHj9YJJ5ygBx98MP+7DRs26IEHHtDee++te+65R2vWrNGNN96o5s2b6ze/+Y0k6eWXX9b3v//9fJ5L0j/+8Q+de+65GjNmjDp06KBbbrmlYJqXL1+uBx54QAceeGDR49+4caN++tOfapdddtHtt9+u6urqwGUoSQcccIAmTZrkluUAAKQOwRMAACwyatQotWzZUpMnT9b999+vxx9/XFdccYUkqWvXrnr11VdVV1cnSWrRooWqqqr06aefasGCBbrgggu0yy67aP369Ro+fLiOP/543Xzzzbr66qt1zTXX6O9//7uGDx+uo446Kv99PXv2bLABbW1trbbYYgv997//1d/+9jf95Cc/0SuvvKKvv/5ap512mubPn6+ZM2fq4osv1mWXXabPP/9ckvTOO+/oqKOO0qRJkzR58mT9+Mc/1nHHHadly5Zpw4YN2rhxY/4Wk9raWnXt2lWvvPKKHnnkEf3yl7/UiBEj1LFjR91///26++67NWXKFN15551q3ry5TjzxxKL59cILL+izzz7TDTfcoKeffloDBgzQoYceWnDj006dOmnevHl69dVX1adPH5188snq06ePJkyYoIULF2rChAl65JFH9Pbbb+vmm2/O/90vfvELbb311rrlllt0//33N/jMuro69ejRQ/PmzZPjOGrbtq3OO+88bbXVVvn37LPPPjr00EMb5PG5556rm266STfffLPuvffeosf39NNPq66uTv379y/6nvfee09t2rTRvHnzdNZZZ2nTpk2ByjBnxx131MyZM/P1DACALGiRdAIAAIA3S5Ys0V133aWPPvpIktS2bVvdc8896tOnjyRpl112Ub9+/fLv32677TRw4EC9/PLLGjVqlFq02Dzs33PPPVq7dq2GDRsmSRo2bJhuuukm7bnnnpKa3vJR/9+dO3fWfvvtJ0k6+uij85vT7r///vriiy/yK1uOPfZYXXnllfrwww/Vu3dvXXfddRo+fLg6d+4saXMQqFOnTurSpYtatGih3XffPf8d7dq108EHHyxJGjFihA455BAdcsghkqTLL79c1157rSoqKiRJEydO1Pvvv180z6655hoNGjRI1113nSSpTZs2GjRokD766KMmT6MZNGiQtt12W3Xt2lXHHHOMjjnmGEnSypUr1atXr/z7evXqld8Q9oUXXtCjjz6qv/zlL5KkiooKHXHEEXr66afz3/eDH/ygSbrc8nj16tX66KOPNHHiRJ1//vk69NBD9dJLLxU8voULF6p9+/Zq27Ztwd8vXrxYV155paZOnap27dpJCl6GOX369FF1dbWWLFmi73znOwW/FwCAtCF4AgCAJV5//XU5jqOuXbvmXzv66KMbvKfxnh/NmjVT27Zt84ETafNjget/RvPmzTVmzBjP6aj/WTlt2rRp8O/KykpJm29byX3nT3/60/zve/bsqfPPP9813ZLUsWPH/GtffvmlqqqqGqR9t912c3260KJFi/S73/1Oe+21V8njyn1v/e+UpMMPP1yrV6/W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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spyro.plots.plot_mesh_sizes(\n", + " firedrake_mesh=mesh,\n", + " title_str=\"Overtrust mesh\",\n", + " show=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above mesh has elements varying from 30 meters to 270 meters. Total DoFs can give us an idea of computational storage and runtime costs. Since we are using ML2Tri, we must look at the nodes inside each element, not just the vertices. For the above problem, the total DoFs are:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total Dofs for ml2tri function space:36709\n" + ] + } + ], + "source": [ + "element = fire.FiniteElement(\"KMV\", mesh.ufl_cell(), degree=2, variant=\"KMV\")\n", + "space = fire.FunctionSpace(mesh, element)\n", + "print(f\"Total Dofs for ml2tri function space:{space.dim()}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For comparison, let us create the same mesh and function space using ml4tri elements" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reading binary file: velocity_models/cut_overthrust_binary.bin\n", + "Mesh sizes will be built to resolve an estimate of wavelength of a 5.0 hz wavelet with 2.67 vertices...\n", + "Enforcing minimum edge length of 112.35955056179775\n", + "Enforcing maximum edge length of 10000.0\n", + "Enforcing mesh size gradation of 0.15 decimal percent...\n", + "Including a 750.0 meter domain extension...\n", + "Using the pad_style: edge\n" + ] + }, + { + "data": { + "text/html": [ + "
Warning: Appending zeros to replace the missing physical tag data.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;33mWarning:\u001b[0m\u001b[33m Appending zeros to replace the missing physical tag data.\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Warning: Appending zeros to replace the missing geometrical tag data.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;33mWarning:\u001b[0m\u001b[33m Appending zeros to replace the missing geometrical tag data.\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Warning: VTK requires 3D points, but 2D points given. Appending 0 third component.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;33mWarning:\u001b[0m\u001b[33m VTK requires 3D points, but 2D points given. Appending \u001b[0m\u001b[1;33m0\u001b[0m\u001b[33m third component.\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "frequency = 5.0\n", + "length_z = 2.8\n", + "length_x = 6.0\n", + "abc_pad = 0.75\n", + "\n", + "cells_per_wavelength = 2.67 # ANSWER\n", + "\n", + "# SeismicMesh takes length parameters in meters, even though the velocity paramter can be in km/s\n", + "length_z *= 1000\n", + "length_x *= 1000\n", + "abc_pad *= 1000\n", + "\n", + "v_min = 1.5\n", + "lbda_min = v_min/frequency\n", + "h_min = lbda_min/cells_per_wavelength\n", + "\n", + "h_min *= 1000\n", + "\n", + "bbox = (-length_z, 0.0, 0.0, length_x)\n", + "domain = SeismicMesh.Rectangle(bbox)\n", + "\n", + "nz, nx, ny = 140, 300, 0\n", + "\n", + "ef = SeismicMesh.get_sizing_function_from_segy(\n", + " \"velocity_models/cut_overthrust_binary.bin\",\n", + " bbox,\n", + " hmin=h_min,\n", + " wl=cells_per_wavelength,\n", + " freq=frequency,\n", + " grade=0.15,\n", + " domain_pad=abc_pad,\n", + " pad_style=\"edge\",\n", + " units='km/s',\n", + " nz=nz,\n", + " nx=nx,\n", + " ny=ny,\n", + " axes_order=(1, 0),\n", + " byte_order=\"little\",\n", + " dtype=\"int32\",\n", + ")\n", + "\n", + "points, cells = SeismicMesh.generate_mesh(\n", + " domain=domain,\n", + " edge_length=ef,\n", + " verbose=0,\n", + ")\n", + "\n", + "meshio.write_points_cells(\n", + " \"new_meshing_tutorial_mesh.msh\",\n", + " points/1000.0,\n", + " [(\"triangle\", cells)],\n", + " file_format=\"gmsh22\",\n", + " binary=False\n", + ")\n", + "\n", + "meshio.write_points_cells(\n", + " \"new_meshing_tutorial_mesh.vtk\",\n", + " points/1000.0,\n", + " [(\"triangle\", cells)],\n", + " file_format=\"vtk\"\n", + ")\n", + "\n", + "new_mesh = fire.Mesh(\n", + " 'new_meshing_tutorial_mesh.msh',\n", + " distribution_parameters={\n", + " \"overlap_type\": (fire.DistributedMeshOverlapType.NONE, 0)\n", + " },\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us have a look at the new mesh" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABE8AAALcCAYAAAAWgJ7AAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAACyTklEQVR4nOzdd5hU1eH/8c8Cy4L0DkrTEAuiIGAXJAZLsERjxRZ7C6ASo6Kiokn4GoOEYGLF3sUYsaAiikgMGA0CGruuNFeUthTZZeH+/uA345aZ2Sm3nHPu+/U8PLqzszPn3nva/cy5d4o8z/MEAAAAAACAlBpEXQAAAAAAAACTEZ4AAAAAAABkQHgCAAAAAACQAeEJAAAAAABABoQnAAAAAAAAGRCeAAAAAAAAZEB4AgAAAAAAkAHhCQAAAAAAQAaEJwAAAAAAABkQngAAABjoqquuUqdOnbRs2bKoi5LWli1bNHXqVA0aNEjjxo2LujgAAASG8AQAAENUVVXpwQcf1OGHH66DDz5YgwcPVr9+/XTOOefoww8/jLp4xrrvvvvUtWtXFRUVqaioSDfccEO9fzN58uTk83v16qUHHnhAF198sTp27Jh8vEmTJtp77721ePHirMuycuVKde/eXTfeeGMBW7RNy5Yt1bp1azVq1Kjg1wrK22+/rddee01z5syR53lRFwcAgMAQngAAYIDly5frwAMP1F133aU77rhDb775pmbPnq13331X/fr104ABAzRx4sTIyrd27VpNmDAhkve+/vrrM/7+nHPO0Zdffqm99tpL0rZgZN26dWmfv3nzZv35z3+WJO2///767LPPdNZZZ+mOO+7QJ598osaNG0uSFi5cqP/85z/q3r17VuX0PE9nnnmmlixZkvL3r7/+umbNmpXVa0nStddeq08++USdOnXK+m/CNmjQIF122WVRFwMAgMARngAAELHy8nINHTpUS5Ys0Ysvvqiddtop+btGjRpp1KhRuuaaazR69GjdeeedkZTx5ptvzhhIBOWFF17QW2+9Ve/zGjdurD59+mj77bfX6tWrdfvtt6d97iOPPKKqqipJUq9evVRUVJT8XZs2bdSxY0c1b95cO++8c05lHT9+vHr37p3yd1VVVbr22mtzej1bNGnSJOoiAAAQOMITAAAidvPNN+ujjz7SqFGj1KZNm5TPGT16tJo3b64rrrhCZWVloZbv0UcfjWTVyYcffqgzzjgj68tBGjRooNGjR0uSJk6cqI0bN9Z5ztatW3XLLbfoiiuuSP5NbQ0bNlTDhg1zKuusWbP04Ycf6je/+U2d33mepxEjRmju3Lk5vSYAADAH4QkAABGqqqrSfffdJ0k6/PDD0z6vefPmOuCAA7RhwwY99thjmjdvnlq3bp28P8eBBx6YfO7f//53tWrVSs2bN9dLL70kSfrhhx901VVXadiwYerZs6f69eunV155RZK0ceNGPfbYYzrqqKN02GGH6T//+Y923XVXde3aVXfddZf+9re/SZIeeOABDRkyRLfddpvmzp2rUaNGqXPnzvroo4908sknq3nz5rr99tvVo0cPFRUVaciQIckyHXTQQWrQoIGKiopUWlqafPyJJ57QoEGDtN9++6lJkyYqKirS+vXr9dVXX+mqq67SDz/8oPfff19DhgzRmWeeWe/+HDBggI444gh99913uuuuu+r8/plnnlHbtm31s5/9rN7XylZZWZluvPHGtKuCbr311uTlOpdddpmGDBmiV155Rc8995xOOeUU7b777vriiy+09957q127dpo7d67ee+89XXLJJWrdunXG93788cf1q1/9Sm3atNGnn36qyZMn67jjjlPLli11+umn64cfftCzzz6rc845RzvttJP22msv/e9//6vxGitXrtQll1yiI444Qttvv70OOuggvfvuu8nfe56nMWPGaPDgwdpzzz1VVFSkgQMHpizP888/r3POOUcdOnTQEUccofLy8ux3JAAAJvMAAEBkPvroI0+SJ8krLy/P+NyRI0d6kryTTjrJ8zzPW758udehQwevefPm3tatW2s895xzzvFef/11z/M8b/Pmzd7BBx/sPfzww57ned6mTZu8gw8+2GvUqJH3wQcfeCtWrPDefPNNr6SkxNttt928P/zhD959993nHXrood5XX33lffXVV54k74Ybbki+/n/+8x/v8MMP9yR5F198sffqq696v/zlL73777/f27hxo9eqVSvv4IMPrlGmCy64wJPkffXVV57ned7HH3/s9erVy9uwYYPneZ735Zdfep06dfLWrVuX/JsePXrUeZ10fv3rX3tvvPGGN2fOHE+St/3223ubNm2q8Zy99trLe/7557358+d7krxf//rXdV6nR48eXqtWrbJ6z6qqKu/II4/0Fi1a5Hmel3JfeZ7n3X///Z4k74033vA8z/PKy8u9uXPnep07d/Y6derkXX/99d7TTz/tDR061Hv33Xe91157zevdu7eXzVTt7LPP9iR5f/zjH5N1aNKkSZ4k7/jjj/fee+89z/M8b+PGjV6vXr28gw46KPm35eXl3h577JGsK6tWrfJ22203r3Xr1t63337reZ7n3Xvvvd7JJ5+c/Jvnn3/e22effZI/J7b5wAMPTO6HDz/80JPk3XTTTVntRwAATMfKEwAAIrRq1ark/2+33XYZn9uyZUtJ0po1ayRJXbp00WWXXab169drzpw5yedt2LBB3333XXJ1xRNPPKENGzbo9NNPlySVlJToiiuuUFVVlcaPH68OHTpo8ODB6tChg7Zs2aKrr75aZ599tl599VX17NkzZVkGDhyovffeW5J02mmn6dBDD9U///lPnXXWWWratGnKFRNdunSp8fPChQu1du1abdiwQZK044476re//W2Ne5Dk48ADD9TBBx+s5cuXJ1f1SNL06dO1detWHXXUUQW9fnU33HCDTjzxRPXp0yenv2vRooX23Xdf9erVSxUVFbryyit1wgknaMaMGRowYIB+/vOfq1+/flm91g477CBJGj58uFq0aCFJOuywwyRJffr0Uf/+/SVJTZs21cCBA/Xee+8l/3bSpEnaZZddknWlTZs2uuSSS7RmzRr99a9/lST997//1ffff68tW7ZIko466ij94he/qFOOoUOHJvdD79691aFDhxorWAAAsBnhCQAAEap+j5P6LnH44YcfJEnt2rVLPnbxxRerWbNmNe5Jcv/99+vss89O/vzyyy9r8eLFGjJkSPLf73//e/Xo0SMZxEjb7vXRpUuXlPcBSSVxX5DEyXuuBg0apK1bt2qPPfbQbbfdpvXr1+t3v/udmjVrltfrVZe4Oestt9ySvDnsH//4R11zzTUFv3bCyy+/rBUrVujXv/513q/RsGFDtWrVKuU2FxcXZ/0ataW7iWtJSUmyHknbtuGdd96pUTceeOAB9ejRQ99//70k6eijj9asWbM0YMAATZ06VVu3bs3qq5ibNm2q9evXZ7UNAACYjvAEAIAI/fSnP1WrVq0kSR999FHG53799deSpAMOOCD5WJs2bXTuuefq+eef16effirP8/TSSy/pl7/8ZfI53377rfbYYw/NmjUr+W/u3LkqLS3VCy+8EMBWZadz58567733NGTIEF1xxRXq1q2bJk2a5MtrH3roodp777319ddf6+GHH9Zbb72lFStW6IQTTvDl9SXpT3/6k+677z41atQo+a9Xr16SpJtuukmNGjXSm2++6dv7BeHbb7/VoYceWqNuvPvuuyotLU3ew+WII47QG2+8oZKSEp144onabbfdNGPGjHpfu6ioKLlaBQAA2xGeAAAQoUaNGiVvhJopyKisrNTs2bPVsmVLDR8+vMbvLr/8chUVFWnixIl68cUX9Ytf/KLG6pHWrVtr3rx5Wrp0aZ3XXbBggU9bUlO2l9706NFDTzzxhObPn68+ffrosssu8y1ASaw+GT9+vH7/+9/r6quvznpVTX0+/fRTTZkyRe+//36Nf4kb9F500UV6//33095Y1RStW7fWq6++mvJrqKvXjUGDBmnevHn65z//qR9++EG/+MUvalz+AwCA6whPAACI2PXXX68ePXrojjvu0PLly1M+529/+5u+++47TZo0qcZlO5LUs2dPnXDCCXrwwQc1efLkGpfsSNvuRbF+/Xodc8wxNU6IX3nlFT366KM1nuul+Frg+oKQVH+z3Xbb1bg8JNXzn3nmmWR5+vbtq5kzZ6p79+6aOXNm1u9d3datW7V169bkz8ccc4z69Omjzz77TB9//HHyni+J51b/by5mzpyp5cuXa8cdd1SfPn1q/Nt5550lSR07dlSfPn2Sl+Nk2o5U+y8sQ4cO1ZIlS3Tcccfpyy+/TJbnkUce0WuvvSZp21dpb968WZL0y1/+UjNmzNCWLVuMX1UDAICfCE8AAIhY+/bt9eqrr6pTp04aNmyYPvnkk+TvNm/erAkTJuiaa67R3/72N5111lkpX+N3v/udfvjhB/Xt21fNmzev8buzzjpLAwYM0Pz589WvXz917NhRnTp10hlnnKHRo0dLkjZt2qS1a9fqiy++qBN6tG3bVkVFRVq2bJkkJW9OW1ZWJklatGhRnfLstdde+uijj5L3zVi0aFHyZHzlypWStp2kn3feefrmm28kbVtds3XrVh1yyCHJ12nXrl3yff/973+nDTs8z9MXX3yhL774IvlYUVFR8h4nV155ZY17iHz22WeSVOP5CevXr9eGDRtShj/z58/X7373Ow0ePDhlOdJJBF7Lli1TZWWl/vOf/8jzPK1YsULffvutvvvuuzp/kwjSEtufzpIlSyT9eDyq/21i3yYknpP47xVXXKEePXpo5syZ+slPfqLtt99e7dq10+9//3tddNFFkrZd2nPxxRdr06ZNkrbde6dhw4bJfZBY0bRixYrk+1RWVmr9+vUqKyuLNBwCAMA3UX3NDwAAqOmHH37w/vKXv3gHHnigd+CBB3oHH3ywt9dee3kXXnih98knn9T794ceeqi3ePHilL9bs2aNd9FFF3nt27f3SkpKvJ///OfeBx984Hme582bN8/r2bNn8iuTu3bt6s2aNavG348bN85r27atd/7553ufffaZd9hhh3kNGjTwJHlNmjTxfvvb39Z4/tdff+3tt99+Xrdu3bxf//rX3tNPP+3deOON3o477uhdd9113qJFi7ynn346+fd77723t/fee3v/93//V+Nrl1988UWvc+fO3pFHHum9+uqrKbft/vvv93r16uVJ8ho3buztt99+3jfffON53ravEj7ooIO8H374wfM8z1u3bp23//77e02aNElu78477+zdf//93ttvv+2NGTMm+XjPnj29wYMHewcffLB3wAEHeDvvvLNXVFTkXXzxxWmPQbqvKt60aZN37LHHejvssIN3zTXXeKWlpd4uu+ySfK8OHTp4jz/+ePL5Q4YMSf6uc+fO3lNPPZXy/X71q195RUVFniSvffv23l//+lfv9ttv99q0aeNJ8oqKiryBAwd6K1eu9Pbcc8/ka3bp0sV77bXXPM/zvKVLl3rDhw/3WrVq5TVr1sw77rjjvCVLliTf4ze/+Y0nyWvbtq134IEHegcccIA3bdo0z/M874EHHvBat27tSfIaNGjg/exnP/Pefvtt76c//WnyvXbffXdvzZo1afcZAAA2KPI8Pg4AAAAAAABIh8t2AAAAAAAAMmgUdQFy8frrr+vOO+9Ux44dVVxcrFtvvVWNGqXehNWrV2vkyJFq3769ysrKdNNNNyVv4gYAAAAAAJAtay7bWbBggQ4//HB98MEHat++vUaNGqWGDRtq4sSJdZ7reZ4GDRqkM888UxdccIH++9//6rjjjtMHH3ygFi1aRFB6AAAAAABgK2vCkyOPPFJt2rTRI488IkmaO3euDjroIH3++efq2bNnjec+/fTTOu2007R27Vo1bdpUkrTrrrvqtNNO09ixY8MuOgAAAAAAsJgV9zwpLy/Xq6++qv79+ycf69evnzzP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v6iLg4Bxw1gYb8GCBTr88MP1wQcfqH379ho1apQaNmyoiRMnRl00RKyqqkoPP/yw/vznP2vDhg0qLS2NukgwwLBhw7TPPvvopz/9qe6//37NnDlTDz30kM4444yoi4aIXX755Ro6dKg6dOigq666SrNnz9YHH3yg3XbbLeqiwQDnnXeepkyZojfeeENDhgyJujgwwN13362vvvpKnTp1kiTtsssu+sUvfhFxqWACz/N0xRVXaN26dfr73/+uRo0aRV0khIDwBMY78sgj1aZNGz3yyCOSpLlz5+qggw7S559/rp49e0ZbOBjh6quv1hNPPEF4An344Yf66KOPdMIJJ0jaFrD16dNHHTt21OzZsyMuHaK0bt06lZWV6ac//akkqbS0VDvuuKOefvrpZH1BfL311lu6//77df/99xOeQJK0detWnXbaaXr88cejLgoMdO2112ru3Ll69dVX1bBhw6iLg5Bw2Q6MVl5erldffVX9+/dPPtavXz95nqepU6dGWDKYpEmTJlEXAYZo27atjj/++OTPjRo10i9+8QutWrUqwlLBBC1atEgGJ5LUrl07tWzZkpNkqLKyUrfddpuuuuqqqIsCgzz77LN67rnndOihh+of//hH1MWBQebNm6dbb71V999/P8FJzBCewGjz589XVVWV2rVrl3ysSZMmatmypebPnx9hyQCYKHFtenVVVVXcEwd1PPbYY3rkkUfUvn37qIuCiN1yyy267LLLVFJSEnVRYJClS5dqyJAheuedd3T88cfrjDPO0NatW6MuFgxw8803a5999tEDDzygoUOH6sADD9Trr78edbEQAsITGG3FihWStn2aXF2LFi30/fffR1EkAJZ54403NHr06KiLAUN89dVXuuKKK/Sb3/xGTz75pCorK6MuEiL02WefqaysTAcffHDURYFhLr30Ur300ktavny5LrnkEj3yyCP6y1/+EnWxELENGzZoxowZ2n777XXppZdq+vTp2mGHHTRs2DB99dVXURcPASM8gdESnyA3bdq0xuNbtmxRcXFxFEUCYJFnn31WQ4cOVe/evaMuCgzRuXNnDR8+XMcee6weffRR3XzzzVEXCRG6/vrrqQPIqFmzZvrb3/6m4cOH684774y6OIjYl19+qcrKSp188slq1aqViouL9Yc//EEVFRWaPHly1MVDwAhPYLQuXbpI2vb1ktWtX79eHTp0iKJIACyxevVqPfLII/rTn/4UdVFgkKZNm2rAgAGaOnWqDjroIL3wwgtRFwkRefDBB3XkkUfWWd0KpHLNNddwY3po/fr1kqTmzZsnH/vpT3+qbt266dNPP42qWAgJ4QmMtttuu6m4uDh5+Y4kbdy4UeXl5dpnn30iLBkAk23ZskVXXnml/vrXv6px48ZRFweGOv7446kfMfbAAw/ojDPOUFFRkYqKirTjjjtKkn72s59xI2HUsdNOO6lr165RFwMRS9SB2jei79SpE0FsDBCewGht27bVsGHDNHfu3ORjixYtUuPGjXX00UdHWDIAJhszZowuueQS7bDDDsnHqoewgCRt2rRJgwYNiroYiMg999yj+fPnJ/+9+OKLycfvvffeiEsH07z77rs655xzoi4GItatWzftueee+ve//13j8VWrVunnP/95RKVCWAhPYLzrrrtOM2fO1MaNGyVtW2Z76aWXkv4jafPmzaqqqoq6GDDEtddeq61bt+rbb7/Vyy+/rBdeeEFXXnml3n333aiLhgiVlZVpwoQJ+u677yRJ33zzjaZNm6YxY8ZEXDJEpVevXurXr1/yX+LeSL169VKvXr0iLh2itGbNGh133HF67bXXJElffPGF7r//fl155ZURlwwmuOWWW/T4448nv7zi3XffVdOmTXXqqadGXDIErcjzPC/qQgD1+ec//6knn3xS7du3V6tWrXTTTTepQQOyP0jPPPOMbrzxRv3vf//ThAkTdOqpp6pjx45RFwsR+fOf/6zf/e53dR5v06aNvv32W240HWOLFi3SL3/5S61atUqHHHKIunfvriuuuIIgHkmlpaXacccd9cYbb3DZTsxt2rRJxxxzjGbPnq3evXtr2LBhGjt2LF9njaSnnnpKTz75pPr166fFixfrhhtuYDyJAcITAAAAAACADPjoHgAAAAAAIAPCEwAAAAAAgAwITwAAAAAAADIgPAEAAAAAAMiA8AQAAAAAACADwhMAAAAAAIAMCE8AAAAAAAAyIDwBAAAAAADIgPAEAAAAAAAgA8ITAABglNdee03HHXecioqKVFRUpNNOO01fffWVJOmrr77SBRdcoKKiIp1++ul6//33M77WmjVrdNttt2nt2rWaMGGCdt11V82aNcu3sj799NN6++23fXs9AABgJsITAABglKFDh+rZZ5/VBRdc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PZ447vGUOO5FRcGtKiY8qUfLli1pdDHEcY8njns8cdzjieMeTxz3eOK4xxPHHX7jhrEAAAAAAAAZEJ4AAAAAAABkQHiSRklJiW644QaVlJREXRSEiOMeTxz3eOK4xxPHPZ447vHEcY8njns8hXHc+bYdAAAAAACADFh5AgAAAAAAkAHhCQAAAAAAQAaEJwAAAAAAABnEKjx5/fXXddJJJ2nEiBG6/PLLVVVVlfa5q1ev1umnn67LLrtMp5xyij799NMavy8rK9PFF1+sm2++WWPHjtUVV1yhH374IehNQB5yOe6SNHv2bA0ePFizZs2q87uXX35Ze+65p1q0aKGf/exnWrRoUUClRqH8PO4JGzdu1J/+9CfdcMMNevjhh7Vx40afS41C5XLcb731VnXv3l3t2rXTGWecoZUrV9b4ved5uvbaa3X++efrtNNO0/PPPx908ZEnP497dY888oh69uwZQInhBz+P+2effaYLLrhAv//973XllVfq5ptv1tatW4PeBOQhl+N+5513qnv37mrbtq3OPvtsbdiwocbvmdfZw8/jnsC8zny5zucTzjvvPJ111ll1Hp88ebLOPPNMnXXWWbr33ntzL5AXE++//77XqVMn77vvvvM8z/NGjhzpXXbZZSmfu3XrVu/AAw/07rrrLs/zPO+9997zunfv7pWXlyefM3jwYO+dd95J/vzHP/7RGzVqVIBbgHzkctw9z/Nmz57tnXPOOZ4k74033qjxu48++sgbOHCgd99993mTJk3y2rdv73Xs2DH52jCHn8c9YfHixd6+++7rvfzyy0EUGT7I5bhPmTLFO/bYY73HHnvMu+qqq7xGjRp5hxxySI3njBkzxhs+fLjneZ63YcMGr1u3bt5//vOfYDcCOfP7uCesXLnS69ixo9ejR4+gio4C+HncKysrvd12281bunRp8rELLrjAu+2224LdCOQsl+P+z3/+0xs/fry3YMEC749//KMnybv66quTv2deZw8/j3sC8zrz5TqfT5g9e7ZXVFTk/frXv67x+F133eUdcMABnud53pYtW7y99trL++c//5lTmWITngwbNsw77bTTkj//+9//9ho2bOh99dVXdZ771FNPecXFxd7GjRuTj+2yyy7eTTfd5Hme561atcqT5H377bfJ30+fPt3r06dPcBuAvORy3BM++uijlCfREydO9NasWZP8+a233vIkeVOmTPG72CiQn8fd8zxvzZo13m677ebdf//9/hcWvsnluN944401fr755ps9Sd4XX3zheZ7nLV261GvcuLE3Y8aM5HMuvPDCtCfaiI6fx726Sy65xDvllFMITwzl53H/73//6zVv3tyrqqpKPueOO+7wjjrqqGAKj7zlctzfe++9Gj8ffPDB3pFHHpn8mXmdPfw87p7HvM4W+cznKyoqvGOPPdbbb7/9aoQnGzdu9Nq3b+/dc889ycfGjx/v/eQnP/G2bt2adZlicdlOeXm5Xn31VfXv3z/5WL9+/eR5nqZOnVrn+VOnTtWuu+6qpk2bJh8bMGCAnnzySUnSdtttp+22205//OMfk7//17/+pSFDhgS3EchZrsc9oUmTJikfP+mkk9SqVavkzwcddJBat26tVatW+VdoFMzv4y5JV199tbp165Zy+R/MkOtxP//882v8fPTRR0tSsj1PmzZNlZWVNV5vwIABeuONN7RixYogNgF58Pu4J8yePVudOnXSLrvsEkCpUSi/j3u7du20fv16TZ48Ofkc5nXmyfW4V3+etO04H3PMMcmfmdfZwe/jLjGvs0G+8/k//elPuvTSS1VSUlLj8VmzZun777+vM6/74osv9N5772VdrliEJ/Pnz1dVVZXatWuXfKxJkyZq2bKl5s+fX+f577zzTo3nSlLHjh310UcfqaKiQiUlJbrqqqs0adIkjRo1Si+88IIWLlyoP/3pT4FvC7KX63Gvz/bbb1/jZ8/ztGXLFu23334FlxX+8fu4l5WV6d5771X//v110UUXqW/fvvXeJwHhy/W4127PVVVVat68ufr06SNp2zjQoEEDtWnTJvmcjh07yvM8LViwIKCtQK78Pu6SVFlZqb/85S+6+uqrgys4CuL3ce/evbvOPvtsjR49WrfccoumTJmi4uJiXX755cFuCHJSyPi+fPlytW/fvkaQxrzODn4fd+Z1dsjnuH/22Wdavnx5yuD7nXfekaQar9exY8fke2WrUdbPtFjiU8K2bdvWeLxFixb6/vvvUz6/dmrZokULbd26VatWrVKXLl00duxYrVq1SpMmTdK0adP0wQcf1FipgujletxzNW/ePPXp00cHHXRQwa8F//h93J9//nlVVVWpb9++OuWUU/T5559r//3316mnnqpXXnnFlzKjcIUe9xkzZmjEiBHJFUgrVqxQ69atVVRUVOO1JPnSf8Affh93adunVpdddpkaN27sb2HhmyCO+5133ql169bp6quv1r777qt//etfatAgFp8xWiOf415RUaHHHntMN954oxo3bqyFCxeqb9++KZ/LvM5Mfh935nV2yOe4jx07Vn/729+yfr185nWxGBUSk9/a4caWLVtUXFyc8vmpnisp+fzKykpt2LBBI0aM0OLFi/Xzn/9c69atC6L4yFOuxz1XEyZM0KRJkwp+HfjL7+P+8ccfq3nz5jrllFMkSb169dIFF1ygV199VQsXLiy8wPBFIcd9w4YNmjZtWo2VBtmMA4ie38f9008/VVlZmQYPHux/YeEbv4+7tO1bN4qLi3XOOedo3rx5Ou6447L+VgeEI5/jXlxcrKFDh+rSSy/V8uXLMx5X5nVm8vu4M6+zQ67H/aGHHtJRRx1V5+qRTK+Xz7wuFuFJly5dJElr166t8fj69evVoUOHlM9P9dyGDRsm06rEMq/Jkyfr4Ycf1rvvvqtLLrkkoC1APnI97rl4+umndcABB2jvvfcu6HXgP7+P+/r169W8efMaj/385z+XpDpfYY7oFHLcr7vuOk2cOLHGte/pxgFJBfcf8I/fx/3666/XTTfd5H9B4Su/j7vnefrVr36lU089VVOmTNH48eP1/PPPa9y4cf4XHnnL57g3aNBA3bp10+jRozVp0iR99dVX+vDDD+s8j3mdufw+7szr7JDLcV+5cqWmT5+u008/PafXy2deF4vwZLfddlNxcXGNm/xt3LhR5eXl2meffeo8v2/fvnVuCFhWVqYBAwaoQYMG+vrrr/XAAw8kE8vTTjtN48eP12OPPcb3gxsk1+OerY8//lhz5szhWmhD+X3cu3btqjVr1mjr1q3Jxzp16iSp7lJCRCff4/7444+rb9++2nfffWs83rdvX61fv75Gn15WVqZGjRrVuawT0fHzuH/99dd68skn1a5dOxUVFamoqEjjxo3T119/raKiIj3wwANBbgpy4Hd7nz17tt59910NGzZM0rabSV588cW69957g9kA5KXQ8f2EE06QpDqX5DGvM5vfx515nR1yOe4vvPCCnnjiieTYXVRUpDfffFMPPvigioqKVFpamrxsq/rrlZWVSVJO5wexCE/atm2rYcOGae7cucnHFi1apMaNGyfvuF7d6aefrgULFqiioiL52MKFC5ONL5FYVb+L78UXXyxp26cXMEOuxz0b33zzjf785z/rz3/+c/Kx8vJybdq0qeDywh9+H/cjjzxSmzZt0vvvv598bNWqVWrVqpUGDhzoR5Hhg3yO+5tvvqkvvviixt32E4PqySefrOLiYs2bNy/5u4ULF+qwww5LXiOL6Pl53Lt06aL58+fX+HfhhRcmH6/9bQ2Ijt/tfe3atSouLq5xj5OLL764xskVolfo+L5p0yZ17txZvXr1Sj7GvM58fh935nV2yOW4H3300XXG7wEDBiQf33777XX44YerQ4cONV5v4cKF6t27t3bbbbfsC5b1lxpb7j//+Y+3ww47eBs2bPA8z/Muvvhi73e/+53neZ43Y8YMb8CAAV5ZWZnneZ5XVVXlDRgwwHvmmWc8z/O8efPmeTvuuKO3fv365O/33HNP76GHHkq+/ty5c70TTjghzE1CFnI57gmffvqpJ8mbMWNGjce///57b9CgQd4TTzzhTZ8+3Zs+fbr31FNPeb/61a+8LVu2hLNByIqfx93zPG/48OHemWeemfz5N7/5jffHP/4xwC1APnI57v/5z3+8wYMHJ9vySy+95N17773eZZddlny9Sy+91Bs5cqTneZ5XXl7u9ejRw3vvvfdC3irUx+/jXt0NN9zg9ejRI5TtQG78PO7l5eVely5dvNmzZydf/4knnvCuuOKKkLcK9cnluN91113enDlzPM/zvK1bt3oXXnih99hjjyVfi3mdPfw87p7HvM4W+cznEw4++GDv17/+dY3HJk6c6B199NGe53ne5s2bvX79+nnPP/98TmWKxbftSNLAgQN1++2369xzz1X79u3Vtm3b5HXNa9asUWlpaXKlScOGDTVt2jSNHj1ac+fO1TfffKOZM2eqWbNmdX6/aNEi7bDDDlq5cqWmTJkS2fYhtVyOu7Qtgfz73/8uadud95s3b6799ttPmzdv1qGHHqr58+frrbfeqvEeI0aM4I78hvHruCfce++9+u1vf6tRo0apWbNm6ty5M19jaqBsj/tnn32mww47TKtXr9bs2bNrvMbUqVOT/3/rrbdq9OjRGjVqlFavXq0pU6ZwyY6B/D7usIOfx71FixaaPn26xo4dq1dffVVt2rRRRUWF/vCHP4S+Xcgsl/H9xRdf1KWXXqr+/ftrl1120QknnJC8NIt5nV38Ou4JzOvskOt8vj6XXXaZ1q5dqwsvvFAVFRW66qqrdNRRR+VUpiLP4zoTAAAAAACAdIhVAQAAAAAAMiA8AQAAAAAAyIDwBAAAAAAAIAPCEwAAAAAAgAwITwAAAAAAADIgPAEAAAAAAMiA8AQAAAAAACADwhMAAAAAAIAMCE8AAAAAAAAyIDwBAMAhxx57rC677LKoi5Gzd955R23bttXSpUvzfo1PP/1Uo0aN0p577uljyXKzePFijRkzRh06dEg+tmnTJu2www565plnAn3vOXPm6KmnntLChQt14YUXao899vDttcvLyzVhwgStXbvWt9cEAMAmhCcAADhk0KBBGjhwYNTFyFmXLl10xBFHqHXr1nm/RpMmTbRs2TKVl5f7V7AcFRcXa9OmTfr++++Tj5WUlOiII47QTjvtFNj7Tp8+Xe+++65OOukkde/eXRs2bNC6det8e/2WLVvqvPPO0wUXXODr6wIAYIsiz/O8qAsBAADgh7Fjx+rhhx9WaWlpZGW47777dO655yqsKdayZct0wgkn6F//+pcaNNj2udj111+vhx56yPf9MGPGDN1777168sknfX1dAABMx8oTAADgjIYNG0ZdhGSAEZZrr71Wv/rVr2q8b1BlOPTQQ/Xhhx/q7bffDuT1AQAwFeEJAAAWqaqq0tixYzVy5Egde+yxOvnkk5P3oZg9e7ZOPPFEnXfeeZK23QPk0ksvVb9+/fTmm2+qS5cuuvjiiyVJK1as0MUXX6xRo0Zp8ODBuuGGGyRJr732mnbffXf17NlTkvTtt99q1KhRKioq0qxZs7Rhwwb9/e9/1+6776433nhD48aNU4cOHbT77rvr008/1dy5c9W3b181b95ct9xyS42yv/feezrrrLM0cuRI7bPPPsl7gKxevVq33nqrdtxxR5WWlqqyslKPPvqoDjjgAN1777069dRT1b59e33yySeSpMcee0znnHOOzj//fB100EFatGhRxn02depUjR49Wocffrj23Xdf/fvf/075vFmzZunEE0/U+eefr4kTJ6ply5Z69NFHtWnTJl100UW66qqrdPzxx2vo0KE17s2yceNGjRgxQiNGjNCpp56qJ554Ivm7zZs367HHHtP++++vBx98UBs2bNBf/vIXNWzYUDfeeKMk6e2339Zee+2V3OeS9M9//lOXXnqpRo0apZYtW+r2229PWeZVq1bpscce0yGHHJJ2+7ds2aKjjjpKu+++u+68806tXbs272MoSUOGDNHEiRMz7XIAAJxDeAIAgEUuueQSFRcXa/LkyXr00Uf1wgsvaOzYsZKkdu3aad68eaqqqpIkNWrUSKWlpVq2bJkWLFig3/72t9p9991VUVGhoUOHavjw4frrX/+qcePG6aabbtI//vEPDR06VMcff3zy/Tp16lTjBrSVlZXabrvt9L///U/PPPOMjjzySP373//WypUrde6552r+/PmaMWOGrrrqKl177bX65ptvJEmffPKJjj/+eE2cOFGTJ0/WL37xC5188slasWKFNm/erC1btiQvMamsrFS7du3073//W08//bR+/etfa9iwYWrVqpUeffRR3XfffZoyZYruueceNWzYUKeddlra/fXmm29q+fLluu222/TKK69ojz320NFHH53yxqetW7fW3LlzNW/ePHXt2lVnnnmmunbtqltuuUULFy7ULbfcoqeffloff/yx/vrXvyb/7vTTT1f37t11++2369FH/1979xoSZdPGAfxvWrKpaS6l5SGeLCskU0iS0iIyNyIrSkuFMvpQIeWhEiTKTCn8oKBRYRBtWhKRZARJllQLJhYmdCBFO2xCkVrmYdXUda/3Q+yNm+u+2dPzvo/5/32RnRnmmntmQfbinpkSiz6NRiNmzpyJmpoaiAicnJyQkpICLy8vpc3y5csRFRVlMcfJyckoKCjA6dOncfny5VGfr6KiAkajEQsXLhy1TVNTE1QqFWpqarBv3z6YTKZfWkOzBQsW4N69e8r3jIiIaCJw+H8PgIiIiH7Ou3fvcOHCBTQ3NwMAnJycUFRUBG9vbwBAQEAA5syZo7SfO3culixZgurqaiQmJsLB4fu//aKiIvT29mLlypUAgJUrV6KgoADLli0DMHLLx/DP06dPR1hYGAAgOjpaOZx21apVaGlpUd5s2bZtGzIyMvD27VvMmjULOTk5iIiIwPTp0wF8TwK5ubnB3d0dDg4OCAkJUWI4OzsjMjISALB+/XpoNBpoNBoAwNGjR5GdnQ07OzsAQF5eHl6/fj3qnGVlZSEoKAg5OTkAAJVKhaCgIDQ3N4+4jSYoKAh//fUX1Go1YmJiEBMTAwDo6OiAp6en0s7T01M5EFan06GsrAzFxcUAADs7O2zevBkVFRVKvNWrV48Yl605NhgMaG5uRl5eHlJT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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spyro.plots.plot_mesh_sizes(\n", + " firedrake_mesh=new_mesh,\n", + " title_str=\"Overtrust ML4tri mesh\",\n", + " show=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, our element sizes are considerably bigger, varying from 80 to 400 meters. Larger elements for the same domain mean fewer elements. However, does this translate to fewer DoFs? A single ml4tri element has more DoFs than a single ML2tri element, so the tradeoff is only sometimes clear. Let us calculate the new total DoFs:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total Dofs for ml4tri function space:18798\n" + ] + } + ], + "source": [ + "element = fire.FiniteElement(\"KMV\", new_mesh.ufl_cell(), degree=4, variant=\"KMV\")\n", + "space = fire.FunctionSpace(new_mesh, element)\n", + "print(f\"Total Dofs for ml4tri function space:{space.dim()}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see using the 4th order element we have greatly reduced our total DoFs!" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "firedrake", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebook_tutorials/premade_useful_examples.html b/notebook_tutorials/premade_useful_examples.html new file mode 100644 index 00000000..fe6b0895 --- /dev/null +++ b/notebook_tutorials/premade_useful_examples.html @@ -0,0 +1,8177 @@ + + + + + +premade_useful_examples + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/notebook_tutorials/premade_useful_examples.ipynb b/notebook_tutorials/premade_useful_examples.ipynb new file mode 100644 index 00000000..9799b78a --- /dev/null +++ b/notebook_tutorials/premade_useful_examples.ipynb @@ -0,0 +1,524 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Premade useful examples" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "firedrake:WARNING OMP_NUM_THREADS is not set or is set to a value greater than 1, we suggest setting OMP_NUM_THREADS=1 to improve performance\n" + ] + } + ], + "source": [ + "# Code in this cell enables plotting in the notebook\n", + "%matplotlib inline\n", + "import spyro" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Author: Alexandre Olender\n", + "\n", + "Contact: olender@usp.br\n", + "\n", + "This tutorial provides simple examples commonly encountered in seismic imaging model development. These examples serve as a foundation for testing and verifying code implementations before applying them to more complex experiments. You can find these examples within the \"examples\" folder." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Rectangle example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Rectangle example is, by default, a 1 km by 1 km rectangle with 0.05 km mesh size and 0.25km absorbing layers. It has a default dictionary located in the rectangles.py file. You can easily modify any isolated dictionary parameter. The example class has a multiple_layer_velocity_model method for quickly adding horizontal velocity layers. For instance, you can create a four-layer experiment with:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parallelism type: automatic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/alexandre/Development/tutorials/spyro-1/spyro/solvers/wave.py:85: UserWarning: No mesh file, Firedrake mesh will be automatically generated.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "Wave_obj = spyro.examples.Rectangle_acoustic()\n", + "\n", + "layer_values = [1.5, 2.0, 2.5, 3.0]\n", + "z_switches = [-0.25, -0.5, -0.75]\n", + "Wave_obj.multiple_layer_velocity_model(z_switches, layer_values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us have a look at the generated model:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File name rectangle_model1.png\n" + ] + } + ], + "source": [ + "spyro.plots.plot_model(Wave_obj, filename=\"rectangle_model1.png\", show=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![title](rectangle_model1.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can easily customize this model. Let's create a model with the following specifications:\n", + "\n", + "- Width: 4 km\n", + "- Depth: 3 km\n", + "- Element size: 100 meters\n", + "- No Perfectly Matched Layer (PML)\n", + "- Source located 10 meters deep in the middle of the width\n", + "- 20 receivers equally spaced between the first and second layers\n", + "- 3 layers, equally spaced, with velocities of 1.5 km/s, 2.5 km/s, and 3.5 km/s.\n", + "\n", + "Simply adjust the parameters that deviate from the default values." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parallelism type: automatic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/alexandre/Development/tutorials/spyro-1/spyro/solvers/wave.py:85: UserWarning: No mesh file, Firedrake mesh will be automatically generated.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "dictionary = {}\n", + "dictionary[\"mesh\"] = {\n", + " \"Lz\": 3.0, # depth in km - always positive\n", + " \"Lx\": 4.0, # width in km - always positive\n", + " \"h\": 0.1, # mesh size in km\n", + "}\n", + "dictionary[\"absorving_boundary_conditions\"] = {\n", + " \"status\": False,\n", + " \"pad_length\": 0.,\n", + "}\n", + "dictionary[\"acquisition\"] = {\n", + " \"source_locations\": [(-0.1, 2.0)],\n", + " \"receiver_locations\": spyro.create_transect((-1.0, 0.0), (-1.0, 4.0), 20),\n", + "}\n", + "Wave_obj_rec2 = spyro.examples.Rectangle_acoustic(dictionary=dictionary)\n", + "layer_values = [1.5, 2.5, 3.5]\n", + "z_switches = [-1.0, -2.0]\n", + "Wave_obj_rec2.multiple_layer_velocity_model(z_switches, layer_values)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we can see, you only need to add the parameters that differ from the default values." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File name rectangle_model2.png\n" + ] + } + ], + "source": [ + "spyro.plots.plot_model(Wave_obj_rec2, filename=\"rectangle_model2.png\", show=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![title](rectangle_model2.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, generate your own model based on the Rectangle example with five layers, 6 km width, and 3 km depth." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Camembert example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A recurring model in the literature for verifying and validating code is the Camembert model, which consists of a higher velocity circle inside an otherwise homogeneous velocity rectangle domain.\n", + "\n", + "Let us create a model with the following specifications:\n", + "- 1 km wide,\n", + "- 1 km deep,\n", + "- 100 meter element size,\n", + "- inside circle velocity of 3.5 km/s and 0.2 km radius,\n", + "- outside circle velocity of 2.0 km/s,\n", + "- 1 ricker source at (-0.1, 0.5) with 6 Hz peak frequency,\n", + "- 10 receivers between (-0.9, 0.1) and (-0.9, 0.9)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parallelism type: automatic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/alexandre/Development/tutorials/spyro-1/spyro/io/dictionaryio.py:301: UserWarning: Both methods of specifying method and cell_type with variant used. Method specification taking priority.\n", + " warnings.warn(\n", + "/home/alexandre/Development/tutorials/spyro-1/spyro/io/model_parameters.py:610: UserWarning: No velocity model set initially. If using user defined conditional or expression, please input it in the Wave object.\n", + " warnings.warn(\n", + "/home/alexandre/Development/tutorials/spyro-1/spyro/solvers/wave.py:85: UserWarning: No mesh file, Firedrake mesh will be automatically generated.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "camembert_dictionary = {}\n", + "camembert_dictionary[\"mesh\"] = {\n", + " \"Lz\": 1.0, # depth in km - always positive\n", + " \"Lx\": 1.0, # width in km - always positive\n", + " \"h\": 0.05, # mesh size in km\n", + "}\n", + "camembert_dictionary[\"camembert_options\"] = {\n", + " \"radius\": 0.2,\n", + " \"outside_velocity\": 2.0,\n", + " \"inside_circle_velocity\": 3.5,\n", + "}\n", + "camembert_dictionary[\"acquisition\"] = {\n", + " \"source_locations\": [(-0.1, 0.5)],\n", + " \"frequency\": 6.0,\n", + " \"receiver_locations\": spyro.create_transect((-0.9, 0.1), (-0.9, 0.9), 10),\n", + "}\n", + "camembert_dictionary[\"visualization\"] = {\n", + " \"debug_output\": True,\n", + "}\n", + "Wave_obj_queijo_minas = spyro.examples.Camembert_acoustic(dictionary=camembert_dictionary)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File name camembert_example.png\n" + ] + } + ], + "source": [ + "spyro.plots.plot_model(Wave_obj_queijo_minas, filename=\"camembert_example.png\", show=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![title](camembert_example.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Looking at our model, you can see that the circle is not well-defined. The visual plotting capabilities of Firedrake have difficulties with higher order elements, such as the ML4tri we used on this camembert example. To have a more accurate representation of the real velocity model used, you have to use a Paraview version higher or equal to 5.8. The reason we passed a debug output boolean in our dictionary is so that it outputs the velocity model. The figure generated by Paraview can be seen below:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![title](camembert_example_paraview.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating your own example" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This section is crucial, especially during code development when you need to test or experiment with similar models featuring variations in single or multiple variables. For instance, you may want to train a neural network with changing velocity model files, run the same model in Full Waveform Inversion (FWI) with different receiver setups, or employ FWI while varying inversion options.\n", + "\n", + "Creating a commonly used example can be beneficial for your own use and for other researchers. For example, you may want to test the same velocity model with hundreds of variations of receiver locations." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can create a default example model such as the one below:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from spyro.examples.example_model import Example_model_acoustic\n", + "\n", + "default_dictionary = {}\n", + "default_dictionary[\"options\"] = {\n", + " \"cell_type\": \"T\", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q)\n", + " \"variant\": \"lumped\", # lumped, equispaced or DG, default is lumped\n", + " \"degree\": 4, # p order\n", + " \"dimension\": 2, # dimension\n", + "}\n", + "default_dictionary[\"parallelism\"] = {\n", + " \"type\": \"automatic\", # options: automatic (same number of cores for evey processor) or spatial\n", + "}\n", + "default_dictionary[\"mesh\"] = {\n", + " \"Lz\": 2.8, # depth in km - always positive # Como ver isso sem ler a malha?\n", + " \"Lx\": 6.0, # width in km - always positive\n", + " \"Ly\": 0.0, # thickness in km - always positive\n", + " \"mesh_file\": \"meshes/cut_overthrust.msh\",\n", + "}\n", + "default_dictionary[\"acquisition\"] = {\n", + " \"source_type\": \"ricker\",\n", + " \"source_locations\": [(-0.01, 3.0)],\n", + " \"frequency\": 5.0,\n", + " \"receiver_locations\": spyro.create_transect((-0.37, 0.2), (-0.37, 5.8), 300),\n", + "}\n", + "default_dictionary[\"absorving_boundary_conditions\"] = {\n", + " \"status\": True,\n", + " \"damping_type\": \"PML\",\n", + " \"exponent\": 2,\n", + " \"cmax\": 4.5,\n", + " \"R\": 1e-6,\n", + " \"pad_length\": 0.75,\n", + "}\n", + "default_dictionary[\"synthetic_data\"] = {\n", + " \"real_velocity_file\": \"velocity_models/cut_overthrust.hdf5\",\n", + "}\n", + "default_dictionary[\"time_axis\"] = {\n", + " \"initial_time\": 0.0, # Initial time for event\n", + " \"final_time\": 5.00, # Final time for event\n", + " \"dt\": 0.0005, # timestep size\n", + " \"output_frequency\": 200, # how frequently to output solution to pvds - Perguntar Daiane ''post_processing_frequnecy'\n", + " \"gradient_sampling_frequency\": 1, # how frequently to save solution to RAM - Perguntar Daiane 'gradient_sampling_frequency'\n", + "}\n", + "default_dictionary[\"visualization\"] = {\n", + " \"forward_output\": True,\n", + " \"forward_output_filename\": \"results/forward_output.pvd\",\n", + " \"fwi_velocity_model_output\": False,\n", + " \"velocity_model_filename\": None,\n", + " \"gradient_output\": False,\n", + " \"gradient_filename\": \"results/Gradient.pvd\",\n", + " \"adjoint_output\": False,\n", + " \"adjoint_filename\": None,\n", + " \"debug_output\": False,\n", + "}\n", + "optimization_parameters = {\n", + " \"General\": {\n", + " \"Secant\": {\"Type\": \"Limited-Memory BFGS\", \"Maximum Storage\": 10}\n", + " },\n", + " \"Step\": {\n", + " \"Type\": \"Augmented Lagrangian\",\n", + " \"Augmented Lagrangian\": {\n", + " \"Subproblem Step Type\": \"Line Search\",\n", + " \"Subproblem Iteration Limit\": 5.0,\n", + " },\n", + " \"Line Search\": {\"Descent Method\": {\"Type\": \"Quasi-Newton Step\"}},\n", + " },\n", + " \"Status Test\": {\n", + " \"Gradient Tolerance\": 1e-16,\n", + " \"Iteration Limit\": None,\n", + " \"Step Tolerance\": 1.0e-16,\n", + " },\n", + "}\n", + "default_dictionary[\"inversion\"] = {\n", + " \"perform_fwi\": False, # switch to true to make a FWI\n", + " \"initial_guess_model_file\": None,\n", + " \"shot_record_file\": None,\n", + " \"optimization_parameters\": optimization_parameters,\n", + "}\n", + "\n", + "class Overthrust_acoustic(Example_model_acoustic):\n", + " \"\"\"\n", + " Rectangle model.\n", + " This class is a child of the Example_model class.\n", + " It is used to create a dictionary with the parameters of the\n", + " Rectangle model.\n", + "\n", + " Parameters\n", + " ----------\n", + " dictionary : dict, optional\n", + " Dictionary with the parameters of the model that are different from\n", + " the default model. The default is None.\n", + " comm : firedrake.mpi_comm.MPI.Intracomm, optional\n", + " periodic : bool, optional\n", + " If True, the mesh will be periodic in all directions. The default is\n", + " False.\n", + " \"\"\"\n", + " def __init__(\n", + " self,\n", + " dictionary=None,\n", + " example_dictionary=default_dictionary,\n", + " comm=None,\n", + " periodic=False,\n", + " ):\n", + " super().__init__(\n", + " dictionary=dictionary,\n", + " default_dictionary=example_dictionary,\n", + " comm=comm,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now create multiple wave objects just by varying the desired variables. The example below creates 2 different objects with different source locations." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parallelism type: automatic\n", + "INFO: Distributing 1 shot(s) across 1 core(s). Each shot is using 1 cores\n", + " rank 0 on ensemble 0 owns 5976 elements and can access 3113 vertices\n", + "Parallelism type: automatic\n", + "INFO: Distributing 1 shot(s) across 1 core(s). Each shot is using 1 cores\n", + " rank 0 on ensemble 0 owns 5976 elements and can access 3113 vertices\n" + ] + } + ], + "source": [ + "temp_dict = {}\n", + "temp_dict[\"acquisition\"] = {\n", + " \"source_locations\": [(-0.01, 1.0)],\n", + " \"frequency\": 5.0,\n", + " \"receiver_locations\": spyro.create_transect((-0.37, 0.2), (-0.37, 5.8), 10),\n", + "}\n", + "Wave_obj_overthurst1 = Overthrust_acoustic(dictionary=temp_dict)\n", + "temp_dict[\"acquisition\"][\"source_locations\"] = [(-0.01, 5.0)]\n", + "Wave_obj_overthurst2 = Overthrust_acoustic(dictionary=temp_dict)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is also recommended for reused examples that require new methods not in the inherited class. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "firedrake", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebook_tutorials/simple_forward.html b/notebook_tutorials/simple_forward.html new file mode 100644 index 00000000..a71fb9aa --- /dev/null +++ b/notebook_tutorials/simple_forward.html @@ -0,0 +1,8120 @@ + + + + + +simple_forward + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + diff --git a/notebook_tutorials/simple_forward.ipynb b/notebook_tutorials/simple_forward.ipynb new file mode 100644 index 00000000..6dd623a6 --- /dev/null +++ b/notebook_tutorials/simple_forward.ipynb @@ -0,0 +1,449 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simple forward" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial was prepared by Alexandre Olender olender@usp.br\n", + "\n", + "This tutorial focuses on solving the acoustic wave equation using Spyro's `AcousticWave` class. Our main objective is to familiarize you with the initial dictionary inputs, which (together with the **simple forward with Overthrust tutorial**) should be enough if you are an end-user only interested in the results of the forward propagation methods already implemented in our software. \n", + "\n", + "Please refer to our **meshing tutorial** if you need more control over meshing. For more examples of simple cases usually encountered in seismic imaging, please refer to the tutorial on using **pre-made useful examples**. If you are interested in developing code for Spyro, both the **altering time integration** and the **altering variational equation** tutorials suit you.\n", + "\n", + "We currently have a **simple FWI** and **detailed synthetic FWI** tutorials for inversion-based tutorials." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Code in this cell enables plotting in the notebook\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We begin by making Spyro available in our notebook. This is necessary for every python package." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "firedrake:WARNING OMP_NUM_THREADS is not set or is set to a value greater than 1, we suggest setting OMP_NUM_THREADS=1 to improve performance\n" + ] + } + ], + "source": [ + "import spyro\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we begin to define our problem parameters. This can be done using a python dictionary." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary = {}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The first dictionary deals with basic finite element options. Here, we define our cell type as simplexes (i.e., triangles or tetrahedrals), quadrilaterals, or hexahedrals (both refered to as Q in the code and quads in this notebook). We also define what our \"variant\" will be. For quads and hexas, we can use equispaced or lumped. Lumped quads (or hexahedra) are spectral elements with nodal and quadrature values on GLL nodes, resulting in diagonal mass matrices. Lumped triangles (or tetrahedra) use specific quadrature and collocation nodes to have diagonal mass matrices. Equispaced elements are more traditional finite elements." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"options\"] = {\n", + " \"cell_type\": \"Q\", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q)\n", + " \"variant\": \"lumped\", # lumped, equispaced or DG, default is lumped\n", + " \"degree\": 4, # p order\n", + " \"dimension\": 2, # dimension\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we define our parallelism type. Let us stick with automatic for now." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"parallelism\"] = {\n", + " \"type\": \"automatic\", # options: automatic (same number of cores for evey processor) or spatial\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We must also define our mesh parameters, such as size in every axis. Here, we also define whether there is a premade mesh file (we accept every mesh input Firedrake accepts) or if a certain automatically generated mesh type will be used. The firedrake_mesh option generates structured or semi-structured meshes using Firedrake's built-in meshing methods and works on both quads and simplexes. The SeismicMesh option allows us to generate automatic waveform-adapted unstructured meshes." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"mesh\"] = {\n", + " \"Lz\": 3.0, # depth in km - always positive # Como ver isso sem ler a malha?\n", + " \"Lx\": 3.0, # width in km - always positive\n", + " \"Ly\": 0.0, # thickness in km - always positive\n", + " \"mesh_file\": None,\n", + " \"mesh_type\": \"firedrake_mesh\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We also have to define our acquisition geometry for receivers and sources. Source-related properties such as type, location, frequency, and delay are among those available. Point receiver locations also have to be stated. Here, we can input locations as lists of coordinate tuples; however, when we have many sources or receivers arranged in an equal-spaced array, the spyro.create_transect method simplifies this input. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"acquisition\"] = {\n", + " \"source_type\": \"ricker\",\n", + " \"source_locations\": [(-1.1, 1.5)],\n", + " \"frequency\": 5.0,\n", + " \"delay\": 0.2,\n", + " \"delay_type\": \"time\",\n", + " \"receiver_locations\": spyro.create_transect((-1.9, 1.2), (-1.9, 1.8), 300),\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our time-domain inputs are also significant to both computational cost and accuracy. The bigger the timestep variable (dt) used here, the smaller the computational runtime of our simulation. However, a timestep too large leads to stability problems. For this example, we will set a value, but it is important to note that Spyro has a built-in tool to calculate the maximum stable timestep for a given problem. We can also define our output_frequency for visualization purposes; an output frequency of 100 means that for every 100 timesteps, Spyro will save the pressure across the domain visualization. The gradient sampling frequency will be set to 1 here and is only essential for inversion problems, which are not the focus of this tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"time_axis\"] = {\n", + " \"initial_time\": 0.0, # Initial time for event\n", + " \"final_time\": 1.0, # Final time for event\n", + " \"dt\": 0.001, # timestep size\n", + " \"output_frequency\": 100, # how frequently to output solution to pvds - Perguntar Daiane ''post_processing_frequnecy'\n", + " \"gradient_sampling_frequency\": 1, # how frequently to save solution to RAM - Perguntar Daiane 'gradient_sampling_frequency'\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also define where we want everything to be saved. If left blank, most of these options will be replaced with default values." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"visualization\"] = {\n", + " \"forward_output\": True,\n", + " \"forward_output_filename\": \"results/forward_output.pvd\",\n", + " \"fwi_velocity_model_output\": False,\n", + " \"velocity_model_filename\": None,\n", + " \"gradient_output\": False,\n", + " \"gradient_filename\": \"results/Gradient.pvd\",\n", + " \"adjoint_output\": False,\n", + " \"adjoint_filename\": None,\n", + " \"debug_output\": False,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now create our acoustic wave object." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parallelism type: automatic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/media/alexandre/T7 Shield/Development/tutorials/spyro-1/spyro/io/model_parameters.py:610: UserWarning: No velocity model set initially. If using user defined conditional or expression, please input it in the Wave object.\n", + " warnings.warn(\n", + "/media/alexandre/T7 Shield/Development/tutorials/spyro-1/spyro/solvers/wave.py:85: UserWarning: No mesh file, Firedrake mesh will be automatically generated.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "Wave_obj = spyro.AcousticWave(dictionary=dictionary)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since we are using a firedrake-generated mesh in this tutorial, we must set an element size for the mesh. Notice that in this case, we have also decided to use a periodic mesh; by default, this does not happen. This will create a periodic mesh in both directions. That means that when a wave propagates to a border, it will appear at the opposite border. For most user cases, we will have absorbing boundary conditions. The simple forward with overthrust tutorial will show how to use PML conditions." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "Wave_obj.set_mesh(mesh_parameters={\"dx\": 0.02, \"periodic\": True})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For our first tutorial, we will have a medium with a homogenous velocity similar to water velocity. For complex models that need to be loaded from a file, such as a 2D overthrust cut, see the **simple FWI with overthrust tutorial**. The set_initial_velocity_model shown below can also be used for conditionals, Firedrake functions, expressions, or files (segy or hdf5). This allows us to represent almost any heterogeneous velocity." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "Wave_obj.set_initial_velocity_model(constant=1.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before propagating our wave, it is helpful to see whether our experiment setup loaded correctly. We can plot our wave object." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File name model.png\n" + ] + }, + { + "data": { + "image/png": "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yyKYAACCi+HLMIC7dQEucfhyKniA7zkBYAztHAxo89266urqdN999+m1117T1q1bZdvJd89/5513XCsO6Eg4ZMMLfM8JAFNo52BCm8PJlClTtGTJEl144YUaNmwYXzwHNOFuXQCAIKOVgwltDieLFy/WCy+8oFNPPTUT9QAdlk13NwAAQLu0eRhy7969lZeXl4lagA7NsjirBAAA0B5tDid33323brzxRq1fvz4T9QAdliXu1gXzuCAegCm0cTChzcO6hg4dqrq6Oh155JHq1KmTcnJykp7fvn27a8UBHQnDuuAFLogHYAqjA2BCm8PJxIkTtXHjRv3f//t/1aNHDy6IB5qEmoZ1EVEAAAAOTJvDyapVq1RaWqri4uJM1AN0WI4IJgCA4KKNgwltHoZ8zDHHqLa2NhO1AB0bR20AQJDRzsGANoeTX//61/rlL3+p5cuX6+uvv1ZVVVXSAziYMcgRpnFBPABTGMkPE9o8rOvss8+WJH3/+99Pmu44jizLUjwed6cyoINxLE4qwTwuiA8+x7FV1/iZ4k61sqw8RbOPlGURN2EexxGY0OZw8tprr7m28TfeeEN33nmn3n77bW3evFkLFizQ+PHj086/fPlynX766S2mb968WYWFha7VBRyIEP0mAFxWE3tXX+9eqLhTmZiWZRXoO53Gq3N4oIeV4WBEJIYJbQ4nI0eObHH74L2++uqrNq2rpqZGxcXFuvzyy3XBBRe0erl169YpPz8/8fthhx3Wpu0CmWA7DnfrAuCamti72lrzhxbT406lttb8QYdpMgEFQOC0OZz88z//s5555pkWtxDesmWLvv/972vt2rWtXte4ceM0bty4tpagww47TF27dm3zckAmWRZfwgjAHY5j6+vdC/c5z9e7F6lTzvEM8YIxtHEwoc1HtPLycl1xxRVJ0zZv3qwxY8bomGOOca2wfRk0aJB69uypM888U3/5y1+MbDOV+vp6bgiABAZ1wQtcEB9Me64xqdznPHFnp+oaPzNUESCFuCIeBrS5vXrhhRe0atUqTZs2TZK0adMmjRkzRgMHDtTTTz/teoHN9ezZUw8//LCeffZZPfvssyoqKtKYMWP0zjvvZHS76cycOVMFBQWJR1FRkSd1wB/4hnh4gQvigynuVLs6HwB0FG0e1tW9e3ctWbJEp512miRp8eLFOumkk/TEE08oFMrs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Nn399df7nOfII49UOBxuMf3LL79UUVGRVq1apREjRmSqRHQQbX0vbdq0SWPGjNEpp5yiOXPmKBTi3BL2OJDjEtecoLVisZg6deqkZ555RuPHj09Mnzx5snbu3MmIALgu2+sCgI6ke/fu6t69+wEta9u2JKm+vt7NktBBteW9tHHjRp1++ukaMmSIZs+eTTBBkvYcl4D9CYfDGjJkiJYtW5YIJ7Zta9myZZo6daq3xSGQCCdABvztb3/TW2+9pdNOO03dunXTp59+qv/4j//QUUcdRa8J2mTjxo0aM2aM+vbtq7vuukvbtm1LPFdYWOhhZeiIysvLtX37dpWXlysej6usrEyS1L9/f3Xp0sXb4uBb06ZN0+TJkzV06FANGzZMs2bNUk1NjS677DKvS0MAEU6ADOjUqZP+93//VzNmzFBNTY169uyps88+W7fccosikYjX5aEDWbp0qT755BN98skn6tOnT9JzjMpFW9166636wx/+kPh98ODBkqTXXntNY8aM8agq+N2ECRO0bds23XrrraqoqNCgQYP00ksvtbhIHnAD15wAAAAA8AUGLgMAAADwBcIJAAAAAF8gnAAAAADwBcIJAAAAAF8gnAAAAADwBcIJAAAAAF8gnAAAAADwBcIJAAAAAF8gnAAAfG3dunUqLCxUdXW1JGnOnDnq2rWr69v553/+Z919992urxcA0HqEEwBARi1fvlyWZaV9nH766ftcfvr06brmmmuUl5eX0TpvueUW/dd//ZcqKyszuh0AQHqEEwBARo0cOVKbN29u8XjkkUdkWZb+7d/+Le2y5eXlWrx4sS699NKM13nCCSfoqKOO0p/+9KeMbwsAkBrhBACQUeFwWIWFhUmPHTt26Prrr9fNN9+siy66KO2yTz/9tIqLi9W7d++082zbtk1Dhw7V+eefr/r6+kRPzcsvv6zBgwcrNzdXZ5xxhrZu3aoXX3xRxx57rPLz83XJJZdo9+7dSes699xz9dRTT7n22gEAbUM4AQAYtXPnTp133nkaM2aM7rjjjn3Ou2LFCg0dOjTt8xs2bNCoUaN0wgkn6JlnnlEkEkk8d9ttt+n+++/XqlWrtGHDBl188cWaNWuW5s6dq+eff15LlizRfffdl7S+YcOG6c0331R9fX37XiQA4IAQTgAAxti2rUsuuUTZ2dl64oknZFnWPudfv369evXq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m9S/f39NnDhRkjRu3LjEfCeeeKKGDx+uvn376umnn9aUKVO8KhkpxONxXXLJJbr99tv13e9+1+ty0E6t3Z98RjuOAQMGJI11HjlypD799FPdc889evzxxz2sDAeiNfuTz2fHYtu2LMvSE088oYKCAknSb3/7W1144YV68MEHlZub63GFaIvW7M8RI0ZoxIgRiWVGjhypY489Vo888ojuuOOOVm+LcOKyI444QpI0cOBAbdmyRbfddlsinHxb165d9d3vfleffPKJyRLRCtXV1Vq9erXWrFmjqVOnStrzwXQcR9nZ2VqyZIlOO+00ZWVlacuWLUnLbtmyRYWFhV6UjTRasz/POOOMFsvxGe1Yhg0blrhA+tBDD+Xz2cE135+p8Pn0t549e6p3796Jf2Ql6dhjj5XjOPryyy919NFHq7CwkM9oB9Ga/fltOTk5Gjx4cJs/o1xzkkG2bau+vj7t87t27dKnn36qnj17GqwKrZGfn693331XZWVlicdPf/pTDRgwQGVlZRo+fLjC4bCGDBmiZcuWJZazbVvLli1LOnMA77Vmf6bCZ7RjKSsrS+wrPp8dX/P9mQqfT3879dRTtWnTJu3atSsx7aOPPlIoFEoM8RkxYkTSZ1SSli5dymfUh1qzP78tHo/r3XffbfNnlJ6TNHbt2pWU9D7//HOVlZXpkEMO0eGHH67p06dr48aN+uMf/yhpz730Dz/88MQ919944w3ddddd+vnPf55Yx/XXX69zzz1Xffv21aZNmzRjxgxlZWWl7VmBu9qyT0OhkE444YSk5Q877DBFo9Gk6dOmTdPkyZM1dOhQDRs2TLNmzVJNTY0uu+wyY6/rYJWJ/cln1DttPebOmjVLRxxxhI4//njV1dXp97//vV599VUtWbIksQ4+n97JxP7k8+mttu7TSy65RHfccYcuu+wy3X777frqq6/07//+77r88ssTQ7quvfZajR49WnfffbfOOeccPfXUU1q9enWLr2GA+zKxP3/1q1/plFNOUf/+/bVz507deeedWr9+fdL1163S7vt9BdTe2xp++zF58mTHcRxn8uTJzujRoxPz/+53v3OOP/54p1OnTk5+fr4zePBg58EHH3Ti8XhingkTJjg9e/Z0wuGw07t3b2fChAnOJ598YviVHbzauk+/Ld1tLO+77z7n8MMPd8LhsDNs2DDnr3/9a2ZeAJJkYn/yGfVOW/fnf//3fztHHXWUE41GnUMOOcQZM2aM8+qrr7ZYL59Pb2Rif/L59NaBHHM/+OADp6SkxMnNzXX69OnjTJs2zdm9e3fSPE8//bTz3e9+1wmHw87xxx/vPP/884Ze0cEtE/vzuuuuSxxve/To4fzgBz9w3nnnnTbXZjmO47QtzgAAAACA+7jmBAAAAIAvEE4AAAAA+ALhBAAAAIAvEE4AAAAA+ALhBAAAAIAvEE4AAAAA+ALhBAAAAIAvEE4AAG3Wr18/zZo1y7fra27dunUqLCxUdXW1JGnOnDnq2rVrRrbVWrFYTP369dPq1as9rQMA/IZwAgAHkXPPPVdnn312yudWrFghy7L0j3/8w3BV0ltvvaV//dd/TfxuWZYWLlzoyrqnT5+ua665Rnl5ea6sb38uu+wy3XLLLfucJxwO6/rrr9eNN95opCYA6CgIJwBwEJkyZYqWLl2qL7/8ssVzs2fP1tChQ3XiiScar6t79+7q1KmT6+stLy/X4sWLdemll7q+7lTi8bgWL16sH/7wh/ud98c//rFWrlyp9957z0BlANAxEE4A4CDyT//0T+revbvmzJmTNH3Xrl2aP3++pkyZIklauXKlRo0apdzcXBUVFennP/+5ampq0q63vLxc5513nrp06aL8/HxdfPHF2rJlS9I8f/7zn3XyyScrGo3q0EMP1fnnn594rvmwrn79+kmSzj//fFmWpX79+umLL75QKBRqMQxq1qxZ6tu3r2zbTlnX008/reLiYvXu3Ttt7du2bdPQoUN1/vnnq76+XsuXL5dlWXr55Zc1ePBg5ebm6owzztDWrVv14osv6thjj1V+fr4uueQS7d69O2ldq1atUk5Ojk4++WTFYjFNnTpVPXv2VDQaVd++fTVz5szEvN26ddOpp56qp556Km1tAHCwIZwAwEEkOztbkyZN0pw5c+Q4TmL6/PnzFY/HNXHiRH366ac6++yz9aMf/Uj/+Mc/NG/ePK1cuVJTp05NuU7btnXeeedp+/btev3117V06VJ99tlnmjBhQmKe559/Xueff75+8IMfaM2aNVq2bJmGDRuWcn1vvfWWpD09OZs3b9Zbb72lfv36qaSkRLNnz06ad/bs2br00ksVCqVuzlasWKGhQ4em/Xts2LBBo0aN0gknnKBnnnlGkUgk8dxtt92m+++/X6tWrdKGDRt08cUXa9asWZo7d66ef/55LVmyRPfdd1/S+p577jmde+65sixLv/vd7/Tcc8/p6aef1rp16/TEE08kgtdew4YN04oVK9LWBwAHHQcAcFD54IMPHEnOa6+9lpg2atQo5yc/+YnjOI4zZcoU51//9V+TllmxYoUTCoWc2tpax3Ecp2/fvs4999zjOI7jLFmyxMnKynLKy8sT87/33nuOJOfNN990HMdxRowY4fz4xz9OW1Pz9TmO40hyFixYkDTPvHnznG7dujl1dXWO4zjO22+/7ViW5Xz++edp11tcXOz86le/Spo2e/Zsp6CgwPnwww+doqIi5+c//7lj23bi+ddee82R5LzyyiuJaTNnznQkOZ9++mli2lVXXeWMHTs2ad1HH320s3jxYsdxHOeaa65xzjjjjKR1f9u9997r9OvXL+3zAHCwoecEAA4yxxxzjEaOHKnHHntMkvTJJ59oxYoViSFdf//73zVnzhx16dIl8Rg7dqxs29bnn3/eYn0ffPCBioqKVFRUlJh23HHHqWvXrvrggw8kSWVlZfr+97/frrrHjx+vrKwsLViwQNKeu26dfvrpLXojmqutrVU0Gk05fdSoUbrgggt07733yrKsFvM0v/amR48e6tSpk4488sikaVu3bk38/sEHH2jTpk2J13nppZeqrKxMAwYM0M9//nMtWbKkxTZyc3NbDA0DgIMZ4QQADkJTpkzRs88+q+rqas2ePVtHHXWURo8eLWnP9SdXXXWVysrKEo+///3v+vjjj3XUUUcd0PZyc3PbXXM4HNakSZM0e/ZsxWIxzZ07V5dffvk+lzn00EO1Y8eOFtMjkYhKSkq0ePFibdy4MeWyOTk5iZ8ty0r6fe+05te6PPfcczrzzDMTYeikk07S559/rjvuuEO1tbW6+OKLdeGFFyatY/v27erevfu+XzgAHEQIJwBwELr44osVCoU0d+5c/fGPf9Tll1+e6D046aST9P7776t///4tHuFwuMW6jj32WG3YsEEbNmxITHv//fe1c+dOHXfccZL29EIsW7as1fXl5OQoHo+3mH7FFVfolVde0YMPPqjGxkZdcMEF+1zP4MGD9f7777eYHgqF9Pjjj2vIkCE6/fTTtWnTplbXls6iRYt03nnnJU3Lz8/XhAkT9D//8z+aN2+enn32WW3fvj3x/Nq1azV48OB2bxsAgoJwAgAHoS5dumjChAmaPn26Nm/enHSr3RtvvFGrVq3S1KlTVVZWpo8//liLFi1Ke0F8SUmJBg4cqB//+Md655139Oabb2rSpEkaPXp04mL0GTNm6Mknn9SMGTP0wQcf6N1339V///d/p62vX79+WrZsmSoqKpJ6Po499lidcsopuvHGGzVx4sT99siMHTtWpaWlKYNOVlaWnnjiCRUXF+uMM85QRUXFPte1L1u3btXq1av1T//0T4lpv/3tb/Xkk0/qww8/1EcffaT58+ersLAw6QsgV6xYobPOOuuAtwsAQUM4AYCD1JQpU7Rjxw6NHTtWvXr1Skw/8cQT9frrr+ujjz7SqFGjNHjwYN16661J8zRnWZYWLVqkbt266Xvf+55KSkp05JFHat68eYl5xowZo/nz5+u5557ToEGDdMYZZ+jNN99MW9vdd9+tpUuXqqioqEXPwpQpUxSLxfY7pEuSxo0bp+zsbL3yyispn8/OztaTTz6p448/PnG74APx5z//WcOGDdOhhx6amJaXl6ff/OY3Gjp0qE4++WR98cUXeuGFFxJ3FistLVVlZWWLoV4AcDCzHKfZvSQBAPC5O+64Q/Pnz2/1N9k/8MADeu655/Tyyy9nrKYf/vCHOu2003TDDTe0epkJEyaouLhYN998c8bqAoCOJtvrAgAAaI1du3bpiy++0P3336///M//bPVyV111lXbu3Knq6mrl5eVlpLbTTjtNEydObPX8sVhMAwcO1C9+8YuM1AMAHRU9JwCADuHSSy/Vk08+qfHjx2vu3LnKysryuiQAgMsIJwAAAAB8gQviAQAAAPgC4QQAAACALxBOAAAAAPgC4QQAAACALxBOAAAAAPgC4QQAAACALxBOAAAAAPgC4QQAAACALxBOAAAAAPjC/wdV4ENTdzQOZgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spyro.plots.plot_model(Wave_obj, filename=\"model.png\", flip_axis=False, show=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All we have to do now is call our forward solve method." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving output in: results/forward_outputsn0.pvd\n", + "Simulation time is: 0.0 seconds\n", + "Simulation time is: 0.1 seconds\n", + "Simulation time is: 0.2 seconds\n", + "Simulation time is: 0.3 seconds\n", + "Simulation time is: 0.4 seconds\n", + "Simulation time is: 0.5 seconds\n", + "Simulation time is: 0.6 seconds\n", + "Simulation time is: 0.7 seconds\n", + "Simulation time is: 0.8 seconds\n", + "Simulation time is: 0.9 seconds\n", + "Simulation time is: 1.0 seconds\n" + ] + } + ], + "source": [ + "Wave_obj.forward_solve()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can look at the last pressure distribution. It is saved as a firedrake function inside our wave object." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "last_pressure = Wave_obj.u_n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As with every Firedrake function, we can access a numpy array containing all nodal values (at every degree of freedom)." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "last_pressure_data = last_pressure.dat.data[:]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All Firedrake functions can be plotted using tools available inside Firedrake. We will use our own wrapper for this tutorial, but we recommend looking into Firedrake tutorials for more information." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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43d+53dE5S+77DIcddRReM973oPHPe5xuOCCC/DCF74Q733ve9WD7aOkZSxrwEKMT9l5TxnBybG8ZRftY0KuhEMIIaQm6NLvJXmmfvPmzTjttNOWbDv99NOxefPmwTo7duzAtm3blvzXptQ3yJbkYFq+KVbrvs99FZzQm0er5S1j3HopJR3rhBBCiAU1u/RABlG/ZcsWrF27dsm2tWvXYtu2bfj5z3/eW+fSSy/FmjVr9vy3fv363nI+l57O4VJSR2NqEe6p16zvIp0wmwq69YQQQqYIXfqlFLn6zcUXX4ytW7fu+e/uu+8GAPz9fY8dbRnLmvBNWtWINldZ7U1CLaLfgtDlLenWE0IIIX6sTaTaXXogg6hft24d7rvvviXb7rvvPqxevRoHHHBAb50VK1Zg9erVS/7rktulryl600dMhGb7EcvE9RvhPvcJs12m6tb3fY506wkhhIzBnF16IIOo37hxI6699tol26655hps3LjRpH269OMwddfdOoLTJtfyljncekIIISQ3U3DprVa9a6MW9T/96U9xyy234JZbbgGwe8nKW265BXfddReA3dGZc889d0/5V7ziFfh//+//4XWvex1uvfVW/I//8T/wqU99Cq95zWuCB+1bMWTOLn2I2NZGctqU7siPydjLW3ahW08IIWSqlO7S50At6m+44QY85SlPwVOe8hQAwEUXXYSnPOUpeNOb3gQAuPfee/cIfAA46qij8Dd/8ze45pprcPzxx+M973kPPvKRj+D00083+hN2w2Us9ay6cxElyoeiNq4ITkkO/5gTZsdy6zVvmY2Fwp4QQog11pNjc7n07Wt5KvPuIdoKp5xyChaLYSHY97bYU045BTfffLO2Ky85XjbloySXvo9Vdy72HMR9B7NUZDe5+pJEeQ0sv/Ue783BiruW73NSuWbLcVleRX3k4T90PvmSzqfYevQKinhCCCFV0b321pqlbyhy9Zsh7vrBwaqXTVmQMlttzZDg1nw+TVlpW0Pl6Nb3U5pb34VuPSGEkFJJuYRll5qy9A1Vifo2Epee0Zu9WAjpksR4TZSWre/icyZisvUAhT0hhJB4YgR9H7lc+pxUK+obxnTpS4/eNOQQ4zVPmB1zecupufW1/CYIIYTMhzFd+qEsvW/RlxCqFPVcxnJfUgn3PtEpnRjrewlWLVhEcObi1g9Bt54QQkgo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i/rnP//52LBhA1auXIlDDz0U55xzDn7wgx846/ziF7/Aq171Kvz6r/86Hv7wh+P3fu/39nkBFsnL9773PZx//vk46qijcMABB+Doo4/GJZdcgp07dzrrnXLKKVi2bNmS/17xildkGjUZIvT75G+zTN7xjnfg5JNPxsMe9jAceOCBojoveclL9vltnnHGGWkHSkSEfJ+LxQJvetObcOihh+KAAw7Aaaedhn/5l39JO1Di5Sc/+Qle/OIXY/Xq1TjwwANx/vnn46c//amzDq+b5fDBD34QRx55JFauXImTTjoJX//6153lL7/8chx33HFYuXIlnvSkJ+Fzn/ucqr/iRf2pp56KT33qU7jtttvw6U9/Grfffjte+MIXOuu85jWvwf/+3/8bl19+Ob70pS/hBz/4AX73d38304hJH7feeit27dqFD33oQ/j2t7+N9773vbjsssvwxje+0Vv3ZS97Ge699949/73zne/MMGLiIvT75G+zTHbu3ImzzjoLr3zlK1X1zjjjjCW/zY9//OOJRkg0hHyf73znO/Hnf/7nuOyyy3D99dfj137t13D66afjF7/4RcKREh8vfvGL8e1vfxvXXHMNrr76anz5y1/Gy1/+cm89XjfH55Of/CQuuugiXHLJJbjppptw/PHH4/TTT8e//uu/9pb/6le/irPPPhvnn38+br75Zpx55pk488wz8a1vfUve6aIyrrrqqsWyZcsWO3fu7N1///33Lx760IcuLr/88j3b/vmf/3kBYLF58+ZcwyQC3vnOdy6OOuooZ5lnPetZi1e/+tV5BkSi8H2f/G2Wz0c/+tHFmjVrRGXPO++8xQte8IKk4yFxSL/PXbt2LdatW7d417vetWfb/fffv1ixYsXi4x//eMIREhff+c53FgAW//iP/7hn2+c///nFsmXLFt///vcH6/G6WQZPf/rTF6961av2/PvBBx9cHHbYYYtLL720t/x/+A//YfG85z1vybaTTjpp8Z//838W91m8U9/mJz/5Cf76r/8aJ598Mh760If2lrnxxhvxy1/+Eqeddtqebccddxw2bNiAzZs35xoqEbB161YcdNBB3nJ//dd/jYMPPhhPfOITcfHFF+NnP/tZhtERLb7vk7/N6XHdddfhkEMOwbHHHotXvvKV+PGPfzz2kEgAd9xxB7Zs2bLkt7lmzRqcdNJJ/G2OyObNm3HggQfiN37jN/ZsO+2007Dffvvh+uuvd9bldXNcdu7ciRtvvHHJb2q//fbDaaedNvib2rx585LyAHD66aerfoPqN8qOwetf/3p84AMfwM9+9jM84xnPwNVXXz1YdsuWLVi+fPk+OcK1a9diy5YtiUdKpHz3u9/F+9//frz73e92lvuP//E/4ogjjsBhhx2Gb3zjG3j961+P2267DZ/5zGcyjZRIkHyf/G1OizPOOAO/+7u/i6OOOgq333473vjGN+K5z30uNm/ejP3333/s4REFze+veTN8A3+b47JlyxYccsghS7Y95CEPwUEHHeT8XnjdHJ8f/ehHePDBB3t/U7feemtvnS1btkT/Bkdx6t/whjfsM4mj+1/7j37ta1+Lm2++GX/7t3+L/fffH+eeey4WfBFuEWi/SwD4/ve/jzPOOANnnXUWXvaylznbf/nLX47TTz8dT3rSk/DiF78Yf/mXf4nPfvazuP3221P+WbMl9fdJ8hHyXWr4/d//fTz/+c/Hk570JJx55pm4+uqr8Y//+I+47rrr7P4IsofU3yfJR+rvktfN+TKKU/9Hf/RHeMlLXuIs8+hHP3rP/z/44INx8MEH47GPfSwe97jHYf369fja176GjRs37lNv3bp12LlzJ+6///4ljuB9992HdevWWf0J5Fdov8sf/OAHOPXUU3HyySfjwx/+sLq/k046CcBuZ/joo49W1yduUn6f/G3mRftdxvLoRz8aBx98ML773e/i2c9+tlm7ZDcpv8/m93fffffh0EMP3bP9vvvuwwknnBDUJhlG+l2uW7dun0mV//Zv/4af/OQnqnMmr5v5Ofjgg7H//vvvs7qb63q3bt06Vfk+RhH1j3zkI/HIRz4yqO6uXbsAADt27Ojdf+KJJ+KhD30orr32Wvze7/0eAOC2227DXXfd1XsTQOLQfJff//73ceqpp+LEE0/ERz/6Uey3n/5B0S233AIASy48xI6U3yd/m3mJOc+GcM899+DHP/4xf5uJSPl9HnXUUVi3bh2uvfbaPSJ+27ZtuP7669UrIhE/0u9y48aNuP/++3HjjTfixBNPBAB88YtfxK5du/YIdQm8buZn+fLlOPHEE3HttdfizDPPBLBbv1577bW44IILeuts3LgR1157LS688MI926655hrd9TFoSm8mvva1ry3e//73L26++ebF9773vcW11167OPnkkxdHH3304he/+MVisVgs7rnnnsWxxx67uP766/fUe8UrXrHYsGHD4otf/OLihhtuWGzcuHGxcePGsf4Mstj9PT3mMY9ZPPvZz17cc889i3vvvXfPf+0y7e/yu9/97uKtb33r4oYbbljccccdi6uuumrx6Ec/evHMZz5zrD+D/IqQ73Ox4G+zVO68887FzTffvHjLW96yePjDH764+eabFzfffPNi+/bte8oce+yxi8985jOLxWKx2L59++K//Jf/sti8efPijjvuWPzd3/3d4qlPferimGOO2XNuJuOh/T4Xi8XiT//0TxcHHnjg4qqrrlp84xvfWLzgBS9YHHXUUYuf//znY/wJ5FecccYZi6c85SmL66+/fvGVr3xlccwxxyzOPvvsPft53SyXT3ziE4sVK1YsNm3atPjOd76zePnLX7448MADF1u2bFksFovFOeecs3jDG96wp/w//MM/LB7ykIcs3v3udy/++Z//eXHJJZcsHvrQhy6++c1vivssWtR/4xvfWJx66qmLgw46aLFixYrFkUceuXjFK16xuOeee/aUueOOOxYAFn//93+/Z9vPf/7zxR/8wR8sHvGIRywe9rCHLX7nd35nidgg+fnoRz+6AND7X0P3u7zrrrsWz3zmM/d8/495zGMWr33taxdbt24d6a8gDSHf52LB32apnHfeeb3fZfu7A7D46Ec/ulgsFouf/exni9/8zd9cPPKRj1w89KEPXRxxxBGLl73sZXsuVmRctN/nYrF7Wcs/+ZM/Waxdu3axYsWKxbOf/ezFbbfdln/wZAk//vGPF2efffbi4Q9/+GL16tWLl770pUtuznjdLJv3v//9iw0bNiyWL1++ePrTn7742te+tmffs571rMV55523pPynPvWpxWMf+9jF8uXLF094whMWf/M3f6Pqb9liwRmnhBBCCCGE1ExV69QTQgghhBBC9oWinhBCCCGEkMqhqCeEEEIIIaRyKOoJIYQQQgipHIp6QgghhBBCKoeinhBCCCGEkMqhqCeEEEIIIaRyKOoJIYQQQgipHIp6QgghhBBCKoeinhBCCCGEkMqhqCeEEEIIIaRyKOoJIYQQQgipnP8ffizxmRjpiHgAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spyro.plots.plot_function(last_pressure)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In seismic imagining, the shot record is one of the most important outputs of the forward problem. It represents the pressure values recorded at every receiver and timestep. This is the data that we will use for inversion. Spyro saves shot records by default in the shots folder. We can also have a visual look at the shot record results here." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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eXl5KCgowIgRI7B06VLU19d7XT3P+cMf/hCNA0z0780337S0sXfvXtxwww3o06cPsrOz0alTJ0yaNAmrV6928UzUUlNTg9deew333nsvvvvd76J3797RtuFdBPP5559j3rx56N+/P3JyctC+fXuMGTMGTz/9NNf74/zSzk7aMjZuNdG///znPwntbN++HdOnT0ePHj2QlZWFrl274jvf+Q7+/ve/SzxTtRw9ehTPPvsspk+fjkGDBiE3NxdZWVno0aMHvv3tb+PPf/5zUhtOn49O+7QOOGlHGc9PwB/39vbt27Fo0SJcdtllGDBgADp06ICMjAx06NABo0ePxi9/+UscO3YsoQ0tn5GMkE5ZWRkrLCxkABgAFgqFWFZWVvTvYcOGsWPHjnldTU959tlnGQAWDAZZ586dLf/985//jJv/1VdfZaFQKNqmbdq0YcFgMPr3D3/4QxYOh10+K/mUlJREz6nlv4ULFybNv3XrVtahQ4donry8PJaenh79e9KkSayurs4yv5/a2UlbLly4kAFgGRkZCftraWmppY2nnnqqWdsXFBSwQCAgdD11IPYcALDs7GyWm5vb7LvJkyez6urquPmdPh+d9mldcNKOTp+fjPnn3p49e3ardszPz2/2XceOHdlbb70VN7+uz0gSZ5JpaGhgQ4cOZQBY165d2RtvvMEYY6ypqYmtXLky2mkuueQSj2vqLZGHS+/evYXz7tu3L/oQGz16NPvkk08YY4xVVlaye+65J3pT/OpXv5Jca/cpKSlh7dq1YxMmTGDz589nzz//POvSpQvXYH7ixIlo2gEDBrD33nuPMcZYXV0de/TRR1lGRgYDwH784x/Hze+3dnbSlhFxNnbsWFtlv/XWWywtLY0BYN/+9rfZp59+yhhj7MiRI+yGG26ItuULL7xgy76bAGAjR45kjz/+ONu7d2/0+9LSUvajH/0oei7Tp09vldfp89Fpn9YJJ+3o5PnJmL/u7eLiYvbggw+yt99+mx0/fjz6fWVlJSsuLmadOnViANhpp53GTpw40Syvzs9IEmeSefrpp6MXJJ5S/9Of/hQ9/uabb3pQQz1w8nCZPn06A8C6dOnS7GaMcP3110d/wZjuoWxsbGz1Xe/evbkExV133cUAsJycHLZv375Wx++77z4GgKWlpUUfKrH4rZ2dtKVTcXb++eczAGzo0KGsvr6+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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spyro.plots.plot_shots(Wave_obj, contour_lines=100, vmin=-np.max(last_pressure_data), vmax=np.max(last_pressure_data), show=True)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "firedrake", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebook_tutorials/simple_forward_exercises.ipynb b/notebook_tutorials/simple_forward_exercises.ipynb new file mode 100644 index 00000000..26c4e3ef --- /dev/null +++ b/notebook_tutorials/simple_forward_exercises.ipynb @@ -0,0 +1,884 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simple forward - exercises" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial was prepared by Alexandre Olender. If you have any questions, please email: olender@usp.br\n", + "\n", + "This tutorial uses Spyro's `AcousticWave` class to solve the acoustic wave equation. Our main objective is to familiarize you with the initial dictionary inputs and is a continuation of the **simple forward tutorial**.\n", + "\n", + "In this tutorial, we will create a 4 km wide domain with a depth of 1.5 km. A water layer will be added (for the first 400 meters), and the outgoing wave will be absorbed by PML layers. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Code in this cell enables plotting in the notebook\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We begin by making Spyro available in our notebook. This is necessary for every python package." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "firedrake:WARNING OMP_NUM_THREADS is not set or is set to a value greater than 1, we suggest setting OMP_NUM_THREADS=1 to improve performance\n" + ] + } + ], + "source": [ + "import spyro # ANSWER\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we begin to define our problem parameters. This can be done using a Python dictionary.\n", + "\n", + "The first dictionary deals with basic finite element options. Please select triangles with 4th-order lumped elements." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary = {}\n", + "dictionary[\"options\"] = {\n", + " \"cell_type\": , # ANSWER\n", + " \"variant\": , # ANSWER\n", + " \"degree\": , # ANSWER\n", + " \"dimension\": , # ANSWER\n", + "}\n", + "dictionary[\"parallelism\"] = {\n", + " \"type\": \"automatic\", # options: automatic (same number of cores for evey processor) or spatial\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us define our mesh paramters based on the problem description." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"mesh\"] = {\n", + " \"Lz\": , # ANSWER\n", + " \"Lx\": , # ANSWER\n", + " \"Ly\": 0.0, # thickness in km - always positive\n", + " \"mesh_file\": None,\n", + " \"mesh_type\": \"firedrake_mesh\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We also have to define our acquisition geometry for receivers and sources. Source-related properties such as type, location, frequency, and delay are among those available. Point receiver locations also have to be stated. Here, we can input locations as lists of coordinate tuples; however, when we have many sources or receivers arranged in an equal-spaced array, the create_transect method simplifies this input. \n", + "\n", + "Please add a single source in the middle of the width and 10 meters down. Our receivers should be organized in an array containing 300 equally spaced alongside the end of the water layer." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"acquisition\"] = {\n", + " \"source_type\": \"ricker\",\n", + " \"source_locations\": , # ANSWER\n", + " \"frequency\": 5.0,\n", + " \"receiver_locations\": , # ANSWER\n", + "}\n", + "dictionary[\"time_axis\"] = {\n", + " \"initial_time\": 0.0, # Initial time for event\n", + " \"final_time\": 5.0, # Final time for event\n", + " \"dt\": 0.0005, # timestep size\n", + " \"output_frequency\": 400, # how frequently to output solution to pvds - Perguntar Daiane ''post_processing_frequnecy'\n", + " \"gradient_sampling_frequency\": 1, # how frequently to save solution to RAM - Perguntar Daiane 'gradient_sampling_frequency'\n", + "}\n", + "dictionary[\"visualization\"] = {\n", + " \"forward_output\": True,\n", + " \"forward_output_filename\": \"results/forward_output.pvd\",\n", + " \"fwi_velocity_model_output\": False,\n", + " \"velocity_model_filename\": None,\n", + " \"gradient_output\": False,\n", + " \"gradient_filename\": \"results/Gradient.pvd\",\n", + " \"adjoint_output\": False,\n", + " \"adjoint_filename\": None,\n", + " \"debug_output\": False,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As mentioned earlier, we will add absorbing boundary conditions with a pad length of 0.5 km." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"absorving_boundary_conditions\"] = {\n", + " \"status\": , # ANSWER\n", + " \"damping_type\": \"PML\",\n", + " \"exponent\": 2,\n", + " \"cmax\": 4.5,\n", + " \"R\": 1e-6,\n", + " \"pad_length\": , # ANSWER\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now create our acoustic wave object." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parallelism type: automatic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/olender/Development/tutorials/spyro-1/spyro/io/model_parameters.py:610: UserWarning: No velocity model set initially. If using user defined conditional or expression, please input it in the Wave object.\n", + " warnings.warn(\n", + "/home/olender/Development/tutorials/spyro-1/spyro/solvers/wave.py:85: UserWarning: No mesh file, Firedrake mesh will be automatically generated.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "Wave_obj = spyro.AcousticWave(dictionary=dictionary)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For our first test, we will use a refined firedrake-generated structured mesh. For the reason seen at the end of this tutorial, this option is not recommended for large problems." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "Wave_obj.set_mesh(mesh_parameters={\"dx\": 0.05})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can use Firedrake's objects in Spyro to define conditionals and functions. Since we want a velocity model with layers, let us generate it using Firedrakes capabilities. The `conditional` class is defined in the Unified Form Language with documentation available at: https://fenics.readthedocs.io/projects/ufl/en/latest/api-doc/ufl.html#module-ufl.conditional. For example, please look at many available Firedrake demos, such as https://www.firedrakeproject.org/demos/DG_advection.py.html.\n", + "\n", + "Please add a water layer with a 400-meter depth and a velocity of 1.5 km/s. Afterward, the velocity is 2.5 km/s, with the exception of an ellipsoidal salt layer. The Salt layer has a velocity of 4.5 km/s, center at point (-1.0, 3.0), major axis parallel to x of 2x0.5, and minor axis of 2x0.2. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from firedrake import conditional\n", + "\n", + "z = Wave_obj.mesh_z\n", + "x = Wave_obj.mesh_x\n", + "\n", + "vp = conditional() # ASNWER\n", + "\n", + "Wave_obj.set_initial_velocity_model(conditional=vp, output=True, dg_velocity_model=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before proceeding, let us see if our experiment was set up correctly. I have added the ABC points just to mark them in the plot." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File name simple_forward_exercise_model.png\n" + ] + }, + { + "data": { + "image/png": "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He94h+644w49/vjjevzxx3XHHXfoHe94h8477zz98Ic/tB2zpDmOowNdEX/FihV6+umni5io/Nx111268847xywijjnmGK1du9ZCqpktajsAAACYHpdffrmuu+46nXvuufut++AHP6gVK1bo0ksv1ec//3kL6crH0UcfPe46vwID/g455BA98sgjWrlypfL5vH70ox/JcRxJUi6XUzKZtJxw5qGAAACgTG3fvl3HHnvsuOuPPfZYtbe3FzFRebryyiu1ZMkS2zHK2pIlS7R69WqdeOKJOv3003XVVVeprq5OF198sd75znfajjfjUEAAAFCmjjrqKH3/+9/XddddN/KN7V7GGH3/+9/XsmXLLKUrHytXruR5LIIFCxboj3/8oz72sY/pLW95i4wxWrBggR544AHb0WYcCggAAMrU1VdfrVNOOUW//e1v9d73vldz5syRtPvMxIMPPqjBwUHdf//9llOWtvPOO08NDQ22Y5St66+/XrW1tSP3W1pa9Mgjj2jNmjXq7OzUihUrGMJkgWMYmIci+vOf/6xDDz1UnufZjlKWeH6nF8/v9OM5LrydO3fqhhtu0GOPPTYyXKm5uVnHH3+8zj//fDU2NlpOCKDUcAYCRffm0+goLJ7f6cXzO/14jgurqalJ//AP/2A7BoAywmVcUVSLFy/WK6+8YjtG2VqyZAnP7zTi+Z1+PMcAEH4UECiaLVu26KyzztJ9991nO0pZeemll/TEE09IkqLRqA455BDLicrLT3/6U23btk3S2M/v/fffr40bN9qIVpb4NwwA4UcBgaLp7e3V3Xffrdtvv12nnXaatm/fbjtSWfjSl76kJ598ctz1P/jBD3TGGWcUMVF5Of/88/We97xHW7duHXP9bbfdpm9/+9tFTlVePv3pT2vz5s3jrv/Wt76l//N//k8REwEADoQCAkX38MMP64QTTtAxxxyje++913ackvfCCy/o3e9+98j9zs5Off3rXx+5f/zxx+u5556zkKx8LF68WKtWrRo5E7Gvj3/843rooYcspCofP/vZz9TZ2Tnu+rq6Ot1zzz1FTAQAOBCuwoSiWbNmjZYtW6ZsNitJevbZZ/XJT35SJ598si6//HJVVFRYTliaKisrtXbtWi1YsECStG7dOh111FHq7++XJG3evFmHHHKIhoaGbMYsWZFIRJs3b9bXvvY1Pfnkk3ro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SCSia6+91kY0TAKvZ3nauHGjPvzhD+uYY47RLbfcYjsOCmCqrykFxBgcx9GBrm67YsUKPf3000VMhKm66667dOedd475ofOYY47R2rVrLaTCZPF6lo+2tjYtWLBg5P6mTZt0/PHHj9w/+OCDtWHDBgvJMBm8nuUrEonowQcf1GWXXabzzjtP/f39tiNhiqbymlJAjOPoo4+W67pj/nz4wx9WW1ub7YiYgEMOOUSPPPKIfvOb3+jv//7vRxWIuVxOyWTSYjpMFK9n+aitrVVfX9/I/Z6eHg0PD8vzPEk64Jc5CB9ez/JljNERRxyhp556Sk1NTVqxYoWefPJJ27EwBVN5TSkgxnHllVfq17/+9Zg/v/rVr/TrX//adkRM0JIlS7R69Wrdf//9Ov3007Vu3Trt2rVLF198sd75znfajocJ4vUsD0ceeaRuvPHGkfu33367ampqdNttt0mSbrjhBr31rW+1FQ8TxOtZ/mKxmP71X/9V1157rc4++2z9y7/8i+1ImKLJvKZ0oh5DJBLRM888o2XLltmOggKIRCLatm2b5syZI2n3KfaPfexjevTRR2WM0YIFC/TAAw/okEMOsZwUQfB6lpeHHnpI73vf+/Se97xHruvq+eef189+9jOdeeaZqqysVH9/v+688069//3vtx0VAfB6lpdVq1bJGKOBgQE99dRTes973jNqfXd3t55//vmRM0wIv0K9ptHpDFmqzjvvPDU0NNiOgQK5/vrrVVtbO3K/paVFjzzyiNasWaPOzk6tWLGCIS8lhNezvKxatUoPPPCAfvGLXygej+vqq6/WoYceqmeffVaPP/64jjvuOB122GG2YyIgXs/yctRRR0mSOjo69PTTT2v58uX7bbNy5coip8JUFOo15QwEAAAAxvXKK69o2bJlymQytqOgQKb6mlJAjOHTn/60mpubuZY8AEyzVatWaf78+frZz35mOwoKgNezfBlj5DiO7RgooKm8pkyiHsPGjRu1bds22zFQIKtWrdJ5551nOwYKhNezvCxatEjz58+3HQMFwutZXvb9e0vxUB4K9ZoyB2IMDz30kO0IKKBFixapubnZdgwUCK9nefnJT35iOwIKiNezvPD3tvwU6jVlCBMAAACAwDgDMYbvfe97qqio0Je+9KWRZX/+85/3ax735ktfAQAm7vHHH9cPfvADPf7442pvb5ckNTc36/jjj9cXv/hF+nqUkK985Suqr6/X//pf/2tk2Z/+9Ce9+uqro7ZjGCJQ2jgDMYaDDz5YV1xxhT74wQ+OLPvxj3+sCy+8cGS8mDGG6x6XED6glA8+oJSXO+64Qx//+Mf1/ve/X+9973tH+nts375dDz74oO69917ddNNN+pu/+RvLSRHE4sWL9e///u86+eSTR5b94he/0Pnnn6/q6moZY9TT08P7Zwnh/bO8FOo9lAJiDFVVVXrhhRe0dOnSkWXPPvusPvKRj+i5557Tzp07dfDBB/MHsETwAaW88AGlvBx66KH67Gc/q6985Stjrr/yyiv14x//WK+88kqRk2Eyksmk1qxZo8WLF48se+GFF/TBD35QGzZs0M6dO9Xc3Kx8Pm8xJYLi/bP8FOw91GA/jY2N5qWXXhq17PHHHzctLS3GGGO2b99uHMexEQ2T8Ja3vMVcccUV466/4oorzKGHHlrERJiKRCJhXn/99VHLnn/+ebNw4UJjjDE7duwwrutaSIbJSCQSZu3ateOuX7t2rUkkEkVMhKloaWkxzzzzzKhlf/jDH0xra6sxhvfPUsP7Z/kp1Hsol3Edw5FHHqkf//jHo5b9+Mc/1hFHHGEpEaZi48aN+uu//utx1//1X/+1NmzYULxAmJL6+np1d3ePWrbvtyXGGBlOrJaMgw8+WDfffPO462+88UYdcsghRUyEqVixYoUuv/zykf8fc7mcvve97+noo4+2nAyTwftn+SnUeyiTqMdw6aWX6r3vfa/WrFmjo446Sk888YQee+wxPfzwwyPbcD3k0rH3A8o3v/nNMdfzAaW07P2AcuONN8p1XT6glLjvfe97OuOMM/S73/1OJ5988qghEvfff7+eeeYZ/frXv7YbEoH98z//s44//ni99a1v1dve9jY9++yz2rlzpx577DFJu987E4mE5ZQIivfP8lOo91DmQIzj4Ycf1jXXXKO2tjYddNBB+vKXv6yjjjpqZP2OHTs0e/ZsewER2P33368zzjhDK1asOOAHlFNPPdVyUgTxwgsv6Pjjj9eCBQv2+4By+OGHa+fOnVq4cKEGBwdtR0VAa9as0TXXXDPmJM2LLrpIhx9+uOWEmIi1a9fqP/7jP0bePy+88ELNnTvXdixMAu+f5adQ76EUEJgR+IBSXviAAgDFwftn+SnEeygFBAAAAIDAmEQNAAAAIDAKCAAAAACBUUAAAAAACIwCAgAAAEBgFBAAAAAAAqOAAIAZbtGiRbrqqqtCu799vfrqq2publZfX58k6YYbblBdXd20HCuoTCajRYsW6amnnrKaAwCKhQICAErU6aefrve///1jrlu9erUcx9ELL7xQ5FTSk08+qc9+9rMj9x3HKVg36Ysvvlhf+MIXVFNTU5D9+fn0pz+tf/qnfzrgNhUVFfrqV7+qf/zHfyxKJgCwjQICAErUBRdcoN/97nfasmXLfuuuv/56HXPMMTryyCOLnqupqUmVlZUF3++mTZt0991361Of+lTB9z2WfD6vu+++Wx/84Ad9t/3EJz6hRx99VC+//HIRkgGAXRQQAFCiPvCBD6ipqUk33HDDqOX9/f365S9/qQsuuECS9Oijj+qEE05QMplUa2urvvjFL2pgYGDc/W7atElnnHGGqqurlUqldNZZZ2n79u2jtvnNb36jt7/97UokEmpsbNSZZ545sm7fIUyLFi2SJJ155plyHEeLFi3Shg0b5LrufkN+rrrqKi1cuFCe542Z67bbbtOyZcs0b968cbPv3LlTxxxzjM4880yl02k9/PDDchxH999/v5YvX65kMqlVq1Zpx44duvfee3XYYYcplUrp4x//uAYHB0ft67HHHlMsFtPb3/52ZTIZXXTRRWppaVEikdDChQt12WWXjWxbX1+vd73rXbrlllvGzQYA5YICAgBKVDQa1bnnnqsbbrhBxpiR5b/85S+Vz+d1zjnn6LXXXtP73/9+feQjH9ELL7ygW2+9VY8++qguuuiiMffpeZ7OOOMMdXZ26pFHHtHvfvc7vf766zr77LNHtrnnnnt05pln6rTTTtOzzz6r3//+9zr22GPH3N+TTz4pafcZkba2Nj355JNatGiRTjrpJF1//fWjtr3++uv1qU99Sq479lvT6tWrdcwxx4z7fGzevFknnHCCjjjiCN1+++2Kx+Mj6775zW/qmmuu0WOPPabNmzfrrLPO0lVXXaWbbrpJ99xzjx544AFdffXVo/Z311136fTTT5fjOPq///f/6q677tJtt92mV199VTfeeONIcbTXscceq9WrV4+bDwDKhgEAlKxXXnnFSDL/9V//NbLshBNOMJ/85CeNMcZccMEF5rOf/eyox6xevdq4rmuGhoaMMcYsXLjQfP/73zfGGPPAAw+YSCRiNm3aNLL9yy+/bCSZJ554whhjzPHHH28+8YlPjJtp3/0ZY4wk86tf/WrUNrfeequpr683w8PDxhhjnn76aeM4jlm/fv24+122bJn53//7f49adv3115va2lqzdu1a09raar74xS8az/NG1v/Xf/2XkWQefPDBkWWXXXaZkWRee+21kWUXXnihOeWUU0bt++CDDzZ33323McaYL3zhC2bVqlWj9v1mP/jBD8yiRYvGXQ8A5YIzEABQwg499FC9853v1E9+8hNJ0rp167R69eqR4UvPP/+8brjhBlVXV4/8nHLKKfI8T+vXr99vf6+88opaW1vV2to6suzwww9XXV2dXnnlFUnSc889p/e+971Tyv2hD31IkUhEv/rVryTtvprSypUr9/tWf19DQ0NKJBJjLj/hhBP04Q9/WD/4wQ/kOM5+2+w7F2TOnDmqrKzUkiVLRi3bsWPHyP1XXnlF27ZtG/k9P/WpT+m5557TW97yFn3xi1/UAw88sN8xksnkfsOgAKAcUUAAQIm74IILdMcdd6ivr0/XX3+9li5dqve85z2Sds+HuPDCC/Xcc8+N/Dz//PP6y1/+oqVLl07qeMlkcsqZKyoqdO655+r6669XJpPRTTfdpPPPP/+Aj2lsbFRXV9d+y+PxuE466STdfffd2rp165iPjcViI7cdxxl1f++yfede3HXXXTr55JNHCpajjz5a69ev17e//W0NDQ3prLPO0kc/+tFR++js7FRTU9OBf3EAKAMUEABQ4s466yy5rqubbrpJP/vZz3T++eePfAt/9NFHa82aNTrooIP2+6moqNhvX4cddpg2b96szZs3jyxbs2aNuru7dfjhh0va/W3+73//+8D5YrGY8vn8fss/85nP6MEHH9SPfvQj5XI5ffjDHz7gfpYvX641a9bst9x1Xf385z/XihUrtHLlSm3bti1wtvHceeedOuOMM0YtS6VSOvvss/XjH/9Yt956q+644w51dnaOrH/ppZe0fPnyKR8bAMKOAgIASlx1dbXOPvtsXXzxxWpraxt1mdN//Md/1GOPPaaLLrpIzz33nP7yl7/ozjvvHHcS9UknnaS3ve1t+sQnPqFnnnlGTzzxhM4991y95z3vGZnAfOmll+rmm2/WpZdeqldeeUUvvviivvvd746bb9GiRfr973+v9vb2UWcQDjvsMB133HH6x3/8R51zzjm+ZzZOOeUUPf7442MWI5FIRDfeeKOWLVumVatWqb29/YD7OpAdO3boqaee0gc+8IGRZVdeeaVuvvlmrV27Vn/+85/1y1/+Us3NzaOa2K1evVrve9/7Jn1cACgVFBAAUAYuuOACdXV16ZRTTtHcuXNHlh955JF65JFH9Oc//1knnHCCli9frv/v//v/Rm2zL8dxdOedd6q+vl5/9Vd/pZNOOklLlizRrbfeOrLNiSeeqF/+8pe66667dNRRR2nVqlV64oknxs12xRVX6He/+51aW1v3+4b+ggsuUCaT8R2+JEmnnnqqotGoHnzwwTHXR6NR3XzzzXrrW986cqnWyfjNb36jY489Vo2NjSPLampqdPnll+uYY47R29/+dm3YsEG//e1vR64Y9fjjj6unp2e/YU0AUI4cY/a59h8AAEX07W9/W7/85S8Dd8z+4Q9/qLvuukv333//tGX64Ac/qHe/+9362te+FvgxZ599tpYtW6ZLLrlk2nIBQFhEbQcAAMw8/f392rBhg6655hp95zvfCfy4Cy+8UN3d3err61NNTc20ZHv3u9+tc845J/D2mUxGb3vb2/TlL395WvIAQNhwBgIAUHSf+tSndPPNN+tDH/qQbrrpJkUiEduRAAABUUAAAAAACIxJ1AAAAAACo4AAAAAAEBgFBAAAAIDAKCAAAAAABEYBAQAAACAwCggAAAAAgVFAAAAAAAiMAgIAAABAYBQQAAAAAAL7/wNUC0DZh4eXbQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spyro.plots.plot_model(Wave_obj,\n", + " filename=\"simple_forward_exercise_model.png\",\n", + " show=True,\n", + " abc_points=[(0.0, 0.0), (-1.5, 0.0), (-1.5, 4.0), (0.0, 4.0)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All we have to do now is call our forward_solve method." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/olender/Firedrake/main/firedrake/src/firedrake/firedrake/function.py:317: FutureWarning: The .split() method is deprecated, please use the .subfunctions property instead\n", + " warnings.warn(\"The .split() method is deprecated, please use the .subfunctions property instead\", category=FutureWarning)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving output in: results/forward_outputsn0.pvd\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/olender/Firedrake/main/firedrake/src/firedrake/firedrake/assemble.py:209: DeprecationWarning: create_assembly_callable is now deprecated. Please use assemble or FormAssembler instead.\n", + " warnings.warn(\"create_assembly_callable is now deprecated. Please use assemble or FormAssembler instead.\",\n", + "/home/olender/Firedrake/main/firedrake/src/ufl/ufl/utils/sorting.py:94: UserWarning: Applying str() to a metadata value of type QuadratureRule, don't know if this is safe.\n", + " warnings.warn(\"Applying str() to a metadata value of type {0}, don't know if this is safe.\".format(type(value).__name__))\n", + "/home/olender/Firedrake/main/firedrake/src/ufl/ufl/utils/sorting.py:94: UserWarning: Applying str() to a metadata value of type QuadratureRule, don't know if this is safe.\n", + " warnings.warn(\"Applying str() to a metadata value of type {0}, don't know if this is safe.\".format(type(value).__name__))\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Simulation time is: 0.0 seconds\n", + "Simulation time is: 0.2 seconds\n", + "Simulation time is: 0.4 seconds\n", + "Simulation time is: 0.6 seconds\n", + "Simulation time is: 0.8 seconds\n", + "Simulation time is: 1.0 seconds\n", + "Simulation time is: 1.2 seconds\n", + "Simulation time is: 1.4 seconds\n", + "Simulation time is: 1.6 seconds\n", + "Simulation time is: 1.8 seconds\n", + "Simulation time is: 2.0 seconds\n", + "Simulation time is: 2.2 seconds\n", + "Simulation time is: 2.4 seconds\n", + "Simulation time is: 2.6 seconds\n", + "Simulation time is: 2.8 seconds\n", + "Simulation time is: 3.0 seconds\n", + "Simulation time is: 3.2 seconds\n", + "Simulation time is: 3.4 seconds\n", + "Simulation time is: 3.6 seconds\n", + "Simulation time is: 3.8 seconds\n", + "Simulation time is: 4.0 seconds\n", + "Simulation time is: 4.2 seconds\n", + "Simulation time is: 4.4 seconds\n", + "Simulation time is: 4.6 seconds\n", + "Simulation time is: 4.8 seconds\n", + "Simulation time is: 5.0 seconds\n" + ] + } + ], + "source": [ + "Wave_obj.forward_solve()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In seismic imagining, the shot record is one of the most important outputs of the forward problem. It represents the pressure values recorded at every receiver and timestep. This is the data that we will use for inversion. Shot record data is saved inside the wave object as the receivers_output attribute." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "shot_record = Wave_obj.receivers_output" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us have a look at our shot record. For a better image, we will use 10% of the maximum value of the shot record as the maximum in our contour plot. The maximum value usually represents the direct wave, and we want more significant emphasis on the reflected waves." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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8iznnXeePmO07MuzCCHEjTfeqNuxWbNmeT4vrH0MW6dlI0g+hrWfQiSnbR88eNA1P1577TV9Ae4f/vCHBftktpGpE0/GhS/r6+t1A3Do0CHxm9/8RnTu3FkAh1fLTitr164VI0aMEA8//LBYs2aNXvkPHTok3n77bX2dL9gsyGhcjHH06NHio48+EkIcni5/5syZekNRYVFLLzQ2NoqtW7fqf3369BEAxJQpUwq27969u+A846KXQ4YMEcuXLxdCHF708qGHHhLl5eWeF71MSj4HycvXX39dnH322eJXv/qVvqCvEELs379fvPbaa2LEiBGuC4C++eab+sLAX/va18S///1vIYQQ27dv1w0soMbCwFOmTNHTO3v2bF/nhrWPYeu0TATNx7D2U4jktO21a9eKE044oU1eCCHEhg0bxJ133qk/Z21trdi8eXPB+TLbyNSJJyEOF2i/fv30ClxZWSmy2az+/cQTT5RazRcb43o/AERFRYXo1q2bqKioKNh+1VVXiQMHDlhe46WXXhKVlZX6sdXV1XrnpJ3r9otEFTTviNvfhAkT2py7fPly0bVrV/2YTp066SuFAxBjxowRzc3NtvdOWj4HycvFixcX7MvlcqJbt24F+VhWVib+53/+x/Hejz76qGjfvr1+TpcuXXTj6sWTKAPr168veOYePXo4/t1zzz1trhHWPoat0zIQJh+jsJ9CJKNtm/OivLxcdOvWTRc02l///v3FihUrLK8hq41MpXgSQohdu3aJW265RQwbNkx07NhRdOrUSZx88sli1qxZoqWlJe7kxcq+ffvET3/6U3HZZZeJIUOGiO7du4v27duLqqoqMXjwYHH11VeLN954w/U6q1evFhMnThT9+vXTDci5554rfvvb35bgKUpHGPEkhBBbtmwRN9xwgzjmmGNENpsVXbp0Eaeddpp49NFHxaFDh1zvn6R8DpKX27ZtE7NmzRIXX3yx+NznPidqa2tF+/btRefOncUJJ5wg/uu//kv84x//8HT/d955R1x22WWivr5elJeXix49eoiLLrpILFq0qEhPHC3mzsrtz04QhrWPYet03ITJx6jspxDqt+2Wlhbx7LPPiu9///ti+PDhoq6uTpSXl4tcLif69u0rLrjgAvHzn/9c7Nu3z/E6MtrIjBBCgBBCCCGEeCJVb9sRQgghhISF4okQQgghxAcU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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "vmax = 0.1*np.max(shot_record)\n", + "spyro.plots.plot_shots(Wave_obj, contour_lines=300, vmin=-vmax, vmax=vmax, show=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Take note of the total runtime when calling the forward solve. You can also look at your PC's RAM usage. For the example above, we used a Firedrake structured mesh of 50-meter elements. We can calculate DOFs excluding those in the PML layer and absorbing auxiliary variables:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Pressure DoFs: 53241\n" + ] + } + ], + "source": [ + "pressure_dofs = Wave_obj.function_space.dim()\n", + "print(f\"Pressure DoFs: {pressure_dofs}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also look at total DoFs. However, those only partially indicate future memory usage since the auxiliary variables are always zero-valued outside the PML and not always calculated." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total DoFs: 159723\n" + ] + } + ], + "source": [ + "total_dofs = Wave_obj.function_space.dim() + Wave_obj.vector_function_space.dim()\n", + "print(f\"Total DoFs: {total_dofs}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now create a new problem with a wave-adapted mesh like the above. Before that, let us save the previously made velocity model as a segy file. The last velocity Firedrake Function used is stored in attribute `c` of a Wave object. To convert to segy, we will first store velocity values in a 10-meter-spaced grid." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/olender/Firedrake/main/firedrake/lib/python3.8/site-packages/segyio/utils.py:18: RuntimeWarning: Implicit conversion to contiguous array\n", + " warnings.warn(msg, RuntimeWarning)\n", + "/home/olender/Firedrake/main/firedrake/lib/python3.8/site-packages/segyio/utils.py:23: RuntimeWarning: Implicit conversion from float64 to float32 (narrowing)\n", + " warnings.warn(msg.format(x.dtype, dtype), RuntimeWarning)\n" + ] + } + ], + "source": [ + "_, _, zi = spyro.io.write_function_to_grid(\n", + " Wave_obj.c,\n", + " Wave_obj.function_space,\n", + " 0.01)\n", + "spyro.io.create_segy(zi, \"velocity_models/tutorial.segy\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "new_dictionary = dictionary.copy()\n", + "new_dictionary[\"mesh\"][\"mesh_type\"] = \"SeismicMesh\"\n", + "new_dictionary[\"synthetic_data\"] = {\n", + " \"real_velocity_file\": \"velocity_models/tutorial.segy\",\n", + "}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is very easy to generate an automatic mesh. We only need the cells per wavelength parameter from [Cite Spyro paper]. For a 4th-order element, it is 2.67." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parallelism type: automatic\n", + "Mesh sizes will be built to resolve an estimate of wavelength of a 5.0 hz wavelet with 2.67 vertices...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/olender/Development/tutorials/spyro-1/spyro/solvers/wave.py:89: UserWarning: No mesh found. Please define a mesh.\n", + " warnings.warn(\"No mesh found. Please define a mesh.\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Enforcing minimum edge length of 112.35955056179775\n", + "Enforcing maximum edge length of 10000.0\n", + "Enforcing mesh size gradation of 0.15 decimal percent...\n", + "Including a 500.0 meter domain extension...\n", + "Using the pad_style: edge\n", + "entering spatial rank 0 after mesh generation\n" + ] + }, + { + "data": { + "text/html": [ + "
Warning: Appending zeros to replace the missing physical tag data.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;33mWarning:\u001b[0m\u001b[33m Appending zeros to replace the missing physical tag data.\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Warning: Appending zeros to replace the missing geometrical tag data.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;33mWarning:\u001b[0m\u001b[33m Appending zeros to replace the missing geometrical tag data.\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Warning: VTK requires 3D points, but 2D points given. Appending 0 third component.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;33mWarning:\u001b[0m\u001b[33m VTK requires 3D points, but 2D points given. Appending \u001b[0m\u001b[1;33m0\u001b[0m\u001b[33m third component.\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "Wave_obj_new = spyro.AcousticWave(dictionary=new_dictionary)\n", + "Wave_obj_new.set_mesh(mesh_parameters={\"cells_per_wavelength\": 2.67})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let us look at the new mesh:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n", + "WARNING:matplotlib.font_manager:findfont: Font family 'Times New Roman' not found.\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABGoAAAKXCAYAAADaa6z7AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACrwElEQVR4nOzdd5wURcL/8e8ssIGwBMk5SFDBQ5BgeCSd4eQUAxg5xXDqYc6Jezyznmc6FdQ7BU8xYuLUMwKC4oOCmBE9gqBgIO0SNgA7vz/4zbizO7s7oUNV9+f9eu1LmdTV3dVV1d+p7olEo9GoAAAAAAAA4LscvwsAAAAAAACAXQhqAAAAAAAADEFQAwAAAAAAYAiCGgAAAAAAAEMQ1AAAAAAAABiCoAYAAAAAAMAQBDUAAAAAAACGIKgBAAAAAAAwBEENAAAAAACAIQhqAABAaEUiEQ0fPtzvYriua9eu6tq1q9/FcNyECRMUiUS0cuVKv4sCAIBjCGoAALDMokWLFIlENGTIkKTPP/XUU4pEIopEIlqxYkW150tKSpSfn6+GDRuqrKzM7eIiiWnTpikSiWjatGl+FwUAABiGoAYAAMvss88+at68uRYtWqTi4uJqz7/zzjuKRCKSpFmzZlV7/v3331dZWZkOPPBA5eXluV5eAAAApI6gBgAAy+Tk5Gj48OHauXOn3n333WrPz5o1S8OHD9duu+2WNKiJPTZq1CjXywoAAID0ENQAAGChWMhSNYhZuXKlVqxYoVGjRmnYsGGaPXt2tfdWDWq++eYbXXXVVdp3333VqlUr5eXlqUuXLjrrrLP0/fffJ7z36aefViQS0cUXX5y0XGVlZWrevLnatWunHTt2JDz31FNPacSIEWrWrJny8/O1xx576Kabbkrr8qt0yhpTXl6uG2+8UT169FBeXp66deumSZMm1bjcNWvW6IYbbtABBxygtm3bKjc3V+3bt9dJJ52kr776qtrrV65cqUgkogkTJujrr7/WUUcdpRYtWqhRo0Y68MAD9eabbya8fvjw4TrttNMkSaeddlr8MrWq91rZsWOHJk+erKFDh6qwsFANGzbUPvvso/vvv18VFRXVyhGNRnX//fdrr732Un5+vjp06KDzzjtPRUVFqW7euNi9e3766SedfvrpatOmjRo1aqT9999f8+bNkyRt3bpVl19+ubp06aK8vDzttddeeu6552r8zHT2/7x583TEEUeoY8eOysvLU9u2bTV06FBdf/31NX7+Qw89pH79+ik/P19t2rTRWWedldG6AwDgt0g0Go36XQgAAJCeJUuWaM8991S/fv302WefxR9/5JFHdOaZZ2r+/PlatGiRzj//fH355Zfac889JUnFxcVq0aKFCgsLtW7dOuXk5Oi2227TbbfdphEjRqhTp07Kzc3Vl19+qTfeeENt2rTRwoUL1aFDB0lSaWmp2rZtq7y8PP3www+qX79+QrmeffZZHX/88br00kv1t7/9Lf746aefrqlTp6pjx4465JBD1KxZM/3f//2f5s+fr+HDh+utt96q9lnJpFNWaVd4cfTRR+vll19Wjx49NGbMGJWXl+v555/XoEGDNHPmTA0bNkxz5syJv+fpp5/W6aefrhEjRqhr165q3Lixvv32W73yyivKzc3V+++/r9/85jfx169cuVLdunXTQQcdpM8++0z9+vXTAQccoLVr1+qZZ55ReXm5nnzySR1//PGSdt2f5qWXXtLLL7+sMWPGqH///vHPuuiii9SsWTNt375dRxxxhN544w317t1bw4cPV35+vmbPnq3PPvtM48eP1+OPP56wbS688EL9/e9/V7t27TR27Fg1aNBAL7/8spo3b64ffvhBubm5Kd90NxKJ6De/+Y22bNmiJk2aaNiwYdqwYYOefvpp1a9fXx988IHOPvtsbdiwQQcffLC2b9+up556Slu3btX8+fM1dOjQhM9LZ/+//vrrGj16tAoLC3XkkUeqQ4cO2rBhg5YsWaKvv/5aP/30U/xzJ0yYoMcee0zjxo3TG2+8oSOOOEJt2rTR7NmztXjxYo0YMSLprDIAAIwWBQAAVmrXrl00EolEf/755/hjJ510UrRx48bR7du3R7/44ouopOh9990Xf37mzJlRSdGjjz46/tj3338fLS0trfb5b7zxRjQnJyd6zjnnJDx+1llnRSVF//3vf1d7z+GHHx6VFP3ss8/ij02dOjW+zG3btiW8/rrrrotKit5zzz0prXO6ZZ0+fXpUUnTo0KHRkpKS+OPr16+Pdu/ePSopOmzYsIT3/PTTT9Hi4uJqy/jkk0+ijRo1ih522GEJj69YsSIqKSopetlllyU899FHH0Xr168fbdasWbSoqCj+eGybTJ06Nel6xrbLeeedF92xY0f88R07dkRPP/30qKToSy+9FH/8/fffj0qK9ujRI7p+/fr44yUlJdGhQ4dGJUW7dOmSdFnJxNbn7LPPju7cuTP++L/+9a+opGjz5s2jv//97xO26dy5c6OSokcddVTCZ6W7/4855piopOgnn3xSrVy//PJLwr9PPfXUqKRop06dot9991388e3bt0f/53/+JyopumDBgpTXGwAAExDUAABgqfHjx0clRZ955pn4Y+3atYv+7ne/i/+7devWCaHMRRddFJUUvf/++1NaRr9+/aLdunVLeCwWCowdOzbh8bVr10br1asX3WeffRIe79+/f7R+/frRjRs3Vvv8HTt2RHfbbbfooEGDUipPumX97W9/G5UUnTVrVrXXxwKEqkFNbY444ohoXl5etLy8PP5YLKhp2rRp0oAnFiZMmzat2rKTBTU7d+6MtmjRItq2bdvo9u3bqz2/cePGaCQSiY4bNy7+2JlnnhmVFH300UervX727NkZBTUNGzastj47duyI1q9fPyopumzZsmrv69q1a7Rr164Jj6W7/2NBzdKlS+ssZ2zb/uMf/6j23KOPPlotqAQAwAZ1zzEGAABGGjlypJ544gnNmjVLxx13nJYsWaK1a9cm3D8mdllJRUWFcnJykt5IOBqNavr06Zo2bZo+/fRTbdy4UTt37ow/n5ubm7Dc/fffX7169dK///1vbdy4Uc2bN5ckTZ8+XTt37tSECRPir922bZs+/fRTtWzZUvfcc0/S9cjLy9OSJUtSWud0y/rxxx8rJydHBx54YLXPGj58eI3LefXVV/Xggw9q4cKFWrduXbX77axbt07t2rVLeGzAgAFq0qRJ0uU89thjWrx4sU499dQ61/Gbb77Rhg0b1LNnT910001JX1NQUJCwzT7++GNJ0rBhw6q99sADD1S9evXqXG5VvXr1qrY+9erVU5s2bbR161Z179692ns6dOigBQsWxP+dyf4/+eST9cILL2jIkCE6/vjjNWLECB1wwAHq2LFjjWXdd999qz3WqVMnSdLGjRtrXU8AAExDUAMAgKViYcs777yT8N+RI0fGXzN8+HA9++yzWrx4sTp37qzPP/9cHTp0UJ8+feKvueSSS3TPPfeoXbt2OvTQQ9WhQwcVFBRI2nU/le+++67ask899VRde+21evrpp/WnP/1JkvTYY4+pQYMGOumkk+Kv27hxo6LRqH755ZdabwSbqnTLWlRUpBYtWqhBgwbVPqtt27ZJl3HvvffqoosuUvPmzXXwwQerc+fOatiwoSKRiF566SV9+umnSW+A26ZNm6SfF1tOqje2Xb9+vSTp22+/rXWbbdmyJf7/sc9OVob69eurZcuWKS27sqZNmyZ9vH79+rU+VznUymT/H3PMMXrllVd055136tFHH9VDDz0kSRo4cKBuvfVWHXzwwdXe06xZs6RlkZQQ5AEAYAOCGgAALNW5c2f16NFD//3vf7V69WrNmjVLzZo10z777BN/zYgRIyTt+qWnLl26KBqNJsym+fnnn/X3v/9dffv21fz586vNoHjqqaeSLvsPf/iD/vznP+uxxx7Tn/70Jy1evFiff/65xowZkxAKxE7o99lnn/isj0xlUtamTZtqw4YN2r59e7Ww5scff6z2+h07dugvf/mL2rZtq48//rjarJkPPvigxvJVvsltsuXUFG4kK7MkHX300XrhhRfSes9PP/1UbabLjh07tG7dulpnpLgl0/0/evRojR49Wlu3btWCBQv0yiuvaMqUKfr973+vxYsXx2+ODQBAEPHz3AAAWCwWurz99tuaM2eOhg0bppycX7v3Pn36qG3btpo1a1bSy56WL1+uiooKHXLIIdWCj++//17Lly9PutxOnTpp5MiRWrBggZYuXarHHntMkqpd2tO4cWPttdde+vLLL7Vhw4as1jWTsg4YMEAVFRV67733qj1X+ZeeYtatW6dNmzZp//33rxbSbNmypdaw4eOPP9bmzZtrXE7lAC12KVKy2R59+vSJ/yrS9u3ba1xeZQMGDJAkvfvuu9Wee++993ybVZLt/m/UqJFGjhypu+66S9dcc43Ky8v1n//8x4WSAgBgDoIaAAAsFrvM6e6779bGjRvjM2gqGzFihObNm6c333xTUmJQ07VrV0nVT+a3bNmiP/7xj9XuzVJZ7F40jzzyiJ566im1bNlSv//976u97pJLLlF5eblOP/10bdq0qdrzGzduTGm2RSZlPe200yRJ1157rUpLS+OPb9iwIen9X1q3bq2GDRtq0aJFCZcWbd++XRdeeKHWrVtXY/mKiop0ww03JDy2cOFCTZ8+XU2bNtXRRx8df3y33XaTJK1atara59SvX1/nn3++1q5dqwsuuEAlJSXVXrN27Vp99dVX8X/H9sXNN9+cEIiUlpbq6quvrrHMXkh3/8+dOzfpvozNWGrYsKFrZQUAwARc+gQAgMVGjhypSCSizz//PP7vqkaMGKGnnnpKK1asUO/evdWhQ4f4c23bttUJJ5ygp59+Wv3799chhxyioqIivfXWW8rPz1f//v31ySefJF320UcfrcLCQt1zzz3avn27zj///KT3gjn99NO1aNEiTZ48WT169NChhx6qzp07a8OGDVqxYoXmzp2r0047TQ8++GCt65pJWU888UQ988wzmjlzpvr27asxY8Zo+/btmjFjhgYNGqRly5YlvD4nJ0cXXHCBbrvtNvXr109jxoxReXm5Zs+erQ0bNmjEiBGaPXt20vIddNBB+uc//6kFCxbogAMO0Nq1a/XMM8+ooqJCDz30kAoLC+Ov3W+//dSwYUPdc889Wr9+ffw+Nueff76aNm2qP//5z/r000/14IMP6t///rdGjhypDh066Oeff9a3336r999/XzfffHP8EqADDjhA559/vu677z717dtXY8eOVYMGDfTyyy+refPm1WYHeSnd/X/BBRfohx9+0AEHHKCuXbsqNzdXixYtil++d8IJJ/i2LgAAeMLX35wCAABZ23vvvaOSoi1btoxWVFRUe/7bb7+NSopKik6cOLHa81u3bo1ec8010R49ekTz8vKiHTt2jE6cODG6bt266LBhw6K1DRfOOOOM+GcvXLiw1nL++9//jo4ePTraqlWraIMGDaJt2rSJDho0KHrttddGlyxZktK6ZlLWsrKy6PXXXx/t1q1bNDc3N9qlS5foNddcEy0tLU3689zbt2+P3nnnndE99tgjmp+fH23Tpk10/Pjx0ZUrV8Z/DnrFihXx18d+nvvUU0+NfvXVV9Ejjzwy2qxZs2hBQUF0//33j77++utJ1+U///lPdOjQodFGjRrFt2Hlz62oqIj+61//io4cOTLavHnzaIMGDaLt27ePHnDAAdGbb745umrVqoTPq6ioiN53333RPn36RHNzc6Pt2rWLTpw4Mbpp06Zoly5d0v557pp+try2z6qtvqS6/5955pnoCSecEN19992jjRo1ijZp0iS61157Ra+55prozz//nPCZyfZHTOxnya+77rpUVhkAAGNEotFo1Pt4CAAAIBhWrlypbt266dRTT9W0adP8Lg4AALAc96gBAAAAAAAwRGCCmrKyMl155ZVq3769CgoKNGTIEL311lspvfeHH37Qcccdp2bNmqmwsFBjxoyp8VcuHnnkEe2xxx7Kz89Xz549dd999zm5GgAAAAAAIMQCE9RMmDBBd911l04++WTde++9qlevng4//PCkP8dZ2ZYtWzRixAi9++67uuaaa3T99ddr8eLFGjZsmNavX5/w2oceekhnnnmm9tprL913333ab7/9dMEFF+j22293c9UAAAAAAEBIBOIeNR9++KGGDBmiO+64Q5dddpmkXT9H2bdvX7Vu3Vrz58+v8b1//etfdeWVV+rDDz/UoEGDJElff/21+vbtqyuuuEK33HKLJKmkpESdOnXS0KFD9corr8TfP378eL300ktavXq1mjdv7uJaAgAAAACAoAvEjJoZM2aoXr16Ouuss+KP5efn64wzztAHH3yg1atX1/reQYMGxUMaSerTp49GjRqlZ599Nv7Y7NmztX79ek2cODHh/eeee662bt2qV1991cE1AgAAAAAAYRSIoGbx4sXq1auXCgsLEx4fPHiwJOmTTz5J+r6Kigp99tln2nfffas9N3jwYC1btkybN2+OL0NStdcOHDhQOTk58ecBAAAAAAAyVd/vAjhh7dq1ateuXbXHY4+tWbMm6fs2bNigsrKyOt/bu3dvrV27VvXq1VPr1q0TXpebm6vddtutxmVIu250XFZWFv93RUWFNmzYoN12202RSKTuFQQAAAAAAJ6LRqPavHmz2rdvr5wcb+a6BCKoKSkpUV5eXrXH8/Pz48/X9D5JKb23pKREubm5ST8nPz+/xmVI0q233qrrr7++ljUAAAAAAACmWr16tTp27OjJsgIR1BQUFCTMWIkpLS2NP1/T+ySl9N6CggKVl5cn/ZzS0tIalyFJV199tS655JL4v4uKitS5c2cNGXaV6tfPr/F92Sr4aq1rnx1Tsmf12Uhu8GJdJGntkV08WU6qmv331zrn1TZwg9v1xOZt4wa2N5A+N48bjhnUhHoHIEy8Ond02o4dpVrw7m1q0qSJZ8sMRFDTrl07/fDDD9UeX7t2VwfVvn37pO9r0aKF8vLy4q+r7b3t2rXTzp079fPPPydc/lReXq7169fXuAxp14ydZLN26tfPdzWoqZ+TfAaQo8twsfwxBV/8IHmwLpLU6ZVd+33Nsd08WV5tmi8tlypt3+17d9u1LSxT0reD6w2NF3XdJm4fl2xvBJHt/THsRL0DEBZenBO4zcvblgTiZsL9+/fXN998o+Li4oTHFyxYEH8+mZycHPXr108LFy6s9tyCBQvUvXv3eGoW+4yqr124cKEqKipqXEaQlfTt4HcRXNP++RW+Lbv50vJdIU0SQd7mcI7bgR71EEFDnUbQ2PjFDgDgV4EIasaOHaudO3fq4Ycfjj9WVlamqVOnasiQIerUqZMkadWqVfr666+rvfejjz5KCGCWLl2qWbNmady4cfHHRo4cqRYtWmjKlCkJ758yZYoaNmyo0aNHu7Fq8FH751d4HtjUFNBUxgkFANiDE2bUhP4cQFjQ3qXP9tlHkqQhQ4Zo3Lhxuvrqq/Xzzz9r991312OPPaaVK1fqkUceib/ulFNO0bvvvqtoNBp/bOLEifrHP/6h0aNH67LLLlODBg101113qU2bNrr00kvjrysoKNCNN96oc889V+PGjdOhhx6qefPm6YknntDNN9+sFi1aeLrOdQnKwNCE9Wj//ArXL4VKJaCxkVeNcknfDkbUlTBhmyMoGDwCAADTBCKokaR//etf+vOf/6zHH39cGzdu1N57761XXnlFBx10UK3va9KkiebMmaOLL75YN910kyoqKjR8+HDdfffdatWqVcJrJ06cqAYNGujOO+/UzJkz1alTJ91999268MIL3Vw1I4VtYBubWeNGYJNJSMNJMgAA9nJzHMX4AADsF4lWnl4CTxQXF6tp06Y6YNRfXLuJXFDuUWHqYMOpwCbbmTSmbp8YLwM907eFH9ze/mxzBAEnzPAD9Q5AWAThC/4dO0r1/jt/UVFRkQoLCz1ZZiDuUYNgMnmgke29a2q7YXA6TG74TC4bnME+hu2ow/AD9Q4AUBeCGqSNAcYumd5sOKj3o/Eb9RIAAABAEBDUAFlKNaxxahZNVQQU8BP1D7bi0kAEEfUOgEkYJ2aOoAZwQF2za9yeRWNaI2haecKKATsAmIX+EQCQCoIapCXsNxGuS9Wwxq1ZNMkw+GMb+IXtDttQZwEAcBd9bXYC8/PcgCliYQ2NEwCEk61fNsBdXG4HAEgVM2qAADEhHDKhDPiVVwN39jtsQV0FAACmI6hByrjsKT1+rQcnIQAAmIW+GUCY0OZlj6AGcBFhjffCvO5+Y9sDwfmyAXah3gFAsBDUICWcgGUuTIMn6gkAk9FGAQDgLvpaZxDUwChBDTX8WC8aScR4Wf+odwCQiHYRAJAughrAI4Q1AOAfL9rDoH7ZAAAAvEVQ46OCr9b6XYSUcLJvN6/2n0n1xKSyhBHbHwB2ISAEECaMAZ1T3+8ChF3lztXUil3wxQ/Gls02bEukKlk9yWYwTt1DmFH3AQCATQhqDOJUaOPGNyvJPtPpgW9YvhHy44S5pG8H17cvQYD7qm7fsBwzgA04HlEVfSKAsLFhEoItCGoMFavkJldwL8KboCKscZ8X6+s3ghugbqa0SYAbaPcBmKqm9ol+OTUENYazLZUkvEmdSaGGk4K6XjZw+nKpunCCYCa3jj/2NwAAyBYBTmoIaixiW2gTk0p4E9YTAK9DDa9mmRDWZMfJbZfpZ4X1mLQdx111XgwIOV6QDF9eAUDqCHASEdRYytbQJoZBbfD5GdZQv+rGNkLY1XYM2Nivwg5Ohje04wDCIKwBDkFNANhwPxvULKizaiR/whoGrnVjGwWTm8da2OpMXetbdVuH4Z5YcE9YT0IAIBtBPwcmqAkQBon2CvKlQl6tG/U/NWwnIHvJBoeENXAal04BQHjl+F0AALt4OcAP0iyXgi9+4OQIocfJm/eSbXP2A9wW6/Po9wAg2AhqAIMEeeDl9LoFdaDKiR5ME8TjzE0lfTtwHAMAgKwQ1AAh5ceJhBMnfEENaIBMEQp4L5Vtzn4BAACZIqgBDBP0ECLT9SOgAWCCdAIYwhoAAJAJghrAQF4FEjb8fDYBjXPYjsHj9jFMnckel0IBAIB0EdQAhgr6CVJd60dAA8A02QQuhDUAACBVBDVAyPl58pAsiCGgAVLDib+3nNje7DMAAJAKghrAYGEILGLrSEDDSRzMEvbj0S1cCgUAAOpCUAPA95MGTgjdxzYOFr+P2bBxY3uzDwEAQE0IagAAQAKCvV+5GagQ1gAAgGQIagDDBf0XoACkh2M1WLgUCgAAVEVQAwAAkISXAQphDQAAiCGoARDHiYJ/3Nz2XMYSHF4co9QX/zC7BgCA9AR13EJQA1ggqA0QAJjKz8CEsAYAgHAjqAGQgBMEwEwcm94xYVubUAYAAOAPghoAMEDBFz8wcwq+ow6ahbAGABBEsUt96edqFoigZtOmTTrrrLPUqlUrNWrUSCNGjNDHH39c5/sqKio0bdo0HXnkkerUqZMaNWqkvn376qabblJpaWm110cikaR/t912mxurZRxbDiZbypkuL0+ggrj9bMGJMpLhmPQO2xoAAO/Q7yZX3+8CZKuiokKjR4/Wp59+qssvv1wtW7bU5MmTNXz4cC1atEg9e/as8b3btm3TaaedpqFDh+qcc85R69at9cEHH+i6667TO++8o1mzZikSiSS85+CDD9Ypp5yS8Ng+++zjyrqZwJYDJ1k5S/p24KQ3C2w//8S2uxPHH/sQXqlaX22se6b1eTZuQwAAalPTeZtkZr9X8MUP2lFR7vlyrQ9qZsyYofnz5+u5557T2LFjJUnHHXecevXqpeuuu05PPvlkje/Nzc3V+++/r/333z/+2B//+Ed17do1Htb89re/TXhPr169NH78eHdWxhCmDVRrYks5bWZyoxkGBV/8QD2HZzI5zqmf7qDNBQCEkUnnHn6XIRBBTZs2bXTMMcfEH2vVqpWOO+44PfHEEyorK1NeXl7S9+bm5iaENDFHH320rrvuOi1ZsqRaUCNJJSUlikQiys/Pd25FfGTbQDud8gZtVohfJ+5B2442cXJ2Dexk0r43qSxBRVsLOMvtdotjFnCen4GNKce09feoWbx4sQYMGKCcnMRVGTx4sLZt26Zvvvkm7c/88ccfJUktW7as9ty0adPUqFEjFRQUaM8996x1xk5MWVmZiouLE/78ls0NnPwKCmwqbxAF9d4/tjCl00Aw1Va/wnTDP7+PM7+XD++F5dgKMvYfkLp0jxevjq/Yj3qY1A9bH9SsXbtW7dq1q/Z47LE1a9ak/Zl//etfVVhYqN/97ncJj++///66+eab9dJLL2nKlCmqV6+eTj75ZE2ZMqXWz7v11lvVtGnT+F+nTp3SLpMTbBts21Zer/jdgLA//JNuB+J3XYG9nGh/3WorvPh2PMzf4ME7lesyYx73eHFssd8A97jZNpoWzlRm1KVPFRUVKi9P7UY9eXl5ikQiKikpSXppU+yypJKSkrTKcMstt+jtt9/W5MmT1axZs4Tn3n///YR/n3766Ro4cKCuueYaTZgwQQUFBUk/8+qrr9Yll1wS/3dxcbFnYY1tHYebA3tTD0IbmXT9aBhx75rw8HI/21CnYmX0ok338jijLUVl9LEAgijbPtXJttGG9tWoGTVz585VQUFBSn9Lly6VJBUUFKisrKzaZ8V+Xrum8CSZZ555RpMmTdIZZ5yhP/3pT3W+Pjc3V+edd542bdqkRYsW1fi6vLw8FRYWJvx5wYZBt+Tdt0i2bA+bsE39Y/I3ALCPDceyH2V0+zjjOEZtmGFjF/YV4I1M20YTL2+qjVEzavr06aOpU6em9NrYpU3t2rXT2rVrqz0fe6x9+/Ypfd5bb72lU045RaNHj9aDDz6YYokVnxmzYcOGlN+DRMwOSJ9J24xv/vxlUl0A3FLTT3l61e64cZzRZiJVlese9SYzXvWVzOAGvJPqOYitx6RRQU3btm01YcKEtN7Tv39/zZs3TxUVFQk3FF6wYIEaNmyoXr161fkZCxYs0NFHH619991Xzz77rOrXT32zLF++XNKuX5qC+ehA3ePXtmUAm/yXocK6LYKEAG6X2raD12FNXeVJ97MQXpnWI74cAWAjN8c0NX227e2kUZc+ZWLs2LH66aef9MILL8QfW7dunZ577jkdccQRCfevWbZsmZYtW5bw/iVLlmj06NHq2rWrXnnllRovlfrll1+qPbZ582bdc889atmypQYOHOjQGsFtnPy4x+1p2pUvk0u2rLDvW9s7JKAqE4/pbI8zjlM4gcui0ufVscd+Afxj2+VNtTFqRk0mxo4dq6FDh+q0007TV199pZYtW2ry5MnauXOnrr/++oTXjho1SpK0cuVKSbuClkMPPVQbN27U5ZdfrldffTXh9T169NB+++0nSXrggQf00ksv6YgjjlDnzp21du1aPfroo1q1apUef/xx5ebmur+yAeb15Rt+zP6oun5ODPZNHQw4sX2z+bYxCI1zukytC0CmUq3Tfhzzmc6uCWPbBHcxkxKA6bgxf2asD2rq1aun1157TZdffrn+/ve/q6SkRIMGDdK0adPUu3fvWt+7fv16rV69WpJ01VVXVXv+1FNPjQc1BxxwgObPn69//vOfWr9+vRo1aqTBgwfr0Ucf1ciRI51fMQReskYrSA1MutOzCRoyw3YLpqDs10xDFFvWP9XAPEhtO8zFZVHmCOuXRkgdISvqEolGo1G/CxE2xcXFatq0qX7b7mzVz3FvJo6bA103GhS/ftHDK9msX03ltOlkpjIvyh30Tq+ubRj09Q86W47tVKRbFzNdd7/rfFCvkYfz+HbZX162r+wDxKRT74JWb4LQ5u2oKNfbax9SUVGRZ7/gbP2MGgSHH5fy2PKNR20nADac0NlQRluwLYMvzPs4m3X3uz1P1h7b0L8guJhhA/gn2/6sMpuP4TCPabJFUANYrKRvB23svWtWVvOl5T6Xxix+n7Q5jY4OQRfEOh60dgh2IrDxD21AuLjVjwUpuEHqrP/VJyTn9oDXrc/3++edbRMLaar+P3axed/G8MseCAOn6rifx0pNy67pV+oAr1EPd/H7xyQQHLX9EqlXyzWZ6eUzHTNqANn5jUeyYGZj71xm1gQEnVs4hXG/277O6ZSfm0fChPpu45gHMIEJx29VzLYJLoIawEK1zZ4hrElk24DUxEEAkI3ajkE36rtXx3y2ZSe0gZ/CfjmU1/f4s20sgl1sHJMR3AQHQQ2M49cNcm3pRFO5xIn71tjHxsEAkA0367yb7blb4ZLEgBreo+55x5ZxZpgFcSxW2zq5WR+DuC29RlADVGJ6J5rufWiYXbOLyfuVjgwxYaoLtq6rl/d/M7XNQuZMrvcENggTk49FL6WyHWgT/ENQA1Rh6kl9pjcLJqzZxaT9ygABYeZV/XfymPdrlmeMKW0Xgi9MgY0fM7hNGouEBWOu7NgS5phQBqcR1MBIfl3+FGPaQCXbX3QirDEDgwWEnW3HgCnlJbSB10waB5lUFicQ1rjHlDY7bNju7iCoAWphQmfq1M9uc98af/YnnRdqEra6YdO9x0zeN4Q28JKXIUldx50bdd/P+yImwzGdOpPbacAJBDVAHfwMa5wKaap+JmGNu/sz6IOHutaPgWZyQa8XtfF7hmSqddK2fcSve9jDtrpVlVOBjVPbIaiBpV83fjWR7ccMkC2CGhjL78ufKvMjrHEjpKn82WEOa9xiSn11S6rrl+x1DDBhuiDsM449uC3V8ZDXP30dE+T6HrRZOEFocwE3EdQAKfJy+q+bIU3lZYQ1rLH9JqN+yHY9gzbArCos9cBWtR3zQd93QT/24D2Tj5lMQhuTvhjMhKnHuM3bFIiJ1eMdO0qltd4um6AGGfNilomJnafb6+1FSFN5WWENa7JhWp10k5c/Rxzj9+CyLmHa/0EW9v1o47EHpCMsM21qEvY2DsiGCccPQQ2QAbfCGi9DmqrLDFtgk+4+NKHB9pLf9xRJxq+Bdtj2fVDFjnn2Z80Ib9xBnfNf2EMbAKkxqb0mqEFKag4QzDqh8lLQfi4yjLNr6gprTGqsvWTqeq85tpuk8IWKcM6aY7tRf9KUSnsQlH7QDUuv6CJJ6vzaDp9LgphkoQ0hLhBuJh7/BDWIc3I2R7qVvbZBnumdp1Oza/yYTZOsDGE/iTG5rrnN5HU34fiA3WJ1KKyzCN2UbdthctATC1oQTEH70g1Aekwe+xLUhIjJP/Vs8kGSimzDGpNOQsMW1the95xg+jaoenyErY4ie8naWAIbc9TUBnnRN27uUeH6MmA+0/tBAM6y4ZgnqAkok078wyLTb2XYV/CT6R0VxweyVVcdIrAxU9CO/VWH1+fyJwDwmenj3soIagLGpsoXVOnMrjFxIMrJSjjY0FbUdnwwqwapSKeNJbABAMA+qZx32TDurYqgJiCqVr7mS8s9CwE4YaoulbCGkAZ+sKWjMvH4sBkhROrYVv7j+AcAZMuWMW9NCGoCwPZKGFS1XQrFIBRes6mdSPX4ICSuW5jv75NtO0tgAwCAfWwa89aGoMZiJlXCMA3+01V1do2pIQ37L7hMaivqYurxYZuwXzbmZD0isPEWbQAAIBM2jXdTQVBjoaBVwjCI7TNTB6CcgASTbW1FJsdHGEKHdKWyHYO83dxqZwlsgodffALgh8r9FH1K9mwb76aKoMYy6VREL+9Tg7qZui9M6iA4EXKObZ2WqceHTdLdhkEMa7yoR7RTMBH1EjBbTf1TEPtirwQ98CKosYQNJ100NMmZfALK/goWWzssJ+4lYtP6Oi2b7Rekbed1W8uJsfNM7i+d4MVPdAfpmAZsF/Q2zS/JtmsQ2z6CGsPZENAgOdMbZ9Mas8rbK4iNrZtMr2twjxP7PgjHm5/HgK0BKYIjzDcNB0zDmMw9dW3boLV9OX4XADWzMaShcdq1DdgO6akpGUftaqtrtmw/p8ppy/o6xel2Jmzbzy20/5lju2XG9j4ACAraf3el84ugQdkPzKgxkJMBDfep8YZt29iktDnsv06TCdvqW02Csh5ecnOb2Xq8mViPuCwKXgjbt8uAaUzsf4Im020chPaPoMYgNs6gSSYIB0aqbGygTdo3Nm4/PwVpe7mxLkFve7y6Ua5N29D0Y4LABm4xve4DQcWx5w2nLu2W7O2DCWoMEZSQJixsbaRtbKhsO3F0mq11rTZBXCc3+XGTXBuOOZvqEfexqZ0f+9Lmn+ZOZ3vZcjwDJrOpv7EdX+T9iqDGZwQ0drG5oTatgWKgWTub61pt3F6vINUVv2+Qa/J2tPn4ILRBNjKp+6Yfz4CJbO5nbMT4sDqCGh+V7NkusDvAxoOhNrY31qbtC9u3p5uCvG2CvG5OM2FbmdqOm7BtnFJ1XUzc3m4L0v5MRTY/0Z3NtjL1eAZME7Y2yW9ebm/bLoUKak6ASrihcObYbs4L803BahP0uuZ1R2xrXTGtHti8LW3EbBu4ieMZ+JVp/a0Xkq2zn22CX/vAlraQoAauseUgSCZIjbet+yAZm+tUTYJU12oShnXMlsnbyKTjzuTt5LQwhDZh2p/ZcmpbmXQ8A16hral5G/gR3piwP2xoCwlqgEpMaDicZFoDFLTtm42wbAu+LambDXXBhO1pw3ZySxhCG9TM6bpvwvGcCuo90hXmfqI26W4Xt8Ib0/aP6ZdC5fhdAKds2rRJZ511llq1aqVGjRppxIgR+vjjj1N674QJExSJRKr99enTp9prKyoq9Ne//lXdunVTfn6+9t57bz311FNOrw48trF3rnGNR7ZMa3Sc/DbQVrF6ZvM6pCMs65kp2+qC3zc3xi6V2xG2S/C5tY9tqztBru+rDq+vVYfz3Xm6qraFNtcPN8vu5Pg7021u+v4xtWyBaBUqKio0evRoffrpp7r88svVsmVLTZ48WcOHD9eiRYvUs2fPOj8jLy9P//znPxMea9q0abXXXXvttbrtttv0xz/+UYMGDdLLL7+sk046SZFIRCeccIJj6+Q0v+5TY/q3NqYemNkybZuH9dvAmKDWM9OZWk9srg9ub1Obt41fbJ114Oe+tuWnufkVlOpM/wY8HYQzqaFfyIxX26222Tc27TsT25ZAtBAzZszQ/Pnz9dxzz2ns2LGSpOOOO069evXSddddpyeffLLOz6hfv77Gjx9f62t++OEH3XnnnTr33HN1//33S5LOPPNMDRs2TJdffrnGjRunevXqZb9CcJ1NDUe6TGpgwi7I9Qypqz4Yr1CTZfZOaM3m5I5jwl22hjZhk8ovP3l5kmViXalr/U08qUoVAU3t6Cey5/c29Hv52TCpTQxESzFjxgy1adNGxxxzTPyxVq1a6bjjjtMTTzyhsrIy5eXl1fk5O3fu1NatW1VYWJj0+Zdfflnbt2/XxIkT449FIhH96U9/0kknnaQPPvhABx54YPYrFDAmVHibGwzbuTlt2+96lUziAMzfE3JTt5GXvN4GqQ7Abf81vtq2q83rFSQmhzbUkdp5vX1s7itsCmzq6h+y+en2oKBtyB7bMHumtImBCGoWL16sAQMGKCcn8YRo8ODBevjhh/XNN9+oX79+tX7Gtm3bVFhYqG3btql58+Y68cQTdfvtt6tx48YJy2nUqJH22GOPasuJPU9QY4awNlImNCqVhWnaNt+QhU+2+9z2sGbV4fWtnhkUJjXVM1PaT/jPpP40EyYHNowP4AWbxxMmMqFNCUTLsXbtWh100EHVHm/Xrp0kac2aNbUGNe3atdMVV1yhAQMGqKKiQq+//romT56sTz/9VHPmzFH9+vXjy2nTpo0ikUiNy0mmrKxMZWVl8X8XFxent4IOsf2koDZBXa9UVL7WvvlSHwtSRRj2CYMvO2R7AuLmfraxXbbl/h6oGwGOefy+abft+96Ek6sY28YIfs/osa0vNAnbzj1+tovGtSAVFRUqL09tY+Tl5SkSiaikpCTppU35+fmSpJKSklo/59Zbb0349wknnKBevXrp2muv1YwZM+I3Cc50Obfeequuv/76ulcowJyu5DRIyVUeFIRl+qypl7bATn7sX5tuulc1pNncw+777SA5NwMcG+q5X0zYNkEIayR/A5ts+hG/whLGNvYyod0Iuo29c9Xky1LPl2vcUTl37lyNGDEipdcuWbJEffr0UUFBQcKMlZjS0l0btKCgIO1yXHzxxfrzn/+st99+Ox7UZLqcq6++Wpdcckn838XFxerUqVPaZQozGqH0Ve10vez4g3h9PYMYuyWrI6btU9Nn1zCTBszAcY9Jx35QwhrJ28DGtD4lVZXL7fesGqTOpDYjDDbtniu94+0yjWtR+vTpo6lTp6b02tglR+3atdPatWurPR97rH379mmXo6CgQLvttps2bNiQsLzZs2crGo0mXP5U13Ly8vJSuplx0KXT8dP4OM+r4CZI+86JQRczDsywuUeFNvcwrsurxtSwpraQhjqOVAMcE+q2iYGjCdulKj9P2N3YHm4GNrYGNFLysvux7008BkzG9goH41qWtm3basKECWm9p3///po3b54qKioSbii8YMECNWzYUL169Uq7HJs3b9a6devUqlWrhOX885//1JIlS7TnnnsmLCf2PDJDg+M9N4KbIFxbb/OAC8Fg2qVQJp7Ywg6m1GETxU6GTdxGsWM+iLMrnAxs3BoveLXdayt/EPd9UJjYZsAdgfgKbOzYsfrpp5/0wgsvxB9bt26dnnvuOR1xxBEJs1mWLVumZcuWxf9dWlqqzZs3V/vMG2+8UdFoVIcddlj8sTFjxqhBgwaaPHly/LFoNKoHH3xQHTp00P777+/0qjnOlKmsG3vnJvzBf6sOr5/wZ6Ns6pLN6w27pDr49bu93jULKbWQhjAHSJ8N45+g9ovZjD/DMl4IwzrahHOm8AnEETh27FgNHTpUp512mr766iu1bNlSkydP1s6dO6vdxHfUqFGSpJUrV0qSfvzxR+2zzz468cQT1adPH0nSG2+8oddee02HHXaYxowZE39vx44dddFFF+mOO+7Q9u3bNWjQIL300kuaN2+epk+frnr16nmzwi7xYqC9uQc/52qLdG9MbGPnwSAEpvPrUqhM+gMugQLsl+zYj/WVQZxhUbl9rSscD9KYwaR1sXH86IbaZoazjcLJnKM0C/Xq1dNrr72myy+/XH//+99VUlKiQYMGadq0aerdu3et723WrJl+//vf66233tJjjz2mnTt3avfdd9ctt9yiyy67LOFSKkm67bbb1Lx5cz300EOaNm2aevbsqSeeeEInnXSSm6vomF0Ns7/ffDKYt09dl0mZ1IHUdQmUSYMTIBVeXwrF7BgEWSbjD46JXYJ+OUxNl0UFbdyQzvoEfZ/bwKQxNrwViUajUb8LETbFxcVq2rSpDhj1F9Wvn+/JMk3rZAhqnGPCANLE/Vl5oOV3/fdz+/h5+YxJgwsTjpOYbAa9bm9TJ7aTie0BgLqlevy7eeJOv/ErN7ZzpuMhfoDCGybegB277Cwr1ZcPXaOioiIVFhZ6skyzzt7hOL9PUGvCrJpg8Xowk2rdMbX+eylIP7MK9y6F8vuEBIA9gnwpVEwQ28RsxkTMrPEWAQ0kgprA4gQVQZbKAMqGn2IGMuH0pVBOn5AQxAPhwMm7PTgvsAchDWIYSQWMTXeiD+K3FQDgFSdmStEOA5AybwtsGXPayLRt63R5CCSqY5ugMoKaADGtQU8FJwlAcHHJlfuaLy3PeDu72f7StgPhYdOXhGHk5L5hPwPeIagJADpIwGyctMJt6YY11EkAMU61B9mORZlN4Dw3zg+c+Ez2NVA3ghqLBSWg4YQBALKXSlizuUeFZ20ubTsQPkEYl5ok2xsAu4X9DLiPoMZCQQloKmNADzgvdlIetPYCNavtUig/2lnadsBsbhyj9Dn+Yx8A9iOosQwNL4BUVB18BzHgRc2qhjUEJgC8ZHOfQ3uZmkz3L5c9AakhqLGEzR1equgYgezVdWlLrC0JentiKi9/yrb50nJPL3Wqid/LB5CcF8cmfU320t2GXm5z9i/gHoIaw4XthIoBPZCZTE7Iw9a+hAn7FoApUmmLmGXhDD/a/XSWyX4GUscozlAMsAGkwolwM9beeDnbA+4wte/Y3KNCTZbx3RBgCq+/GKOfcZ+f7f+qw+uzbwGHmTmiCzFTB9leYkAP1M3tG0Ay4LILfQcAG3BCn5m6tht9AIKs8pg3TOeI4VlTC9DI/sqmS6Bil5zwiyrwilf3FaBNMp9N+4n2CoDEeNdppmzPusrBZU9IVeVzqzCPHcw4skPOlAYWqaup0Yg9Hqa0F97xo7Nilo2Z6DcAZMqEEx8uhQomZkwhEya0SSZipOej7w+pr5x8dkFNTLwEKtWGhMAGTjKlA2Ng7T/bAxoT23UA/ll1eH3ahBQlC0FM7BOSldO02TSM0/1lyrjWdOYd3YBhsmlMOClBTCZ1wdSOjMDGeyYOxjNFu5gcJw7wgon9Cm1CZoLULziNL1bNYmK7YwOOcBjNr87byQaFTgDpsqVD47IodzEID4/Kx3zV45++A2HAWCk9pvcPbl8C5fQ4ibDQebaMZU1m9lEOyNvG081GhUEIUmFrx8YsG+eYPgB3AoPiX9V1zBPcwCk29C+MlepmSx8RC2syuezJzx/ooO5lxot9FrZ9Y8eRDrjI686AjgDJ2DCATgWzbDJny+AbzsnkuCe4QRgwVkrOtn5i1z2Iqj9u8piHLxJSY/I+DAq7jnaElhuNpt8NDIMQSP7XQzftGlAy4KmLbQNvp4R5MOzG5bWVhXW7oma29jVej5Vs3U4ms3GbMkavzsb9aLtwjg5hJScG9SY2MnQG4WRiXXRL5XUNcz234dc64D4vjn1m3SBoGCvBD3yhAD8xSkQo2NDYMAgJBxvqops4gSSgqSxsg2C/jn+OOwQFYyV4LWx1LuzjVJMwWoRV0hnU29rQhK1DCBNb66SbwjTbhoAm3Ew6/gluwsWkuueUsIW88F/Qx+dBbCdsx6gR1qmrcw5KQ8MgBGHDyWM4haGtM71f4tiDjYJ+4gwzBanPMr1vCjuCGgRCUBsaBiH2i+27oNZRN4Vptk3YBbmts/HY5wbFwWFj/UtXkNsPmMn2OheGdiEICGpgpViaHZaGxvYOIYzYV87iG/9wCFpbF6Q+imPQPkGqf6kIWvsB89k2uyZsbYLtCGpgpcbdihTtJmlFU7+L4ikGIWarab/QMTqP2TbBFoS2LujHPcGNWYJe39IRhPYD9rChvtE+2ImgBlZr3K1IW0IW1kg0uCYxuWMOC04Yg8u2bytjwthGcxx6J4z1KxM2nEAjOEzrr4LWTpi0bb1CUAPrNO5WVO3fYQxr4J8wdhY2YbZNsNh2shW0wXGmuM+NM6hP2bOtDYG9TKhrtBnBQVCDQCCsgdsY4NmJAUtwmDAArgv1rXbMuqkd9cddNrQhCAY/ZtfQfgQPQQ2sUnU2TdXnCGvgJAZzgHlMPdlikJy+MAc31Bf/1HQSzT6Bk7zqq6i37tvco0IVpd5vZ4IaAKjE6Q6VDhRwhymBDce4c4J6uRR1xDymtB8IPrdm19CueMPP7UxQA2vUNpum8muYVYN0MVAD7OXnCRcDZffZNuuGOmEXAht4wal6RvviHRO2NUENAoewBqlgUAYEi9cnXCYM4sIo+tuNfhehGsYc9uN4hhcynV1D/fSOSduaoAZxLx51jyTp6Jcu8rUcTiCsQTKEM0DweRHYmDSQC5NUZtYCgMnS6aPoa7xl2vYmqEE8oKn8b9PCmkwGZ4Q1kPiJRCCs3ApsOK4BANmqbXYN/Yy3TN3egfl6edOmTTrrrLPUqlUrNWrUSCNGjNDHH3+c0nsjkUiNfwcffHD8dStXrqzxdU8//bRbq+aaF4+6p1pIU/m5IODbN5ja+ALwxuYeFY61A7Qn/jG1P+cLIQCZqtw/xf6ffqY6t750NX17B2JGTUVFhUaPHq1PP/1Ul19+uVq2bKnJkydr+PDhWrRokXr27Fnr+x9//PFqjy1cuFD33nuvDjnkkGrPnXjiiTr88MMTHttvv/2yWwkPpRrCmDKzJtvBGTNr4NYd9wHYI9sZNiYP5gAAdmrcrUjRbpI4V/GMLf15IIKaGTNmaP78+Xruuec0duxYSdJxxx2nXr166brrrtOTTz5Z6/vHjx9f7bE5c+YoEonoxBNPrPbcgAEDkr7HdEGZJZMJwhoQ1gCQMmsLbBnUBZWps2kAIBO0af6xqT8PxFnLjBkz1KZNGx1zzDHxx1q1aqXjjjtOL7/8ssrKytL6vLKyMj3//PMaNmyYOnbsmPQ1W7duVXl5eVbl9kptlzil8l4/OdmQ0SjCpsYZgHvSme5MuwEAyFbjbkXxP3jP9MuckglEULN48WINGDBAOTmJqzN48GBt27ZN33zzTVqf99prr2nTpk06+eSTkz5//fXXq3HjxsrPz9egQYP05ptvZlx2N2UT0FT9nKCgcYRXjbRtnQEQRnUN3DiO/Wdyv81MXQC1IZzxn40BTUwggpq1a9eqXbt21R6PPbZmzZq0Pm/69OnKy8uLX0YVk5OTo0MOOUR33HGHZs6cqbvvvls///yzfve73+nVV1+t8fPKyspUXFyc8OcmpwKaqp/pNbcaNRpL2NpgA3BHsoEc7YT/6K8B2IZwxgw2BzQxxt2jpqKiIuVLivLy8hSJRFRSUqK8vLxqz+fn50uSSkpKUl5+cXGxXn31VR1++OFq1qxZwnOdO3fWG2+8kfDYH/7wB+2555669NJLNXr06KSfeeutt+r6669PuQyZCtLMF7dxzxpwzxoAVdk+qAMAeI9QxixB6cuNO0uZO3euCgoKUvpbunSpJKmgoCDpfWhKS0vjz6fq+eefV2lpaY2XPVXVokULnXbaaVq6dKm+//77pK+5+uqrVVRUFP9bvXp1yuVJhRszaGpajle8aPBoVBGUhhwAgsb0Ppove4BwY+aMeYIwi6Yy42bU9OnTR1OnTk3ptbFLm9q1a6e1a9dWez72WPv27VNe/vTp09W0aVP9/ve/T/k9nTp1kiRt2LAh6c2H8/Lyks74yZYfM2hM+clupzCzBm7MrAlSJwEAAADzA+SwCuq427igpm3btpowYUJa7+nfv7/mzZunioqKhBsKL1iwQA0bNlSvXr1S+py1a9dq9uzZmjBhQlrByvLlyyXt+qUpL/h9iZPbYY3XjSBhDQAA5uBkCIApaI/MFdSAJsa4S58yMXbsWP3000964YUX4o+tW7dOzz33nI444oiE0GXZsmVatmxZ0s95+umnVVFRUeNlT7/88ku1x3744Qc9+uij2nvvvZPe0NhJXl3iFEY0wuEW9IYeAAAAqeGyJnPUNOs9DGN342bUZGLs2LEaOnSoTjvtNH311Vdq2bKlJk+erJ07d1a7ie+oUaMkSStXrqz2OdOnT1f79u01fPjwpMu54oortGzZMo0aNUrt27fXypUr9dBDD2nr1q269957nV6tOBPDmaBdAiUxsybsuLkwAPjPhhMjxgpA8NjQ9iAcAU1MIIKaevXq6bXXXtPll1+uv//97yopKdGgQYM0bdo09e7dO6XPWLp0qRYtWqRLLrkk4fKpyg455BA9+OCDeuCBB7Rx40Y1a9ZMBx10kCZNmqQBAwY4uUqSzAxoKnMjrPG7kSSsCTfCGgAAgHDw+7wDqQtTQBMTiUajUb8LETbFxcVq2rSputx+k3L+/0+IV2Z6QFOVk2GNKQ0mYU24ZRPWhLEjAQAnmDIGqAtjBMBeprYztCvJNVmWY8TYuqK0VN9dOUlFRUUqLCz0ZJmBmFETFLYFNDFOzawxqeFkZk24MbMGALxl0higNowNAPvY0r6gOhNCGr8Q1BjA1oAm6Ahrwo2wBgAAwG6ENLAVZyE+mj56cmBCmmzXw9RG1NRywRthTvEBwCv0tQAAJCKogWOCEjpVxQAy3AhrAAAAAHiJoAaOyiSssSEIsaGMcE+qYQ2hDgCkx6b+lcuhAftw3MJWBDVAimwaTAIAAAAA7ERQ46OnNw3xuwiuSGdWjW3hh23lhXOYLQMAzqJPBQAgOYIanz2+cX+/i+CKoN6vhumT4UZYAwDhRP8PAPASQQ1cE9SwBuFWU1hDiAMAqWM2DQCvELTCRgQ1BgjqrJq62DZIo5FHDKEMAGTOtv4fANxGu4iqCGoMEdSwhlk1CCrCGgAAAABuIKgxSJjCGttSY2bTIBnCGgBIj239v8QYAADgPYIaw4QprAGCgLAGAADAbASusA1BDTxn27dpNOyoC2ENANTNtv4fAAC/ENQYiFk1AAAA/uPLGgCAHwhqDEVYYwYGaAAy0bhbEbMHgEo4HgAASB1BjcGCGtbYgpAGQCYqn5AS2ACENAAApCvroKa0tFRlZWVOlAVJBC2s+cOnp/ldBADwHIENAAD+4ktY2CTtoGbOnDm6+OKLNXjwYDVu3FiNGjVSw4YN1aRJEw0ePFgXXXSR5syZ40JRwytoYY0NaMgBZKKuMIbABmFjc31nLAAA8Ev9VF60fft2PfTQQ7rrrru0cuVKtWjRQgMGDND48ePVvHlzRaNRbdy4UStWrNATTzyhv//97+rSpYsuvfRSnX322WrQoIHb6wELMJvGW427FTHIBDyUzglp7LUcowi6LSuaWh3WAADgh5SCmt13313l5eU69dRTddxxx2nAgAG1vn7RokV67rnndMstt+hvf/ubVq5c6URZQ+3xjfvrD83n+12MjNkU0gThxCk2KCasAcxGYIMwqFq/CW4AAKhdSkHNNddcowkTJigvLy+lDx04cKAGDhyoG264QVOnTs2qgPiVrWGNTSFNEDAABryX7XFHYIMwsSG44VgEgolZfrBFSveoOfvss1MOaSrLzc3V2Wefnfb7UDPuV+Mu2wdmyToeOiPAHtzDBmG0ZUXThD8AAMIupRk1MItNM2uYTYNUMaMAtnIjWOF4QJglq/cEmACAMMk4qNm6dauef/55LV++XBs3blQ0Gk14PhKJ6N577826gEjOhrDGtpDG9hOi2gaxpt+rpnLZTS8rUJnbJ48ENsAuNlwuBQCAUzIKat555x2NGzdOmzZtqvE1BDXhZltIY7tUBqymBiA1Xa5lYlkBvxDYAIncDG44zoBg4z41SEeTZTnaWZbSHWMcldESzz33XDVq1EhvvPGGNm3apIqKimp/O3fudLqsqIL71TjH5kFZUDuaoK4XgsOPOso9bIDkqt7nxuZ+HQBghibLvA9oYjJa8qpVq3TFFVfo4IMPVmFhodNlQhpMDGtsm01j82Au3RM2007w6iqPaeUFYvyumwQ2QN0IbgDYgjbKLE2W5fga0kgZBjV77723iooYIJrCpLDGtpDGZrafpKVaftvXE3ATgQ2QulSCG06WAHiJINk8fgc0MRmV4vbbb9fkyZO1cOFCp8uDDJkQ1tgY0tjaMGZzYsZJHZAdE48hAhsgfVwuBYSX38c7bY55TJhFU1lGNxMeNmyY7rnnHu23337aY4891KlTJ9WrVy/hNZFIRC+//LIjhQTwqyCcjGVyyRadGUxg+vHHTYcBADAX/bOZTApoYjIKap5//nmNHz9eO3fu1Pfff6/NmzdXe00kEsm6cEiPnz/ZzWwabzh1kuhn8JHpOhDWAKnjeAEAwBz0yWYyMaCJySioueqqq9S7d289//zz6tWrl9NlQhb8CGtsDGlsZPo3+V7g5BN+su0YZHYNAAD+og82l8khjZThPWrWrFmjP/3pT4Q0hvLyfjW2hjS2NZpunCD69dPCJnwGkC6b6x33rwEAoDo3zwe4B425TLsXTU0yKuGgQYO0atUqp8sCB5lwc2E4IygnWE6uR1C2CYBgiIVhhGIAEG4ENGazIaCJyaik9913n55++mk9++yzTpcHDnI7rGE2jfvcHvDbfEJhc9lhlyDUNZvaPdskqx8ENgAQLgQ0ZrNlFk1lGd2j5uSTT9aOHTt04okn6o9//KM6duyY9FefPv30U0cKCfMQ0rgvSIN8t9aFe9bAbUE4DjlG3FNX/aj8PPsBAIKHtt18tgU0MRmVukWLFurZs6cOOuggDRgwQK1bt9Zuu+2W8NeiRQuny5rU2rVrddVVV2nEiBFq0qSJIpGI5syZk9Zn/PDDDzruuOPUrFkzFRYWasyYMVq+fHnS1z7yyCPaY489lJ+fr549e+q+++5zYC3c48asGltDGpt4eXJo+6ydIJxIA7BPum0Ps2wAIDiYQWM+G2fRVJbRjJp0gxA3LV26VLfffrt69uypfv366YMPPkjr/Vu2bNGIESNUVFSka665Rg0aNNDdd9+tYcOG6ZNPPtFuu+0Wf+1DDz2kc845R8cee6wuueQSzZs3TxdccIG2bdumK6+80ulVc4yfP9ttElsaU79u8mvL9knG9vLDTEE4qea4cEc2dYNf4wIAc2xZ0TStNp222w42BzQxGQU1v/zyi1q1alXraz766CMNGjQoo0KlY+DAgVq/fr1atGihGTNmaNy4cWm9f/Lkyfr222/14Ycfxsv7u9/9Tn379tWdd96pW265RZJUUlKia6+9VqNHj9aMGTMkSX/84x9VUVGhG2+8UWeddZaaN2/u7Mo5yKmwhtk07grCiWFlXs8MovMEfsXx4A6n2jUuiwIAe9BO2yEIAU1MRmsyatQobdy4scbnZ8+erd/+9rcZFyodTZo0yeoyqxkzZmjQoEEJoVKfPn00atSohJslz549W+vXr9fEiRMT3n/uuedq69atevXVVzMug1eyvQzK5pCGxrVuTocqtv78NyBRl5Ccm/fbos4BgHm4xMkeQQpppAyDmm3btunggw9WUVH1QcUrr7yiww8/XAMHDsy6cG6rqKjQZ599pn333bfac4MHD9ayZcu0efNmSdLixYslqdprBw4cqJycnPjzyZSVlam4uDjhT5Jmfd9Tb67q7dTquMrmkMYWDNKdwXZEtoJQhxhUOs+LekFgAwBmIKCxh+33oqlJRmv0zjvv6JdfftFhhx2mLVu2xB9/+umndcwxx2jUqFF67bXXHCukWzZs2KCysjK1a9eu2nOxx9asWSNp102L69Wrp9atWye8Ljc3V7vttlv8dcnceuutatq0afyvU6dOCc+/uap3wp+b3P7JbhPZ0MiaMjB3Y0q/H/xePuAnG9o823jdpsQCG9oyANmgLUlN5X6TgMYuQQxoYjJasy5dumjWrFlavXq1Dj/8cG3btk0PP/ywxo8fr2OOOUYvvfSS8vPz0/7ciooKlZaWpvQXjUYzKXqCkpISSVJeXl6152Llj72mpKREubm5ST8nPz8//rpkrr76ahUVFcX/Vq9eXWu53A5u0g1rmE3jrqB1nqasjynlgF2oN6jK7zrBSRaAdNQUztCO1I6Axi5BnUVTWUY3E5akHj166O2339bw4cPVv39/LVu2TKeffroefvhhRSKRjD5z7ty5GjFiREqvXbJkifr06ZPRcmIKCgok7bo0qarS0tKE1xQUFKi8vDzp55SWlsZfl0xeXl7SMChVycKaQzovzfjzpNRvLmx7SGN6g2tipxmkm/IGaV3gPhOPx3RR351lUp3g5sMAkjGpnQK8EPSAJialoGbDhg1JH2/durWeeeYZHXHEETr11FN12223JdxkON2b/Pbp00dTp05N6bXJLldKV4sWLZSXl6e1a9dWey72WPv27ePL27lzp37++eeEy5/Ky8u1fv36+Ou8UjW8ySS4qSusIaRxVxA7VhPXibAGYUE9d5aJ7VkMP/ENhFc2bRNjItgsLAFNTEpBTcuWLWudJRONRvXYY4/pscceS3h8586daRWmbdu2mjBhQlrvyUZOTo769eunhQsXVntuwYIF6t69u5o0aSJJ6t+/vyRp4cKFOvzww+OvW7hwoSoqKuLP+yXT4KamsMb2kMZ0Jp8ASJl15CavEwMT1MXk+psK6rezbKkPBDZAONjSJgFuCVtII6UY1Pzv//5vxpczmWTVqlXatm1bwiVTY8eO1VVXXaWFCxfGf9Fp6dKlmjVrli677LL460aOHKkWLVpoypQpCUHNlClT1LBhQ40ePdq7FUmBEzNubJdqp+b1AJfO1h+ENagJxyQqs7E+cFkUECxutkOMhwA7RKJO3JXXZzfddJMk6csvv9TTTz+t008/Xd26dZMkTZo0Kf664cOH69133024EfHmzZu1zz77aPPmzbrsssvUoEED3XXXXdq5c6c++eQTtWrVKv7ayZMn69xzz9XYsWN16KGHat68efrXv/6lm2++Wddcc03K5S0uLlbTpk2159NXqF7DzO9dk42qwU3lWTXMpslMKp2ebScAqXbktqwXAxMkY0v9rQn12jm214XKqBeAPbxue2gfYCu/ZtbsLCvVlw9do6KiIhUWFnqyzEAENXVdlhWTLKiRpO+//14X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EcBvjZfnaKffhahFRWmFke0oYyHAXya2CzGxsnl51xijgpqKigqVl5en9Nq8vDxFIpFqj8+dO1fXX3+9jjvuOI0cObLWzygpKYl/VlX5+fkJrykpKVFubm7Sz8nPz4+/Lplbb71V119/fbXHl55+b63l88OBfhfAGC/6XQAAAAAAgCE2b96spk29uQrBqKBm7ty5GjFiREqvXbJkifr06ZPw2Ndff62jjz5affv21T//+c86P6OgoECSEma7xJT+/28FY68pKCioMUQqLS2Nvy6Zq6++Wpdcckn83xUVFdqwYYN22223pGETvFdcXKxOnTpp9erVnt0gCrAJxwhQN44ToHYcI0DdOE7ME41GtXnzZrVv396zZRoV1PTp00dTpya/qWtVVS9XWr16tQ455BA1bdpUr732mpo0aVLnZ7Ro0UJ5eXlau3Zttedij8V2Rrt27bRz5079/PPPCZc/lZeXa/369bXutLy8vGqzdpo1a1Zn+eC9wsJCGkSgFhwjQN04ToDacYwAdeM4MYtXM2lijApq2rZtqwkTJqT9vvXr1+uQQw5RWVmZ3nnnnaT3nEkmJydH/fr108KFC6s9t2DBAnXv3j0e+PTv31+StHDhQh1++OHx1y1cuFAVFRXx5wEAAAAAADJl/a3Xt27dqsMPP1w//PCDXnvtNfXs2bPG165atUpff/11wmNjx47VRx99lBDWLF26VLNmzdK4cePij40cOVItWrTQlClTEt4/ZcoUNWzYUKNHj3ZojQAAAAAAQFgZNaMmEyeffLI+/PBDnX766VqyZImWLFkSf65x48Y66qij4v8+5ZRT9O677ybcrXnixIn6xz/+odGjR+uyyy5TgwYNdNddd6lNmza69NJL468rKCjQjTfeqHPPPVfjxo3ToYceqnnz5umJJ57QzTffrBYtWniyvnBHXl6errvuuqQ3lgbAMQKkguMEqB3HCFA3jhNIUiTq5W9MuaBr16767rvvkj7XpUsXrVy5Mv7v4cOHVwtqJOn777/XxRdfrDfffFMVFRUaPny47r77bu2+++7VPvMf//iH7rzzTq1YsUKdOnXSeeedpwsvvJCbAgMAAAAAgKxZH9QAAAAAAAAEhfX3qAEAAAAAAAgKghoAAAAAAABDENQAAAAAAAAYgqAGoVZWVqYrr7xS7du3V0FBgYYMGaK33nrL72IBxtiyZYuuu+46HXbYYWrRooUikYimTZvmd7EAY3z00Uc677zztNdee6lRo0bq3LmzjjvuOH3zzTd+Fw0wwpdffqlx48ape/fuatiwoVq2bKmDDjpI//73v/0uGmCsm2++WZFIRH379vW7KPAJQQ1CbcKECbrrrrt08skn695771W9evV0+OGH67333vO7aIAR1q1bpxtuuEFLlizRb37zG7+LAxjn9ttv1/PPP69Ro0bp3nvv1VlnnaW5c+dqwIAB+uKLL/wuHuC77777Tps3b9app56qe++9V3/+858lSUceeaQefvhhn0sHmOf777/XLbfcokaNGvldFPiIX31CaH344YcaMmSI7rjjDl122WWSpNLSUvXt21etW7fW/PnzfS4h4L+ysjJt3LhRbdu21cKFCzVo0CBNnTpVEyZM8LtogBHmz5+vfffdV7m5ufHHvv32W/Xr109jx47VE0884WPpADPt3LlTAwcOVGlpqb7++mu/iwMY5YQTTtAvv/yinTt3at26dYT+IcWMGoTWjBkzVK9ePZ111lnxx/Lz83XGGWfogw8+0OrVq30sHWCGvLw8tW3b1u9iAMbaf//9E0IaSerZs6f22msvLVmyxKdSAWarV6+eOnXqpE2bNvldFMAoc+fO1YwZM3TPPff4XRT4jKAGobV48WL16tVLhYWFCY8PHjxYkvTJJ5/4UCoAgO2i0ah++ukntWzZ0u+iAMbYunWr1q1bp2XLlunuu+/Wf/7zH40aNcrvYgHG2Llzp84//3ydeeaZ6tevn9/Fgc/q+10AwC9r165Vu3btqj0ee2zNmjVeFwkAEADTp0/XDz/8oBtuuMHvogDGuPTSS/XQQw9JknJycnTMMcfo/vvv97lUgDkefPBBfffdd3r77bf9LgoMQFCD0CopKVFeXl61x/Pz8+PPAwCQjq+//lrnnnuu9ttvP5166ql+FwcwxkUXXaSxY8dqzZo1evbZZ7Vz506Vl5f7XSzACOvXr9f//u//6s9//rNatWrld3FgAC59QmgVFBSorKys2uOlpaXx5wEASNWPP/6o0aNHq2nTpvH7oAHYpU+fPvrtb3+rU045Ra+88oq2bNmiI444QvyuCSBNmjRJLVq00Pnnn+93UWAIghqEVrt27bR27dpqj8cea9++vddFAgBYqqioSL/73e+0adMmvf766/QhQB3Gjh2rjz76SN98843fRQF89e233+rhhx/WBRdcoDVr1mjlypVauXKlSktLtX37dq1cuVIbNmzwu5jwGEENQqt///765ptvVFxcnPD4ggUL4s8DAFCX0tJSHXHEEfrmm2/0yiuvaM899/S7SIDxYpeYFxUV+VwSwF8//PCDKioqdMEFF6hbt27xvwULFuibb75Rt27duOdZCHGPGoTW2LFj9be//U0PP/ywLrvsMklSWVmZpk6dqiFDhqhTp04+lxAAYLqdO3fq+OOP1wcffKCXX35Z++23n99FAozy888/q3Xr1gmPbd++Xf/6179UUFBAsInQ69u3r1588cVqj0+aNEmbN2/Wvffeqx49evhQMviJoAahNWTIEI0bN05XX321fv75Z+2+++567LHHtHLlSj3yyCN+Fw8wxv33369NmzbFfwnt3//+t77//ntJ0vnnn6+mTZv6WTzAV5deeqlmzpypI444Qhs2bNATTzyR8Pz48eN9KhlghrPPPlvFxcU66KCD1KFDB/3444+aPn26vv76a915551q3Lix30UEfNWyZUsdddRR1R6/5557JCnpcwi+SJQ7eCHESktL9ec//1lPPPGENm7cqL333ls33nijDj30UL+LBhija9eu+u6775I+t2LFCnXt2tXbAgEGGT58uN59990an2eYhbB7+umn9cgjj+jzzz/X+vXr1aRJEw0cOFDnn3++jjzySL+LBxhr+PDhWrdunb744gu/iwIfENQAAAAAAAAYgpsJAwAAAAAAGIKgBgAAAAAAwBAENQAAAAAAAIYgqAEAAAAAADAEQQ0AAAAAAIAhCGoAAAAAAAAMQVADAAAAAABgCIIaAAAAAAAAQxDUAAAAAAAAGIKgBgAAIAUVFRXq27evbr755vhjf/nLXxSJRLRu3TpXl/3ggw+qc+fOKisrc3U5AADAfwQ1AAAg0MaPH6/8/Hx988031Z677bbbFIlE9Morr9T5OU899ZRWr16t8847z41i1mrChAkqLy/XQw895PmyAQCAtwhqAABAoN11111q2LChzjnnnITHV6xYoRtuuEHHHnusfv/739f5OXfccYdOOOEENW3a1K2i1ig/P1+nnnqq7rrrLkWjUc+XDwAAvENQAwAAAq1169a6/fbbNXv2bD322GPxxydOnKgGDRro3nvvrfMzFi9erE8//VTHHXecm0Wt1XHHHafvvvtOs2fP9q0MAADAfQQ1AAAg8M4880wdcMABuuyyy7R+/Xo9/fTTev3113XTTTepQ4cOdb7/pZdeUm5urg466KA6X/vdd99p9913V9++ffXTTz9JkoYPH66+ffvqs88+07Bhw9SwYUPtvvvumjFjhiTp3Xff1ZAhQ1RQUKDevXvr7bffrva5AwcOVIsWLfTyyy+nufYAAMAmBDUAACDwIpGIHnroIRUVFelPf/qTLr74Yu27774699xzU3r//Pnz1bdvXzVo0KDW1y1btkwHHXSQmjRpojlz5qhNmzbx5zZu3Kjf//73GjJkiP76178qLy9PJ5xwgp555hmdcMIJOvzww3Xbbbdp69atGjt2rDZv3lzt8wcMGKD3338/vZUHAABWqe93AQAAALyw11576bLLLtOtt96qevXq6dVXX1VOTmrfWX399dcaMmRIna8ZNWqUOnTooDfeeEPNmzdPeH7NmjV68skndeKJJ0qSDj74YPXp00cnnXSS5s+fH//8PfbYQ4ceeqief/55TZgwIeEzunfvrscffzzFNQYAADZiRg0AAAiNli1bSpLat2+vvn37pvy+9evXVwteKvviiy80bNgwde3aVW+//XbS1zZu3FgnnHBC/N+9e/dWs2bNtMceeySEQLH/X758ebXPaN68uUpKSrRt27aUyw4AAOxCUAMAAEJh9erVuu6669S3b1+tXr1af/3rX9N6f22/tnTEEUeoSZMmeuONN1RYWJj0NR07dlQkEkl4rGnTpurUqVO1x6Rdl0rVVIaqnwMAAIKDoAYAAITCeeedJ0n6z3/+o3Hjxunmm29OOmslmd122y1pcBJz7LHHatmyZZo+fXqNr6lXr15ajycLhjZu3KiGDRuqoKCgjhIDAABbEdQAAIDAe/HFFzVz5kzdeOON6tixo+655x7l5uamfDPhPn36aMWKFTU+f8cdd+iMM87QxIkT9eSTTzpV7GpWrFihPfbYw7XPBwAA/iOoAQAAgbZ582ZdcMEF2meffXT++edL2nWPmhtvvFGvv/66nnvuuTo/Y7/99tMXX3yhsrKypM9HIhE9/PDDGjt2rE499VTNnDnT0XWI+fjjj7X//vu78tkAAMAMBDUAACDQJk2apDVr1uihhx5KuMzo3HPP1YABA3TRRRcl/SnsysaMGaPt27fr3XffrfE1OTk5euKJJ3TIIYfo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V66s9rr169frjjvu0EEHHaRWrVqpWbNmGjp0qJ555hkX1gZec7v+SFLXrl0ViUSq/Z1zzjkOrw285kX9KS0t1a233qo999xTDRs2VIcOHTRu3Dh9+eWXDq8NvJZp/XnhhRd0/PHHq3v37mrYsKF69+6tSy+9VJs2bar22meeeUbjx49Xz549FYlENHz4cOdXBL7wov5UtmzZsnh7tXDhQofWAn5wu+7MmTMn6bgn9nfzzTe7tGbwQqb158UXX9Shhx6q9u3bKy8vTx07dtTYsWP1xRdfJH39zJkzNWDAAOXn56tz58667rrrtGPHDqdXBx7zov5cfPHFGjBggFq0aKGGDRtqjz320F/+8hdt2bIlozLzq08ZOvHEEzVjxgxddNFF6tmzp6ZNm6aPPvpIs2fP1oEHHljj+6ZNm6YzzjhDe+65p+rXr69PPvlEK1asUNeuXRNe98orr+iYY47R4YcfrhEjRqh+/fp6/vnnNXv2bP3v//6vrr/+epfXEG5yu/5Iu4Ka5s2b69JLL014vFevXho8eLDTqwQPeVF/jj32WM2cOVN//OMfNWDAAK1Zs0YPPPCASkpK9Pnnn6tLly4uriHclGn9admypdq3b6+jjjpKnTt31ueff64HH3xQ3bt318cff6yCgoL4a4cPH65FixZp0KBB+uSTT7T33ntrzpw5Hqwd3OZF/ansyCOP1KxZs7R161Z99NFH2nfffd1aNbjM7brz008/JT3xevzxx/Xmm2/qww8/1KBBg1xbP7gr0/pzww036KuvvtI+++yjli1b6scff9Sjjz6qtWvX6oMPPtBvfvOb+Gv/85//aPTo0Ro+fLhOPPFEff7553rggQd01llnacqUKV6sJlziRf058MADNXDgQO2+++7Kz8/X4sWL9eijj2rffffV3LlzlZOT5hyZKNK2YMGCqKToHXfcEX+spKQk2qNHj+h+++1X63vXr18fLS4ujkaj0egdd9wRlRRdsWJFtdctX748unLlyoTHKioqoiNHjozm5eVFt2zZkv2KwBde1J9oNBrt0qVLdPTo0Y6VG2bwov58//33UUnRyy67LOHxWbNmRSVF77rrruxXBL7Ipv7Mnj272mOPPfZYVFL0H//4R8Ljq1atiu7cuTMajUaje+21V3TYsGFZlx3+86r+xLz++uvR3Nzc6KRJk6KSoh999FFW5Yd/vK47le2+++7Rnj17pl1mmCOb+pPMjz/+GK1fv3707LPPTnh8zz33jP7mN7+Jbt++Pf7YtddeG41EItElS5ZkvgLwlVf1J5m//e1vUUnRDz74IO3lcOlTBmbMmKF69erprLPOij+Wn5+vM844Qx988IFWr15d43tbtGihJk2a1LmMbt26VfvGOhKJ6KijjlJZWZmWL1+e+QrAV17Un8rKy8u1devWjMsLs3hRfzZv3ixJatOmTcLj7dq1k6Qav/mG+bKpP8kuXzr66KMlSUuWLEl4vFOnTul/cwTjeVV/JGn79u268MILdeGFF6pHjx7ZFx6+8rLuVPbhhx/qv//9r04++eTMCg4jZFN/kmndurUaNmyYcPncV199pa+++kpnnXWW6tevH3984sSJikajmjFjRtbrAX94UX9qEpu1nsprq2IUlYHFixerV69eKiwsTHg8djnJJ5984tqyf/zxR0m7poHCTl7Wn1mzZqlhw4Zq3Lixunbtqnvvvdexz4Y/vKg/PXr0UMeOHXXnnXfq3//+t77//nt9+OGHOuecc9StWzedcMIJWS8D/nC6/tAnhYuX9eeee+7Rxo0bNWnSpMwKC6P41fZMnz5dkghqLOdE/dm0aZN++eUXff755zrzzDNVXFysUaNGJSxDUrXLK9u3b6+OHTvGn4d9vKg/MTt27NC6deu0Zs0avfnmm5o0aZKaNGmS0W0n6tf9ElS1du3a+DfLlcUeW7NmjSvL3bBhg/75z3/qf/7nf5IuH3bwqv7svffeOvDAA9W7d2+tX79e06ZN00UXXaQ1a9bo9ttvd2QZ8J4X9adBgwZ6/vnnddJJJ+nII4+MPz5w4EDNnz9fzZo1y3oZ8IfT9ef2229XvXr1NHbsWEfKB7N5VX9+/PFH3Xjjjfrb3/5WbWANO/nR9uzcuVPPPPOMBg8erN133z29AsMoTtSfoUOHaunSpZKkxo0ba9KkSTrjjDMSllH5M6sux63zO7jPi/oTs3DhQu23337xf/fu3VszZ85UixYt0i43QU0GSkpKlJeXV+3x/Pz8+PNOq6io0Mknn6xNmzbpvvvuc/zz4R2v6s/MmTMT/n3aaafpd7/7ne666y6df/756tixoyPLgbe8qj/NmzdX//79NW7cOA0dOlT//e9/deutt2rcuHF666234suDXZysP08++aQeeeQRXXHFFerZs6djZYS5vKo/V155pbp3764zzzwzuwLDGH60Pe+8845++uknXXPNNekXGEZxov5MnTpVxcXFWr58uaZOnaqSkhLt3Lkzfplu7DNqWk5xcXE2qwAfeVF/Yvbcc0+99dZb2rp1q+bPn6+333474199IqjJQEFBgcrKyqo9XlpaGn/eaeeff75ef/11/etf/0q4uzTs40f9kXbd4+jiiy/WG2+8oTlz5mj8+PGuLAfu8qL+FBUV6X/+5390+eWXJ/xq2L777qvhw4dr6tSp+tOf/pT1cuA9p+rPvHnzdMYZZ+jQQw/lJ29DxIv683//9396/PHH9c4773CfowDxo+2ZPn266tWrp+OPPz79AsMoTtSfyrMcTjjhBO2xxx6SpL/97W8Jn1HTcrg/n728qD8xhYWF+u1vfytJGjNmjJ588kmNGTNGH3/8cdrn8PSAGWjXrl18elxlscfat2/v6PKuv/56TZ48Wbfddpv+8Ic/OPrZ8J7X9aeyTp06Sdp1GR3s5EX9ef755/XTTz8lXPYkScOGDVNhYaHef//9rJcBfzhRfz799FMdeeSR6tu3r2bMmJFw00UEmxf154orrtD//M//qFu3blq5cqVWrlypdevWxZezatUqB9YEXvO67SkpKdGLL76o3/72t9VujA/7OD32ad68uUaOHBm/h1FsGZU/s+py3Byfw11e1J+aHHPMMZKkp59+Oq1lSAQ1Genfv7+++eabalPgFixYEH/eKQ888ID+8pe/6KKLLtKVV17p2OfCP17Wn6pivxbWqlUr15YBd3lRf3766SdJu67vrywajWrnzp3asWNH1suAP7KtP8uWLdNhhx2m1q1b67XXXlPjxo3dKioM5EX9WbVqlebOnatu3brF/y6//HJJ0pFHHqm9997bmZWBp7xue2bOnKnNmzdzE+GAcGPsU1JSoqKiooRlSLvuMVLZmjVr9P3337s6Poe7vKg/NSkrK1NFRUVKr62KoCYDY8eO1c6dO/Xwww/HHysrK9PUqVM1ZMiQ+KyFVatW6euvv854Oc8884wuuOACnXzyybrrrruyLjfM4EX92bBhQ7WT7O3bt+u2225Tbm6uRowYkfkKwFde1J9evXpJqp7+z5w5U1u3btU+++yTYenht2zqz48//qhDDjlEOTk5euONNwh8Q8iL+vPwww/rxRdfTPg7//zzJe2aYp7KN5gwj9dtz5NPPqmGDRvGf8Ybdsum/vz888/VPm/lypV65513En7haa+99lKfPn308MMPJ4yhp0yZokgkwk3zLeZF/dm0aZO2b99e7bX//Oc/JVX/NbFUMF85A0OGDNG4ceN09dVX6+eff9buu++uxx57TCtXrtQjjzwSf90pp5yid999V9FoNP5YUVFR/GbAscsH7r//fjVr1kzNmjXTeeedJ0n68MMPdcopp2i33XbTqFGjqg1M9t9/f3Xv3t3tVYULvKg/M2fO1E033aSxY8eqW7du2rBhg5588kl98cUXuuWWW9S2bVsP1xhO8qL+HHHEEdprr710ww036LvvvovfTPj+++9Xu3btkt7lHnbIpv4cdthhWr58ua644gq99957eu+99+LPtWnTRgcffHD833PnztXcuXMlSb/88ou2bt2qm266SZJ00EEH6aCDDnJ7VeECL+rPIYccUm25mzZtkrTr8stMBrvwn1dtj7Try6r//Oc/OvbYY5n1FxDZ1J9+/fpp1KhR6t+/v5o3b65vv/1WjzzySPwLzMruuOMOHXnkkTrkkEN0wgkn6IsvvtD999+vM888M35PEtjHi/ozZ84cXXDBBRo7dqx69uyp8vJyzZs3Ty+88IL23XffzO4NGkVGSkpKopdddlm0bdu20by8vOigQYOir7/+esJrhg0bFq26iVesWBGVlPSvS5cu8ddNnTq1xtdJik6dOtWDtYRb3K4/CxcujB5xxBHRDh06RHNzc6ONGzeOHnjggdFnn33Wi9WDy9yuP9FoNLphw4boxRdfHO3Vq1c0Ly8v2rJly+gJJ5wQXb58udurB5dlWn9q65OGDRuW8Nrrrruuxtded911Lq8h3ORF/akqNib66KOPnF4deMiruvPggw9GJUVnzpzp5urAY5nWn+uuuy667777Rps3bx6tX79+tH379tETTjgh+tlnnyVdzosvvhjt379/NC8vL9qxY8fopEmTouXl5a6tF7zhdv3573//Gz3llFOi3bt3jxYUFETz8/Oje+21V/S6666LbtmyJaMyR6LRSpERAAAAAAAAfMM9agAAAAAAAAxBUAMAAAAAAGAIghoAAAAAAABDENQAAAAAAAAYgqAGAAAAAADAEAQ1AAAAAAAAhiCoAQAAAAAAMARBDQAAAAAAgCEIagAACLg5c+YoEolozpw5fhfFFStXrlQkEtG0adMc+8yuXbtqwoQJjn2em6qW1c/9/eGHHyo3N1ffffddQvl+//vfu77soUOH6oorrnB9OQAAuI2gBgAAAI649tprdeKJJ6pLly6eL/vKK6/UAw88oB9//NHzZQMA4KT6fhcAAAC466CDDlJJSYlyc3P9LoorunTpopKSEjVo0MDvohjBr/39ySef6O2339b8+fM9XW7MmDFjVFhYqMmTJ+uGG27wpQwAADiBGTUAAARcTk6O8vPzlZNTe7e/bds2j0rkrEgkovz8fNWrV6/W123d+v/au/egKKs+DuBfBFlYkEuKF1ggxQBFYlJGM0HuIBejUFDR0MAkzFsFeckgB6SRjDK8UE6SGE4zLMEAeSXJkJTMLmpjZqt5IWcUcTFE47Ln/aNhY9tdYH15fWH6fmaY4TnPOb9znj37z/7mPOfcfUgj6pkQAvfu3fufxO7tfPe1goICODk54cknn3yo/XYaNGgQZs+ejcLCQggh/i9jICIi6gtM1BAREQ1w9fX1SEpKgr29PSQSCUaPHo2UlBS0trYC0L1nib+/PyZMmIBTp05h+vTpkEqlWLduHQDg/v37ePPNN+Hq6gozMzOMGjUKMTExUCgUeuMBuveKWbRoESwtLXHlyhVERUXB0tISDg4O2LZtGwDgzJkzCAwMhIWFBZydnbF3716t51MqlXj55Zfx6KOPQiKRQCaTISEhAQ0NDT32q1AoEBERgSFDhmD+/PkAAJVKhS1btsDT0xNmZmaws7PDjBkz8O2333b7OSuVSqxatQqOjo6QSCQYO3YsNm3aBJVK1eMcde7TcvDgQXh7e8Pc3BwffPABgL8SHIGBgRg+fDgkEgnGjx+PHTt2aMUQQiArKwsymQxSqRQBAQH46aeftOrpmh99e+74+/vD399foywvLw8eHh6QSqWwtbWFt7e3znn5p7KyMgQGBsLIyKjHurt374aJiQnS0tIA/D2HmzdvxrZt2zBmzBhIpVKEhobi6tWrEEIgMzMTMpkM5ubmiI6ORmNjo1bckJAQXL58GT/88EOPYyAiIuqv+OoTERHRAPb7779j8uTJUCqVWLJkCdzd3VFfXw+5XI6WlpZuX3+5desWwsPDMXfuXCxYsAAjRoxAR0cHoqKi8MUXX2Du3LlYuXIl/vjjDxw+fBhnz56Fi4uLwWPs6OhAeHg4pk+fjpycHBQVFWHZsmWwsLDA66+/jvnz5yMmJgb5+flISEjA1KlTMXr0aABAc3MzfH19ce7cOSQmJmLixIloaGhAeXk5rl27hmHDhuntt729HWFhYfDx8cHmzZshlUoBAElJSfj4448RHh6OxYsXo729HTU1NThx4gS8vb11xmppaYGfnx/q6+uRnJwMJycnfP3111i7di2uX7+O9957r8fP4fz585g3bx6Sk5PxwgsvwM3NDQCwY8cOeHh44Omnn4aJiQkqKiqwdOlSqFQqvPTSS+r26enpyMrKQkREBCIiIvDdd98hNDRUnZDrCzt37sSKFSswe/ZsrFy5Evfv38fp06dRV1eH+Ph4ve3q6+tx5coVTJw4scc+PvzwQ7z44otYt24dsrKyNO4VFRWhtbUVy5cvR2NjI3JychAXF4fAwEB8+eWXWL16NX799Vfk5eUhNTUVu3bt0mg/adIkAEBtbS2eeOKJB/gEiIiI+gFBREREA1ZCQoIYNGiQOHnypNY9lUolhBCiurpaABDV1dXqe35+fgKAyM/P12iza9cuAUDk5uYaFE8IIS5duiQAiIKCAnXZwoULBQCRnZ2tLrt9+7YwNzcXRkZG4tNPP1WX//zzzwKAyMjIUJelp6cLAOKzzz7TO57u+l2zZo1GmyNHjggAYsWKFXrjCSGEs7OzWLhwofo6MzNTWFhYiF9++UWjzZo1a4SxsbG4cuWKVryunJ2dBQBx4MABrXstLS1aZWFhYWLMmDHq6xs3bghTU1MRGRmpMc5169YJABpj1TU//3yeTn5+fsLPz099HR0dLTw8PLp9Fl2qqqoEAFFRUaF1z9nZWURGRgohhNiyZYswMjISmZmZGnU659DOzk4olUp1+dq1awUA4eXlJdra2tTl8+bNE6ampuL+/fta/ZmamoqUlBSDn4GIiKi/4KtPREREA5RKpUJZWRlmzpypcyVIT6+gSCQSPP/88xplJSUlGDZsGJYvX25wvO4sXrxY/b+NjQ3c3NxgYWGBuLg4dbmbmxtsbGxw8eJFjfF4eXnh2WeffaDxpKSkaFyXlJTAyMgIGRkZBsUrLi6Gr68vbG1t0dDQoP4LDg5GR0cHvvrqqx7HMnr0aISFhWmVm5ubq/9vampCQ0MD/Pz8cPHiRTQ1NQEAqqqq1CtNuo5z1apVPfZrCBsbG1y7dg0nT540qN2tW7cAALa2tnrr5OTkYOXKldi0aRPWr1+vs05sbCysra3V11OmTAEALFiwACYmJhrlra2tqK+v14rROUdEREQDFV99IiIiGqBu3ryJO3fuYMKECQ/U3sHBQevVKIVCATc3N40fxf+tzn1gurK2toZMJtNKjlhbW+P27dsa45k1a9YD9WtiYgKZTKZRplAoYG9vj0ceecSgWBcuXMDp06e1nqPTjRs3eozR+TrXP9XW1iIjIwPHjx/X2tC5qakJ1tbWuHz5MgDgscce07hvZ2fXbXLEUKtXr0ZVVRUmT56MsWPHIjQ0FPHx8Zg2bVqv2gs9m/gePXoUn3/+OVavXq3el0YXJycnjevOpI2jo6PO8q7fla5j+G+SikRERP9vTNQQERH9S3VdyWEIfT+COzo6dJbrO41JX7m+H/uGkkgkfXbykUqlQkhICF577TWd911dXXuMoevzVigUCAoKgru7O3Jzc+Ho6AhTU1Ps27cP7777bq82Ku6N7uas6zyMGzcO58+fR2VlJQ4cOICSkhJs374d6enp2LBhg974Q4cOBaA7cQIAHh4eUCqV2LNnD5KTk/Umrfriu6JUKrvdu4iIiKi/Y6KGiIhogLKzs4OVlRXOnj3bZzFdXFxQV1eHtrY2DB48WGedzhUcSqVSo7xz1UdfcnFx6fPnO3jwIBobGw1aVePi4oLm5mYEBwf32VgAoKKiAn/++SfKy8s1VpNUV1dr1HN2dgbw18qeMWPGqMtv3rypNznSla2trdZ8AX/NWdd4AGBhYYE5c+Zgzpw5aG1tRUxMDDZu3Ii1a9fCzMxMZ3x3d3cAwKVLl3TeHzZsGORyOXx8fBAUFIRjx47B3t6+x3Ebqr6+Hq2trRg3blyfxyYiInpYuEcNERHRADVo0CA888wzqKio0Hm09IOsTJk1axYaGhqwdetWvfGcnZ1hbGystS/L9u3bDe6vN+P58ccfUVpaqnc8hsYTQuhcHdJdvLi4OBw/fhwHDx7UuqdUKtHe3m7wWIC/V4p07bupqQkFBQUa9YKDgzF48GDk5eVp1O3NaVPAX4mmEydOaJwQVVlZiatXr2rU69xrppOpqSnGjx8PIQTa2tr0xndwcICjo2O3R5zLZDJUVVXh3r17CAkJ0eqrL5w6dQoA8NRTT/V5bCIiooeFK2qIiIgGsOzsbBw6dAh+fn5YsmQJxo0bh+vXr6O4uBjHjh2DjY2NQfESEhJQWFiIV155Bd988w18fX1x9+5dVFVVYenSpYiOjoa1tTViY2ORl5cHIyMjuLi4oLKyslf7tBgqLS0NcrkcsbGxSExMxKRJk9DY2Ijy8nLk5+fDy8vLoHgBAQF47rnn8P777+PChQuYMWMGVCoVampqEBAQgGXLlukdR3l5OaKiorBo0SJMmjQJd+/exZkzZyCXy/Hbb7890Os2oaGhMDU1xcyZM5GcnIzm5mbs3LkTw4cPx/Xr19X17OzskJqairfeegtRUVGIiIjA999/j/379/eq38WLF0Mul2PGjBmIi4uDQqHAJ598onXcemhoKEaOHIlp06ZhxIgROHfuHLZu3YrIyEgMGTKk2z6io6NRWlra7R4xY8eOxaFDh+Dv74+wsDAcOXIEVlZWvfikeufw4cNwcnLi0dxERDSgMVFDREQ0gDk4OKCurg5vvPEGioqKcOfOHTg4OCA8PBxSqdTgeMbGxti3bx82btyIvXv3oqSkBEOHDoWPjw88PT3V9fLy8tDW1ob8/HxIJBLExcXh7bfffuCNjfWxtLRETU0NMjIyUFpait27d2P48OEICgrS2ii4twoKCvD444/jo48+QlpaGqytreHt7d3tKgypVIqjR48iOzsbxcXFKCwshJWVFVxdXbFhwwaNk4oM4ebmBrlcjvXr1yM1NRUjR45ESkoK7OzskJiYqFE3KysLZmZmyM/PR3V1NaZMmYJDhw4hMjKyx37CwsLwzjvvIDc3F6tWrYK3tzcqKyvx6quvatRLTk5GUVERcnNz0dzcDJlMhhUrVug9pamrxMREbN26FbW1tfDx8dFbz9PTE/v370dwcDBmzpyJAwcO9Bi7N1QqFUpKSpCUlMTNhImIaEAzEn21Yx8RERER/asFBQXB3t4ee/bseeh9l5WVIT4+HgqFAqNGjXro/RMREfUVJmqIiIiIqE/U1dXB19cXFy5cUG+A/LBMnToVvr6+yMnJeaj9EhER9TUmaoiIiIiIiIiI+gme+kRERERERERE1E8wUUNERERERERE1E8wUUNERERERERE1E8wUUNERERERERE1E8wUUNERERERERE1E8wUUNERERERERE1E8wUUNERERERERE1E8wUUNERERERERE1E8wUUNERERERERE1E8wUUNERERERERE1E8wUUNERERERERE1E/8B2LV09qaLxwSAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spyro.plots.plot_mesh_sizes(mesh_filename=\"automatic_mesh.msh\", title_str=\"Wave adapted mesh\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally let us call our forward solve on the new mesh." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Writing velocity model: velocity_models/tutorial.hdf5\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/olender/Development/tutorials/spyro-1/spyro/solvers/wave.py:238: UserWarning: Converting segy file to hdf5\n", + " warnings.warn(\"Converting segy file to hdf5\")\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: converting from m/s to km/s\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/olender/Firedrake/main/firedrake/src/firedrake/firedrake/function.py:317: FutureWarning: The .split() method is deprecated, please use the .subfunctions property instead\n", + " warnings.warn(\"The .split() method is deprecated, please use the .subfunctions property instead\", category=FutureWarning)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving output in: results/forward_outputsn0.pvd\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/olender/Firedrake/main/firedrake/src/firedrake/firedrake/assemble.py:209: DeprecationWarning: create_assembly_callable is now deprecated. Please use assemble or FormAssembler instead.\n", + " warnings.warn(\"create_assembly_callable is now deprecated. Please use assemble or FormAssembler instead.\",\n", + "/home/olender/Firedrake/main/firedrake/src/ufl/ufl/utils/sorting.py:94: UserWarning: Applying str() to a metadata value of type QuadratureRule, don't know if this is safe.\n", + " warnings.warn(\"Applying str() to a metadata value of type {0}, don't know if this is safe.\".format(type(value).__name__))\n", + "/home/olender/Firedrake/main/firedrake/src/ufl/ufl/utils/sorting.py:94: UserWarning: Applying str() to a metadata value of type QuadratureRule, don't know if this is safe.\n", + " warnings.warn(\"Applying str() to a metadata value of type {0}, don't know if this is safe.\".format(type(value).__name__))\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Simulation time is: 0.0 seconds\n", + "Simulation time is: 0.2 seconds\n", + "Simulation time is: 0.4 seconds\n", + "Simulation time is: 0.6 seconds\n", + "Simulation time is: 0.8 seconds\n", + "Simulation time is: 1.0 seconds\n", + "Simulation time is: 1.2 seconds\n", + "Simulation time is: 1.4 seconds\n", + "Simulation time is: 1.6 seconds\n", + "Simulation time is: 1.8 seconds\n", + "Simulation time is: 2.0 seconds\n", + "Simulation time is: 2.2 seconds\n", + "Simulation time is: 2.4 seconds\n", + "Simulation time is: 2.6 seconds\n", + "Simulation time is: 2.8 seconds\n", + "Simulation time is: 3.0 seconds\n", + "Simulation time is: 3.2 seconds\n", + "Simulation time is: 3.4 seconds\n", + "Simulation time is: 3.6 seconds\n", + "Simulation time is: 3.8 seconds\n", + "Simulation time is: 4.0 seconds\n", + "Simulation time is: 4.2 seconds\n", + "Simulation time is: 4.4 seconds\n", + "Simulation time is: 4.6 seconds\n", + "Simulation time is: 4.8 seconds\n", + "Simulation time is: 5.0 seconds\n" + ] + } + ], + "source": [ + "Wave_obj_new.forward_solve()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What was the difference in runtime between the previous case and the new one on your computer? Can you calculate the reduction in DoFs? Please also evaluate the error between the new and the old shot record." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New pressure DoFs: 8255, decrease of 84.49503202419189%\n" + ] + } + ], + "source": [ + "new_pressure_dofs = # ASNWER\n", + "print(f\"New pressure DoFs: {new_pressure_dofs}, decrease of {100*(pressure_dofs-new_pressure_dofs)/pressure_dofs}%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "New total DoFs: 24765, decrease of 84.49503202419189%\n" + ] + } + ], + "source": [ + "new_total_dofs = # ASNWER\n", + "print(f\"New total DoFs: {new_total_dofs}, decrease of {100*(total_dofs-new_total_dofs)/total_dofs}%\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "firedrake", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebook_tutorials/simple_forward_exercises_answers.html b/notebook_tutorials/simple_forward_exercises_answers.html new file mode 100644 index 00000000..ee2c95bd --- /dev/null +++ b/notebook_tutorials/simple_forward_exercises_answers.html @@ -0,0 +1,8599 @@ + + + + + +simple_forward_exercises + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/notebook_tutorials/simple_forward_with_overthrust.html b/notebook_tutorials/simple_forward_with_overthrust.html new file mode 100644 index 00000000..d042fc35 --- /dev/null +++ b/notebook_tutorials/simple_forward_with_overthrust.html @@ -0,0 +1,8125 @@ + + + + + +simple_forward_with_overthrust + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/notebook_tutorials/simple_forward_with_overthrust.ipynb b/notebook_tutorials/simple_forward_with_overthrust.ipynb new file mode 100644 index 00000000..f51e3c50 --- /dev/null +++ b/notebook_tutorials/simple_forward_with_overthrust.ipynb @@ -0,0 +1,471 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Simple forward with overthrust" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial was prepared by Alexandre Olender. If you have any questions, please email: olender@usp.br\n", + "\n", + "Here, we continue focusing on solving the acoustic wave equation using Spyro's `AcousticWave` class. Our main objective is to familiarize you with the initial dictionary inputs, which (together with the **simple forward tutorial**) should be enough if you are an end-user only interested in the results of the forward propagation methods already implemented in our software. Unlike the **simple forward tutorial**, this also loads a heterogeneous velocity model and unstructured mesh files.\n", + "\n", + "If you need more control over meshing, please refer to our meshing tutorial. For more examples of simple cases usually encountered in seismic imaging, please refer to the tutorial on using **pre-made useful examples**. If you are interested in developing code for Spyro, both the **altering time integration** and the **altering variational equation** tutorials suit you.\n", + "\n", + "We currently have a **simple FWI** and **detailed synthetic FWI** tutorials for inversion-based tutorials." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# Code in this cell enables plotting in the notebook\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We begin by making Spyro available in our notebook. This step is necessary for every python package." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "firedrake:WARNING OMP_NUM_THREADS is not set or is set to a value greater than 1, we suggest setting OMP_NUM_THREADS=1 to improve performance\n" + ] + } + ], + "source": [ + "import spyro\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we begin to define our problem parameters. This can be done using a python dictionary." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary = {}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The first dictionary deals with basic finite element options. Here, we will use T for a triangle as our cell type (try typing out triangle instead of T; it still should work). Lumped triangles (or tetrahedra) use specific quadrature and collocation nodes to have diagonal mass matrices. We have chosen 4th-order elements since they generally perform much better than elements of order 1, 2, 3, and 5 when paired with waveform-adapted meshes. For more details on this choice, we direct you to the Spyro paper at https://gmd.copernicus.org/articles/15/8639/2022/gmd-15-8639-2022.html. We also have support for newer 6th-order elements. The choice between the 4th and 6th orders is more complicated and hardware-dependent and not the focus of this tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"options\"] = {\n", + " \"cell_type\": \"T\", # simplexes such as triangles or tetrahedra (T) or quadrilaterals (Q)\n", + " \"variant\": \"lumped\", # lumped, equispaced or DG, default is lumped\n", + " \"degree\": 4, # p order\n", + " \"dimension\": 2, # dimension\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we define our parallelism type. Let us stick with automatic for now." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"parallelism\"] = {\n", + " \"type\": \"automatic\", # options: automatic (same number of cores for evey processor) or spatial\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We must also define our mesh parameters, such as size in every axis. Here, we define our mesh file location (we accept every mesh input Firedrake accepts). We will use a waveform-adapted triangular mesh already built specifically for our velocity model. This unstructured mesh allows us to adapt and increase element sizes across the domain. Therefore, with our higher-order elements, we significantly reduce degrees of freedom, computational runtime, and error compared with standard lower-order methods." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"mesh\"] = {\n", + " \"Lz\": 2.8, # depth in km - always positive # Como ver isso sem ler a malha?\n", + " \"Lx\": 6.0, # width in km - always positive\n", + " \"Ly\": 0.0, # thickness in km - always positive\n", + " \"mesh_file\": \"meshes/cut_overthrust.msh\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We also have to define our acquisition geometry for receivers and sources. Sources for this kind of model tend to be located on the water layer near the top, and receivers can be located anywhere on the water layer but are usually right at the end of it. The setup varies considerably based on the type of acquisition geometry you are trying to model. You will also notice we are choosing to use only one source. This is to simplify computational requirements for this notebook. Try multiple sources using various cores to check out our parallelism strategy and performance. This experiment is the focus of a future tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"acquisition\"] = {\n", + " \"source_type\": \"ricker\",\n", + " \"source_locations\": [(-0.01, 3.0)],\n", + " \"frequency\": 5.0,\n", + " \"receiver_locations\": spyro.create_transect((-0.37, 0.2), (-0.37, 5.8), 300),\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This overthrust model also has a perfectly matched 750-meter layer to absorb outgoing waves on every side except the top." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"absorving_boundary_conditions\"] = {\n", + " \"status\": True,\n", + " \"damping_type\": \"PML\",\n", + " \"exponent\": 2,\n", + " \"cmax\": 4.5,\n", + " \"R\": 1e-6,\n", + " \"pad_length\": 0.75,\n", + "}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We also have to load the velocity model file" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"synthetic_data\"] = {\n", + " \"real_velocity_file\": \"velocity_models/cut_overthrust.hdf5\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Our time domain inputs:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"time_axis\"] = {\n", + " \"initial_time\": 0.0, # Initial time for event\n", + " \"final_time\": 5.00, # Final time for event\n", + " \"dt\": 0.0005, # timestep size\n", + " \"output_frequency\": 200, # how frequently to output solution to pvds - Perguntar Daiane ''post_processing_frequnecy'\n", + " \"gradient_sampling_frequency\": 1, # how frequently to save solution to RAM - Perguntar Daiane 'gradient_sampling_frequency'\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also define where we want everything to be saved. If left blank, most of these options will be replaced with default values." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "dictionary[\"visualization\"] = {\n", + " \"forward_output\": True,\n", + " \"forward_output_filename\": \"results/forward_output.pvd\",\n", + " \"fwi_velocity_model_output\": False,\n", + " \"velocity_model_filename\": None,\n", + " \"gradient_output\": False,\n", + " \"gradient_filename\": \"results/Gradient.pvd\",\n", + " \"adjoint_output\": False,\n", + " \"adjoint_filename\": None,\n", + " \"debug_output\": False,\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now create our acoustic wave object." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parallelism type: automatic\n", + "INFO: Distributing 1 shot(s) across 1 core(s). Each shot is using 1 cores\n", + " rank 0 on ensemble 0 owns 5976 elements and can access 3113 vertices\n" + ] + } + ], + "source": [ + "Wave_obj = spyro.AcousticWave(dictionary=dictionary)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All we have to do now is call our forward solve method." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: converting from m/s to km/s\n", + "Saving output in: results/forward_outputsn0.pvd\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/alexandre/firedrake/src/firedrake/firedrake/assemble.py:209: DeprecationWarning: create_assembly_callable is now deprecated. Please use assemble or FormAssembler instead.\n", + " warnings.warn(\"create_assembly_callable is now deprecated. Please use assemble or FormAssembler instead.\",\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Simulation time is: 0.0 seconds\n", + "Simulation time is: 0.1 seconds\n", + "Simulation time is: 0.2 seconds\n", + "Simulation time is: 0.3 seconds\n", + "Simulation time is: 0.4 seconds\n", + "Simulation time is: 0.5 seconds\n", + "Simulation time is: 0.6 seconds\n", + "Simulation time is: 0.7 seconds\n", + "Simulation time is: 0.8 seconds\n", + "Simulation time is: 0.9 seconds\n", + "Simulation time is: 1.0 seconds\n", + "Simulation time is: 1.1 seconds\n", + "Simulation time is: 1.2 seconds\n", + "Simulation time is: 1.3 seconds\n", + "Simulation time is: 1.4 seconds\n", + "Simulation time is: 1.5 seconds\n", + "Simulation time is: 1.6 seconds\n", + "Simulation time is: 1.7 seconds\n", + "Simulation time is: 1.8 seconds\n", + "Simulation time is: 1.9 seconds\n", + "Simulation time is: 2.0 seconds\n", + "Simulation time is: 2.1 seconds\n", + "Simulation time is: 2.2 seconds\n", + "Simulation time is: 2.3 seconds\n", + "Simulation time is: 2.4 seconds\n", + "Simulation time is: 2.5 seconds\n", + "Simulation time is: 2.6 seconds\n", + "Simulation time is: 2.7 seconds\n", + "Simulation time is: 2.8 seconds\n", + "Simulation time is: 2.9 seconds\n", + "Simulation time is: 3.0 seconds\n", + "Simulation time is: 3.1 seconds\n", + "Simulation time is: 3.2 seconds\n", + "Simulation time is: 3.3 seconds\n", + "Simulation time is: 3.4 seconds\n", + "Simulation time is: 3.5 seconds\n", + "Simulation time is: 3.6 seconds\n", + "Simulation time is: 3.7 seconds\n", + "Simulation time is: 3.8 seconds\n", + "Simulation time is: 3.9 seconds\n", + "Simulation time is: 4.0 seconds\n", + "Simulation time is: 4.1 seconds\n", + "Simulation time is: 4.2 seconds\n", + "Simulation time is: 4.3 seconds\n", + "Simulation time is: 4.4 seconds\n", + "Simulation time is: 4.5 seconds\n", + "Simulation time is: 4.6 seconds\n", + "Simulation time is: 4.7 seconds\n", + "Simulation time is: 4.8 seconds\n", + "Simulation time is: 4.9 seconds\n", + "Simulation time is: 5.0 seconds\n" + ] + } + ], + "source": [ + "Wave_obj.forward_solve()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us have a look at the experiment we just ran." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File name model_overthrust.png\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAwkAAAK6CAYAAAByshDjAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOz9y69l23aXC36tP8aYj/WKiP04+5yzj32OSa51L5nmYrBcuEJykhI1/gQsCtwKkpWJQCmTUpKuYAkSXEBClCiQWbhQQzdBkAKRGASFTEsYgW1s48fZZ793PNZjPsYYvbeWhT7mY614rRV7x44dK/q3zzzrMecco4+xImK11lv7/ZqYmVGpVCqVSqVSqVQqI+5VL6BSqVQqlUqlUql8s6hJQqVSqVQqlUqlUrlETRIqlUqlUqlUKpXKJWqSUKlUKpVKpVKpVC5Rk4RKpVKpVCqVSqVyiZokVCqVSqVSqVQqlUvUJKFSqVQqlUqlUqlcoiYJlUqlUqlUKpVK5RI1SahUKpVKpVKpVCqXqElCpVKpVCqVSqVSucRrmyTcv3+fX/zFX3zVy6hUKpVKpVKpVG4dYmb2qhfxIvz2b/82P/mTP4mqfuXHVlU++ugjDg8PEZGv/PiVSqVSqVQqlcqrwMw4Pz/n29/+Ns49vV4QvsY1vTZ89NFHvP/++696GZVKpVKpVCqVykvhgw8+4Lvf/e5Tn69JwhM4PDwEys07Ojp6xaupVCqVSqVSqVS+Gs7Oznj//fe38e7TqEnCE9i0GB0dHdUkoVKpVCqVSqVy63heS/1rK1yuVCqVSqVSqVQqL4eaJFQqlUqlUqlUKpVL1CShUqlUKpVKpVKpXOK11iR8VfakXdfRdd3267Ozs6/kuJVKpVKpVCqVyuvIa1tJ+MEPfsBv/uZvfiXH+uVf/mWOj4+3j2p/WqlUKpVKpVJ5k3lth6l9lTypkvD+++9zenpa3Y0qlUqlUqlUKreGs7Mzjo+PnxvnvlaVhL/wF/4CH3zwwVOf/6Vf+iX+xt/4Gzc+btu2W7vTantaqVQqlUqlUnnTea2ShH/4D/8hDx48eOrzJycn/NN/+k+/xhVVKpVKpVKpVCq3j9dOuPxP/sk/4T/+x//4xOc++OCDpz5XqVQqlUqlUqlUrsdrlyT87b/9t/HeP/X5tm2/xtVUKpVKpVKpVCq3j9cuSfjVX/1VfuqnfupVL6NSqVQqlUqlUrm1vFaahEqlUqlUKpVKpfLyea2ShH/9r/81f+SP/JFXvYxKpVKpVCqVSuVW81q1G/3pP/2nX/USKpVKpVKpVCqVW89rVUmoVCqVSqVSqVQqL5+aJFQqlUqlUqlUKpVL1CShUqlUKpVKpVKpXKImCZVKpVKpVCqVSuUSr5VwuVKpVF4EW/w/sPX/On5hkD8A9xa4Wfle+rB87u6Mr8mQPwL/Hsj4z2Q+BZblewCWQD8G910QGY/7I/D3QPaO6++BTMbnN19P917/LZBYjpc/Av9OeX3+HOjBfRtQTocL/nD4cf7tI8//9+ItDuJdPludctgoPzZriO5TTuJD7rUnaD7jxN3n2+1bHLvMu/wBE38A7hBsgPwJhO8CAtqBfQ7uO+U6dMnDdM59vs+D5PlPywWn+uN0ec5vng0IM47inM/Wj5iGhsMw49P1A2ZhwmGYcTos6LTnnfYOvSYG/YSfOJgxDwPn6WO8fJe1tvxo1XMcj3nUrVnmxDvtCQ+6MxTjrfaYdep5mC54b3IXNeWz7gHvzw5pvbLSj3h/OmfqlPP0Marvs0gH/PqDgYncJdLyh6cXHIYJ89Dyo/Mz3psdogYfLy54f37MF8slZvD2dM4Hp6fcm8zIWXnUdXzn8IhVOuUn31KO5r/D9+48ZB7v8Hn3GReLH+Pzs7f5tT+c8K35PT48PeOobTloWn50dsq3jw5Z5YHD6Yf8iW/Btw7/gHvzDzmI79HlR3TaMQnf5mI45ffOjjgf/gj/v08bHq4bvjM/4sOLUw6blmmInPUd89jw6fKC78wP+fD8jPfkiIefdtzzM07PVxy1LWcXa769DqT/8DEHZ0tWn94HNeYnMy4eLJjfnbF4tMI5oZ21LM9WHN6dc/FwQWwj4oRu2XF454Dzhwti43HR0y16ZkdTVucr/tj/9JP8X/6X/9Mzh5lWKpXbhZiZvepFfNM4Ozvj+PiY09NTjo6OXvVyKpXKl0Qf/s/Q/X9e9TK+BMrvr4/4N2fv8eurt/iNxQkZT1bhMHacxCXvTs650yw48B3H0vFe7LjnB972A98JPU5AkGucy1ir45M84Q/7Cb+xvstvLU74cH3Mg+HgRqs2M47jmjvtkuO4ZBYSp0PLg27Gg2FOVofadYNOY+ITrU8cN2uO44Kj0OFEedAd8MV6xifLYx6tJyy7hjR4MMFUAAEDFMzKPTAFsfJ9MWD8HAQzOJ4s+cl3P+Z7d+7zrcOHJPF0Fvno0R1++5N3+ezRCYuLCU7kyiqN4BPff/czfvzOF/zv3vshh7FjIgOIoSZkHBep4Q/P7/FbD77Ff/z8e5g9q7Bv5VoM3MIRLjz+QpBUfqIChPsdd//dJ4Tf+QRyvtHP6br8vxb/T9pp+1KOXalUvj6uG+fWdqNKpVL5hpNN+M31HR7qhE/7CSq7f7rVILoSFJpBizLzRhRl6pQ7bkCwMQC+DsLEKTNJZI14dQSEh8Psxut2YhjCoB41h2B4gdZlVAU1oSzs+YszE7IJauVCxQQvuVyzG3BOaf3A1CfESlFk793l/2X3pSDscqbLgb4INGFAyKRUXpfU02dHpx4RIUge33Vl/QrH8xXijMmkQ52j08AwJiBOjIDSp0jKgeUQMZ5zH1RAx48m5TrS5VW7weiOHKYvJ0EQJ6jWPcVK5U2iJgmVSqXyDceLcNeviOQxoByxkgwIggIHLnHoMhPJNChTGZjKiwWNASMgBBPud1Pshr8uVEvw3vqexiXKKg2H4ccd9avB+fPI6vBoSXpQzISkHqQcs3WZ1iey7mUDlwLvMTGQ8bknxuXlGxddy3Jo6XLEieGk3PmEx3tlSP5KIjLiDBcMHwwXoDfHYA63l5AYxiI1LFLkfjcvpYzt808IxMc1i4CJYdkYS0Pl6U7xBHyYQHg5XcSmhnM3+3lVKpXXm5okVCqV20/6+FWv4EvxaIic5QkgNKLbLXERoVdPlz2qbuyWEVqBiRgnkradNmUD/ro7wcKBS0S/IknPLK7HwPz6iJQ41svu814d6+xZq79ytOcHnzJupm8C86kb0DFydgIT15Oy0A1ul0htXmzl//avYfuZlBahbb4w3ls1R8qepJ6UHI5SFckidAiDFy4F9FYqOXh42E84zy0/vLiDqTB1A37vEgXBB2VwgTgxjqZrRLQcxp5wL2Q8TwLJDj84cHtnV/CrTLAA88lz7+ULUfODSuWNoyYJlUrlDeD1jnCCKHPXE8glSRgRMTKutNOLcJ4bei0715kS2LL/eOLW95O5MI93AwuBWRiY+v5GaxYxFCGZw8YAWtkE8IKX/TTh+gmIUoJ6BTyGFyWI0jpFzbFM7e4yzZDNLv1GfzBWEASe3G40fprNc95NWHaRRd/SZ8f99ZxOPY/6GT4arlF22/zlmsUb2RynuaHH8cniiFaGS9famydZQLwxbQYO2p7j6Qon+pQ/qbtkR8zGn/eYlFC+J0kJjzrkJVUSbpgjViqVW0BNEiqVyu0nfOtVr+BLEZxx4BMzlzn0PULGKDG/YIjAYJ6VRtY4OnN05jnXSNm33vx3HYxs8CALgylzt2biOk7C6kZr3rQWmUEyj4nQa2CwsFEIPHbep0Wim2DYiWHmcCJkImoOL4qYcta1mMJqiOP5dyvZHv9SZlCEwE+Lfb1kDidrpk0iaaB1CQxWyZERxBmETElb9tY5Vi1cVpwqU9eza+UvbVaG0bieiRuIkkkqrPumiLiv/pA2xxQglKRHsiF578q8QFYkZZCX4z4kTm6UZFYqldefmiRUKpXKN5xkwoPcsrAIIkRnbGI2Q0jqSOroNbLUQGcOBZbEsX1FkGsGeAas1ZhJh5fEges4dkuOw4LAcO0d5ZK8lBdnE4ZcRNZCJrpM5Elaiafso4vhUBqnGOBFGcyxNg9qLIaG9RBJGkqlYnQCupwgyDaAL5v/ZW1i++nE7nPvtCQgYogoF6llmSLZYmlRSg5dR3a/RkuyxiC0ZO6FjkPpeW9+RhZP3rQjIQSBA9/RkngnnOHM6NPTKgB7LVICxQzKcLqfJICKQsrIS3I2MrVdtlapVN4IapJQqVRuPzY8/zXfYHp1pL1gVvc/x9FroMuBQR3L7Cn7257ePB1PEdg+hY3rT3DCjA5V6LVBzdG6/LQ4/jGyedY50msABLWAE+FOXNI4JYYrguJnHlgQUZwom517s7E6MoqWp2HAuzS+vLTkbA+7uV+yt9u/v0M/YnsBecqes/WUbghMY49gtC7hUEyK0RB66c3bdc1cz1w6juKaxidKauS2qgjDGLInZ4dmV+7z0+JvkXI9NqoqBCzuUgcBNAhqiW6i2PpmbWHXReSmqpRKpfK6U5OESqVy+xl+91Wv4Esx85m3wpq59PRZSDjYhpvF6N+JEpySLPIwNyzVs1THRXbcRJNhVo5pZmCZbG70Eyoi6esio22oWakkJHMkFYKDIBnPXj//81dFObOAlGuXvSg/m8NwDOp24uTx6aub3/KUz6/inDJvOtqYxqqN0Dgt2ow8Nm8F3Tv+RkwOp8OMM9fSR8cX6wPc6A0l47odVixVU2CRWla5Hd//7DB8K7D2dnntImg0dLmA5fKZx3hRzOwGf4oqlcptoCYJlUrl9hO+/6pX8OUwSOpYW0CfsKMbRAlu990ysKv8A+/E3Ti4y2YkDOfhTlhx4FYM6hi4mShWBPpxRoIaBFdkxw5oXOImathkDjUpwl0TvCgeZVBHlz0XKXI+THa2oONvt50MYSNsKB/s6vfZtRuZwSQMBF9sVs127VNm8uTsQiiD28QIMbOUwFJbPusOWeWAQ0tRQIwuB1apAYxVCpz37XPuRDmhNwFfROqbqodROqe08Te6nzfmuvlcpVK5NdQkoVKp3H7cS7KF/JpYqudcG0BoniD73d89N4OAMkEJcvN/5MugMCuTF9To1bPSCevx/DdFEUyMxudRF1GCbH+jQ8nYVuVxAtkcQZSJS0RXKiiDBnoN466/7WQIV+Jmu9JiNH53/N/ufRocg3gyZYCbGixTZJUaTAQ1g+T2nJQoYmYrA48bEoGEk8ygbtdOZIy2taEMmVPo8vN/SsJoYZvKzcvN3pMZdNKQD56XbHwJnq4rr1Qqt5SaJFQqlco3nEaUVvJ2YNjOAxMwuaRJMIOpU/I4IMHJzSI7M8aRbUpnjt6KW1Cym/+6EJTWp2LFOs452AxVe6F404qAubTs5HFH3XHSrjmIK7xkNgF/ef3+x0070Pjx0gs2TlElUQh+IPiEtUpz0LOkIbg8JmGG5jHZ2avebITQBtyJK+7QM5eB785OOW660tNvkPFMQuKg7QjBmDTK27MV0eVn6oINEJVyGoNtUcfKMpw0yGSCTl+SBSpUd6NK5Q2jJgmVSuX2Y+tXvYIvReuUk9Bz4jta2bTpjC49zrYd+mVoWfl+pgz9ukk0bmb0ZJQy/GwqSqDoEga7ubVmSVB2nfjZhKyj8FoY5zhcd23jJ2M1otNYAnYgAyEY70wXuO0057L7vxf9w3g/yi0qjk+j99N47PLSJireQxMTJkIi4h28P3/AgetRNWyciXB5kSXJ6DWAgvTCcViVliVKgpRNSHgmocM75bBZcxjXZUbCU4Pw8vMWpTySsB2kLeWcDqHpA67TpxzjK6C6G1UqbxQ1SahUKref9NGrXsGXQsbt47U6FMdd3+3Cays79lEyTkYffdgGvzeqJIztSaORKCIw9UorA8MNRMvbpVlJEJI5xEogq+J3LkE3rCd4MXSMgRXZxv9Ziw1s8MZhfFJC+JTg2658MjoI9YNnSJ7TxYyp6wHDSybnwJSMU0FTaRXaXoOWtioJwlmacJomnA5TzvrJmCaVpMhJES5Hl7eVj2UO9PnpFqibKcwaDXOAt5KE7C3fsqLxKXqJr4jr2uhWKpXbQU0SKpXK7ec1Fy4nE85yQxrtTL0bd8vHzhovo0f/XuAMm+6Q6wfimx11J0IDJA0sU8tapxyH9QttJJdk5fIbsxVHopuYasro3jSOQdiKop0ogzaoCssUSYwC3lFbUE5ho/3p6AglYLKbXKzs7pJIsT8dkidqqaS8054zDYnGJ2ZhwA+gnQNzuypFyQLAGRnhNLesLPDx4pjtrGkRAmX+gmqxiL3fzfloefKYiPpJd9K211KGqe3WLJhXkvTkeaw7/pVK5SuhJgmVSuX285rvgG52oWXTKjPGgCLgELJ5spVoVbYRK0Qy8UUu3Ur1YqUNvcXx2MJNGlnMSmAvUgJkFcgGmJLM4cWI7nrBrFlxcCotOSXpUCuzGFRhnTyYsEqBRd/sPP2F3c3icSHzZmbCboha+ZhVkGzMXYfPxUVJTcbzG4HMtjfJ9t6qOx2ImtCbY5Fauhx2rkuimMIny0O6FLi/npeZD0/9bbz7AZb1S+mv2puVYR6MTG5AuvTS/rxbTT4qlTeKmiRUKpXKN5zWGXd9x4nvaFziNEXcRm4w7ohnHIM5TA2PMpWSINwkXCwzEoCxPWbuFUExpATONziWjC5Eux39TU3B4QSi07Hd5nrHSuZJ5snqMHMIQuMGvDPakHCiNC4RXNolArb3eIKg+fHrsa3Uo/GZdW5wQVhqLFWL0b1oEhIoZQrx3oHEBDPhoOk5agfEO96eLxgkbtek5hAR3pleEESZhZ4xd7q8uEvXP7Z/5VFDMTj23WhVQLqBcH+J717OxOU6TK1SefOoSUKlUrn9pM9e9Qq+JFYCVDGwMr1XrjwfRGmckiWwUMdGzuxutP9fjgVj5QLPoVM8xv00x914g3pMDMbZBmabwWcytkldf21mQp89mI2WpGVWQjZH6xPeG7OYOGgSMs4k2FsC26ansR/r8qb4/qCwohvwTnGuBOCDBZIFgi9VkcYlZJz4fJVpGJjHnlnoeGt6TgiZTiNLbbbuRhlH9ErwmbcmC47jissjop9wH4sHKttbJnvFBAHp89bw6mVQh6lVKm8eNUmoVCpvAC9nd/XrwgzOc2SRm62F6PY5II72qBsGHB2ewYSbyI1FBI/gx4+DBs7zlMGaccrzzdYcJBXRrpRWpWyydR4q57uJJqEkBiJl7rCTVITQFF1AcMVutXEDtr/Wp/j7Xwp4rzwfXCL6RPQ94sq5vSiYse4jzi4nN9s2HAHNgjOldYl7s8VoJisMFsYWqdHhSIU2ZCY+cW+yfPIiry7YA6U4g+61OpkIrhvnULykSF7cyxVFVyqVbx41SahUKref8N6rXsGXxLCx193J5R1dobTiZNsaZQLQYDTO9lvXr4ngxuNe5Ol47KftcD97zUiZaqDbVqOin1CDQUfh743Ey2XvvnXGQShWsEG0zI/wA8vUjG5C20vZW47ttMG2yx021YV9p6VsggQjtspaW9Y5gCnnyykYRH+lArJ3nmFo6PqIYKyWDY0kWkm0MuBdcaEKkulywzpFLlLLRWqvd3sFcDttyq7NydBZxMReWinB9OUdu1KpfDN5iVNXKpVKpfJl2AauAnfDmgPX49AyTsx2FQXDlZ59dYgpJ27Y2Zje8HyGYhSHowPXAdC/wIyEzUyCfW8jGS1ag1ixbHU3a4USing5uKFMXsbhRfEqpOTJGZb9GHDrLpC2jYp5uxm+d1ds08qzkXwLkzajIojPpLFNCoTolXnbczRZs7M0Kp/aWBXR8dtOIfWBPDjaODBxAyCjjauwTA2qcNG3nPYt2wM97ycme3psobQfqSHLAf+we2mb/c5LtUCtVN4waiWhUqncfl5DV5arMwQaZ0xdohHlxHWAXbI7dVgJmF3GxNHhULvZP/KlbWYXCDZOORzP9ULbyFdjyjFrMUqLzM1QWj8QvZUEwQQ1h5oxqKPPYaxY2BhA26UkC3aXsLFCLf+7vEMuLhPCgBMtE533Ep1NkuOvujJduRSnCgOQYLWMREt7rlPKOgVWQ6TLgXV29OqvYYG6vQ2XpAsmQBBsGtDWv7TNfs1W3Y0qlTeMmiRUKpXbT/r9V72CL4VhJIOFFivN4zjstcqAJxOc4oSxVahUEYbHBM7PRtiIcQU1WKvnTFuCME56vtma2Xbkj5+NNj5qRlYh66ax6TnHGi90nRuWQ6DLcWy/yggOFc+kGVDnmca0rQxse/THpGFbVdiectOctau6mDouFjNSClhyBClTFDZaEFVYDbHoKZ4SM2dzhCYTm8yd4yXJBQYCZsXdaBIz87ajaTLH0563Z0v8Nvp/Bn4UECuXhjuYAdnQkwnavpxf6/IiHWeVSuW1piYJlUrl9uNfd02CkA2cgGejS5CtXafC9vONvlS4ebvR7mwl/rzQljxWJG66R+1EUJVLa/BmNJJxIgRnDHq91e0LnJN5uuw5Hxo8xmCBwTzr7Gl8Cb6FvJsVcLWD59Ip5fHvjTv6Xg0dhCCZiR9wFLHwamjIOg5s22YWuwTLieAd9DkQmwHnjEE9gwYSQq+eXj2z2BO8chB7DmNHlHx1cU+5F2CqW5ejTetRnkUsOCy+SGvY87FtVlqpVN4UapJQqVRuP276qlfwpTAzDvyCQ7+klQFnmdJ0X55X8wzmUSuDvhyl738qw7bt5mr70vPwIqMGogTD+eYK6LGOUPbhS7WjWItulpL0ZgGtGeN6BMMzaHEJylqsVXXcqW9CcSJ64iVvWma2QonNt3ffz+YYciClWLQTAgmPi3BytCDGYec/eiUJiX5g1q5pYsIcJByDeQZzSJkwVyxWxZiFrsyKUGWd43PupZUfuUrJBEfNxeb0Novk4xbSTS1vK5VK5cnUJKFSqVS+iViJZ5MpvZX+kiAD0a1ZmlC8MGXrehREy263ORqUlky8YjH6vEThajLROOVQeoYsdC/gc7FJVqKzsR0q40enpiE78rUNWke5sZSqQkk4SrWg9UOxPvWGx0jqxjamq0qKTYllnG6wX2lAdv0048c+e/rkWXcBA5I5BvVcpAmdNk9vN3Kg4kniOUtzuhxoJNNIwjtoJJWvXeJQOlZ9wxerQ67161jHl8kTZhaoIetU9BgvSTtQhcuVyptFTRIqlcrtR7tXvYIXQlGyKYoCiYXCQj1X+2OCZLwo4qAncKEOpATN+/89j6tB4FoDj3TCwpor53w+Zkb0ZV1eyhTo1mUEI5nQ68Z/6XqoCo3LNF6LQ5LTbUAvZnhTZq4nZceQ/WVXo+2ixl33vezBYBtUb+6RE+iGQMqeYQi0kgjkEqN7mLVdSRK23UqjsmFsCVvlQI9nwHGWJwwmZQAbJaGLMhDInHcTGhs4X0+uf2P18b4fA1yXcGcDrttrtfqKqcLlSuXNoiYJlUrl9pP+26tewY0pg80cJqAYS4WV+suBtRgmxmDu0pAyRehNwPJ2U/lJ04EfO+eedDmpcJomJBxpO8/gZo3pZkUZYSZEl8cdfMfMJ1RvFnCKM8I4Y6BoMnS8tiKC7rOjV0fKIPuTnG3/4+V7B/uCXGNXeCmJjJdMN8Rtq9Rm4rRzNtqibl69O4kbk5e1BgY8SQNnqS1tUuWMiBmLIZLMcaEt7urchadhoE4wv5vrIOP5w6MeL4LdfCz2tSgi8FpJqFTeJGqSUKlUbj/x+696BS9IGWzWm2NwgegHvGTERmsb28S6jsEcfXa00jOToQTULjGYju351w/KE5DFOAwLPIlOd78qrr+ZPCYIo/mnG7URarDSSAzFsvU62Gjl2mmg04jhGDSSzeMw0qjJeDTMUAn4jQvQdqAA20TAbFNOYKdbsL0kygwfM741PktzcmB7vzdvHZIrx77qhCrgfDlgSg6S4S1zLyzZxe7GSlvW1hLbzMJaDqYdwT/bPUp0TOA8WAsadymbIIhzOBVceDnjj8zstbQSrlQqL05NEiqVyu1HXk/hsmGssmdlDZ0FVCALiFfCaElqgEOJojReaV0iS8Ys4WWgR+lMrxXfqRkJozfoTJi4Fa1fci+eFccgbraZbFZ6fhpJeEolQFDUHNEZM99f6zhlPkEJ1FOGrI5BHVlhkRqWqWFQxyrFS05I47uvHGvv21snqD2XIzHEKy5mTo4vSN6xzpE43u8hQ5fiFStVyi6/wXIVWa0bSA43CLkPzN2u3S2bsNKGlTacpwbxEIMybQeeWaUxsAyWBd1UDMYCj2QFJ0gyxL0cd6M6TK1SefOoSUKlUql8Q1ETEpG1RVZaetvPLBarT5/I5nCieMl4p8Wu02WiKErCTOm1CJ+XlsnPyRQyxmAwWEkUkJ5EZu57Tvz68R7/ZyDFgIcopVUHUbzAYJHBHJ16TDZtTNc6IiJCwpPHAkB0ZT5E9Jk2ZI6a1bZasRU7j8e3re5gd8rdsLTdOUwdlh1RyjFBOE8TzIRknnaa6cURYtp7V2n/EQFVx2oVkAT0QsypXP/2tWyHvrlxxsW+2PppmBh4donBfudXUvxKaVYOmU5eSltQHaZWqbx51CShUqlUvqEEZ0Qx/KgUWFksbkGiRJdwkpn6VL4WZTIKYgPKXNYMKJldS8rzmns8gkdw4yMZRCmC4+v6+O+TxxagLntydmQVkjrMintQl/02iH8WZoJa0TaYCUGUdrzu4JTGZxqfOWx6ostodntx8s4Zafv1ZrbB1WrAuJYw3lcPBK8MOLKV4W8XXSSJ4/BojXd7LUKbYoSAqidYYhbXfOvwlMH2FQlwFFYc+DUR5a14gWVI2V/SOeyvqQitBXVgo7uRudHmVcCpID5gk6ZMsn4JwbxsBnBUKpU3hpokVCqVyjeYIBDFCBhJGy5yRA265JlKppXExCXElIlPBCm9/oZhe8PMHM//B9+JlL56ym73mR6wsIYMPMzTa21Q23arfuzNt9Ia1FkkbpMNQ81K4H+Ne1Di3pLktC4hAjPXo1LmI7RuILqMJ3MSV1ySEj9rCthTdAWaHM7KQLXWZya+rNuRWaUGxIEIsc3buHkr+jbBmcMQ2jgwaXsSnjRWChTBO6V1AxMZmMrAsVuSBvfE2P7q98wUMkjetUlJMhwOvTOF4Mscia84UTB9iuVrpVK5tdQkoVKp3H707FWv4AURPIoz42GaskgzztOUz4cDznVKb55kHrUS3HsyQh7djRrOrEHHNp2db9GzccCA50fDCY/0gHNt+WC4S+vzC16BMphjnQOd+tIe5IxJGMXXN9ieFitWqk60tBzt9Q6pluFtp92MPVPSMbHZt0HdC3afcnqH0oaBVgailHYhEUHNMbWMS0q3DnRd4JJJ07im6AcO2w7nE2ZSqhDj5OpSGVCyOs67hofdnB8t7qIio2vSlWseEzdxRfwt6rZL3qRCFh259aCCeQdmVT9QqVS+NDVJqFQqt5/82atewQsjJjwaZjwcDui14WyY8HA4wDtQHL16egv0GjjN0yJyVk8vkaVNeKhTBnVsdK7PI5nnizEJWeQZ99MRiKP1Zaf+ueu9Ept6MUw8Gc8itwiKQ/Ho2Jt/XY2D4R10Vmx9kjWYuaK/UEHVIyYsh9Hd5wkzEnYHK4/HHJ+2PUGCx3AidCky4BgUpBd8b8ytp+/8tq1pexwp9qvSJu7rlKGJnKYpXowoqWgQpFSHlkNDVs/n6wMSfq9d6RlVj3HItuTxINtKSEl83Doh/uUIlyuVyptHTRIqlcrtJ/z4q17BC9N643vtkqkoaAm0oQSTzhmdBXr1ZBN6jVzkloyj04beIoM1nNuE/rEhbI9jGBOn/ER7znthybH0NBhJS9uOl43C4dnYOAlaKGvERiNUKTvqIoZyvdkN5XiGk/HhDEVYq2eZGzAjZU/KjlUfMNyuv8quZgZsrUs3dqab695nyJ7z9YSuDzhK9cKL0TvHBQ2fDXNS8LhmM6tgN0zNN4qPsHaeixz5ZH3Ecgh7v2yNi75lmSZk89vF7BqvnnxPtu1DwZA0FkR8ETSbgWRDTLB8zZkLlUql8hxqklCpVG4/8nK8478upi5x4Aa8GG+HHqPYlaoaYkrSIugd1HE2tAzm6YkMRMDRAgfu2aY3tp0ZYDiM78ULWpd5N3SYOc6GKf01Jy+XuVtGdEVQvfmeGqxzJI9OQc1zZgPsH28zQE3IJRFAcGIonoQw4FhaQ+MzmIyiZNkbJrDpLdokC3seqJfuw+5jb67UThSylrauwcGFtUjQ8thLMEQM53OxUBUliNG4TGfhUhrSxMw0DkybgW8dnjEJA6bPqapsns6GM3Cp+OEKgkuGS0puQXJ+KZoEcdfTj1QqldtDTRIqlUrlG05pUSlOR2+FjoBi5pj4zEEcmMdE8JmJGzgIPb0FvAlTMSZizJ1eT3Q87qmXcDozJ3EsHW/LkmQ3a2MpjkTl85IgCJ1G1loqH2FMIp7vubRzN1JziBXD0Sg7MbGNuUAWRxMz3mU2OcIuL9jMJr58vVevX3zGgrImkIOQNOAlcz60rDTivOF9Lg5DV4YlmAlpCAx9ICro4Jj4gakrU6Kh3IdsnoNmzbTpaX3m2/Ozy1Oin3ojgATqrIiVx2qIBiG7MithI6r+qjUJpnbNuk+lUrkt1CShUqlUvuEMJkxc4tvxgu/EC972S1o30PrExA9M/MA8rDmKHd6VwDVRHHqiWGlVeg4bcfMmVcgmiFPMwWEzXEuPcOWIJPOk0bI0inLgO+a+R4BkRWy82+p/1to2wbXgnWE4BCM4ZbDAoBETGWdGKN6N13tFVLyZaMA4fflq0FsmJoOP0OGLADw5Ipngy7C6Sci8c3iOU2C40sIlRh4cqffEZOgyEDUx9x2bJCWZI1kZiNa4RJCMk4y3691fCYI1oGFvaHQUdBawrkP6fnNjv1KcqylCpfKmUZOESqVS+aZjiSO/Yu5XTP2K77SPSOpKLKhGIDNziTw25EAJgTs2u+7XaxQpiYLDIyxowSlJjBVwFNc3WvLGtnS7mS9KIwN+7PHP6lnlBuP5WgnYHKu0QglGNhBTenVb7cMs9KixHUxmV0YG2JVPNkH29msTNDlMBTNHNwjRJWIwoi+OR5OQmEri7mRZxl9vD7k7kdNMQ2LiBqJk8lZhLGWehUu0MqDZ4c0466YkmufcADblkiK63uguxoSH8xX+wQqG67Vw3RTV6/0ZqlQqt4eaJFQqldtP+vBVr+CFUTOSGNGvib4j+g6TIqQdrLgGKYKaR82T1eExGpTpGIKnG8R3IsbSPAsN9Dh6IJtwEDq4RmvQPrapFljRTrQujQJkMFX0Br+CNi5CgUSUxNQncBC9cRDX4wC0gXnoySp7GcB2NWCjxajshMty9RwKOghz6TlxA4exwyiTnhufCS7TWCaiY6Q+vnf8z4lxNO2RaEzmPU2r9Bbpx8RFBBpJoAIKpsLD1ewa17/JDMZUye30FuYdOGOYZHQYXoomYbzISqXyBvHaJgnDMPDxxx/z8ccfMwzDq15OpVL5JiPtq17BCzOgDGYE6Qiuo7dMFOXIrYqLEGO/fok5UTGaUb+wGZKbbxDdCcJKHZ1BNiXQc+jX4+78zaLE4kTkSOYxHGttSjuQKPOYuPF0LnNbu1DvFDVXbFTFlV9mZRwxk5DY+A6NXqflq52d0Ua+vHVK3XoLOSBDYyUZaGUgjecRUYKWliOu6BG2xxUjxkwMmem06EN6uyycd2KowjIFHvWTsuwrx3n82scPYqN2YtRESMk3zAxtHCXzeEm7/rWYUKm8Ubx2ScI//sf/mJ/92Z9lNpvxne98h+985zvMZjN+9md/ln/0j/7Rq15epVL5JuLfetUruDFmRmfKMD4yCbPibBQk46XoBkqQW0S92Rxqjn4c3OXGvnt3zXajgnDgeo79kkO3ZOY7DnzH1PVcN0nYxKheimA6jWLdtTYwagmcGK27WWvMZoZAHi1Z8yhoNoxIRq1oF8oAOdhmA/uyAfYSBBtNj7YvGHfqxXN/ccCybxi0tAE1UpIzD3gzcnr8XpgY2Rxn65bF0HLeN+PPqNi37vkgYcDKGr5Ih4TG8GFPk3D10EbJAG0sJijIRqdsZVL2cHfCMHdkyy9tmFod0FapvFm8Vr6Af+/v/T3+yl/5K/zFv/gX+Wt/7a/x7rvvAvDpp5/yr/7Vv+Lnf/7n+eKLL/hLf+kv3ei4XdfRdd3267Oz13U6a6VSuS2ICN7KTIA4Bq5RGg6lTBhYaIuIkFRQhWRu7x90Rzdag4rczJUmj+ecSM9EEnOZMZcB2TbvX8cCtegGZC84Hszh1OMljIPRlGno6PpwrWOqgg+Kd4aK5zxPOQrrknCYwzsjSKZLjj75bfJ0afzAnhZhb7G7b17qPTIu0oRVahjMF/chM3KC9eC50BZziujlvTZxsOhazrsJn5ye8L2Dh0zDADjUSpKhwP3hABPocTineA85AbiSCWzXUuZL4AFnYIrLggxuVzAw0FkEBd+/pDkJUgsJlcqbxmuVJPzNv/k3+ft//+/z5//8n3/suT/35/4cP/3TP81f/+t//cZJwi//8i/zS7/0S1/VMiuVyjeN/PBVr+CF8KOQWAR6dahFTBpmzujUjS4+jpVGZrpm6tkG58Lo86+CeLhugJ8xFEPwHEkiqXCRI2c6xWGjKuF5xykrSCaYKq0Tpn5g6gdmPpHxCGv67ImSGCw+d10ikM2R1JPUM2hgpQ0TGTBR+uRLNQWH9wa52MRu9AfbNW81xIa4cXjDOC+h2HxuBqM5TB2LLpLNlRYun2magRWBzgI0hvUKKuwGwxkicL5qGNaez88O+M47D7ftTxljlRpaNzAPHe+1p/y37u1x+vSYcIgVobbsrXtzH/pRr5x3zq5i4NUTtSHNW8LFzUTm1+IxjUelUrntvFbtRp9++ik/8zM/89Tnf+ZnfoZPPvnkxsf9xV/8RU5PT7ePDz744Msss1KpfNOw81e9ghfGidDg0NwgFkEDQ25IFsZ+f0evkbN+zjLFcXrvpqHFGES5MGFte1vpzzofQkAIIsy858QZZp5eA84ZXvZ6X55LaekJkskqxQp0u3FvBK/MQn/NYzHqGjyDerrs6LOn08imb8i58gheCWEz9YHR25TteWS//WpzW/Z25cuHMqBuMbScrVvEFIdy0i45mKzIrrQX8VilpgxyG7qWs8WcR+cHRXdgG8kxHMSe788f8r3JQ74/u8+32jMsXznKk1rEEsSVR9Lln6Wp4XtBXICTeREyf8WIkz1RR6VSeRN4rZKEP/7H/zi/8iu/8kTXBjPjV37lV/ipn/qpGx+3bVuOjo4uPSqVyi0ifO9Vr+DLIaWq8FF/yIf9EX/QH5PMs86BdQ50yZNV6HLkIjUkMwZgbTCYsVBhqcJKnx/kBRGiOBoEjyPZlEMvZFwJXLdB8XVsS0uwqzgGC6xyIJBxDJgJWR3OgdubwfDsdKHUMTr1JEqSJGJ4EWZhIEgu+YCzSzqMTXUF2TzG5YuNhkH732ObNKQsfH5xyI8evcVHZycw6jwOYsf3733B0WS1287fW6JkQVRIybPUho/7O3zSHRfbVtm98KRZcq9d8BOHXzD1/aXDPPZrzsAtBTLIIONQufH7CiaKe7jAn63hJdiVWrVArVTeOF6rdqO/+3f/Ln/2z/5Z/tk/+2f8mT/zZy5pEv7lv/yXLJdL/sW/+BeveJWVSqXy5bkscxXuhMTcZXr1nKV224oyaEClOOUEaYgy5dM+0chAK8ZaS3UAB3E86vPC+zCOKl5oBDxzl/nvZ5/yX5bfYmXxCWPInowIJA2ss+JRzmXCUeiIUqZIqwlJPQ4YtKxLKc89vmldWoHMHMkgq2fIniyl38aAJiRmodt+X9wmuBVMuXzQrTvRJnK3S/oFM7h/ccA0dUxiuZ8Hccmd6Zrv3/2CXgMzMp+6ns/u32G8yztUWWjDB4t7HPULjDLs7dvtKWUumSP6zFvTCxa55X+49zG//tl3Wedmt4hy2Xs3FEggaXcJYsVOVh5ckKdCXL1Et7/ablSpvFG8VpWEP/kn/yS/9Vu/xS/8wi9wenrKr/7qr/Krv/qrnJ6e8gu/8Av89m//Nn/qT/2pV73MSqXyTUNfQo/2S0auBOIGRKcchoEL3e3vmJU2nKVOeZjm9FaGlH3az7nIwsocHcJgwvUkwgWHMHOZPzX/iO83X/Dd5oyfmn3EoXTPf/P+dQisc2CZAkk9Z0Mz2rZCEGVQz4Anm2NQTzbPMIqxL8ekReOQzJHUMagbpxcXD6folFkY+M7BGSfNslQONvH//mNzNzcVBK48L7t1qznOuwmPllNS9rQR1DlMHMfNGueVEA0/HVCnl21XcUCxfj3tp9vv6zhbQQ0UxzT23J1ecGe65CfufD5WQGSbt+xjzoq+xO0tf3xI9OjRBIK/0c/nutR2o0rlzeO1qiQAvP322/zVv/pXX/UyKpXK60T+6FWv4IUoM4ZLpLjpkccow8Iew0hWWo46Fxks8kWaczf0HKFEgXiDbSGhBPETl/nfTD/j3MChdBr59YtvMfB8sfF2ZebIBJYaObSAmpK1JApJYdBi3ZqyjM5HnkQmyCbQ31h6OpIW8XT2jmxCtjLAbBqKRauIcdIuwYwf9XdgnNS8f2GbQB0ZXYQ2VYUnBOaSIfWBxdAwmBDNgcHJdMGj1QwTQ4XiPrQRQe9+JOQkfPbghHtuyTvxbKzC7GyW1ITjZsF35o4he4bk+J1H75YxeDJ6no4yEBNBHWMl4jLubKC93+GcR0W+8l1/U6uVhErlDeO1qiRUKpXKCxF/8KpX8MJsfHN69XTqeZAbEo7NFvLG4rTEhUKnpes/mWewyGmeMpjHU+w3r8smNTFgKpmpSxR3Hkdw1xUuX0bNs8yRzjw9gdZnvjW94F684CQux+THlWRoz6fJLomupVQdcpkNYSb0FlATJqFj6gZar3gneMeuWrC9LtmTEdgll1RjjMk3MbYJNlqcrlLLb9//Fou+oZGBKMpB0zFvlziviOT9AcwADINjuZqwWrU8Oj/gbDkZZzmMVydFCN464+3pBUdhzTz2e0mAlP6rBCRXhsWZlK/3iZ7uu3Ny4zEtcxJeysTlSqXyRvHaVRIqlUrlTeR+avFiHMrw2ARlw1ADL47ePIvcMPcdg3kMz7lOuOsyj0eXT2fjDSRA9J4DVwaFDebL3IBtjeOa+gR2ImYQDkKPjpUAU3jUB1QdCYghl2nSVlayieRlWwCQkjRlzyyUz70YjQwEr0zjQOjS6Hi02eCX/TxjnPtwVZMglxcMOAenqym9c/TOkT380aNPmLuhTLj2QjtJeIF+2N93E7quQQbDWWbdRs6XU+xOSbSKWasRRDED55RpWHPQrDlulzxYH5bDjIMVTMENQJKdeGO/QJKV1CR+Wj/mLTvnPhP+s71dqgpfBfIiaWGlUnmdqUlCpVK5/dhLFHN+TQzmSTg681wNzLM6cFZcgkYx8DJHjoLDNJA0c5EjFy5z6K8X6nlx22m+SQGLTJznPDW4cdrzddkE+2aQ1LHOkdYN+LGdae0ibmxBymPVQIJu6yWXQ++CG79YpsjMpzKQTGxMQUqr1Cb2LwLmzfuttPzI5bXttx6NqoDxlaXy0KXI54s55jLvTmcMMRGaTNNmYpMRFQanWL5crzETFquWRdewzpFVapjFATA8ViYnSKkYHIaOO+2a7x485MH6YHe1njIfYQ2uN0SvlD8E/ve/95/5P/+n/5V3h8X23J8z4e/Z/8i/k+9c+2f1VIxanahU3jBqu1GlUrn9pNd79klWYVDHWQp80B9c7rGn7Kzr2Lu+aWfpcuA0tfxwmHKqnhXCZ9pwnq//z74Th8exzi09kdYnDuOKd5oLjsPy+sdxsNbAIjUsc8siNZz2E5KGrbw3SIniizWqp0+OrJSWnzEQ3o0zUEyELgfWueF0mJBV8JRWGzEjusRxs9q1YzlD3J7V6fbejaLc8XNz5X5usw3KSW0Ap0LODRc64ZPuiE+HQ2IcaELCe8Vd6efaiYsdSQPnfcv95bzoMcZfv5vlBGfM48As9BzGjtan3ckBl8bhelLsT0s1pLz3//Bf/xP/93/zv/DOXoIA8BZr/q/8B/4n+/DaP6tn4apwuVJ5o6hJQqVSuf2E77/qFXwpFDhXz4M8YaFh209/GaE3zzpHFqllrZFeI0tt+GRoWKjnNHk+TpHTGyUKwtRlZr5n4tcchBVHYVkGo90AAfrsuRgiixQxkVGQbNvBYYex21YbkkbWQyCPV2q2/yjWr8vUskyRdQqcDS3LHAkuM48dx5OOd+YXvDs/w0veW8ieJZDtb8ez1S4/pmMwhyWPV3DJGFKgcZnD0HPSdrwzPx3bl648bO/ozuhp+HB1jx+u77DMDVomrCFS5jrMY8fU9zgxjtvldh2W2bX7bJY2VkacKv+3//c/vpr7bO85wP+RX8N9BVWAOiqhUnmzqO1GlUrl9vMa74AaZYrwH50+5HfXx2ST/Q3uKziWuQGMqS/D1lrxDF64UE/wxlSMpXlmqtd2O5p6492w5KEqd+IExfFZd3Cjq4CyQz9YIGnCxpaZzcc2JGbNACKcdy0pOfCevocmDjgne8mRjEctbk45DJg5Bo1M4wrvFJwxaChJx0z4YnkA6sjZbecgXHIz2rgcSanTbHOJseVIszCsHSsXOT+fcXT8gKNpqVQ0TSbPIr97Orl8yZsflAqfnx9jQQhtphmGopXwcDeutn88J75nEnrmcc070zMedTP6PoAJKmVoGn6/HQr+5A9/lzvrp1d1BDim53/LZ/w6797gZ3blOO4J1k+VSuVWUysJlUrl9mP5+a/5BmKjS42ZESWRzXivuaAhj1aaT94/XuVIlwLrFFimhofDnGSOKEbEiGI3ypvEwDvj/WbN/zC9T1LHRd4ExNcJHDdBfZlYvNbISgO9CSbGYbNiGgcmcWAaB44mazxKyp6UHf0QGZJ7ogOn4eg1kK3YqKp5EOEwrJnHNW1MiBOaYISgiIxT21xpKdo4jO5fiWySg03LkYDz0PcN3bqhW0XOzmfkleOOX3Lg1txtLvjW/NHuKPsOReVorIeGLxYzHq2n9OqIThms/Bo2ExDHt+bn/Pjxfd6eXfDu7BRESlvRZoDalT/KP/PB71zj/sP/yGfXet3TMLXablSpvGHUJKFSqdx+0h++6hW8EMUev/gMXWRPlMS3mwt+cvo5zwrO1TwPhhkPhwMeDjP63PBxd0xSTxQlYmUGwTVxYswkc+AyP95c8OPhAnfp/Du1wNORbZAv2Dg4LdJbEfKeNAuOmjWTkIhemTQZL4aaI2tgnSLrPtIlf+lMRfMrdOrJVgaugZUe/9DT+gHvlOAzzmnRJbBnsLodqLZLFexqqcY2drOw6iOLVcPZasJpN2XRtbxzdMbJdMndgxV35xdX7oWV9qjes14HnAnWexrLZHH0FshWZj+oCYpwZ7LguF3yzuyCWVgjSfAqOAOXN1LqsuRvnz681s/wHRbPf9EzKLqXL3WISqXymlGThEqlcvsJP/aqV/BC7GJV40I9bzdLjkLPcUwcu/VTw3IRSBY4Sy3L3NBrS5dbPu6OxkThZtGeCMydciADTh33YsdPTB499rrnt70LTsqueq+BQWU7XXniMifNEu8zIhCcIihD8qwHz7r3DNmRs6dPnrydyDxKmU1YpIaLNGHIEbFSa5mGxGG7LgnCtoJyVfgNmGxdUjfC8O3ht2Oby1C3B+s5Z/2EdY6c9xOWqWXerrl3cMb37n1O9JetZp2DIUW6viH3gX4duFhOyhoF8piybBIFcBy2a5om8/bxBTIY0oF0glwtin1NjkNm9phgvlKp3G5qklCpVG4/cpMxYt8shDKR+Dy3fJGnfD4+eudxG/Xqk94nMGhglSPr1LBKkWVu+ag7oMt+HFB2/VUIRc9wN/R8t1nwx2ZfjGHt5XM+8ygC2QQdrU4H9YgYWRzRZSY+jzv/meAyweftMDW1kiQkK85AZdd9dHJKroiYc7EZXecit4suM/UD704v+O7hw3EIXLFExY1JwLZr61J9YvfbcXNRtnttMsciNZz3U866KefdhMPpmpPZkqPZmu+99RlPSkTW64YHj+Z8+uiYz89PWA0NDsPhCKIEMbyU5OhCp2RxZPPMwoBLghtGZyO77nSKrxap7UaVyhtFFS5XKpXbjz09mP4mI1JadD7NBzzUGRfa8DBP+HyYggnRKb0+SZew4zy1fCKGFyGI8Fbo+HA4QFlyL66f+d4rq8GJcBwSx77jwCfuhjX302z7/HUwEzKCH6c3Z3Nk9bgw0LrE3WbJOrdlknIuQ9sY70PKAScgJrioKFbugUVSLi1BQRQnSkKYx47gMtFlUhby7JQfphOWfREbiOxVI8ahCuKEraWSA8vjjOa9FiQROOsmPFhPEZ9ppx3H4pm1PavUcTjrOJ4tOV1eFneLwMPlIcEr56spH9y/x8m3f7hN9hyKR1hpRJyRBax3TF0GyUV0zW4dX+fGvnNS5yRUKm8YtZJQqVRuP+kPXvUKXggz436a8cUw5UGacp5bHqSG3gTnrozcfQKlvUa4nybcTy1rdTxIE+6nCQ/ShE6vvzNcNt8dHs+PT8449mvej+ebM137OJuWnqTCWTfhbJhyOkxYpAZHJjplIgOIFS2By9uOGjMhZceQHd0QyMmP05SLLuEiN/Tqi47BIoNFGp85DGsOw4rDZsXb8wu8173WmY1GYX+R4/9dKS5sVc4mOITTNOVCW3oN3O8PaJqBg7ZnEhJvHZ7jZL/taOeH+mAx5+HFjAeLOV+cHY7XJzgpTUeL1BIkc0DHXbcs9qW6U1m/iv18VauVhErlDaMmCZVK5fbzumoSRDhwS07CgnvhjKNwxreaM77VXCAY2fYnfl1ms+srFEv9R0PkPEfup5b7qeWLoblRsOnEbeSyRAf/3exz3m3PeTdcvECvupDN01lxYErq6TXiBaJkZmGFR/GixFC29TczElJ2o+uRJ6mjHzatU4Lh6TTQqSOp205wDk65M+14e35BGxL3JovNMraB76UrkL1teinC8c3rzUr5wXlF1THgWGjLIrU8GqaEMBCbAQuKhr33jr1KJtDnwMP1jD5Hvlgc82B5OLZulerQe80p32rPeMctuReWvDW9wPlc2qQ2i/2aKwkAqq9nRa5SqbwYNUmoVCq3n9dYkxAc3PMXHPsFc1lzxy2545a8FZbkZ+oKdsGvCJxr4EFueKiRUw1knznXm3Wcyl468FZYcccv+GPzj/mp+YeUsWc3Q1Xo1ZP314pxEAfuTZY0QQnemDSZJui2MpKyo0+Org+s+4blOpLy6ONv0OXIIgcEI0rRNWRziBiHTc/xtKMN6ZIWQeSKpsIBbnQ62uoTdiJnTQIK5+uWLjkcymHTYa7MQpAmowGIupcojIcWWPYti75lMbRc9C199pTakDDxA3f9BUeyok0J1pDWfruGx4ylKpVK5SVQNQmVSuX285r2UpsZamVH/cSdc+I96xDosud0aHlW48mlgNcEwbEwR4+hknGiPNTIHUu4a5YUZOwVMjOSwfemj5AOznTKQVjzKE25zt6T2W592Txdjqwk0crA1A14UYZYWokW6S7BNuKB3bFT8mSB6MsOvJoQw8DgZhzbitZN8ChT341JSNErtHEg5oaT+ZLPzo7Qjd5gK0zeTVkr6xwTGDF2Y+wESx7NyrBuWS5m0F4QUBZaqhwrDeAe7w2ScTbEsm/50em90UFJeefwlNZlBMUQvIdA5vx8zsViThoiwWy3Tt10RV1fU/Jlca7uK1YqbxL1b3ylUrn9pN9/1St4IXQzW1dg5sGJcpYaHqUZP+rv8LzAb+NgZLB1DeqAxvd0ONbmWdrNfg04ik+/R4lkeistPiuN4+yF57ekiMiea5GQVVhqYKETOo1MXOIg9BzHju/MHgFG1s3Qsc1ByvX1ydP1gW4IDKkhZc86R1a5YZGnPOhniGUaSQRnzMJACLnMYoh9yQucgegVhyN2guVL7Ue7NaRVYOg82jvWy5bVEIulqwixMQ6n3VN3+p3A6WrKeTclxox4GMyhuCLuNsfxyYLJpEOz4NSQJ9zaTw5Pnnu/AT5n9vwXPQPnpLYbVSpvGDVJqFQqt5/wvVe9ghdiMxjYA0HgvdCRLLDIDYvcPv/940wCwZj7gcYp2ZQMrNSzVM/DHG9UaPHiCOJI1tBIwCEsNJQJ0HJ9L30Z19ep5zy1rFLDMkdWOSCSmYeOeey50654e3LO1lZoL0jfiIiH7BkGz5DKY7FuWAzFCnWRp1ykCU4MJ8UWtfWJ4FMRf8veQDU2G/NXB6wBTrAxkdgMZAOHDg5RIZnnop8yb3smbU8MuXwe+sfcVTds5j2sNLLKsVQ1xjVkpNi5WmAlAfFPzjdOp9cL/s9orvW6p6FqtZJQqbxh1L/xlUrl9iOvZ2eljFahTsojEzhwysNhil7zn+/R2JPToWWVI9k8p+pYY6xM6Uw5U7lRouAMFjajp6URx7vhgv9u9gXvxXPCNSoJV+ktcJFaVimSzGP4MuXZd8xDz0EcuDO5gDGMlq3gWLY6gWRl4Fo3RIYcuOgmLIaWTj2dRnp1RBmYuIGZL8d0G6sg2UsOZPc9tgmDsT+ReR9Tx2JoSqJinowjBOWoXRFIJVETxrnZGzH5Lhl4sD7gdHXAF2dH+NF71TA8yoOLI4bGsSSgjWFij63geLW61j0+or/xz+Uq1QK1UnmzeD1/c1YqlcpNeE3nJECJU/04OGzQCOZZaHupr/9plNeUwC5ZYJUiGj1nQ8sQFnifURHWOFqUyeiw8zycOFQ9J67jJ6YPcDLls+GAiSTW2fF5Or7m1e36/wcLLDVywHq7o+5dYuKEd+dnLFJDtgVn6wlditu3blqqBMjqMVOCFJvU867BS8KbcRxXCMrU90xcQzvarBYNgCvVAX28DmKbhGS31Cv3WEjZc9G3DOro1dG6MsBNDoVhHVmtGthr6zKzrbNSNzR0fWTRt2QNtL4vFrFWhN1iIHH88+uBfLnA8a3zh9e602+zuNbrKpVKZUNNEiqVyu3ndZ2TwM6dKGfPaZpzP08508kY/D87oN9vo9lsAvfZsc6RC3WoGWsVWoTGoPVyLR2siPC99oJ70fg8NWTraFwmkvnM3aT3fSMQLidVdfRaJis3XvEieAcRZdr0JPWoefrkMfHszx7YFAXMhGUfNjUHZiHRuWF8iUNEaSQhYkxiIttAnwIp7YL4TZFiNxdhd69tbHliK2HY3eMsDuccUzeQSFyECUfTNY9mE/rlBEywbQNYESGvh8j95Zx31xO63jOfWTl1BkyJMXE4XfPwUVvW8xhf3+5+nZNQqbxZ1HajSqVy+3lN5yRsAkAHPEiHrK2lz2HP+vT6AaKT0c5TBMRxlqd8lifczw0Pc+R+nt5wbUIr8HZc8IP2IXPpSLhrt0HBphIyipiBQYXzYcIX/QHLFDArDUZmpfVIKfG6d2U3fr/7ZTs6QB2mnnUXWPWB9eBZDJEH3QwZV3cQeiYxEYIyaRLTZigJ1baFaU+wPH5rnHe2O9n4SakKGPiSJCQrU6Rnri9iaK/4qFhj4Gw7p213D4wv1gd8tjzk8+XRduZDjMqdoyV3jha8dXTOW8dnZajaVb6mHKFOXK5U3jxqJaFSqdx+XtM5CSJlsm82YW0N6xz5NM3Y7EcXnl5RMBNESjDd+rzXz1921T9Mh6xt4EKMb4ee3ta0T7LQeQKb0WqRwDvR+GOzR3wynJQ+/5te57jWTluGPuAw1EqlYOoHps44jD3LZsVZN9nall42IxqrEYzzHMyx7Fruu9Lp0/bl+r81PUcFGj8wjT0pOzrzGG6XGOxlBIKN1QMrKnK9rEJ2TuhzZDVEuuQZVJiI0YbE8WTFF6s55ikzFxyIXk6ixMGia3nUTfnh2T0Omo53Dk5ZamTa9pwcLDiKhwxNyyOVy4nK1xiz14nLlcqbR60kVCqV24/dfNDXN4Ey3UBI6nmYWj7XCWfWMA8DmwjRnjFQbbNT70VpfC7uRqOl6lJbTtOUz4cZF9qyNM+pxhutz4vbPk6C8k4ceL9ZIKMA90bXup2bIHQaGNSzyDOSBVqXiJKZuMRJXHHQrGl92lmT7u/2G6iCqaAqdEPgdDO0LLU87FuywlvNOd+dn3KnXRJcYhN1izD+ZtwTNVtxRioJlu2E0yW7AYz10PDhg7s8vJiPrzeCVyYhMQweEMTJ4+JlE2LMpCagE2HlI4s8YSKpuFK1K2azHud0m/C9qg19rYWESuWNoiYJlUrl9pP+8FWv4Esx9caPTc5xomSBxhcbT7jeLC3BSiPQWCUY1HGhE5ba8nF/wFmKXOTAZ6m9dgC6qXL48TF3ylSUeVRmYaB1CV4oWXAM5llrcQvqNTKYMA9rDpuOedtz3K6Zx25zcSO2rZKIgFqZw5CTY7Wa8Gg9xURYa8tKWyZeudssuNsueWuyYhb7selptEQV2yYAbNqbUExApTgN2djCNWTPuo8su5aPT0/45OyY1RDIg5BTKPMjjMcmLwvgfeLwYIUGJbZDsUPVWJIRjCyOo7fOcdPMbL7evm/7+BoD91pHqFTeLGqSUKlUbj+vrSZhhzNwIgSUrMVR57okC6xzpMsBNU9nsQwXE+V8aPmPp+/w66fv8Btnb/E7y8Nr7xg7kW0lweG4Gzt+fHLGYegJ3mj8i0SwpX0qqadLnpX6koiIcNismMeeWexx+8LtTVC/Z1UqFH1CGiJDFzi/mPIHX7zFWZqQLKAmTPxAdEpwRgwZcbslbCsKG2Oj8fOrkbI4Yd1HzpcTzpYTLlYt6y7iKNOgJ75j4oeScOTHw+wYFM2elDyL3HKRW85TyzoHDPAYbTMQm8T0sCeEvHNmBey647K/ghC/dhtVKm8WNUmoVCq3n9dUk7BBzfhimHBI4m3Xs86RTGlhuQ4ixdXIrLTzZHWowToF+uz5cH3AB6sj7vczPu4PeTC03EgUPVYTDn1m5jI/MX3IgV/hKG03N22PyVkY1JNwJC2TF0yE4OBuW6w8B/PjULMnH1+2Q9KMnB19H1mtGx6eTulSERh7MQ5iR3QDqxTKe1xJNGw7bXl0ibq0fc+l+7MZWtf1AcuwGFoG9aw1kiRwMl8Sw9jStPcjMxNy9uTssRx4NMy4yBMucosTI4oRJRMlc3JniY+J0KaSxI3Jy9c1cVlE0CpcrlTeKGqSUKlUbj+6ftUreGHMjIRyEJYc+yU/1pyR1d048Nax6SirkAyCKKqOrJ4+O75YT/limPDFMOXB0D5T6/AkvEDrMtElvt2e8p3pKe+1j2jkpkO8hMECXS5rS+pY5YBD8aJMQ2IWevIoABaRMW6X0ZRof922PWY3BIbBc76c8+EXd3m0npABcSWRmcceEdgMFd60Hm0D+73fluIAv3Mk3ZxXzOFxiElpd/JC02ams4GTO0sOj9b4mHa6BIE8OGycfbBet6xzxAkMFgiSi57EZe7dPeXunQuiH3YWrcDZ5OuZuGxmuFpKqFTeKKq7UaVSuf3ox696BS+EmjFg9GZM3YLGtzS+4V684NPhugPLCiJC4xLTkDiOHSIQfCZ4ZRYTv3d+zNRlZm7gJyYPX6C1RJnIwN1wQS8ND3Ogz45e79z4WCKQzbMYAl4iqzzFGJiHNWbFV6kJiXUKZPNjH4xhuu8tyq4lxwATzlZT+iHQTQJBlOH4EZN2oA2ZO5MV5/20OEI5w3R0SYJtNiAOTG03P0GKXoGNY5HZGEwrSTyTpmeKZ2mBSeqwJJBh0Y9ZBqWasLiYorlYjK5bj04di9zQugVREq3zHLRrok+Q3a6QIXC8/nomLosTVF/foYSVSuXm1EpCpVK5/YTvv+oVvBACZDMGM1QGvKwYTHmnOeO67UCbikPrEt5p6cOXTDZP8JnoMk4U74xPuhnnqeGTbo6asfnvWmsVx3eaNX9iesZPNAt+LK5oKLaZL9KlIlIqCsscWauns1JVUBMO2o4YMgeTnrhxJjJ2guNNrnBVQyCwGhoeXMz58OEdPnx4h/UQmISBJiRmcQ2io/7AtiMTirGqbRa213YkJV9wo4jZyjq8N7I5VBzBG9O2Y950ND6j2VGU0GzvrgHLVct6GZmSmPmeWUiICEduzdT1eDK+TUwPO/Z/9u9+TROXTQ3nashQqbxJ1L/xlUql8g1FRAgieDE6bejNYwITlxCuv6vryBz4NY3LtDKgImQEFJwoToyjpuMiRXr1nI2OQpsE4bqJAkAQ+E7oeMcn/tjklB9M7m9dlW5GCbqzeYbsCWI0Lm/Vs96PNqPBtu03m/EGstEpbAyKxvfYmD9k9Txczjhdzfj40R1EDMUxazKzWJIO2dcdbJMOG0e/sdMq7CFOOO8nXPQtqxTJViY8z8LA3CVmfmAW+/0BzlvUBMmwuJjSuESyoptwozPSwlri2x35AOJxv5d4fX06gTpMrVJ5s6hJQqVSuf3kz1/1Cl4Yh4B5TvMBCz1gpS1/0L2Fv4b3pY2764rjLE0Z1LHWSECLJgHHoKFYovaRz4YJv7M65g+Wh/zmxTFJb5YgjGelFeOduOJOXPKT88/48ekX4+yEmyAoAcxQhCHLdiyBk8zduLzscLQ7/RhAbzIHeSzWhzJH4dFqxv3lIWfrCcGPsyRCxjvDuaJPkO1jPMimyrDRFYxVhM3p3Pj5SgONDGW6M4lWEpGMZdkpJUy2DzB6DSz6lm7wCIpRahi9eTxGiBk3HYiThLhX0fpTNQmVyptETRIqlcrtx75cP/arxGOc5ikXecZ5nvL5cMhS2xLIPifwlj070GQOVUeXI4N6HEaXIn0q9pun/RQvwn+9uMsP18f8l8U7/LfVnU1X/g2SBaER5a14wXG84DAueLc9oyHd+Np10/Jvjl4D56lhFnoOQ89R0/FWe4F7yqC8zdCzffY3350Iq3XDuo88ODsiukwbEo0b8JIvz1+4epRLLkebDyU7cV5Za8ScY60NgjHzPUGVoff0o7Xp3hsxiih40TWsuoaLxYTGZRrZDZGb+IG0CiTnUGdI+Pp39atuuVJ5s6hJQqVSuf2E77zqFXwJhKyRlbYs0pQ/7O5uW4Tc3o72sw9hOMmoODLCIkeGLKyzY50DX3QzbPQFWubAp/2MD7sDPlpPi0vPDRMFRXBivBWXvBMXZQrzC0aYyRzZPMkCnTZ0OTJ1PVPpCZIZ1BUL0m31YMQYxce7h9vKCcrrsjq6LtL1gX4IhJAJQfGbXfr9+3tJ32CXnxtxDpyHZL60GmHFZrVZgxWdQtpURDaKhL1Ewcyx6BoeLg4YBkcQxRAmrkc7YX02wbJjsZ7caE7GV4G4aoFaqbxp1CShUqlUvsGIwN3Qc8cPOHEcuITiUHMY7plWpZvnzITWZ4Ikoss0rrTWTEMmSGYa0rZNJ4jxn8/v8hsXd/iwO+Qs3dw6M5vhzbgnA285mOKYx4EgN2s5clIC61UOdBrGCcyBElkr6xRJVkz6LkkINknDfulgdDgqH20b56+7wMW64dHZfCt+nsS8U3wzJkm2q8zsOx7tI3tahvN1g5jhUSLKJCSmviNlv92R37drFVdU0hf9tMyvSDvzwZZEOySalHBrGAaHhc06bnRLXxjTaoFaqbxp1CShUqncfux6NpHfTIS5H4hkoijvN4txV1rIJs9tASmBf6JxO8/96BIJoXEDzsEsJCZuwKzIcjOOT7oZH3UH/OsH77NIcVzJ9YLEVuCez9zzmSmwxuOcMgkDQQZuNKhtHGy2zo5liqVyADS+CIKnoedSjcNKUnTpvjwm8hVMy1cZx9lyzqPFjEYSdydLjpsVh82KTWvR1Xs8GhvtjI5kM1DNSAqLPnK2mvHFxQE5gTfFMvT5Ga7jYx7jnJLM82B1gIjhBYI3vnv3IXdmS06aJcftGhm4Ut14uVQL1ErlzaMmCZVK5faTXs85CRvCGCgfusSxH7jn1yR9dhUBdsFt6W8vVYNGEo7iGuQk07hE6wdOxpYY0/KL4aPugN9bHfO7qxP+3aNvcz5ErhvciwhePFE8J0H5fnPOVAa8KMFxreMUJ6IS/GcTknqyeXoL9OM05Fkz8O3DMw6aDrl0TNvOEXjsVLs8Ya/VB3LJGGhdInplEpXodOeSxF51YnvfL/urmnkulhMu1i2rvmHVRxZdSzeEIowOaStsLue9vLjolOOTC4jCeZ5emvsQfeZ4csF700e8f3Kftuk3N/u59/Lxtd6caoFaqbx51L/xlUrl9hN+/FWv4EtitJIxg14dp0NDsem0a8wgENY5lnYdDfRahLNBMkkDvXrWOXC/n20DTlWhU89vLu/w++sjPk9TfmN1j0fjea+Pw4ngxbHxKS270c8PWEWKLeignqR+/OjIWqZGN6PQuPUDR+2a48maXdi9lzLsu0Dp5ePL2H7kRJlOe9YpMnM9rSSClMpNSRD2PVZHrkoSNp87oR8ii1VDIDML3bgeIXolxscF3AIElzg6WBG8IbEIky9y3B3eYBIHpvOBMM00kwEDPjk8ee69BPic601mfhbVArVSebOoSUKlUrn9yOv7T12xwTTmvudCA7+3PuJMJ9vWn6te/dv3jdN/RYyMY7CiYxhGEbAHeguoChdDw6D7QljDO3iQJiws8EWa8lk/HZOEm+xICwsVHuSGiZTBZ62/fsvKZpN801pV3I4UD0zCwCx0mAhNUGbNQOs3PTiX11CuCFQEk80U5c2zRtMm1AnZCfcmF3x7fsq3Z6dMQzeKmG03hGFzbynuSzYmJNucQcGyIOrohritOmQTlqlhbYGriZaZ0DSJ6DPRZyQqbTuw0sk2CezxuKnhZwkVYTofiE3i0eR6wf8ZN9eWPE7VJFQqbxLPaJCsVCqVW8JrugNqe1OPxYxDt2alUioI8ETx7O69xWVo48E/ZEcKZUBXUmFlkS5FsnnUShK1SzjKlGQx4feXRxy6ATIc0/Nj08W1O1wMGMyXNiNvRFWys1LFsOv9+imDywzDFetWMcSV4LzLERRWQ2SVIiq7/qL9Sc/jLbhkSGQbPYEzxBeNR5CEYMxDxyM3YW0R78ubUr60oiui6KKUFtx4MmXiBmY+jVWQxEVq6PFkESyADFdmPJgQfKaNmWSeZGWmRTaHQ1noBN9m/CTjYiKEhLjM8fp6epsjvrwNcNUtVypvFq/v9lqlUqlcl/T7r3oFN+Zya4fQA5153mtXozvPk/rit+/GcGT1ZN1UEIolJ2al797lolUIiZN2WYar6W4Amalg6vhiPeO3zu/y+4sTfm91wgfrg2tfw7kGFhrJUhIb7ww/Oitdv21pN6eg08BaAynDeggMySNAlzyLPpJy2NRX9u7j7lT7LUil+6kMYkjZ03UtjxYHrPqIl4SI4ZzinO2mW19qMZLHv96eVJi4gSCKAw7ankkzgFfwBq6cV7aVCWhiGs9VkofBPJ0FHg4tnQqRVOYm5MxB22FAPzR86/zhte7i2yyu9bqnIVItUCuVN42aJFQqldtP+O6rXsGNuTpXwJNxYnhKsM1T2oyg9PKrCarCoI5BHc6KMHbiU+l7d0YTEhOfCQLHzWZHemyPyULOgmbHh8sDvlgXt6NfO32H9TU8+tXgfmrIJkwk8YPmnKSQVMh6vS1p22vnAcOJIhgxwLzpmcXSwjTxPW6cULy9L1cC+DJ9enx+P3FIQh4ceRDISkqe4I1JGLgzWSDjrIL9e3N5kW53ntFa1SH0qQi9vSvrcmLMYkcc3Zj2DyUCw+DJeUzOxuM1ZFpvTH1m7nsazTSqNKYcxA7VS+qLl4pZtUCtVN40apJQqVRuP/JV9GO/GjaahInvmbp+1zm11+5iVoLyrJAzpCwkLYlCzg5nxswX4fMsDGWol3miS3jJOJTWZ5zp9tBmJUnIydOnwA9Xcz5YHfDB6pDfOD8h66VFPIYA32+W/PeTR7wXVrwTeu75nl49nW4EudcRXo/HE2Pqi5WrAxTHJA60IXPQJI4nPdEr2GZYGXuT1PZOcmVacvEuFTQLzhwXQ4uIcdR2/OD4IX/k5HOO2u7KpZYqgqjsnWj/JcWNajU0ZIVBPdNJx2w28O7JgruHF7Sxv+RulNWThkDfBRitRhOObIKZ4EXxObM4a3n0aMYXD4++1v6faoFaqbx5VE1CpVK5/byGbRIbPYKiqBnOjFYSU9ePwbwfXyfboN6ApJ4+Fe1Bzg4RQy0waEkcNpvoux1ow7vMYoio+FGNK+NAMiniZ4T7/ZQH6wk6FR70Mz7uOr47PX/q+jfx670wcJE77ruOP9qc8ruLkyuvu9Kbf/lZwFAVJqFYkk7DwEDZvQ8uj0PhBroQaCQxELbup9uQ1i4d7nJn0FZ17FF1qDqOwnpsFVJyKyyahge612a1V6zYDnEbNRyw8TJSGp+Ytz3i4WS6JssZ6xTwg+CSJ3UBHbUZ25+jN1xjpHFw3FobYAE4juYdPiRWXcu6j3ydjqTVArVSefOof+Mrlcrt5zXUJGzajTYxrMpmf1oZ9kS/211/K735fXZFpJw9JYR1ZPOc9y1ZHd4pHi0aBCu6BTPHYhh397c775cdfdbJM/GJzjxLjSyfNRjsEsrUDUz8QOMzPzZ5dPVKn3EPGAejGcEpTvK4q254MUQEJ7qdqdAlz862aOz4t40p6t5cgsfsSwXLIKq0bihTl33iMK7KXAllFCfLpVkJl6Yk7InIBSX6jPeGipDwJDxtk7kzX7Jdlmwcqkr1RwUye1avxqiP2B170mTm0xVJ3dduNlQtUCuVN4uaJFQqldtP+M6rXsEL4cThkbIzbQkzZeIzrUuX4lyD7S54Vo+aR8fe9k0rEhiNz9sdazElj5qFPnuS7oL+rfi3DBPYnuRHywM+Xs/5dD3l0dBykSJrfV6hRhCnBDfg/cA09sx8z3XHBW/i45SFPped9TJFuiQ6UTKrFDnrJ2TZT1zKHdpORpaiFdiedV8TQHnus/NjFv0U27ZSOVQ9pjvdwROnTtsuATGFJmSyCX0uzlHZ3NbCNbqEyBPadkzoh0CfHF0OqEErGb/XKpWz0IuwngvuvY50kJ5lcPUSqJqESuVNoiYJlUrl9iPtq17BCyM4DEF8Yh6WNLLijl+w2x/f6BZKIJpHLcKm6WXTWjQJu7RCRMrutBmL1HKaJjg/vmOMoC9NFzbBOeHjbs6PVof8cHXE768P+OEw47M041zdU9UJasWpp3UDwSW8G3i7Od85Bj2DrYWpCWuNJCsJzcXQ4EwRU9ZDRDDOuxaxMSnY2JLuJSK291DZXhb7T2b1DNnhKQLpjch51bWQZddfJHZJT1Dyqd15Zm1P02Sydzzsp2TbDEwzgjNmcSD6dKmasTGs6nLk/vkhq2HXNrWhU0973HPyzjnztxbkGZuus6+FqluuVN4sqiahUqncfl7zLomB0urxIAcSjuO45rMh04/997pJDlTI6rAr28syNs47KbMTyjdhEjLRJbocECmD11R3LTA6Rqk2RrArDVykyMPc8DkNn+YpKwY8ypFfP2HlxtI8a3MkDJGByTiLQFDsuftUm6SmXGNSR6+uiJ9zifKTyp7/0f47NyH2Xqg93ofNpOU9gQYA0Q+kHMp8CYGAogmGLpQkgdKbv42WhU3f0KVVrzWw0oaYEp+vj8jecdys8SgeI4iCCmq7CN8EUg4wgDr45OKYQ+k5dksslNaq0nKlJW2MZebE9SP3LxfhVwvUSuXNo1YSKpXK7Sf/3qtewQtTJh4Igmfqeyaup5HE1A1FLCuMtplC0qJD2LTXwNjTP7YbeTGS+q0FqZrgxIgulaB6rw/H9lw6t8dSeJQjDzTyeWr5JE35ZJiSniE8jiSm9BzIwBzj2CkP+jl6jT2qsjtfqhpOjGQOHQeWZQk4D3cmK7xT7k6Xo4aBUnnZLyQIZTbBpnVHNnd2c48BKZOXzYFqaSpyAncOVhxNFkg2SIJk2XM12t2czdGcGF2OnHYTLlLLRWo47Wd8vjoo7lOD0A972onNGtTR94GuD/RDZDm05CRk9dsqhRdFzFj0LX0fwQkfHR8/9z4CfM71JjM/jWqBWqm8edQkoVKp3H78t1/1Cl4YGe02F9YgTmhD4sCvt5riTVCsxpUKwi4ITVqCV0TGHnlPVlhnX94rjklIOFH2g+fHDiXCUhs+6mf8t+URn6S2BPDPWH8UuOtLzWOC4RXO8+Rm92ArXC6TkcUZgwYGC3hfhrMdxjVH46yHTYKwH9Nu2pAuaRQExJXvl/YjY9qsMXN4MqB0Jix8w+xwjWx6gvJeknC18UsMJ4aa47RvWeRIUiFKpiXx+eqAhMPEygyGfa2DFcvZrnc4U9YWWaRmm9Q5gVYSLpe5bGbCo+n8WvfwjC9nA1wtUCuVN4/XKkn4N//m37BYPH1q5D//5/+cf/tv/+3XuKJKpfJaIDcLSr9JGLA2z8oivQUutOEPuruc6xSwnQbBZNvGI5L3LE4FJw41oU8etfIaL2DiaUPeBszBXem/Gbt19h1EBaXrGz48u8N/+Ph9HnYT0lO6UIqNazn2oQx8Nsz5zfXbPF3B8DQ2O+mlBSpZaTnq1ZPNjT3+xR6VS6LgsXrw2OA521YTMLAsxe6UwB+s3uLT7mBcv2DikGg0TaZthrH3q8xU2LUr7bkmURK3nCGiHFjiUDruNMuinegbPrw45vP10Zgo2PY9pTKkhDbTxPJzGSjXCCWxOZ6sOWmWtPSQ4GS1vNYdPKK/4T2/cseqBWql8sbxWv2N/7mf+zl+93d/96nP//t//+/5W3/rb32NK6pUKpWXiwAn3jhxCa+RizThPE+3z9s4bMsQEPBeCd7wQbeVgeiG4pIjxX8fdvFtUvCXgvZdj//lfpyyli6VVphFN+H+as6Pzk/4g4tDFk9wOdpZdwp340ArysPcjv7+108UzGTPJciNrUBG6xKqwjpF1jmw1rgpDezajZ4gXr50beMSnRgOI6njfjrgfj8rYmYTZrEHFcTcqGewMf8Y3ZJsl1SZOlLvySkwMaVRJarS2sDZsqXxmVWOJbEI+2sxkEwIWhIHAXHKLAyXHI6Cy/Tm6Z3HGuNbjx5e6x6+zdM32K7FRptSqVTeGF6rJEFEnvmP1E//9E/za7/2a1/jiiqVymtBej01CWa7sLbLgZSntBa31qb7r9tKd4UibhXDudLS4pywzBGTolvo1aEqJBNUhFkcaFzauQlx+eMGEaNPkfNVy6PlhPPlhE+WUx5o4IM04aMUHqsqbKoPaq5MDVYr4t/LIfszESnBejbBjcdsJBNEGTSQVFjnyCrF7XE3cfd4g/YXs3V92n7bQx4CmgVvJfkQcbhRGDwPA4ftCk0lIbu8dNnlIVuNiMNlxWcFNfrkWatnEjP3Jku85NE4yjC3cUoSHA7U8CiIMg99aV3aaj6MR92EySRxNFuD06/vt/imv61SqbwxvHbuRn/iT/yJpz73vCTiaXRdR9d126/Pzs5eaG2VSuUbivs25B++6lW8MEJmrQestGFlkYlTLkR3bSiM7SBitD4z8z3L1LImIMjoXFSqCk4MLyDOiFLajryUvn53xZZ04w10+TuACH0KaBA+7Cf8QT/h7RRIXgjZeNtnnOypG0w4TYEDBt6JS75IM24S3RqjOFugkZLMNK6nz6EkT+pY5UCX/eWhaSK7dqnRhWgzeG0UKey+D6TBM7MBEkQ3sNCGIBmnRr+O9EMsdZa8yQj2btLejRKBPkUe9FPolRTgbTtlHnqmvuduu+Kzrin6Btv87ipJgwvgRXjUz1jkhqTuUkKoo+epUqpGXydVt1ypvFm8dknC3/k7f4cf/OAHX+kxf/mXf5lf+qVf+kqPWalUvkG411OTIGOQ21tgsMgax4UGOglMfGIxxLKzLeCccdAMzEKHd4Z3hq4n9ARkdEjqc8DCGpFiPuqd0ciAk3GwWo6MEtrLG+WOccDYGICPLU7eKQg8GFo6c0Bmf7fZzFCMhOFdYhJ6vtc+4r+s3r72PTArFQxxkKzsyM/UkRWSFsG2jK1Hx23Hoo8MOWxGHIz3sVyIbXf62dub314mzjLRZ9QL69xyGJYs8wQdhLPldJtUyGb6cunV2ukbNvZKo7vSIrW4PhPagdN+zvHBfTzKW9MLHl7M6Ydm26pUBOD7NRwhm2elgUHddhCeIqwIaONop3mraXjZVAvUSuXN47VLEn7u536On/qpn/pKj/mLv/iL/OW//Je3X5+dnfH+++9/peeoVCqvkNc8tnmUWy40gsEaITLQaVNEtWP7TBOU4DLT0AOOQQLRK122bXuNmqPTEkCrFQtUpDjvONm3BN3bMr5072y7+715KmXhR+d3+XVntCef8tY0bV8tCGpKRmncQOs6WtcTyQzX/PWzCYI3K0oWUTqCQBsHkvUscstBO5QKCXDeCUnDYyMMtomDyJgo7Au0BQmwFk/Cs8qRIw+pd3hTEEOt1D/ExkTDCZY32oJNZWK8M1KahBZdy1vz022L0XTa0/QD01WHZk/OYa9NSbDEtkhhVlyS1hY5YMCAeeyIIRNCpvHDC4jAX4xqgVqpvHm8VknCz//8z3P37t2v/Lht29K2r+9E1kql8hxe4zkJJWRPRLfGB8/dcMEQHWdDu9dqb6VlyGUOm47l0KKj2NfMkU3JlJ5+obTSIEIjueygA96MxiWGvOn6332w/a6WbaeNsegaPuGYVW54O2T+q8CRGW8dPWJ/wNlg0CskSwzmmbuOR3rTXz826glsbIsSQGl8YhZ6uhToxaMKObutR9NmENxmLdgm8bjs4IQoTaPlOOoBxYtiASwKs8M1XdegyZd8YEwU2BRQNt1Tm+OpxwZFUA5D4u5kQR4TkHbS46IymQ6sEmgKZadeHZjDksMyXHRTTn3P3PXciwtEhLvtkrvtkjNtOJ1Ov7b817kXa+etVCqvL69VkvAP/sE/eNVLqFQqryOvsSZhQIh+4MTOmSTHiV+yDgG1e2x2r7035r7nuFkz8wOqniCZlDd75YIqREk0rgh+zUqwm7WImRWHcyUl2e4XPxYTyjbgFoFlalnmhovU8E7seT+uuBs7nOi2Tb8br2EQz3FcMfUrZm7NI72ev//+ub1kDsIwrnMj6C3C688WnlWKrFNArQxc22oOSj6xvQRM2CoRtpURYxgCzjVoJ9w5WNJpxAUlB7AgTA46Vo+me8dmO1htN4xucxIgC9OYmMtAyh4cZUYC4ExRFZwbLWzHt6hC3wfMtyxjy8pHXLsZqVdqPNOw5p3JguXxOTHuKjcvE1Xbc6uqVCpvAq+Vu1GlUqm8EK+pJsHM6FUZDBqXOPFLGulJufTnF61BCRyDz0SXCONUXo+STcAZg5YZA4O6sW1kPD6U149SX2OczrwvxC02PM9YpXDWzVgNkSAZQYvb0qYlyYTBHEuFP+wPeJBnJLn+/pSNAXxwmdZngtetJeimR98Qos9khS6FsVJg4863bBxLt59v2ns2Vqkll3AMvSOo4rOguVReDCG4xMz1pFVg21ZEaVfaaApkrw1rP1k4aNeoF5Y6ofWJ47jmOHa8Nz2nlQFNe+1bCsPgSYNnvY6s15GWUu1JWio8anAUe45ix/F8iQvXDdy/ZIAvpUWtUqm8OdQkoVKpVL6hZIpgNAANwl1nXKQpnw9HbIJVw3CitK4Ek13yrHPgfGi2MxEwYeoHpj4RxEha0oISJxtREg4l6Sb6HVt7tkHhvnXP3gKNcX4ApOxZDJ7z5LfBpIz/GdCPwXkg7dZ1LQSPEpziJeMlE1zGu5IgYZSKwije3qzL9pOd7WNrdbT3ut215QSRTKceCULrExOfmKLMJJH6JyQ3Nt4vZ4i3Yks6HlPE8NFQ51APF3lG6wZaUgn+8/hSV9Yl5iB7yMKxX9PIgIiRzJHGZMQJeJQue86Hlk/eOrjWXfyc2Q3u+ROwL51mVCqV14zXqt2oUqlUXoj0B696BS9EENkPzznPB0zF86CfXXLuMTW+WM+wBk6Z0iVPn8N2lxwAM/w44VfZzBsoLSTiirPOkF1xMtprKdp062xj6W2FYCf89aYcxSV45fe6Q46nC+55IwjkbY0CEEfrM/mmO9JS1hpECeN8hM36vDO6zrEcmnJ9GMpO0G2jDqOsv7gObSsjezZOZjCfZGJUYlR6K85RE9czkR5nRSw8DO7xtTmwsOd0NFixUQ0JH4xeXanioDgz1qvIZ2dHPFwebH1FbdPopXDSrLjbrjiZ9EzbnqPJmuh1ex+zwvnQktVx//B6bVtnNDe86U+ilhIqlTeJWkmoVCq3H3d9y81vGlGERgTMczbM6XJLN4p+Nz3qhmPQwKN+wuerOZ8uD1mmBrWNmSYk9WQVvBjO7fz2jTLsazA/9vI/YRFja87TES5yYGXC0uCBej7JATUb7VbHRMHKpvtwA9GyURIYL6VNKllAKVoKM7joGh6u5qyHhj6HMmV6v5Kwf0FPaqMaHwI0YSCGBLE4GTWSmYeOZpKw6Dg+WeJ9Ht+/6VcaP4hByDif8TER40D0A8susuoalsspOnjakDCF9RBQAdtYmO4lK41LzEPHUbNkFgcOmjXJHGqCiPBue8G78ZSJDhyd7mb8PIsj+mvf8yciVOFypfKGUZOESqVy+3E3Fcl+s3AmPBwOSBa5n6ZkCVtr0L19es66KcuhQTdKBSuOOV50DLBLK1BWQcZXDRq2O/U7rgSD++06+1+Pn5sJp/2Uz7qWIXZcWNw2M0UMj439/bDMEb3Brx7ZXN+mhWfccTcRuuxKsG3CcgishkhWv3vnNhG4ctCrbUiUqsjZaspKAtkJp8MEGaXR06YntInJpOfwaFnu/SZpGluYTIXD2PPtO4949+SMd+6cgxNW6wl939B1kXXXkAdBnPDe3VNmTT9qQOzSoLLT1YxFN2EYAifNRUmQ9u+JwL24ZH6ufO/+/Wvdx7dZXPuePxED52rIUKm8SdS/8ZVKpfINZ2WBM21J5ljkAFKCyp3o1kaXIrdtFdpucdtmiJjRhISJo9Ox9UTKVLFknt4CrU9bge8+uxh7F3CLYxu4e8nMmjU+GJ01LDVyoREFLjTwSTrgNM15OMxYaUu+8a+eUaBsxerVjcG7l9I+NGRHyo71EMsMhDH431RRtirl/aTBRsXE3qXm7FmtW4bkRlF4JpLwKK0MrLrIo0dz1MpabGwSMi1TraNTGlHuHV4wn6w5OVxiGOvOY5kyvdkbs0nPbNrx1tEZ0ae9u11Sq9UQWfSlnShKEaGHcZbF5uehg3A8WROvTMl+WTgnaFUuVypvFDVJqFQqt5/0etqfFoxFDnTZc5EjFxYuDdAyjGyerG7rdaRjK055HhDotfTpd9kzWHHJGcaBY516nLDte+dZbSVPbT0SEp6BwGCBwTydeu6nOQ/SnI+Hu/yoP2GpkWQ3+9WzSVy8GDPf41wJzvvs6HMAHM1m7ds32VaLYGPf1TbEvtRyVF5UZkEYc+mY5MyduKTxhvPAIExTwrpiRfrY+jI0okSUKMo0DjivKIL4kpe0caCJCXOO6aSniYlZ09P4YXtDNz8rQ0i5rOvhYoaYjQlCSXDU4GDSMWvXiPt6kgRVw7kn/uArlcotpSYJlUrl9iPHr3oFXwo1+EwjZwSSh6O4xqFjy31pxcljbL9vPQolifAOMsI6e6JTJi4z9QNBrAiJddw5d8VFqLz/8R502/vElJ3o1hyn/YxFash4eouAQ81zmuZ80h/z8XDMp8MRH3R3biR/lbEVqnGJxufR5Uhx4+hjh7Lr/pGt29J2poMJ45zkYlOq7srFlNduzmPZ0a8jrU/FpcmUnB1rjfiGzV2/NDNAgKnvOWjWHE9XzNuOQQODBpI6DiYd5gTvM6scWJsn44mtMmkzm8xrKwR3xiK0PJIpi9DyxXB45WdgiM80bb/TSLxkxEm1QK1U3jBqklCpVG4//nVOEoT32jU/aM+ZuJ7oMk1IHIQOGWch2OiKs3n91sWnfFZEvgQWqcVJZuIGDPCiiCmDetY5MmR3JRC8snO8r0XYw4kSQ0LF8+nqgGjGZIzcvUBvgf+2vsOP+juc5Qk3/dWzSXg2AmZPaa9ScbhgBFfEwLpJlEZXoydqEWDnbrRpPxr9PZN5VqlhPTQs1g1g9LlhkSPnqeG0nyBhr/lqPIcA0zhw0PbMmp6UPU6MPjmiH5jGnmlTKiBrjUUb4gwTcCFduqkCNPMOFzIpwnlqOU8ti1TWIwIOYa0t08NEO/l6hqmZ7uZrVCqVN4OaJFQqlcprwL3Q825c48zoU+BsmJA0YFaaj+QpEdzGCjQpeGxsXVHKzrqyGgKahfUQWKdYHI7ksfTg8ePufR59xo3ThB+kAz5N/3/2/izGsi2764V/Y865mr13NJl58nTVnKIM2AYsCwQvtmQwZeNrLKwrLqYpkMwLluiMrwWyXDzhDyQLi+4KhBHt9QOYa5mLDELYppFdCD7LWDb+CkwZG1ONq+qckyczo9vNWrMZ38NcazeRmScjIrMyT2bO39HOiNix91pzrciTMcYc4/8fM1bJ5iFq0fJmN+NTq+t4dcR00UhzoyyWYeBbZSJoGuYG2NxmpYZe81wIg7AjMhgPc64Taf381sWoKM4GVsEiJI4XUxyJ075lFSpOQsMyOsRsiZaH083aFW0dqJ2nrXpWvaORwKvTY770pbeYNn2+BgNRDTEZEMUnk+/3zo1NoAaSYRUdXXIsY8UqVusli+Tp2b2adWHkSVDcjQqFF4syJ6FQKDz/hM8+7RU8IkorEU3CK6bjs/GQqHbTwz5UEUR0S7i8ea9I7mmv7SB+zWpeapuoJdJITx/3tt+y5d6zeVrWNpi75/DJMPc1B8nSqyUkx51Q0yfDW37C3dDgk11PSL4sdmiLspKwApUJrKIjqrDoKxa+XrsajS1XY/vR1iiEvOt/vzhXlVnbM2s7pm1P23Q0VWDpK45WE06WE45O9jAhty2db1XyQwCfkjJ1S6azDt9YvLc4m3AuYU0iqZCwiAht5QfHoPNJjSEFAxXUCZxVrFWM0XU7EsCe9cxiR9c/jvkHhUKhcC8lSSgUCs8/sv/w17yHURXmKTE1K153Z2h6NbvrjC47Q7ag67kA2/3ygxnpUFGwkna0xwnBDdOLg25nBlstOesjsbNTr5pbbkQMMVnO+obrac6v9DOc7dmPkV/pp9z1uZVpPdzsAnlCdmTKC2hsv7ZsNcNANVXHW/M9TvoJ/eBGlNbORdx34rLcc00ZZyOzpmN/suLadM6vffkdDuqOyig1CbwBLxCz5uF+P59KIq/tnfByO6ci0PWWUDvE5ORNrBIwrGKNIeEwTFygqzqcC3jvEMn3RyMcViumRESza5InC8Eriev7Fz0PyHrux6P3CslFfnCFQuG5oSQJhULh+cdegyfTuv1YGds7Fhpxpqe1Cxrb8Ep1ymfTDZQsSE7YIfjOzkabWE6xJq2nMhvyhOV1B04SjCgHzYrJynPWt8CWQHUrQUB0yz6UnWnFxiTEJKIRllJxN0z4dNcR1fJ2v8fd1KyPc9E4UwScJKauY895Zs5TScBJZN473pzvM+9bbi9mzPvmvqLaTUVhcz/vlyjEaElJaKzng9fv0riY76uOpqRQEVmFe4fNpQQTt2JmOyqJVESmtufaNGRBOTa3RmFZxIpAR6RFoqJJSdHkJGItIlGCt9w5mjGtO05OJ7jZO1gSlpQtXodzH0w60hsXu6G3mF7odQ9CzMYxq1AovBgUTUKhUCi8h1lpolNFCRhZEUi8Vp8gpLV159his9l9HxGSmmE6sRnmCyg6TO/1OIwRjIF9t7zX0Uh0q1VnrYTe6ckH8FFYJUefbJ5ZkAzHPvfwb2oZl0OAPbdi6jxT1+dH5fEpT572WnHa1yx8Pbxe2Bmctu14tH3UtePRJllIKlQmMXU+uyiZSCWJkAxnfUNKlkkV79+TL9DUER00HjPTUQ3D62qTaEygNjm5cRJpJDCTFdpl69Rl7/DBotvWrCKsYsOtk33OupbbpzNaE3KgPtzPXnNtxbx0scj9hEdrS9KkF07wCoXC80GpJBQKheef8KtPewVXQkRwCBXwdpgxjy1+EBYf2gW34v4QnOra2UdGP/2BpIJJOeA25L74qBDU5fcaRSVPATaiRDXrIsGm+WgdvebvDAmDDO1NIoJPlrmvmPsaVQguJyIhGYJePlHQ8fol27VWEumSI6nB67A7n+w45mD4qMP92Jo7sD7eUEPQcychj0RrrMdJzNUACRhRFrrZ5TeDq9BglLq+J7o+AljS8PohlB8mJTsSqgErhkZ6VrGCaLEKjYnElI+vo15E8jGPl1Pe198hhIpFV9NONzMVQHAk5NbF7ucB/aXu/3lyJaGUEgqFF4mSJBQKhecfebRWi6eJBRap5jjucZJmzHXC7bjPkgZncsuKVImaRIzDnOCouReeQfQq0KUKESGoA+0RcgIRU64EWJOtRGPa2nHemt68HR/mUNwMomlBTO7L96HmCycHHExX+HqBEaGLltUgKr4MYzhuRGlMQIfkoI+WVXKc+ZqkmwnTWby9mTWwTkq2dQnrtW9ae4yLiIXe5OC/wlMR6ZNlmVq8WNQoKW2vTTaJ03Co1nn2qo7aJIb50Ou7ZSRRGWVKz1KABBqVzluOuvb+WolBJL7frGgrj3HZ1clJvhgnkIi0v3qxPrqXmV/odQ8iVxJKKaFQeJEoSUKhUHj+sTeeSU0C5ID0NEw5CTOOw4xFnHAntFjJO9fRKIY8UCsZCEmoXRY1G9E8dIwcf1qT6NVmsbJEVqGijxVRLXPv8HE3mNct8UEW1W5Wtf1ZDAasQY2ixjHvGpIqzm6/9uIBpiqDQDlQkYW61iSqmJjHmmWww+57Lp+kZHZbjXIGtE5ycnWBjXh5u6PKgtSBdtoz3VvhMUSUpVaskqPTin6YHr1Z30bskCsHkWnV44wOwvC8LiH39OpQVbBiIEKIwtFqykk/Ze6bnNBtzW5QUUSFa5Mlbe1pa09lI0EtQsij4UR5+/SQL+MLF76vj0qpJBQKLxYlSSgUCoX3MCLwatVxHFtuGei1xm+1CjmJ6/YgVagsGBmcfob3C+AkENTgo2EeKvZsTxccXcg98UerGbkksK1XHgLXdG5XnuFkW5WGGC0pgJqEcwZVy2bC2+V2oGsTuFHPmbocHDMkDd0wy6H3NUtf0w0zDRiqGiC5oqCycf0ZepHk/DwC8utUhdoF2sYPl2+wKJqEed8w7xvOupZl126uQzYPEeGsn7IKjhRBNGFF8zyFLVWEKKyio6eGCjyWeaiImqs4xI37kiYQo9zcO8WQmNQr+mRoTQQRRJQuWs5CQ3yC0sKSIxQKLxYlSSgUCs8/4dNPewWPgLBvAwfWs286zCBCzpOWzdredK0jGIJjK2lHaNrYgDMp252KIJJIQEiGu6scAG86+bcCz3NORudnD2QJRA7E07A2TWbrjRcluy/NbMfU9cxcT2sDjQlYifTB0UVHFyq66Fh5x6If5iOobAamrR/5OVVZtwftrHzL7vVas+KwXeIkrr/fecdbd69xupxsrFXv/dFkHYFRIoZVrBDNrV7b1+6T4XY/ZaU1AUenNZ04Vr7KdzqNKxn0DwqHkzmzSU/dRnqpWCRQ6XG6xIoQVZirIz3BFqB7ZjoUCoXnmpIkFAqF5x9zA+L/etqreASUmfGghhuux2miS6Pt6eZV552Nxs78SiKNyfMFKhOpTcAITCvPmQ/Mas/dbkgPjBDjEOA/cOdYhsFqY3Yi60ThHnHwBa9PiOy7nokLNCYysz2Ny2uNUXhnNWXR1yx7x6p3LPqKEG2ufIzVg7G9SId0QIckJm2184z3ZnjBtWbBK9MzrtcL9qqOxva8dXbAr965wcnplLuneyyW9eZ45+JkFaWpeiZVx7XJArvVsRUVjv2Ud/oZZ6HlLNY0LnB3NaXrHJUkurARWm+kCYJXx2nfcMMYOq2wGqmTMBZEJjay1/QY82S298UI6X4+s4VC4bmlJAmFQuH5xzzbw9RACBqopWciK16yc+bh8CHvyQmClUjrArUNVCZRmTx1OeCoxCMCrQtMq46zvh0CwXOe+JrXsNmHz61GMlYLhNySNGQIxiSM7JQf3nWd+RINSQQjkYnrmbgeJ4nTvmHuW077lpN+wknXcrRqCdHlvGSsImwdTlBIWYewmTU2Jj67WZUlse+W3GgXOEl4rTjq91nGhrfvHLLqm80Sd38kWBs53FvyvmvHWLdp8Vq/ROFWt8cdP+M0tMxjzfysYblsWYU6tySdv0XD1yEYUhJWfU2I4IxSmzQIl7PoHBGsSTwJNGmpJBQKLxglSSgUCoX3OMsk9EaIJnBXhLryHGrHnX66E7iqkl2KJAEJQx62Ftc70Dr08OvQqi/DXIDAQbXirG8YjT433jzbQfVwknX7zjBcbIjGRQ0pmGFn/OEJgqoOrVKjh+mm0SmqoY+Ok75lGWpO+oaTvuWom9AHy5jIjK5GbAl/d5KGdRJz7/lFEnttx0uTRbY8TTU+WZapYhlqvLf3abcCSLRNz3TqmU5XTNuOadtRuXjuBFkfEpLh1mrGrcUekiyhN3SrhmV3n9kFw3IrG2lsxA3aj6w92bgmncWWiQ1YeTJJgikWqIXCC0dJEgqFwvNPeHZbjaLCF6LlNAkLSSCJ1nompqeShkjubxlnJJjhYU3K4X0ydMlhU2SihqAW8FQmsgotXXJ0yXESWpxN+JSHdOW4eMsuaKfff3hu/WFMJEYdhJ7bdn8IQxAuoiQRohoCltZ56IRlqDjuJpx0DTEajECKOtifbq2NQfR77rndAH/4wij7+wumbU9b9QgJ7y1HfcPxquWsa8kJ071tV84qkzowrXumbc+k8TR1oHL3WmipShaI9xWLrqXrKlI0hJXBJJP7l/Tem7WMFSnmWQhGlcokHJExOUoI7yynnPWTS9zkq5OKBWqh8MJRJi4XCoXnH/Pa017BlekUGhEqEUxKGLK2oDaBymwHpVn4a0WxRqklYiVhJeXN9ZSD75gMqsoyVCxjRcRwFhuS5ODbWnZEyusP68fg2LMdhMN6p94O7TAXCyfH3f+scchzBkIWVKslqsGZBMKWSBlSHAoa2wmLkmc2qCDjx3HRQzKRqw4KdYI20bQeVwcmVc9iOeF0MeXkbJ+7pzNCMvdehUBK0FSeSdOxP1mw1y6YtisO2zmN3e17Cmo46yr6ziEBkhe8d8RkEDcsy+hWMjbeDiUyTLEOhtZ4apP1B0lhHivenO/zK3df5lcObl7oTt/i0WeFlEpCofBiUSoJhULh+cdcdLf1vUcj0EiiRrA4jvyUIz/lOExZxnpLSCskchCZFIzNbUdGdC0yTiokzUJmwWKG1paQzLoCEeL2EAF2EoF1jCgCBjRmP/91LD0E4ymZewS+9yOvK3+c2Y6p8Uydz/MPUPpkOPUVJ11Ll/Ik55QEGI+/0U5seykpw7rO6xEATNYSUEWsi9RVQBFWXcXZouVkMeX0bIbvHaqDverOomGvXnGjWXDYzDloFxwOg+O2b9YqVvyPk5sc9XssY01MBqO5zUu2kg9FMcM51lOcjaKSh8eJgRvNGbUkzHDNnda0daKPjtP6Yn+3T7hPa9MlKZWEQuHFoiQJhULh+ecZ3gG1ojSidLHmxO+j6jgNLSe+ucfZSNWQdEgXVKhMwJjcxmNJWJNoXSBJRWM8bXJMnGfmOs66lpCEtX3OGPOud+s3gbaqZhmCjF8PC7hnT/xezjsEiSi1CRxWK+rB1SginHQtby4PWPQ1Z12dJx4Pcw221yWMO9zC1izlzaiEnXkOyqztaSc9bdszbZc0Q5KQMPTecud4yvF8kifTjVe8dQgli4Ynpud6veBateCl+mR9j1Thrp/wueU1bvX73O73uNtPubXaYxHrzUHGe2okJ1tsTYtWBY1Y62lNDypYyVUkK0pQYakVtjO8fLZ4yB3PHNBf6HWFQqEwUpKEQqHw/PMMaxKyjWdiFbPlaaNw7Fuimk3/PzAG8GkMnIevKxOoxjYkiVhRogpOoDEeS0VrfO7JD9WmA2hrGFuOZ2XzxahD2N2+H8J0xZr4rpUEGaJ71byG/aqntR2H1YJFqDjzE476SRYt+5rTvl0nCGP1ge0EZQyvdXtBW7dleF5EmFUrDidL2rbnxt4ZrfWEZOlCnrtwNJ9tEqXtYwxrNgacTbkVSgM3mxMO3RJDIip8frnPO/0+x37CXT/lVjfjzmqGjy4LycdjBRnWfe+NElUmU890r6eaePpgsHUaKixCI5E7d/Y5O55x7c2LJQkvM7/Q6x6EiNzr/1ooFJ5rSpJQKBSef9wHoP/M017FlQgoIpEPT97hrVDxchCm5jpex3++N4GbSN7J9wo2Cd4ItWzajqyEtflPUJOHhyWlksREeuY0u7vzW4LatQaATcy8OfHwmmQwknDuwfUEWWsWEtbAxEWaYW4DCstQc9y33OmmnPUNi74mRLvbOTR8zGsa1yhro6XzrxvXV7mAtcrE9bw8O2Far0Dhc6eH+K4hRoeova+QeMRYT1v1XJue8etvvsVhu1zHzpoMp2nKSZzydr/H55eHnPUNSQ0imh+qkEyueuhunjWu1zWRpgnsz5bUVcSbPJnZDAnSfFEzv9vSzR3BPxlpYf778OxW5AqFwuUpSUKhUHj+kUfvx34aRFUCSpbyJl6v7/K/ugnvb445XkzZDi/HNh5VISZLkkSfavqUw2dHZK8SrHgqiVSSWGlFWydSJySbdQnJCDo42ej9dubhnC3oli5BhRQMKd3bVrRZ46aq4UykMZ7GBq5Vc058y6mfcOrzLISFb0hDQD2eV7eOdd61aJzksOtuJOv3tXXHrFnx8vSE/XrFm2eHrIJjsWpZLR0a3LvGwbULfODwNh84vMOXvvQm1ybLnWu742d4tcxTzZHfAzU4A33KCURKw60bE7AxY5OhhQshiVK5/HN8uTqlMZ6kWTfiTEQFfnV+jf8xf5m3/cET+y1ujCmahELhBaMkCYVC4flHn4yX/OMmoURNQ7KQqM2KxgRuVnNmpmOe2vVrxx16I0pSxavFxxy7xwT7LusUQhT2bKTXij45es1zAZxJNC6w6Ostc6ONMPheS1RlR9SrgEkYm6jrcO8O+fD+1gRqu3Fo2nM9e66jNZGTZLLrkq/oYkVS2ZqDsCWNGJIN3RIKqG51PulWoiD5GyLKrOq50Sx4de+ERapQVT57fI3T+ZTQVUML0H1WrjBtVvzG13+VG9M5+82S9x0c7bxkHivu+ilnseHEt5skJuWWopgGQbcZ1r/9V3I4raIkUVSVN/Zu8XJ9xo12zp5dUUlcC8/vdDOOU0tyaat08sUlpVTcjQqFF4ySJBQKheef8KmnvYIrUYnJjkXkQP+Xlze57fd4J8zWQ7VG1pUEsvtPvzU52YiSMETNMxOSCiEZ+mRYBIdicEaHJMEhmHsFyJIrBsqmN12TDhMVBn1BFbFWMS4H8GlYzbgBbQRaG2htoDKR1nqmtmdie/pUEdWSkrDwFX10kAY3Ix0SgnEuws6F5yqGrKsGQ2WDrZcKXJvNeXn/hL16Set65l1DCMLRfIqGatjZP297mtd/sLfgtRt3mU4807bnpenpoA/IRIW3ukPmsWURWvro6L0lBYGUW4w0yaayMl6GgWG23eDUqjgXeP/+HV6fnLAnS16qztZaBAV8tPTLliYkem/ypOsnQNYkPJFTFQqF9wglSSgUCs8/7kPPrCbBIUSEz4V9TuI+d8KMO2HGWWrY3fXeWHDK1nMCa7HyICsmqqExgaAmT0lOBp8sUe1QiRjerroj4t3eSB53+DeVBsUYxbgwCIy304y8TkvWRrS2p7WRqe1obUAInPkJp77mTjfhzNeQ2CQG5xyN7nFdSufbiwZ9gkSMBVPF7GhUBWZtj0Y4m9fcOTlAu5phOtlwjE0wbG3kcLrgYLairj3Tacds0jGbdjt3/qifMo8181hzFmsWfU3nHb2v6Lyj846dCFsgn9AMd06zpazCtXbJQbOitT2H7Wrt2aTDGc8WE/xZjT+rYfnECgmDJuHJnKtQKLw3KMPUCoXC84/Yp72CK2MQFqnm091LLNOUO2HKHT8dPPM3jHoEGGJdFVJimH+QyAaaOdjs1GEloim381gCfbD0wZJ0q4ogshWFDjvhWwJWWf8xtP9Idv33wZzbdc5rcSZbeBpJ7LsljfOAskoN76z2+NzZNW4tD4hpCKrXiQ1DFSFnAqO70TgsLT87nHBo45Gh1aeZ9uwdLtnb6zBVpHYBMULoak7OZuAFvM2tRnFL44ESkiVhMCbS1j2zyYpZu1rPMADok+U4TulSxSLWHPcTfKiIweR76u2mLWpzO8Ay/AbeRN7WRg7aJfvNklndMXH9oAPIsy5ElFXfkFZCOKuxvYH4kL9AhUKhcEVKklAoFJ5/Uve0V/BoaOK6WdDQcSgdjfghTt9V78o6at+05eQJAHkac9IcTIdkCSkLlFUVR6IPhj6Ye0w5173945lktO7ceREJISZDUEPvK3pv19qBHOQmGuOZ2Y5rbsXEBVBDp5a7/ZQ7/YTTvln38u+Y6awvcWgH2uQLiIIZHuur3nq9JsN+u+BgsuBwb4lzkTvzPZa+YrWoIZghQRhad2RzXlXog8mtURIhCE4igtKpxSfhnW7GPNYsYsVR33K2qlkFQx9zgpD0AYPlxgTB6HrdjQm8tn/EBw7v8r6DO1ij658iKnhv+Pzta/zq3ZukZIck7qI9QI/YKzSIqwuFwotDaTcqFArPP/HzT3sFVyah7Lk+77zbKTfqFSdpxWlsefg+jxDUIgo1ESeKAWoTsAYal2hCQgQmrqcPlpDMbnsPmnfpx4Flo0B4092Uv7bgg2CDJYkSxFKZgDUJKwlrsx7BmcjEhvV7++g46xv6WA9C5TwPYfy4c6px3gFs50YDg2PQ1peg3KxPeKM94oMHdzER3lns89k71/kfb75O6t1uurOuiiSo8u59bX1uk0qKTYpEmLmO2gRWyeHFkTDMU4smiwO8d/ShIib7gBh+XOvGAsqS+JIbt9irOvbrJYftirFKElTwaknGcqYNndrcrKTwhYNrD/k7kLnF9EKveyBKcTcqFF4wSpJQKBSef6oPQ/fpp72KS6OqeCJRlYldcuhWXHdLjl2L4ZCHeTaNLUgK62BdBlehkGze5Zdsdzpxib6KnEa3ZRM0qAkGRx6NQ0ybzjkADVae4nL7khjF2URdJZxRnChOIo0NTGyPSCSqYRkdx37KKlRZkJscimSx8rqCIVvzEHRdZdjco031YLs/v648b9y4zfsOj7nezLEx8fb8gM+fHPLf33odTcPsgnOtQKq5O03qBALttOeNw3eYVR2tdNR4ahOyjSmGSiIBi6hgNIuUnSR8eEAFIf9ksgahgkYC16d5cvNr+8dcb+fMqtVa05EwBHV4zU5Ux4s9Gtez0hYBjicXC/5PeDQbYGOElO7JzAqFwnNMSRIKhULhPUoiuwepGBpJ9MkSVTgKDeky3aKqiCZElVp89uMfNArjbnltXVYjj7qDYf9edoL04XAiOSkYvwZIQmUUO8qjbdooBiSxV63Yq3pamzACR75hHhtO+paz0LDwjhDMve1GW2tRzQPI1pUOxu+b9ZwGBfYmC27unfLSwSmHszn7kyVdcsxTze1Vnqgs9zRWZURAXEJsRBwc7p/x8t4J19ol19ozKhsAg4qwCo7jfsKir+g6y7JzLDvHoqtIyWAe8iMSo9w8OOblyZzrzYJXZ0e8fnDEQbMc7xxJDVGFLjnudDNSNJhkkZjv07Xl8t1PMnBAf6HXPYiUFHNeCFMoFJ5rSpJQKBSef9Lx017BlTCDt01CWKQJx3HCF/w+n/cHWElEfbggWwSCWnqt8BoJVAg9VoYURDTPJgiWua/ZdUzaoFvBuMhmYz9/D5yLVCZSmYRFsSbmnXryzrozeeKwkayRUDV0wbIIFYu+ZtE1qNpzlYJhHVv6g229xTpR0exwpChUEWmUeuLZmy7Ymy3otGKRHEfdDEPe8U9pK9HYuWEJqohpIi/vH/GBwzvM2iUvTU8Hi9l8z30ynPmWed9w1rXcXsxY+ZqzbsLSNw9NEEbOqPlge4fD6Zwbe6fM6m5Is/LiVtHwZnfI3TDjzmrKIlT4lUVCbjZ77ejuhc7zMvOLLegBiJRKQqHwolGEy4VC4fknnjztFVwJEcEg+OT4TH8drw2f6a+BZFtTeWjDEYBijdKnHOAHtXTJ5SAeZdlXrKLjzmqGymjJudECMD4jm3af4bCbdQK1i9R1oBkeloQj4iTR2PzRkqctZ/G0sAoVJ8uWPtrBeemcWHn9GLOS/LUkHWek5Y+aTULFJIwDUwWmbU9llYaIUeWkmzA4jSIJ5H7zBSRhZz22idw8POUD+ydcbxa81C52WoeSwnGYcHs55e5qyp3FlKPVhOOu4ayvLqglzqLleWg4DhOsjVxvl4zjCLLjqKGxkXmsud1NuTXf59Z8n9W8Xv/ylickJlYtlYRC4UWjJAmFQuH5p/rg017BlckBaUufXBavjq04jHH6uwWJg4GoQlIlJJOD8+RICU77mi5Z+mAIaT0WbXPkcWjaTtIw/DnG7ArWJqxNVDZSDzajbR1wNuGsUpmAk4SRiBCZ+4rTruF01bAMdR5sFk2ejcAgXN5azWZZsil0DDqIzXIFMbmBaFZ3Q/0lYTSyXNR085bT0yknx23WDqRdjyZVMJUiRmgmPTfaOdfrBYfVitqG9Wt8Ek5jy0mccOIn3FlOubOccrKacNo1XOzX6uD4ZEZ3o8R+tcIMlZdxUF5uuRKcJDQKt2/vE7xdO06NidKTorgbFQovFiVJKBQKhfcoqjkSfLme86XNLaZmwYebI0BIKqwtQR/I0K6kQmOyq1FtIns2++8fNkta66lNNtu/NwTUtUZgPRtBt3bxyVOU6zriqiFRcJ668tQ2DdWDQC0JKxE7TA5exRxMG4VlV5OS4Z5fR+sWo011IydHMkxz3sxPUCXbuSblWjPn/XtHvD495vX2mDun+9w92WN+0nDn7h5n8xlxS5ytw38YJaZ8mVPT81p7zKvtMa9Njnbu8Ty1LFLDmW9Zas2pr3nzbB8fh+D9XZO23VYqTZJnOYgnplwNMeS2LF2nOUK/qli9PcEtDd2dNudJJV4vFApfZEqSUCgUnn/Cp572Ci5NHgKs+aFKZTpecgveV53ymjsl6sX++c470ZHaeAzKxPhBp5CnJxiT8iTiql/vyG/PIpC1ddDOUXc+F5MwkgbNQWTiPLXxNDZQmYglkk2R8hyFqEJUQ5dstj3dqo5sFTLYCJa3vh6dlBJDC5JgrFLVkeuTOe/bO+aV9ozXJ6ecrqbcWexxdznjpJuy6mrOb4aP7kbOemoJXK/nfKA55lq14KVmTm3T+ufRq8WrY5XcMBdhQqeDlmKdNl1ib1+FWjzXm1W+N2mcK7Gxef3c/IB3Tg7pYoP2ee4DkgXcMlZSnhDFArVQeLEowuVCofD8Y16D+KmnvYpLolv/JXp1OdBG2LM9a7efh8RtSt7tH1uFLIk47MgnBSPCxEWuNUtOu3Z4FaQ0fkbOFNZzC4Y2IB1754XgHcEkKhcwKpgk1FXEDNane1W3bh7yKVudnvU1XaiHFqJN/5SOiUraajfazUl2BNPWDlUMF9EaDvfnXG/nzLuGu8sp78z3uH22z52zGek+90uBa9M5+5MVe+2Ka9NTXp6ecs0uOaiWjIlKHJIZQ6Q2ibfn+yz6mhglD6q7YHIg2yUAhb1qhbEgVuhoIHlqiSSF/zV/iU8vXuKW3+Od5T5npzPM1n16ktUEMXJPglUoFJ5vSpJQKBSef0z7tFdwaUQEM1QLksAyTThJNcep5SS1OdiXd68mqIIRpR8C8ypFTmPDK26OklikmoClS5agFrN2TDrncDTu8g/b25o2O90CBG8IxuTbnPKsBIPiTGJqPZWJ1BJZhZqzUDP3eS6C6sbadNQciIKOemwdG3hkS38w9OOPVQVRjIu4xvPS9VN66zjRCVEtt1dT3p7vcft0H9LoBHVuLsLQ3L/XrHhp75SXZqd88PA2N5pT7HCRSQUk26ZOTOC0a+n6ijvzPY6W07V+4kKobC1BqV1iGSsSgk826xRQ3l7NeLM74J1uj1urPVbzBqPZilSCPPF2I0168eHOhULhuaC0GxUKheefdPa0V3AljOQg+Ti0nMYpXhtOUsNRaqjsqNx9MOuBXCr0ydCrIQ3iZR1ck5KCV8sqWezYWgO7AeHabmfb3UjW3TVpaNnRJFsPaMQzMx0TE0gpzxVY+pouVJz6lmV0rCc5bz02rf1jhUHX+oNNspCFvwo4E7nezjmsV3i13OlnLGLNXb9PlBqzbde6lnsPWgSBLlX06nAm8MFr7zCruyFBYC2ijioENXg1fPb4Gp23HC0n908Q3i2A3xJ8OxOpbaILjrmvmPs6D7xT4SzNmKeG28t9glhipXmGXRoqCGMl4cKR+6NF+LmSUEoJhcKLRKkkFAqF5594+2mv4MqoGs5iy2lqWKWK01APrT47zfv3ed9u/NgnyzQJAWEZLS0Rn3Lw20VLUIM1OQrNDTQMceWwcy9ju9GWTmHLgcgHQ/BCqIQQDTEZJtJRm2x/ehYbTn3DWV9z5msWvuK0b4cgW7fajka7U9m0H60rDcPFyKbdqbaRw2bFnus5rBaoGCJwtJyC5isxMgTYbDQIGAU76B0k0VYdH77xNgd1h5Gwcy9XydFpTVDLrcUet5bXOO0fUEE4/5Te57nhHoYEx92EtupZ9C1n1jM1PUkSR35Cpw5SQiJoBUEFVwFB1/fkzb1rD/w7sM0tLjaZ+UHkSkIpJRQKLxKlklAoFJ5/qg897RVcGZHETXfKK+6EiVnw/vqEV6t5nnvAg4epnY/nWhdxNg822689E+eZ2Z4KZWo9tSR673YDQc2OQeOu/mhMmucmbDscKdFYvDh6HL1anI1MqogzESHRYelwLFLNcddy5ut1y9IolN459bjnv65cbKoA4wdjInvTJTM3JAluxYFbMTOeVe9yW5Tm1qfcyjQuWpFKkVpxM89L1044aBfs190w7G2zFpG8Y98lx7Gf8NnT6xx3LXe7yabqcV6zfL/n9Nz3AMRy1LV0vmLe16xCRUjC293hMI16QlBHCEL0giazFiuP1YTj9mLB/wn1hV73bpRKQqHwYlEqCYVCofAeRoHaePbsKddsy6ldciTthaYtj9GokUQlkWpwOUIhYqgk4khUJEIQkpqNzShkFx0DGjfH2jn0kEBIlRCreDVZJ2GUStMwUC1xEtq1YHmVKrrk0NGd6bxr6Lq1aVNJyMmJbL6dlLrxTPZ69mYrrs/OeKk+48AuCRjePL3GatmwWDR0ncP3dpOAKGBzlB1t4tfevMX7Zke8NjmmEo8j7MxPgHyvTruW/373Vd6eH3C0mjDvq4tNVX7Q5vtQpYnJcdZXvD5TahdRa1h0bZ4f0TfEZOixxFVFFQTxGz2GAIfL5QUWAQf0F3rd1S6mUCg8j5QkoVAoPP+ETz/tFVyJqImEImK4ZgJ9Msxjxf9c3bjwMVSVxub5CJWJzJxHh4ai2kRUlKCCDlUJkfweWc8hWP+x2V4fW3/GXXmjOUlA8EDrfG7xSYoXYRUrvFr6ZO51GNpuXRq0BmDW7U2SLZ02bj5D61NVKU3T88q1Yw7rFbPK42zi9GTCatXQrSqWq5rlotlxgVIZNRAwq1fMXMdhveJas2JWd5hzJZhlsPzSyct8/vQav3J8k7vLPVTlYgnCNvdrQ1o/LYhRnE3c9TPOYsM81nS9I0SbB931gFdIZlNJAF47vXuh07/M/JILvs8llByhUHihKO1GhULh+cfcfNoruBIy/GcQrDGsUssvr17iLDWXOkrQ/EgKfXJ5sq+Ax5IQlqnGuZjbi3bsRsee/Y1GYKMZGF8DG5GAMnEdE9PTGM+s8hgRWhewknJlQoaBYcQt29NtzcFG2bvdsWMG5yPGyoIobeNJW3MXIoYuZeekfuVYLmpSygnCWqg83hWjXK86mpSY2Y5r9YLK6Hhqogq3+j1+efkqR36PpTacdJPHZwM6uDiJJg6qnhCzePzN1QFfWBzw1tkePlp8sMTOggN15K29oQUqz0p4Mi1AIkIq7UaFwgtFqSQUCoXnHzN72iu4EkY2Afk7fkbQmruhgY3cl4e1gIgoqpagDhXJQbT2NCTOQk3AYGzCSsKaRBisQsd5BbL+fGD9+aYBX1NCbJ4XHNUSk8FKbjUKw3ssSojZXQkRjEjW365PtlE8rI+/1Xa0fk7ASKSte2aNx5rcDjS6Dy36hpNly9HZDFWT8xvdPWwS5dCuOHQLXqrnvNycslct10LkkOBW2OckTDiNLXfDlLe7fdbCiUeNlXXzMKp86u2bnLZTjk+nTK713Fru0wdHjDZPYl4ZjBfEbLs7PeIaLrtk1XuqLIVC4fnmmaokGGP4k3/yTz7w+9/0Td/E937v9z7BFRUKhWeCdLG+7fciRrJW4PPdAce+xcmWVSjwoGhRt8TGxij9MAwtJkMfLX0aqhSi2RZVDc7GXZtTQLcDw43SePNx2BFPOvgtqaGPDh8NiezI0yXLqa/xWxOWE2OVQM9Znup67fe/MGibQF1FqmGicyW5lerodMa8a3jn5ADVYctdZVP4EEiqTKuel6ZzXt074Y1rt3lt/xRrNtcZxSLAPLbcXc046WtCzInPuvJx1f784fqSCiQh+IoQKt4+OeCX7rzG/3znJl1w9MHSB0PwBhPzayUMAu/1HImrLeEqFAvUQuHF45lKEkSEH/zBH+RP/+k/fd/vf/SjH+Wf//N//oRXVSgU3vPEN5/2Ch4BYRkr9l3kffWCV9zyQi0vu05BObCNmvtUrICIQURxpPw86w39nTcLuhmRoLK9Cb41a2AIxgcnodoErIE+OI5XDXeWU466KStfZbvVIWHR9YHkfOqBInnTXLfPBc5FprOepvE0xtNaT+sCkmDpW7pQc7qcbK793NSxykbet3+XV2fH7FcLXts/3vl+UpjHbDfbeUeKBomGla8IUe6xlr0U2+1USSGC9IKdgzk2pGhYLCf0Xa4ipGjB2/y+xGZGAjlREH1yUuJigVoovHg8c+1G/+Jf/Av+8B/+wwD8rb/1t3a+99t+22/jk5/85KWP2XUdXdetvz45OXm0RRYKhfcW1YehezbFywATm6glURnlul1d4p0b8bE1eQJyDvrzFGGfDKuYNQp5lsDuDvm97Ux6rt3mXDVDyQnCoDuwJicMMRnmPlt8rrzDBwNqdgoIMiQNG8H0mLGsMxeMJGaTjtp6Zu2KSR0xkqgl0PU1fbAsO0eIm19topIThaF76dpkzo3pnJemZ9yYnGElbp0DTkPNaZxwGia8dXrAMlSsepdFxKl6xARhc9dsZ5CVYJaCCfme+qUh1go+T15OCSQMyVkaDpJA0pho7B73i80Tkj8UCoX3CM9UJQHgS7/0S/nJn/xJ/uW//Jf8iT/xJ3bKnyEEJpPJu7z7/nzv934vh4eH68cHP/jBx7nkQqFQeETyvnqXDP4e69MLRK2qTG2Pk8jUeZyFSiJCTgyi5pYms3Yl3U0PxLDeth6kvfesbzhNduLZcS8SKhORmFj2Wa+wU5FYVwrOX5Osr3w8dmUT1irGwKTuciXBBEBZ9Y6T+YRbxwe790R0ratoq579pmevWXFtsuT6dDUMkMuv98lwyx9yGia8uTzgbjfl7fkBb50e0IdHSBDOE0A8mAC2E+xCcmWgE9JCiMESvQW/0aSIAZyi9tzNekITlx/PEQqFwrPEM5ckAHzJl3wJ/+E//Ad+7Md+jG/+5m/ml3/5l3nnnXf42Mc+xld/9Vdf+ngf+9jHOD4+Xj8++9nPfhFWXSgUnhrhM097BVcmqWKJebc8WUTMaFbKw8K2cQ+lMmEdTCrDjrAIE+txEoeYM+28d0uWvHXA8YPc97UgxGjpgl074WgSVt4x71piqIhBNmLkdeXint6o4XNZzwRAFWsTxkScDcyqnkoSziRqk4bzVCy79r73wkjixnTOYbugsZ6DNn++Pajhjp9xGibc9jM+f3qYh6YtJ/jkHl+C0IM5M5i5wS4Es8paAzV5KN3axUkErKCVojanURLBdFtWsApvzq5d6LSPOnE5U0oJhcKLxDPXbjTyxhtv8B//43/kD/2hP8SXfdmXoaq88cYb/PiP//ilj9U0DU1zGUvBQqHwTCHPnruRqg6t6IqK8np9wmvVPm/7BkMiXmCPZzM1WAbBsMEOEaZoRLXCma2EYBjnq5jd/CNtDijbucToSjTqCnQUR1u6UBEThGAwEawqMZh1T/9OuDm2FunmWLAJhiFbp7ZVx/XpgsO9RZ6LIBEneWhbiBYrOlQqxmvfVDiaynPz8JibsxPef+0uh+0CI2lYv7BMjqMw5SQ0vDk/oIsOnxwJ88gJgqJ52nMAOzeYTvJjOUyBtmA8JC+IV2jIw7Q1txaJgATBeMkiZt1oEo6bJzNx+XGYOhUKhWeLZypJ+Ef/6B9xeHi4/vr111/nJ3/yJ/mFX/gF7ty5w2/9rb/1Su1GhULhOce+RPbifLbIYt08UM2IkvAkEgeu53a42D/fOswQEHQdUNeSsAaMQGUSIRmS2mEqwyBH3pp4nDnnJbozK0FyeWLohBLN+gdjADFYo1SSSNFsGfyzcQoaPspW/eLeHiSh6ytqE9hvVtQ2V1ccgRgMq77i6Gxybljb2Kqk3Jgt2Jv0fMlrt9irOhAhYnFERLJYuVfHSmsiFjGCs4OG4xEbbSQJ4gVZJwc5URCV9X0Qb7BdQitZ30dkEAyrQc2oS2CTtCkcrp7MxGVVigVqofCC8UwlCd/6rd963+d/42/8jU94JYVC4ZlC49NewaXJE4cTSXM14TgKzkQmpmdqem5fsH0kuxIptQRkCNDXYuGxqjBG5MMYgG058qYZZ+uT81vKY8UiZe2EsxHFrIXQSYUUDHjWw8A2cXc+i5w/ro6iZl23HFmjxGiwRBrxNBJopeft5TVSNBwtpjvHHJk0PbNZh6sC1sShQpO1GJbNgDejkRiFmIQQTbZxPT8h+rJorgJIkCw4jsPnIptVjqKJyLoJOBdQFGMEiVnDIORWJJFNleVJTVw2xQK1UHjheCY1CYVCoXApwrPpbGTIAWpCEYlMbMf1asFr1cUd2ETAmjxdOQyb+EqOR0MSVtHlNiIZ7E5hiLE3EuVdt6Hxg+5+msAmeHl6RmUjquDV0kfDYtWQvEWiQYKBIDCMZFhPcNZsP7RdaDDjPRi+tpIIYlikPEm5tZ4Uc0Afk6Hz1daVD0oLhboJVFWgqhMK6wSjlggCUeHMt5z5CSFYlr6iD24YLPdou+ea8oMgWYgczbpisp0rCYJEQTzQbfX2GEWi4ra0CGtLVD2nGfkikooFaqHwwlGShEKh8PzjPvS0V3Al1sE8gkhCJCKSmLqOi3aIr3fJARkmEIsIgqE1YQjyZRA0Z90CDG1A+QhbO+m6feDNbn80EAx7Vces7nh5dsbN/TNWqcqBdjRYVYgyJAgytBlt3HvGcw0n3wTDY8uTKAtq3lrusYyO26spC+9IybDqHXfOZtwvoHc20jaeqo5M2o6kQi2BakgQhivER0MXHKvgWHhHHw2aduool+d8d5YKtidXBuI5YyLJ1ygiWG9ywjBYn8owUEK2O4bSE/4FLsUCtVB40ShJQqFQeP6R87ahzwZK3uWOmgM0IxEh0KvBSXro+yHvNBuJWIk4Ewijs5AwiHKVhJKGAWfDm+51NxJAZKeysGlNypH8Kwen7NcrZk3PMtYsU02swVtBLNgU10Lk80nAdl6QBjH0urtFwVod1ir4lHuWfKqIyXC6nHLnbJ/8K23jmKQKxkbERYJVQpSsyzAJHSZXp+FiK0nEYFj2FV2o8uC3MB7vCj+7cf3KeqCb9OQBamFdqNklSG7JigzD0wRcnhit9VYZKI0dSvpIOczlLihrWAqFwotDSRIKhcLzT/jVp72CK+HEYIcgNajgU+IsOhShuqASWzH4ZLP4VoSgjl4tKSnLaAkqGMn2m5uI/Z6DsG1Ner9YsWk8xpLnLUjEq2GVHF2qCEaYx4qkBtIgwB2djM6fZ3uq8NZWu47tQ9Gw7Ooh+cmJijGKD+6+k6ijEU5DQy+WVaxw68b/TcuPqqCSNR8LX7HsK0Iy5OEEV2O99CEp0qAYVSQN93j7Jg7rNkkwvSAd61xH0SxktkDFKB2BpPdPNL6IFE1CofBiUZKEQqHw/PMMWqCOWIEuVdyOhyx1SpDcelTLxe2aFMGrzYPTxl16BMRQmzxQTZEskh2FzcM7R+HzWDa4/2ay0k56xCWSFabGM7MdlsgqOk59Q5Kt9qJkcn9+kp2pwev85D4nSUEI0ZLUEHrHfNUgKPO+Yelr+ij3bM+LZIclHx3LzuIITO12z84orM4i5ZN+wt3VHujgPPSu2/RbwoCdx70vM11uISIJJsm9vf1bX4of2rvWVRRBjZDMVpC+VWF5khRNQqHwYlGShEKh8Pxjrz/tFVyZkBxHcY+jsMdR3GehE47jjF4uZ06nKsxDjY92LQoeW4XG+Qiw5W40xLyb/XZYD0Hbfkqhrj3WJaII1qY83dgEWhOYdxWrWJMwrEUMYytNHBOFzXHXw9O2yPIHQ/CGGASNilXFpshZ3zIPFR7HYN66uWYgBYMGw9nplIl4Gnve6UpZRkcfHVENMW1pLdaJwjm18PmbsL7HO4dFwjA4bWXyTATPfQN72fpEAtAJsgQCGG/W8ydUJf/cBNY/sot1nRUKhcKleaYsUAuFQuFKpIt5yb8XmdrELCrgmKcJJ3HGO35G1Ivu8SR8EhbBYSSyCDWWRFUF+pRbgqxke1EhoZthB6BmY4M6JgyGrd3//F1joK0CzkRUYBUrWuNZ9DUhZZegPtisaUiwrUlQwzAXYHt7fFvUfK46EBIHbslLzRlOICbDoq9ygG12KxOI5AnPJjEjkDpLSEJtt46pymnfcuZbfMhzIlJ69x3zd99Q3+z2SzAYBBPyZGWrD5+6IORZCjYaYlKSJkyyOcGwg75h0Fwbfehido78KIh5YBmpUCg8p5RKQqFQeP6Jbz7tFTwSB8ZzzQT2JPH5/gAVw30b8O+LADZrEaLLQl2EhKGS3bnNWXYw1Be23I0eHBzm1qXaBiqTqEyeaWCMUptAIx6rkRAhRjMcamjlSQz6g02VQtaOR+fXP3QSRaFx+Qoa6wlJ6LzlrL93iKZuBes2RWbO89L+GUutd1x6EobjMGGeGlbUVC4iooNOWzFm2LkfXaEuEiivtRVZiGyWw+fK2v70ntdvYfpcZZGh6OEW4FaGapjQPGjIQeHt2bULLAhuXXCuxoPQ9ARF0oVC4T1BSRIKhcLzT/Xhp72CR0BoJA8AO4o1XcragqsEbF5NDpCHINiZhJMAqqQ0JgZDUD4OJ9gsI3OfVqCuq/C9RaNSiR9enkjJUgVoYsy782Zrl10Ngtnt4lmfSgZ5wZblkeS86HqzZK/uSZptS0/7CctQ5WOPiZNsgnFVYX/SU7vApO1RDJ2a9UtjMtQ2EtTg1RGw2HWl4XJb5yJbgbQCfQ74TZTNjIMLYFSQFWgPthuE5f0oKt/SJSgc1xcL/k+oL3Ut5xHzpCYyFAqF9wolSSgUCoX3OFGFPuUhXEkNIRnUXOKfb9kEvSHZ7NyD4qOhT5thapnzAfIYqOd+/x2p7mCDGtUQoqW2kf3KD1aZBkIesFYpJG/RYIYEgXta/Ld3/tenP58MmXwvjFGshcolxGRb1PNNPKPtqDGJtup55doRiGBUMYP1KyjLlLUIlQ05iSIH+2KGUsdl2VgzAZqrAYH88UGHk23j1qGVyAsSciKlyO5v67EKAxx0iwst64D+4S96FzS9e5tUoVB4/ihJQqFQeP55ptuNlKU65snhFfZtRxyev/ARtl8qw2wAADE0JpI0Jx95yJrec+RB0rzbeTRGswopCbXpcESsJKwotQ1ca5ZMpc8Bspc8nGy9065r8a0gGDXDZGXZPunOZr4ZBMx9sBhJzH1DpxapEqN64t52HqHzjr2mQ4AogpHImJ1ksbJBUBrr2XP9UKkx68FyF2ftW5pNnCpQxzqgv+jUBSUPTjMrhgF0kOz6m8gYsCu8Or97oZW9zPyS13KfdRUL1ELhhaIkCYVC4QXgWQ5ulKNUEYfA+2Yz50a1GCxQL3JdmxYiQ6KSSCURJ2nd2769wT26G6mOf8jmMOPOvu4ePqnBh4oYLDEIVgII1C4yrbtBaDtUKyS/KbcaCZIkT4t7UAC69bSRiDWRykacDfTG0luDqRLVpAdJ6xRn7JYSzaJsHwyWSCt99llSIall4jx7dYezyp7r2XcdrfGkdAlN8D3rHbQV+RKJ08vtwguCCwa3MrjT4R4xiMfT2I6V79mTnJVQLFALhReLkiQUCoXnH/v6017BIyDsmZ6J8VQmYCXQ2MDEei7bMx8ZWpXYTE7uos0OPOea5mVbHQs7/f5sPT1G4o3L4uUQHAYlqsWZQOfzVOQHjRGARD3tqWcdtvJA2n3t1ukr47nWzHlpekZl83yHIDJMYxZsNWzhj+sCkEglgbOzaU6QBl2EIsThta3taEyeodAaT23C1fPKsRCigN8kK/fWZx6O7SQ/esnzE9JofZrnVzzpYWqFQuHFoiQJhUKh8J4mW+vMbGDPrHBk16CL703vRtyjc49Phj4a4tCok1uJ9J4NfRG2Bg8PAbYyuBHlrfKYZN3+AmAlpyERoak8k6rnflG3AFWTqFyinXiaNuHqdK49Kv+hKuzVASuJieuBhAK1jdQ2bFmm7l51FEMQxzJWkHRwNtoVOE9swGikD465bzjuptny86qIYgPYziAhT1EeE7NLHgbjwXRgtsTduS3syZItUEsloVB4kShJQqFQeP4Jv/K0V3BloubHIhmOkyOKxQhUnB8K9mDGkNIATnJzkYhh5npqCTiJxMh68vKa88G6jDaegGwGl4kIp92E+aohRUAVr1kQ3aeKuk40ld/sso8hvAxJiwqzpsPIOWXv1vmdiRy0C/aaHp8sqoozSu0i19oV03qFxnPiZ5MXu/AVPllOVlOMKAaGj4qRrEtYxppT33LaN0OF4Wrb9IJmwfZSMAGkJ+sKht+2l2rrH5Yh4+dDkK6Q501cwjHpUckWqKV0USi8SJQkoVAoPP+Y1572Cq5MVFgmIQyh/iaMv2zApkSVIcDOzxjJvfl9tCBmvVE8NuSMtqPbYuMH/dZIKbfvTFyHNdkhqE8OFcFKonZxfayxTUZV0Ah79ZL3z454ff+ISdVt7blvgn5nA8ZqXrQVeqlwNlKbOMxkiGjcXMNa+zAkCYbEvGuyV9DwPUNCNHFrvk/nK/pgmIeKTRHhKkGxYLyBJEhnszuR3X3FUIjZkXicf2yfWnWTFChAGkTQTzBmN7ZUEQqFF40ycblQKDz/mEcbJPU0MQITo9QxrZUEPhn6S/3zPQqQhYihV0dM0AWTd81ViIm1y49uCQ9EZHC1yYpmXQt6ZWen20fDxPZZKExC1eCqSNv0VC4MzfTnl6U04rneLNh3HbZVZCYs5pONjmFYT2Vz0hERkgjz0NKpozKBFITgHTsZjG6+DMmx9A13FjOSMugvBBFl3tcsk2OZKqIx58S5VwiMZaiUbFmejoJtvV/HzgNOMSYKYgaxcjZcGpIs3SRvT2h3P8UnmJEUCoX3BKWSUCgUnn9S97RXcGVUoVeDxzAxCUsijrY5F+LcvryCQalNRIxQmURMhqQWRYahWRuv0/xewz0Gnrp1VFEihlPfMO9rUoJeLQkDBlwVidGu22bW4w+MsOgmOcDv4EZzykGz4ubeyW7wq+Bstlb1CL0YOioiAmqxZnQ12nqPDJadmu+AM5FrkzmIrKdJR7V0VLhKqepIMkJT9xgiV0oQxhuWBDWbGF7TxpnoSoc060sa9COytlQ1F9YJPFolQGTn7hYKhReAkiQUCoXnn/i5p72CK9MheAQhYSUxsz1T01Otvf4vwiZANESsSYxTl0UVOTfla22BCpvMYt3rsntIHaJVlUTjAkEdXbBDRUJRgZWvSckOUwx2g9WkgkZl5npemSy42Z7y8uSMw+liWJVgTKJygabxzKaeII6ghqTCqqvofZ0XsX0dmrUYqrBfLalM5GCyIqgZkgulS5ZeHb06KhfBKLVV6urcCOhLMCY/6oTU5j5+GVusrjB2Yaf1SDUPrVZd6xLenF670KFu8WjVNNVHTTMKhcKzRkkSCoXC84/78NNewZWpUWriWmgrg9hWJHG5sE3RnCLgoyUmgyosQg6wU8oBdVrHx+OOe37IuWOdb6JPanIAroaoDisJRfBqcC7mlqP7BN7GBEBYdtUwwyDhbKRxPguPjaKaJ0XnVqN8rjRurytoGga1rdUaGy2DoBy2HbWLzJpVThyGtCgki0+GmISk+RrT8LleuFJzz21GjaIm6ze0Yp0oXOowW6dfV2D03udO6osF/yfUl1tAoVB44SlJQqFQeP55hq0bRaA2yoRcOUgqhMSlLVBlqEYYk3AmEXDUxg/hcu77HxOF7Lp6LiLdftyzSCU4w2lqmIeaqNliNSRDwNJFS1UFGByZ1imHKFoJUQxeKyrjqW2gtnlgmpE8M0F1SGCi4IPNugKU2ijT2lM7T9pKEmRLgd3aQGUilfWcpnY9bVoxTG3Pvus5qFYcuCUT8cQkhPAIvxp1aKcyiq5LCA+4b+/CemzF2sVI1x+HQhAoHHSLCx3vgP5yC7hnQZCKu1Gh8EJRkoRCofD8Ez7/tFdwZTbxodCpZaEN1git8Rc8ggy755msfc075Y1NTFxkzyxJaRTz7ogRNqsYFpJbkbYCchRb5ag1iSEaaCqfh5uJ4oNFRDBWcYPLjw6TwNQpUQw9Bk8Wad+YzLnWnjGrVjS1p2oixkUqF7EGUnQ4SVQmYCQhqlSSBnH1+asWrGRNhLSw0oZj364rDEYiM7tkZntqiRzYFVPpUJWdOXL3/aHc7+kxsB+qCOIUrD74DQ9j3coFDGsSHX6WSRFVXlkcXehQLzO/2hpG9DL6h0Kh8DxQkoRCofAC8AwbuSn0yXCiFcvkcqvNlvvQQ9++FTwL4NUQxkATJSZYporNrwMdet5l7cqzFj+LrE2NxgPqcA4RRU2ksQHRnJYkFZIFsSnPQxh21YWt40ikV0tlI3101C7SVoGqCdSTSNN46jqwWtWkKIjmtisritVEikLXV3Rdw/2265d9zZmviRZWqWKeWs5iM7TuGGoT2bdLJCZIsPANKubeqsl5PcZ9qiqCIhbEgjGDM5HA2lP1srnC2DWl6x/DTlFMFAxXVENfEmNLuFAovGiU/+sLhcLzj3vlaa/gymSzHINqno4c1OCTYZWqSx3FSsqBK4JqbgXqoqWxkZnzpNGBZ0gE8rA01m0uMi7mnl79bFdUu0BVK70YFtERk3DmGyJCMIZkIaax1QewiriEreAo5MD9qJvgJIJTJtOepvVUtUdMHni26mt8EIwmZmbJvnQ0EgZNxP13uY1JzNoeE6EmMJGOienZGMoKtYkYwGsWRK+FFrJVBXhQTradMBjJv1VtQiWiokSnpKvmqMMlmfXnWQ6+Fo3f9+fxxSHFJ5OMFAqF9w4lSSgUCs8/6WJ92+9FrEAtSoVilRzcpwq94D/fIpuqgQxtQ0YSleT2HSPQ2LAOdMc4OH+5sTjKWoX7tc7kqce1TTRVz6zK7kON8RiyNWkXLb13pLSZKibrGQKKihCwLFNFW624Pp3z0vSUw+kpSMIYzeJiFKo8DG1iIrE3EMCRW4/Ot8ynBPvNEuMVVkJNz2G1HFqqNjayIvDa9ITWdHk9Ozdw62G2Pn/YPa8h1aCVkCry2i8bzw95Sk7QxgoPa63Ck2z+ESNP9oSFQuGpU5KEQqHw/JNuPe0VXBlVWCbDUh2IxZk0tO1cZmd341AkJCxKksHnRxXdcgrayguGIHoMpDcWqLoTKA/HVsFJIpGdh4Q8g0GToQsun090K5CXIefQbG9qez58cJtpFXm5OeV6teT19pTD2YJYQaoVP1QkfDJU6ln5is5X+OSwNnDeQsgYWPQNq76h62vw+TqT5ruXtu7LxHqMEW60uwnlVV2OREAtJKc5I3qE37ZpSNLWh8hurU8UTXr/SkqhUHhuKUlCoVB4/nEfetoruDKKEDS3G+Vg3gxeRXCRRGEMygWlEk9l01aHitLFPG/AoEO/+1Z7jdn6ensbeVRTj58ncCbg7LheIargvWHl7TqYV7s7x0CTkILhZbugUWXqekCY2p6Z7dmzPXuup6o89V6Hr6ALFSmBM4nKBoIKPlm8uqESsuvN6qOjC3l2w8lyku/hVmP/uovIJCwRJ2mnz1+uEhmP3UpVhFkkzRJajQPfLshWZWdsAdOtS1sXdgqFQuGLREkSCoXC848+u/3UtVGmEjHkvnxVoR+GgJkL9H+Mgb8VxRhwEqlNyBpkIxw0SxobqE0YBLIyeHieO/ZWhUFGS9XRkpPAzHkckdpFGhsRUaxRaon4OHgqmU17jyaBINgEVVKm0uUhZwAYXp8c80p7zEv1KftuRQyO0FesFhUT60GFpIbKRkIS+mhzSw+78wRSgi7kCkxIjj6ZdQoxni0N132tXVBbP8x4GC/3CpUEIWvlK6BWMAnur6t++HEYFyu5xUiy7iG7Lz3ZLOG8g1ShUHi+KUlCoVB4/gmfetoreCSswNREJhpYREtUs5bdXgTZ+sOYPIRNh0dlFWcjVnYTqXXv+9hbNE5oHpKLUc0sRlh1DcEbYm+oxOMkZfchyZ7+KRnSsMu/PX9Bo8EGZSIdL+3P8VoRh+TEiHKzPeNmM+f9zTF1iISVYY8ev8oD0FTBmci07rk5m2PNlnvSsH5j4Hi5x/+89SrvnO1x63RvWHq+g0mVNFRnahuY1p5r0yUT5x9fDG5kmJ0w5F/rG3yR97KxPN0WLF+aRxcUSLFALRReKEqSUCgUnn+e4XYjyBOWowrHseFWv4dPjsTownORI+S5CKI5cEeyG1DUvIseVQaXTlnvnI8iZ8Yd63V14d5AUTUPYmtspDI5eUlDu1GKNg9pSyZPRt5+o0Afa466KWrAI6w0D0sDYeay0Pjlds4H9u6yV614c37AUhsWqebadMms6TloOw7aFVPXr+1VQdYBuQInqwnvnB3w9ul1YhoGy62zJyUpWJuoq8he3bPXdFQ2PvocPgGt8wTmZJXk8pC1y7QeKbkYgYIkxY5C8gRvTa5d6Bi3uNhk5gchRp504aJQKDxlSpJQKBSef8Q+/DXvUZKCT8Kd0NJrRVCb242SHRKFi6BYyTMMDIozCWty5aBPVR6qVnWbLe5xt3soGGxpjcfD7WCGlYTe0ppAbbLd6iuTEyampyYQvYEkm13+LVahZtFVVEQmEtZjBUA4nCzZn66YTXuu76/QSrnTTfjM2Q2CKG3Vs98s2auX9Nv343xAK3BnMeVk1XLr9GB9DiNgUaJa9hrPrOmZ1R21DVv6hkcgmxJl8XUraAV6SUvUtW5idGVFkJSrOqf1xYL/E+rLnfQculXJKBQKLwYlSSgUCs8/8c7TXsGVMZJb26ckSEJMZp0kXDxmM/ho1/MOzKDRUDYi3trcx1NziEy323fut52sCCkZel8xGJWiCI2LtNYjwWTLU+U+O+iJabPieDmlWu/uZ3o1qEBb9xxMl+xPFkQxBHWIwDLWTJqO5JQjbYjWbNS8ArKlq1DNxzsLLZ87u0ZK6wsCIA7zEl5rj8AoQSxqxuM9QrKggIPUKLFKuZIwWqle9BBmCNDtIFXXjfZiv7uYve8B/SUXvouIDFO5C4XCi8KVk4SPf/zjD31NSs+uWLBQKDxH6LM7JwGgMkolWda7R8Cnjcf/RYkYApY+jXoGISGIpNx6hMnC1O04cGuA1yhP0PW2NutPVAwxGaqhUmFIqKZ8rgS1RiRsiaHHON4kmv0eO1GSMYS4aaHK8ajBCsxcR0jCSZ/Vv85EJq5n6gJRhIU6bvczxCi4uHPdMtizioW5NpyEhrPQcnu1x7Y1bGMirXhEYc90w/kVWTs8XXEb3ZDFy42is4Q63Rp+sF7CLueeExHWOc1Y5Rl+e7+6Or7QMl5mfrX1D6gqxpRSQqHwInHlJOHrv/7r+Y7v+A6Wy+V9v/9Lv/RLfM3XfM2VF1YoFAqPDfeBp72CRyIoLLVCBFqX0NESE7joDncaNAmVRCqTsBIZN4YjWShsxl347WrB+d8SmnezR4nw+uWaaI0HBSuJoI63VtfoqRFJ9yQ1glDVnroKdMYQKzhLGwsgI3lKtCXRR0dMhturGRPXU0lkYj2N9dzt9vAIQQxqFdzQrD+2TamiJoHJOomzUNOp4a3FwVokLQKNBByBFGHmet6Y3qG1fjDGerTgWA2oU7RV4iyRWiWdH3Qg3JswrO2XNq6tsv58UI88wc244m5UKLxYXDlJ+Mmf/El+9Ed/lN/8m38z/+k//aed7/3Nv/k3+S2/5bfw2muvPfICC4VC4ZF5pmMbwZHFqobsrGmUSw35yoO4Yp5hkCxCBBVUDSJC4xKVya1BurNdzX3bi3ZcdkQQI9xeHHCymnA0b1hFy0mY0uFYpgox5h4tgqpSVZGmDohR1MKJb3eGrVkiMcHbixk+GjSBHSoqfRSWwfFSfcae7Tl0S4iC9nZwbmLtYsTghGQETrqWW8s97qymvLOYrWNsEaW1gS85uMNvuPYFXp8e82WHb3JYP6YqlFGwmuclVDlheFjQPYqTRcmJ4dARJglI8qi5S6FQKLwrV04Svuqrvoqf//mf55u+6Zv4nb/zd/Jd3/Vd/OIv/iJf93Vfx1/4C3+Bv/f3/h7/7J/9s8e51kKhULga8Vee9goeiUPb87JbcmA6DBEhXW7esuSWFVVBybMWsquRyXaqokSyTWm2udSNqxEyDPMaqgY7B92cI6nQeYsjz3aICotQc9ZXODsKEnLrzzgYTIfWJ2egSxULrVnFjchch5aom7MF4pS9tue4m3K3n3LazzjtJ7SV59CteGN6l1p9LiKMSczY1jN8EAVNji+cXeed1R63VvtbBQ5ZJxcz1/PBvTtMq8BL0wXW+OHKxxrKRTUKgzpDFLEJNbpuP9KGXPl4yM9tbWKVsp0sKTsbierlhm4/BooFaqHwYvFIwuW2bfnrf/2v8+M//uN8//d/P7/hN/wGjDH8wi/8Ah/96Ecf1xoLhULh0TDvf9oreCScJBqT2HeeA9MTkn2gHemDSGqIQ3KQd+lzABtV6NWSyBam64B66/B5V/5cQHvua5FEMobbyz1UYeVruuBYWUcnBoYBbuMxRYQULKFzSEjMpOP9zV0ma02BEtQScXkom41cb5fsNytO+5qjfsJRNyEk4eV2wc3JnA9fe4f7Be+CgBeIeTdek5BUsFbxWLyO9p669R4liZBEsBXYc1qHzZ3ZTh62n9N7bplIQqo0tEQpOHn3XGM89HDfVBWDZJ2EHdf5ZBBzpdnThULhGeaR3Y0+/elP85f+0l/CGMNXfuVX8olPfIKf+qmfehxrKxQKhceDaZ72Ch4JN0xK7pJloe4S1qeZIWQlW6EmohqMKE6UmISTbkJSwZjxldseqMPMhKHCkNvhdfvAIAnbBpKDaAyalBihD44+WOZ9zTqqHdekECPEKDQkJimy57qt7wtBLUFz7WRtWQqc+YY7/YSjMOEkTpk0HQeTJa/sn3Fz/xQ2S19/LmQLVoKhFY9TYdlXxDhKrfN1GvIsiaSGxvks7jYJY4aKgHCfh6x1ArD7vW1EJDsUScpVDvfweQnbiRUmD38jKYShmvCE0HSl2dOFQuEZ5pGShL/7d/8uX/mVX4mq8olPfIKf/dmf5Tu+4zv4g3/wD/Kt3/qtnJycPK51FgqFwtVR/7RX8EhETfgkeDW0sgn54eHaBNXRn8iwCBUhWfqY23yW0XHmG/po6aKji27dl3NuY531QDVy9LtpO4rYOoLNCYK4bGO653om0rFYOFRN7snfQozifYUPFSaBSynbsG5OiMPTSKAi0hqfKxOxIibHWWyJxjCPDUdxwn6zZNZ0vO/aEU3Vrxev46AHVSQAAeqUh5N5X3Haj/MDciJiJDc5dWqpTWS/mvNKc4aTwMP27c8nBjuJxJjlCOiektpEqrM+Yfc+73xYf66A5gXmtq3xYE+IUkkoFF48rpwk/O7f/bv5c3/uz/F93/d9/Nt/+2954403MMbwsY99jJ/5mZ/hE5/4BF/xFV/xONdaKBQKVyN89mmv4Mooikhi33W8Wp0xMx1TE4bkQO7ZrT7/bpGND1FUS1DLMtWEBCnmNpsIdNESklkHnyJbqcj97DrXGoUsMKidJ0rCSKJXS2M9Gg3JV2gccoztwJ2hmyYAQZlUHYtUrQNREaG2icb0qAoViUYCK+8wJtu2zkPNWag5Cw3JKtf3j7m5f8qHX35rlC6znsAskm2GgiF5w3zR8Kt3b/CZuy+z9JvpZqN2Yc/0vNqc8Npkzs12zvXqMQiYx/tnIE0gVUqaKqlOm6xg++e59ZwBZNAjbOvKLz7h7BEdmkoloVB44bjk3McNIQT+63/9r7zxxhv3fO8rvuIr+M//+T/zPd/zPY+0uEKhUHgsuF8D/Wee9ioujaoSSSQShkCviopy0y04jfVDgzbV3RgyIcTBojQH4AFDotLcgoRITipUUc3TkdcuQaJDYjKImceDJ2hdZFZ5agtqDSehZZp6Kk3sSccyTBDd7EmJCjq07qSYqwzVJLJILRaY2LHyI1SSICp9MKyCw0chJIMzBh8qFKGxgWgctlZee+kup6Hm/S/d4XPv3Ny9IZKThtNFiyLsTVbMmhWfun2TX/vS27RVwA6zEQ6bJc5EFqHGB8ORm+BMJKQr/9rc/FwAqRI6EdQoKYEEzaJuHde5cY1du0mRKx0YyQPZQuLt9tqFznmLi01mfjeKcLlQeLG48r92/+bf/Jt3P7Bz/MW/+BevevhCoVB4fMizOVx+01RkCJq1ArVEGhuxkgZT1HdjS0cgw5AzhC5V+JQTAINgJVusjs+tzY2GVeSt63Nb3MM9VQVLpJbAxHqMiVQ20VSJtg4ctp6zhWcZmuGYmoNNBWcSLx2eZbmCgS45DDWWPMtBJLsmLVPNaWp5u9tHTa52pCgQYdnVMIU+ufXCp23PwcGK48WSs8X9gmPD6WpCr8KNWU0Ug1qLV0gkHBERYeZ6bjRnnMYJtoKDacfpkjy9+gr76iJj+1duHdJaSUkxFrRWpNtyjFLWw6M15XLPeoJ0GnQJAifV5ELnPqF++IseQmk3KhReLJ7N35yFQqFwGdLZ017BFdkEolO7ZGpW1BKoJeHk4v6XY+DvJA27wTlRsIPJ6CjdDSnPThjtQO8V/+oQvOZKQh7sJXhfEb3FpNwWlbBEgahC7TyT2q+1zptuJSGmPGXZmESXLKjiCNQ2rYXA81CzjA6fILKpdIRoWPmKed9yZzEDBYtSmYhWiqki08MlVbWrRxkTL1HwXU1Uh7GKH1qx0tZFiwizyrNXdTgXqJxnUvcXvu/3Y70Zb5VUJdQlcKBuOyk89x6V/Ms6kYe7DU6qKOz7+w80Pc8Bj7Zu4F6Hq0Kh8FxTkoRCofD8E2897RVcGQW8JoyJzNySa9UZ+3aBvYBJ/npKL4ozCWNyQUAFEoZJ5Zm6nsZ4zNhiNATna2HsjhJ3CPK3NAoi4IMlJmHP9kxdoDKRmCyVTdQmUEvEsrUTvjVnYL5sMS7bnQK0ZttqVFmlmiSWtkrMXMc4BC4kx0nXcNo1vLPYwydDZSNBDU0VkEoxLjHdX2JMXJ9Phv8Y2q40CUeLGV204+b8ZvI0WcdhTeKl5ozKxItLAB7AvXF2bh3StNVnNN7j8Td0yg8jwOB+OwqyX1keXei8LzN/pHUb84gXXigUnjlKklAoFJ5/qg8/7RVcEaXXhEeH8WkRiHhVVlpd+BiC5sB3cEYyKKoWAZzJCUQc22B0EPsO79212dlSLm9FyynCxHgk5dajSgKrVLE0jt5agh2Sgu1Ge7Kdp3OBxDAzTPP8gtEYVDFMXM/EeVoXuDFZZkH0kMz0oWLRV/hgeOt0nxAhqMFK4nB6yvteusONwzNeeemIyoWduyLkwW7LvuGd00N+5e1XCNGy7eGjCaZ2xav1MS/Vc96Y3OGgWmS3qCtuqo9uR0bARMnVmOHeqrlPEpFyZ5dK1qiM/UpKPsiT8hxKqVQRCoUXjUdXYBUKhULhi4IRQ6U5UUgqiHjOkgURDLrTGvMgVA1iEqqCI2E1B+BRhaCCGeYmTFzktM877OuAdGjv0fN6hHN9SAlHH11uA+oN1Iaz0LJSxyJVdKkeGvJ332pEcC73zsiQRATM+mVBDQlLaz2ViTgiM7vijGadKMx9yy/ffZVlrJm4ng8cnJCiwVZCaxWSQSLACe/cPSCETXLlk+Hu2QwfLdNqxWdv3+DDN96masO4QBywX6046Ff4yvFyO6fzjrcXBzzSPtvQOiQCajfzF7bEIPk+mPza5FgLryUpdrB1fVJCATEy/J0oFAovCqWSUCgUnn/C2097BVfGicGJQbAsUvbudzIIly8YsyVGR6MsKq4ltwQlFbpoEVGciVgJeSLzZghDThCGCsL2t7aLDEmVifNYgRQd+7ZHSPQpz2YQo4gEdqJfyBOIRUliso0pkgeTDZqIpEIYWnBEldpETv2E87+6VIS35/u8NT+k84ap65najqntuTadUw1zHOo2kHaiauF02XI0n/DOfJ9b/R6fWt7kjp/u3FsjyrTqqFyiriJ7E8/12ZLaea4cpRvQiZImKT/auLGGHZKFsfCSjCI6zElQICk6aEKeFJq0uBsVCi8YV64kxBj51//6X/NjP/Zj/NzP/RzvvPMOk8mED33oQ3zN13wNv//3//772qMWCoXCkyc8/CXvUfJ8A8OJOqIIHZalWvbNinfSbKs16MEYErWJVJJwEnGSqExkFR0dFSt1nMUG55QQx1aazXFVNGcaCGsX1LHkACDC3cWMg3ZJnOaAfmJ6ptYzs555mKLjVvhWNUGBJEqSPF15HmpuuLO1q5KIUklkheaKCMLK36fNSsEny2lo+ezpDb78xhewkvJ06aT4ZPHJsooWrK7Fv+N7kxiOw5S0ADtJ1H0gJeF6vRwmPeeZDbN6xUlsqUKkjoGkQh8M56dJXxiBVA+zLFoh+YRZmGwRq1tjKcZkwOQKjxlbwuTJ7uyn9ASzkkKh8NS5dCUhxsjf/tt/m1/za34N3/Zt38bnPvc5vvZrv5Zv+7Zv41u+5Vt4//vfzw/90A/xpV/6pfy+3/f7+MVf/MUvxroLhULh4rj3Pe0VXJkEeBwGy1lqWKQaI9C4wEW0pNl2U5DsOURlEpVNdNFw6lu65JiHZuiTV5zNVqtb5qk7iYiMbS75G3nQl4EuOM66lrlvQOB6s6SWyGG15LBasikfbL1RDVENfW+BxJ5dsec8DFUEVcHKMGhMlZUX0jBvQbbackZfoKN+wpGfcreb0tpAYz1EQ2N7+mQJ6jayCt2ImMdJy4pw1E+yPkLsunIyaiWMKFO7YmY7GhMGK9QrJghbSQoOUpvQijyBebhFafi+0aw/GJO3J9xptKZUEgqFF4tLVxK+7/u+j49//OP8w3/4D/n6r//6B/6j8YUvfIEf+IEf4Gu/9mv5whe+8MgLLRQKhRcRQcYxAkR1ZIObXA2wJOIFgtSEIaglqmRffiJRHJVJNBqH2QVbD4ZEQTazEsTI2oFnTCKy4DZXFHyy1K7n5v4ZGENSaDXmXW8D2IRGWSccqmBsok+OTg2SlAPX7V67gFFlZjtO+prb3f5GXT3046gO/fyG/JrVhC8srnGtWVJJpA+GWgKzaoG0yqpv6FbVlvBa0aSkJKyWFY1tCAeCVGl9KgWsBKamY2EbpsYzNZ7WeOY0V5iYsAnw18nORIlnEXEO9Xmw2mA0hShDa1G+1kHlfaXzXnqdotx94xb9/oqPf/bjfN2v/zqsuWJiVCgUnikunST8mT/zZ/jYxz720Ne9/vrrfPd3fzff/u3ffqWFFQqFwmMjPHvTlkfGYPwkNiy1ZUlNlLzzftG95KRCVEPSPATMkRCJRBPoosWpEqIjRkNMgopsYvHxkYYqwtqlczdEPZgscJVibRZCCwkjWXCdHYvMJqgfEwUHfbJMTMAmJaYxYs679qLZ2kfI1qhdGAemyVaiIGBGCbfh7mrKS+2cXz075PX2hJnrkb1TFGFeRZarnoVrOD6dZocnEbquJiHUjWXmOu4e73HDndEZQ2MjBuHA9VQkzpgwNR3XKkNohDurvStt6ctQucg3guxstKeklDALu/nhjzIOYa1BUJHcehR016L23c946TW+/eWf4xe/8b/QHeZZDP/bP/3f+MDBB/i/vvH/4v/4Df/HpY9XKBSeLS7dbjSbzb6or383/tgf+2P8+T//5x/4/X/1r/4Vf+fv/J3Hdr5CofCcIPebuvusoHTJcpwmBCqWWnMaJ9wNM+IF93mEnCiMot2QLI0JOIl03lFLJCUhJENSM7QU7ToY7cSYw072mEBYEzicrXAu0Ce7Fkq3NiCSCMNzKqO5KYzenylBt6pwkohq2dpjR0Qxmpj3FT7loWsy9uHLuKZcyUgqpCSkZIjJMPcNxzph2nZcny64Nllw0C6ZVj1O4jnHJui7itWiInrD0WrK51fXeXt1jVPfMCYuExd4tb3La9NTXpuectgsqeQCehe532MrQSCLw1OtqFW0zUmPpI3j0Vi/UXSYtpzvw9vttYefH7jF5f4fePvLP8f/7w/8f+kOdoe1fe7kc3zLD30L/+9//38vdbxCofDscWXh8g/8wA9c+LV/9I/+0aueZod//+///c55Qwh88pOf5Cu+4isAsNby/d///fzxP/7HH8v5CoXCc4K9+cxql0WEV13gVgzcIeGjY5WqdW/+RVFV7BCiZ5tRpQ9mHSw78SzS9g72tl/p+PmoJWDHrzM6OAktLgbeWuyzX6+wgxaisZGwtkzatBuBoD0gFvGGxbKhPdydjgyCV0MvFWqFII62DvhgCTFPFZPtYHtY5q3VPpWLTEJkKTWVJG7undF1FUknebKy5KnRmzNBCJZ3un3EJ9q+G/KQyEHVr1/Vush1OUNF2etaPrB/l0+d3Bwsai/1I1nfQo26TnpSo8SgEBK2M0MONIitUUzMdqT5dion1cWC/xPqC69LRfnFb/wv4yXvfm9IVv7PH/0/+d+/7H8vrUeFwnPMlZOE7/zO71x/HmOk6zqm03v/sVLVx5YkfOELX9hxTPrMZz7DV33VV3F6egrAr//1v55PfepTlz5u13V03aYX9uTk5JHXWigU3kPoM5ohDIjANeupBN5vV5yYCe9csn1EAGsSlUlMrCeqxVihcgH1gtgccOvQVrRuhRlbXrZsUTf2RAouYZxy7BuatGIRKm4t9nn/9C4aYbmq6FYt5lzhWhgcfGLEmcDNg1OCWJKaLUG20mtFr5ZFtFQuT2NOaghjYH0OFcMi1ry5PMBWiakLXK8Xebp03edAXM0mzxmdgoar8snwznLGtF/iTOR6PceroSINGg3BGWXfrbhWLziqJ1xvz3hnuX9ljYCME5etkCaKXQna5gFmthtqCDLUYMYELuVKzoFfXOgcB/QPf9HA3TdurVuM7oeifPbks/yHz/wHvvbXfO2Fj1soFJ4trjwn4c6dO9y5c4fbt2/zzd/8zRweHvJTP/VT6+fHx927dx/bYg8PD9cJAcDx8TGr1Wpty3bVQS/f+73fy+Hh4frxwQ9+8LGst1AovEcIn3raK3hk9ozHRWEGvEJPQ+QyzfDW6KAVyNOX++TwyYEYnI0oUNkIorttQUPrkY6uOqOYeXjd2PEztT0GxZosfh6LB1ZSfmdiY+U5vEdEuXl4xmTWYytlldxuoD1Ym/poSWKHVqONy8+DEKBLji46DCmvi0RjA/vNCjEBJe0kCGuCIXQ1t09mIIK1SsLiNYuxDUotgdb0qDIkXQE7tP9c/tfQrlMUDkIVcttRNdzjyKDDyFOZxzeIwMurowud5WXmF15Rv7+60Ou+cFpMSQqF55lHGqYWY+SjH/0oP/3TP80f+SN/hN/1u34Xn/3sZx/X2u7hK7/yK/nH//gfr7/+4R/+Yfb39/mhH/ohAP7v//v/5jf9pt906eN+7GMf4/j4eP34Yl5DoVB4CrgPP+0VPCLK26sJR7Hlbmg4Si1RzYUDUiNKbQKVJFSVoIYu2fVDh9agjSR5qCqc6zLaPIZZCpo1AFPr2as9+1Wgdom9ugcrTNueadNxbXbGMPZ464oUYxIpOXw0eDVMTL+T9uR1JYxRGpu4Xi+wg57gvKZg99iQkuGsawnR5ERhmBGhQNUETJvApE0yxLBB31lSb+jnE+bzCbCZ/KyAGbTStY20LtDansYGKpurVZd3CZW1vELGO9MKyUGyQ1eRGYs5ur4v43rliptj70Z92l7oda/vv/7Yz10oFN47XLndKKXERz/6UX7+53+en/iJn+B978s+5N/wDd/Axz/+cV5++eXHtsiR7/7u7+YbvuEb+Omf/mmMMfz8z/88/+Sf/BN+7+/9vfypP/WnODs740d+5EcufdymaWia5rGvt1AovEeQZ7tv+jjUfCHscRwrbseGd0LDWaow5t7BZ/dDSbkSIEISR9KexkSCCURrWAU3tODkUFUHI35Z79qztjpdtx4NVpxWI9OqZ2rzlOPWesQoq1SBSWAM0zqwV3tOlzZ3y4iCgI9C8DDTQKPZVjQNzkjjfIfKKnUMWEk0NnLgOhar+wxUG680QQwGNUKMlmVfs3QV1yvFuMB0tmJPa+72E0JqoCdXOGSwF1UhdQbjlH5ecftoyv7Ly6zVxsAw6VqAl9oFfapYhIZPn93ILUEX+Hncb91rx6ZoQITkFKMmJ2uSrWDFDP1R46CER9rmezDXP/MyzfEki5bvcymC8IGDD/A1b3zNF2cBhULhPcGV/4n5A3/gD/CJT3xiJ0H4a3/tr/HVX/3VfNM3fdNjW+A2H/nIR/jxH/9xPvShD/Hrft2v4+Mf/zjf+I3fyM/93M/xV/7KX+G//Jf/wjd+4zd+Uc5dKBSeYeKtp72CK6L4JPyP1SF3YsMyOe6EhluhHeYZ5Nc8rO1IELxaohqiCl4d1ow74QFLnp0QwjiobGPPKdvHX2uEN5FjSJb5smWxaliuGvZtN0x2zjlFZULeda9DXsnWVruxebd/1Tl8Z9ZniuSW+y45olpqE2iNx6ZESEIfq/vu2IsIMVl8yC1K/fDRB4ugOKc4G2mqwGSyyuILs+Uxuk6IDMdne5wuJ8z7KUktCUMEouY7oAi18RjSuhojZuM6dDk24uvxk6Tk6dAm6w9kECpnUcXwU338RYS8GhW+7Ed/87k1jSvNa/0b3/g3imi5UHjOuXIl4ZOf/CQ/8RM/wauvvrrz/N//+3+fb/mWb3nkhT2Ij3zkI3zkIx/Zee7Lv/zL+fIv//Iv2jkLhcIzjtl/2iu4Eikpb/kKSFTG05iOfbvitcryhX621XLzLq03mvUIAH0yTDD4ZFAqKok0xhMT2MEgNW+mD2absqkkjNqDzedDEmFg1Vd0dYWVRCWRyuTEY2Ec0YGpIkZyNUAxiEoOfkWJCB2Ws36CIhiJGPLGemMCHodLyipUOdFJ7763JYMwOQWl8w5tclIT1ax7gcTC/rSnn/d0YbA4FTkXEAvHiwmnyylLX+GaiGKGl0QUwUokIDibsCYRoh0KAtvOUBdDhDy8OeW1mAR0QwuUgSRDjpB0vbv3RcoRAHjlk+/nK3/oq3bmJAB84OAD/I1v/BtlTkKh8AJw5SThJ37iJ7h58+Y9z4sI//Sf/tNHWlShUCg8VuRiPdbvOUR4tV6SgKiJO8Zx0y0JauiT5ZafctFgdKwL+GhojeSWHqDzefc9DcG3jhan54479sPLUFsY214EwSdBNNGIp+8t9TSyGqxavXH0xuGN3alAoHm33LiIWsOpn3C0bHh1FtZVAiG3M534fbwaVslxGmogDYPQHnjbAOGsrzn1LXt+xdzXGIm4KjGTjmgM3cEKp7AI0+2bNC4PAZa+5nTZsld3Q9KURQmCUhvlsFoxdT03mjm3lvvrxE3vb7708B+SVWSZtQ95mLYgdtOilWxOsCRevqnpsrzyyffz8i++j7tv3MIfdPyV/+f/w9d9aZm4XCi8KFw5Sfhv/+2/vev3f8fv+B1XPXShUCg8XuLpw1/zHmQMzF9yC06i4ZprWSVHnyy9c9z2LYl3D9hEIKoQ1FCpYCVhRWmsR1RZpD16dXRqh576sdXo/G64rId+7SQRw3yAFY6TULMIjs4bVmrxydJjWCWLmsHGUweVLoI1kVnTY6scvC9SC+dceJahYh5qvFqOupaIxVoIF3C1jWo56SZMXc+tKnJzesq1doH24NXQuoZYVYgkUjI7rVAAXahZ+ZrbZwe8cnCK0YSIYBgFzPknVEliVnlur7KD1HjfL4/m4xvAKdjcDqUxtxiJSH5Oclnhi1lJGBEVbnz6FQB+x4d+R0kQCoUXiCsnCR/5yEdQ1Xv+UR1tSEdb0kKhUHjq6LOnSUianXdUFSuJQ7vkum2YW8epqUhpRrqgrEzVEGLCuiHw1yyEPembrCkINdYIzip9GlMEWXfhpLEHPh8NtpIIRRGX6IdEIIhjGWsO6hXL1KBeWa0qUgIxMQe8w7Fndc9e7alsoqkDVZVIyOAjNAThIjhJEBOeXD0Q0bUd6rthBBY+W6EuQ8XS11yfLFilgNeedH1O6A3t1LKct2zL9ERg5StWfYUP+Ri5XWqQdw+nnrqexvqhdSuwSOah63owwx12igo4CziQLucGWbAx3vthDRfORh697pCeRFZSKBTeM1xZuHz37l2Ojo64e/cud+/e5e233+bf/bt/x1d91Vfxoz/6o49zjYVCofBouC952iu4NDL8ZyQ/9l3kdpjwjp/ydtjjU/11LtxqJBAxxCREzZWFrFWA1mYNgQ6yYTOqYTeenPfMEpDxuaElRwwEI2CgTxYfHUZSFtx2gu8cZ8dTYnQMul8AgrdIGuYODIPOVsntnKc2kdpEps6Tolx6Hk4XHXNfM/c1C1/TBYczgcZ6pq6jbT1NG6mbeI+lrKpwPG846xpun82IQxVk474qHFQrbjQLbjRnGBOuWEHYOqdR1CUgi5ZVsmg5CRDznAYd8wQV3m6vXei4t7jYZOZ3wzx6nlEoFJ4hrpwkHBwc7DxeeuklvvZrv5a/+lf/Kt/93d/9ONdYKBQKj8YXwUv+i01ua1mnCvSpRtRwEhs+1+/hNZtyXlS+KmS//6iWgBmEwZHGBg5cT0o5cdi18Ny0B41uSrKTPOTTa8r2nQGDH2Yv+GiJK0eDYoKSxilgmvvrRaDzFieRVhPaG5wEom5+XNlBKOAkkqLBipKSHb5/seu2RjhaTTlaTbnTTTntaiyKM4ozCdcErA1YG4fqxO5dm/czbp0ccudsH7Zat8Y11jbRGI+ghOQAxZh7fy65ArJ79Pv9tRSG+92ANoq6MRGTsQCzc4CT6mLB/wn1hV73blx1YGmhUHg2eewuy5PJhE9+8pOP+7CFQqFwdeKnn/YKroTImCLAbT/FUxPUcqu/zK5wDlgrkx150pB2dOqobaAaXIdqkwbx8vavBdl48sPQ/rQVxI9Bqyr7VU9TBeoq4twwAM0Fmspzc+9sJyIeY+UYDTFYiIrTtG7Z2Q6mnURCyPeglkDSLLTOdqr3D763nxcBr45lqFn5ik6rQXgMXXAEdXhjCO/S43+2ajhbtfTBkedHm0H2ndvBjMlzHSYu5ERD7l3D/dZmzL2vW79+7MqyrGdrbyoIw0oNHPjFw/8KAAf0F3rdAylVhELhhePKmoQf+IEf2PlaVXnrrbf4B//gH/DVX/3Vj7ywQqFQeGzI3tNewZUxAsvkuB0mLJOjV5MD0d0tZd49itu8PiTBkAZ3ISVqTh2mruO0y0PKdHsjfBAZ51PkliLS7ulr8UyqSDXMIKhdohdHXQVqFzBGmTaeZdfsLFMEbs+ntHXPXrPkS657rOw6A1VGudnOOepn7Nsld+LeWiuxvch3a/MRYBkcXXKc9FP2mp6QhEVfEZLBS54dcb9DqJKHw/mGpa9onc8uR8h6vtzUeQ6qJTebU+ahwo+6i4f+XB6MpuG21xCnipwNugQDaiW7HQEvr44udLyXzwnCL78g7tEgFgqF55srJwnf+Z3fufO1957FYsFv/+2/nR/8wR985IUVCoXCY8PehAu44bwXSSqcRsthvcCECUuBm80Zb64O8gTghwahOWD1w9ywxkRqk5hZzyLWRCqwgheLs4oPm0Pq2n9iM1ztnu32QWDrJOA0UjufE5lY4WxEgdpGDidzll3NmHCsW45iRVLh2mROY+N9r8aZhJVE0uyss4lVL67JWIaau6sZRpTaevbrjtZ4zjTQe5Nvjk1oNFsS7Vxd6IKli5bjxZRrkyVJ86QEM8xWsESMgVnrOUwrjlYTotf1ei+NG1qNFIiKGsHErRrP+HORjSHtFxsx8ix27RUKhUfgyu1Gd+7c2Xmcnp7yK7/yK7Rty8/8zM88zjUWCoXCC4qyUkOPIQnsuwW19Ry6JTfrs0seSTAaqUwEIhFDlyq6VJEQQjLULjGWCXYDwiFs1iFwlY09a+7BT0Q12QFIlanxzExH1zekZFgFxztn+5t3bPX+VzZSGU+MBjSPK9vVBig+GRah4thPtvKCy+1qG4FFX+GTZeEbfHK8Ojtjv+ogGTSYnAgNBYCx/z67HNXEKCz6ZnB/lZ30rLKJ2voshq481kSsfUSHP8swDTq3L6kZqhrbycITjNo16SOLsguFwrPFY9UkfOhDH+Iv/+W/zJ/9s3/2cR62UCgUHo3wqae9givTqdCpoUuGk2SZ2p7KZA2BIV74OKpQmUTSLC5Oadwp12Fj2pA0OymNTTLrlh65jxB3PKjAWd9yZzkhRMedsxkOT/SGZV+x8jXHywYf3bCOzXESyo3JnOvTPD1sEdz20QGISTjzE6xAH7Z35i8fIIdkmPc1i75i4R2qUEvioFquk5/sXqQ7jk6qhs/fucHRYsbRfLqeJTGuoDGB682Sa/WCxnZZ5P3IVkCK8WDj8LkKard0IE+YXEkopYRC4UXiyu1GD+L09JTPfe5zj/uwhUKhcHXMNS4RT79niCpEFRKKlci+iYRo6VLur7/onAQAO8wcSAgJg4owtYGonrO+AVViEtLY7z86ocowJ2Fnc3+IqEUQAxglqCF4gQZWvcOhrPqKRVdxvBw0IcI9EW5be7BCsoaVNEgSZqZf221GNcM92HTZXBVjhONuhjVZNFzbiAxtQreHpa2LGOdi/D46jhaTfJ1qMOT5FaPrU27jCnjvSMkR09U1CYoiMd9bsfmBKNhcURiXp/fmbl80ciWhlBIKhReJKycJ3/M937Pz9Shc/uEf/mF+z+/5PY+8sEKhUHhsmGtPewVXwohyaP26tafBcN0GfrWrWKWKpOah3vVZBKw4k1uMohoUwyrVzExHLQ6jsOdWzLuacQ89DW9WlRw4D/33+XRbPqhDb5JNicYGWuepTA7+ax+QHq4fLOj6OrsnCYhm4a0ziWWoc8JjBJ8sDrtT4/bJ5gpAaBArSNrSR1wBVZj3NY3tOa4nVDahCG3l6UKz89pxneN1+ug46Vpenp2BKAlDRQSExvT4YGitJyVIaev+XGW9w9wJFEg5wds+nJphbsETjNvLkNRC4cXiyknCj/zIj+x8bYzhlVde4bu+67v49m//9kdeWKFQKBTAiXJgAmfRYlWYAXVSlsFdsEd8MD4dtp51eKAQVFgF6KPheDUhJTOaF+UKgkjub5Gxx2WISrfj3wQJYdJGjELrAq3riRiatsPME7O249r+nLtH+2xU0ULtEs7mINzYQFBH0Lg+NICRxDK5PJsAsFYJIXHVblkRWAXH8WKCRsP12YK9yRI/c9xaOdLwa1HZajlSUBFCcpytpqy0GhIjQU12Vmqt59XpGSut8ani82cHLPrmXVbyEIyiSTFeMZhcQYiDw1G+Eh5s2vrFQeSxu6YXCoX3MFdOEn72Z3/2ca6jUCgUvnj4X3raK3gEhMpEaoGkllVyHIUJEZPDRH2I/adkX/+YoLGBahgi1hiPFWUZJixDxf+fvb+LkW3L8rvQ3xhzrhURmfvjnFN1jrtxf7hoWja4pYtlbGGBHwDJ8AgI+UOAELwgS/CCH+iGhzZXQo0uFrpCSICR4A37gReEhEDixfhKYLfte8U1ti4Y3E1Vd3XVqVPn7I/MiLXWnGPch7nWihW59z47IvLUzr0z529X7MyMjFhrrpV1do4xx/j/xza38261eHFVmruDJq/P1+FTkJxoMVoSjWYGD0QxRMujCWVoWcoRGe1DoyY2q46mTcSmaCOWLkoOrEIZ+BY1E9UYXMs1vWk9x+DCy2cXrC6dLiQ+enTF+ltf8sWXj7FSGHilAOBWHl0K9BYIEtloP4fp2YXH7Y7HfcfHq2v6FPmtFEkWF+qFI9fsUoTKG0OvBQuOLio80UqKsBCOHMHtSw6126hSeVh845qESqVSee+Qp8CHpZWadomnHe2rvOKLtObz4YIv0obJ/FLk7YmCu+ACQQ0VQymJwpe7NdvU0FsAccQdOwi+ZXQykrkSUQLow5MFSj9+2wysVgOJwKPQ06UGH+AHXz7h2fNHB++TMenph1jmg1lpr1GxecozTAY+govshdS3xB0swIsvLmiT0+ZEH6BpEik1+1N48XVycVSF7z//CBPn0yfP+Xs//hGN2Pw6o7T/bEKPYATJRMnk6dfs4pqOXmcDwyeZJim0ho/tWsvb/8P1R0cd63NOGcD3KtPE50ql8nA4uXb4n//n/zn/yX/yn9B13Vtf+7/+r/8rf+pP/amzFlapVCrfGPGzu17BycjiT2+R69zgLvxwWJcg8YZN6NuPV5IDpcwcwJ3skRiMR01PwOZ5aUt7U3z08ZnbjaZv+WyV2gTDghJax6PSeUtyyL2yEqMfAi4+9/dPH82m5ENJWQhuhPFqysPZWcAQBg+IQtTJIvX8ZMEBDUWM/OJHj/j8y6e8TBdIc3hMF99f8njKr64v+dFXT9jt4ljNKMlaECXgXISe7bDi5bChs3b5AziaubsLwaNgatgKrOHADtWB5+1xwf9z2re/6Gtw828iP6tUKh8QJ1cS/ugf/aP86//6v86v/Mqv8E//0/80f/SP/lF+6Zd+iU8++YTtdstv//Zv81f/6l/lv/lv/hv+j//j/+Df/rf/7Z/EuiuVSuVBIDifD2t+lFu2KI/CDnHDD/rDvz4CFYHsgTQ+BotkHVjHRG+JbWj4ZL3l8+0l5gEZt6pnmepidoC4zLMCkBI8rtoBCcaAsMsRxXgUvDTQR/jWRy/ZfX89twjJuKseQ2khEoc+t5hcY7MXaaGzFhVoNCGjBafq6Lg0s4xev/5eTMUQ3FF1dleR/sUlzXrLzuLeuYl5OsSBgPmi7Ykr56t8wVPfjtUEKboPF65TS6MD1ynss525ArK8tpvrXH7PER0dhUwQ1VJpiWPFwkobmSg87q+/9nonntAf9bo3USoJtZRQqTwkTk4SfvEXf5H/7r/77/j1X/91/vyf//P8O//Ov8P3vve98stDhLZt+cN/+A/zL//L/zL/wr/wL/D48eOfxLorlUrleD7gOQml5SfThESrA09XOz5LL/lh92Tf6f7W2K1UAyb7zuTKla1YaWIVE3EwLpqBj/KOH283ODraa+7tc+ad7XFi8tIi1VUYLPLF7hGfPXpB2yQcRaMj0dAGLi92XL28OGi76YZISvshalMcOl3P8rLMFWQp1fWDD/sXv2m7exGEC6gJomVAWM6B4csNPg0ws+laR/HymCRFMT66vCbETGyM53nDU7ZELedMXnQfq5h5st5xPazG1qC9WPtwPTfXN13L/ryelBwgmhfx8phw+Di+4rPdV2+43kM+5eqo172JUkmopYRK5SFxtibhD/2hP8Qf+kN/CCjTlz///HMuLi74qZ/6KZqm+cYWWKlUKrdGP4P8G3e9irMwFxo1LnTHKrQEHfio2WIu/LB/jLu+JUkoEX1QSoJgyuABDESdKJkohvskHKbMP7gh2gVeO+cAEa76Bm0zmhu+3G74dvuCQZRkAQtFoDxbgi7apMyFPkV2XYN60UvExTSESZWxzQ29R2JwzIyU9c0b8m+6BVNys7gm6QWSwnZxcaM2Yq+fnlyOhHXTM1ikHyJd1/BJc4VOLVQuZBpihG9fXPHlcEHKgc+vH482tG9Z48EL9p97tDKfYB3gOWgexehTwvQuA/daSahUHhTfiHD5k08+4ZNPPvkmDlWpVCrfPHo70ebd4fQuDK5ENRrtUSkVhVYTKxK7t/aaTxLn0i5kXvbIW81cxIGX3ZoghkoZXMbommQLlyGm3XA/+FCEvaF4+F8PDWvrcJOiL1AliZIt0Khj+YYEbioEOIRgXKw6cCXfCJY32qOyCNVl/72jWSYUYxXEVz52TDlioNejvkDAW1797SjOy37NRd6RhkDqI54FjeVuJJdx8BuEkMGnUQcZk3jykqd7QwC7NPyF4K3OpRaBUvV4h9QUoVJ5WFTT40qlcv/xD3DcMiVI7BEGwHBa3fFYO1oxBg8kwlGbuyU2LtvZvUeiGI1kdJQoT+5IMneUTC1Fi16e+fPXC4ctK202yMKTtkMo7TkBxw1yvrFQKUlLjIkQrfTgvyYMVRHakHkcrhGsCHZvYX8qi4TBguPB0aSEJGgSxCjTuReah9lpymG7a+j7wJCn1KsccBKFN2K4USoOOVKk2GewaOdSB0mCbksSM1V83nX3j9d2o0rlQVGThEqlcv/Jv3nXKzgLEQieaKRjpVuCZ1ZiXOjARhP5yF4bo7T2uJeqQnZBSWBOzmXnv0tKl8N+011hcu45CA5vagCy4EkJ7jQY6owtOEIrxmXYsdu1ZA9ll34+hCCiWA6YSWn/wQjL6BwneVnfta1QcVRsWtWZd3XEKBoBBRkcEsg4I0Gm70+XOiYlKgKipdKwEgYJ2BzMO60mGsn0fWStPbsUFpOXT2NymkJAOym/rMc2KLEibH6X3T+yFItUKpUHQU0SKpXK/Sf8nrtewVmYFyefCxnYyEAjRksmjsG0HbmjPvXUG8VmtFEbh6zBy2HFy7TmOq3orTm0/JQpTRif8v0Ry4fRKigL0YzGM4+bLSpOFCNKZh0Sj9pdaRny5XqgbQbW64QGeNZtwCHc0PZ2uaENVhKPse1J5HYJggByreigSF/GlI0mRcgyqJ+27MfzNpp5erlFghOazM4bXuYGs1IFUTGGXJykwFk3A+s43GrHX9zRHUgPDGUtrkub2ndDmdJdKwmVykOiJgmVSuX+Ix/mP3XmjrkhQkkSSPRZeTG0PE8bjrH7BPZi4XGXv8xgdtqQCZpJJmTTWWB7WDiYpjUwug/tW5Nkcf6chcerHU/X2+JspEYUYxiUNChD1ldWayZkLxv6Lsrgkc70YHfeZHJX2icZeosfp3vpPhOT0laE4+p7jYU5MlUVfAzGi0KYIQWev9zw4nrDWhIbGXgcBlSnTf5iS9o0GQnCpsls2oSIvXlBr13k/oNspVQwfKH51sXL3tHEZb3NTa9UKh8k9b/6SqVy/7Hnd72CsyhuoUJAWEvgqcLWWjpvDlqD3n6gqc2nBLEy2o2aCSoQ1ehyGG1PxzfM9YdldDruJi++nB6KkCwQg5UNeKcMM8g6tgjtHYn2Ab/QD5Eh6axzaCWj8+tKcJworS5Rp1aj8xEBAuTHRtpk/AJoKOLl7IiPGgAHxjkF04wEBF5er5Eerl+2PIq7vV2rFAvUzlpWcSBEK5WEZjhjkWO1Q3xMPnwWUo8dSHNy8MOLj4465G0nLpudmOhUKpUPnpokVCqV+499edcrOIuI0IjSoDSimK94rGX6cOfNWzeRX/f9MM5LEDKDKX0ODBZAJ/Hw+EbxWUOwb9S/EaIvjr+KAzFmhrFdp9FSg1C8zEJgn1yIC+6g05wCLcdProRFK5E7JBd6C6WdRyFGW3iynsfskhQdN8cH3zsf5fLwtHjDjaToou1JOY56+OV6S0tSo8ZF6BDPDLk4Rp21SEY9RBZkYJ6zMFufCjxv3s3EZYS5wlOpVB4GNUmoVCr3n/jzd72CsxAZkwQRIsp13uAeWbuQ/Ph/vh3BTeZ2IvXSXtNZg6pjAiJK0LHHZgrobX+EsqBllCiLzzKb1YAIrFe5zEcwEDd2feT59uJ14xUwgRwFi5T5DTkQFq05IsJaE404gZKiuAnZwsH5z0WQ0jZlOldt1MpDDq5xf67Hj7Zo60jrvPD1gdPSSo21JjY68Fh3ZAvshvPnBgkCg6O9IGO7lo/JzDRY7fHwbiYuT5aulUrl4VCThEqlUnnPiQK7vGKb13yVN1z5Gjv1n2+BRhIrHWhjRsVJHnAR2pBwponHi3aiect9cZjptAvhQhMcFWewSGkIElyEVUgMKTLkyDy5eDoujgSnT4q5kEzJUwS8wFBUnFYHzHy0Sb1ltCqUhMcO3XGdfZuRZpDErAUQc1ZN4snlDolO0yaSR7a2H6gg4qy1J5AxC1xoT5/PtEAd1+lakqkZG9uhxnvw2fWzow5124nLlUrl4VGThEqlcv9J373rFdwS4YWtCQqtOIPLGFC/DT/4fOroj2TMS3CeXUahcdEhFJvPG6G67I/mNpr+6GSPCqqGqiHic//+pGVoYuai2TFVJ+Y9+rGdKYty1bfgxkrSgbuRO3QW2ObI1lpEFbBRCHzL3pc8jmdrHQ82N/u7FH2CGKiVCceY464gRj8oZOhSJLvQWWTZrm8G7uVOt5qJcgt3I6c4GimksHcXmu57uZ/vRiswuWFVKpWHQ00SKpXK/Uce3/UKbs3gZaqv4mw0HTnYShYPJcj0j36ZlTB4wF1RhVYTk+fnVECYhcxzcUFAS4vOgQOPOE1MrNue5M3cCpVN6VNgl9pFW/++TNEPZeDYi92G7dASJR8ULtKYwKxDQslkE9wD4ajfXP7ahzt4EhgijFWPvQBgfPgiK7KiCxCg7xrGzAZPpUUpBht3+p3BlM5bXCKrJmEitNEX13RzPbb4/MbqvSRk6TGkjeEbxzbj83fQ9uP+ulF3lUrlPlOThEqlcv8JH931Cm6JYC7zzv/getaubiOJRvPoFJSJaoRQqgCrkIunP4vkoJz6RsuRH8S1IjBYmfyswTCERGAw4bpvuFz1PFpdH9ilulF6ZhB2fdEv9EPkZbc6EMfO7koUUbBPzkuyDPpfH3S/mkMt1MfmWAbLQGIvWp5QYGo3ynudtCUh9YGhD6ROsSGUl3oge0kQOm/ovEGDkxA2TbdY1zGU65BpLkQL6XGxXPL16HI0aqHf5c6+VkFCpfLgiG9/SaVSqVTuksGF3pWvcsvnec1za0cRaQm0jyWMO/U6inSjGo1mgvg4P0EwK3MQpgrBQRVh3N0WpVjdjFvaTTDKXr+STUGcoIZK0ROsm3ywjvL+cojLduCyTYTGCc3hbrVTEoPBFEOL6dLXDlKbWple5+y0/57hFJOkpb7h8F6qC+Y+6xHcBVXhx1ePaNpMbDI/Zc8IjIPe3MbWH8MxXg4tgzfIeA9u7sNPFrDTmqbKUPlcDnIaN0eSlNajm4WHdxS7W7U2qlQeHLWSUKlU7j/D373rFdyKiPMyN1xbaeUZTBf/eL++XeV1qDjmZXd+0ifoqN7NHkh5tMyZDrtsV3rFnmj/oYllFsDL3QrxPAbyZXu+GyJdenU/yhEuVj2Xq56LpifGfSVjyc4ivTUlyB4HuYHPiYDqPil4fXLwuvsAYlI0ByZlo/91bxy1CZKL/6dT7t/10JJMudq1TIPpgjorTbSScRN+tHvENjXscvPan87N9R5+PpVpxlaul4omQXqdxczvGhF5ZwlJpVJ5P6hJQqVSuf+Ez+56Bbcij73zDbkkCa43hkgfJ2IOKhgCbpg7QYwmOhdNYq393Lozt/UwaQiWLT43jirwfPuIL1884mq7hqxsZEABC0A0mjjtsh8sB5Lgg+ADPI4dRsAWDfeNGp8017QyjMmMk61UO24VsQbKVn6WcR7Ca44lpZpQNAnMU5jVoc+CGVz1K3ZDRMd0K4z358vdBdmUbWq46s+xQBVEHRFHshMGhbR3W5p/JN+E09OR+HKIXqVSeRDUJKFSqdx/9PKuV3ArthboRrvSKInBi6r42PDQ3Wkk00jZ6V6FjIjSaqaRgan9vVEbg0+Y/162/DNqBCZB79xE47zYrpHsPHu5IUpmay07WlaXCVaO6D5J8LG1ZxgiqY9crjrWMaE4+UYg2mpmHRIb6ck2y6rPvJMjCgSQwVHft1e9euNKxaFUHRRJgmRoW4M1XDzu2LKis0CpbjhDVhRHzNgOgez76sdpjCnAbnRMQrCWvUZkdGI6qnQyHu+2yNHnqlQq94GaJFQqlfvP2R6Ud4+58yIFkhvQ81F8weOwBRZt62+5PBFBpfTOqzouStnbF4I45o75sm9nPLaPvfRejlH+6KI1RhYJhHGx6pFYtAbZobNIjzIEYbUakOWfUtBg03QEvIioNeMHv5bKhfVZucot2WNx9zlQVp/G5K4k2REdk6CxWvDqjRurCFaqCGKwbgbWIbFaD5gonUU6a4oeGuEy9qzCwCoYH7UdtwnOHchRMVWsBVs7Fse1jgH7DzYfHXWszzluMvMbeVMiValU7i01SahUKvef/OFqEgZg02z5PZsf8HH7nMt4zUfN9SJgk6M2k1daxMPqNgba5VEqA4o72Ogg5Iw2p6/Y/txg8a3HFx0anRCdgYaNJlaaisVpt3mNTUYRPuesyDiLIGKvDInb5UhUZ62ZlIVkgRu9VqcxXqMFh2Zv+cokx7hRqJiThLGqoC54H8i9AOVe5sXcikYzKrDSxDokWs2jM9MZycKYDOTWyvwJo/wfAsoPT+FZuznqUM9pTz//zbV8wMl2pVI5nZokVCqV+0/42btewVmYO2l8NJr4udUXrHXH72pe8DRcc/wutdOG4mzUeSCNI3x1dDoKGEGMbLIPBqceJJanWe7gj41GDkGNx5tdcf1pE701RElEMl9ebdilhl1q9u8dg03VzGbVE6OxHVZkiij7IBYVGac85AO9wjlMswdcBI/jx+YNge/NhGFMFtIuYoPS9vA49jxqeh413egYVda7Dj2r0NNqTyRzbCL32mUEh42hCcIghOWPwOFpvz3qOE/oz1vASLVArVQeHjVJqFQq9x85Rzx6t7gXC//Rqp+MEzTRe2TwyCYMRw5UA3B2OdLlSGcrnqc15lLajygtRUG82J/qjVaiRTLA/kv24gVnFfuyo57BUqlOJBdeds3oJGT79+xFD2Xug+k+Hxndi5YBteKIO9kCNlY8ZgOmEza29x1KUhIFHU/6tt+CUzFlzJusF9Y2sLZEY4lL3Y33sLxsMKWJRRDehky+xa9Zp6xTE9A4pMMkC4HPtl8ddaxPuTp7HVAtUCuVh0hNEiqVyv3Hnt/1Ck5GRmdOKJ06K4Q+b3iRN7zMK67S+vhjUaxEd9bQ5cAut7xIK3IuswCGXHbw7aa3pvhiLkEJhG3ULbvI2A0jbPsVu6HFUyD1DeJGFGe1cgjwZN2zkoF9T894RFe6oeW6axGfVBKHwai7c5UmPYLgrmPrjp+0O3/4WtkPJFMvrTxvuX8yOT6J8MMvn/Kjr57y4tmaoDbfm2RS9AnjNSYPbMLwxuMes2btFfEyJ8K0lDZkKgEhr9yvnxQyl0oqlcpDoQ5Tq1Qq9x/76q5XcBZrkcmIBxy+GJ4wWMPLvOKltQvXnNdHb+7FZ38antaZEkRptfTIZ4TdUKYFC6BqZcLwlAiMQ73mwV/LOHGOTcsk6GkScR6EnJStRgQvIl9N2BAWK9tvz4s7Tcw8Wu/QMU1Yvu5x07PWYoHqDtnO7O+f7pMXEbK7YuLoMZmGcKNsIXzx4pJnV5eIO6GMLyCI05uVxCYHVJ1VHJDObgiyj0MAckZSKD/LSFmzL5KWd4RP2WGlUnkw1EpCpVK5/8Sfu+sVnE0rQiuC0fIyXRK85SqvyP6qC9DrmYaAOY0Ym5DZhIHL2LPWxIu0ZmctL1NL8sA+6VgKEsaH39i3nrpeRjvOZpWJK6NtB1ot4t2nYctu1zIMTVmJ++wwBE4Miai2HyT2misI4qx1IPj5u/IzxiwktjCauB7xm1Cm9isbH+5cbS8Y+sO9tkYy16mlszVXacVX/UVxlDojwHYH34A3TuzL9GfC3e3oVwvUSuVhUZOESqVSec9pEH7Ubegs8MxKsL2SNAeeb9pZF2Fsz1EEoVUjitGo0WrmamgYcuDlELlKLaKggVeCUFkMS7gZKLo7gvFovUOCEWImEwiSaCUxXDdES3vJ8/x2IYZyvKnd/dV2Iy9VCheucyTRjM/6eO5T7uIkZFgE+2M+ZJFF4vLmt4sLaoK40Pct292KH371dNlAxW5s5+qT8uPdRXGKEhZtW29ndp8CXEvVxTcl0RCjtEnx7jf2ayGhUnlY1CShUqncf9Jv3PUKboUDQ3AIiU7gcTPwuO2IYrh/vXOOSNERNJpoNBElE0qHO4NHVITeSvBdZnONycD0TBEgLM5xOJEZnDYOxJBxnCYOZFeyC9s+MohwlVtCkxe1iTKxeLPqWa0TEsuUYnvFBUjocqBR4yIklCkhOm5H+2YSIUqZZDz112f2CcMxjImFZBi6hmEIPL+6ODiPiBClvNAo93NKEPZJ3esPP1276ig5ALCx+tIDOK7TnV/+/Y6oFqiVyoOiJgmVSuX+I9++6xXcimsTBhcGMdZh4CL2XGjPJvQcFyYWF6HBAr0XAbNgrDSxComP2uuxBYeSEDBZ9O9ddIqN5ySWXTQhidCllh+/eMT1bo1npZEELuxosdbppKHZDMRVmtt2RCC5khdC6F1u6PNNC1RQKcPWbNz5L+flIPg+Kn51cPESviuIO5oETdNVveUuKvMWviXl+z/6mC9fPuLFrp11GytNtJp4FLoyd4J9OP+2ZG75kVGbLFcBsYBLyRw8Mv1w8HfYeSTVArVSeXDUJKFSqdx/wqO7XsHZOLB1iJr5Xc1LhEyXla0VbcIxfeIiTvLAYJHBwthWH1iFTKuJVo0gvk8UYF9JmBYx9r/IMthdbOrvhoZomZcvNqwksbOWXW74or9Am4wG0DBu289LVrqhYXABKRWKRuwgmA7i88sbyW+4vrL7Pg+Mltc9Vy5MVJAAiJV7l0/cHR9Fw+JCt1vzWz/6Fr9x/Snf3X7El/2aIKVKY640YkUgbYfVj5s/sjf+CPNeB2ANmC46pWT83tE6gdsF+V4tUCuVB0dNEiqVSuV9xp0oiQvpeCw7PtEdnTXscnNDvPzGA5TAXp3OlTQGrde5pZVUNApiXMQOH52KfBYoH/TRvDJ016fGeWDd9KwaQwJc9y3JyhTiRhwVx00wKxam5XCOqiHq9ET6XBKYqIe9P4JjBtscD5MTDhOCo2/ndFUyOgTFV7//dZ/PS/By/c98xe/sHvOj4TFfpke8yBsexx1rTbQ6lAF1C13CzUTmTWt3p/yGjiVJihnUKEPVFuv4wcVHR13351wc9bo3ISonzOWoVCr3gZokVCqV+0/67l2v4GwmkfKFdLQkHknmqXT0phzbATJJCNyFlfbEYDSaGFBEynyCtSZ8Gno2W6syjzaYRcey1/4yth+pOE8ugDHevwAApotJREFUdoQm0zSZrIFVzGziwEYzn65eom7YcOjINCB0RHqUIQf6HNEbAoFGDUfKZGi9GaSesjsu831AvAiA9xdSPl1e3/j5NIR6cUf2rk4I6soXLx/T5WLx6g6tZtZh4GmzO9vVCMaKQTRkKD+j+eco+0U9a44L/p/Tnr6Q5ZrsVdF6pVK539QkoVKp3H/k8q5XcBZl6nKZiLwicTWseJFanuU1Ozt2irQAWiYs48TRTnQtmT43DB4xFXoCIUwCgXGXe+mhM1YN3GG2I/LiSPTk4orNagBxQpMZJNBZg4qxlsRGEk/CtnQayX6XfMiB6xRIriRTzEDlMElwKPMGdCBqKs/cIvAWdVQdtbL2o0XL4630aR7ceB9SEp69uOR3vvyYHzx/zG6IbHPkKrf8aHhEjHnhPnVckD3NvxAr7lT5AiwuBAi+92J62l8fdcwn9Mde5RuplYRK5WFRk4RKpXL/CZ/c9QrOpLSaNAJf5I/50j4mecvztCLIIoA/8kiPYkfUxEYHdh7Zjm1LhpA80Gje75r7OENgjIhlDIxV2LsDKWQXskWGJIRxOJriGEIjmZgzaQi83F6MrUYl05DgiILlwHbX0HhiEwbijTjaxgTCEZ402xsK5ePnD8ztPdPXNr53FDOPh3v1fdNfcuPJ8Rvb65bttmV73fLyes1VXvFb3SdsvcUQojoixrEJwn69o0A8C65CxrHGyW1xOCq/vZ1Pr7866nifcnXS+V+7Jq0hQ6XykKj/xVcqlcp7iojQSCBbw3Vec51X7LwIfU9v/HAMJVnDNkcCpYUFcVJWsik+zjyeXn8gQhirC3OlgeIOJEDXK404wR3JwkZ6LkNPDI7GMuugHw6b/y0LnhVLgauXFwxDpJV00KPv7mxzIAQDFZoIl+1yR/zr7V/fyCB4KOf3aTjZOZvk4rgK3XVkmxpyCGxpeZnXbHMzi7/DGNCfTADGaof0ipgwFVpkdKJ6p7/EayWhUnlQfHBJwtXVFf/1f/1f873vfe+ul1KpVD4U8g/uegW3ohHBJRHjjlW8olmIe0+J2wYLDB7ovGEVBh7FngsdMBeyBWy/Rb3fQZ9aZWQ82cGuegnSU45YkjHwd9TKgLXswmCxCKwXbUSi4CakXsmDotnxPkCyxfU4gwcSDW00LtsOCcbjtisWq+e49Th4p/gQ8KRY66COh7GacMohxxxKktCo44PS5cCztOE6N2ziwNN2ux+M9oaBd1+HKLByQg8hgQ6lsjBrRN5h0F4tUCuVh8cHlyT85m/+Jv/sP/vP8g/9Q/8Qf/Ev/sW7Xk6lUvkgOKXx/P1DBB7Hjifxik3Y8TPrLwmSS8z+1titbP0Hgd4CvZWZCdepZaUJwVmJgTlDkoVAeZEcjOoEExm9kqagtzyyCF8OF/y4v+DL/nJur3Eg5YAoXG6GAwGwAJhgvXBJz6Xu+PTi+uB6khe9QkaJwQiSCQrrZtQmnMDkxFQGqjH3HhnMbUcnE4qwOYpjOfL8xSO224anzY7L2PO06djoMFqgnkO522EYnZEG5urEJGB+V2lCtUCtVB4eH1ySABBC4H/4H/4Hfu3Xfo1/6V/6l3j58uWtjtd1Hc+fPz94VCqVe0T46btewa0YXBhcubaW67zih91jTovZHPOyu1/C+iKGzuYMWXGcqEUiPbUTue0FwnOAXb4oQbUU5yXEkehcecPOAzEOrGNJPgShaRNBbVGA2CcfIrCOAx+tdwRx1s2+lci8WKgagmJEEn1qGLKSXU/emRcRJJTKAeJIdoIJ6lJEzCf2LU06BgF2Vy1XVyuurtd8+dVj0i6yCQOtDDQyUDQdx+snFifBs9JfFuGyrRdpljL2HL0bqgVqpfLw+CCTBHfnl37pl/hrf+2v8emnn/IH/+Af5Nd//dfPPt6v/dqv8fTp0/nxsz/7s9/gaiuVSuV8zOF5DvzW9lt8vvuEq+ERX6VLnLB3Cvoaph1/EWEdMivNBHFazWSJmASubIUEZd3YuMvOPBcBpqFdzG0uU7IAY6IwJiExJoL6GOAryQOoEJu8dy3yqZ++XNumKROKL9oOlcUcA5xGUnFi0gFxIYqVqdGj3eipFFclh+SoyastVSdRfFIFUBf6bcPVVcvLbcuPrzb89ssnPOs3fL57XKYlw36C9ZFYBjfFG0iNlYFqkwPVstrzDqgWqJXKw+ODTBImmqbhz/25P8d//B//x/yJP/En+Pf+vX/vrOP8yq/8Cs+ePZsf3/3uh+upXqlUXsMHrElQMcwbLkNmI04gjs1TxwWcImVXPkqi1UwbMk0wVDIv0poBMJQYMlGNqPlgSNp8HCiuRHNiMgaMLpCVVo3LVeLJZkfnkUAGjD4p277hxy8uOagiAIqziR2btufx5Y5ra8swNwREaYOx1oGVJAZTLrQnm5IsnBwf+yj0dQTWjqnhWlKSkzfk5zkJ+88dGDzwo+sn/M72Md99/jG/+fyj0iY0pT0nnGeWf+hY9XCBBJKXkpBz+6TOo9YRKpWHRXz7S94P/vF//B/H3bm6uiLnzD/2j/1jB99/+vQp/9a/9W/xy7/8yycfe7VasVqtvqmlViqV9w3/kDUJwmUYuBj1A9c50orRedlNPzZYzqbYLKI1DGGliRwVkUyyFvNpBIIUcXGe3j3XD15lnBfw7fUVrQ40MZMJtJLIu4AmZ9s1ZBvtT5c2pAg9EWkcaZyttTjChfbji5QgRjJhSIEhKykDGO5TorBc22EY6y5jUuMlQfB9Lz9rh97PkT8vjs/e7Ulk9IeF58OGnKc9OF/8jI4/m4zOSajTXAvBIfQLV6up50uO3eu7fRVAayWhUnlQfDBJwj/4D/6DAHzxxRf89b/+1/kDf+APvPKam4lDpVKpABB/GvL/765XcSbCShJmRmeBlRqPNXGdG+RIC1AVyCi9R3rLJI9Edqw0kUzZyMCX+RHJIuZhDCdLe0mJRSc7naW10T44j2IMqWHXrRAMA8yUnCM+KJZu2ozuF63q7CySVeg9IuZEGWiYZhsI13nFy7zmB/0TehpCgJyLOHrfwuOv6BTK92SspkxNTDLNhSsTq9VHhcZUGWGZxbx++1wmO1hfiJ/LjAgchm1E1j63bs2D3E6KsWXUThh4wG1yNJpavMryfnDx9Kijfc5xk5m/jqpJqFQeFh9MkvAf/Af/AQB/+2//bf7CX/gL89eVSqVynxncGTC+s/mcH6bAs6z8X7IBNkcGnaXdRdVLkuFF+HudGy7DwIu8YiVOJPMytcu37T+Ou9bTxr0sg2cpLkRdaoqffxYetx2dRXSVkNa4vOiIzxIpNXMw7g6qRtskvHGuc8Pj2LPWgXaxOZ4Nng0ruhy4Squ5ehACmI0LgNe28uzdnwRVm3UQ7mWdUnqeDhtvb97Tr7vHk0ZjTGY86TywzTvDV1YSOS3C5enndbjWm5lIEQiLOGKKeyBdgL+EsAbrKUJrB8/O8/a4aeLPad/+oq+71HfopFSpVN4PPjhNwt//9//9dF1318uoVCofEvmHd72Cs3AfkwR3Es7vWn3JZbzicbw+qfVjjvddUDfAUXEGK9OMu1x25G05E2E6vo7tOdPzy1Bx2p13sAStJ3ZdILvQW0MIDq1DAx89veZmmBmagRdpxbUHXg5rdinQaj54zS5HjEgIjkqeKwfT/IGJN9+OUXewHLc82YeKnN+J9pouJ8mCD4IngaR4X1yYXq1wLB/+ytfzYOM8PkzKlGWYE5Op2+hJf9wk5Sf0b3/R1+B+u9asSqXy4fHBJQlwulVdpVJ56KS7XsBZiJR/pBMw4FyGHY3s+Kx9wUZ2xx4FUMwEwYnitGpsQmKwSKI4EGUCQW0xM2D2Px33usf5CFrsP4sOuPxb3KyMPka+9AtyCIgbgUzKiuVJDyFFdTutSo3QjC31KjRiRbS8PDdFVL2JA5dx4NP1NZjPgffh7wK5EWyD6v7z/asWwa4t2oxOxOfMYHG7hLL7PyieBM8lCXE7TbQM4859dGjKQ3sgL67FSm3ns+1XRx3vU45LJt6EVgvUSuXB8UEmCZVKpXIS4e+56xWcTStS+kIdXmTlpa0ZPPI0bDnFb8YRLmNHqxkl4y70Fukt4giDybiDPe3ULxvzYW7kNw520TVkYpMJTWJzseN5WnOV1gjQdS1D17LbNfz42WPc9x2uMs4r6F3ZbVfkpOMk6f3BzYWgzjr2tGHgInY8arrX7s4fw76YUGYmuPrpk5bnuyLFAnW0QZ2MhlxGkbQJpLGycIYJUakUOB5LlqFS7r8tWpyY5lq8A6xaoFYqD46aJFQqlcp7jIrQiuBE/s72p/giXfLjdMGVr4/enS76WiPKXoOcTOgtlJYjX85EGD/eOIZMmmVZxKUCNlYLohhtyER1rm2FpdI/nzvhx18+LoG97HfgLSu7XcO2j3gWXlytUTPCIidJLmSUlkQUI4pxEQfEz68M7cW/U7nh9F3+6drFgezFltQYd/fHb7uAKaSADwEf79NRa5xbqcr6BEcG3WsoKE5K77QBSN5ZPlKpVN4TapJQqVTuP/l37noFt6JB+c3dt9naI54NF/yof8SLvPqaPvxDRJwwOhwlDyRvyATWMbGJA6uQiWKjRaiMAaHP9p7TjrWMrUeKID6KZ8dd88swoA7rmGhD5tGm46OLLesmsW5LP/yN5iBwZ6OJqIlHmy0hZnofm5vEUSCS6a1hlyPb3PByWJM8njyYbH8vpuuZHkA4c0dexmRgyp6UfRZWxkSUrf+x9ej4jfi9yFmkJCHeOj4lJF4cmd6pSOAdn65Sqdw9NUmoVCr3nw+8lzq50OCYlQD/+UGC4G+8vOl5FSOo01ugNBspV3lFGHf/FeciDrjbvr9+ipunnf1FhHize0bNaSyhg7MOPauQGCygaqg6j9r+lQDTDS6axJO2Z9MOfHJ5TSYyeDNPiZ6mNHc5MqRINuVqCLcPVgXIgrjiATxwepIwSTakiHqXA+jGBqSiT8jlWsm6mDtxzAL3VR03wSN43F+52LsP2mu7UaXysKhJQqVSuf/En77rFZyNuaM68AsXn7OJL/m0ueKn2yvMFgHj18Zuho6y46nNqMuBzgLXFgmSEXei2Dx3YdqlX+QhpR+ehRZgnKEgImyHFS+6NZYD3a5BKDMMQsw0IXHZ7l5J1ESh6xtSDpAh+TTsbZ+CqDhDDvRZ6bMy5EB2fYOV6DF4GRaXpATtk8bCOCvinpKo2TlpOt70uYOYoKOQ4FQnpYP5CmMS4ku3qXeJ7GdNVCqVh0FNEiqVSuU9xR0GEsmNKAM/137FR3HL07hjE4axY+bNAWOJJRVHy4Rlyaw0swkDFyGxCZlsgiEEcaLmuRNHFoLepTPq0vVo6rIRgZQClpVhiEh2sgtZA9YIO214dHk9B70+/klZ6fqAZmHlmUsduNSBfcQuRM2sYmbdZNZNOmxZOjlW3nsZ+aQCNjm0Rz3tcOOt8Pk+HGAlSSBLqSKkw1+5b4q55yRtcmoCpC9TsAmAetFSnLHks6ntRpXKg6MmCZVK5f6TfvuuV3AW0yTgogEQVJ21DjzSjsfajzagbzmGF5egSKbRTJRMI8Y69Kgb13nF4A0vc0suHp7jVGLZ72TDHK3uA1PZ75YDg5cd/+ywigPmyuDK1lpCcNrGx4rGYsHq9EMgmNFtI2vtRzXwtO6SoKxC0TkEcZ6uxjk5Z7gbQalgCGUgGdPO/uRK9LZDvun7Y+XgYG7DnEEAuSQjxYp2KUx++4lKq9KYbPjeRXYvHj/2PtwuxFetKUKl8tCoSUKlUrn/+PVdr+AsVEqCoCI8yxu+zE94bmu+tAue2Wo/dOtrmHS0UYyAEWRKFDIvhhVDVl4OLV1uUBXCFAwexIRTFDwJjm+exWnbRNgYF496ksfidiRGcsfEGbLMB5VRHC0BOhp+6/ojvthdEmTf9+NA70r2QJRcdBWS2cS+tDOdGbOKAOYl8RIpwffyguVrHq+5JeXTZeqzr7TMb3VBkkCiDFo7wZUKEezSoTXIezH5mErxg4uPjjrW51wcd9I3YFZbjSqVh0ZNEiqVyv2n+fvuegVnoyjmgf9r+JhnecNA4MdpTZiM+Y/AHbY50o1zEXpT3JxGjUadYQyUZXQVekWTwN4V6JWzurFZd2zWA+vNgItwlVuiZNycIUW+vL7kRbe5sflfdu5doDclIexy3A9CE2jFCON6bBwxbKY44cy7ub8WcRDzce6DHMyP+/o3v/oowuXxHsmNpGPCBDPBk2JpbD9600kXT7mDtwYRaPeLkFEQ8bw9Lvh/vn/zWYgKNU+oVB4WNUmoVCqV95yvUsu1NWyt4UVeMbgS1Med97dHbiKQPDJ4YPCAIEQ1GiktSCvNi/aX6RO5kQzIvGOu7O1QQdhuW3ISguX5XdngatcSzHjZt7hSeunLW0ZbIAgYTx9f4xrYpuaGTSrgjgs0AVZh+Eb68KUBxItDkE9XcSLLhSznLfiUSi1fWpIIHyKWFBsCnsM4ifk1Z15oEqAIxk3H6sk4ddnHZOppf9wk5Sf0p17h4TWYUzuOKpWHRU0SKpXK/edUW5n3BHfHMD5udvwD6x/wWXzOhXSIw3UqTj9vOQLMI7ec3mQ8Zokwm1CSg1YT2XR+lE3+0avfDw9X9Anj14s2nOBG3gXWOvC46QgRPrt8gbuyJpW5AZmD+QsqxuNHO5rWCI2x88Pd7rHbBnHYpUCf47iGW/48dby2YaooMOoSTjjG4rVzUvBKhlNukLgWMYSDZ1mc6OtP6F7ak9yVtCkakfknPrpLfXb91VHL/ZTjkomvX08tJVQqD4maJFQqlftP/o27XsHJlGB+fLjxWHd8El7wSAa+Ha9JFnh9s/xrjrX4REXILvRZEDdyhkYMd8g2WpzKYprvNLRs3NFfOh5NMaMpdNbQ5YZN6DGU7MrLtOajZsvPPPoSGfb2Py6ldagJuQiqQ2bd9hjhoKXlYN2UdEeUsYJyLmOSkh0JMJ/wNrvkx77XgUFh4MbMhJvB93if8lhpyMV+1OO4XFsmazVwr1QqPxlqklCpVO4/4WfuegUnczi4qliGbl0RdRLHBsr7qN6BRosAuNUMGojRebLqWIXESvp968vBjrGMcXX5nvmNZhpxNDjXObKzyIvdatzpd0SdpjXadSaE9Go8a8I6ZlYYgwWSC73tJ5sVl6UyF2AVE20YxntzxKVPF/26e5LGsoHNXU9nxtpe2pbGw86CjRsliWK3WvItHVucJCs+JgtucjC4DimFh9CCBhuNpaS0J8W7SwvqMLVK5WFRk4RKpXL/kduJNu+KIEpAiRLY+oodDQNCFuFinJNwDI7Qkmi0DE1TdQYLDBaKtagamzbvbU+XO9Tyao/9Da9PEGfwIj5+2a8J2BzcZxV6ApcXPZOqQUbrVpWiq4gY6kUncVCuoPTjTzTqtDoUa9Q3XexSVKw3vp7Xr2NioCXwHV93egg8Zhg32q9uxtIi4wxmEyQLZMEyuCjuWpIzXxxjgQYndhANfEWZkxBhnlPxjjIGUTn6/2+VSuV+UJOESqVSeY8JouDKD/MliDLgtKON6fFemoCWoWpBjOxK8kDygGiZxvyo6VDJY34wWXqONp43e+3nCcM+f22mPN+uedGvMYPOWrocGUTIJsSQmQcTjMfruobdtiV1gVYGWs0MC51FFKeRjGB0KfBst+Z7Lz8mE2fx7tfalN5Epj5/xUVw9cNN/xOD4CmXmoymDvQab8ApugQ3KR9zqRDMbV03Xj27JoXSpjUVKuZXv6PdfTd/V6eqVCrvCTVJqFQq95/0W3e9glsxODT0uPdc6kAjmV2OHLv3LQLJlGTKkAOtZgIl+O4tkCiPNqTxHTfUyjBHwnMsO6mKjTIoLCueFesVt9EuU8BcWTcZFWf/K6ccy4FsiuJ8srqi0TwH8hNRjZQjL/sVP9xdYqM6YWxGOvleMi0jOr6+4Q51YhA8dRcZ4NME5Fc8YpcHLw9ntEIddGwtKhmP3+h7cpfSFdWAqRdzKIVym5YC6J88pZJQSwmVykOiJgmVSuX+I+u7XsHZuIMxcKHXNDKwInEpA/D2actLshfBMkAg06oRxcpANQ90OYyCZV7ZnfY58JXXzEmQcaIwXEjPZdMRQ6kABMnFZtScnJa/bspWeDaIOrCOA3gZ+BYPtBaOmfBld4G70A3NwcnPiVnNwYOXoF7BV1OicE4A7MiNyzoGMUFN0F4hAVN7ljPrQtwZLVLBYhmkJjKWLb4+G/mJUCoJtZRQqTwkapJQqVTuP+Fbd72CsymSZWUjDll4Oaz4ctiwteak44iUoNwpcWaUzFVqGSzSWaCzSAhFJ7DoZXntcebiwqLFp5HEOhpDbsbddSUKNDpwvWvZdauDq5p69y0reYA8aJkGrTaf3l1IrlzEgcum55PNy727ksvpQetk1BR8TBSKbZIHn02PXnksnrfXfX95m46I2eeJ1WNiRSqVl9JDtD+QSElA1IXQK8TRecoEC4vzHn0Pbh/g10pCpfKwqElCpVKpvMc40IiyksBjDby0Fc/yevHdYwO3EpUbSmdKZ1LmLLijGI2meXLwMYdidB6ahAGNOipO1LSXLYjzeN2xbsZjT+tdnOPxuuNilTAP8/mn4Dy7ENRZhYEgxuNmYB0GzlQZj6eVsae/uC+xGAp3NDdfeqwmYvl2L8PcSIr1sq/W+FQhKdmYDiAJNAum4+2OlOQG+MHFR0ed83OOm8z8teuulYRK5UFRk4RKpXL/Sd+76xWcTRChESEiPEtPWRMYLC6m3x7vBxol00riMiailAnGbcisgrEONmsCyoQxwH1vizqpdP1G05GDZYiaWYWejzfXiDpBig5BXMp5m7TXEMj8Voas2LxFLzi62LUXJu2BjTWVy9DhB55HR1792L4z7dbLlGhESkXhdSLoGw5Jy8pJ+VpmLfbidhzxIxFkFKRjAoPOAuYy92+654LJeAccGMah1QvN+rP2uOD/OR+mw1elUrk7apJQqVTuP3J51yu4FUGEZ/kRL23Fj+2CnTec0o/uXnQIq5hxcTJC7wEowuBGc5mhgHGwJa5wGP3ud+OLA1IJtFWhT5FNM7COPeKCAuqZoVfMhF3XcjN6FoTdsOLFbkOfAobi7gQgiKNiJItc5RW9texyw4u8Qs4Q7Mocd/vhxr/DGImfxZRT3ch/jsKdMoE5B3zQ4rqUdL8WB4tgjZNDaT8SG4XLUtqtnvbHTVJ+Qn/Cyl5F9CyZeKVS+YCpSUKlUrn/hI/vegW3wl34Mm1QEVZiZZjaiSFbE5yA02gmE0AUFaHVhGVIWehSXFQKeI17jrw+CHZIObAb4ii0Lq1CRiBbpAmGmRy8vhyttBdpcFYXPT1xYYFaznWVWnapYZcigwX6LIdR+dH4XAkogb0jCUiU2QXn/DZcBPM3Zt8dhQilKpDHasJUUUilcmAZcMGigzkSHBPDbWrccj67/uqoc33KccnEm3DzkxKgSqXy4VOThEqlUnnP2Vpg64FdDmTGlpNZwPv294sU3YHgJG+wsQUoirEJqdiMWmDI8cZxDw++7JtndtopbUBRjSZkYjQMKU48TJ31N461iDYlZEKb0QjJAkH2AbeK00giaqbVjOCohDelKl+LLwJ6FAhlcJv4+VWEssjFcU9mzFpM8F7wVFqLzAXPpUKjWobfhZ0QnyuaFRqZb+xrxyv8BBAVTrLTqlQqHzw1SahUKvef9N27XsGtSA7XJnTiiCYuYs8UaR8XtznJSotRcuU6t/jocFS0Ck6jiew6C4cLhzarBwPM9i8Bdy7aLYgQtMxGMBcEIw3C9a7ldb9uHMEk0KN0FsCdVtPBwddxYBMT6ziwDgnFRmHzaQHrzVd7AMKBR9HpyOHHKWA/emnTaU0QD3jWUZPAPIlZFCQ5YRoPMes5/JswLDoaN+eojLRSqdwbapJQqVTuP/LorldwKy6CcRk72jAQdOCTdstGdiccQUke6XJg8ECfA1epBYyUlZxL9K/k8fV7ZfE8VHl8TBvKyx1sEaGNjvkkOlYEZaXO5aojjC/2m/G4O22TgcCLfjOudB+4m1PsX2NP0MwmDjxtr0+9fTcvqVh5+iRmFsTP+1W4r5XcaKU6xXDKBTGZ255kVyxRZ6cnB01KjsKw2R9fxlakd0qtJFQqD4qaJFQqlfvPB65J6FxQgSdhy2XsCGQSgWO2kpdtQ8kVsyIIvgwdyRqu05qdN1zlFaZhPwdhft8Nd6PFh4OagyyGfQEqhogRxFnHgVXsXo0xBYZB54DYGG1Zx8BbBYIYSuZ5f8HzYc2X3aP5fOcwryFRdunDWYe5wSIJmrQPx7+trAtBk6CDIs91vAMlmckqWARvIDdedvXnA7zD3f1aSahUHhQ1SahUKpX3GHdnlx285zJc80SuxyD0uEh0em1GWKvRinERehrJXKfIdW4YXNlZKIPMprxAhYXP6mu8Pfc76DLalK6aAVcZPZIccSdnIWchLYa/zbUCEZIFuj6w0gHF6e3w11LA+HJ3SZ+VH20fsZTPnjzca0x0RECWYoIzYt+DMH3qxBHO32130F6Qvkxi1qvicuR51DYDLo5LGbC2vPuVSqXyk6AmCZVK5f6Tf3jXKzibTBkm9tPNMz7SK1ae+Za+nJ2BjiVgtDrQSKZloLdInyODKdmFbCXslEXQPPXH7xm/Ya8+G0JGdfL0VxLKkAVzZbMeUBnfJIuihDtBM5ex56Omow1W5iMsKhXJhd4jUQybduqPv+zDdd7cgO8FGfSsOHtuwbKxeLKsSJzQbnTQreSCdop2Ap0iV0WngEMwQYs37OmL/QYQEaxWEiqVB0VNEiqVyv3H89tf855ShoqVacZPdUdnDdkDj7Q7+hjuZe6AiOCidDS0ofT4P4o9K01kC5gvHGyWAeHrxMqLr1W8zErIgexC8lIhMMI43VgI4XU/gzKMTDKkXmgYWC3aiNxL+9FFHLhser69ecHCoPXECcCyH4YmUoS/8xC3Ew6zZGrJmjqkxknIpyYdB2tJwBDwlw22C3A9Zh9u4+TlsbVpMmU6+h7cLrlwd7RqEiqVB0VNEiqVyv0n/vRdr+As3Muk4WnEWSIyeKCzSGfx6OOIFF/9waTMLzCly5F16FmFAXGh1bzPC4RiD7o/QlnP/Fj8cWhjQtVIImyHMjTNgISQXLnqJnejudN+TjSCOKuY8KwEMio2x+wZIWqppETNPGoS69CN9+b8th5waHyetuzqy8s8nptyjbNNkkrEL+MP2sf777uAiyIihEHQHrRj39Ykwg8uPjrqHJ9z3GTmN6FaE4RK5aFRk4RKpVJ5TxERGoQVQouQ84qnavjYgnNMUOteHIOiOo6Sx8fgoQw9M0HFWGuP28KBSPYWm0VYvBTnjkmDC6LOEITv757weX/J82HFWntaEl2O9CiGEuSV3iVwWMehVDkYnX4oSUj2aSAbqCQcR8g8ih1fLw5e2praja8XwmsZE4SV42vDTxFCy43H4phnM3V5TccwysA3NSQYBBuTEZkTHQGetccF/89pb7E4MDszA6pUKh8sNUmoVCr3H3t+1ys4m6mLpRHlRb6g8xXP0gU7j0fqEkbXIXwO9o0yaXmtmZ1HrnLLzlc00V4b58orc7Qm809Ho5deeS3TkQ1F3Njlhl1uyVHpopKj8bqt9ue7NdddS7bSDqTihLHCEMUwU66GC4bc0ueGr4YN8ySx8dpkMYF5FhHPd2+/3ulaQHArW/ZigiTdv+RmAvC6x81bYWMV4BZtSwIQSlojWdAsRaA8HluzlKnLs66iLORpf9wk5Sf0Zy5u5HXXXqlU7jXH16srlUrlQyV/ddcrOAv30iYkIuSs/J/dJ/zG7infG57srUrlbVvYQrIy3Ezj1M9SBp5lB3PhOrfjZF/F5y1xeaX9aA5ml302UhKVnJSPV9dFfOwC7iRTtinSq9Lrq16jIuCudLmhzwHDSSjRrSQMwDY1bFNkmyK7UWgtr7lmWTgXve4elNc4ZuWjdgqDlEeS6bac3nEkgo/Bu1upTpymlVgsUYotK+bFzghHOghXERlkdI5ydE6ShM+uvzrq8J9yXDLxRvx1QvZKpXKfqZWESqVy/2l+7q5XcBYigoqiInxlG5JFfnt4hCBzmHjccUprkOJESax04CLsSDSsohMl40YZqjbZDy3j3LkFSRbPlxdZF7AuoAku40CjCRfhUdNxEXqCG8+36zH6nd4p83HNYNP2PL3c4qLkhQWqCFw2PZuYuGgGmpARWViXnoEAsisDyjQDA6dnBjePOb//zATB911Ey24vvQ5IF5CdFrejtP/5TDqIM8dFnEXVLVcqD4uaJFQqlcp7zNSr/93uMVngSdxi7MW9bxfwjq1BUtqBGnE2IRMVGs20mnnS7kim9Cm8Pvj0RVS6OObUR5+Tchl36OCsQiKjtJpoJbHtG3b9iiGF/YDghTQgBGdAIcDggXQjYg/iXMaOdSj2rTruoJ9nhFqqIy6BLGCq+ApQPz/gnrQEDthpa5rfB6UiI8z67vI9Qa4UtopsywyF+Rb66Ir0DjGrpYRK5SFRk4RKpXL/yT++6xXciuRCCAOXcctPr7/ip9rnnGbGD1Ov0OAByCSXUdSciTiN5Bvag0XrzrK7COZKxmSy4wiiwioYw9AgwGDKLkXUwLOR02HtoyQ/zuVmR9tknndrsgthcV53SFbE0S6wbhKPmh1lNvN5iIJrSbI8OB7AouPBz6ooLAP7Ux2ODlyRpiRlMloav1bXopvw8a7vO43e+c6+ag0ZKpWHRP0vvlKp3H98e9cruBUd8DS+5DJesw5bfmr1gk+bFwh2dKDoLgTKQLJpmLKhxOCYQBtt7PVnnicwOx0t1dHz9veBYIF1SOX97gRxdtayaTMShW9tdgRs/95pzVrWZQg9Db1HbNrtZ/QmGlucDKW3SBNuaSM0tk159DLBmMXJzjjU/Ml8WSfu7sukQZa9CHpZsLlhyrQ/vBwhWv/mUNXqcFSpPDBqklCpVO4/8Xff9QrOxh2uxujwSXzJWvpijQmYvyoGfhMiZSBbJnCdIzYOKtsNZYjaRdNx2XSvhrhjucC9tPjcNBUd6wi83K142bUMSUkuDDQMLgQxNmEgmrNolgEERehTQ5ciqGHI3rHJKe02Y/C8yw37bfTzmWxGpQGPjlpxONIT5y5Mx/HFo3zjxOMcSEDGa1smLLk8xCiC5lAE7e80Q6C0GtVZCZXKw6ImCZVKpfIeYzgrBqL0DKasRFhpZvBwQsORjwPVtMxYyBEZA80uR7IHsgei+nhGX0TT3NAjwLyTPzkCKQwWGHLATGjEaDUR1chZsAwpvc7daKxeaOmu73NDJJekhDJ0zcalrMNAo0Opntw2UdByTpkGqZ2xQ75sDZoF3We0HM1lEyjlnbGPaEqo5paupWZ8tJx99efyk0NEaiWhUnlg1CShUqncf/IP7noFZ+M4jTgf6UBL8a1uxWnl2L78fWDn4pg7USiTjb0EpX0O7FIsO/rouJ9f/j5spVk4E81B7NgO5KDu9ENLHpSGxHa3IufAj68eYarYmAxMm+COYwFYGSZKNgg3Lio7OEoQaNVpQjEBvRVTm5Etgv2zNslLsjENnnPx0kJ1yrEWyYWMA9TmFqSp/cgA30u1fcrOpgzt6BOdj7vXSkKl8sCoSUKlUnkAfJiuLO4weGYKiz8NW7oUeT6s+XK4OEm4Wja7hZ019FZajPocSTkg7myHyDY3o13qGFIuj7/YLF9unAO4wWYz0GwyoR3YWSAlpesikmCX4hxAl7WUd6pCRtkOLe7GZegOrmkSSU/RcjIZ9RTn/TzdGb3+pQwskzLATU5sEZoXd7hQUDnc8T/qMPub6dxYy3Ljfhr+5jKOxihv+MHF06PO8znHTWb+Oqq7UaXysKhJQqVSuf+En77rFZyFj777ihBFeZYeYURe5ob+aD3C2O0+7jxr+UAyBcmAgQsqJRDfx4HLCPU1bSZjm4y7s2o6Vk1is+l48qiDoDTR+T0f/YiP11elm+c1wbhGwxW2Q8uz/oIotmi19/l96k6Xi4ZCxWh1eP2ajrkbY4uOjjv35DOTBGA53MBLhH/2fr3PguXFVOXROcmXTkZ+eI7n7XHB/3PaM1e256wZEJVK5YOlJgmVSqXyniJARImiCIEv0yOSr2ilWIMedYzZOUcQym79x+01McA6Gk0wNBiX7cBlk/Y2qDc1wksrn2knW0pwqyKYjZUDLwLk5IKOVqNThLv09XcHMyVlJVMmQl+ldlFJEIJCHKXSfWq5Si0/3l6wy+18jJPu59TyD/jAOG1Z8TN+E06SDUwWjkTnBdHLKgsA4wTnqbowtXv5Ur/gxSr2aXd91Dme0J+1tiU1SahUHhY1SahUKvef9P27XsFZiAgBIaB0uaXPK65zy8u8Hl/hbw2Ul3aiFzGxCpmVJnrXOZBfhcwqJFYhlWPOvkWvXdQUsZbKhEI3RLqh4fn2kpbMo1CmLQ85EtQx2wtxDw8smAl4Zi0DV2lFvtHREsT4qrsgm3LdN1yndq+KOHPcsARHVuOO/KiMPrQX/Xr85lfTE/bKN4872OI989yF5XqNvTZh2fPl8Nn1V0ed5lOuTlzYjTXou7VcrVQqd09NEiqVyv1H4l2v4GxktLb5nf4xX+YNX6QLvsorwtzf72WH+Y3vLx+DOFGMIBkQAhAFNnEgZSFl5XqIs5j5ZuPMvrOm/LF9pI65susDKwZ225YL7Wk1YYst+rmKML7PHYImHrU9395cs2kSTTDSwba+kx3MFRVHXqNF+Lprfy1zUO6gxtSF84oG42tY6jFKPxQHv02PnoQ835Kx0jJpE5brUPCwMJua1r9Xf78T3PwkDUylUvnwqUlCpVK5/4RP73oFt+LKWrbe0mgxBr22eGAD+vbgTXB3shWh8FVuiZJoJNFKplFjlyO7aZfei8vOfvLx3ldn+jB3I41PKVbmLaw7eleyK4LTpYjqIkGYPhVo1VgzEDO0IdOGMgl6wl3IHmhDZhUyH7UdKjdD8NMi1+maJAEN0BoEOzvYFl3ch8kJ6liNw41kAAHXfTI2YcvqwmSm5Kde+e0olYRaSqhUHhI1SahUKpX3Gmfnkd4DgytRxx31hYuOvzUoLTMSOotFO5AbeotEyaSsNFLmGQAHTjvL95f/jS0nstjuHt2Q1k0mBifGTCaSPKIykCzgLmNFhP05gKBG22TWYUDdWOmAEeZN8oQQ1ViFRKsJFedJs5sX6chJu9uTu5H1ghEwBHM4Ut7x+mNSAnaf5xac+Obpo++j/pIAHNo8CUBYpIZTQvKOSgmlklBLCZXKQ6ImCZVK5f6TfuuuV3A27tCbsDNha8q1y2wB+vbwsESSghHVSC50FuhdyxA1Kx93SenyPjjnxi717PCJo3Ny4vNHVaMJiXXTkdDRlclRURpNiNgsup1ba9xZrwZCsHEYW6Qn0nuDOWQX3BVjOoazjokn7Y5WB9xPdxJyB0+CuBadRhbIWnbJxY+5oa8gvrCMmnb8j12YHH4xVySWz08C5kkHwk09+bsL3GsdoVJ5WNQkoVKp3H9kddcruAVCwulEyeqEYDxpei5Cz9vDtmnHXckmiCgqsNLMo6ZjZw0DgZ23uCox5tc4GvnNJvy91efCiQiF1XpgoGFnLTnD837NC2uhcUKTD9YrImz7hpS1VCjGSHhKEKYFCMYwViN2SdnmFvObCzoOVS/yFHHIAnmhszgn1p6sT5d9V6dG0otOLlkMd3tF1yAU8fIr7313obvWSkKl8qD4oNR8f+pP/Sl+5md+hn//3//35+f++//+v+d//p//54PX/eqv/uq7XlqlUnmfCd+GdNeLOA8RuNDMhXZE3RBIbEIqcbs7L/PqiDYQIaNcyIBgCBl3ocsNXW54PqxQLcODi0VPmN520FY0M7nsjDpicaeNmT4Fhqysx4b5jgZpHI0OZkijWFdmOct47D41bLvI48sScV9oTxv2gW+yQJ8jvQUcZZcDg4XR2eiMoFUctXGatIJnKcH5OT1Hk3WpFB2ClQaoV6sBx2CUsRVZXn3raE8167aXbkzvaOIynCESr1QqHzQfVJLwl//yX+a//C//y4PnXr58yZ/7c3+OX/zFXySlxN/8m3+zJgmVSuVece2BoPAodqxiS0wJJ7K1lmMLworTjCJlESGZjM5BzmUcuO5XuAmiguV9SCmjngEf8wVjDIVlThRW7YCLY43QWeRpaTgiu9BbQMXH3f9lqFoCzozQS6BPkd3QoG1erNnocqS3yJAj2QLJ9MYxTgl+pbhBYUVcnL20C2WZtRUnh9Kz69N5OMukifkjymypOnVCuey/1vGkP9h8dNR5bjtxWfRdqR8qlcr7wgeVJHzxxRf8/M///MFzv/f3/l6ePHnC3/gbf4PPP/+c3/W7ftfJx+26jq7r5q+fP39+67VWKpX3CLudR/xdYg6dK+aZC9nyUVC+ax+VIWRHTgFzL/MGVHx8AOJs4sBVXnHRDFzGnq/ypgTK4nsx9Bi1+qLFZ9IiTHZFGuE6r3Bxclb6rKxE6S2QNHDR9lx3inkcRcwl/r3KEU8Na1eGFBlSoF3anEqJ3LML42zo4n50G6GxSPnN1wADZeLyrBg+9WgyB/aOl2rEiQLmKdlyKUUBmXKksZVpMoYqicOonZh+FA4v3tHEZbd3qX6oVCrvAx+UJuHTTz/lt37rUID4/e9/n/W6DBY6txT6a7/2azx9+nR+/OzP/uyt11qpVN4j7Id3vYKz6d34LD7j2/E5j8KWC+n4dnxBf6KdThN8oa01Bm9AoNFMK4mLpkfdRm3s0oZ0IegtGcT4ZXlNiIao4eJsh4YgmaBCFCstTAKrmNmsBlT2ugTBSRZ42Td0KdAPSivDPP9hOkMTMquYaaMRg3ERh8XaTglbx8FzBqblUZyJZI65Tw2CZ0nAPHF5+Y0TGfUGy/s96T7mrq+bgmbgSX9cAnzbicuiQh2UUKk8LD6oJOEf/Uf/UX71V3+Vly9fAvDs2TP+7J/9s/yRP/JHbnXcX/mVX+HZs2fz47vf/e43sdxKpfK+EL9z1ys4myn2/Chs+Sw8R81YSeYx3eK7X4+IEcTK9GN8Hgws43b1dWr50fUlg8XZqnTaXZ9C6NISU55Tig5BHMyEwYWOQJ9anu0uULfiRhQGWs00ktHs5L7ZW6FKObKlQLcLPGp6vrW+Prx2h0Sg1UTUjIpx2SZaHcbrOuN+Tq07o/ep63muRizeJtOYhRtD1Y45xnyQUKxYlxro+TU2VSum94xaCPydTVx2m1TalUrlofBBtRv9u//uv8s//A//w/z8z/88v/ALv8D/9r/9b3z88cf8xb/4FwGIMfL7ft/vO/m4q9WK1epDdj+pVCr3kez7oN7caWVg8IbBAqhgLqV16GtwB5US4JUAWUnW0ISBVgcCIBh9jq9trj8w3fSpcX9xUlOG6xbXjnwhCEZ2yFYmKkdL9H1DzuHw2Aakoi/Y7TZYFi6a5W63k1zn9iclFxFzUnqLvGm7fmEW9Mp3READiBnZYtFgSGnheY1c+ChmCYGX1rCzWNqc3uymWjgeiRUNxd7USA4rGD9JhFpJqFQeGB9UkvCd73yHv/W3/hZ/4S/8Bb7//e/zp//0n+aP//E/zuXlJQCffPIJf+tv/a07XmWlUnnv+EB3QMsmcvm7BPPl784jnZ3yz3d5TzBjZcqgSnSlMaHPAUGJmsjWzo6n8x3zSbE8rmjc0p6kAT4KaS8083S15e95/Jw2OqrKI+2xpFx1DVfbFeD7acSTO1KCtQxIFvqsrOPUFCU06jyRDsmw1mJ9ejWskCO26w+u4eCOCgzjNSSZtQBkztY6LIyeZiHy0W+cEoSxWnDwVqVMYM4lQZiSOJ8ThncYtJ8j6q5UKh80H1SSAPCtb32Lf+1f+9fuehmVSuVDIv/du17BWShOS3ETQpxsDY/EiDiDh6OCNpEy2EygCIk9kCyVuD+U+QAGhEBpCZrsdSZtwngSt1EDMOsTytb3JGJeiRGsWK1mVwzohoY1metdi6NIAM/74NaBi9XAepP4+KOXdNIwWKLRvXg5iHMhPbu+IXqiz/GVYsYxTM1T7kBQRPeXWD6eVk1Y5k3+ulkSp+Cl9UtfKSMw2qtS2rSmUsUN3ci7olqgVioPiw9Kk1CpVCpnET5MMwJBCAKNOI1Abw2DN1znFdvcHB0oT7oCp8wxWKmxDgMmZd5AcsWFMkxtEvgCuE8GQ8A+2Sj99z4Hx4bSDZGui7SS2ISBlSRclLbJrHVgqX8usXCpB2xWA5tVhzSwtZZnaX2jbccZTOloyRIJkk647oURkO+vAzVQR4KVANw4eVdeXvlkf13nHWisRoxOSeNKme1QdXJAkvk3t5/d33Q6ItUCtVJ5aNQkoVKp3H+kuesVnMUkEVCEKM7vDE/4fv+E/3P3CYmweMXXh2+T1rV0rJT5CI1kkinmQpfH7XRfNMbD6GS0lzBPf+vi6zJbwEhZCG7kvrgUJSIxZESci2YoQfgNQW7TDMSYiDHjQMCJ4uTF69yFr4YLeg/0HiEourRJPRnBtSxcos9zH6blnRwIL4out+NGFWFs45r1B3kvXpZxsNqU+r0L3KsFaqXy0KhJQqVSqby3lABQgCiBjcA2BzoPBzKLr+sCcS8C5+wlIWgl4wi9Q5eU7dCSrCF7wKbeFsYWp+ngMgWk8yfI+ArBWK0ztMLvDE94kVtEiuBa1Ihknra7sYd+sTCBlAJd32BZuXq5IUqm0Uyj+xcOrmxthTnsckBxghrzpLET7uV0n1QdbcYkQfMc/J7SMXRw5smN9kyXpJkxRyv2rItvjOJlcZA8/ojeccSuKrXdqFJ5YNQkoVKp3H/Sh2dr7DgiQpRAkIAgvEhrVCKN5BstN2+OGMvrBBv/VjEaEhfBaMRpJRPJ5CzkLK9JPvah8+zhP71mbEUyA8QITaLzpliXWiQTuXzUsd70qNncyO/jn+xKnwM2BHbbht2uoZE0n8AMng1rkgsuQqMJFZvTk9PmJMjhPctjwK36zenazw3cp/eNeY8ePAkE8KlwxM2fwbEnvV1WYeazBW6lUnkY1CShUqncf+TyrldwK0RKy9G1NzhClFe//zbchUYMQxk8kA2SK10OBHFww13Zz0VYhpWOi+NSgvWpfWk6uSXFs/Lp5RVtNLIp2YXBlUwRS7eNIYs/07rToNjgBC86CcGw0g+EiLMJiYswEMW4CJmLOOobXM5z5Fzu+AeHYMh0MecmCydOWZ6ZT7uvGE0akEk/jlGclyb9gR42GP1g8/SoU33OcZOZv453KIGoVCrvATVJqFQq95/wyV2v4Gz2AWSJQqdGm/n7RwZuAouZCoIh7HKDocVXRycbnXLW8rJp539MHObsYGpJKsJmy86jZkujiTYkXKQMUSPzo+2GgUDTptddHG6waoxHm44nF1tcAkZpjUouZRCcZtYhsR4F0ZF87O17PZNb0CSwOCPAX9RXyt9+Ro7xGg3CgdWo7r+WRUIxC5odnjXHJcDPaU9d3dcut1Kp3H9qklCpVCrvOWVEmXKpeRysNglWl+5DRx0IoUxfjuK0Yz/+DRXC+NUi6J0iYJ2en0TM5WOMzqrJWArFiSg3uDvP+zVXaU3nkRxuHHxk1WRUM6umx7y0EM3nFggiXOiAmdPnwDZHdna8s9MrLC8sA2k8EQtb0xOONXdk6em2rOxPPR9PlirosaKw1HxLHpO9Mcd52h83SfkJ/dtf9FZqKaFSeUjUJKFSqdx/7Nldr+Bk5EYg7qMzUcBJXoaqHbO3u+8KKlOL15pYadE0RM2sQ4+SF36b+8NObTgi48N9cuM8OPOqSYiCiTJ45HocymZeetmvhwYT5ZUgUyCETBOc3mJxUhIfaxtOABQjamlB2uWGbWoP13rs/Vyoph3w7EUhvLiSN00vnvIKY28ANZlBHbzgzBhalrMplp1PtkhCpirDlDCMidtn2+P+v/0pxyUTb14jVbhcqTwwapJQqVTuP/nDSxKAgx7+7MJLayih9IH3zdfsYDuLfXmuUltmIozP+dh2lBFWjdHGzD4y9dfHvDd2vt1gMKVPYe5Eyl5sSi0rTRbAyCageYxtx9qFQcqRLgccpR9dm6befBVHMbZDAwZ9VnY5orP70RlBqwluAWuErOOdED8M/heHfuMZpt18kYXR0plB9JRxTFWhKSkYKxSuPv6MvQiubZ/QyDsK3N1BtYYMlcpDov4XX6lU7j/Nz931Cs7GHZJnkJ5P4wueho5Hmsa2nOOqCdOrHCV5oLNInwODCSZKlkDUsmO/3x5fjgVbtOMspiX7qGFQKW1CbcisYx53xgU1R7IT3emHiKHIYslGsSMdciyJw6hDcPcxvSkzHVSNoMaj2N+42tPcjaAkJljJCCxIUTeMrTvzjVq2+CzW+8ZkbJZy3Ezf3s5BQjIlBa85hDhImGY8sMhH3k2SUC1QK5WHR00SKpVK5T3EAXOj84HejYxxGV6SGfi0eYGIvTVEPrAIHbfn0+gK1KixaRJNSDQxEzWXIFBKa870OAhCXcqu+yJ6NoMYEo9XO9Y6EDXTaJmK/HTV8aTp0OxYWrgajVG4qtMj5MYZorJNLVF91j4kF7I0xOA8XW9pYuJxuzvR+nRxC8SRMFYODHwolYDXHu5m/nVTsHHzW1M14dyWo2Ufl9480OgqNfeOlURCTh0VcQuqBWql8vCoSUKlUrn/+Dch2ny3uDtp3JfOGMmNRrpRR5BoTp067EWTAGBja4uOwudN6Mkuo/XoXjTr82yB/SCymxG1BrjetVxvW/ouomYEyQwEoiaCZvpdRBczEgBEnbAyclAsQjdEuhzH80zuRpHBlcGVoEZU56PVjiAZTqoiTAiijoojCTQHZLJSPSXY3t+i6WpedSY69lA3Oqdet4z5NXmhVXnX8bpUC9RK5aFRk4RKpXL/Sb911ys4i2UcaO6sQk8jmUYya3mNpehbUBxEyV5ajXpTzBT3MtcgG0wJweR4tDTbmdclex9/QUgW2Q4NQ4q8vN4gBhEr7UgZHrd92fVe9O67OWQBE7qkGBAkl5kN4zmUTBSjEePlsGKXGnYp0tzKAnVUVDvgpaJwzm78/JYb7kPnRu9T+9bS7nT6hqfynI7nmzQL7zRR8KWFbqVSeQjUJKFSqdx/mu/c9QpOZhmPZReuUFSNJ/GKR6Hspr+tRVzmHW8fbTNlNtExn234x7hZcHSaiMByqNq0Qz6PUbixRhVIOZCSkAal6xuUjAclR0Wis2m7g/WKCJ4huvFYMps2sWkTvcX56FGdgJFdSDkwWKDLDZ1Hzorsp1WbIq4HT52qJTiMlxc35sQCz3ywKQO4oUnwMOYENs1HmDO4d25IWjUJlcrDoiYJlUql8l5Stqg7F65dSK5lWoIaqonO4tE7ybKI9EunkSEobcg0pBKEWykPiB6Gy8s++Ff78vdPGEKzzsRVZhV7MkoyocstTXBW0RbJB6MzkqAZgjmtJ2RMCPJim97duepbsinBM9sUx7aoeRGnkRm1FeDR8fD6askxlNvqeBi7sG5mUW/jRsVgmpQnzn7YG/uv3SntUfO5OaGccMsywOTkVKlUHgw1SahUKvefD3IHtKw5IySUYXxkMzqLtHp8u9E+di2epUEgoQRxonppDbIS6crsv3lwhPnDsoXe8XF32XlysSO0mVXb0zQZFaERJ0eHkLkewt7ZyCcnH2E7tHy13fBiu2FF4iIMhEUsGtRpRh2BidLqcGN1J7oJHQTz413R83bl/Ub94WTDocV1ujjibzmGjhUPc3BH3Pnh5qOjTvU5F0cu6g18iP8JVSqVW1GThEqlcv/Jf/euV3AyIkKjymNV1iI0QPKGl94QxLgIPcdFbiXAVinDyTprGFxJFooA2J1GjLUmzKTMM1gkCrIIqqd2k1nAOga5q6anbXpiyEjwIkA252VuGVC+ShfE1bIPZ5F0KLzcren7hl1qDlaevVQoTJzBA1EybRhbp5Z+pafcV5iDbc/MwuWTxcss3jv2bJ21076oFjBqEmb9+NzPtU8ipkRtqiQ8i8cF/89pT1/bAq2ChErlwVGThEqlcv8JP3vXKzibBqEVAY/8xvZTXuTH/Hb/ET8cHnPgOvRGZBYhyxSEupBd6ZIyWKCoEUYL1KnffRL2jqrcfdvRoR2POzTBSLkBnCiZoGUQWjLh2hpQJzSG6A3B8egI9GizpWkzW5pFS7/PugjFaXWgGe1Vo9xGuAyYIL0QkpTBZJnzdsp9/xAmvccZLUdTv9PoOjVrJIq1VRENOxDKWwQpbUkCT9P1Uad5wu0cvqxaG1UqD46aJFQqlfuPNG9/zXuKCKyA39h+yhfDR7xIj/id7vEoMpZxlsGbmYauTXOao2RaHdjERBuMwQNXueUqr1Atw82QSeM8RbujmmDqF5LXxdTjq730zu9yw/WwwrOQpFQDtBkjXim78Dism4HLi45mNRDV2M42qMI0Li4Eo43Gukm0mm7V+eIGkkq8rQaSBLGFVuIUph3/pSD7xOP4jYLIQdJn49OLAL3kET4OtoPPrp8ddZ5PuTppXTdZaiQqlcrDoCYJlUql8h7jwFVuiRpoRRBvFsLd8TVfGzVPlqZlp3oVMiqgGLvUkE24GlpExtYZ3WthJ2HuWFYoz7kzzjsrsa04QTMXqx7VonVwh4CxkYFIZtc3bPsVduNXTpDMx49fsmoHmibRW3jF2nMwwcpEB8yF3hvMw9n3UyLQGkRD4g3/0xODYJ/eNBdXfF9NOHY90yfjUmTKGpaTrZeaiXFE9DQETvwcO6XTWVaZKpXKw6AmCZVK5f6TfuOuV3A2gnOhxhMdeBwGAsY62MFgq69vhS/78WZCECeM+gTFeZnXqAqXTYd7ESG7jTMM2Le+zJGs+2KWmoznFl5sN3RDg4z6BkSIagR1VjqQUtjrhGU/h6FtMzFm2sYQBSPgi4uRxe56cqWzZqxynBetTvmOq5dSQutIa4dq7DMQGdt/Fs5Npy9uuj1eEoapaDPeOEH2emv32y75LKq7UaXysKhJQqVSuf/oZ3e9glsgXITE49DTjsPFnoZhnGtQ2om+9t1SXmMIQWxuPAKjUaMNmUftwKOmY4pM9xUEDh+ThanCMjx1F7IpQ1aSlWnJIo5ikEuCYixaaxYMKZBMcBNwK+9Z4EgZ/uahVC0kv/Kak+6mAMExAVPH1cvQN15d27FMOgSXkoCcHEwLEMeKwULS4ABpnGfhDmnsbrqLHX0pA/0qlcrDoSYJlUrl/qO3tH+8Y9zh0ehm9EgzjzQRpYiKj4lHS6JQAk3FyntFaEOiDZkgxqN2oA39fhhxOfM+Yl1GrvsXlKBWDPEyLXkSLWdTuiGSU4QseFY8lYRhWk8TE7Fxci6JwkozzeK3UnboLZAsFA2vK11uaG+2CZ16P3OZMG1eJk6fmx1MDkMewCOLkcgnrmf5UUczqfHW+zRbTkcJ+tSOJbzb3+An6rErlcqHT00SKpVK5T3GHb60lhce+Gh1BdoTtCNw/JwEoLT/iBO0uBwNFhFxWk3j7nxmpXkWDY+Cg31ACqOz0avBeaMGHkgp0EhCxRGFn7p4zqPQIdnxQfAs40znEuhml7HKoezyil2O2KTIZV/XEHH6HOhyGSAnZycIoxtUVtzA86QnuJUUehYvz9akJzIlGyJTNWFMBsaHjZqSLPsTifmoT3g3iMqHOW6kUqmcTU0SKpXK/Sd9965XcDYGfGWBryzSiRBjxxXKOuYjQ1tHMB7Fjjbk0gYkTmnsEcQzjTiN5tFL6DDMLRaoi3KCHBx6v5vuAlbExVOiEbSIACRPvTiHot6cI/0Q5uqFUaYtTy1UY56CjN+bg/lzt7Rd8DQG4eNBXGGRl5x+yGk9C8H3OQdx8bkda5qT4IxuTJPUQQGVMjNhIRl5F7j5+ddXqVQ+SGqSUKlU7j/y+K5XcDZB4IkmkgvXprywBhVhdfS8gKJCSGNrUgnIizYhiuGj+b6OmgVfbocvA+cbbUY3X9CERBszqmNa4rAbAq2Uic4sZxFMsb44jtAnLUGowyakw2DUnd6Ku5HIZIv6+orGEbeiHGOhqRAYvz6TksGcz3xPZG+UNK3Py3wEKRlUmcjsgMhehH105H67CF9E6qyESuWBUZOESqVy/wkf3fUKbsW3NPFZ6Ghc+F73Edvc0Hssof5RcZvQ54ZkxWVorWVWQhwnLvdZeTm0vOxXZZrxpDeY/pq29MdjLY8L0KVI3wdykmKRKlbmGXgkai5zCFxuCHodo+QOOQrmYzvRjZUnp2gSXMmuGEqy2zXk7908v6GgVxmrKeclLo6XoW7jXASXGwLmaV7C9ITNb+XzzUdHneZzbqfLcfc6dblSeWDUJKFSqVTec0TgI018OVyACde54WVqOGV3OHlgNBkFoBmnL6uWeQm9BZLrbLt5MDur2Ci9YXFlgbvUcN1HhIyOqlsZxdVNfF3FQxiyss2R5IFkSiOHVQSRUkkRnOyQXBhMkGUmcyKlHUpL204QXMEmy9FvgnPyBB/tTcdqgWQp62PMzSbxch4HwMFcdXjeHBf8P6c9fWE3OCsJqlQqHyw1SahUKvcfe3HXK7g1AWflzgbnOrWYlTahY2YG+Nhi5K4kD7iVhqOMsorOuukRsbHvfy+cfaXpfXmqYuo/fyOIsQ6l3UgAMyFn2PbtXmjsi/fiSCxpy64PNN7zuOleWXc2RQQaT/RpatY/L1gVAQ1T3864oHE43Nks85Xlx1OOubzH+TX5yuRMtRSOj+t/3F8fdYon9CcsqFKpVGqSUKlUHgL5y7tewa2J4lznhogUyfEJke00kCu7FBtRi4BTDHKEdSwzE2Ry1ZmHgi36/+eE4WamUGiC0+eGiBHFEBG+/eiKx6stl82Om1GzSGmjcYdtv6a3hmbUWEyhcHa4iD0r7Wlj2XHvU2A3NPOrTs0XRIpFq4iNQ8vkm6ki3NQlnHBMF98LqFnc6ikBEUfHaoeF6UVlZsVn26+OOsenXB2/oDdQh6lVKg+LmiRUKpX7T/Nzd72CW9NI8ciM4rRjGw8wC1jfzBRyF6vR7JA9EHEUyAY7U1Rh0/SHFQLZi2nn2WpS4uHDc8o4d6Ds8s/fEieosWlft4stuCmWBcEwg850HtjlBqCoQBQjkngUt+xSfK0L03GM054zSFbEBbLjZyQb49EWVQMvx7vh4HQMMrpGuQO6OO7odCRGmRCNlM+F+Qdwvh3saVQL1Erl4VGThEqlUnkP2ZsJOUaZCvw4dLSSaCXPAdvXb+7uozp3J5mgFPtTxIlq9Cny5faS66EtrUjTG5bagBtHleVkrYVb0jr2THUIx0mmaMhcNP08HGxfjCiBsJvwcdzydNXxuB3orSkVhrmG4XNvfhNA/DAEP6bdan8PAAOdprmlcbUqi9ap4xFkTgymj3Ob1qm4owuLV8dHN6MxaVpmg8I7Sg0Wy6sWqJXKg6MmCZVK5f7zIW6BuuNuuHvpR3fjSej5OO64zs0Nge/rr2+uNlBabMyF5GXacJ8DfVKe96vS8pMifW7GAH9/cL+RLbxiuyklWO6TEvDiboRjrry0FtfAapVRWfTjyLSjL7RiXEjm25srjFDci9A5SSgpiZK8PNqYxzRk+TiOqe0KAV1OR75l8CvTAOjRdElOPaABXsxdb85JwARMCJM7VNi7Hfk7DNpLJeED/O+oUqmcTU0SKpXK/Sf93btewcnIOOrYF3/MjZ0pUZeq4rcHygJELdOWS7VAGYggzjokgvisB5DJjH8uZSwCQx87kF4R5gq7oeW6bxmyklzovCETMSlBfxvT8jClv96EaGXa89YiZtPVyDz0bTAleyAjDK7EUIbDnX9fy/V5OtQv3wZZbO2f2mr0SkVo+XafihzTUf2Vtb6rsL1UEmopoVJ5SNQkoVKp3H/ih6lJmAJmgOTGWntWUioKR71/3PmPYkRxojo+DuLqcsBcEXXWMbFpEq0mzPbVh8nedNG0dLi4feiKirOKiUa8JCGuDKb0rnS5ITZTM/0+0FTAIsjG6bzlx/0FPoqlSysPDBbpc5irICpTknBOeDyKlFNpORIHNRkdhc4PgH1qv/IzjjPeklm07FMLUxl+5woWx5/JqBEpPxa5cTd/8tRKQqXysKhJQqVSuf9IfPtr3lPcnd6NToyPmxc8ic/5OLwgkt7+5v1BEDEUI4iXdiNv6DwSxsFnqzCwjompb0YW790zTvpdepmOycJF2zPkgI1b8+6KE1i1AzFkVs2wOEo5frMaWK0ynSudlcSiN5nVCCWtKNqJoLCJCRXDCJwbHksAiY5ISUbcShvPbcLfOaGavzhhPdMneazQmMPY/z/JHPAxiXBmi1S/g+nHNUeoVB4WNUmoVCqV9xQHdjgdzuCOSMeVC01IPI07josTyzwE8/IQd1Yxs9LESg035Sqt2KWWXYrMvxYmtx2XOagXSvuLjrv88yKRucdfxkrCdIwYjabJXKx3CHl+j+MMfeTqxQq2gQsZ+LS94kmTStDuQrKIiBM1oxiNZFp53WC2Ewn7y9Rl69RZQbAjevOZExEglqqBB/bZwWJtProLFWOjfQXHj24Bun3NoU5crlQeFjVJqFQq95/023e9grNYGuUY0OhAlBIsN3pcsCxSxMfmpUlnFXoimVYT6sZ1auhTYJsjfY6l1WduneFQ9rAMqG+0G7mPFQAXioLAZnvWnJVtv8KLjycue02FurO2jF8rl3HfRmXAWPsgjAlCxMg2n/w85q15h2jzdc7XeurhJkvY6f3TfTp2LQeJwNR25IiV6cpLfbXros6jpYrzw81HR53qc46bzPwmRAW7g+pFpVK5O2qSUKlU7j+yuusVnMWy9zw7vLSGKIkg48CyI4/j4wyDVhKRIoRtJfFVt2aXGrJDl/YtPAdtJTcCWW44G5WOIyfqwGXbEdRHIbQTMPo+olImMr+6LgjiaDQISsr7CDuI02gu1QM1omS6HNnl2/0spxkEEkHb4sZ0ywMiS3HAmYdzKL+RZZ8MuBcno7kViUUOMn79vDku+H9Oe97CpvWZ10pCpfLA+HAbdSuVSuVYwrc4pYX/fWHarO888GNbsUMJobTfRBkQyYxTtr7+OALuxkqLFWkTMr0FHq96EjsGIs86o5/2qeU1O+PC3g5oShSmGQEIL3drhqxjVFtmO2QRssdiOTpbIk3NS6BirNtEFkGicU3LyjM6butHybhAlmIDKl7cjs7Fp4sKMvcZuTpitwt+hX1itaz+HPfG8dOF3mC2QQUkl9zBZZGMRIFsiMOT/vqoUz3hOLH7m9BqgVqpPDhqJaFSqVTeW4TelRe2GoXGDS7OziNXtuKYBGEi0zCYEsRwAskjicBF03PRdHy0vh4j3WU//H6k2YE69zUOpG3MuCv9oJT9et230TgM6VWxcemxL4PYJBjJlV0+vKYgxpAVs1IJET9zOvJiLagxTVmWeLvAd04QxiRqzp9OOcCYLOhYNSgZAXsNAoAtPl9oEj7bfXXUaT7l6tgVvRarFqiVyoOjJgmVSuX+Y7u7XsFZiMBjFaIr5pGdNVzllp7ASjOn9raYC8P4yKNdjoiTMpgHVnEcHjCrkmX/YaFBmJ9ctCKtQ88mdKhKsVuVhGCkLOy6hmT7wvU09yF7SRHWMSNZuAg9TbBFQF/MUB81PZs4sIqJlabbT/7NCyFBcF6b9ZzKrLOYY/zjmZKDaRlj2xGLhGM+noyvKwryUxZ4yotfS60kVCoPi5okVCqV+0/+MIXLUEK7Vo1WnF1e81W+xLxUBAJ2wq56sUEVURpJXMaOizCw1kSfW3qLODo7Gu3fxcHnThkC7IveeZUSuL/s11z1LeaQPSCiPNrsCMEW7z5EMTxDwzhjYRRYF6vVaQqxcNl0BDEu2+68+zi1OxmQFDFFXCErt/5VKOzv2akJArDvOWJu5ZosT+f7LOCLRGIK2H9w+fFRZ/ghlyev6lVqJaFSeUjUJKFSqdx/4nfuegVnIwIrjC/6C37YP2abV3zRX/Lb3dPRUPTYLEGQ0avTKEnGWga6QWklkbMwZN33vUvZqd63zzivbOGPW9wiMlusXnUrDC2OSpRjhMZZr3v2EW75oOI8313wolvRpQijs9KcjODYaETUaEbEuYgDyjk2qGXtkgXJMlcT/JZaFT/86zzhsu4zCx+nXcvydk+lhEnYvEhE/sa3/r6jTvH/4bMzFnZI7TaqVB4WNUmoVCr3nw84ujGH52lNlzcMtqLPa340XGJocT464dKSTfMOSiCaXGlDZjA9CLx9ttZhdgvldY/p9WNCYEAItrBALdGumY/ORfv5CQAeYBinPu+GliEVC1Yd1ygIOiYNX3QXJI8gwiqcGdkLmPi+UmFFEH0r5kBebpUnHBxrOkjxgS0dUdP3x9too2bh//3p38uz5uKN53TgGS3/C5+eu6r53FbbjSqVB0VNEiqVSuU9RsWIIqw1s8aIDsn3w81OwVxIVlp8eo90HseAU2kiNKHs9Mu0db0wM5JpeMLMMiJ2gmQu2p4m+Ggg5AQ3rroVSQPtap4mMLsbCeBq7HKkS5Hn2/W+HX+0VnV3nvdrrvOKbW7Y5oZzw3BxKZ5+jUMDHv1W1qWOz9WW5XA5OTeWdl75uS6Fy1O31PwdFVwC/4/f/89Mb795OAD+n/xB7LaJsoN+wMl2pVI5nZokVCqV+0/6zbtewS0QVlpmBQRxdhYJ6GG4flRQ6qh4GcamzmR5bygrHQjj/AU3cJGxvYgpWp977UXKdOUpXBR1PApfDBd0Fku1YqxEXOUVXw0bOosMhIPoWcQJbaZZOUOMpEbpVNllBfF5TsB1anmR1vQW2OaGXY4MlNakUyn5jODRsTBOKlsVp6NzmZ1dx4zK8aWk43gWv41nk6lFexEs7FFt2QYG/+PP/d/41X/gT/Cj+OjgkJ+z4f/OH+H/Jb/7jAXdWF6dkVCpPDjqnIRKpXL/kY+Bv3vXqziblWSCGzsLhDHQTxbxMXo8boN3tM10xVxQNQaLXKeWEJ2gAIYoo/emjBacJWLdJyJTMCyIOBIc1Olo+NFwyUXasR0iL60tAb0pXQ5sh4gEP9AAqDghZELT07Y9RqCzlg0JR8hOOUYODDlwnRquUjtWNny+/qOu3m8kUzpexyDolAEd2Ai9HRnvi7iPzVHcrrXtdUZLSy2CUyYxj68dZd1gxv/48e/jr/ziz/BLf/f/y7d2X/EFa/4mn96+gjBSpy1XKg+PmiRUKpX7T3jyQQ5TgxI3lp1+I2JEcVaSubJ4Qjxaot+r1PK42dKZ0npilxsGK/MLOgulfWUZeHux19lrEqa++zGottJbI1qShedpRVLFUC5i4vmQwWA3RNx1P6QNyja5CeKCBiejJFfymMQgQggQQ7Fp7W1fPZDR98hPiOpLYjEGu0pxOJoEzPOLTrqd5S1ebsP09Dk4JfifizbT/R7v1zwVOow/gimnmYXjICpkg/8l/DSEj8C+AVvXBTItrlKpPBhqu1GlUqm8p0xhcKPG37v+ip9qX/JpuGYlieMitkOFcXYhiGAeaRQChplwNbQ4QqNGENuPRJBpr1rG4PSwad7nXpgxWYhGcuF5WqM4KQViLpv2OQuie11C2dkXBC+tVAEMYfCinaCclWzCYGFslZqenQTbp4flKiDmaF62VJ18mOkGlHsyCym8fH0iAhDHOWqL56ZuL2y/Rh+f9Bvr9mFArndIzt94ggB716VKpfJw+OCShL/21/4a//w//8/ze37P72G1WrHZbPj9v//388u//Mt89dVXd728SqXyPmLbu17ByZSefJ8faxnYaKLRxEVINwLb10dvN9txDCVP4leBJmQceN61JFOyKaoZ94VV6TTheKwgTFqBgowOQcpKE9/eXNEGZ5cbUhJICoOgGfo+koZwMPlY3XjSblmJ0YbEOqZicarT6cuJdZ+qgJfEwW5GySfcV3LRXRDBmkXwe2wQvGxN0uVDZlH2yYzHkrBYyNgiNedDpQtszsvm8ogq+VuPyCslp768tToRVSqVW/JBJQn/7X/73/KP/CP/CN/73vf4Y3/sj/GH//AfRkT4p/6pf4r/6X/6n/gDf+AP8Nu//eEOTapUKj8h8vfvegUnszfdKX9UjMBAlMxGEnHeXj42WC5i5evcMORQBp4lZzdEsgWu+pbroSV7RFTmriK/eewppp6CVnPWceDppqNtjDYm2pi5aHrW2hPc8CSkPoxtROUwqjD0Ld22xXtYa0+riTwNdBvvwmXseRQ7NqEbpzjfLvgtXVKKSalceACLZ4iXZfzZLBOM2wTmTmknmo/FnIxMXV6TcHk0VWL5UtyxdQOr0kUsPwEnopp4VCoPiw8qSfjlX/5l/uyf/bP8pb/0l/jzf/7P85f/8l/mz/yZP8N3v/td/tJf+kv8E//EP8G/+W/+m3e9zEql8r7xgQ5Tk/GPI2zdacPA07jl0+aKb8Xrt8ak+zjRCVKEwskjCS197hJoxMBhyAuJmu93xEWKJmDuwS/Om/POtzu0momSi3NSzKxDIqizaQYeNR3B8t5C8yAKLkkKLsTgZdaCLZ2bhKhGDJl1yOP6A7fYrx/HP5QkoSRC+2s7/aBFUzFH8ePHkxOORY+Rz5nA4iwqiC4qCOzHO8w/YxE8jDWXn1Aw/5NIPCqVyvvLByVc/jt/5+/wx//4Hz947p/75/45/tgf+2MA/Ok//af5J//Jf/Lk43ZdR9d189fPnz+/3UIrlcr7xQcc3DgwuNNjIAmZH3aqGQ9QRLYBQ0RoNBM1swn9KATeB/GTRHh67K0+CyJAdtwUcVhLYuUDrSRcynC1VUgoNiYd+opzj7kTQwYvQmTFiWplyJvkcl4p3+tyoMsNZ9kQTffSwdL+OlwdCTI7Lp39/xIbE43p+iaNwlGLOvxc5ifl4NtzgqBS1j0maLPAeUiElBCzn8j/30XlJ5V7VCqV95QPqpLwne98h7/yV/7KwXN/9a/+VX737y4e0N/+9re5vr4++bi/9mu/xtOnT+fHz/7sz34j661UKpXb0rmzcye7IzIAia2XqsBxzN38CGUasiHsciBKxr0MPvPly1nEuAe73L4sAszuO4oRcBTBvExx6C2QTUimXDQdMukclg5HCFEdt0jACOqjb5EsXuFFL+ElYYCitTgnXvVJjCEO6qjunZlO1Rv78hOj/DYN5XHSjvuyY2yRwMzo+NQU+4+PA0taEWgbJBmSy33+pluD3PxDzrUrlcoZfFCVhD/zZ/4M/+q/+q/y67/+6/ze3/t7+dt/+2/zn/1n/xn/4X/4HwLwv//v/zu/+Iu/ePJxf+VXfoV/49/4N+avnz9/XhOFSqVy54gIjQvXQHII0rM1IYrT6jj87Mi9HsVYh4EYSlKgAjIOaItqRLFih+qO+2zXs9+4h32AP+6aiyuq8OJ6w6bpebTaEsm0IbGRnh9zyTUtP0qP8Nah99lydO7SmTp1/NUumUm0u46ZTSgD38qJw6vB9DH3c95991EETakCDKc3MC1f7dNxbxNEG4SbnU/j12LskxhbvGAWjjhxl9FmhYgC+RtvDSqVhFpKqFQeEh9UkvCv/Cv/ChcXF/xH/9F/xH/1X/1X/NzP/Rz/6X/6n/Iv/ov/IgC/8Au/wH/xX/wXJx93tVqxWq2+6eVWKpXKrYkiqCtf5g1fphXDmBREjEYyvX99klDmojmtZhotCQE4JooTeLra8bxf87S94kfbxxwWmBfq2VlxDOTyteOIC0MOdENDlyOiTjalJ9DlyCDClbXlfdH3zfTjEcyEGBKTtWn2ErcfdM04JI80KqxD5moIN67v2Lvp+8xEBDJIVsTOLKo7swXqMpc6i1yGw4lR9AVTYlC6rvbtTO43Wr8c2WXCLqEaYbWCfrjNSl5LqSTUUkKl8pD4oJIEgD/5J/8kf/JP/snXfu873/kO3/nOhylQrFQqP0HyD+96BWcjlLkDL/IFL/KKK9/wZb7kq3zB4PrWIHmegTXOPBAcFSe7IjC3+AwWgb1o2MdGomkVM76w5Zwcfgw8Os0mj+5Ezsu0obMGUVDNZMIrLT0y7oLr2L80NRrFUTMxnq60GjG2S4mN55+E1Wfc1ClXCGP8fW4VYGksVWyTxkEMpyylJFoyOj8tXYx8Eok75fuLeQzjZZSf6CaSssOPnqG77lYFjTci1d2oUnlofFCahEqlUjkLP12r9L4gAlGUl3nD83zBdV7zZdpw7Q3xyH/BnXGugMNaB5pgCIa505sQ1HjUFn99YZFYzH6nLLw2WTTHl+dWm4EchCxOn5W1JjZtz6rJrNvEtx9foe6QdXZsmhp8LlbDPFRNZUwYDiJ/YRN61qGnEWMpizjnXsoUaI/2Rm7nmwH55EU6L0hOXtyU7Lh4EScH8FCGskkeXzO1R439Rz7mb6L///buPEqK6uwf+PfeW9XdM+xGfuKCIMY1RoIgioYIowQ14hYDLoka8VWPwZyTV98omhN9NXndo8b9GAMxUXDhNRA3cAkcDLzH3UTBLcqigJogMMBMd9e9z++Pqu6pmumBmZ6Bnh6+n3MIM1XVt56+Tez71N1QvJlNKTi4ps3cOrtBXxxnRkQ7CiYJRNT9eYMrHUGH7GQC1CiBiIf1QQ9slnQ4rwAOSm19d12RcCWhGp0Ld1WGRVoH8JRDTjwoDaS9cL/f+LxkACi2yhMFxv5W4QRg5ww2NGSQ0mHZUAoZP4eUZ5HxAvStbUj0JIgIPJNH4DTyTkMCDU9ZQIUP5AvFCxSMFqSMRY2fg4LtQFNVFZd0lSgBUu178B+rgliQTQeLPQDtKCikS7y2MEm52W2SH1LUo5K3gGcA3wszn20yNIg9CUQ7EiYJRERdXI226GHyqNUW6/I1sKJhRcMlxru0TimFnPMQSLiMpYlamBYaOhrSYkXBRHsmJEtUiTnMCoURNdFqN6KQz3nI5wx6OAsEEg0R0tDawWiLlATwEXtkHz0Jz4uPL+t7IrAamxpTSCmLGp1HSicTn8IyqDmrsdmm2/Se20IXMoRyJkHHVo2Kr1AkibWZ2lVgWFYxAVDFpMGZWKZQmJMQX4ZKAPgGrtAbsS2e+HO4EdEOh0kCEVEXpxWQgcWmwEeNtshbjbwzyDtTclWgVkrBZptGzvmwouGpQqNdYB3gJBx2VFxWJ67QaG02NwEIH1gHgUJa8vBhkVZhj4SvLFLaImUFHgT1mzMIBxNJ05AYBRgTrtDUkEvDUxa+duHTfZUMIOc0nOho/kKsVd5eCuFQHSWItp5o98pG8aJU/Kl+iwSrrYUAUCocViQI44qdUwI4HVWKKyQ2TcOKFAQwGtpJtEneNkgSBNCaTQaiHQn/H09E1MVZEWhYZJRFX52FE4W8bZq4q5rPCG5F1npwTqKJwAIHjaz1kY96JVImAMQln4UXM5AS94jG9Nf6Fpm0RRYe/EyABptGSgXwxEE7gREgm/VbvlYEaT+A9hyMJ7Au+Vi/0BZWSqHGc0jrAGmTKx1LGzkXFqw8iTZb7oQGdXHycpkzoKNVjFSxO6HpuMQ2aHNRJlL8SJr3GuQCoCHf+fMRAG6mRrQDYpJARNSFiQgCsdgj/RV28jYgoxvwNbMZ0s6nxSKAVhYBPGRtClmbQj7QyFkPVjw0Wi/smVCqMIgmGl7SbGUjhHMGREUbtCkgncoj7eWRTuWgoGCVCYcbWYG2DjYHONFNRUVP3VOeRSZloYzAeA6N4iF5UdOk68ApWBjU+OGcinIVRhhpKfQmlPP4vxChNBuu1P6Jy0UGcDpccSkqPNzzrdCb0DRPvPj5JCaSOwcVWChnAdX5expwMzWiHQ+TBCLq/oIVlY6g3cLh54JGCZCHBWCRUpuR1gF6e1mkYg1lacNM2XCCs4KVaOqvEtSYfLingTVoDDxY6KjNG7ZKVXFWb9Rgb34bBTgl2OwMNroUGsTH5nwaJhovE4jC+nwGaxp7Q7ymcVGF8fxGOXjGwjMBvFQeeTHFScuxpnCYeESTjjUk3KW53W3g+GscEBT2G2g5hKqNpYV/R0mBSIkJ3m0rqIlBmGfo2PHCZGgr4WZwGoBO7jgtALChAcr3oIy3TYYbcTM1oh0PkwQi6v6k8zeX2uZEYGMDfxwEA9L16Gly6Gly6GWyUXKg2tAmdOHkZGg4aCiEG3dlvAAZkw97K5yJGu9NDcGWbcKmuQQFCgrZwEd94CMbePhyUy2cAwwcGgMfeRhsDDJhAzi2M49SgLVhr4VRQCAagfOQj20OFw3VRyBh74RSAs/YNg+vKkUpQImCFgOlm1VcO9rWxWVco6VlFVT4jVpYYrWdiglRcUpBMg1QQNNcheaxioRLzOZdeTdvS3zcTI1oh8MkgYi6P3/vSkfQbkopaChoFTZGG8VDPdIwOkBa55HS4QThtguXTHUS9ShEvQZaWSgJdz52rrBETzQBthhMi6KS5epwOFHgNAJnsCGbQs5q1OczUR9I+ORddDLJ0BrwjYUBIDbc2cwmdj8WBE6hPsgg7zRy4iPrvHBn5zJmCBca30ok3OE4D0iUlBRXFmp3qZEOfpsqoNi7Udx1OVrdCFYV5ysoFc4NULFRUmECIdB5G45Pkm3ToGdPAtGOhUkCEVEXFfUTICsGq4Oe2OwyyCuNRnhoEK/NDUERBQOHWi9ASgMaFgjbyVAiSGkHX7umJAHR7szFjgWVSBSUilqpCJ9616Ry6JnOIe0H6NdjMzIpgTFA71QjMiaAOBXNBUjGawMNG2iILdzRwYqOrZyk4Ec7ijlRyDqDrPXKXWS0WKYYQJyCiAo3MSt7aaJo2FHsm7TdDelClQugRQEO4Z+mfA1AYdIyogkhSG5foRSkZw2cryFB+fM1thoqexKIdihMEoiIuiyFvAsThI0ug40uDauAeue3qych7JVw4ZKkOoBnBGkTwMKgdzoLKKDWy0IpG3s6XWipC0QK4/lV8e/COHyjHQzCZU57ZrLQOlw9yYpGjZdDPuchBQGsggTFAfwIl17VyOV9NOZ81Ogs0jrcBCz+rrQCfGWhIdgc+Mg7g+T8gjJqVYUThJPD/stsABca8ypWTntji1Y3Kr7OFPdXjhICCb+si/eI93xEKU4QwGU8iGFDnog6B5MEIur+JFfpCMqiFFBrgL7awUAhLx42BGkoFQ3TUa7NjeVADBQcfGXhqQB5p5F1Hhw0jLJIG0GN50quntM0Rr6wmk/TBGRnNZzTxSFK1ulwfoEoBIGHniaPGpWFyiso0fHSw1s4oIeXRa2Xh68sfGVbtLEVHHKBRt4qNASmXcu+lhQN6VE6HN6jy0wQYn0QaFZl7aejREMj3EdCS1N1IzY/RJquDUVL4VoH1ZBLDhPrTIUJIkS0w2CSQETdX/BppSPokH4mj57GQouCdRlssimsy9XASXKi8ZZYGORsODE4owJoBRQeOhtjYZSFiXon4vumFXsWVGHwU/woABH4Oo/e6Sw8DaSMg6fDPQ2UUvC0Q8YLksFEL3cCeCoId4AOFFI6gB/rzUD07qxo+FoApxJt1bLawwJIXkF5gE4JlHFlN+ybJi/HllWNvb/2xBT/MZEUaEBM4U5RL0hTAOFfIuEQJX/brGzUFBjnJBDtSJgkEFH35w2udAQdklEO4hQ25msh1iCwBg02VZw30La2Wzim34ODKA2jHLQ4bMhlYBGuQmSjR+ItFtcprNUfnSusEKQAiNKo31QDl1dQgQsb+tqGPRTaQhuHGj/X9LQ9/sRdAbkghWzOQ0M2Fa71H+shKLwvXwk8bdHLb4SgaSnO9raHRQBrw92LnQo3L3apWFDlkKY3FK6C2s6yCuOHFMKJ3bFJycUETaJehsK52J4FCtFySL4P5aWgPb/0fTpIadX+5IeIqhqTBCLq/lR1/6fOSThxt1YHqNEOtcrF9hNo29CbcF6BwEq4L4KBQ32QRoNNIW8NGgODrPUhoqN9EoDEZF6JNX9jDVsgXJGoIe+joTGFGp2FgiAQBa0E4hQ81fSUPdmwVwhEQWmBE41skEbTRm5NOYWnAygJ90eAAyC6TXtDJN6/A5wNVzNyomCVwEo0vMcvf6xQcQ6CBmDKmPwcnxAuTZPFlSt0t0ShFfaiE1Xs1Wl6cwKtNHQmA3jb5t+6OOlQLkVE1ae6vzmJiLo5EYFTOfTUjeipG9BDN8BTAXQ7x+RrJchJGg3RzsqB0/BUACeCrNPIOgOtAa0lNmk56juQcIWk4uReDTTvasjlPNToHDwAtTqPtBJoz0JHm6Vp5VrEpFSY/DgXLZ9qw/kG8a4MEQWtHHxtkdIWvgnCHKWdjXGlw6VaoSRcYtSGexwoAWAEolz5T8pVU1JTzgTowqix4kdaSAyA4t4LhYnRoiQxP6Hw+SgRSMrAprxtslSpNpqrGxHtYJgkEBF1USKCPCwsBLWmARmThcAhrSxqVb64mdrWW7euONm3wXqwMLAw6ONn0cPLIw0LTwmCIGrsFgfGo2mScnyibIkROh4c8vkUlHNRQmJglUFtbR6ptEXKi21oV2zkChqcj/X5NDblUgichov1OCiEcymc8pBJWRgt6JUqd2O8sEAlgLaAcQpaECYLgQ4nVZexKlGhf6X5w/12RVZIEArJgg73SFCCqOcEYW8BEK58lJiKIoATuJSGhQDOof1vZOucddwngWgHwySBiLq/4ONKR9Bu4VxUCf8IUKOz8JFFWgfoaXLo7eXa2BSUxNNtQbjyULizscAgnDic1jbayEw1LUOqY+3e5jNqYw1iJwpODAJn0JDzwg3URCMvBmIEeWgor1m0SiC+wBogrwwa4CMn4eZxBRbhUqqBKzTgHTxtkTblJgoCZcOhPNoqqOgPClsLlNPILw7HQlltc4mPq4oXq5tGdRXKd+ELknmhRL0XSkHEhfM6tsHkAdVsaVoi6v6YJBBR96d3r3QE7aYUYJSGBwOBxuf5PtgsaWx2HgIohL0DLYfwNCfS1LjzlEUvPwtfB/CUg0QJhG8sMjqAr11xbc3C2HgpbAIAtOy0KPYyCAJnYEUhsB58OCiEy7M25H1AC9JeUCxAFZboUeGT+AAKyjjknB+bhB2O4bGCaNdmBa0FaWPRw8+hnBa5UgrwJZw7oF24/4DtwB4JQFRH8SVJy4gpNnyo8JcU6qe4glF0wiE2aTm6XgnEC5M+2UY9CSId2cKOiKoRkwQi6v50utIRlM1A4d9BD3xle2C97YEG8fEv2wNrXQ+YNqxsVGhQGjj08cIdkH3VtJma1uF8BVGA0YXEIVZu1JCPj0AqjoCJNldTCsg5BU8FyAY+lBIYJciLRr2tgSgNP91887ewaa4VIMYhEI2s9cJeg4inAV85eBAETmNTkEGD9dHo/LKawYWGN4wAnkB0OLG6A2ugFldpatOor9boaAK1KSQFkqjoqBMlvGVh+dfivRUQDU9yDQ0wGxu32UR9Djci2rEwSSAi6tIUIBrrglp8ZXtgs0vj30EGGg5GtbE3AQiv1xa+CpDSeeRFIy9euDJS1OBMGRctqSqtTAyOluiMtaqVFngpi8Az+CLfE5tsCoFVyFoPOZuChUIADaeSxRR+8FIBlKcRwMAqhY25VGzlJokmaAs25jLIBRqNgYfGvB+1x9vRaI31UIgnYaM8MZ6n+XVtk5hnXWZ3QmFqSXFCtgLgJLbnQvR5KGlKGCR+3yiOlL+Nd1xmXwLRjoRJAhF1f1X8BFQpYDc/i146Bx9Ag00hEA+CaKnSNjTcFBSykkJDkELOeXCi4cMWx7kHIjA6HO9vCqv8FKYeFIeZhPcq9CAUV/40Dtpz0J5FVvnIhUsIwSgLQdOSqyK6KfFQYUmecfCNQJsAteksUp6FVSYZvAg25dPIWQ/iFBryftSYbjZGZ4skNp0ialUHgkLmkvjn0c52sMSGBKGdK04lbhmOICtOGleFmdvxkV6xoUkKsV6HKLGD9iC1GUC2njiWp3r/f0RE7cckgYi6v+CTSkfQIb4WDEptRj+ThYdwnoEVhcC1b7+AzdaHc+HrFARWFBqDFKzz0Rj4yDkDG+1qXHhMXVyGM76KT+ypuXM67HnQApMKoIyg0RmIKGQDH+KAoNDgjTcyw+WWoETB9yy0DpMPBxV7TwpOBE4UjLYQrdEznUPaBCikLm17/6pwu/A3pyCBAXIGolT4/sqZdBz/IT4BuZy2dGEYFBAu0RovS8KlaEUpiFZhL0h8hSMr0DmB1hrI5rfZcCMi2rHwvyRE1P2ZPSodQYf19xugIfial0MKDtaFjem2Ll1fWBTHwiAnPrLOh1ggZw3yziBrDTbnU9EqOYXH4yrRq9A03CikFCBWQZyGAfC1HhvhGUHeGTRaD4FTCEQjB40sTLgXQayowtAlowUBgEAUnFNwxYZ/2PCt8fOAAmq8ADUmjxovQEoF7Xr/hXgVAOSiVZxijfnyRgpJsYqal1ce1VTdhaFQUXITDo9qSkSUSPgNrhTgabiUAtbXQ+WDjgbRenTcJ4Foh8IkgYi6P52qdAQd5gTRqkQB+uoGaLRvSInRLpqYrIoPqDNeHuKAXKCRDTyUGmujEn/HhzcVhvBIuGpSugFp7ZAyFkpr1JocjDhYG/ZcNOY8NG/RG+VQk8rBUwJPA1qFPQnxeRZOFBQEGS+AVuHkZydA3ul2JQhF0cB/5QmQseWOEGoSzw062oYuvN4gXs1NQ4xc1ElQWB41thoSBDA5C+X74bFOHmLHJVCJdjxMEoiIqsBqm4FTDmuh4DxBLz8XjvtvU8vNwY8SBAcFJ4AVA60Uevg5ZEyAvDPhgqjRo+ymEf9NLV9VmCAbPd0OH2QrKAdoJ0hpi4yXh1KFnwOkVYBNm9NwLb5uFCRQ8ODgqXA+RMrk0dPPwiteGiYiWgE9vSwUBI2Bj0brw0mzuQttJIVhPUYALRAvnAhQdrJQeOLf2d+m8fkIEiYIurXJ5ACQC4cZKc8HTOd/tXMJVKIdD5MEIur+qnjiMgA0Oo0vbRp5aHzNb0BK5ZE2edSYfGwCb2tiE3xVNKQH4UZnAQx6+lloJejh5aBEWq74U2LztKZiw1V3nNPwlQPyGq4wDAoC5QSNjT5yea/kXFobGDhrIDmFtMkjbaKJ00UKWjnoaJWjWi8PJ0A28MrqRShMXBbjUNyF2osvFdQ+if0VOjIfIfa6wlyDwtCipo2go8/GhXeGNI0IAwDkHIxJQaVSgDadPjRIa9XxnhIiqipMEoio+6viicsCYIPTqFEWPU0e1vlw4qEhSKHR+m2YuBs+5rZOIaUt0sbCg4WBg4gg7xSMdqj1smHCUWwvF7oTVLERKyrcMaH5ap/5wMBZBS0C5zS0DjdTC6yGgoJYDQkKqzFF7ysqWylBOh1AKQUbzVGI53RahcOSjHKAAL62KEdik7bCJGwTtrRVmV+F0iK5KKMhXSqpiK1uVIxWKxS3kCgsj1pIGAD4ASA1aYgBEHT+vATnpLwEiIiqFpMEIur+zMBKR1A+ERgF9NABUnAQZ1ALF04MFtPmJ+pOFDxl4SkLY8K5AyIKDS4DUQobbQ2MceFa/MUyw+wg3Lgr7GJIzGGOHmUHzmBjYw02NaRgJICvAmgdJiVGLFIISiYzxgRQoqAE0BB4yiEvzb6Wol4J5wCHcBhTRx5pKwUoCZOXQtkdWjG00AvgOvagXYCm/Q9inT8tOsHiQ5CKL5bwjRkN8bzWxiV1iN4GZRJR18YkgYi6P+1XOoLyKaCXzqOXziPvDLyoUepc+1b2cdDQzsFH+EfB4qtcLTYFPvLQEBW2LT1TvC0KA2oK8xCaxsQ0tVwLE2QDG36d9Ek3QqtwJ+bGfAriDHr4AVyxId70WmOaxuiktEU4okU3X0wJWedhQ1ADK1403L79j7SblkAVKItwhaOsAnKmacXQthZbHIGloFz0J/7asoYuofiNXFzdqBB3s2MorHYUOya+gVMCEQvYzt8nwbn21zkRVTcmCUREXVShWWaUQwoBUhJgZ7MJfU0WtSZox1SL8MLNLoWcKDg4iAApbeFrgYFAKxvNb3CItYJjQ4RUcTlOpVRyArMSpL08UiaA0sUdDIpDl3RsPc94yCJNy60GTkFcsnUtABqsRqPzkReDvHjIOoNykoTisH+lIOlo1zKnOrzvmFIIl55SCKuu3BWXYgkAvELPTVQmAOUk/MIuldAU7h8EgGe2yaggpVWnr5hERF0bkwQi6v5cttIRlEXEIRCHrDgIcqgxOfTwsuif2oTd0/XY+sPdptanApC1Pqx4ENFIaYe0CZDSAaxT8CHR3gs62WAtNAyl2RCXiHNA39pN6Fu7Gb4JYK2BgoOnBT1TWaRNPmz8izQ1fIvvD/C8ANoIRGkEzZbZVAowSsNJ2FORjfZ0MLqMxmp8jwEFwI9WOFKSfL9tkZjY3ex4Oe3oZvOfAURLR5UorpDUxGNQCk4rOHGQhkZsix2XxQn3SSDawTBJIKLuz62qdATtJtFKQ3lxsOIAFQAqj7xYrAsM1gWpNgw3atrdQCAIoOAcEIgHB4WMCaBEYIBwiVSJRqqocIffYgdAVEh87m/hZ60AqxTy2iBnPHyZ6wEb9QhoJfC1RRAo6GZzEpQC8nkv7E0otK4LqyI1rwsoaAN4JuztMCrW29FGhaFGgihfKCx/WuOSm7y1hcR/lHBfg0KjviMbL0jsr1hCI9Gmaa7EtUWehmRSgN5GX+sKbUhKiag7YZJARN2ft1elI2g3pRSM1vCip+v1gY/1gcGXQQaN4kGrcFnQtii29QXRhmpAYBWU2HClHHHQ4qAL659KfGBQcfAQgGgwUnSZc4A2DoEy2GhTaBQfOfHwZbYXclaHm6hlUwgCL5yg3Hw4kWhksz5yjQbiAKPCJVDjyY+T8LpCh4ZWgG/KW+Go8F6cVRAxEKMhWoUrArWnAaySPxc7X5Qkl0VtK4emUV4qltpFRRXmJav4N3azeEVFic+22s9Atsl8aCLqwpgkEBF1YQYa9bYGX7leCFQaQXGScbg1WluHiUu00KdRLloK1aHRpgClkXceGm3YwG+6PvrfQq+BNHvMXWi0awetHQKnkHeAcg61JhsNH9IwvoNvgqi13zwqh5wo5I2K5ho0TyIAK9FE5qjHQRBO2m7/4P8o2QkAyRs4CVd8coGCKFVWD4AUlmuN/kipt9ieCCXcNC1RxxJ+UQvQNOch+tNs8BZUPgDSHrYVzkkg2rEwSSAi6sI0NNbbGqy3tVhna5FFCutsDdbZmjb+B7ypQa3h4JyChwBWDHLWQ9Z6UMoh53Q0vCWaw9C0pBHi/xuOqol+1kAQeMjlPHgQ9JAAaTj08xsQiAcvY+FSQE3PADU12VieETautS+w8JATjYbAx+bAb9HK1iq+LqjACcKlWsuZvCwIH/s7gTgFlw/floqGCqGdo46UqGhJ1aiWRbV/5+ZwZncsGUNTMhDvzLGFKpDCyKymHpeot0msAzZl0bHFWLcQKuckEO1QmCQQUffn6isdQdmUAmoVsNmmsdFmkHU+vrJpOAh8Y2GUbWNvgoJnHNLGQStBzmo0Wg+NNkwWnBSWVI1mMEghNWia+KwKQ2BU01r+IoALFDII4ItFLbJwUGhwHvJOI9Aa6VQeNZkAvm/DeQeFslXY+Nca8FR4z5xr+loKJy4DBuFqROEyqwrJFnTbaS3hngxWw1gFAwUVKCjbNISqXVSYGCDqAVCFHoVy+hMEUMVEQIWb0JkoEYiSgmISoZpeU+TpMGtTzU90HvYkEO1YmCQQUfdX831A71npKMr2//xG9DIWGhobXQ2yEg4TchLuYtzWB7wZnQ/nMSiNjJdH2gvgGwdjBC7axCxsLIcTiAtDaAC0WOmo2L+gABf1QogA4gOetsjoABljUWsa4ZyC1ha+F6AwhKnQO+AU0JD1ABculWrhtZggqxTCjd9sCtaFcx3KEw7RUiLQzkE7gQoEyirojjx9l1gDvgNlqEICFh9SVOhV8KJxRxqxYUexGyoF5xsgte2GGxHRjoVJAhF1e7r2NOj/9wJU/4VQva8DUkchXJKmOtQoiz46hxoVQKyCdRp5Z5B3uti43zpBYVS/hkApiSUKATxtURwEDzTtuhzLQETFVhKNNVCVEmySFIKURh4eNuYz4bwHZVEDi3zeoDGbRjbnF5+CK6UQRA1+Zw02ZtNQyoUTl5vFLQI0Wh+B08iLRt6V26TXQApQaYFKRb0K0jRBuF2aJwQ6msCsy5u8XEwM4oW7puOFnh2JRl6pWKIWXitQ1gGZ9DbqR+BwI6IdDZMEItphKLMLVO0k6J0egNrlbah+vwNqzwLMHpUObYuUAvqaLJwz6GfyyMAisGGC0J52m2cAKwpaCZwYGO2Q8fJIGYs+6Ww0iCc+AwFNy3I2b5SqpiH86UwAqxWsUdhsU1gX1ELDQjmLIGtQo3LI5Qxs1AMgkDDJUECQ1xCrYfMGmxoyMHDFeRESrbK0OQh7ECBAPjDFBnM5VJgZAFYB+cLyQeUUlPy5qXraH1u0nxzExYqIfhYVDmXSAkBHE5vjAUQ9PCrvoEVBbc42Wwapc3i+gdpWy6sSUZfEfkki2iEplQLS34FKfwcA4PLvQUlD4hoRB0geSqebHW+AUjXNjuWhlJ88BgvVrMdCJIBSsVWEXAAok3hKK+KgYg09EYedAo3x/dPIOoMzxGB9XpAyTfcLnIMXNeKsOORsgBovFb43EVgXoNbT4R4DkoVnfAgUcjaAIIPAaWQDoPC1YJ3ARBNi89bC116sLAdfm+heFilPRY3vALVe2BugEcA5gRucRt552Jg1MCp8zeZcHjUpLxyqBIGoPHqkFDxtobEZtb6J3rcgJ4CVFKwz2BQ4QDzo6N7ZIEDKeMX3D6BYB402j0xUPzkbwNMaOqrThlwONSoDsUA2Z5HxPTTmA2R8D04E2SBAje9jUy6HWt+HUgrZwCLthffNBxZ+4Wdr4XkGUEDOWaRM03G/+LODb3SLn3OBRSoqJwgsPG2gpOlnALD5qBwBnBUYKDgrcIGD7xnYwBbnPrvNWRgnCDY3wvgebGDhp3xYawEJG/r5fADf95BtyCFdkwrv5RkEgS0OeRIItNaw1sEYg3w2j/4Dd0Yqnfz3TUTdG5MEIiIA2t+/xbHWngmXOl7usbZe0ztd4kIiIqJthH2HRERERESUwCSBiIiIiIgSmCQQEREREVECkwQiIiIiIkpgkkBERERERAlMEoiIiIiIKIFJAhERERERJTBJICIiIiKiBCYJRERERESU0K2ShH//+9/Ya6+9Kh0GEREREVFVq6ok4fnnn4eItHreWovly5dvx4iIiIiIiLqfqkoSjj32WOy111645pprsGLFik4rN5vNYsOGDYk/REREREQ7qqpKEt5//32cddZZeOCBBzBkyBAce+yxeOyxx5DP5ztU7vXXX48+ffoU/wwcOLCTIiYiIiIiqj5KtjR+p4tyzuGZZ57BAw88gGeffRZ9+vTBWWedhQkTJmDcuHFwzrWrvGw2i2w2W/x9w4YNGDhwINavX4/evXt3dvhERERERBWxYcMG9OnTZ6vt3KpMEuLWrFmDP/zhD3jwwQfxz3/+EyLS7iShubZWHhERERFRNWlrO7eqhhuVMmDAAFx++eX44IMPMH/+fPzkJz+pdEhERERERFWt6nsStgX2JBARERFRd7TD9CQQEREREVHnYpJAREREREQJXqUD6IoKI7C4XwIRERERdSeF9u3WZhwwSSihvr4eALhfAhERERF1S/X19ejTp0+r5zlxuQTnHFatWoVevXpBKVXpcAhNe1esXLmSk8m7CX6m3RM/1+6Jn2v3w8+0e2rL5yoiqK+vx2677QatW595wJ6EErTW2GOPPSodBpXQu3dv/sesm+Fn2j3xc+2e+Ll2P/xMu6etfa5b6kEo4MRlIiIiIiJKYJJAREREREQJTBKoKqTTaVx99dVIp9OVDoU6CT/T7omfa/fEz7X74WfaPXXm58qJy0RERERElMCeBCIiIiIiSmCSQERERERECUwSiIiIiIgogUkCERERERElMEmgqvLvf/8bU6dOrXQY1Mn4uXY//Ey7n3w+j9WrV2P16tXI5/OVDoc6QGuNiy++uNXzxx9/PK6//vrtGBF1lk2bNmH27Nn49NNPO1wWkwSqKv/+979x4403VjoM6mT8XLsffqbdx2OPPYbDDz8ctbW12H333bH77rujtrYWhx9+OB599NFKh0dlUEphxowZmDJlSsnzZ5xxBp588sntHBV1huXLl+PUU0/FiBEjMHPmzA6VxSSBiIiISrrnnntw7rnn4rDDDsOsWbOwePFiLF68GLNmzcJhhx2Gc845B3fffXelw6QyzJkzB7Nnzy6ZKIwYMQLvvfdeBaKizmCMwQsvvIDrr78e55xzDjZu3FhWOV4nx0VERETdxE033YT77rsPZ599dotzJ554IoYPH46rr74aP/nJTyoQHXXEvvvuiwULFmDs2LGw1uKee+6BUgoAEAQBampqKhwhlUtEcNBBB+G1117D1KlTMXz4cPzpT3/CoYce2q5y2JNAREREJX3++ecYOXJkq+dHjhyJNWvWbMeIqDMNGTIECxcuxNy5czFhwgR89NFH+Ne//oWpU6fiiCOOqHR41EG+7+OWW27Bvffei0mTJuGGG25o1+vZk0BEREQlfetb38Jtt92G++67r/iUuUBEcNttt2Ho0KEVio46w5577om//e1vOP3007HffvtBRLDnnnti3rx5lQ6N2qGurg4igk2bNsFai7FjxybO9+nTB1deeSWuuOKKNpfJJIGIiIhKuvPOOzF+/Hg888wzOProo7HLLrsACHsYXnjhBWzevBlz586tcJTUXtOmTUOfPn2Kv++6665YsGABlixZgrVr12L48OEcblRlvvWtbwEIF414/fXXMWzYsBbXNE8ctkaJiHRGcETbwwcffID9998fzrlKh0KdiJ9r98PPtPv48ssvMX36dCxatKg4tGjAgAEYNWoUzjvvPOy8884VjpCICpYuXYqhQ4cil8t1uCz2JFDVad7lTd0DP9fuh59p99C/f3/813/9V6XDIKI2OOCAA5DNZjulLE5cpqoyZMgQLF26tNJhUCfba6+9+Ll2M/z/KhFRZXTWAxomCVQ16uvr8fLLL+P111/Ho48+ildffRXW2kqHRR306aefYuLEiXjuuecqHQp10B/+8AesWrUKAOB5Hvbdd9/E+blz52L58uWVCI2IiNqJcxKoy7PW4rLLLsN9992X6EJTSmG33XbD7bffju9///sVjJA6YsmSJRg6dChGjRqFnj17Ytq0acXJkVRdjDEYMmQI5s+fj913373F+cmTJ0Mphd/97ncViI6IiNqDPQnU5U2dOhWzZs3CPffcg5dffhkPPPAABg0ahAcffBDXXXcdLrjgAjz99NOVDpM6aP78+Rg9ejRGjBiBZ599ttLhUJn22msv1NXVFXsU4s4880y89NJLFYiKiIjaiz0J1OXtuuuueOihhzBu3LjisT//+c+49tpr8cYbb+CBBx7Agw8+iP/7v/+rYJRUrkJPQj6fBwC8+eab+OEPf4hx48bhpptuQiqVqnCE1FbGGKxcuRI///nP8eqrr+Kll15K9CgsW7YMBx54IDZv3lzBKImIqC3Yk0BdXkNDAwYNGpQ4NmjQoOJSfHV1dfj73/9eidCoA5YvX47ly5cXnzgvW7YMy5cvx0477YRZs2bho48+2uJOr9Q1GWPwxz/+EUcccQTGjBmDjz76qHjunXfeKTkMiYiIuh4ugUpd3mGHHYabbroJ9957L3zfRy6Xwy233FLcOCSfzyOTyVQ2SGq3IUOGQESglIKIYMiQIYnzheNUPQqfl1IK06ZNw4UXXogRI0ZgypQp6Nu3L2655RZcdNFFFY6SiIjagkkCdXm333476urqMHDgQAwePBgff/wxRATz588HED6dHDNmTEVjpPZ78803AQAff/wxJk6ciNdee63CEVFHNV927/7778dhhx2G++67D2vXrsXZZ5+Nq666qkLRERFRe3BOAlWF9evX4/HHH8fy5cux55574gc/+AH69u1b6bCoE6xatQpXXXUVpk2bVulQiIiIKMIkgYiIiIiIEjhxmYiIiIiIEpgkEBERERFRApMEIiIiIiJKYJJAREREREQJTBKIiIiIiCiBSQIRERERESUwSSAiou3i/fffx4ABA1BfXw8AmD59+jbZ7+T000/Hrbfe2unlEhHtSJgkEBFRm8yfPx9KqVb/jB07douvnzp1Ki655BL06tVrm8b5i1/8Ar/+9a+xfv36bXofIqLujEkCERG1yRFHHIHVq1e3+HP//fdDKYWLL7641deuWLECTz31FM4999xtHudBBx2EvffeG3/605+2+b2IiLorJglERNQmqVQKAwYMSPz56quvcNlll+HKK6/ED37wg1Zf+9hjj2Ho0KHYfffdW73myy+/xIgRI3DKKacgm80Wey7mzp2LYcOGoaamBnV1dfjiiy/w7LPP4oADDkDv3r1x5plnYvPmzYmyJkyYgJkzZ3baeyci2tEwSSAiorKsW7cOJ510EsaMGYPrrrtui9cuXLgQI0aMaPX8ypUrMXr0aBx00EF44oknkE6ni+euueYa3HXXXVi0aBFWrlyJiRMn4vbbb8cjjzyCp59+GvPmzcOdd96ZKG/kyJF45ZVXkM1mO/YmiYh2UEwSiIio3ZxzOPPMM+F5Hh5++GEopbZ4/fLly7HbbruVPPf+++/jyCOPxPjx4zFt2jQYYxLnf/WrX+HII4/EsGHDMHnyZCxYsAD33nsvhg0bhtGjR+O0007DX//618RrdtttN+RyOaxZs6Zjb5SIaAfFJIGIiNrtyiuvxOLFizF79uw2TURuaGhAJpMpeXz06NE49dRTcccdd5RMNg4++ODiz7vssgtqa2sxZMiQxLEvvvgi8ZqamhoAaDEMiYiI2oZJAhERtcvMmTNxyy23YObMmdhnn33a9Jqdd94ZX331VYvj6XQaxxxzDJ566il89tlnJV/r+37xZ6VU4vfCMedc4tjatWsBAP37929TfERElMQkgYiI2uytt97C5MmTccMNN2D8+PFtft2wYcOwZMmSFse11vjjH/+I4cOHY+zYsVi1alWnxPnOO+9gjz32wM4779wp5RER7WiYJBARUZv861//wsknn4wxY8bghz/8IdasWZP48+WXX7b62vHjx2Px4sWw1rY4Z4zBww8/jKFDh6Kurq5T5hEsXLgQ3/3udztcDhHRjsqrdABERFQdnn76aSxfvhzLly/Hrrvu2uL8oEGDsGzZspKvPe644+B5Hl544YWSPRCe52HGjBmYNGkS6urqMH/+/LLjbGxsxJ///Gc899xzZZdBRLSjUyIilQ6CiIi6v7vvvhtz5szB3Llzt+l97r33Xjz55JOYN2/eNr0PEVF3xp4EIiLaLi688EKsW7cO9fX1bVoRqVy+77fYN4GIiNqHPQlERERERJTAictERERERJTAJIGIiIiIiBKYJBARERERUQKTBCIiIiIiSmCSQERERERECUwSiIiIiIgogUkCERERERElMEkgIiIiIqIEJglERERERJTAJIGIiIiIiBKYJBARERERUQKTBCIiIiIiSmCSQERERERECUwSiIiIiIgogUkCERERERElMEkgIiIiIqIEJglERERERJTAJIGIiIiIiBKYJBARERERUQKTBCIiIiIiSmCSQERERERECUwSiIiIiIgogUkCERERERElMEkgIiIiIqIEJglERERERJTAJIGIiIiIiBKYJBARERERUQKTBCIiIiIiSmCSQERERERECUwSiIiIiIgogUkCERERERElMEkgIiIiIqIEJglERERERJTAJIGIiIiIiBKYJBARERERUQKTBCIiIiIiSmCSQERERERECUwSiIiIiIgogUkCERERERElMEkgIiIiIqIEJglERERERJTAJIGIiIiIiBKYJBARERERUQKTBCIiIiIiSmCSQERERERECUwSiIiIiIgogUkCERERERElMEkgIiIiIqIEJglERERERJTAJIGIiIiIiBKYJBARERERUQKTBCIiIiIiSmCSQERERERECV6lA+iqGhsbkcvlKh0GEREREVGnSqVSyGQyW7yGSUIJjY2N6FPTDzk0VjoUIiIiIqJONWDAAHzyySdbTBSYJJSQy+WQQyO+jePhwQdU06gspRViv5Q4Fvu5cDx2TGndyrW65bH462IxlCp362WhlWtLlVXiXolr2v5+JXG+RAytvIeSr2vl/RSvbeU9SqnXxc+XiEtaq69S99UljrW4tsR9Y7coXqvjcZc431pZHbw2WQclXt+sjC3fq7XXb+3ardVdJ94XbThfqJtWri2eb8u92hFjm8st9V62VG4b42pPPUNJy/OtlLv1+8bKahH1lsqSlte0+n6k3WWp9twLJc4DUKrlO1KlyorfL3ZeJX7eQizNr93S65v9rKNr1Fbuq1s5r7G1+8auLXEv3VpZ0XHd2rWlym16WyXPt/p6lLrWlbxWoeXr4teaEuUmy2qK0STer2txr2RZLeMxJY6Fr3MtYjWJGGLxljwmLa4tdf/mMZjita3cN15GidcnY3DR60vXQalyW7uvKfGZtXZelahbU+LfR/x48jyafi58/k2HYBKfv4pdWzgfP7bln5PX6hLnWx7bUO8waPgy5HI5Jgnl8uDDU82ShBKN6VLHohPJv9GssV/qdaUa3c1fV1aS0Oo3Q+uxNIthaw377ZsklLi2VPnNyyrVaK5UklCqIVMFSUL7Gv7tuXZrdbdt7tstkoS2lNvBuLZrklAi7m6RJJQ61uJ4y3uVaqy3/vq2N9a3WZKwtWu3cZJQqrGfON8JScLWrt16ktCykRm/pjOThFYb6yUayFtLElq9V5lJgim+h6Z/jDr2D7NwPFkHsfMlrk3eN36+ZT0mG+stP1ODLZ+PH28tSTCdmCQkr1Ulrt1aktC+qcicuExERERERAlMEoiIiIiIKIFJAhERERERJTBJICIiIiKiBCYJRERERESUwCSBiIiIiIgSmCQQEREREVECkwQiIiIiIkpgkkBERERERAlMEoiIiIiIKIFJAhERERERJTBJICIiIiKiBCYJRERERESUwCSBiIiIiIgSmCQQEREREVECkwQiIiIiIkpgkkBERERERAlMEoiIiIiIKIFJAhERERERJTBJICIiIiKiBCYJRERERESUwCSBiIiIiIgSmCQQEREREVECkwQiIiIiIkpgkkBERERERAlMEoiIiIiIKIFJAhERERERJTBJICIiIiKiBCYJRERERESUwCSBiIiIiIgSmCQQEREREVGCV+kAurIAeUCAeC6lRMWu0CWOxX4uHm86pkS3cq1ueczFXqdKvE7FrlUt40qeRyvXliqrlRhVy/eTeJ20LEsS50vE0Mp7KPm6Vt5P8VpXovzmZakS5xNvVyXeSpvuq0sca3FtifvGblG8VsfjLnG+tbI6eG2r/yxb/XezpXu19vqtXbu1uuvE+6IN5wt108q1xfNtuVc7YmxzuaXey5bKbWNc7alnKGl5vpVyt37fWFktot5SWdLymlbfj7S7LNWee6HEeQBKtXxHqlRZ8fvFzqvEz1uIpfm1W3p9s58lukZt5b7SynlBy/u6Vq7VJe6l4z+j5et0a9eWKrfpbZU83+rrUepaV/JahZavi19rSpSbLKspRpN4v67FvZJltYzHlDgWvs61iNUkYojFW/KYtLi21P2bx2CK17Zy33gZJV6fjMFFry9dB6XKbe2+psRn1tp5VaJuTYl/H/HjyfNo+rnw+Tcdgkl8/ip2beF8/NiWf05eixLnW8ayoT7eYGodk4QSUqkUBgwYgJfXPBMeiP/33VYkJCIiIiKiTjFgwACkUqktXqNEpORDmx1dY2MjcrlcpcMoacOGDRg4cCBWrlyJ3r17VzqcqsF6Kx/rrjyst/Kx7srDeisf6648rLfyVbLuUqkUMpnMFq9hT0IrMpnMViuv0nr37s3/Q5aB9VY+1l15WG/lY92Vh/VWPtZdeVhv5euqdceJy0RERERElMAkgYiIiIiIEpgkVKF0Oo2rr74a6XS60qFUFdZb+Vh35WG9lY91Vx7WW/lYd+VhvZWvq9cdJy4TEREREVECexKIiIiIiCiBSQIRERERESUwSSAiIiIiogQmCURERERElMAkocosWLAAmzZtavX8c889h4ULF27HiKoH6648WmtcfPHFrZ4//vjjcf3112/HiKoD6618rLvysN7Kx7orz/nnn48rr7yy1fNPP/007rvvvu0YUfWohrpjklBlxo4di48++qjV84sWLcLNN9+8HSOqHqy78iilMGPGDEyZMqXk+TPOOANPPvnkdo6q62O9lY91Vx7WW/lYd+V56aWXcNxxxxV/D4IA77zzTvF3YwzuvffeSoTW5VVD3TFJqDJKKWxp1drhw4fj9ddf344RVQ/WXfnmzJmD2bNnl/wCHTFiBN57770KRNX1sd7Kx7orD+utfKy79lu9ejX23HPP4u8rVqzAqFGjir/vs88+WLZsWQUi6/qqoe68it6dynLIIYe0em5rDeEdHeuuPPvuuy8WLFiAsWPHwlqLe+65B0opAOHTj5qamgpH2DWx3srHuisP6618rLv269OnD+rr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cgpfAtdcc03J8/wS2LLCF8C4ceOwYsUK3HjjjejRo0eX+ALoqn784x8DANatWwfnXPH3gr333htnnnkmTj/99EqE12Xtvvvu+OCDD7DPPvsUj3300Ufo378/AGC33XbD2rVrKxVel9a/f398+OGHGDx4MICw3qy1+PjjjzFkyBB88MEH2HnnnSsbZBd0880346STTsLzzz+PcePGYZdddgEAfP7555g7dy7eeOMN/PnPf65skFVg2LBhWLBgAU4++WRMnz4dvu9Da4158+ZVNC4mCVXmuuuuw4YNG3DaaafBOQelFM455xxcd911AMLJV7/85S8rHGXXdMMNN+D73/8+vv3tb0NEcN555+H+++/HlClTcNNNN2HkyJG4+eabKx1ml8MvgY7rql8AXdX69esBoLiqUeH3gp122gknn3zy9g6ryzv//PMxefJkTJ06tfgQ6de//jUuvfRSAGGvYKERTEknnngiLr74Ylx77bVwzuHKK6/ERRddhBNOOAHHH388ZsyYgUmTJlU6zC5n/PjxeOONN3DXXXdh9uzZxZEMAwYMwKhRo/Dggw/iwAMPrHCUXdPVV1+Nnj17Fn//xje+gXfffRcvvPAC1q5di6OPPhq77rprBSPkPglVK5vN4tNPP8Uee+yBdDpd6XCqypo1a6CUKjZ2aeuWLFmCu+66C4sXL27xJTBlyhR+CZRw7bXX4tJLLy0OBQTCJT270hdAV7d06VIMHTqUK7a1kYjgxhtvxF133YXVq1djwIABuOiii3DVVVdBa41//OMf+Oyzz3DsscdWOtQuZ8OGDbjoooswZ84cAMAVV1yBX/ziF7jxxhvx9NNPY+TIkfj1r3/N71vaoTBJICKiLikIAqxcuRJ77bVXpUOpOkEQwPM4WKC9RARKqUqHQdQlMEkgIiIiIqIErm5EREREREQJTBKIiIiIiCiBSQIRERERESUwSSAiIiIiogQmCUREO7jBgwfj9ttv77Llxb3//vsYMGAA6uvrAQDTp09H3759t8m92iqXy2Hw4MF47bXXKhoHEVFnYpJARFSlJkyY0Oqa9wsXLoRSCn//+9+3c1TAq6++igsuuKD4u1Kq0zbcmzp1Ki655BL06tWrU8rbmh//+Mf4xS9+scVrUqkULrvsMlx++eXbJSYiou2BSQIRUZWaPHkynn/+eXz66actzk2bNg0jRozAwQcfvN3j6t+/P2prazu93BUrVuCpp57Cueee2+lll2KtxVNPPYUTTzxxq9eeddZZePnll/Huu+9uh8iIiLY9JglERFXqhBNOQP/+/TF9+vTE8Y0bN+Lxxx/H5MmTAQAvv/wyRo8ejZqaGgwcOBA//elPsWnTplbLXbFiBU466ST07NkTvXv3xsSJE/H5558nrvnLX/6CQw89FJlMBjvvvDNOOeWU4rn4cKPBgwcDAE455RQopTB48GAsW7YMWusWw3Nuv/12DBo0CM65knE99thjGDp0KHbfffdWY//yyy8xYsQInHLKKchms5g/fz6UUpg7dy6GDRuGmpoa1NXV4YsvvsCzzz6LAw44AL1798aZZ56JzZs3J8patGgRfN/HoYceilwuhylTpmDXXXdFJpPBoEGDcP311xev7devH4488kjMnDmz1diIiKoJkwQioirleR7OPvtsTJ8+HfF9MR9//HFYa3HGGWfgn//8J4499lh8//vfx9///nc8+uijePnllzFlypSSZTrncNJJJ2Ht2rVYsGABnn/+eXz88ceYNGlS8Zqnn34ap5xyCo4//ni8+eabePHFFzFy5MiS5b366qsAwp6N1atX49VXX8XgwYNxzDHHYNq0aYlrp02bhnPPPRdal/5qWrhwIUaMGNFqfaxcuRKjR4/GQQcdhCeeeALpdLp47pprrsFdd92FRYsWYeXKlZg4cSJuv/12PPLII3j66acxb9483HnnnYny5syZgwkTJkAphd/+9reYM2cOHnvsMbz//vt4+OGHiwlQwciRI7Fw4cJW4yMiqipCRERVa+nSpQJA/vrXvxaPjR49Wn74wx+KiMjkyZPlggsuSLxm4cKForWWhoYGEREZNGiQ3HbbbSIiMm/ePDHGyIoVK4rXv/vuuwJAXnnlFRERGTVqlJx11lmtxhQvT0QEgDz55JOJax599FHp16+fNDY2iojI66+/Lkop+eSTT1otd+jQoXLttdcmjk2bNk369Okj7733ngwcOFB++tOfinOueP6vf/2rAJAXXniheOz6668XAPLPf/6zeOzCCy+U8ePHJ8reZ5995KmnnhIRkUsuuUTq6uoSZTd3xx13yODBg1s9T0RUTdiTQERUxfbff38cccQR+P3vfw8A+Oijj7Bw4cLiUKO3334b06dPR8+ePYt/xo8fD+ccPvnkkxblLV26FAMHDsTAgQOLxw488ED07dsXS5cuBQC89dZbOProozsU98knnwxjDJ588kkA4SpFY8eObfF0Pq6hoQGZTKbk8dGjR+PUU0/FHXfcAaVUi2viczN22WUX1NbWYsiQIYljX3zxRfH3pUuXYtWqVcX3ee655+Ktt97Cfvvth5/+9KeYN29ei3vU1NS0GLJERFStmCQQEVW5yZMnY9asWaivr8e0adOw995746ijjgIQzk+48MIL8dZbbxX/vP322/jwww+x9957l3W/mpqaDsecSqVw9tlnY9q0acjlcnjkkUdw3nnnbfE1O++8M7766qsWx9PpNI455hg89dRT+Oyzz0q+1vf94s9KqcTvhWPxuRBz5szBuHHjiknJIYccgk8++QTXXXcdGhoaMHHiRJx22mmJMtauXYv+/ftv+Y0TEVUJJglERFVu4sSJ0FrjkUcewUMPPYTzzjuv+DT9kEMOwZIlS/D1r3+9xZ9UKtWirAMOOAArV67EypUri8eWLFmCdevW4cADDwQQPpV/8cUX2xyf7/uw1rY4fv755+OFF17APffcgyAIcOqpp26xnGHDhmHJkiUtjmut8cc//hHDhw/H2LFjsWrVqjbH1prZs2fjpJNOShzr3bs3Jk2ahAceeACPPvooZs2ahbVr1xbPv/POOxg2bFiH701E1BUwSSAiqnI9e/bEpEmTMHXqVKxevTqxROjll1+ORYsWYcqUKXjrrbfw4YcfYvbs2a1OXD7mmGPwzW9+E2eddRbeeOMNvPLKKzj77LNx1FFHFScNX3311ZgxYwauvvpqLF26FP/4xz9w4403thrf4MGD8eKLL2LNmjWJnoADDjgAhx9+OC6//HKcccYZW+2hGD9+PBYvXlwy4TDG4OGHH8bQoUNRV1eHNWvWbLGsLfniiy/w2muv4YQTTige+81vfoMZM2bgvffewwcffIDHH38cAwYMSGzktnDhQnz3u98t+75ERF0JkwQiom5g8uTJ+OqrrzB+/HjstttuxeMHH3wwFixYgA8++ACjR4/GsGHD8Mtf/jJxTZxSCrNnz0a/fv3wne98B8cccwyGDBmCRx99tHjNmDFj8Pjjj2POnDn41re+hbq6Orzyyiutxnbrrbfi+eefx8CBA1s8aZ88eTJyudxWhxoBwHHHHQfP8/DCCy+UPO95HmbMmIFvfOMbxWVOy/GXv/wFI0eOxM4771w81qtXL9x0000YMWIEDj30UCxbtgzPPPNMcSWmxYsXY/369S2GIBERVSslEls3j4iIaDu67rrr8Pjjj7d5Z+i7774bc+bMwdy5c7dZTCeeeCK+/e1v4+c//3mbXzNp0iQMHToUV1555TaLi4hoe/IqHQAREe14Nm7ciGXLluGuu+7Cr371qza/7sILL8S6detQX1+PXr16bZPYvv3tb+OMM85o8/W5XA7f/OY38bOf/WybxENEVAnsSSAiou3u3HPPxYwZM3DyySfjkUcegTGm0iEREVEMkwQiIiIiIkrgxGUiIiIiIkpgkkBERERERAlMEoiIiIiIKIFJAhERERERJTBJICIiIiKiBCYJRERERESUwCSBiIiIiIgSmCQQEREREVECkwQiIiIiIkr4/13oZ7kuJyx8AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spyro.plots.plot_model(Wave_obj, filename=\"model_overthrust.png\", show=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "From the plot above, we can notice two things. First, it is rotated by 90 degrees. In cases like this, the saved PNG image will be in the correct orientation. However, the plotted image in this notebook is rotated because the velocity model we used, commonly with segy files, has the Z-axis before the X-axis in data input.\n", + "\n", + "Another observation is that, unlike in the **simple forward tutorial**, we looked at the experiment layout after running the forward solve. For memory purposes, a 2D or 3D velocity file is only actually interpolated into our domain when it is necessary for another method of the Wave object class. If you need to force the interpolation sooner, call the _get_initial_velocity_model() method.\n", + "\n", + "It is also important to note that even though receivers look like a line, they are actually located in points, which can be visible by zooming into the image, not coinciding with nodes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In seismic imagining, the shot record is one of the most important outputs of the forward problem. It represents the pressure values recorded at every receiver and timestep. This is the data that we will use for inversion. Shot record data is saved inside the wave object as the receivers_output attribute" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "shot_record = Wave_obj.receivers_output" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us have a look at our shot record. For a better image, we will use 10% of the maximum value of the shot record as the maximum in our contour plot. The maximum value usually represents the direct wave, and we want more significant emphasis on the reflected waves." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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ldfPP//c8TyrVq3ChAkT0L9/fxQUFKBfv3740Y9+hFdffVXgnYbLli1bMH/+fEyYMAEVFRXo2rUrCgoK0L9/f5x99tl6bKgTQdvHoGVaBoLko4j2E0hG3V61ahVmzpyJH/7whxg6dCh69eqFzp07o1evXhg9ejRuv/12bN261fEcUraR0a8IEz/19fUslUrpa+QUFRWxgoIC/fvw4cPZ1q1b405mrMyfP58BYLm5uaxPnz62f6+//rrl8YsWLWJFRUV6nnbr1o3l5ubq3y+55BLW1tYW8V2JZ9myZbZrL82YMcP1+JUrV7JevXrpxxQXF7O8vDz9++mnn85aWlpsj09SPgfJS23Nys6dOzuW17q6OttzPProoxl5X1paynJycjw9Txkw3gMAVlhYyLp27Zrx2xlnnMF2795teXzQ9jFomZaFIPkYtP1kLDl1e+rUqR3ysaSkJOO3srIy9vbbb1seL2sbmXXiad++feyoo45iAFi/fv3Yyy+/zBhjrLW1lT399NP6Qx03blzMKY0XrfIPGjTI87FffPGF3siMHj2affrpp4wxxpqamtj06dP1Qnv33XcLTnX0LFu2jPXo0YOdfPLJrLq6mv3xj39kffv25epst2/fru87dOhQtmLFCsYYYy0tLezBBx9knTt3ZgDYFVdcYXl80vI5SF5q4mns2LG+rv3222+zTp06MQDs7LPPZhs2bGCMMdbY2Mguv/xyPS+feeYZX+ePEgBs5MiRbN68eRmLp9bV1bGf/exn+r1MmDChw7FB28egZVomguRjkPaTsWTV7ccff5zNnj2bvfPOO2zbtm36701NTezxxx9n5eXlDADr3bs32759e8axMreRWSeeHnvsMT3DrJTuH/7wB337K6+8EkMK5SBI5Z8wYQIDwPr27ZtRWTR+/vOf628Aqlv49u/f3+G3QYMGcXX4t9xyCwPAunTpwr744osO2++44w4GgHXq1Emv9EaSls9B8jKoeDrxxBMZAHbUUUexvXv3dth++umnMwAslUpZplMmXn31VcftRjG4fv36jG1B28egZVomguRjUPGUtLrtxJIlS/R8fPLJJzO2ydxGZp14GjNmDAPATjrpJMvtbW1tbPDgwQwAu/jiiyNOnTz4rfy7du1iXbp0YQDYzJkzLfepq6vTK8vvfvc7AamVC94Of+DAgbrZ2IqmpiZWXFzM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Y/+PP/4YP//5z3HYYYehqKgIxcXFOProo3HzzTejsbHR8Vq7d+/Gvffei7Fjx6KsrAz5+fno378/xo4di3vuuQfffPON5XH19fW4+uqrUVlZieLiYhQVFWHo0KG46qqrsH79estj7ALGzzrrLOTk5ODHP/6xY1rXrl2rH//GG2902N7Q0IBbbrkFw4cPR2lpKQoLC3HIIYfgZz/7GWpra7nS9I9//AMXXngh+vfvj86dO6OqqsoxTUZSqRRycnKwYMEC7N27F7Nnz8awYcPQtWtXlJaW4vvf/z5eeukly2Pr6+v1dNTX13Ndw+n4devWYfLkyRg4cCAKCwtx6KGH4pZbbsHu3bv1Yz7++GNMmDABAwYMQGFhIQ477DDcdttt2Ldvn+u97t27F3fddReOPvpodO3aFT169MCpp56Kv/3tb67H+imvNTU1yMnJ0Z/Hc889h9NOOw29e/dGbm4uBbETasMIggjM2LFjGQB2ww03sBNOOIEBYHl5eaxHjx4sJyeHLVu2TN/37rvvZrm5uQwAA8CKiopYfn6+/r1fv35s1apVltf54IMP2IABA/R9c3NzWc+ePVlBQYH+23333dfhuCeffDJjn4KCAtalSxf9e0lJCVuyZEmH45YtW6bvY2ThwoUMAMvPz2dbtmyxzZeamhoGgA0ePJi1tbVlbHv55ZdZ9+7d9fN37tyZde3aVf+en5/PHn/8ccc0/elPf2KdO3dmAFi3bt1YYWEhGzt2rG16zAwaNIgBYA888AA7/vjj9XQUFxfr18jJyWG//e1vOxxbV1en71NXV+d6jfnz59se/9xzz+l50a1bN9apUyd925gxY9jevXvZiy++yIqKihgAVlpaynJycvR9zj//fMtra+XypptuYmPGjNHLpTHfAbAZM2bYpt9veZ0xYwYDwMaOHcuuvfZaPS979OjBOnXq5HhNgpAdEk8EIQCtkyouLmbFxcVs/vz5LJ1OM8YYa2xs1AXGY489pu93++23s6+//poxxtj+/fvZypUr2fe//30GgPXv3581NTVlXGP9+vWsrKyMAWADBgxgTz/9NNu9ezdjjLG2tjZWW1vLampq2JNPPplx3N///neWm5vL8vLy2PXXX8/q6upYW1sba2trY5988gkbP3683mmvW7cu41g78dTc3Mx69OjBALDf/OY3tvkyZMgQBoBNnz494/ePPvpIF2+TJ09mq1evZvv372eMMbZu3To2ZcoUvaNfsWKFbZqKi4vZuHHj2Jo1a/Ttn332mW16zGjCpkePHuzggw9m/+///T+2d+9exhhjn3zyCfvud7+rX2f79u0Zx4oUT927d2cnn3wyq62tZYwxlk6n2a9//WtdRN1yyy2stLSUnX/++ay+vp4xxlhTUxO7+eab9XO8/PLLHa6tlcvS0lJWUFDAHnroIbZnzx7GWHt5Ovfcc/Xj//rXv3Y4Pkh51cSTJkRvuOEGtnnzZsZYe/nR7oMgVITEE0EIQOukALDnn3/ecp+dO3fqb/wvvfSS5T779u1jxx57rKUFacKECQwA69WrF1u/fj1XulpbW9lhhx3GALCHH37Ydr8f/vCHDAC76qqrMn63E0+MMXb55ZczAGzUqFGW53z77bf1Y//9739nbNM63Ztuusk2TVdeeSUDwM466yzbNI0cOVIXXX7QhE1BQUGGANPYvHkzKywsZAA6iFKR4qmyspI1Nzd3OPaiiy7S9zn11FM7WO8YY7pF6Wc/+1mHbcZyaWU9a21tZd/73vf0NBgJWl418QSAXXvttZbHE4SqUMwTQQiksrISZ555puW25557Dtu3b8fw4cNx+umnW+6Tl5eHn/70pwCAJUuW6L/v3r0bzzzzDADgxhtvxIABA7jS8/rrr+Pf//43ysrKcNlll9nud/HFF3e4phsXXXQRAOCdd97B559/3mH7E088AQAYNWoUhgwZov9eX1+PV199FXl5eZg2bZprml555RW0trZa7lNdXY1OnTpxp9mOc889F0OHDu3we3l5OUaNGgUA+OijjwJfx45rrrkGBQUFHX43lpMbb7zRcrJSbR+n9A0YMACXXHJJh99zc3Nxyy23AABqa2vxr3/9S98WpLyar3HDDTfYpo0gVISmKiAIgYwePdp221tvvQUAWLNmDfr27Wu73549ewAA69at039buXKlHhRsJ86crrljxw4cdNBBtvvt3bu3wzXdGD16NA499FCsXbsWTz75ZEYA8N69e3Wxp4kgc5ra2tpQUVFhe35NMO3evRtbtmxB7969LdMgguOPP952m5ZvW7duFXItK0aOHGn5e58+ffTPI0aMcNxn27ZttufXBjRYMWbMGOTl5WH//v1YuXIljjrqKADByquRIUOGWD47glAZEk8EIRCnTmLjxo0AgObmZjQ3N7ueK51O6583bdqkfx40aBB3erRr7tu3z3YUnhGtI+TloosuQk1NTQfxtHjxYmzduhX5+fk4//zzLdPU1tbGlSYgMy+MiOqUS0pKbLfl5bU3kzwj2kRfX7s2zz5O6Tv44INttxUWFqJXr1745ptvsHnzZv33IOXVCAknIomQ244gBOLkQtIsKeeffz5Ye7yh459x+LvfteW0ax5//PFc12SMeTq/5rpbu3atbqkADrjs/uu//gs9evSwTFOfPn2405RKpSyvL8JlR1gTpLwaoWdEJBESTwQREZrrw4trzHys1+ODXJOHQw45RHedaYJp27ZtWLRoEYCOLjtjmhobGzPmMFINo1XIyTKzY8eOKJLjyFdffWW7raWlBVu2bAGQaSUKu+wQhMqQeCKIiNBExgcffICvv/7a07HHHXcc8vPzAQAvvPCC52tu2rQJK1eu9HRNXjSB9Oyzz2Lv3r149tln0dLSgrKyMowbN842Ta2trVwTNMqK0aK2YcMGy30+++wzbN++PaIU2fPaa6/ZWhXfeOMN7N+/H0B7OdMIUl4JIumQeCKIiBg/fjy6d++Offv24dprr3V0kbW1tWV0ukVFRfjJT34CALjrrrtsO2szJ510kj7S7ZprrtEDw+3wExR93nnnoaCgANu2bcMLL7ygW6B+8pOfoHPnzh32P+yww/RZp2+++WZXy0yYgdpB6Nq1Kw499FAA7SPTrLj99tujTJIt69evx+OPP97h97a2Ntxxxx0AgIqKCj1YHAhWXgki6ZB4IoiI6N69O+6//34AwNNPP43//M//xHvvvYe2tjYA7R3QmjVrcM8996CyshIvvvhixvG33347ysrKsGXLFowePRrPPvusHuDNGMPHH3+M6upqXbwA7a6lhx56CHl5eXjzzTfxve99D0uXLs0ILv7iiy/w0EMPYcSIEZg3b56v+9JGAN5555167JMWD2XFAw88gOLiYnz22Wf47ne/i7/+9a8Zrq+vvvoKTzzxBE4++WSph7lrw/R/97vfYd68efrz2LBhAy677DI888wzKCoqijOJAIDS0lJcccUVePTRR/V83rBhA376059i2bJlAIDbbrst45ig5ZUgEk1YE0gRRDahTUbIs+TEb37zm4zlLQoKClivXr30ZUa0P/OkjIy1L89y8MEH6/t06tSJ9erVS5/IETbLs/zlL39hJSUlGUuh9OrVK2PJFgDstttuyzjOaZJMI88//3zGeYYOHeqaD2+++Sbr27dvh3sxLhsDgF122WW+0sSD3QSWRiZOnMgAsIkTJ3bY1tTUxCoqKvT05Obm6hNLdu7cmf3xj3/kmiTTbpJNnnudP38+A8AGDRrUYZtxeZYTTzxRT5c2O7z2d8stt9ie3295NS7PQhBJgyxPBBExv/jFL/Dpp59i2rRpGDZsGAoKCrB9+3YUFxfjuOOOw3//93/j5Zdf1q0aRo455hisWbMGd911F7773e+ipKQETU1NKC8vR1VVFe69915ccMEFHY47++yz8fnnn2PGjBkYOXIkiouLsX37dhQUFGDYsGG47LLL8Je//AXV1dW+7umMM85AeXm5/t3J6qQxevRofPbZZ5gzZw6+973voXv37ti+fTs6deqEI444AhMmTMBTTz2lWz9kpLi4GG+++SauvfZaDB48GHl5eejcuTPOOeccvPPOO7qrNW7y8/OxdOlS3HHHHTj88MPR0tKC0tJSnHzyyVi0aBH+93//1/bYIOWVIJJKDmMexyYTBEEQBEFkMWR5IgiCIAiC8ACJJ4IgCIIgCA+QeCIIgiAIgvAAiSeCIAiCIAgPkHgiCIIgCILwAIkngiAIgiAID5B4IgiCIAiC8ACJJ4IgCIIgCA+QeCIIgiAIgvAAiSeCIAiCIAgPkHgiCIIgCILwAIkngiAIgiAID5B4IgiCIAiC8MD/B0P8flFtysQOAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "vmax = 0.1*np.max(shot_record)\n", + "spyro.plots.plot_shots(Wave_obj, contour_lines=100, vmin=-vmax, vmax=vmax, show=True)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "firedrake", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/00-basics.ipynb b/notebooks/00-basics.ipynb deleted file mode 100644 index a362a8ca..00000000 --- a/notebooks/00-basics.ipynb +++ /dev/null @@ -1,111 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Basics\n", - "\n", - "This notebook will show some of the basic features of [Firedrake](https://www.firedrakeproject.org), which spyro is built upon and also go over the basics of spyro. These are the kind of things you'd need to know to before you begin to model anything. First, we'll show how to use some of the builtin meshing capabilities in Firedrake and then talk a little bit about how Firedrake represents things on meshes using their concept of Functions.\n", - "\n", - "## Meshes\n", - "\n", - "To perform any finite element simulation, you'll first need a mesh. In spyro, we make use a specific kind of mesh composed of simplices, which is just a fancy word for a mesh composed of triangles in two-dimensions and tetrahedrals in three-dimensions. These meshes offer a number of advantages to model complex geometries in a computational efficient manner by being able to largely vary element sizes. \n", - "\n", - "Firedrake provides a number of utility/helper functions to generate uniform simplical meshes. You can type\n", - "```\n", - "help(firedrake.utility_meshes)\n", - "```\n", - "\n", - "For example, a uniform mesh of a rectangle ∈ [1 x 2] km with ~100 m sized elements would be created like so: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import firedrake\n", - "nx, ny = 16, 16\n", - "mesh = firedrake.UnitRectangleMesh(nx, ny)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Note on using external meshes \n", - "\n", - "2D/3D triangular meshes can also be created with external meshers such as [SeismicMesh](https://github.com/krober10nd/SeismicMesh) and the like and used within Firedrake. We'll go over how to use external meshing tools in a later tutorial. Mesh files should be preferably in the gmsh v2.2 or > version in ASCII text file format. A great way to convert mesh formats if via [meshio](https://pypi.org/project/meshio/). \n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Functions\n", - "\n", - "Scalar and vector-valued functions can be defined on meshes using the concept of functions in Firedrake. \n", - "\n", - "In order to create a `firedrake.function`, we first need to construct what is called a function space `V`. Since we already have a mesh, now we need to decide what element family and polynomial degree to use. In spyro, we exclusively rely on the element family consisting of continuous piecewise-polynomial functions in each simplex, which is abbreviated to CG for \"Continuous Galerkin\" or \"KMV\" for \"Kong-Mulder-Veldhuizen\". \n", - "\n", - "Next, we have to make an expression for the function and interpolate it to the function space `V`. The function `firedrake.SpatialCoordinate` returns two symbolic objects `x`, `y` for the coordinates of each point of the mesh. These symbols can be used to define expressions. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "V = FunctionSpace(mesh, 'CG', 1)\n", - "x, y = SpatialCoordinate(mesh)\n", - "velocity = conditional(x < 0.5, 1.5, 3.0)\n", - "vp = Function(V, name=\"velocity\").interpolate(velocity)\n", - "File(\"simple.pvd\").write(vp)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here we've created a function called `vp` that has the value 1.5 on the left side of the domain (where x < 0.5 coordinate) and 3.0 on the right hand side (where x >= 0.5). We use the `firedrake.interpolate` to interpolate the symbolic expression onto the mesh and then we can write it to disk as a pvd file for visualization in ParaView. \n", - "\n", - "Note all functions have a method `write`, which enables the user to write the function to disk for analysis and visualization in [ParaView](https://www.paraview.org)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "Spyro uses `Firedrake.Functions` to implement solvers for seismic inversion problems. They can also be used to define input fields for simulations, if those fields have a simple analytical expression. In the next tutorial, we'll learn how to simulate a forward wave simulation on a synethic geometry with a point source and adding absorbing boundary conditions. \n", - "\n", - "To learn more about Firedrake, you can visit their [documentation](https://www.firedrakeproject.org/documentation.html) or check out some of the [demos](https://www.firedrakeproject.org/notebooks.html).\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/solver_disc_adj.ipynb b/notebooks/solver_disc_adj.ipynb deleted file mode 100644 index af64ca1c..00000000 --- a/notebooks/solver_disc_adj.ipynb +++ /dev/null @@ -1,235 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from firedrake import (\n", - " RectangleMesh,\n", - " FunctionSpace,\n", - " Function,\n", - " SpatialCoordinate,\n", - " conditional,\n", - " File,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from firedrake import *\n", - "# from firedrake_adjoint import *\n", - "import spyro\n", - "import numpy as np\n", - "import math\n", - "import numpy as np\n", - "import matplotlib.pyplot as plot\n", - "import matplotlib.ticker as mticker \n", - "from matplotlib import cm, ticker\n", - "from mpl_toolkits.axes_grid1 import make_axes_locatable" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model = {}\n", - "\n", - "# Choose method and parameters\n", - "model[\"opts\"] = {\n", - " \"method\": \"KMV\", # either CG or KMV\n", - " \"quadratrue\": \"KMV\", # Equi or KMV\n", - " \"degree\": 1, # p order\n", - " \"dimension\": 2, # dimension\n", - "}\n", - "\n", - "# Number of cores for the shot. For simplicity, we keep things serial.\n", - "# spyro however supports both spatial parallelism and \"shot\" parallelism.\n", - "model[\"parallelism\"] = {\n", - " \"type\": \"off\", # options: automatic (same number of cores for evey processor), custom, off.\n", - " \"custom_cores_per_shot\": [], # only if the user wants a different number of cores for every shot.\n", - " # input is a list of integers with the length of the number of shots.\n", - "}\n", - "\n", - "# Define the domain size without the PML. Here we'll assume a 0.75 x 1.50 km\n", - "# domain and reserve the remaining 250 m for the Perfectly Matched Layer (PML) to absorb\n", - "# outgoing waves on three sides (eg., -z, +-x sides) of the domain.\n", - "model[\"mesh\"] = {\n", - " \"Lz\": 0.75, # depth in km - always positive\n", - " \"Lx\": 1.5, # width in km - always positive\n", - " \"Ly\": 0.0, # thickness in km - always positive\n", - " \"meshfile\": \"not_used.msh\",\n", - " \"initmodel\": \"not_used.hdf5\",\n", - " \"truemodel\": \"not_used.hdf5\",\n", - "}\n", - "\n", - "# Specify a 250-m PML on the three sides of the domain to damp outgoing waves.\n", - "model[\"PML\"] = {\n", - " \"status\": False, # True or false\n", - " \"outer_bc\": \"non-reflective\", # None or non-reflective (outer boundary condition)\n", - " \"damping_type\": \"polynomial\", # polynomial, hyperbolic, shifted_hyperbolic\n", - " \"exponent\": 2, # damping layer has a exponent variation\n", - " \"cmax\": 4.7, # maximum acoustic wave velocity in PML - km/s\n", - " \"R\": 1e-6, # theoretical reflection coefficient\n", - " \"lz\": 0.25, # thickness of the PML in the z-direction (km) - always positive\n", - " \"lx\": 0.25, # thickness of the PML in the x-direction (km) - always positive\n", - " \"ly\": 0.0, # thickness of the PML in the y-direction (km) - always positive\n", - "}\n", - "\n", - "# Create a source injection operator. Here we use a single source with a\n", - "# Ricker wavelet that has a peak frequency of 8 Hz injected at the center of the mesh.\n", - "# We also specify to record the solution at 101 microphones near the top of the domain.\n", - "# This transect of receivers is created with the helper function `create_transect`.\n", - "model[\"acquisition\"] = {\n", - " \"source_type\": \"Ricker\",\n", - " \"num_sources\": 1,\n", - " \"source_pos\": [(0.1, 0.5)],\n", - " \"frequency\": 3.0,\n", - " \"delay\": 1.0,\n", - " \"num_receivers\": 100,\n", - " \"receiver_locations\": spyro.create_transect(\n", - " (0.10, 0.1), (0.10, 0.9), 100\n", - " ),\n", - "}\n", - "\n", - "# Simulate for 2.0 seconds.\n", - "model[\"timeaxis\"] = {\n", - " \"t0\": 0.0, # Initial time for event\n", - " \"tf\": 1.00, # Final time for event\n", - " \"dt\": 0.001, # timestep size\n", - " \"amplitude\": 1, # the Ricker has an amplitude of 1.\n", - " \"nspool\": 100, # how frequently to output solution to pvds\n", - " \"fspool\": 1, # how frequently to save solution to RAM\n", - "}\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "mesh = RectangleMesh(100, 100, 1.0, 1.0)\n", - "# V = FunctionSpace(mesh, family='CG', degree=2)\n", - "# Create the computational environment\n", - "comm = spyro.utils.mpi_init(model)\n", - "\n", - "element = spyro.domains.space.FE_method(\n", - " mesh, model[\"opts\"][\"method\"], model[\"opts\"][\"degree\"]\n", - ")\n", - "V = FunctionSpace(mesh, element)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "x, y = SpatialCoordinate(mesh)\n", - "velocity = conditional(x > 0.35, 1.5, 3.0)\n", - "\n", - "vp = Function(V, name=\"vp\").interpolate(velocity)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sources = spyro.Sources(model, mesh, V, comm).create()\n", - "receivers = spyro.Receivers(model, mesh, V, comm).create()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "solver = spyro.solvers.Leapfrog\n", - "usol, usol_rec = solver(model, mesh, comm, vp,sources, receivers, source_num=0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "misfit = usol_rec\n", - "\n", - "J_total = spyro.utils.compute_functional(model, comm, misfit)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "solver = spyro.solvers.Leapfrog_adjoint\n", - "dJdC_local = solver(model, mesh, comm, vp, receivers, usol, misfit)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from mpi4py import MPI\n", - "# sum over all ensemble members\n", - "dJdC_local.dat.data[:] = comm.ensemble_comm.allreduce(\n", - " dJdC_local.dat.data[:], op=MPI.SUM\n", - ")\n", - "\n", - "\n", - "fig, axes = plot.subplots()\n", - "axes.set_aspect('equal')\n", - "colors = firedrake.tripcolor(dJdC_local, axes=axes, shading='gouraud', cmap=\"jet\")\n", - "\n", - "fig.colorbar(colors);\n", - "plot.savefig('grad.png',dpi=100,format='png')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "new_firedrak1", - "language": "python", - "name": "new_firedrak1" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} 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..db6ea5a7 100644 --- a/spyro/__init__.py +++ b/spyro/__init__.py @@ -3,13 +3,26 @@ 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.inversion import FullWaveformInversion + +# 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 +41,13 @@ "solvers", "plots", "tools", + "Wave", + "examples", + "sources", + "AcousticWave", + "FullWaveformInversion", + "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..2abb71b0 --- /dev/null +++ b/spyro/examples/camembert.py @@ -0,0 +1,158 @@ +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 +} +# 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, +} +camembert_dictionary["camembert_options"] = { + "radius": 0.2, + "circle_center": (-0.5, 0.5), + "outside_velocity": 1.6, + "inside_circle_velocity": 4.6, +} + + +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): + camembert_dict = self.input_dictionary["camembert_options"] + z = self.mesh_z + x = self.mesh_x + zc, xc = camembert_dict["circle_center"] + rc = camembert_dict["radius"] + c_salt = camembert_dict["inside_circle_velocity"] + c_not_salt = camembert_dict["outside_velocity"] + cond = fire.conditional( + (z - zc) ** 2 + (x - xc) ** 2 < rc**2, c_salt, c_not_salt + ) + self.set_initial_velocity_model(conditional=cond, dg_velocity_model=False) + return None diff --git a/spyro/examples/cut_marmousi.py b/spyro/examples/cut_marmousi.py new file mode 100644 index 00000000..989c50f3 --- /dev/null +++ b/spyro/examples/cut_marmousi.py @@ -0,0 +1,127 @@ +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): + """ + Class for the cut marmousi model. + + Parameters + ---------- + dictionary : dict, optional + Dictionary with the parameters of the model that are different from + the default model. The default is None. + """ + 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..56df9b4d --- /dev/null +++ b/spyro/examples/marmousi.py @@ -0,0 +1,135 @@ +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): + """ + Marmousi model. + This class is a child of the Example_model class. + It is used to create a dictionary with the parameters of the + Marmousi model. + + Parameters + ---------- + dictionary : dict, optional + Dictionary with the parameters of the model that are different from + the default model. The default is None. + """ + 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..8f2f6693 --- /dev/null +++ b/spyro/examples/rectangle.py @@ -0,0 +1,206 @@ +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, +} + +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): + """ + Rectangle model. + This class is a child of the Example_model class. + It is used to create a dictionary with the parameters of the + Rectangle model. + + Parameters + ---------- + dictionary : dict, optional + Dictionary with the parameters of the model that are different from + the default model. The default is None. + comm : firedrake.mpi_comm.MPI.Intracomm, optional + periodic : bool, optional + If True, the mesh will be periodic in all directions. The default is + False. + """ + 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"] + mesh_parameters = { + "length_z": mesh_dict["Lz"], + "length_x": mesh_dict["Lx"], + "length_y": mesh_dict["Ly"], + "dx": mesh_dict["h"], + "mesh_file": mesh_dict["mesh_file"], + "mesh_type": mesh_dict["mesh_type"], + "periodic": self.periodic, + } + super().set_mesh(mesh_parameters=mesh_parameters) + + 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..58116832 100644 --- a/spyro/io/__init__.py +++ b/spyro/io/__init__.py @@ -1,21 +1,24 @@ -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, - ensemble_forward_elastic_waves, + # ensemble_forward, + # ensemble_forward_ad, + # ensemble_forward_elastic_waves, ensemble_gradient, - ensemble_gradient_elastic_waves, + # 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", @@ -25,10 +28,17 @@ "load_shots", "read_mesh", "interpolate", - "ensemble_forward", - "ensemble_forward_ad", - "ensemble_forward_elastic_waves", + # "ensemble_forward", + # "ensemble_forward_ad", + # "ensemble_forward_elastic_waves", "ensemble_gradient", - "ensemble_gradient_elastic_waves", + # "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..2653d2d9 --- /dev/null +++ b/spyro/io/backwards_compatibility_io.py @@ -0,0 +1,263 @@ +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 + convert_absorving_boundary_conditions() + Convert the absorving_boundary_conditions section of dictionary + convert_acquisition() + Convert the acquisition section of dictionary + convert_time_axis() + Convert the time_axis 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 50% rename from spyro/io/io.py rename to spyro/io/basicio.py index 166e6f17..64b4a3c4 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" - ) - ) - return wrapper - - -def ensemble_load(func): - """Decorator for loading 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") - 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", + source_id=snum, + file_name="shots/" + + custom_file_name + + str(snum + 1) + + ".dat", ) ) - else: - values = func( - *args, - **dict( - kwargs, - file_name=custom_file_name+"shot_record_" + str(snum + 1) + ".dat" - ) - ) - return values return wrapper def ensemble_plot(func): - """Decorator for `plot_shots` to distribute shots for ensemble parallelism""" + """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) 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))) + func(*args, **dict(kwargs, file_name=str(snum + 1))) return wrapper -def ensemble_forward(func): +# 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[2] +# for snum in range(num): +# if is_owner(_comm, snum): +# u, u_r = func(*args, **dict(kwargs, source_num=snum)) +# return u, u_r + +# return wrapper + + +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)) @@ -115,83 +89,81 @@ def wrapper(*args, **kwargs): return wrapper -def ensemble_forward_ad(func): - """Decorator for forward_ad to distribute shots for ensemble parallelism""" +# def ensemble_forward_ad(func): +# """Decorator for forward to distribute shots for ensemble parallelism""" - def wrapper(*args, **kwargs): - acq = args[0].get("acquisition") - num = len(acq["source_pos"]) - fwi = kwargs.get("fwi") - _comm = args[2] - for snum in range(num): - if is_owner(_comm, snum): - if fwi: - u_r, J = func(*args, **dict(kwargs, source_num=snum)) - return u_r, J - else: - u_r = func(*args, **dict(kwargs, source_num=snum)) +# def wrapper(*args, **kwargs): +# acq = args[0].get("acquisition") +# num = len(acq["source_pos"]) +# fwi = kwargs.get("fwi") +# _comm = args[2] +# for snum in range(num): +# if is_owner(_comm, snum): +# if fwi: +# u_r, J = func(*args, **dict(kwargs, source_num=snum)) +# return u_r, J +# else: +# u_r = func(*args, **dict(kwargs, source_num=snum)) - return wrapper +# return wrapper -def ensemble_forward_elastic_waves(func): - """Decorator for forward elastic waves to distribute shots for ensemble parallelism""" +# def ensemble_forward_elastic_waves(func): +# """Decorator for forward elastic waves to distribute shots for +# ensemble parallelism""" - def wrapper(*args, **kwargs): - acq = args[0].get("acquisition") - num = len(acq["source_pos"]) - _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)) - return u, uz_r, ux_r, uy_r +# def wrapper(*args, **kwargs): +# acq = args[0].get("acquisition") +# num = len(acq["source_pos"]) +# _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) +# ) +# return u, uz_r, ux_r, uy_r - return wrapper +# return wrapper def ensemble_gradient(func): """Decorator for gradient to distribute shots for ensemble parallelism""" def wrapper(*args, **kwargs): - acq = args[0].get("acquisition") - save_adjoint = kwargs.get("save_adjoint") - 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): - if save_adjoint: - grad, u_adj = func(*args, **kwargs) - return grad, u_adj - else: - grad = func(*args, **kwargs) - return grad + grad = func(*args, **kwargs) + return grad return wrapper -def ensemble_gradient_elastic_waves(func): - """Decorator for gradient (elastic waves) to distribute shots for ensemble parallelism""" +# def ensemble_gradient_elastic_waves(func): +# """Decorator for gradient (elastic waves) to distribute shots +# for ensemble parallelism""" - def wrapper(*args, **kwargs): - acq = args[0].get("acquisition") - save_adjoint = kwargs.get("save_adjoint") - num = len(acq["source_pos"]) - _comm = args[2] - for snum in range(num): - if is_owner(_comm, snum): - if save_adjoint: - grad_lambda, grad_mu, u_adj = func(*args, **kwargs) - return grad_lambda, grad_mu, u_adj - else: - grad_lambda, grad_mu = func(*args, **kwargs) - return grad_lambda, grad_mu +# def wrapper(*args, **kwargs): +# acq = args[0].get("acquisition") +# save_adjoint = kwargs.get("save_adjoint") +# num = len(acq["source_pos"]) +# _comm = args[2] +# for snum in range(num): +# if is_owner(_comm, snum): +# if save_adjoint: +# grad_lambda, grad_mu, u_adj = func(*args, **kwargs) +# return grad_lambda, grad_mu, u_adj +# else: +# grad_lambda, grad_mu = func(*args, **kwargs) +# return grad_lambda, grad_mu - return wrapper +# return wrapper def write_function_to_grid(function, V, grid_spacing): """Interpolate a Firedrake function to a structured grid - + Parameters ---------- function : firedrake.Function @@ -203,12 +175,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 +209,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 +238,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,16 +258,20 @@ 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 ---------- + Wave_obj: `spyro.Wave` object + A `spyro.Wave` object + source_id: int, optional by default 0 + The source number filename: str, optional by default shot_number_#.dat The filename to save the data as a `pickle` @@ -304,10 +281,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 +309,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 +317,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_active: + minz = -Model.length_z - Model.abc_pad_length 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_pad_length + maxx = Model.length_x + Model.abc_pad_length + miny = 0.0 - Model.abc_pad_length + maxy = Model.length_y + Model.abc_pad_length 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 +368,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 +377,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 +393,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 +412,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 +444,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 +463,55 @@ 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 + + Parameters + ---------- + string: str + The string to print + comm: Firedrake.ensemble_communicator + An ensemble communicator + """ + 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): + """ + Saving the source and receiver locations in a csv file + + Parameters + ---------- + model: spyro object + Model options and parameters. + folder_name: str, optional by default None + The folder name to save the csv file + """ + 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..c7b2c73d --- /dev/null +++ b/spyro/io/boundary_layer_io.py @@ -0,0 +1,71 @@ +# # 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): + """ + Reads the PML dictionary for a perfectly matched layer + """ + 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..9c956693 --- /dev/null +++ b/spyro/io/dictionaryio.py @@ -0,0 +1,546 @@ +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 = parse_cell_type(dictionary) + variant = parse_variant(dictionary) + method = parse_method(cell_type, variant) + + return method, cell_type, variant + + +def parse_cell_type(dictionary): + """ + Parse the cell type from the dictionary of a CG. + + Parameters + ---------- + dictionary : dict + Dictionary containing the options information. + + Returns + ------- + cell_type : str + The cell type to be used. Returns either triangle or quadrilateral. + """ + 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." + ) + return cell_type + + +def parse_variant(dictionary): + """ + Parse the variant from the dictionary of a CG. + + Parameters + ---------- + dictionary : dict + Dictionary containing the options information. + + Returns + ------- + variant : str + The variant to be used. Returns either lumped, equispaced or DG. + """ + 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" + + return variant + + +def parse_method(cell_type, variant): + """ + Parse the method from the dictionary of a CG. + + Parameters + ---------- + cell_type : str + The cell type to be used. + variant : str + The variant to be used. + + Returns + ------- + method : str + The method to be used. + """ + 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} results in a not implemented method") + + return method + + +def check_if_mesh_file_exists(file_name): + """ + Just checks if the mesh file exists. + + Parameters + ---------- + file_name : str + The mesh file name. + + Raises + ------ + ValueError + If the mesh file does not exist. + """ + 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..1904dc55 --- /dev/null +++ b/spyro/io/model_parameters.py @@ -0,0 +1,829 @@ +import numpy as np +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_active: 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.real_shot_record = None + 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_active = 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_active: + 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): + self._sanitize_optimization_and_velocity_without_fwi() + 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" + if "optimization_parameters" in dictionary["inversion"]: + self.optimization_parameters = dictionary["inversion"][ + "optimization_parameters" + ] + else: + 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, + }, + } + self.optimization_parameters = default_optimization_parameters + + if "shot_record_file" in dictionary["inversion"]: + if dictionary["inversion"]["shot_record_file"] is not None: + self.real_shot_record = np.load(dictionary["inversion"]["shot_record_file"]) + + 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, + user_mesh=None, + mesh_parameters={}, + ): + """ + Set the mesh for the model. + + Parameters + ---------- + user_mesh : spyro.Mesh, optional + The desired mesh. The default is None. + mesh_parameters : dict, optional + Additional parameters for setting up the mesh. The default is an empty dictionary. + + Returns + ------- + None + """ + + # Setting default mesh parameters + mesh_parameters.setdefault("periodic", False) + mesh_parameters.setdefault("minimum_velocity", 1.5) + mesh_parameters.setdefault("edge_length", None) + mesh_parameters.setdefault("dx", None) + mesh_parameters.setdefault("length_z", self.length_z) + mesh_parameters.setdefault("length_x", self.length_x) + mesh_parameters.setdefault("length_y", self.length_y) + mesh_parameters.setdefault("abc_pad_length", self.abc_pad_length) + mesh_parameters.setdefault("mesh_file", self.mesh_file) + mesh_parameters.setdefault("dimension", self.dimension) + mesh_parameters.setdefault("mesh_type", self.mesh_type) + mesh_parameters.setdefault("source_frequency", self.frequency) + mesh_parameters.setdefault("method", self.method) + mesh_parameters.setdefault("degree", self.degree) + mesh_parameters.setdefault("velocity_model_file", self.initial_velocity_model_file) + mesh_parameters.setdefault("cell_type", self.cell_type) + mesh_parameters.setdefault("cells_per_wavelength", None) + + self._set_mesh_length( + length_z=mesh_parameters["length_z"], + length_x=mesh_parameters["length_x"], + length_y=mesh_parameters["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_parameters["mesh_file"] is not None: + self.mesh_file = mesh_parameters["mesh_file"] + self.mesh_type = "file" + elif automatic_mesh: + self.user_mesh = self._creating_automatic_mesh( + mesh_parameters=mesh_parameters + ) + + if ( + mesh_parameters["length_z"] is None + or mesh_parameters["length_x"] is None + or (mesh_parameters["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, mesh_parameters={}): + """ + Creates an automatic mesh using the specified mesh parameters. + + Args: + mesh_parameters (dict): A dictionary containing the parameters for meshing. + + Returns: + Mesh: The created mesh object. + """ + AutoMeshing = meshing.AutomaticMesh( + comm=self.comm, + mesh_parameters=mesh_parameters, + ) + + 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.user_mesh is False: + non_file_mesh = None + else: + non_file_mesh = self.user_mesh + + 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 non_file_mesh + elif self.mesh_type == "SeismicMesh": + return non_file_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..4ca7514b --- /dev/null +++ b/spyro/meshing/meshing_functions.py @@ -0,0 +1,598 @@ +import os +import firedrake as fire +import SeismicMesh +import meshio + + +def cells_per_wavelength(method, degree, dimension): + cell_per_wavelength_dictionary = { + 'mlt2tri': 7.02, + 'mlt3tri': 3.70, + 'mlt4tri': 2.67, + 'mlt5tri': 2.03, + 'mlt2tet': 6.12, + 'mlt3tet': 3.72, + } + + if dimension == 2 and (method == 'KMV' or method == 'CG'): + cell_type = 'tri' + if dimension == 3 and (method == 'KMV' or method == 'CG'): + cell_type = 'tet' + + key = method.lower()+str(degree)+cell_type + + return cell_per_wavelength_dictionary.get(key) + + +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. + set_seismicmesh_parameters(cpw=None, velocity_model=None, edge_length=None) + Sets the SeismicMesh parameters. + make_periodic() + Sets the mesh boundaries periodic. Only works for firedrake_mesh. + create_mesh() + Creates the mesh. + create_firedrake_mesh() + Creates a 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. + create_seismicmesh_mesh() + Creates a mesh based on SeismicMesh meshing utilities. + create_seimicmesh_2d_mesh() + Creates a 2D mesh based on SeismicMesh meshing utilities. + create_seismicmesh_2D_mesh_homogeneous() + Creates a 2D mesh homogeneous velocity mesh based on SeismicMesh meshing utilities. + """ + + def __init__( + self, comm=None, mesh_parameters=None + ): + """ + Initialize the MeshingFunctions class. + + Parameters + ---------- + comm : MPI communicator, optional + MPI communicator. The default is None. + mesh_parameters : dict, optional + Dictionary containing the mesh parameters. The default is None. + + Raises + ------ + ValueError + If `abc_pad_length` is negative. + + Notes + ----- + The `mesh_parameters` dictionary should contain the following keys: + - 'dimension': int, optional. Dimension of the mesh. The default is 2. + - 'length_z': float, optional. Length of the mesh in the z-direction. + - 'length_x': float, optional. Length of the mesh in the x-direction. + - 'length_y': float, optional. Length of the mesh in the y-direction. + - 'cell_type': str, optional. Type of the mesh cells. + - 'mesh_type': str, optional. Type of the mesh. + + For mesh with absorbing layer only: + - 'abc_pad_length': float, optional. Length of the absorbing boundary condition padding. + + For Firedrake mesh only: + - 'dx': float, optional. Mesh element size. + - 'periodic': bool, optional. Whether the mesh is periodic. + - 'edge_length': float, optional. Length of the mesh edges. + + For SeismicMesh only: + - 'cells_per_wavelength': float, optional. Number of cells per wavelength. + - 'source_frequency': float, optional. Frequency of the source. + - 'minimum_velocity': float, optional. Minimum velocity. + - 'velocity_model_file': str, optional. File containing the velocity model. + - 'edge_length': float, optional. Length of the mesh edges. + """ + self.dimension = mesh_parameters["dimension"] + self.length_z = mesh_parameters["length_z"] + self.length_x = mesh_parameters["length_x"] + self.length_y = mesh_parameters["length_y"] + self.cell_type = mesh_parameters["cell_type"] + self.comm = comm + if mesh_parameters["abc_pad_length"] is None: + self.abc_pad = 0.0 + elif mesh_parameters["abc_pad_length"] >= 0.0: + self.abc_pad = mesh_parameters["abc_pad_length"] + else: + raise ValueError("abc_pad must be positive") + self.mesh_type = mesh_parameters["mesh_type"] + + # Firedrake mesh only parameters + self.dx = mesh_parameters["dx"] + self.quadrilateral = False + self.periodic = mesh_parameters["periodic"] + if self.dx is None: + self.dx = mesh_parameters["edge_length"] + + # SeismicMesh only parameters + self.cpw = mesh_parameters["cells_per_wavelength"] + self.source_frequency = mesh_parameters["source_frequency"] + self.minimum_velocity = mesh_parameters["minimum_velocity"] + self.lbda = None + self.velocity_model = mesh_parameters["velocity_model_file"] + self.edge_length = mesh_parameters["edge_length"] + self.output_file_name = "automatic_mesh.msh" + + 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. + output_file_name : str, optional + Output file name. 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 + 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 + elif self.output_file_name is None: + 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. + Only works for firedrake_mesh. + """ + 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 : 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): + """ + Creates a mesh based on Firedrake meshing utilities. + """ + 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. + """ + if self.abc_pad: + nx = int( (self.length_x + 2*self.abc_pad) / self.dx) + nz = int( (self.length_z + self.abc_pad)/ self.dx) + else: + 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): + """ + Creates a 3D mesh based on Firedrake meshing utilities. + """ + 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): + """ + Creates a mesh based on SeismicMesh meshing utilities. + + Returns + ------- + mesh : Mesh + Mesh + """ + 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): + """ + Creates a 2D mesh based on SeismicMesh meshing utilities. + """ + if self.velocity_model is None: + return self.create_seismicmesh_2D_mesh_homogeneous() + else: + return self.create_seismicmesh_2D_mesh_with_velocity_model() + + def create_seismicmesh_2D_mesh_with_velocity_model(self): + if self.comm.ensemble_comm.rank == 0: + v_min = self.minimum_velocity + frequency = self.source_frequency + C = self.cpw # cells_per_wavelength(method, degree, dimension) + + Lz = self.length_z*1000 + Lx = self.length_x*1000 + domain_pad = self.abc_pad*1000 + lbda_min = v_min/frequency + + bbox = (-Lz, 0.0, 0.0, Lx) + domain = SeismicMesh.Rectangle(bbox) + + hmin = lbda_min/C*1000 + self.comm.comm.barrier() + + ef = SeismicMesh.get_sizing_function_from_segy( + self.velocity_model, + bbox, + hmin=hmin, + wl=C, + freq=frequency, + grade=0.15, + domain_pad=domain_pad, + pad_style="edge", + units='km/s', + comm=self.comm.comm, + ) + self.comm.comm.barrier() + + # Creating rectangular mesh + points, cells = SeismicMesh.generate_mesh( + domain=domain, + edge_length=ef, + verbose=0, + mesh_improvement=False, + comm=self.comm.comm, + ) + self.comm.comm.barrier() + + print('entering spatial rank 0 after mesh generation') + if self.comm.comm.rank == 0: + meshio.write_points_cells( + "automatic_mesh.msh", + points/1000.0, + [("triangle", cells)], + file_format="gmsh22", + binary=False + ) + + meshio.write_points_cells( + "automatic_mesh.vtk", + points/1000.0, + [("triangle", cells)], + file_format="vtk" + ) + + self.comm.comm.barrier() + mesh = fire.Mesh( + 'automatic_mesh.msh', + distribution_parameters={ + "overlap_type": (fire.DistributedMeshOverlapType.NONE, 0) + }, + comm=self.comm.comm, + ) + + return mesh + + def create_seismicmesh_2D_mesh_homogeneous(self): + """ + Creates a 2D mesh based on SeismicMesh meshing utilities, with homogeneous velocity model. + """ + 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, comm=comm) + 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 + if quadrilateral: + 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) + else: + mesh = fire.BoxMesh(nx, ny, nz, Lx, Ly, Lz) + mesh.coordinates.dat.data[:, 0] *= -1.0 + + return mesh diff --git a/spyro/plots/__init__.py b/spyro/plots/__init__.py index 0a6f0610..e65ea223 100644 --- a/spyro/plots/__init__.py +++ b/spyro/plots/__init__.py @@ -1,3 +1,11 @@ -from .plots import plot_shots +from .plots import plot_shots, plot_mesh_sizes, plot_model, plot_function +from .plots import debug_plot, debug_pvd -__all__ = ["plot_shots"] +__all__ = [ + "plot_shots", + "plot_mesh_sizes", + "plot_model", + "plot_function", + "debug_plot", + "debug_pvd", +] diff --git a/spyro/plots/plots.py b/spyro/plots/plots.py index 7215fc8b..68742439 100644 --- a/spyro/plots/plots.py +++ b/spyro/plots/plots.py @@ -1,6 +1,11 @@ # from scipy.io import savemat import matplotlib.pyplot as plt +import matplotlib.patches as patches +from matplotlib.ticker import MultipleLocator +from PIL import Image import numpy as np +import firedrake +import copy from ..io import ensemble_plot __all__ = ["plot_shots"] @@ -8,13 +13,12 @@ @ensemble_plot def plot_shots( - model, - comm, - arr, + Wave_object, show=False, file_name="1", vmin=-1e-5, vmax=1e-5, + contour_lines=700, file_format="pdf", start_index=0, end_index=0, @@ -49,12 +53,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 +70,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, contour_lines, cmap=cmap, vmin=vmin, vmax=vmax) # savemat("test.mat", {"mydata": arr}) plt.xlabel("receiver number", fontsize=18) plt.ylabel("time (s)", fontsize=18) @@ -77,5 +83,144 @@ def plot_shots( # plt.axis("image") if show: plt.show() - plt.close() + # plt.close() return None + + +def plot_mesh_sizes( + mesh_filename=None, + firedrake_mesh=None, + title_str=None, + output_filename=None, + show=False, +): + plt.rcParams['font.family'] = "Times New Roman" + plt.rcParams['font.size'] = 12 + + if mesh_filename is not None: + mesh = firedrake.Mesh(mesh_filename) + elif firedrake_mesh is not None: + mesh = firedrake_mesh + else: + raise ValueError("Please specify mesh") + + coordinates = copy.deepcopy(mesh.coordinates.dat.data) + + mesh.coordinates.dat.data[:, 0] = coordinates[:, 1] + mesh.coordinates.dat.data[:, 1] = coordinates[:, 0] + + DG0 = firedrake.FunctionSpace(mesh, "DG", 0) + f = firedrake.interpolate(firedrake.CellSize(mesh), DG0) + + fig, axes = plt.subplots() + im = firedrake.tricontourf(f, axes=axes) + + axes.set_aspect("equal", "box") + plt.xlabel("X (km)") + plt.ylabel("Z (km)") + plt.title(title_str) + + cbar = fig.colorbar(im, orientation="horizontal") + cbar.ax.set_xlabel("circumcircle radius (km)") + fig.set_size_inches(13, 10) + if show: + plt.show() + if output_filename is not None: + plt.savefig(output_filename) + + +def plot_model(Wave_object, filename="model.png", abc_points=None, show=False, flip_axis=True): + """ + Plot the model with source and receiver locations. + + Parameters + ----------- + Wave_object: + The Wave object containing the model and locations. + filename (optional): + The filename to save the plot (default: "model.png"). + abc_points (optional): + List of points to plot an ABC line (default: None). + """ + plt.close() + fig = plt.figure(figsize=(9, 9)) + axes = fig.add_subplot(111) + fig.set_figwidth = 9.0 + fig.set_figheight = 9.0 + vp_object = Wave_object.initial_velocity_model + vp_image = firedrake.tripcolor(vp_object, axes=axes) + for source in Wave_object.source_locations: + z, x = source + plt.scatter(z, x, c="green") + for receiver in Wave_object.receiver_locations: + z, x = receiver + plt.scatter(z, x, c="red") + + if flip_axis: + axes.invert_yaxis() + + axes.set_xlabel("Z (km)") + + if flip_axis: + axes.set_ylabel("X (km)", rotation=-90, labelpad=20) + plt.setp(axes.get_xticklabels(), rotation=-90, va="top", ha="center") + plt.setp(axes.get_yticklabels(), rotation=-90, va="center", ha="left") + else: + axes.set_ylabel("X (km)") + + cbar = plt.colorbar(vp_image, orientation="horizontal") + cbar.set_label("Velocity (km/s)") + if flip_axis: + cbar.ax.tick_params(rotation=-90) + axes.tick_params(axis='y', pad=20) + axes.axis('equal') + + if abc_points is not None: + zs = [] + xs = [] + + first = True + for point in abc_points: + z, x = point + zs.append(z) + xs.append(x) + if first: + z_first = z + x_first = x + first = False + zs.append(z_first) + xs.append(x_first) + plt.plot(zs, xs, "--") + print(f"File name {filename}", flush=True) + plt.savefig(filename) + + if flip_axis: + img = Image.open(filename) + img_rotated = img.rotate(90) + + # Save the rotated image + img_rotated.save(filename) + if show: + plt.show() + else: + plt.close() + + +def plot_function(function): + plt.close() + fig = plt.figure(figsize=(9, 9)) + axes = fig.add_subplot(111) + fig.set_figwidth = 9.0 + fig.set_figheight = 9.0 + firedrake.tricontourf(function, axes=axes) + axes.axis('equal') + + +def debug_plot(function, filename="debug.png"): + plot_function(function) + plt.savefig(filename) + + +def debug_pvd(function, filename="debug.pvd"): + out = firedrake.File(filename) + out.write(function) 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..56ff8ad8 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,124 +47,33 @@ 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 ---------- - 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 - my_ensemble: Firedrake.ensemble_communicator - An ensemble communicator + wave_object: :class: 'Wave' object + Waveform object that contains all simulation parameters Returns ------- 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 +90,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 +109,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 +135,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..9589204c --- /dev/null +++ b/spyro/receivers/changing_coordinates.py @@ -0,0 +1,766 @@ +import numpy as np + + +def change_to_reference_triangle(p, cell_vertices): + """ + Changes variables to reference triangle + + Parameters + ---------- + p : tuple + Point in original triangle + cell_vertices : list + List of vertices, in tuple format, of original triangle + + Returns + ------- + tuple + Point location in 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 + + Parameters + ---------- + p : tuple + Point in original tetrahedron + cell_vertices : list + List of vertices, in tuple format, of original tetrahedron + reference_coordinates : list, optional + List of reference coordinates, in tuple format, of original tetrahedron + + Returns + ------- + tuple + Point location in 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 + + Parameters + ---------- + p : tuple + Point in original quadrilateral + cell_vertices : list + List of vertices, in tuple format, of original quadrilateral + + Returns + ------- + tuple + Point location in 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): + """ + Changes variables to reference hexahedron + + Parameters + ---------- + p : tuple + Point in original hexahedron + cell_vertices : list + List of vertices, in tuple format, of original hexahedron + + Returns + ------- + tuple + Point location in reference hexahedron + """ + 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..941a5c9d --- /dev/null +++ b/spyro/receivers/dirac_delta_projector.py @@ -0,0 +1,573 @@ +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: + """ + Class that interpolates the solution to the receiver coordinates + + Attributes + ---------- + mesh: firedrake.Mesh + Mesh object + space: firedrake.FunctionSpace + Function space to be used + my_ensemble: mpi4py.MPI.Intracomm + MPI communicator + dimension: int + Dimension of the mesh + degree: int + Degree of FEM space + point_locations: list + List of tuples of point locations + number_of_points: int + Number of points + cellIDs: list + List of cell IDs for each point + cellVertices: list + List of vertices for each cell containing a point + cell_tabulations: list + List of tabulations for each point in a cell + cellNodeMaps: list + List of node maps for each cell + nodes_per_cell: int + Number of nodes per cell + quadrilateral: bool + True if mesh is quadrilateral + is_local: list + List of cell IDs local to the processor + """ + def __init__(self, wave_object): + """ + Initializes the class + + Parameters + ---------- + wave_object: spyro.wave.Wave + 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): + """ + Chooses UFC reference element geometry based on desired function space + + Parameters + ---------- + cell_geometry : firedrake.Cell + Cell geometry of the mesh. + + Returns + ------- + T : FIAT reference element + FIAT reference element to be used in the interpolation. + """ + 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..5e7a19f6 100644 --- a/spyro/solvers/__init__.py +++ b/spyro/solvers/__init__.py @@ -1,9 +1,11 @@ -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 +from .inversion import FullWaveformInversion __all__ = [ - "forward", # forward solver adapted for discrete adjoint - "forward_AD", # forward solver adapted for Automatic Differentiation - "gradient", + "Wave", + "AcousticWave", + "AcousticWaveMMS", + "FullWaveformInversion", ] 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..252c0180 --- /dev/null +++ b/spyro/solvers/acoustic_solver_construction_no_pml.py @@ -0,0 +1,51 @@ +import firedrake as fire +from firedrake import dx, Constant, dot, grad + + +def construct_solver_or_matrix_no_pml(Wave_object): + """Builds solver operators for wave object without a PML. Doesn't create mass matrices if + matrix_free option is on, which it is by default. + + Parameters + ---------- + Wave_object: :class: 'Wave' object + Waveform object that contains all simulation parameters + """ + 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..6d4a2ed0 --- /dev/null +++ b/spyro/solvers/acoustic_solver_construction_with_pml.py @@ -0,0 +1,170 @@ +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): + """ + Builds solver operators for wave propagator with a PML. Doesn't create mass matrices if + matrix_free option is on, which it is by default. + """ + 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 for 2D wave propagator with a PML. 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 + Wave_object.mixed_function_space = W + 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): + """ + Builds solver operators for 3D wave propagator with a PML. 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 * V * Z + Wave_object.mixed_function_space = W + 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..3ec51d44 --- /dev/null +++ b/spyro/solvers/acoustic_wave.py @@ -0,0 +1,153 @@ +import firedrake as fire +import warnings + +from .wave import Wave +from .time_integration import time_integrator +from ..io.basicio import ensemble_propagator, ensemble_gradient +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, +) +from .backward_time_integration import ( + backward_wave_propagator, +) + + +class AcousticWave(Wave): + def save_current_velocity_model(self, file_name=None): + if self.c is None: + raise ValueError("C not loaded") + if file_name is None: + file_name = "velocity_model.pvd" + fire.File(file_name).write( + self.c, name="velocity" + ) + + def forward_solve(self): + """Solves the forward problem. + + Parameters: + ----------- + None + + Returns: + -------- + None + """ + if self.function_space is None: + self.force_rebuild_function_space() + + self._get_initial_velocity_model() + self.c = self.initial_velocity_model + self.matrix_building() + self.wave_propagator() + + def force_rebuild_function_space(self): + if self.mesh is None: + self.mesh = self.get_mesh() + self._build_function_space() + self._map_sources_and_receivers() + + 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 + + self.current_source = source_num + usol, usol_recv = time_integrator(self, source_id=source_num) + + return usol, usol_recv + + @ensemble_gradient + def gradient_solve(self, guess=None, misfit=None, forward_solution=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 misfit is not None: + self.misfit = misfit + if self.real_shot_record is None: + warnings.warn("Please load or calculate a real shot record first") + if self.current_time == 0.0: + self.forward_solve() + self.misfit = self.real_shot_record - self.forward_solution_receivers + return backward_wave_propagator(self) + + def reset_pressure(self): + try: + self.u_nm1.assign(0.0) + self.u_n.assign(0.0) + except: + warnings.warn("No pressure to reset") + if self.abc_active: + try: + self.X_n.assign(0.0) + self.X_nm1.assign(0.0) + except: + warnings.warn("No mixed space pressure to reset") + diff --git a/spyro/solvers/backward_time_integration.py b/spyro/solvers/backward_time_integration.py new file mode 100644 index 00000000..5b440235 --- /dev/null +++ b/spyro/solvers/backward_time_integration.py @@ -0,0 +1,273 @@ +import firedrake as fire +from . import helpers + + +def backward_wave_propagator(Wave_obj, dt=None): + """Propagates the adjoint wave backwards in time. + Currently uses central differences. + + Parameters: + ----------- + Wave_obj: Spyro wave object + Wave object that already propagated a forward wave. + dt: Python 'float' (optional) + Time step to be used explicitly. If not mentioned uses the default, + that was estabilished in the wave object for the adjoint model. + + Returns: + -------- + dJ: Firedrake 'Function' + Calculated gradient + """ + if Wave_obj.abc_active is False: + return backward_wave_propagator_no_pml(Wave_obj, dt=dt) + elif Wave_obj.abc_active: + return mixed_space_backward_wave_propagator(Wave_obj, dt=dt) + + +def backward_wave_propagator_no_pml(Wave_obj, dt=None): + """Propagates the adjoint wave backwards in time. + Currently uses central differences. Does not have any PML. + + Parameters: + ----------- + Wave_obj: Spyro wave object + Wave object that already propagated a forward wave. + dt: Python 'float' (optional) + Time step to be used explicitly. If not mentioned uses the default, + that was estabilished in the wave object for the adjoint model. + + Returns: + -------- + dJ: Firedrake 'Function' + Calculated gradient + """ + Wave_obj.reset_pressure() + if dt is not None: + Wave_obj.dt = dt + + forward_solution = Wave_obj.forward_solution + receivers = Wave_obj.receivers + residual = Wave_obj.misfit + comm = Wave_obj.comm + temp_filename = Wave_obj.forward_output_file + + filename, file_extension = temp_filename.split(".") + output_filename = "backward." + file_extension + + output = fire.File(output_filename, comm=comm.comm) + comm.comm.barrier() + + X = fire.Function(Wave_obj.function_space) + dJ = fire.Function(Wave_obj.function_space) # , name="gradient") + + final_time = Wave_obj.final_time + dt = Wave_obj.dt + t = Wave_obj.current_time + if t != final_time: + print(f"Current time of {t}, different than final_time of {final_time}. Setting final_time to current time in backwards propagation.", flush= True) + nt = int(t / dt) + 1 # number of timesteps + + u_nm1 = Wave_obj.u_nm1 + u_n = Wave_obj.u_n + u_np1 = fire.Function(Wave_obj.function_space) + + rhs_forcing = fire.Function(Wave_obj.function_space) + + B = Wave_obj.B + rhs = Wave_obj.rhs + + # Define a gradient problem + m_u = fire.TrialFunction(Wave_obj.function_space) + m_v = fire.TestFunction(Wave_obj.function_space) + mgrad = m_u * m_v * fire.dx(scheme=Wave_obj.quadrature_rule) + + dufordt2 = fire.Function(Wave_obj.function_space) + uadj = fire.Function(Wave_obj.function_space) # auxiliarly function for the gradient compt. + + ffG = -2 * (Wave_obj.c)**(-3) * fire.dot(dufordt2, uadj) * m_v * fire.dx(scheme=Wave_obj.quadrature_rule) + + lhsG = mgrad + rhsG = ffG + + gradi = fire.Function(Wave_obj.function_space) + grad_prob = fire.LinearVariationalProblem(lhsG, rhsG, gradi) + grad_solver = fire.LinearVariationalSolver( + grad_prob, + solver_parameters={ + "ksp_type": "preonly", + "pc_type": "jacobi", + "mat_type": "matfree", + }, + ) + + # assembly_callable = create_assembly_callable(rhs, tensor=B) + + for step in range(nt-1, -1, -1): + rhs_forcing.assign(0.0) + B = fire.assemble(rhs, tensor=B) + f = receivers.apply_receivers_as_source(rhs_forcing, residual, step) + B0 = B.sub(0) + B0 += f + Wave_obj.solver.solve(X, B) + + u_np1.assign(X) + + if (step) % Wave_obj.output_frequency == 0: + assert ( + fire.norm(u_n) < 1 + ), "Numerical instability. Try reducing dt or building the \ + mesh differently" + if Wave_obj.forward_output: + output.write(u_n, time=t, name="Pressure") + + helpers.display_progress(Wave_obj.comm, t) + + if step % Wave_obj.gradient_sampling_frequency == 0: + # duadjdt2.assign( ((u_np1 - 2.0 * u_n + u_nm1) / fire.Constant(dt**2)) ) + uadj.assign(u_np1) + if len(forward_solution) > 2: + dufordt2.assign( + (forward_solution.pop() - 2.0 * forward_solution[-1] + forward_solution[-2]) / fire.Constant(dt**2) + ) + else: + dufordt2.assign( + (forward_solution.pop() - 2.0 * 0.0 + 0.0) / fire.Constant(dt**2) + ) + + grad_solver.solve() + if step == nt-1 or step == 0: + dJ += gradi + else: + dJ += 2*gradi + + u_nm1.assign(u_n) + u_n.assign(u_np1) + + t = step * float(dt) + + Wave_obj.current_time = t + helpers.display_progress(Wave_obj.comm, t) + + dJ.dat.data_with_halos[:] *= (dt/2) + return dJ + + +def mixed_space_backward_wave_propagator(Wave_obj, dt=None): + """Propagates the adjoint wave backwards in time. + Currently uses central differences. Based on the + mixed space implementation of PML. + + Parameters: + ----------- + Wave_obj: Spyro wave object + Wave object that already propagated a forward wave. + dt: Python 'float' (optional) + Time step to be used explicitly. If not mentioned uses the default, + that was estabilished in the wave object for the adjoint model. + + Returns: + -------- + dJ: Firedrake 'Function' + Calculated gradient + """ + Wave_obj.reset_pressure() + if dt is not None: + Wave_obj.dt = dt + + forward_solution = Wave_obj.forward_solution + receivers = Wave_obj.receivers + residual = Wave_obj.misfit + comm = Wave_obj.comm + temp_filename = Wave_obj.forward_output_file + + filename, file_extension = temp_filename.split(".") + output_filename = "backward." + file_extension + + output = fire.File(output_filename, comm=comm.comm) + comm.comm.barrier() + + X = Wave_obj.X + dJ = fire.Function(Wave_obj.function_space) # , name="gradient") + + final_time = Wave_obj.final_time + dt = Wave_obj.dt + t = Wave_obj.current_time + if t != final_time: + print(f"Current time of {t}, different than final_time of {final_time}. Setting final_time to current time in backwards propagation.", flush= True) + nt = int(t / dt) + 1 # number of timesteps + + X_nm1 = Wave_obj.X_nm1 + X_n = Wave_obj.X_n + X_np1 = fire.Function(Wave_obj.mixed_function_space) + + rhs_forcing = fire.Function(Wave_obj.function_space) + + B = Wave_obj.B + rhs = Wave_obj.rhs + + # Define a gradient problem + m_u = fire.TrialFunction(Wave_obj.function_space) + m_v = fire.TestFunction(Wave_obj.function_space) + mgrad = m_u * m_v * fire.dx(scheme=Wave_obj.quadrature_rule) + + # dufordt2 = fire.Function(Wave_obj.function_space) + ufor = fire.Function(Wave_obj.function_space) + uadj = fire.Function(Wave_obj.function_space) # auxiliarly function for the gradient compt. + + # ffG = -2 * (Wave_obj.c)**(-3) * fire.dot(dufordt2, uadj) * m_v * fire.dx(scheme=Wave_obj.quadrature_rule) + ffG = 2.0 * Wave_obj.c * fire.dot(fire.grad(uadj), fire.grad(ufor)) * m_v * fire.dx(scheme=Wave_obj.quadrature_rule) + + lhsG = mgrad + rhsG = ffG + + gradi = fire.Function(Wave_obj.function_space) + grad_prob = fire.LinearVariationalProblem(lhsG, rhsG, gradi) + grad_solver = fire.LinearVariationalSolver( + grad_prob, + solver_parameters={ + "ksp_type": "preonly", + "pc_type": "jacobi", + "mat_type": "matfree", + }, + ) + + # assembly_callable = create_assembly_callable(rhs, tensor=B) + + for step in range(nt-1, -1, -1): + rhs_forcing.assign(0.0) + B = fire.assemble(rhs, tensor=B) + f = receivers.apply_receivers_as_source(rhs_forcing, residual, step) + B0 = B.sub(0) + B0 += f + Wave_obj.solver.solve(X, B) + + X_np1.assign(X) + + if (step) % Wave_obj.output_frequency == 0: + if Wave_obj.forward_output: + output.write(X_n.sub(0), time=t, name="Pressure") + + helpers.display_progress(Wave_obj.comm, t) + + if step % Wave_obj.gradient_sampling_frequency == 0: + # duadjdt2.assign( ((u_np1 - 2.0 * u_n + u_nm1) / fire.Constant(dt**2)) ) + uadj.assign(X_np1.sub(0)) + ufor.assign(forward_solution.pop()) + + grad_solver.solve() + if step == nt-1 or step == 0: + dJ += gradi + else: + dJ += 2*gradi + + X_nm1.assign(X_n) + X_n.assign(X_np1) + + t = step * float(dt) + + Wave_obj.current_time = t + helpers.display_progress(Wave_obj.comm, t) + + dJ.dat.data_with_halos[:] *= (dt/2) + 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 deleted file mode 100644 index e3659b10..00000000 --- a/spyro/solvers/gradient.py +++ /dev/null @@ -1,311 +0,0 @@ -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 diff --git a/spyro/solvers/gradient_old.py b/spyro/solvers/gradient_old.py new file mode 100644 index 00000000..3bcde5a2 --- /dev/null +++ b/spyro/solvers/gradient_old.py @@ -0,0 +1,210 @@ +# 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/inversion.py b/spyro/solvers/inversion.py new file mode 100644 index 00000000..98b56021 --- /dev/null +++ b/spyro/solvers/inversion.py @@ -0,0 +1,582 @@ +import firedrake as fire +import warnings +from scipy.optimize import minimize as scipy_minimize +from mpi4py import MPI +import numpy as np + +from .acoustic_wave import AcousticWave +from ..utils import compute_functional +from ..utils import Gradient_mask_for_pml, Mask +from ..plots import plot_model as spyro_plot_model + +try: + from ROL.firedrake_vector import FiredrakeVector as FireVector + import ROL + RObjective = ROL.Objective +except ImportError: + ROL = None + RObjective = object + +# ROL = None + + +class L2Inner(object): + def __init__(self, Wave_obj): + V = Wave_obj.function_space + # print(f"Dir {dir(Wave_obj)}", flush=True) + dxlump = fire.dx(scheme=Wave_obj.quadrature_rule) + self.A = fire.assemble( + fire.TrialFunction(V) * fire.TestFunction(V) * dxlump, + mat_type="matfree" + ) + self.Ap = fire.as_backend_type(self.A).mat() + + def eval(self, _u, _v): + upet = fire.as_backend_type(_u).vec() + vpet = fire.as_backend_type(_v).vec() + A_u = self.Ap.createVecLeft() + self.Ap.mult(upet, A_u) + return vpet.dot(A_u) + + +class Objective(RObjective): + def __init__(self, inner_product, FWI_obj): + if ROL is None: + raise ImportError("The ROL module is not available.") + ROL.Objective.__init__(self) + self.inner_product = inner_product + self.p_guess = None + self.misfit = 0.0 + self.real_shot_record = FWI_obj.real_shot_record + self.inversion_obj = FWI_obj + self.comm = FWI_obj.comm + + def value(self, x, tol): + """Compute the functional""" + J_total = np.zeros((1)) + self.inversion_obj.misfit = None + self.inversion_obj.reset_pressure() + Jm = self.inversion_obj.get_functional() + self.misfit = self.inversion_obj.misfit + J_total[0] += Jm + + return J_total[0] + + def gradient(self, g, x, tol): + """Compute the gradient of the functional""" + self.inversion_obj.get_gradient(calculate_functional=False) + dJ = self.inversion_obj.gradient + g.scale(0) + g.vec += dJ + + def update(self, x, flag, iteration): + vp = self.inversion_obj.initial_velocity_model + vp.assign(fire.Function( + self.inversion_obj.function_space, + x.vec, + name="velocity") + ) + + +class FullWaveformInversion(AcousticWave): + """ + The FullWaveformInversion class is a subclass of the AcousticWave class. + It is used to perform full waveform inversion on acoustic wave data. + + Attributes: + ----------- + dictionary: (dict) + A dictionary containing parameters for the inversion. + comm: MPI communicator + A communicator for parallel execution. + real_velocity_model: + The real velocity model. Is used only when generating synthetic shot records + real_velocity_model_file: (str) + The file containing the real velocity model. Is used only when generating synthetic shot records + guess_shot_record: + The guess shot record. + gradient: Firedrake function + The most recent gradient. + current_iteration: (int) + The current iteration. Starts at 0. + mesh_iteration: (int) + The current mesh iteration when using multiscale remeshing. Starts at 0., and is not used with default FWI. + iteration_limit: (int) + The iteration limit. Default is 100. + inner_product: (str) + The inner product. Default is 'L2'. + misfit: + The misfit between the current forward shot record and the real observed data. + guess_forward_solution: + The guess forward solution. + + Methods: + -------- + __init__(self, dictionary=None, comm=None): + Initializes a new instance of the FullWaveformInversion class. + calculate_misfit(): + Calculates the misfit. + generate_real_shot_record(): + Generates the real synthetic shot record. + set_smooth_guess_velocity_model(real_velocity_model_file=None): + Sets the smooth guess velocity model. + set_real_velocity_model(constant=None, conditional=None, velocity_model_function=None, expression=None, new_file=None, output=False): + Sets the real velocity model. + set_guess_velocity_model(constant=None, conditional=None, velocity_model_function=None, expression=None, new_file=None, output=False): + Sets the guess velocity model. + set_real_mesh(user_mesh=None, mesh_parameters=None): + Sets the real mesh. + set_guess_mesh(user_mesh=None, mesh_parameters=None): + Sets the guess mesh. + get_functional(): + Gets the functional. + get_gradient(save=False): + Gets the gradient. + """ + + def __init__(self, dictionary=None, comm=None): + """ + Initializes a new instance of the FullWaveformInversion class. + + Parameters: + ----------- + dictionary: (dict) + A dictionary containing parameters for the inversion. + comm: MPI communicator + A communicator for parallel execution. + + Returns: + -------- + None + """ + super().__init__(dictionary=dictionary, comm=comm) + if self.running_fwi is False: + warnings.warn("Dictionary FWI options set to not run FWI.") + self.real_velocity_model = None + self.real_velocity_model_file = None + self.guess_shot_record = None + self.gradient = None + self.current_iteration = 0 + self.mesh_iteration = 0 + self.iteration_limit = 100 + self.inner_product = 'L2' + self.misfit = None + self.guess_forward_solution = None + self.has_gradient_mask = False + self.functional_history = [] + self.control_out = fire.File("results/control.pvd") + self.gradient_out = fire.File("results/gradient.pvd") + + def calculate_misfit(self, c=None): + """ + Calculates the misfit, between the real shot record and the guess shot record. + If the guess forward model has already been run it uses that value. Otherwise, it runs the forward model. + """ + if self.mesh is None and self.guess_mesh is not None: + self.mesh = self.guess_mesh + if self.initial_velocity_model is None: + self.initial_velocity_model = self.guess_velocity_model + if c is not None: + self.initial_velocity_model.dat.data[:] = c + self.forward_solve() + output = fire.File("control_" + str(self.current_iteration)+".pvd") + output.write(self.c) + self.guess_shot_record = self.forward_solution_receivers + self.guess_forward_solution = self.forward_solution + + self.misfit = self.real_shot_record - self.guess_shot_record + return self.misfit + + def generate_real_shot_record(self, plot_model=False, filename=None, abc_points=None): + """ + Generates the real synthetic shot record. Only for use in synthetic test cases. + """ + Wave_obj_real_velocity = SyntheticRealAcousticWave(dictionary=self.input_dictionary, comm=self.comm) + if Wave_obj_real_velocity.mesh is None and self.real_mesh is not None: + Wave_obj_real_velocity.mesh = self.real_mesh + if Wave_obj_real_velocity.initial_velocity_model is None: + Wave_obj_real_velocity.initial_velocity_model = self.real_velocity_model + + if plot_model and Wave_obj_real_velocity.comm.comm.rank == 0 and Wave_obj_real_velocity.comm.ensemble_comm.rank == 0: + spyro_plot_model(Wave_obj_real_velocity, filename=filename, abc_points=abc_points) + + Wave_obj_real_velocity.forward_solve() + self.real_shot_record = Wave_obj_real_velocity.real_shot_record + self.quadrature_rule = Wave_obj_real_velocity.quadrature_rule + + def set_smooth_guess_velocity_model(self, real_velocity_model_file=None): + """ + Sets the smooth guess velocity model based on the real one. + + Parameters: + ----------- + real_velocity_model_file: (str) + The file containing the real velocity model. Is used only when generating synthetic shot records. + """ + if real_velocity_model_file is not None: + real_velocity_model_file = real_velocity_model_file + else: + real_velocity_model_file = self.real_velocity_model_file + + def set_real_velocity_model( + self, + constant=None, + conditional=None, + velocity_model_function=None, + expression=None, + new_file=None, + output=False, + dg_velocity_model=True, + ): + """" + Sets the real velocity model. Only to be used for synthetic cases. + + Parameters: + ----------- + conditional: (optional) + Firedrake conditional object. + velocity_model_function: Firedrake function (optional) + Firedrake function to be used as the velocity model. Has to be in the same function space as the object. + expression: str (optional) + If you use an expression, you can use the following variables: + x, y, z, pi, tanh, sqrt. Example: "2.0 + 0.5*tanh((x-2.0)/0.1)". + It will be interpoalte into either the same function space as the object or a DG0 function space + in the same mesh. + new_file: str (optional) + Name of the file containing the velocity model. + output: bool (optional) + If True, outputs the velocity model to a pvd file for visualization. + """ + super().set_initial_velocity_model( + constant=constant, + conditional=conditional, + velocity_model_function=velocity_model_function, + expression=expression, + new_file=new_file, + output=output, + dg_velocity_model=dg_velocity_model, + ) + self.real_velocity_model = self.initial_velocity_model + + def set_guess_velocity_model( + self, + constant=None, + conditional=None, + velocity_model_function=None, + expression=None, + new_file=None, + output=False, + ): + """" + Sets the initial guess. + + Parameters: + ----------- + conditional: (optional) + Firedrake conditional object. + velocity_model_function: Firedrake function (optional) + Firedrake function to be used as the velocity model. Has to be in the same function space as the object. + expression: str (optional) + If you use an expression, you can use the following variables: + x, y, z, pi, tanh, sqrt. Example: "2.0 + 0.5*tanh((x-2.0)/0.1)". + It will be interpoalte into either the same function space as the object or a DG0 function space + in the same mesh. + new_file: str (optional) + Name of the file containing the velocity model. + output: bool (optional) + If True, outputs the velocity model to a pvd file for visualization. + """ + super().set_initial_velocity_model( + constant=constant, + conditional=conditional, + velocity_model_function=velocity_model_function, + expression=expression, + new_file=new_file, + output=output, + ) + self.guess_velocity_model = self.initial_velocity_model + self.misfit = None + + def set_real_mesh( + self, + user_mesh=None, + mesh_parameters=None, + ): + """ + Set the mesh for the real synthetic model. + + Parameters + ---------- + user_mesh : spyro.Mesh, optional + The desired mesh. The default is None. + mesh_parameters : dict, optional + Additional parameters for setting up the mesh. The default is an empty dictionary. + + Returns + ------- + None + """ + super().set_mesh( + user_mesh=user_mesh, + mesh_parameters=mesh_parameters, + ) + self.real_mesh = self.get_mesh() + + def set_guess_mesh( + self, + user_mesh=None, + mesh_parameters=None, + ): + """ + Set the mesh for the guess model. + + Parameters + ---------- + user_mesh : spyro.Mesh, optional + The desired mesh. The default is None. + mesh_parameters : dict, optional + Additional parameters for setting up the mesh. The default is an empty dictionary. + + Returns + ------- + None + """ + super().set_mesh( + user_mesh=user_mesh, + mesh_parameters=mesh_parameters, + ) + self.guess_mesh = self.get_mesh() + + def get_functional(self, c=None): + """ + Calculate and return the functional value. + + If the misfit is already computed, the functional value is calculated using the precomputed misfit. + Otherwise, the misfit is calculated first and then the functional value is computed. + + Returns: + float: The functional value. + """ + self.calculate_misfit(c=c) + Jm = compute_functional(self, self.misfit) + + self.functional_history.append(Jm) + self.functional = Jm + + return Jm + + def get_gradient(self, c=None, save=True, calculate_functional=True): + """ + Calculates the gradient of the functional with respect to the model parameters. + + Parameters: + ----------- + save (bool, optional): + Whether to save the gradient as a pvd file. Defaults to False. + + Returns: + -------- + Firedrake function + """ + comm = self.comm + if calculate_functional: + self.get_functional(c=c) + comm.comm.barrier() + dJ = self.gradient_solve(misfit=self.misfit, forward_solution=self.guess_forward_solution) + dJ_total = fire.Function(self.function_space) + comm.comm.barrier() + dJ_total = comm.allreduce(dJ, dJ_total) + dJ_total /= comm.ensemble_comm.size + if comm.comm.size > 1: + dJ_total /= comm.comm.size + self.gradient = dJ_total + self._apply_gradient_mask() + if save and comm.comm.rank == 0: + # self.gradient_out.write(dJ_total) + output = fire.File("gradient_" + str(self.current_iteration)+".pvd") + output.write(dJ_total) + print("DEBUG") + self.current_iteration += 1 + comm.comm.barrier() + + def return_functional_and_gradient(self, c): + self.get_gradient(c=c) + dJ = self.gradient.dat.data[:] + return self.functional, dJ + + def run_fwi(self, **kwargs): + """ + Run the full waveform inversion. + """ + parameters = { + "vmin": 1.429, + "vmax": 6.0, + "scipy_options": { + "disp": True, + "eps": 1e-15, + "gtol": 1e-15, "maxiter": kwargs.pop("maxiter", 20), + } + } + parameters.update(kwargs) + + vmin = parameters["vmin"] + vmax = parameters["vmax"] + vp_0 = self.initial_velocity_model.vector().gather() + bounds = [(vmin, vmax) for _ in range(len(vp_0))] + options = parameters["scipy_options"] + + # if self.running_fwi is False: + # warnings.warn("Dictionary FWI options set to not run FWI.") + # if self.current_iteration < self.iteration_limit: + # self.get_gradient() + # self.update_guess_model() + # self.current_iteration += 1 + # else: + # warnings.warn("Iteration limit reached. FWI stopped.") + # self.running_fwi = False + result = scipy_minimize( + self.return_functional_and_gradient, + vp_0, + method="L-BFGS-B", + jac=True, + tol=1e-15, + bounds=bounds, + options=options, + ) + vp_end = fire.Function(self.function_space) + vp_end.dat.data[:] = result.x + fire.File("vp_end.pvd").write(vp_end) + + def run_fwi_rol(self, **kwargs): + """ + Run the full waveform inversion using ROL. + """ + if ROL is None: + raise ImportError("The ROL module is not available.") + parameters = { + "vmin": 1.429, + "vmax": 6.0, + "ROL_options": { + "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": kwargs.pop("maxiter", 20), + "Step Tolerance": 1.0e-16, + }, + } + } + parameters.update(kwargs) + vmin = parameters["vmin"] + vmax = parameters["vmax"] + + warnings.warn("This functionality is deprecated, since the pyROL library is no longer supported.") + params = ROL.ParameterList(parameters["ROL_options"], "Parameters") + + inner_product = L2Inner(self) + + obj = Objective(inner_product, self) + + u = fire.Function(self.function_space, name="velocity").assign(self.guess_velocity_model) + opt = FireVector(u.vector(), inner_product) + + # Add control bounds to the problem (uses more RAM) + xlo = fire.Function(self.function_space) + xlo.interpolate(fire.Constant(vmin)) + x_lo = FireVector(xlo.vector(), inner_product) + + xup = fire.Function(self.function_space) + xup.interpolate(fire.Constant(vmax)) + x_up = FireVector(xup.vector(), inner_product) + + bnd = ROL.Bounds(x_lo, x_up, 1.0) + + algo = ROL.Algorithm("Line Search", params) + + algo.run(opt, obj, bnd) + + def set_gradient_mask(self, boundaries=None): + """ + Sets the gradient mask for zeroing gradient values outside defined boundaries. + + Args: + boundaries (list, optional): List of boundary values for the mask. If not provided, + the method expects the abc_active to be True and uses PML locations for boundary + values. + + Raises: + ValueError: If no abc boundary is present in the object and boundaries is None. + ValueError: If mask options do not make sense. + + Warnings: + UserWarning: If abc_active is True and boundaries is not None, the boundaries will + override the PML boundaries for the mask. + + """ + self.has_gradient_mask = True + + if self.abc_active is False and boundaries is None: + raise ValueError("If no abc boundary please define boundaries for the mask") + elif self.abc_active and boundaries is None: + mask_obj = Gradient_mask_for_pml(self) + elif self.abc_active and boundaries is not None: + warnings.warn("Boundaries overuling PML boundaries for mask") + mask_obj = Mask(boundaries, self) + elif self.abc_active is False and boundaries is not None: + mask_obj = Mask(boundaries, self) + else: + raise ValueError("Mask options do not make sense") + + self.mask_obj = mask_obj + + def _apply_gradient_mask(self): + """ + Applies a gradient mask to the gradient if it exists. + + If a gradient mask is available, this method applies the mask to the gradient + using the `apply_mask` method of the `mask_obj`. If no gradient mask is available, + this method does nothing. + + Parameters: + None + + Returns: + None + """ + if self.has_gradient_mask: + self.gradient = self.mask_obj.apply_mask(self.gradient) + else: + pass + + +class SyntheticRealAcousticWave(AcousticWave): + """ + The SyntheticRealAcousticWave class is a subclass of the AcousticWave class. + It is used to generate synthetic real acoustic wave data. + + Attributes: + ----------- + dictionary: (dict) + A dictionary containing parameters for the inversion. + comm: MPI communicator + + Methods: + -------- + __init__(self, dictionary=None, comm=None): + Initializes a new instance of the SyntheticRealAcousticWave class. + forward_solve(): + Solves the forward problem. + """ + def __init__(self, dictionary=None, comm=None): + super().__init__(dictionary=dictionary, comm=comm) + + def forward_solve(self): + super().forward_solve() + self.real_shot_record = self.receivers_output 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..410bf5cd --- /dev/null +++ b/spyro/solvers/time_integration_central_difference.py @@ -0,0 +1,317 @@ +import firedrake as fire +from firedrake import Constant, dx, dot, grad + +from ..io.basicio import parallel_print +from . import helpers +from .. import utils + + +def central_difference(Wave_object, source_id=0): + """ + Perform central difference time integration for wave propagation. + + Parameters: + ----------- + Wave_object: Spyro object + The Wave object containing the necessary data and parameters. + source_id: int (optional) + The ID of the source being propagated. Defaults to 0. + + Returns: + -------- + tuple: + A tuple containing the forward solution and the receiver output. + """ + 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 + + 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): + """ + Performs central difference time integration for wave propagation. + Solves for a mixed space formulation, for function X. For correctly + outputing pressure, order the mixed function space so that the space + pressure lives in is first. + + Parameters: + ----------- + Wave_object: Spyro object + The Wave object containing the necessary data and parameters. + source_id: int (optional) + The ID of the source being propagated. Defaults to 0. + + Returns: + -------- + tuple: + A tuple containing the forward solution and the receiver output. + """ + 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 + + 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) + + 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 + + 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..4351d17c --- /dev/null +++ b/spyro/solvers/wave.py @@ -0,0 +1,317 @@ +import os +from abc import abstractmethod +import warnings +import firedrake as fire +from firedrake import sin, cos, pi, tanh, sqrt # 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.wavelet = self.get_wavelet() + self.mesh = self.get_mesh() + self.c = None + if self.mesh is not None: + 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, + user_mesh=None, + mesh_parameters=None, + ): + """ + Set the mesh for the solver. + + Args: + user_mesh (optional): User-defined mesh. Defaults to None. + mesh_parameters (optional): Parameters for generating a mesh. Defaults to None. + """ + super().set_mesh( + user_mesh=user_mesh, + mesh_parameters=mesh_parameters, + ) + + self.mesh = self.get_mesh() + self._build_function_space() + self._map_sources_and_receivers() + + def set_solver_parameters(self, parameters=None): + """ + Set the solver parameters. + + Args: + parameters (dict): A dictionary containing the solver parameters. + + Returns: + 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, + dg_velocity_model=True, + ): + """Method to define new user velocity model or file. It is optional. + + Parameters: + ----------- + conditional: (optional) + Firedrake conditional object. + velocity_model_function: Firedrake function (optional) + Firedrake function to be used as the velocity model. Has to be in the same function space as the object. + expression: str (optional) + If you use an expression, you can use the following variables: + x, y, z, pi, tanh, sqrt. Example: "2.0 + 0.5*tanh((x-2.0)/0.1)". + It will be interpoalte into either the same function space as the object or a DG0 function space + in the same mesh. + new_file: str (optional) + Name of the file containing the velocity model. + output: bool (optional) + If True, outputs the velocity model to a pvd file for visualization. + """ + # If no mesh is set, we have to do it beforehand + if self.mesh is None: + self.set_mesh() + # 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: + if dg_velocity_model: + V = fire.FunctionSpace(self.mesh, "DG", 0) + else: + V = self.function_space + 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, + self.initial_velocity_model_file, + self.function_space.sub(0), + ) + + if self.debug_output: + fire.File("initial_velocity_model.pvd").write( + self.initial_velocity_model, name="velocity" + ) + + def _build_function_space(self): + self.function_space = FE_method(self.mesh, self.method, self.degree) + 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 get_and_set_maximum_dt(self, fraction=0.7, estimate_max_eigenvalue=False): + """ + Calculates and sets the maximum stable time step (dt) for the wave solver. + + Args: + fraction (float, optional): + Fraction of the estimated time step to use. Defaults to 0.7. + estimate_max_eigenvalue (bool, optional): + Whether to estimate the maximum eigenvalue. Defaults to False. + + Returns: + float: The calculated maximum time step (dt). + """ + # if self.method == "mass_lumped_triangle": + # estimate_max_eigenvalue = True + # elif self.method == "spectral_quadrilateral": + # estimate_max_eigenvalue = True + # else: + + 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 + + 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..32e47c89 100644 --- a/spyro/tools/__init__.py +++ b/spyro/tools/__init__.py @@ -1,19 +1,32 @@ -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 +from .velocity_smoother import smooth_velocity_field_file + __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", + "smooth_velocity_field_file", ] + +# 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..8c92391c --- /dev/null +++ b/spyro/tools/cells_per_wavelength_calculator.py @@ -0,0 +1,517 @@ +import numpy as np +import time as timinglib +import copy +from .input_models import create_initial_model_for_meshing_parameter +import spyro + + +class Meshing_parameter_calculator: + """ + A class used to calculate meshing parameter C (cells-per-wavelength). + + ... + + Attributes + ---------- + parameters_dictionary : dict + a dictionary containing all the parameters needed for the calculation + source_frequency : float + the source frequency in Hz used for the calculation + minimum_velocity : float + the minimum velocity in the domain in km/s + velocity_profile_type : str + the type of velocity profile, either "homogeneous" or "heterogeneous" + velocity_model_file_name : str + the file name of the velocity model .segy file + FEM_method_to_evaluate : str + the Finite Element Method to be evaluated, either "mass_lumped_triangle" or "spectral_quadrilateral" + dimension : int + the spatial dimension of the problem (either 2 or 3) + receiver_setup : str + the setup of the receiver either "near", "line", or "far" + accepted_error_threshold : float + the accepted error threshold for the calculation. The cpw calculation stops when + the error is below this threshold. Usually 0.05. + desired_degree : int + the desired polynoial element degree for the calculation + reference_degree : int + the polynomial degree to be used for the calculation of the reference case + cpw_reference : float + the cells-per-wavelength to be used for mesh generation of the reference solution + cpw_initial : float + the initial guess for the cells-per-wavelength parameter + cpw_accuracy : float + the accuracy of the cells-per-wavelength parameter + reduced_obj_for_testing : bool + a boolean to reduce the object size for testing purposes + save_reference : bool + a boolean to chose to save the reference solution + load_reference : bool + a boolean to load the reference solution, if used paramters_dictionary should also have a "reference_solution_file" key. + timestep_calculation : str + a string to define the time-step calculation method, either "exact", "estimate", or "float". + fixed_timestep : float + a float to define the fixed time-step if the time-step calculation method is "float" + estimate_timestep : bool + a boolean to define if the time-step should be estimated + initial_guess_object : spyro.AcousticWave + the initial guess object for the calculation + comm : mpi4py.MPI.Intracomm + the MPI communicator + reference_solution : np.ndarray + the reference solution + initial_dictionary : dict + the initial dictionary used to build the initial guess object + + Methods + ------- + _check_velocity_profile_type(): + Checks the type of velocity profile. + _check_heterogenous_mesh_lengths(): + Checks the lengths of the heterogeneous mesh. + build_initial_guess_model(): + Builds the initial guess model. + get_reference_solution(): + Gets or generates the reference solution. + calculate_reference_solution(): + Calculates the reference solution. + calculate_analytical_solution(): + Calculates the analytical reference solution if it is possible. + find_minimum(starting_cpw=None, TOL=None, accuracy=None, savetxt=False): + Finds the minimum cells-per-wavelength meshing parameter. + build_current_object(cpw, degree=None): + Builds the current acoustic wave solver object. + """ + def __init__(self, parameters_dictionary): + """ + Initializes the Meshing_parameter_calculator class with a dictionary of parameters. + + Parameters + ---------- + parameters_dictionary : dict + A dictionary containing all the parameters needed for the calculation. It should include: + - "source_frequency": float, the source frequency for the calculation + - "minimum_velocity_in_the_domain": float, the minimum velocity in the domain for the calculation + - "velocity_profile_type": str, the type of velocity profile for the calculation + - "velocity_model_file_name": str, the file name of the velocity model for the calculation + - "FEM_method_to_evaluate": str, the Finite Element Method to be evaluated for the calculation + - "dimension": int, the dimension of the problem + - "receiver_setup": str, the setup of the receiver + - "accepted_error_threshold": float, the accepted error threshold for the calculation + - "desired_degree": int, the desired degree for the calculation + """ + 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._check_velocity_profile_type() + 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 + self._setting_up_testing_options() + + # Setting up reference file read or load + self._setting_up_reference_file_read_or_load() + + # Setting up time-step attributes + self._setting_up_time_step() + + self.initial_guess_object = self.build_initial_guess_model() + self.comm = self.initial_guess_object.comm + self.reference_solution = self.get_reference_solution() + + def _setting_up_testing_options(self): + """ + Sets up the testing options. + """ + if "testing" in self.parameters_dictionary: + self.reduced_obj_for_testing = self.parameters_dictionary["testing"] + else: + self.reduced_obj_for_testing = False + + def _setting_up_reference_file_read_or_load(self): + if "save_reference" in self.parameters_dictionary: + self.save_reference = self.parameters_dictionary["save_reference"] + else: + self.save_reference = False + + if "load_reference" in self.parameters_dictionary: + self.load_reference = self.parameters_dictionary["load_reference"] + else: + self.load_reference = False + + def _setting_up_time_step(self): + if "time-step_calculation" in self.parameters_dictionary: + self.timestep_calculation = self.parameters_dictionary["time-step_calculation"] + else: + self.timestep_calculation = "exact" + self.fixed_timestep = None + + if self.timestep_calculation == "exact": + self.estimate_timestep = False + elif self.timestep_calculation == "estimate": + self.estimate_timestep = True + else: + self.estimate_timestep = None + self.fixed_timestep = self.parameters_dictionary["time-step"] + + def _check_velocity_profile_type(self): + if self.velocity_profile_type == "homogeneous": + if self.velocity_model_file_name is not None: + raise ValueError( + "Velocity model file name should be None for homogeneous models" + ) + elif self.velocity_profile_type == "heterogeneous": + self._check_heterogenous_mesh_lengths() + if self.velocity_model_file_name is None: + raise ValueError( + "Velocity model file name should be defined for heterogeneous models" + ) + else: + raise ValueError( + "Velocity profile type is not homogeneous or heterogeneous" + ) + + def _check_heterogenous_mesh_lengths(self): + parameters = self.parameters_dictionary + if "length_z" not in parameters: + raise ValueError("Length in z direction not defined") + if "length_x" not in parameters: + raise ValueError("Length in x direction not defined") + if parameters["length_z"] is None: + raise ValueError("Length in z direction not defined") + if parameters["length_x"] is None: + raise ValueError("Length in x direction not defined") + if parameters["length_z"] < 0.0: + parameters["length_z"] = abs(parameters["length_z"]) + if parameters["length_x"] < 0.0: + raise ValueError("Length in x direction must be positive") + + def build_initial_guess_model(self): + """ + Builds the initial guess spyro acoustic wave solver object. + + Returns + ------- + spyro.AcousticWave + the initial guess spyro acoustic wave solver object + """ + dictionary = create_initial_model_for_meshing_parameter(self) + self.initial_dictionary = dictionary + return spyro.AcousticWave(dictionary) + + def get_reference_solution(self): + """ + Calculates or loads the reference solution to be used for error calculation. + + Returns + ------- + np.ndarray + the reference solution + """ + 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": + return self.calculate_reference_solution() + elif self.velocity_profile_type == "homogeneous": + return self.calculate_analytical_solution() + + def calculate_reference_solution(self): + """ + Calculates the numerical reference solution for heterogeneous models, using cpw and degree values in parameters dictionary. + + Returns + ------- + np.ndarray + the reference solution + """ + Wave_obj = self.build_current_object(self.cpw_reference, degree=self.reference_degree) + + Wave_obj.forward_solve() + p_receivers = Wave_obj.forward_solution_receivers + + if self.save_reference: + np.save("reference_solution.npy", p_receivers) + + return p_receivers + + def calculate_analytical_solution(self): + """ + Calculates the analytical reference solution for homogeneous models. + + Returns + ------- + np.ndarray + the reference solution + """ + # 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, savetxt=False): + """ + Finds the minimum cells-per-wavelength meshing parameter that is still below the error threshold. + + Parameters + ---------- + starting_cpw : float (optional) + the starting cells-per-wavelength parameter to be used in the search. If None, + the value from paramters_dictionary is used. + TOL : float (optional) + the accepted error threshold for the calculation. The cpw calculation stops when + the error is below this threshold. Usually 0.05. If None, the value from paramters_dictionary is used. + accuracy : float (optional) + the accuracy of the cells-per-wavelength parameter. If None, the value from paramters_dictionary is used. + savetxt : bool (optional) + a boolean to chose to save the results to a text file + + Returns + ------- + cpw : float + the minimum cells-per-wavelength parameter that is still below the error threshold + """ + 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) + cpws = [] + dts = [] + errors = [] + runtimes = [] + + self.fast_loop = True + # fast_loop = False + 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_initial_velocity_model() + + # Setting up time-step + if self.timestep_calculation != "float": + Wave_obj.get_and_set_maximum_dt( + fraction=0.2, + estimate_max_eigenvalue=self.estimate_timestep + ) + else: + Wave_obj.dt = self.fixed_timestep + print("Maximum dt is ", Wave_obj.dt, flush=True) + + t0 = timinglib.time() + Wave_obj.forward_solve() + t1 = timinglib.time() + 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) + cpws.append(cpw) + dts.append(Wave_obj.dt) + errors.append(error) + runtimes.append(t1 - t0) + + cpw, error, dif = self._updating_cpw_error_and_dif(cpw, error, dif) + + cont += 1 + + self._saving_file(savetxt, np.transpose([cpws, dts, errors, runtimes])) + + return cpw - dif + + def build_current_object(self, cpw, degree=None): + """ + Builds the current acoustic wave solver object. + + Parameters + ---------- + cpw : float + the current cells-per-wavelength parameter + degree : int (optional) + the polynomial degree to be used in the calculation. If None, the value from paramters_dictionary is used. + + Returns + ------- + spyro.AcousticWave + the current acoustic wave solver object + """ + dictionary = copy.deepcopy(self.initial_dictionary) + dictionary["mesh"]["cells_per_wavelength"] = cpw + if degree is not None: + dictionary["options"]["degree"] = degree + Wave_obj = spyro.AcousticWave(dictionary) + if self.velocity_profile_type == "homogeneous": + lba = self.minimum_velocity / self.source_frequency + edge_length = lba / cpw + Wave_obj.set_mesh(mesh_parameters={"edge_length": edge_length}) + Wave_obj.set_initial_velocity_model(constant=self.minimum_velocity) + elif self.velocity_profile_type == "heterogeneous": + Wave_obj.set_mesh(mesh_parameters={"cells_per_wavelength": cpw}) + return Wave_obj + + def _saving_file(self, savetxt, info): + """ + Saves the results to a text file. + """ + if savetxt: + np.savetxt( + "p"+str(self.initial_guess_object.degree)+"_cpw_results.txt", + info, + ) + + def _updating_cpw_error_and_dif(self, cpw, error, dif): + """ + Updates the cells-per-wavelength parameter. + """ + if error < self.accepted_error_threshold and dif > self.cpw_accuracy: + cpw -= dif + error = 100.0 + # Flooring CPW to the neartest decimal point inside accuracy + cpw = np.round( + (cpw + 1e-6) // self.cpw_accuracy * self.cpw_accuracy, + int(-np.log10(self.cpw_accuracy)), + ) + self.fast_loop = False + else: + dif = calculate_dif(cpw, self.cpw_accuracy, fast_loop=self.fast_loop) + cpw += dif + + return cpw, error, dif + + +def calculate_dif(cpw, accuracy, fast_loop=False): + """ + Calculates the difference between consecutive cells-per-wavelength to be used in the search. + + Parameters + ---------- + cpw : float + the current cells-per-wavelength parameter + accuracy : float + the accuracy of the cells-per-wavelength parameter + fast_loop : bool + a boolean to chose to use a fast loop or not + + Returns + ------- + dif : float + the difference between consecutive cells-per-wavelength to be used in the search + """ + if fast_loop: + dif = max(0.1 * cpw, accuracy) + else: + dif = accuracy + + return dif + + +def error_calc(receivers, analytical, dt): + """ + Calculates the error between the numerical and analytical solutions. + + Parameters + ---------- + receivers : np.ndarray + the numerical solution to be evaluated + analytical : np.ndarray + the analytical or reference solution + dt : float + the time-step used in the numerical solution + + Returns + ------- + error : float + the error between the numerical and analytical solutions + """ + 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..12b85ce3 100644 --- a/spyro/tools/input_models.py +++ b/spyro/tools/input_models.py @@ -1,504 +1,356 @@ import numpy as np import spyro -import shutil +import warnings -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 - +def build_on_top_of_base_dictionary(variables): """ - (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] - - -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. + Builds a model dictionary on top of the base dictionary. Parameters ---------- - grid_point_calculator_parameters: Python 'dictionary' + variables : dict + Dictionary containing the variables to be used in the model dictionary. It should include: + - method: string + The finite element method to be used. Either "mass_lumped_triangle" or "spectral_quadrilateral". + - degree: int + The spatial polynomial degree of the finite element method + - dimension: int + The dimension of the problem. Either 2 or 3. + - Lz: float + The length of the domain in the z direction. + - Lx: float + The length of the domain in the x direction. + - Ly: float + The length of the domain in the y direction. + - cells_per_wavelength: float + The number of cells per wavelength. + - pad: float + The padding to be used in the domain. + - source_locations: list + A list containing the source locations. + - frequency: float + The frequency of the source. + - receiver_locations: list + A list containing the receiver locations. + - final_time: float + The final time of the simulation. + - dt: float + The time step size of the simulation. Returns ------- - model: Python `dictionary` - Contains model options and parameters for use in Spyro - - + model_dictionary : dict + Dictionary containing the model dictionary. """ - 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"] - - 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 - - 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 - - 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_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)) - ) - - # 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 + mesh_type = set_mesh_type(variables["method"]) + model_dictionary = {} + model_dictionary["options"] = { + "method": variables["method"], + "degree": variables["degree"], + "dimension": variables["dimension"], + "automatic_adjoint": False, } - - 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_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["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_dictionary["absorving_boundary_conditions"] = { + "status": True, + "damping_type": "PML", + "exponent": 2, + "cmax": 4.5, + "R": 1e-6, + "pad_length": variables["pad"], } - - 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_dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": variables["source_locations"], + "frequency": variables["frequency"], + "receiver_locations": variables["receiver_locations"], } - 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_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["testing_parameters"] = { - "minimum_mesh_velocity": minimum_mesh_velocity, - "pml_fraction": padz / Lz, - "receiver_type": receiver_type, - "experiment_type": "homogeneous", + model_dictionary["visualization"] = { + "forward_output": True, + "forward_output_filename": "results/reference_forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, } - - return model + return model_dictionary -def create_model_2D_heterogeneous(grid_point_calculator_parameters, degree): - """Creates models with the correct parameters for for grid point calculation experiments. +def set_mesh_type(method): + """ + Sets the mesh type based on the method. 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 + method : string + The finite element method to be used. Either "mass_lumped_triangle" or "spectral_quadrilateral". Returns ------- - model: Python `dictionary` - Contains model options and parameters for use in Spyro - - + mesh_type : string + The mesh type to be used. """ - 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.") + if method == "mass_lumped_triangle": + mesh_type = "SeismicMesh" + elif method == "spectral_quadrilateral": + mesh_type = "firedrake_mesh" else: - print( - "Warning: running without a velocity model is suitable for testing purposes only.", - flush=True, - ) - padz = pad - padx = pad - - if receiver_type == "bins": + raise ValueError("Method is not mass_lumped_triangle or spectral_quadrilateral") + return mesh_type - # time calculations - tmin = 1.0 / frequency - final_time = 25 * tmin # should be 35 - # receiver calculations +def create_initial_model_for_meshing_parameter(Meshing_calc_obj): + """ + Creates an initial model dictionary for the meshing parameter calculation. - receiver_bin_center1 = 2.5 * 750.0 / 1000 - receiver_bin_width = 500.0 / 1000 - receiver_quantity_in_bin = 100 # 2500 # 50 squared + Parameters + ---------- + Meshing_calc_obj : spyro.Meshing_parameter_calculator + The meshing calculation object. - 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 + Returns + ------- + model_dictionary : dict + Dictionary containing the initial model dictionary to be later incremented. + """ + 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") - 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 +def create_initial_model_for_meshing_parameter_2D(Meshing_calc_obj): + """ + Creates an initial model dictionary for the meshing parameter calculation in 2D. - 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 + Parameters + ---------- + Meshing_calc_obj : spyro.Meshing_parameter_calculator + The meshing calculation object. - receiver_coordinates = receiver_coordinates + spyro.create_2d_grid( - bin2_startZ, - bin2_endZ, - bin2_startX, - bin2_endX, - int(np.sqrt(receiver_quantity_in_bin)), + Returns + ------- + model_dictionary : dict + Dictionary containing the initial model dictionary to be later incremented. + """ + 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": + return create_initial_model_for_meshing_parameter_2D_heterogeneous(Meshing_calc_obj) + else: + raise ValueError( + "Velocity profile type is not homogeneous or heterogeneous" ) - receiver_quantity = 2 * receiver_quantity_in_bin - if receiver_type == "line": +def create_initial_model_for_meshing_parameter_2D_heterogeneous(Meshing_calc_obj): + """ + Creates an initial model dictionary for the meshing parameter calculation in 2D with a heterogeneous velocity model. - # time calculations - tmin = 1.0 / frequency - final_time = 2 * 10 * tmin + 5.0 # should be 35 + Parameters + ---------- + Meshing_calc_obj : spyro.Meshing_parameter_calculator + The meshing calculation object. + + Returns + ------- + model_dictionary : dict + Dictionary containing the initial model dictionary. + """ + dimension = 2 + c_value = Meshing_calc_obj.minimum_velocity + frequency = Meshing_calc_obj.source_frequency + cells_per_wavelength = Meshing_calc_obj.cpw_initial - # receiver calculations + method = Meshing_calc_obj.FEM_method_to_evaluate + degree = Meshing_calc_obj.desired_degree + reduced = Meshing_calc_obj.reduced_obj_for_testing - receiver_bin_center1 = 2000.0 / 1000 - receiver_bin_center2 = 10000.0 / 1000 - receiver_quantity = 500 + # Domain calculations + lbda = c_value / frequency + pad = lbda - bin1_startZ = source_z - bin1_endZ = source_z - bin1_startX = source_x + receiver_bin_center1 - bin1_endX = source_x + receiver_bin_center2 + parameters = Meshing_calc_obj.parameters_dictionary + length_z = parameters["length_z"] + length_x = parameters["length_x"] + + # Source and receiver calculations + source_z = -0.3 + source_x = 3.0 + source_locations = [(source_z, source_x)] + + # 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_locations = spyro.create_transect( + (bin1_startZ, bin1_startX), + (bin1_endZ, bin1_endX), + receiver_quantity + ) - receiver_coordinates = spyro.create_transect( - (bin1_startZ, bin1_startX), (bin1_endZ, bin1_endX), receiver_quantity - ) + # Time axis calculations + tmin = 1.0 / frequency + final_time = 7.5 - # Choose method and parameters - model["opts"] = { + variables = { "method": method, - "variant": None, - "element": "tria", # tria or tetra - "degree": degree, # p order - "dimension": dimension, # dimension + "degree": degree, + "dimension": dimension, + "Lz": length_z, + "Lx": length_x, + "Ly": 0.0, + "cells_per_wavelength": cells_per_wavelength, + "pad": pad, + "source_locations": source_locations, + "frequency": frequency, + "receiver_locations": receiver_locations, + "final_time": final_time, + "dt": 0.0001, } - 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_dictionary = build_on_top_of_base_dictionary(variables) - 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_dictionary["synthetic_data"] = { + "real_velocity_file": Meshing_calc_obj.velocity_model_file_name, } - 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, - } + return model_dictionary - 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_initial_model_for_meshing_parameter_3D(Meshing_calc_obj): + """ + Creates an initial model dictionary for the meshing parameter calculation in 3D. + + Parameters + ---------- + Meshing_calc_obj : spyro.Meshing_parameter_calculator + The meshing calculation object. + Returns + ------- + model_dictionary : dict + Dictionary containing the initial model dictionary. + """ + 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" + ) -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"] +def create_initial_model_for_meshing_parameter_2D_homogeneous(Meshing_calc_obj): + """ + Creates an initial model dictionary for the meshing parameter calculation in 2D with a homogeneous velocity model. - model = {} + Parameters + ---------- + Meshing_calc_obj : spyro.Meshing_parameter_calculator + The meshing calculation object. - lbda = minimum_mesh_velocity / frequency + Returns + ------- + model_dictionary : dict + Dictionary containing the initial model dictionary. + """ + dimension = 2 + c_value = Meshing_calc_obj.minimum_velocity + frequency = Meshing_calc_obj.source_frequency + cells_per_wavelength = Meshing_calc_obj.cpw_initial + + method = Meshing_calc_obj.FEM_method_to_evaluate + degree = Meshing_calc_obj.desired_degree + reduced = Meshing_calc_obj.reduced_obj_for_testing + + if c_value > 500: + warnings.warn("Velocity in meters per second") + + # Domain calculations + lbda = c_value / frequency + Lz = 40 * lbda + Lx = 30 * lbda + Ly = 0.0 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 - 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_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 + 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 = create_3d_grid( - (bin1_startZ, bin1_startX, bin1_startY), (bin1_endZ, bin1_endX, bin1_endY), 6 + 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, - "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 - } + # Time axis calculations + tmin = 1.0 / frequency + final_time = 20 * tmin # Should be 35 - model["acquisition"] = { - "source_type": "Ricker", - "num_sources": 1, - "source_pos": source_coordinates, + variables = { + "method": method, + "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, + "receiver_locations": receiver_locations, + "final_time": final_time, + "dt": 0.0005, } - 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", - } - - # 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/tools/velocity_smoother.py b/spyro/tools/velocity_smoother.py new file mode 100644 index 00000000..44d6a057 --- /dev/null +++ b/spyro/tools/velocity_smoother.py @@ -0,0 +1,75 @@ +import os +from scipy.ndimage import gaussian_filter +import segyio +import numpy as np +import matplotlib.pyplot as plt + + +def smooth_velocity_field_file(input_filename, output_filename, sigma, show=False): + """Smooths a velocity field using a Gaussian filter. + + Parameters + ---------- + input_filename : string + The name of the input file. + output_filename : string + The name of the output file. + sigma : float + The standard deviation of the Gaussian filter. + show : boolean, optional + Should the plot image appear on screen + + Returns + ------- + None + + """ + f, filetype = os.path.splitext(input_filename) + + if filetype == ".segy": + with segyio.open(input_filename, ignore_geometry=True) as f: + nz, nx = len(f.samples), len(f.trace) + vp = np.zeros(shape=(nz, nx)) + for index, trace in enumerate(f.trace): + vp[:, index] = trace + else: + raise ValueError("Not yet implemented!") + + vp_smooth = gaussian_filter(vp, sigma) + ni, nj = np.shape(vp) + + for i in range(ni): + for j in range(nj): + if vp[i, j] < 1.51 and i < 400: + vp_smooth[i, j] = vp[i, j] + + spec = segyio.spec() + spec.sorting = 2 # not sure what this means + spec.format = 1 # not sure what this means + spec.samples = range(vp_smooth.shape[0]) + spec.ilines = range(vp_smooth.shape[1]) + spec.xlines = range(vp_smooth.shape[0]) + + assert np.sum(np.isnan(vp_smooth[:])) == 0 + + with segyio.create(output_filename, spec) as f: + for tr, il in enumerate(spec.ilines): + f.trace[tr] = vp_smooth[:, tr] + + if show is True: + with segyio.open(output_filename, ignore_geometry=True) as f: + nz, nx = len(f.samples), len(f.trace) + show_vp = np.zeros(shape=(nz, nx)) + for index, trace in enumerate(f.trace): + show_vp[:, index] = trace + + fig, ax = plt.subplots() + plt.pcolormesh(show_vp, shading="auto") + plt.title("Guess model") + plt.colorbar(label="P-wave velocity (km/s)") + plt.xlabel("x-direction (m)") + plt.ylabel("z-direction (m)") + ax.axis("equal") + plt.show() + + return None diff --git a/spyro/utils/__init__.py b/spyro/utils/__init__.py index e7d72643..652ad31f 100644 --- a/spyro/utils/__init__.py +++ b/spyro/utils/__init__.py @@ -1,3 +1,14 @@ -from . import utils, geometry_creation, estimate_timestep +from . import geometry_creation, estimate_timestep +from .utils import mpi_init, compute_functional, Mask, Gradient_mask_for_pml +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", + "Mask", + "Gradient_mask_for_pml", +] 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..0474aeb1 100644 --- a/spyro/utils/estimate_timestep.py +++ b/spyro/utils/estimate_timestep.py @@ -7,13 +7,14 @@ def estimate_timestep(mesh, V, c, estimate_max_eigenvalue=True): - """Estimate the maximum stable timestep based on the spectral radius + """ + Estimate the maximum stable timestep based on the spectral radius using optionally the Gershgorin Circle Theorem to estimate the maximum generalized eigenvalue. Otherwise computes the maximum - generalized eigenvalue exactly - - ONLY WORKS WITH KMV ELEMENTS + generalized eigenvalue exactly. + Parameters + ---------- """ u, v = fd.TrialFunction(V), fd.TestFunction(V) @@ -43,7 +44,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 +58,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..1e6a8a2f 100644 --- a/spyro/utils/utils.py +++ b/spyro/utils/utils.py @@ -1,8 +1,9 @@ import copy -from firedrake import * +from firedrake import * # noqa: F403 import numpy as np from mpi4py import MPI from scipy.signal import butter, filtfilt +import warnings def butter_lowpass_filter(shot, cutoff, fs, order=2): @@ -36,35 +37,26 @@ 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 + num_receivers = Wave_object.number_of_receivers + dt = Wave_object.dt + comm = Wave_object.comm - if regularize: - gamma = model["opt"]["gamma"] - Ns = model["acquisition"]["num_sources"] - gamma /= Ns + J = 0 + for rn in range(num_receivers): + J += np.trapz(residual[:, rn] ** 2, dx=dt) - J = 0.0 - for ti in range(nt): - for rn in range(num_receivers): - 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 + J_total = np.zeros((1)) + J_total[0] += J + J_total = COMM_WORLD.allreduce(J_total, op=MPI.SUM) + J_total[0] /= comm.comm.size + return J_total[0] def evaluate_misfit(model, guess, exact): @@ -81,11 +73,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 +85,36 @@ 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 + print(f"Parallelism type: {model.parallelism_type}", flush=True) + 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 +146,167 @@ 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 +class Mask(): + """ + A class representing a mask for a wave object. + + Parameters: + - boundaries (dict): A dictionary containing the boundaries to be applied. + - Wave_obj (object): An optional wave object. + - dg (bool): Flag indicating whether to use DG space for the mask. Default is False. + - inverse_mask (bool): Flag indicating whether to invert the mask. Default is False. + + Attributes: + - active_boundaries (list): A list of active boundaries. + - z (array): The z coordinates of the wave object's mesh. + - x (array): The x coordinates of the wave object's mesh. + - y (array): The y coordinates of the wave object's mesh (if applicable). + - mask_dofs (array): Contains the indices of the mask degrees of freedom. + + Methods: + - _calculate_mask_dofs(Wave_obj): Calculates the mask degrees of freedom. + - apply_mask(dJ): Applies the mask to the given Firedrake function. + + """ + + def __init__(self, boundaries, Wave_obj, dg=False, inverse_mask=False): + possible_boundaries = [ + "z_min", + "z_max", + "x_min", + "x_max", + "y_min", + "y_max", + ] + active_boundaries = [] + + for possible_boundary in possible_boundaries: + if possible_boundary in boundaries: + setattr(self, possible_boundary, boundaries[possible_boundary]) + active_boundaries.append(possible_boundary) + + self.active_boundaries = active_boundaries + self._calculate_mask_conditional(Wave_obj, inverse_mask) + self.in_dg = dg + if dg is False: + self._calculate_mask_dofs(Wave_obj) + elif dg is True: + self._calculate_dg_mask(Wave_obj) + + def _calculate_dg_mask(self, Wave_obj): + """ + Calculates the DG mask. + + Parameters: + - Wave_obj (object): The wave object containing the necessary data. + + """ + V_dg = FunctionSpace(Wave_obj.mesh, "DG", 0) + dg_mask = Function(V_dg) + dg_mask.interpolate(self.cond) + self.dg_mask = dg_mask + + def _calculate_mask_conditional(self, Wave_obj, inverted=False): + """ + Calculates the mask degrees of freedom based on the active boundaries. + + Parameters: + - Wave_obj (object): The wave object containing the necessary data. + - inverted (bool, optional): If True gives nonzero value inside the boundaries + + """ + # Getting necessary data from wave object + active_boundaries = self.active_boundaries + self.z = Wave_obj.mesh_z + self.x = Wave_obj.mesh_x + if ("y_min" in active_boundaries) or ("y_max" in active_boundaries): + self.y = Wave_obj.mesh_y + + # Getting mask conditional + if inverted: + cond = [1] + true_value = [0] + false_value = cond + else: + cond = [0] + true_value = [1] + false_value = cond + + for boundary in active_boundaries: + axis = boundary[0] + if boundary[-3:] == "min": + cond[0] = conditional(getattr(self, axis) < getattr(self, boundary), true_value[0], false_value[0]) + elif boundary[-3:] == "max": + cond[0] = conditional(getattr(self, axis) > getattr(self, boundary), true_value[0], false_value[0]) + else: + raise ValueError(f"Boundary of {boundary} not possible") + + self.cond = cond[0] + + def _calculate_mask_dofs(self, Wave_obj): + """ + Calculates the mask degrees of freedom. + + Parameters: + - Wave_obj (object): The wave object containing the necessary data. + + """ + if self.in_dg: + raise ValueError("DG space can have different DoFs than the functional space") + warnings.warn("When applying a mask in a continuous space, expect some error in the element adjacent to the mask") + mask = Function(Wave_obj.function_space) + mask.interpolate(self.cond) + # Saving mask dofs + self.mask_dofs = np.where(mask.dat.data[:] > 0.3) + + def apply_mask(self, dJ): + """ + Applies the mask to the given data. + + Parameters: + - dJ (object): Firedrake function. + + Returns: + - object: The masked data Firedrake. + + """ + dJ.dat.data[self.mask_dofs] = 0.0 + return dJ + + +class Gradient_mask_for_pml(Mask): + """ + A class representing a gradient mask for the Perfectly Matched Layer (PML). + + Args: + Wave_obj (optional): An object representing a wave. Defaults to None. + + Attributes: + boundaries (dict): A dictionary containing the boundaries of the mask. + + """ + + def __init__(self, Wave_obj): + if Wave_obj.abc_active is False: + raise ValueError("No PML present in wave object") + + # building firedrake function for mask + z_min = -(Wave_obj.length_z) + x_min = 0.0 + x_max = Wave_obj.length_x + boundaries = { + "z_min": z_min, + "x_min": x_min, + "x_max": x_max, + } + super().__init__(boundaries, Wave_obj) + + +# 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..ab2cb44c 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(mesh_parameters={"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..bc5e99c7 --- /dev/null +++ b/test/test_cpw_calc.py @@ -0,0 +1,70 @@ +import numpy as np +import spyro + + +def test_cpw_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, + "time-step_calculation": "estimate", + "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.4, # 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_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..568cc2cf --- /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() diff --git a/test/test_estimate_timestep.py b/test/test_estimate_timestep.py new file mode 100644 index 00000000..7887aa4d --- /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.000644745, 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.00101317122593, 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_2d.py b/test/test_gradient_2d.py new file mode 100644 index 00000000..d3ebac60 --- /dev/null +++ b/test/test_gradient_2d.py @@ -0,0 +1,168 @@ +import numpy as np +import math +import matplotlib.pyplot as plt +from copy import deepcopy +from firedrake import File +import firedrake as fire +import spyro + + +def check_gradient(Wave_obj_guess, dJ, rec_out_exact, Jm, plot=False): + steps = [1e-3, 1e-4, 1e-5] # step length + + errors = [] + V_c = Wave_obj_guess.function_space + dm = fire.Function(V_c) + size, = np.shape(dm.dat.data[:]) + dm_data = np.random.rand(size) + dm.dat.data[:] = dm_data + # dm.assign(dJ) + + for step in steps: + + Wave_obj_guess.reset_pressure() + c_guess = fire.Constant(2.0) + step*dm + Wave_obj_guess.initial_velocity_model = c_guess + Wave_obj_guess.forward_solve() + misfit_plusdm = rec_out_exact - Wave_obj_guess.receivers_output + J_plusdm = spyro.utils.compute_functional(Wave_obj_guess, misfit_plusdm) + + grad_fd = (J_plusdm - Jm) / (step) + projnorm = fire.assemble(dJ * dm * fire.dx(scheme=Wave_obj_guess.quadrature_rule)) + + error = 100 * ((grad_fd - projnorm) / projnorm) + + errors.append(error) + + errors = np.array(errors) + + # Checking if error is first order in step + theory = [t for t in steps] + theory = [errors[0] * th / theory[0] for th in theory] + if plot: + plt.close() + plt.plot(steps, errors, label="Error") + plt.plot(steps, theory, "--", label="first order") + plt.legend() + plt.title(" Adjoint gradient versus finite difference gradient") + plt.xlabel("Step") + plt.ylabel("Error %") + plt.savefig("gradient_error_verification.png") + plt.close() + + # Checking if every error is less than 1 percent + + test1 =abs(errors[-1]) < 1 + print(f"Last gradient error less than 1 percent: {test1}") + + # Checking if error follows expected finite difference error convergence + test2 = math.isclose(np.log(theory[-1]), np.log(errors[-1]), rel_tol=1e-1) + + print(f"Gradient error behaved as expected: {test2}") + + assert all([test1, test2]) + + +final_time = 1.0 + +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 + "degree": 4, # p order + "dimension": 2, # dimension +} + +dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial +} + +dictionary["mesh"] = { + "Lz": 3.0, # depth in km - always positive # Como ver isso sem ler a malha? + "Lx": 3.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_file": None, + "mesh_type": "firedrake_mesh", +} + +dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-1.1, 1.5)], + "frequency": 5.0, + # "delay": 1.2227264394269568, + # "delay_type": "time", + "delay": 1.5, + "delay_type": "multiples_of_minimun", + "receiver_locations": spyro.create_transect((-1.8, 1.2), (-1.8, 1.8), 10), + # "receiver_locations": [(-2.0, 2.5) , (-2.3, 2.5), (-3.0, 2.5), (-3.5, 2.5)], +} + +dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": final_time, # 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 - Perguntar Daiane ''post_processing_frequnecy' + "gradient_sampling_frequency": 1, # how frequently to save solution to RAM - Perguntar Daiane 'gradient_sampling_frequency' +} + +dictionary["visualization"] = { + "forward_output": False, + "forward_output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": "results/Gradient.pvd", + "adjoint_output": False, + "adjoint_filename": None, + "debug_output": False, +} + + +def get_forward_model(load_true=False): + if load_true is False: + Wave_obj_exact = spyro.AcousticWave(dictionary=dictionary) + Wave_obj_exact.set_mesh(mesh_parameters={"dx": 0.1}) + # Wave_obj_exact.set_initial_velocity_model(constant=3.0) + cond = fire.conditional(Wave_obj_exact.mesh_z > -1.5, 1.5, 3.5) + Wave_obj_exact.set_initial_velocity_model( + conditional=cond, + # output=True + ) + spyro.plots.plot_model(Wave_obj_exact, abc_points=[(-1, 1), (-2, 1), (-2, 4), (-1, 2)]) + Wave_obj_exact.forward_solve() + # forward_solution_exact = Wave_obj_exact.forward_solution + rec_out_exact = Wave_obj_exact.receivers_output + # np.save("rec_out_exact", rec_out_exact) + + else: + rec_out_exact = np.load("rec_out_exact.npy") + + Wave_obj_guess = spyro.AcousticWave(dictionary=dictionary) + Wave_obj_guess.set_mesh(mesh_parameters={"dx": 0.1}) + Wave_obj_guess.set_initial_velocity_model(constant=2.0) + Wave_obj_guess.forward_solve() + rec_out_guess = Wave_obj_guess.receivers_output + + return rec_out_exact, rec_out_guess, Wave_obj_guess + + +def test_gradient(): + rec_out_exact, rec_out_guess, Wave_obj_guess = get_forward_model(load_true=False) + forward_solution = Wave_obj_guess.forward_solution + forward_solution_guess = deepcopy(forward_solution) + + misfit = rec_out_exact - rec_out_guess + + Jm = spyro.utils.compute_functional(Wave_obj_guess, misfit) + print(f"Cost functional : {Jm}") + + # compute the gradient of the control (to be verified) + dJ = Wave_obj_guess.gradient_solve(misfit=misfit, forward_solution=forward_solution_guess) + File("gradient.pvd").write(dJ) + + check_gradient(Wave_obj_guess, dJ, rec_out_exact, Jm, plot=True) + + +if __name__ == "__main__": + test_gradient() diff --git a/test/test_gradient_2d_pml.py b/test/test_gradient_2d_pml.py new file mode 100644 index 00000000..8af7e981 --- /dev/null +++ b/test/test_gradient_2d_pml.py @@ -0,0 +1,208 @@ +import numpy as np +import matplotlib.pyplot as plt +from copy import deepcopy +from firedrake import File +import firedrake as fire +import spyro + + +class Gradient_mask_for_pml(): + def __init__(self, Wave_obj=None): + if Wave_obj.abc_active is False: + pass + + # Gatting necessary data from wave object + pad = Wave_obj.abc_pad_length + z = Wave_obj.mesh_z + x = Wave_obj.mesh_x + V = Wave_obj.function_space + + # building firedrake function for mask + z_min = -(Wave_obj.length_z) + x_min = 0.0 + x_max = Wave_obj.length_x + mask = fire.Function(V) + cond = fire.conditional(z < z_min, 1, 0) + cond = fire.conditional(x < x_min, 1, cond) + cond = fire.conditional(x > x_max, 1, cond) + mask.interpolate(cond) + + # saving mask dofs + self.mask_dofs = np.where(mask.dat.data[:] > 0.95) + print("DEBUG") + + def apply_mask(self, dJ): + dJ.dat.data[self.mask_dofs] = 0.0 + return dJ + + +def check_gradient(Wave_obj_guess, dJ, rec_out_exact, Jm, plot=False): + steps = [1e-3] # step length + + errors = [] + V_c = Wave_obj_guess.function_space + dm = fire.Function(V_c) + dm.assign(dJ) + + for step in steps: + + Wave_obj_guess.reset_pressure() + c_guess = fire.Constant(2.0) + step*dm + Wave_obj_guess.initial_velocity_model = c_guess + Wave_obj_guess.forward_solve() + misfit_plusdm = rec_out_exact - Wave_obj_guess.receivers_output + J_plusdm = spyro.utils.compute_functional(Wave_obj_guess, misfit_plusdm) + + grad_fd = (J_plusdm - Jm) / (step) + projnorm = fire.assemble(dJ * dm * fire.dx(scheme=Wave_obj_guess.quadrature_rule)) + + error = np.abs(100 * ((grad_fd - projnorm) / projnorm)) + + errors.append(error) + + errors = np.array(errors) + + # Checking if error is first order in step + theory = [t for t in steps] + theory = [errors[0] * th / theory[0] for th in theory] + if plot: + plt.close() + plt.plot(steps, errors, label="Error") + plt.plot(steps, theory, "--", label="first order") + plt.legend() + plt.title(" Adjoint gradient versus finite difference gradient") + plt.xlabel("Step") + plt.ylabel("Error %") + plt.savefig("gradient_error_verification.png") + plt.close() + + # Checking if every error is less than 5 percent + + test1 = (abs(errors[-1]) < 5 ) + print(f"Gradient error less than 5 percent: {test1}") + print(f"Error of {errors}") + + # Checking if error follows expected finite difference error convergence + # this is not done in PML yet. A samll percentage error is present here and in old spyro + # test2 = math.isclose(np.log(theory[-1]), np.log(errors[-1]), rel_tol=1e-1) + + # print(f"Gradient error behaved as expected: {test2}") + + assert all([test1]) + + +def set_dictionary(PML=False): + final_time = 1.0 + + 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 + "degree": 4, # p order + "dimension": 2, # dimension + } + + dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial + } + + 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, + "mesh_type": "firedrake_mesh", + } + + dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.1, 0.5)], + "frequency": 5.0, + "delay": 1.5, + "delay_type": "multiples_of_minimun", + "receiver_locations": spyro.create_transect((-0.8, 0.1), (-0.8, 0.9), 10), + } + + dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": final_time, # Final time for event + "dt": 0.0002, # 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": 1, # how frequently to save solution to RAM - Perguntar Daiane 'gradient_sampling_frequency' + } + + dictionary["visualization"] = { + "forward_output": False, + "forward_output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": "results/Gradient.pvd", + "adjoint_output": False, + "adjoint_filename": None, + "debug_output": False, + } + if PML: + dictionary["absorving_boundary_conditions"] = { + "status": True, + "damping_type": "PML", + "exponent": 2, + "cmax": 4.5, + "R": 1e-6, + "pad_length": 0.25, + } + return dictionary + + +def get_forward_model(dictionary=None): + + # Exact model + Wave_obj_exact = spyro.AcousticWave(dictionary=dictionary) + Wave_obj_exact.set_mesh(mesh_parameters={"dx": 0.03}) + cond = fire.conditional(Wave_obj_exact.mesh_z > -0.5, 1.5, 3.5) + Wave_obj_exact.set_initial_velocity_model( + conditional=cond, + dg_velocity_model=False, + ) + spyro.plots.plot_model(Wave_obj_exact, filename="pml_grad_test_model.png",abc_points=[(-0, 0), (-1, 0), (-1, 1), (-0, 1)]) + Wave_obj_exact.forward_solve() + rec_out_exact = Wave_obj_exact.receivers_output + + # Guess model + Wave_obj_guess = spyro.AcousticWave(dictionary=dictionary) + Wave_obj_guess.set_mesh(mesh_parameters={"dx": 0.03}) + Wave_obj_guess.set_initial_velocity_model(constant=2.0) + Wave_obj_guess.forward_solve() + rec_out_guess = Wave_obj_guess.receivers_output + + return rec_out_exact, rec_out_guess, Wave_obj_guess + + +def test_gradient(PML=False): + dictionary = set_dictionary(PML=PML) + rec_out_exact, rec_out_guess, Wave_obj_guess = get_forward_model(dictionary=dictionary) + forward_solution = Wave_obj_guess.forward_solution + forward_solution_guess = deepcopy(forward_solution) + + misfit = rec_out_exact - rec_out_guess + + Jm = spyro.utils.compute_functional(Wave_obj_guess, misfit) + print(f"Cost functional : {Jm}") + + # compute the gradient of the control (to be verified) + dJ = Wave_obj_guess.gradient_solve(misfit=misfit, forward_solution=forward_solution_guess) + File("gradient_premask.pvd").write(dJ) + Mask_data = Gradient_mask_for_pml(Wave_obj=Wave_obj_guess) + dJ = Mask_data.apply_mask(dJ) + File("gradient.pvd").write(dJ) + + check_gradient(Wave_obj_guess, dJ, rec_out_exact, Jm, plot=True) + + +def test_gradient_pml(): + return test_gradient(PML=True) + + +if __name__ == "__main__": + test_gradient_pml() 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..bd6c6d58 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(mesh_parameters={"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(mesh_parameters={"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_mask.py b/test/test_mask.py new file mode 100644 index 00000000..2b3c2eb1 --- /dev/null +++ b/test/test_mask.py @@ -0,0 +1,284 @@ +import spyro +from spyro.utils import Mask +from spyro.utils import Gradient_mask_for_pml +from spyro.examples.rectangle import Rectangle_acoustic +import firedrake as fire +from random import uniform as rand +import numpy as np + + +class Interval(object): + def __init__(self, lower, upper): + self.lower = lower + self.upper = upper + + def __contains__(self, item): + return self.lower <= item <= self.upper + + +def interval(lower, upper): + return Interval(lower, upper) + + +def test_mask(): + dictionary = {} + dictionary["options"] = { + "cell_type": "T", + } + dictionary["absorving_boundary_conditions"] = { + "status": False, + "pad_length": 0., + } + dictionary["mesh"] = { + "Lz": 1.0, + "Lx": 1.0, + "h": 0.03 + } + Wave_obj = Rectangle_acoustic(dictionary=dictionary) + boundaries = { + "z_min":-0.9, + "z_max":-0.1, + "x_min":0.2, + "x_max":0.8, + } + + # Points we are going to check + tol = Wave_obj.input_dictionary["mesh"]["h"] + points_not_masked = [ + # Interior + (-0.15, 0.25), + (-0.5, 0.5), + (-0.85, 0.4), + (-0.3, 0.6), + (-0.4, 0.7), + # Vertices plus tol + (-0.1-tol, 0.2+tol), + (-0.9+tol, 0.2+tol), + (-0.1-tol, 0.8-tol), + (-0.9+tol, 0.8-tol), + ] + points_on_boundary = [ + # Vertices + (-0.1, 0.2), + (-0.9, 0.2), + (-0.1, 0.8), + (-0.9, 0.8), + # Edges + (-0.1, 0.3), + (-0.9, 0.6), + (-0.5, 0.2), + (-0.7, 0.8), + ] + points_masked = [ + # Inside pml + (-0.05, 0.25), + (-0.95, 0.5), + (-0.85, 0.1), + (-0.3, 0.9), + (-0.02, 0.7), + # Vertices plus tol + (-0.1+tol, 0.2-tol), + (-0.9-tol, 0.2-tol), + (-0.1+tol, 0.8+tol), + (-0.9-tol, 0.8+tol), + ] + points_in_tolerance = [ + # Vertices + (-0.1+tol*rand(-1, 1), 0.2+tol*rand(-1, 1)), + (-0.9+tol*rand(-1, 1), 0.2+tol*rand(-1, 1)), + (-0.1+tol*rand(-1, 1), 0.8+tol*rand(-1, 1)), + (-0.9+tol*rand(-1, 1), 0.8+tol*rand(-1, 1)), + # Edges + (-0.1+tol*rand(-1, 1), rand(0.2, 0.8)), + (-0.9+tol*rand(-1, 1), rand(0.2, 0.8)), + (rand(-0.1, -0.9), 0.2+tol*rand(-1, 1)), + (rand(-0.1, -0.9), 0.8+tol*rand(-1, 1)), + ] + + # Testing mask that applies zeros to a function in the objects space + Mask_not_dg = Mask(boundaries, Wave_obj) + V = Wave_obj.function_space + u = fire.Function(V) + u.interpolate(fire.Constant(10)) + u = Mask_not_dg.apply_mask(u) + + unmasked_results = u.at(points_not_masked) + boundary_results = u.at(points_on_boundary) + close_to_boundary_results = u.at(points_in_tolerance) + masked_results = u.at(points_masked) + + # Checking results close to or in the boundaries + for result in boundary_results: + assert result in interval(-2.1, 12.5), f"Value of point failling in boundary: {result}" + for result in close_to_boundary_results: + assert result in interval(-2.1, 12.5), f"Value of point failing close to boundary: {result}" + + # Checking results in mask + for result in masked_results: + assert np.isclose(result, 0.0), f"Mask not zero: {result}" + + # Checking interior points + for result in unmasked_results: + assert np.isclose(result, 10.0), f"Interior is masked: {result}" + + # Testing DG mask for 1 in mask and 0 outside + Mask_dg = Mask(boundaries, Wave_obj, dg=True) + dg_func = Mask_dg.dg_mask + + unmasked_results = dg_func.at(points_not_masked) + boundary_results = dg_func.at(points_on_boundary) + close_to_boundary_results = dg_func.at(points_in_tolerance) + masked_results = dg_func.at(points_masked) + + # Checking results close to or in the boundaries + for result in boundary_results: + assert result in interval(0 - 1e-5, 1.0 + 1e-5), f"Value of DG point failling in boundary: {result}" + for result in close_to_boundary_results: + assert result in interval(0 - 1e-5, 1.0 + 1e-5), f"Value of DG point failling close to boundary: {result}" + + # Checking results in mask + for result in masked_results: + assert np.isclose(result, 1.0), f"Value of DG point in mask should be 1 not: {result}" + # Checking interior points + for result in unmasked_results: + assert np.isclose(result, 0.0), f"Value of DG point unmask should be zero not: {result}" + + # Testing DG inverse mask for 0 in mask and 1 outside + Mask_dg = Mask(boundaries, Wave_obj, dg=True, inverse_mask=True) + dg_func_inverted = Mask_dg.dg_mask + + unmasked_results = dg_func_inverted.at(points_not_masked) + boundary_results = dg_func_inverted.at(points_on_boundary) + close_to_boundary_results = dg_func_inverted.at(points_in_tolerance) + masked_results = dg_func_inverted.at(points_masked) + + # Checking results close to or in the boundaries + for result in boundary_results: + assert result in interval(0, 10), f"Value of inv DG point failling in boundary: {result}" + for result in close_to_boundary_results: + assert result in interval(0, 10), f"Value of inv DG point failling close to boundary: {result}" + + # Checking results in mask + for result in masked_results: + assert np.isclose(result, 0.0), f"Inverted DG mask not zero, but {result}" + + # Checking interior points + for result in unmasked_results: + assert np.isclose(result, 1.0), f"Inverted DG mask interior not 1, but {result}" + + assert True + + +def test_gradient_mask(): + dictionary = {} + dictionary["options"] = { + "cell_type": "T", + } + dictionary["absorving_boundary_conditions"] = { + "status": True, + "pad_length": 0.2, + } + dictionary["mesh"] = { + "Lz": 1.0, + "Lx": 1.0, + "h": 0.03 + } + Wave_obj = Rectangle_acoustic(dictionary=dictionary) + # Points we are going to check + tol = Wave_obj.input_dictionary["mesh"]["h"] + points_not_masked = [ + # Interior + (-0.15, 0.25), + (-0.5, 0.5), + (-0.85, 0.4), + (-0.3, 0.6), + (-0.4, 0.7), + # Vertices plus tol + (-0.-tol, 0.+tol), + (-1.0+tol, 0.+tol), + (-0.-tol, 1.0-tol), + (-1.0+tol, 1.0-tol), + ] + points_on_boundary = [ + # Vertices + (-0., 0.), + (-1., 0.), + (-0., 1.), + (-1., 1.), + # Edges + (-0., 0.3), + (-1., 0.6), + (-0.5, 0.), + (-0.7, 1.), + ] + points_masked = [ + # Inside pml + (-0.05, -0.1), + (-1.10, 0.5), + (-0.95, -0.15), + (-0.3, 1.12), + (-1.13, 1.15), + # Vertices plus tol + (-0.-tol, 0.-tol), + (-1.-tol, 0.-tol), + (-0.-tol, 1.+tol), + (-1.-tol, 1.+tol), + ] + points_in_tolerance = [ + # Vertices + (-0., 0.+tol*rand(-1, 1)), + (-1.+tol*rand(-1, 1), 0.+tol*rand(-1, 1)), + (-0., 1.+tol*rand(-1, 1)), + (-1.+tol*rand(-1, 1), 1.+tol*rand(-1, 1)), + # Edges + (-0., rand(0.2, 0.8)), + (-1.+tol*rand(-1, 1), rand(0.2, 0.8)), + (rand(-0.1, -0.9), 0.+tol*rand(-1, 1)), + (rand(-0.1, -0.9), 1.+tol*rand(-1, 1)), + ] + + # Testing mask that applies zeros to a function in the objects space + test1 = True + + Mask_not_dg = Gradient_mask_for_pml(Wave_obj) + V = Wave_obj.function_space + u = fire.Function(V) + u.interpolate(fire.Constant(10)) + u = Mask_not_dg.apply_mask(u) + + unmasked_results = u.at(points_not_masked) + boundary_results = u.at(points_on_boundary) + close_to_boundary_results = u.at(points_in_tolerance) + masked_results = u.at(points_masked) + + # Checking results close to or in the boundaries + for result in boundary_results: + if result not in interval(-2.1, 13): + test1 = False + for result in close_to_boundary_results: + if result not in interval(-2.1, 13): + test1 = False + if test1 is False: + print("Boundary going crazy") + assert False + # Checking results in mask + for result in masked_results: + if np.isclose(result, 0.0) is False: + test1 = False + if test1 is False: + print("Mask not zero") + assert False + # Checking interior points + for result in unmasked_results: + if np.isclose(result, 10.0) is False: + test1 = False + if test1 is False: + print("Interior is masked") + assert False + + assert test1 + + +if __name__ == "__main__": + test_mask() + test_gradient_mask() diff --git a/test/test_misfit_2d_calculation.py b/test/test_misfit_2d_calculation.py new file mode 100644 index 00000000..71970b26 --- /dev/null +++ b/test/test_misfit_2d_calculation.py @@ -0,0 +1,129 @@ +import numpy as np +import spyro + + +def test_misfit_2d(): + 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, + }, + } + + 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": 1, # 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 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": 3.0, # depth in km - always positive # Como ver isso sem ler a malha? + "Lx": 3.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_file": None, + "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`. + dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-0.5, 1.5)], + "frequency": 5.0, + "delay": 1.5, + "delay_type": "multiples_of_minimun", + "receiver_locations": spyro.create_transect((-2.9, 0.1), (-2.9, 2.9), 100), + } + + # Simulate for 2.0 seconds. + dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": 1.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": 1, # how frequently to save solution to RAM - Perguntar Daiane 'gradient_sampling_frequency' + } + dictionary["visualization"] = { + "forward_output": True, + "forward_output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": True, + "gradient_filename": "results/Gradient.pvd", + "adjoint_output": False, + "adjoint_filename": None, + "debug_output": True, + } + dictionary["inversion"] = { + "perform_fwi": True, + "initial_guess_model_file": None, + "shot_record_file": None, + "optimization_parameters": default_optimization_parameters, + } + + # Using FWI Object + FWI_obj = spyro.FullWaveformInversion(dictionary=dictionary) + FWI_obj.set_real_mesh(mesh_parameters={"dx": 0.05}) + FWI_obj.set_real_velocity_model( + expression="4.0 + 1.0 * tanh(10.0 * (0.5 - sqrt((x - 1.5) ** 2 + (z + 1.5) ** 2)))", + ) + FWI_obj.generate_real_shot_record() + + FWI_obj.set_guess_mesh(mesh_parameters={"dx": 0.05}) + FWI_obj.set_guess_velocity_model(constant=4.0) + misfit = FWI_obj.calculate_misfit() + + # Using only wave objects + Wave_obj_exact = spyro.AcousticWave(dictionary=dictionary) + Wave_obj_exact.set_mesh(mesh_parameters={"dx": 0.05}) + Wave_obj_exact.set_initial_velocity_model( + expression="4.0 + 1.0 * tanh(10.0 * (0.5 - sqrt((x - 1.5) ** 2 + (z + 1.5) ** 2)))", + output=True + ) + Wave_obj_exact.forward_solve() + rec_out_exact = Wave_obj_exact.receivers_output + + Wave_obj_guess = spyro.AcousticWave(dictionary=dictionary) + Wave_obj_guess.set_mesh(mesh_parameters={"dx": 0.05}) + Wave_obj_guess.set_initial_velocity_model(constant=4.0) + Wave_obj_guess.forward_solve() + rec_out_guess = Wave_obj_guess.receivers_output + + misfit_second_calc = rec_out_exact - rec_out_guess + + arevaluesclose = np.isclose(misfit, misfit_second_calc) + test = arevaluesclose.all() + + print(f"Misfit calculated with FWI object is close to the individually calculated: {test}") + + return test + + +if __name__ == "__main__": + test_misfit_2d() 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..996dda75 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(mesh_parameters={"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..3009e567 --- /dev/null +++ b/test/test_plots.py @@ -0,0 +1,33 @@ +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) + spyro.plots.debug_plot(Wave_obj.u_n, filename="test_debug_plot.png") + spyro.plots.debug_pvd(Wave_obj.u_n, filename="test_debug_plot.pvd") + + +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..7ed511ef --- /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(mesh_parameters={"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..f266b5b2 --- /dev/null +++ b/test/test_seismicmesh_integration.py @@ -0,0 +1,81 @@ +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_parameters = { + "length_z": Lz, + "length_x": Lx, + "length_y": 0.0, + "cell_type": "triangle", + "mesh_type": "SeismicMesh", + "dx": None, + "periodic": False, + "velocity_model_file": None, + "cells_per_wavelength": cpw, + "source_frequency": freq, + "minimum_velocity": c, + "abc_pad_length": pad, + "lbda": lbda, + "dimension": 2, + "edge_length": lbda/cpw, + } + + Mesh_obj = spyro.meshing.AutomaticMesh( + mesh_parameters=mesh_parameters, + ) + Mesh_obj.set_seismicmesh_parameters(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..a92f66ea --- /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, + "forward_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(mesh_parameters={"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..7a8ecdb2 --- /dev/null +++ b/test_3d/test_hexahedral_convergence.py @@ -0,0 +1,133 @@ +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) + mesh_parameters = { + "dx": 0.02, + "periodic": True, + } + Wave_obj.set_mesh(mesh_parameters=mesh_parameters) + + 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() diff --git a/test_parallel/test_forward.py b/test_parallel/test_forward.py index dc7aaae9..91287542 100644 --- a/test_parallel/test_forward.py +++ b/test_parallel/test_forward.py @@ -1,165 +1,104 @@ -from firedrake import File -import matplotlib.pyplot as plt +from mpi4py.MPI import COMM_WORLD +from mpi4py import MPI import numpy as np -import math +import firedrake as fire import spyro -def plot_receiver( - receiver, - receiver_id, - dt, - final_time, - show=False, - file_format="pdf", -): - """Plot a - - Returns - ------- - None - """ - receiver_data = receiver[:, receiver_id] - - nt = int(final_time / dt) # number of timesteps - times = np.linspace(0.0, final_time, nt) - - plt.plot(times, receiver_data) - - plt.xlabel("time (s)", fontsize=18) - plt.ylabel("amplitude", fontsize=18) - plt.xticks(fontsize=18) - plt.yticks(fontsize=18) - # plt.xlim(start_index, end_index) - # plt.ylim(tf, 0) - plt.savefig("receiver" + str(receiver_id) + "." + file_format, format=file_format) - if show: - plt.show() - plt.close() - return None - - -def compare_velocity( - p_r, receiver_in_source_index, receiver_comparison_index, model, dt -): - receiver_0 = p_r[:, receiver_in_source_index] - receiver_1 = p_r[:, receiver_comparison_index] - pos = model["acquisition"]["receiver_locations"] - time0 = np.argmax(receiver_0) * dt - time1 = np.argmax(receiver_1) * dt - x0 = pos[receiver_in_source_index, 1] - x1 = pos[receiver_comparison_index, 1] - measured_velocity = np.abs(x1 - x0) / (time1 - time0) - minimum_velocity = 1.5 - error_percent = ( - 100 * np.abs(measured_velocity - minimum_velocity) / minimum_velocity - ) - print(f"Velocity error of {error_percent}%.", flush=True) - return error_percent - - -def get_receiver_in_source_location(source_id, model): - receiver_locations = model["acquisition"]["receiver_locations"] - source_locations = model["acquisition"]["source_pos"] - source_x = source_locations[source_id, 1] - - cont = 0 - for receiver_location in receiver_locations: - if math.isclose(source_x, receiver_location[1]): - return cont - cont += 1 - return ValueError( - "Couldn't find a receiver whose location coincides with a source within the standard tolerance." - ) - - -def test_forward_5shots(): - model = {} - - model["opts"] = { - "method": "KMV", # either CG or KMV - "quadrature": "KMV", # Equi or KMV +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_forward_3_shots(): + final_time = 1.0 + + 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 "degree": 4, # p order "dimension": 2, # dimension } - model["parallelism"] = { - "type": "automatic", + dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial } - model["mesh"] = { - "Lz": 3.5, # depth in km - always positive - "Lx": 17.0, # width in km - always positive + dictionary["mesh"] = { + "Lz": 3.0, # depth in km - always positive # Como ver isso sem ler a malha? + "Lx": 3.0, # width in km - always positive "Ly": 0.0, # thickness in km - always positive - "meshfile": "meshes/marmousi_5Hz.msh", - "initmodel": None, - "truemodel": "velocity_models/vp_marmousi-ii.hdf5", + "mesh_file": None, + "mesh_type": "firedrake_mesh", } - 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.5, # maximum acoustic wave velocity in PML - km/s - "R": 1e-6, # theoretical reflection coefficient - "lz": 0.9, # thickness of the PML in the z-direction (km) - always positive - "lx": 0.9, # 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 - } - model["acquisition"] = { - "source_type": "Ricker", - "source_pos": spyro.create_transect((-0.1, 1.0), (-0.1, 15.0), 5), + dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": [(-1.1, 1.2), (-1.1, 1.5), (-1.1, 1.8)], "frequency": 5.0, - "delay": 1.0, - "receiver_locations": spyro.create_transect((-0.1, 1.0), (-0.1, 15.0), 13), + "delay": 0.2, + "delay_type": "time", + "receiver_locations": spyro.create_transect((-1.3, 1.2), (-1.3, 1.8), 301), } - model["timeaxis"] = { - "t0": 0.0, # Initial time for event - "tf": 3.00, # Final time for event - "dt": 0.001, + dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": final_time, # Final time for event + "dt": 0.001, # timestep size "amplitude": 1, # the Ricker has an amplitude of 1. - "nspool": 100, # how frequently to output solution to pvds - "fspool": 99999, # how frequently to save solution to RAM + "output_frequency": 100, # how frequently to output solution to pvds - Perguntar Daiane ''post_processing_frequnecy' + "gradient_sampling_frequency": 1, + } + dictionary["visualization"] = { + "forward_output": False, + "forward_output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": None, } - dt = model["timeaxis"]["dt"] + Wave_obj = spyro.AcousticWave(dictionary=dictionary) + Wave_obj.set_mesh(mesh_parameters={"dx": 0.1}) + + mesh_z = Wave_obj.mesh_z + cond = fire.conditional(mesh_z < -1.5, 3.5, 1.5) + Wave_obj.set_initial_velocity_model(conditional=cond, output=True) - comm = spyro.utils.mpi_init(model) + Wave_obj.forward_solve() - mesh, V = spyro.io.read_mesh(model, comm) - vp = spyro.io.interpolate(model, mesh, V, guess=False) + comm = Wave_obj.comm + arr = Wave_obj.receivers_output + + if comm.ensemble_comm.rank == 0: + analytical_p = spyro.utils.nodal_homogeneous_analytical( + Wave_obj, 0.2, 1.5, n_extra=100 + ) + else: + analytical_p = None + analytical_p = comm.ensemble_comm.bcast(analytical_p, root=0) + + # Checking if error before reflection matches if comm.ensemble_comm.rank == 0: - File("true_velocity.pvd", comm=comm.comm).write(vp) - 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"], - ) - p, p_r = spyro.solvers.forward(model, mesh, comm, vp, sources, wavelet, receivers) - - pass_error_test = False - for source_id in range(len(model["acquisition"]["source_pos"])): - if comm.ensemble_comm.rank == (source_id % comm.ensemble_comm.size): - receiver_in_source_index = get_receiver_in_source_location(source_id, model) - if ( - source_id != len(model["acquisition"]["source_pos"]) - 1 - or source_id == 0 - ): - receiver_comparison_index = receiver_in_source_index + 1 - else: - receiver_comparison_index = receiver_in_source_index - 1 - error_percent = compare_velocity( - p_r, receiver_in_source_index, receiver_comparison_index, model, dt - ) - if error_percent < 5: - pass_error_test = True - print(f"For source = {source_id}: test = {pass_error_test}", flush=True) - - spyro.plots.plot_shots(model, comm, p_r, vmin=-1e-3, vmax=1e-3) - spyro.io.save_shots(model, comm, p_r) - assert pass_error_test + rec_id = 0 + elif comm.ensemble_comm.rank == 1: + rec_id = 150 + elif comm.ensemble_comm.rank == 2: + rec_id = 300 + + arr0 = arr[:, rec_id] + arr0 = arr0.flatten() + + error = error_calc(arr0[:430], analytical_p[:430], 430) + if comm.comm.rank == 0: + print(f"Error for shot {Wave_obj.current_source} is {error} and test has passed equals {np.abs(error) < 0.01}", flush=True) + error_all = COMM_WORLD.allreduce(error, op=MPI.SUM) + error_all /= 3 + + test = np.abs(error_all) < 0.01 + + assert test if __name__ == "__main__": - test_forward_5shots() + test_forward_3_shots() diff --git a/test_parallel/test_fwi.py b/test_parallel/test_fwi.py new file mode 100644 index 00000000..275216c0 --- /dev/null +++ b/test_parallel/test_fwi.py @@ -0,0 +1,140 @@ +import numpy as np +import firedrake as fire +import spyro +import pytest + + +def is_rol_installed(): + try: + import ROL + return True + except ImportError: + return False + + +final_time = 0.9 + +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 + "degree": 4, # p order + "dimension": 2, # dimension +} +dictionary["parallelism"] = { + "type": "automatic", # options: automatic (same number of cores for evey processor) or spatial +} +dictionary["mesh"] = { + "Lz": 2.0, # depth in km - always positive # Como ver isso sem ler a malha? + "Lx": 2.0, # width in km - always positive + "Ly": 0.0, # thickness in km - always positive + "mesh_file": None, + "mesh_type": "firedrake_mesh", +} +dictionary["acquisition"] = { + "source_type": "ricker", + "source_locations": spyro.create_transect((-0.55, 0.7), (-0.55, 1.3), 6), + # "source_locations": [(-1.1, 1.5)], + "frequency": 5.0, + "delay": 0.2, + "delay_type": "time", + "receiver_locations": spyro.create_transect((-1.45, 0.7), (-1.45, 1.3), 200), +} +dictionary["time_axis"] = { + "initial_time": 0.0, # Initial time for event + "final_time": final_time, # 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": 1, # how frequently to save solution to RAM - Perguntar Daiane 'gradient_sampling_frequency' +} +dictionary["visualization"] = { + "forward_output": False, + "forward_output_filename": "results/forward_output.pvd", + "fwi_velocity_model_output": False, + "velocity_model_filename": None, + "gradient_output": False, + "gradient_filename": "results/Gradient.pvd", + "adjoint_output": False, + "adjoint_filename": None, + "debug_output": False, +} +dictionary["inversion"] = { + "perform_fwi": True, # switch to true to make a FWI + "initial_guess_model_file": None, + "shot_record_file": None, +} + + +def test_fwi(load_real_shot=False, use_rol=False): + """ + Run the Full Waveform Inversion (FWI) test. + + Parameters + ---------- + load_real_shot (bool, optional): Whether to load a real shot record or not. Defaults to False. + """ + + # Setting up to run synthetic real problem + if load_real_shot is False: + FWI_obj = spyro.FullWaveformInversion(dictionary=dictionary) + + FWI_obj.set_real_mesh(mesh_parameters={"dx": 0.1}) + center_z = -1.0 + center_x = 1.0 + mesh_z = FWI_obj.mesh_z + mesh_x = FWI_obj.mesh_x + cond = fire.conditional((mesh_z-center_z)**2 + (mesh_x-center_x)**2 < .2**2, 3.0, 2.5) + + FWI_obj.set_real_velocity_model(conditional=cond, output=True, dg_velocity_model=False) + FWI_obj.generate_real_shot_record( + plot_model=True, + filename="True_experiment.png", + abc_points=[(-0.5, 0.5), (-1.5, 0.5), (-1.5, 1.5), (-0.5, 1.5)] + ) + np.save("real_shot_record", FWI_obj.real_shot_record) + + else: + dictionary["inversion"]["shot_record_file"] = "real_shot_record.npy" + FWI_obj = spyro.FullWaveformInversion(dictionary=dictionary) + + # Setting up initial guess problem + FWI_obj.set_guess_mesh(mesh_parameters={"dx": 0.1}) + FWI_obj.set_guess_velocity_model(constant=2.5) + mask_boundaries = { + "z_min": -1.3, + "z_max": -0.7, + "x_min": 0.7, + "x_max": 1.3, + } + FWI_obj.set_gradient_mask(boundaries=mask_boundaries) + if use_rol: + FWI_obj.run_fwi_rol(vmin=2.5, vmax=3.0, maxiter=2) + else: + FWI_obj.run_fwi(vmin=2.5, vmax=3.0, maxiter=5) + + # simple mask test + grad_test = FWI_obj.gradient + test0 = np.isclose(grad_test.at((-0.1, 0.1)), 0.0) + print(f"PML looks masked: {test0}", flush=True) + test1 = np.abs(grad_test.at((-1.0, 1.0))) > 1e-5 + print(f"Center looks unmasked: {test1}", flush=True) + + # quick look at functional and if it reduced + test2 = FWI_obj.functional < 1e-3 + print(f"Last functional small: {test2}", flush=True) + test3 = FWI_obj.functional_history[-1]/FWI_obj.functional_history[0] < 1e-2 + print(f"Considerable functional reduction during test: {test3}", flush=True) + + print("END", flush=True) + assert all([test0, test1, test2, test3]) + + +@pytest.mark.skipif(not is_rol_installed(), reason="ROL is not installed") +def test_fwi_with_rol(load_real_shot=False, use_rol=True): + test_fwi(load_real_shot=load_real_shot, use_rol=use_rol) + + +if __name__ == "__main__": + test_fwi(load_real_shot=False) + test_fwi_with_rol() diff --git a/velocity_models/tutorial b/velocity_models/tutorial new file mode 100644 index 00000000..81a389f5 Binary files /dev/null and b/velocity_models/tutorial differ