Skip to content

Topography and Refinements

Jupyter Notebook

This page is automatically generated from a Jupyter Notebook.

Note that everything here is fully automatically tested against - and thus guaranteed to work - only for the latest versions of all the Salvus packages. So please update if anything works differently on your machine.

Click here for a version that is compatbile with the 0.9.X releases.

Beyond piecewise structured grids

Vertical refinement in layers with strong thickness variation

This notebooks shows how problems with small time steps in layers with strongly varying thickness can be avoided. One prominent example is the crustal thickness variations.

Salvus provides structured grid classes in 2D and 3D, that allow to vary the number of elements in one dimension, creating anisotropic element sizes. When combined with a corresponding topography model, the elements end up with isotropic sizes again and avoid the small time step that would result from a regular grid.

The workflow is as follows:

  • create a topography model
  • compute the number of elements needed in vertical direction according to the resolution criterion
  • create a structured grid with anisotropic refinement
  • convert to an unstructured mesh and deform the elements according to the topography
# initialize notebook
%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np
plt.style.use('ggplot')
plt.rcParams['figure.figsize'] = (12, 8)
from salvus_mesh.structured_grid_2D import StructuredGrid2D
from salvus_mesh.skeleton import Skeleton

# general input
max_x = 10000.          # domain width in m
fmax = 2.               # maximum frequency in Hz
elements_per_wavelength = 2.
vs = 1000.              # s-wave velocity in m / s
vp = 1700.              # p-wave velocity in m / s

refine_x = 1.2          # extra security factor to make elements smaller before
refine_y = 1.2          # deformation

# compute edgelengths
hmax = vs / fmax / elements_per_wavelength

# create some artificial topography model - h(x)
nelem_x = int(np.ceil(max_x / hmax * refine_x))
h0 = .6
h1 = .5
h2 = -.1
h3 = .15

x = np.linspace(0., max_x, nelem_x + 1)
norm_x = x / max_x * 2 * np.pi
h = h0 - h1 * np.cos(norm_x) - h2 * np.cos(2 * norm_x) - h3 * np.sin(3 * norm_x)
h = h * max_x / 2 / np.pi

# number of vertical elements needed for each element in horizontal direction
nelem_y = np.ceil(h / hmax * refine_y).astype('int')

# create box mesh with refinements
sg1 = StructuredGrid2D.rectangle_vertical_refine_doubling(
    nelem_x, nelem_y, min_x=0, max_x=max_x, min_y=0., max_y=np.max(h))

# make unstructured mesh
m1 = sg1.get_unstructured_mesh()
m1.find_side_sets('cartesian')
m1.plot(linewidths=1.)

png

At this point we have a somewhat unusal looking mesh on a cartesian domain and varying number of elements in vertical direction. Once we deform it according to the topography, it looks much more reasonable

m1.add_dem_2D(x, h - np.max(h))
m1.apply_dem()
m1.plot(linewidths=1.)

png

For reference we also create a mesh without refinements,

sg2 = StructuredGrid2D.rectangle(
    nelem_x, max(nelem_y), min_x=0, max_x=max_x, min_y=0., max_y=np.max(h))
m2 = sg2.get_unstructured_mesh()
m2.find_side_sets('cartesian')
m2.plot(linewidths=1.)

png

which has very thin elements after applying topography:

m2.add_dem_2D(x, h - np.max(h))
m2.apply_dem()
m2.plot(linewidths=1.)

png

These result in small time steps due to the Courant criterion discussed in exercise 2. We again use the accurate algorithm to estimate \Delta t, which accounts for the shape of the elements:

dt1, dt1_elem = m1.compute_dt(vp, fast=False)
dt2, dt2_elem = m2.compute_dt(vp, fast=False)

print('dt1:       {:10.4f}'.format(dt1))
print('dt2:       {:10.4f}'.format(dt2))
print('dt1 / dt2: {:10.4f}'.format(dt1 / dt2))

# plot dt over the mesh
m1.attach_field('dt', dt1_elem)
m2.attach_field('dt', dt2_elem)
dt1:           0.0199
dt2:           0.0061
dt1 / dt2:     3.2646

If we include the number of elements, which is also reduced with the anistropic refinement, we can estimate the resulting speed up of the simulation:

cost1 = m1.nelem / dt1
cost2 = m2.nelem / dt2

print('nelem1:  ', m1.nelem)
print('nelem2:  ', m2.nelem)
print('speedup:  {:.2f}'.format(cost2 / cost1))
nelem1:   273
nelem2:   480
speedup:  5.74
m1

<salvus_mesh.unstructured_mesh.UnstructuredMesh at 0x7f7e6f585278>
m2

<salvus_mesh.unstructured_mesh.UnstructuredMesh at 0x7f7e6adc8390>

Outlook: Time step optimization

The mesh can be further improved by smoothing with a criterion that directly optimizes the time step. Note that the method is work in progress and currently does not ensure that the resolution criterion remains fulfilled and hence should be used with caution.

from salvus_mesh.optimize_dt import optimize_dt

# collect side sets where nodes should not be moved
side_sets = [m1.get_side_set(ssn) for ssn in m1.side_sets.keys()]

# actual optimization
m3 = optimize_dt(m1, [side_sets] * 2, maxiter=50)

# compute new dt and speedup compared to unsmoothed mesh
dt3, dt3_elem = m3.compute_dt(vp, fast=False)
cost3 = m3.nelem / dt3

print('dt3:                  {:10.3f}'.format(dt3))
print('speedup by smoothing: {:10.3f}'.format(cost1 / cost3))
print('total speedup:        {:10.3f}'.format(cost2 / cost3))

# plot dt over the mesh
m3.attach_field('dt', dt3_elem)
m3
dt3:                       0.033
speedup by smoothing:      1.632
total speedup:             9.368

<salvus_mesh.unstructured_mesh.UnstructuredMesh at 0x7f7e64a1a0f0>