import pathlib
import numpy as np
import sympy
import xarray as xr
import salvus.namespace as sn
import salvus.mesh.layered_meshing as lm
UtmDomain.from_spherical_chunk
constructor that takes WGS84 coordinates and
converts them to an appropriate UTM domain. The UTM zone and coordinates
could of course also be specified directly.# Define our domain.
d = sn.domain.dim3.UtmDomain.from_spherical_chunk(
min_latitude=36.7,
max_latitude=36.5,
min_longitude=-122.5,
max_longitude=-121.8,
)
# Have a look at the domain to make sure it is correct.
d.plot()
domain
object
itself.# Where to download topography data to.
topo_filename = "data/gmrt_topography.nc"
# Now query data from the GMRT web service.
if not pathlib.Path(topo_filename).exists():
d.download_topography_and_bathymetry(
filename=topo_filename,
# The buffer is useful for example when adding sponge layers
# for absorbing boundaries so we recommend to always use it.
buffer_in_degrees=0.1,
resolution="default",
)
# Load the data to Salvus compatible SurfaceTopography object. It will resample
# to a regular grid and convert to the UTM coordinates of the domain. This will
# later be added to the Salvus project.
t = sn.topography.cartesian.SurfaceTopography.from_gmrt_file(
name="monterey_topo",
data=topo_filename,
resample_topo_nx=1501,
# If the coordinate conversion is very slow, consider decimating.
decimate_topo_factor=5,
# Smooth if necessary.
gaussian_std_in_meters=0.0,
# Make sure to pass the correct UTM zone.
utm=d.utm,
)
xarray
DataArray
. These datasets are simple to manipulate and visualize, and we
can for instance plot the digital elevation model in the notebook by simply
plotting the `xarray`` dataset.topo_dem = t.ds.dem.T
topo_dem.plot()
<matplotlib.collections.QuadMesh at 0x7f960778c810>
# Define an analytic parameter.
depth = sympy.symbols("depth")
# Define our ocean layer.
ocean_properties = lm.material.acoustic.Velocity.from_params(
vp=1500.0, rho=1010 + 40 * depth / 5000
)
depth = ...
: The definition of depth
above introduces the ability to
define "analytic" parameters with respect to model coordinates. Possible
values here would be one of x
, y
, z
, v
, or depth
. x
, y
, and
z
are the three Cartesian dimensions of our model, while v
is a generic
coordinate corresponding to the vertical coordinate in a given dimension
(y
in 2-D and z
in 3-D). depth
, which we've used, is similar to v
in that it is generic over the domain's dimension, but is defined instead
with respect to the top surface of the domain. Analytic parameters are
defined using sympy
symbols.ocean_properties = ...
: Here we declare the material that defines our
ocean -- an acoustic material defined by P-wave velocity and density. A
variety of other materials, including multiple parameterizations for many,
can be found in the layered meshing
[materials]
(https://mondaic.com/docs/references/python_api/salvus/mesh/layered_meshing/material)
submodule. Each argument to the .from_params(...)
method associated with
a given material's parameterization can take one of a constant value (as
we've done for vp
), a symbolic expression (as we've done for rho
), or
an xarray
DataArray
(described
below).# Density varies only with depth.
rho = xr.DataArray([1500.0, 1600.0], [("v_relative", [1.0, 0.0])])
# Velocity varies with our x coordinate and depth.
vs = xr.DataArray(
[[300.0, 600.0], [600.0, 900.0]],
[("x", [576065.0, 544662.0]), ("v_relative", [1.0, 0.0])],
)
# Define our propeties, using a simple scaling to compute vp from vs.
near_surface_properties = lm.material.elastic.Velocity.from_params(
vs=vs,
rho=rho,
vp=np.sqrt(3) * vs,
)
rho = ...
: The definition of density here introduces the use of an
xarray.DataArray
as a way to define discrete parameters. xarray
is a
fantastic toolbox for working with regularly sampled data, and a host of
tutorials and examples can be found on its website.
Here we define a simple density model with two values that are associated
with the coordinates "v_relative"
. "v_relative"
is a parameter that is
currently exclusive to discrete, xarray
-based parameter representations,
and is a flag telling Salvus to interpolate the model solely within its
host layer, with the value at v_relative=1.0
corresponding to the value
at the layer's top interface, and v_relative=0.0
to the value at the
layer's bottom interface. This is handy to ensure that your parameter
definition shrinks and stretches with any mesh deformations, as we'll se
below. We could have alternatively used any of the other coordinate values
outlined in the discussion surrounding analytic parameters, in which case
the specified locations would be interpreted as absolute with respect to
our coordinate system. The presence of v
in the phrase v_relative
holds
the same meaning as before, in that it is a generalization of the vertical
coordinate name.vs = ...
: Here we expand on the definition of rho
above by defining a
2-D model for vs
. We again use v_relative
to signal that we want the
variation in the vertical direction to occur exclusively within the layer,
but in this case also include an x coordinate to signal variation along the
x direction. The values of the x coordinate here are absolute with respect
to the UTM zone we'll be working in. In summary, then, we've defined a vs
model that vertically varies between 300.0 and 600.0 m/s in the east, which
increases to between 600.0 and 900.0 m/s as we travel west. Note that these
x
values do not span the entirety of our domain but they, along with
the missing y
coordinate, will be extruded to fit.vp = ...
: Here we simply create a vp
model by scaling our vs
model by
the square root of 3, which corresponds to a material with a Poisson's
ratio of 0.25.# Velocity varies with depth.
vs = xr.DataArray([1000.0, 1500.0], [("v_relative", [1.0, 0.0])])
# Define our properties.
compacted_sediments_properties = lm.material.elastic.Velocity.from_params(
rho=1700.0, vs=vs, vp=np.sqrt(3 * vs)
)
"v_relative"
informs Salvus that the materials should be interpolated with
respect to the layer's vertical bounds, and thus be shrunk or stretched
accordingly.bedrock_properties = lm.material.elastic.Velocity.from_params(
rho=xr.DataArray([1000.0, 2600.0], [("depth", [0.0, 15_000.0])]),
vp=xr.DataArray([1450.0, 6800.0], [("depth", [0.0, 15_000.0])]),
vs=xr.DataArray([1000.0, 3900.0], [("depth", [0.0, 15_000.0])]),
)
"depth"
as the coordinate of
interest. This now means that, rather than interpolating uniformly between
the layer's bounding interfaces, we instead interpret the coordinates as the
absolute depth below the domain's top surface.xarray
dataset, we have a choice to
define a a model in any of the 0 to n dimensions of the domain, or any subset
of them. The missing dimensions will be added accordingly, and if they
defined model does not span the domain's extent in any of these dimensions it
will additionally be extruded to do so. The only restriction is that multiple
values can not be specified for a single coordinate, i.e. "depth"
and
"v_relative"
would be invalid.ocean_surface = lm.interface.Hyperplane.at(0.0)
Hyperplane
. A Hyperplane
is a simple interface in that it is always flat in whichever coordinate
system we choose. In this case we are requesting a flat interface at an
absolute elevation of 0.0 which defines our ocean surface, a Hyperplane
in
spherical coordinates would represent a constant radial value. The value of
0.0 used in this case is relevant in the context of our UTM domain, where
elevation is provided with respect to mean sea level.xarray
representation of our digital
elevation model (DEM) from the GMRT dataset we downloaded, and specifying
that it is also with reference to mean sea level.topography = lm.interface.Surface(
topo_dem.assign_attrs(reference_elevation=0.0)
)
.map(...)
attribute
on our newly-created surface topography object. This gives us access to the
xarray
representation of the interface, and allows us to assign a new
reference elevation. Below, we shift the interface down by 200 meters, giving
us a layer with a consistent thickness of 200 m. It is also possible to use
the map
interface to, for example, scale our topography or perform other
operations while keeping its basic structure constant.bottom_of_near_surface = topography.map(
lambda dem: dem.assign_attrs(reference_elevation=-200.0)
)
Hyperplane
to declare our intent
that this interface is flat in our Cartesian coordinate system. We set the
depth of this layer to 4km below the top of the domain. Importantly, this is
below the bottom of the near-surface layer, so we do not have another layer
crossing boundaries.bottom_of_sediments = lm.interface.Hyperplane.at(lm.interface.Depth(4000.0))
Domain
object, the UTMDomain
classification leaves the top
and bottom bounds of the domain undefined and expects them to be specified at
the time of mesh generation. Our top surface has already been well defined --
by the ocean_surface
and the topography
interfaces, and it thus remains
to define our bottom interface. Below we define yet another Hyperplane
to
specify the bottom of the domain, here at a depth of 10km below the surface.bottom_of_domain = lm.interface.Hyperplane.at(-10_000.0)
LayeredModel
. This class is accessible from the
layered_meshing
module we've already imported, and simple takes a list of
alternating interfaces and layers.layered_model = lm.LayeredModel(
[
ocean_surface,
ocean_properties,
topography,
near_surface_properties,
bottom_of_near_surface,
compacted_sediments_properties,
bottom_of_sediments,
bedrock_properties,
bottom_of_domain,
]
)
mesh_from_domain
function from the layered_meshing
module. This takes our
domain and our layered model which we just defined, as well as a minimal
MeshResolution
object that encodes properties like the order of model
representation, the elements per wavelength we're targeting, and the
reference frequency for which we want to generate the mesh. Let's go ahead
and execute it with the layered model we've put together.try:
lm.mesh_from_domain(
domain=d,
model=layered_model,
mesh_resolution=sn.MeshResolution(reference_frequency=1.0),
)
except ValueError as e:
print(f"ValueError: {e}")
ValueError: After interpolation, layer boundaries have been found to cross.
ValueError: After interpolation, layer boundaries have been found to cross.
lm.InterlayerConstant()
: enforce a constant element size between layerslm.InterlayerDoubling()
: allow for a doubling of mesh size between layerslm.InterlayerTripling()
: allow for a tripling of mesh size between layersInterlayerDoubling()
, which
will attempt to insert one or more doubling layers at each interface where
the minimum wavelength jumps by at least factor of 2.lm.IntralayerConstant()
: enforce a constant element size within each
layerlm.IntralayerVerticalRefine()
: allow for anisotropic coarsening within
each layer, which can result in a variable number of elements in the
vertical directionIntralayerConstant()
, which
enforces a constant number of elements in the vertical direction within each
layer.lm.IntralayerVerticalRefine(refinement_type="doubling", min_layer_thickness=100.0)
: For our top layer we will allow for a variable
number of elements in the vertical direction and, most importantly, clip
the minimum thickness of the layer at 100 meters. This will allow us to
model the shoreline, as elements less than 100 meters thick will be removed
from the resultant mesh.lm.IntralayerConstant()
: We expect the geotechnical layer to be of a
constant thickness, and so don't require any non-standard policy here.lm.IntralayerVerticalRefine(refinement_type="tripling")
: Our third layer
is also of variable thickness, so we again choose a policy allowing for a
variable number of vertical elements. We don't specify a minimum thickness
here as the layer does not pinch out, and we choose the "tripling"
refinement type.lm.IntralayerConstant()
: We again switch back to the default case of a
structured mesh for the bedrock layer and below.lm.InterlayerConstant()
x2: We want to ensure that no coarsenings occur
between the first and third layer. The reason for the top layer is that we
don't want to place coarsenings where we expect a layer to pinch out, and
for the second layer is that we want to avoid coarsening in very thin
layers.lm.InterlayerDoubling()
: Below the top two layers we relax our
restrictions and allow the mesh to double the element size at will. This
will ensure that faster velocities in deeper layers can be modelled using
fewer elements.LayeredModel
in a MeshingProtocol
object which takes the policies as
arguments.mesh = lm.mesh_from_domain(
domain=d,
model=lm.MeshingProtocol(
layered_model,
intralayer_coarsening_policy=[
lm.IntralayerVerticalRefine(
refinement_type="doubling", min_layer_thickness=100.0
),
lm.IntralayerConstant(),
lm.IntralayerVerticalRefine(refinement_type="tripling"),
lm.IntralayerConstant(),
],
interlayer_coarsening_policy=[
lm.InterlayerConstant(),
lm.InterlayerConstant(),
lm.InterlayerDoubling(),
],
),
mesh_resolution=sn.MeshResolution(reference_frequency=1.5),
)
mesh
<salvus.mesh.data_structures.unstructured_mesh.unstructured_mesh.UnstructuredMesh object at 0x7f96091f7d50>
SalvusProject
via an
UnstructuredMeshSimulationConfiguration
.
For more information on this, please check out our collection of related
tutorials.