Tutorial 6: Spatially-variable Strain Field

In this tutorial, we will create a synthetic secular velocity field of two plates moving past each other, with one also containing an inflating volcano. We will then recover the Euler Pole of a plate, as well as some strain (field) quantities.

Preparations

We will start by some (hopefully) self-explanatory imports, including the strain tools from the tools module:

>>> # imports
>>> import os
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> from pathlib import Path
>>> from matplotlib import rcParams
>>> from scipy.spatial.distance import cdist
>>> from cmcrameri import cm
>>> from disstans.tools import (get_hom_vel_strain_rot, get_field_vel_strain_rot,
...                             strain_rotation_invariants,
...                             estimate_euler_pole, rotvec2eulerpole)

Let’s continue to set up some output formats:

>>> # decide output quality, looks & folder
>>> outdir = Path("out/tutorial_6/")
>>> rcParams['font.sans-serif'] = ["NewComputerModernSans10"]
>>> rcParams['font.size'] = "14"
>>> fmt = "png"
>>> os.makedirs(outdir, exist_ok=True)

We now build up a synthetic network of stations with random locations and velocities:

>>> # initialize RNG
>>> rng = np.random.default_rng(0)
>>> # make stations
>>> locs = rng.uniform(low=[-121, 33],
...                    high=[-116, 37],
...                    size=(100, 2))
>>> n_locs = locs.shape[0]
>>> # get indices west and east of plate boundary
>>> i_west = locs[:, 0] < -119
>>> i_east = ~i_west
>>> locs_west = locs[i_west, :]
>>> locs_east = locs[i_east, :]
>>> # make velocities
>>> # create noisy zero background velocity field
>>> vels = rng.normal(scale=1e-3, size=(n_locs, 2))
>>> # add far away euler pole motion to the western plate
>>> vels[i_west, :] += np.array([[-0.055, 0.045]])
>>> # add inflation motion from volcano based on distance and direction
>>> center_vol = np.array([-117.1, 35.4])
>>> dist_vol = cdist(locs, center_vol[None, :]).ravel()
>>> vecs_vol = locs - center_vol[None, :]
>>> azims_vol = np.arctan2(vecs_vol[:, 1], vecs_vol[:, 0])
>>> vels_volcano = (vecs_vol / np.linalg.norm(vecs_vol, axis=1, keepdims=True)
...                 / dist_vol[:, None]**2 / 70)
>>> vels += vels_volcano
>>> # make for uncertainties
>>> vels_var = (rng.uniform(low=9e-3, high=11e-3, size=(n_locs, 2))) ** 2
>>> vels_cov = rng.normal(scale=1e-6, size=(n_locs, 1))
>>> vels_varcov = np.concatenate([vels_var, vels_cov], axis=1)

With these commands, we have effectively created a toy representation of California. On the west side, we have a plate that moves northwest relative to the east side. On the east side, there is a volcanic caldera inflating outwards. Let’s have a look (all plotting code can be found in the script):

../_images/tutorial_6a.png

Note

For the plots, we have not converted latitude and longitude to a cartesian grid like UTM, or used a geodetic map projection. This is only to keep this tutorial simple; usually, we would either convert to UTM or use proper map plotting.

Note

This tutorial is only to show the strain tools, which are independent of the Network/Station/Timeseries structure of DISSTANS, since they only require locations and velocities. Both of these can, of course, be derived from the standard DISSTANS classes, like station_locations or parameters.

Average strain and rotation

All of the next calculations assume that plates are rigid, and that rigid motion on a sphere can be expressed as a rotation around a pole on the sphere. For more details on the algorithms, please refer to, e.g., [tape09], [shen15], or [goudarzi14]. Let’s start by getting the average velocity, strain, and rotation values of the entire area using get_hom_vel_strain_rot() and strain_rotation_invariants():

>>> # get global strain somewhere in the west
>>> v_hom, eps_hom, om_hom = get_hom_vel_strain_rot(locs,
...                                                 vels,
...                                                 covariances=vels_varcov,
...                                                 reference=[-120.5, 35])
>>> # convert to scalars
>>> dil_hom, strain_hom, shear_hom, rot_hom = strain_rotation_invariants(eps_hom, om_hom)
>>> print(f"Average motion = [{v_hom[0] * 1000:.2f}, {v_hom[1] * 1000:.2f}] mm/a\n"
...       f"Average rotation = {rot_hom * 1e6:.4f} rad/Ma")
Average motion = [-58.92, 41.94] mm/a
Average rotation = 0.0851 rad/Ma

The average motion is dominated by the motion of the western plate, and the rotation arises from the relative motion of the two plates. Let’s see if we can recover the Euler plate motion of the western plate using estimate_euler_pole() and rotvec2eulerpole():

>>> # get euler pole for the western plate
>>> rotvec_west, rotcov_west = estimate_euler_pole(locs[i_west, :],
...                                                vels[i_west, :],
...                                                covariances=vels_varcov[i_west, :])
>>> # convert to more readable format
>>> ep_west, ep_cov_west = rotvec2eulerpole(rotvec_west, rotcov_west)
>>> ep_west_deg = np.rad2deg(ep_west)
>>> print(f"Longitude = {ep_west_deg[0]:.1f} °E\n"
...       f"Latitude = {ep_west_deg[1]:.1f} °N\n"
...       f"Rotation = {ep_west_deg[2] * 1e6:.1f} °/Ma")
Longitude = 6.7 °E
Latitude = -39.3 °N
Rotation = 0.6 °/Ma

These are indeed (very approximately) the Euler pole values of the Pacific plate.

Spatially-variable velocity, strain, and rotation

Finally, we know by construction that there’s more going on than those average values. We want to be able to see the strain and velocity field as it transitions from the west to the east is affected by the volcano we put there. Let’s create a field of where we want to evalute the interpolated values, and then call the field-based get_field_vel_strain_rot():

>>> # define field coordinates
>>> x_range = np.linspace(-121, -116, num=50)
>>> y_range = np.linspace(33, 37, num=50)[::-1]
>>> x_mesh, y_mesh = np.meshgrid(x_range, y_range)
>>> xy_mesh = np.stack([x_mesh.ravel(), y_mesh.ravel()], axis=1)
>>> # get strain fields
>>> v_field, eps_field, om_field = \
...     get_field_vel_strain_rot(locs,
...                             vels,
...                             xy_mesh,
...                             2,
...                             covariances=vels_varcov,
...                             estimate_within=1e6,
...                             distance_method="quadratic",
...                             coverage_method="voronoi")
>>> # convert to scalar fields
>>> dil_field, strain_field, shear_field, rot_field = \
...     strain_rotation_invariants(eps_field, om_field)

Yielding the following northwards velocity field:

../_images/tutorial_6b.png

The velocity field indeed shows a transition from one plate to the other. The border is relatively smooth because there aren’t a lot of stations close to the dividing line. The smoothness can also be controlled by the parameters of get_field_vel_strain_rot(). The north-south velocity around the volcano is also recovered. Finally, the strain map also shows the plate interface and the volcano:

../_images/tutorial_6c.png