Tesseroids#

When our region of interest covers several longitude and latitude degrees, utilizing Cartesian coordinates to model geological structures might introduce significant errors: they don’t take into account the curvature of the Earth. Instead, we would need to work in Spherical coordinates. A common approach to forward model bodies in geocentric spherical coordinates is to make use of tesseroids.

A tesseroid (a.k.a spherical prism) is a three dimensional body defined by the volume contained by two longitudinal boundaries, two latitudinal boundaries and the surfaces of two concentric spheres of different radii (see Figure: Tesseroid).

Figure showing a tesseroid defined in a geocentric spherical coordinate system

Figure: Tesseroid#

Tesseroid defined by two longitude coordinates (\(\lambda_1\) and \(\lambda_2\)), two latitude coordinates (\(\phi_1\) and \(\phi_2\)) and the surfaces of two concentric spheres of radii \(r_1\) and \(r_2\). This figure is a modified version of [Uieda2015].

Through the harmonica.tesseroid_gravity function we can calculate the gravitational field of any tesseroid with a given density on any computation point. Each tesseroid can be represented through a tuple containing its six boundaries in the following order: west, east, south, north, bottom, top, where the former four are its longitudinal and latitudinal boundaries in decimal degrees and the latter two are the two radii given in meters.

These two radii represent the top and bottom surfaces of the tesseroid, and should be given as distances from the center of the Earth. Note this is different from the vertical boundaries used for prisms in Cartesian coordinates, which are given as heights above or below some reference level (e.g., mean sea level or a reference ellipsoid).

Note

The harmonica.tesseroid_gravity numerically computed the gravitational fields of tesseroids by applying a method that applies the Gauss-Legendre Quadrature along with a bidimensional adaptive discretization algorithm. Refer to [Soler2019] for more details.

Lets define a single tesseroid and compute the gravitational potential it generates on a regular grid of computation points located at 10 km above its top boundary.

Get the WGS84 reference ellipsoid from boule so we can obtain its mean radius:

import boule as bl

ellipsoid = bl.WGS84
mean_radius = ellipsoid.mean_radius

Define the tesseroid and its density (in kg per cubic meters):

tesseroid = (-70, -50, -40, -20, mean_radius - 10e3, mean_radius)
density = 2670

Define a set of computation points located on a regular grid at 100 km above the top surface of the tesseroid:

import bordado as bd

coordinates = bd.grid_coordinates(
    region=[-80, -40, -50, -10],
    shape=(80, 80),
    non_dimensional_coords=100e3 + mean_radius,
)

Lets compute the downward component of the gravitational acceleration it generates on the computation point:

import harmonica as hm

gravity = hm.tesseroid_gravity(coordinates, tesseroid, density, field="g_z")

Important

The downward component \(g_z\) of the gravitational acceleration computed in spherical coordinates corresponds to \(-g_r\), where \(g_r\) is the radial component.

And finally plot the computed gravitational field

import pygmt
import verde as vd

grid = vd.make_xarray_grid(
   coordinates, gravity, data_names="gravity", extra_coords_names="extra")

fig = pygmt.Figure()
title = "Downward component of gravitational acceleration"
with pygmt.config(FONT_TITLE="12p"):
   fig.grdimage(
      region=[-80, -40, -50, -10],
      projection="M-60/-30/10c",
      grid=grid.gravity,
      frame=["a", f"+t{title}"],
      cmap="viridis",
   )

fig.colorbar(cmap=True, frame=["a200f50", "x+lmGal"])
fig.coast(shorelines="1p,black")

# Plot edges of tesseroid
fig.plot(
   x=[tesseroid[0], tesseroid[1], tesseroid[1], tesseroid[0], tesseroid[0]],
   y=[tesseroid[2], tesseroid[2], tesseroid[3], tesseroid[3], tesseroid[2]],
   pen="1p,red",
   label="Tesseroid boundary",
)
fig.legend()

fig.show()
../../_images/tesseroid_5_0.png

Multiple tesseroids#

We can compute the gravitational field of a set of tesseroids by passing a list of them, where each tesseroid is defined as mentioned before, and then making a single call of the harmonica.tesseroid_gravity function.

Lets define a set of four prisms along with their densities:

tesseroids = [
    [-70, -65, -40, -35, mean_radius - 100e3, mean_radius],
    [-55, -50, -40, -35, mean_radius - 100e3, mean_radius],
    [-70, -65, -25, -20, mean_radius - 100e3, mean_radius],
    [-55, -50, -25, -20, mean_radius - 100e3, mean_radius],
]
densities = [2670 , 2670, 2670, 2670]

Compute their gravitational effect on a grid of computation points:

coordinates = bd.grid_coordinates(
    region=[-80, -40, -50, -10],
    shape=(80, 80),
    non_dimensional_coords=100e3 + mean_radius,
)
gravity = hm.tesseroid_gravity(coordinates, tesseroids, densities, field="g_z")

And plot the results:

grid = vd.make_xarray_grid(
   coordinates, gravity, data_names="gravity", extra_coords_names="extra")

fig = pygmt.Figure()
title = "Downward component of gravitational acceleration"
with pygmt.config(FONT_TITLE="12p"):
   fig.grdimage(
      region=[-80, -40, -50, -10],
      projection="M-60/-30/10c",
      grid=grid.gravity,
      frame=["a", f"+t{title}"],
      cmap="viridis",
   )

fig.colorbar(cmap=True, frame=["a1000f500", "x+lmGal"])
fig.coast(shorelines="1p,black")

# Plot edges of tesseroids
for i, tesseroid in enumerate(tesseroids):
   if i == 0:
      label="Tesseroid boundaries"
   else:
      label=None
   fig.plot(
      x=[tesseroid[0], tesseroid[1], tesseroid[1], tesseroid[0], tesseroid[0]],
      y=[tesseroid[2], tesseroid[2], tesseroid[3], tesseroid[3], tesseroid[2]],
      pen="1p,red",
      label=label,
   )
fig.legend()

fig.show()
../../_images/tesseroid_8_0.png

Tesseroids with variable density#

The harmonica.tesseroid_gravity is capable of computing the gravitational effects of tesseroids whose density is defined through a continuous function of the radial coordinate. This is achieved by the application of the method introduced in [Soler2021].

To do so we need to define a regular Python function for the density, which should have a single argument (the radius coordinate) and return the density of the tesseroids at that radial coordinate. In addition, we need to decorate the density function with numba.jit(nopython=True) or numba.njit for short.

Lets compute the gravitational effect of four tesseroids whose densities are given by a custom linear density function.

Start by defining the tesseroids

tesseroids = (
    [-70, -60, -40, -30, mean_radius - 3e3, mean_radius],
    [-70, -60, -30, -20, mean_radius - 5e3, mean_radius],
    [-60, -50, -40, -30, mean_radius - 7e3, mean_radius],
    [-60, -50, -30, -20, mean_radius - 10e3, mean_radius],
)

Then, define a linear density function. We need to use the jit decorator so Numba can run the forward model efficiently.

from numba import njit

@njit
def density(radius):
    """Linear density function"""
    top = mean_radius
    bottom = mean_radius - 10e3
    density_top = 2670
    density_bottom = 3000
    slope = (density_top - density_bottom) / (top - bottom)
    return slope * (radius - bottom) + density_bottom

Lets create a set of computation points located on a regular grid at 100km above the mean Earth radius:

coordinates = bd.grid_coordinates(
    region=[-80, -40, -50, -10],
    shape=(80, 80),
    non_dimensional_coords=100e3 + ellipsoid.mean_radius,
)

And compute the gravitational fields the tesseroids generate:

gravity = hm.tesseroid_gravity(coordinates, tesseroids, density, field="g_z")

Finally, lets plot it:

grid = vd.make_xarray_grid(
   coordinates, gravity, data_names="gravity", extra_coords_names="extra")

fig = pygmt.Figure()
title = "Downward component of gravitational acceleration"
with pygmt.config(FONT_TITLE="12p"):
   fig.grdimage(
      region=[-80, -40, -50, -10],
      projection="M-60/-30/10c",
      grid=grid.gravity,
      frame=["a", f"+t{title}"],
      cmap="viridis",
   )
fig.colorbar(cmap=True, frame=["a200f100", "x+lmGal"])
fig.coast(shorelines="1p,black")

# Plot edges of tesseroids
for i, tesseroid in enumerate(tesseroids):
   if i == 0:
      label="Tesseroid boundaries"
   else:
      label=None
   fig.plot(
      x=[tesseroid[0], tesseroid[1], tesseroid[1], tesseroid[0], tesseroid[0]],
      y=[tesseroid[2], tesseroid[2], tesseroid[3], tesseroid[3], tesseroid[2]],
      pen="1p,red",
      label=label,
   )
fig.legend()

fig.show()
../../_images/tesseroid_13_0.png

Tesseroid layer#

A common use of tesseroids is to model geologic structures on regional or global scales, where the curvature of the Earth cannot be neglected. Harmonica offers the possibility to define a layer of tesseroids through the harmonica.tesseroid_layer function: a regular grid of tesseroids of equal size along the longitudinal and latitudinal dimensions and with variable top and bottom boundaries. It returns a xarray.Dataset with the coordinates of the centers of the tesseroids and their corresponding physical properties.

The harmonica.DatasetAccessorTesseroidLayer Dataset accessor can be used to obtain some properties of the layer like its shape and size or the boundaries of any tesseroid in the layer. Moreover, we can use the harmonica.DatasetAccessorTesseroidLayer.gravity method to compute the gravitational field of the tesseroid layer on any set of computation points.

Important

Unlike the harmonica.prism_layer, the surface and reference boundaries of a tesseroid layer must be given as radii measured from the center of the Earth, not as heights above a reference level. We can use the boule.Ellipsoid.geocentric_radius method from boule to obtain the radius of the reference ellipsoid at each latitude, and add our height values to it.

Let’s create a simple tesseroid layer over a region in South America, whose top boundary will approximate a synthetic topography and whose bottom boundary will be set on the surface of the reference ellipsoid. We can start by getting the WGS84 reference ellipsoid from boule and defining the region of the layer and the horizontal dimensions of the tesseroids (in degrees):

import boule as bl

ellipsoid = bl.WGS84
region = (-80, -40, -50, -10)
spacing = 0.5

Then we can define a regular grid where the centers of the tesseroids will fall:

import bordado as bd

longitude, latitude = bd.grid_coordinates(region=region, spacing=spacing)

The bottom boundary of the layer (reference) will be the surface of the ellipsoid, so we compute its geocentric radius at each latitude:

reference = ellipsoid.geocentric_radius(latitude)

We need to define a 2D array with the radii of the uppermost surface of the layer. We will build a synthetic topography and add it to the reference radii so that the surface is also expressed as radii from the center of the Earth:

import numpy as np

max_height = 3e3
topography = (
    max_height * np.sin(longitude * np.pi / 20) * np.cos(latitude * np.pi / 20)
    + max_height
) / 2
surface = reference + topography

Since the surface is expressed as radii, its values are all close to the radius of the ellipsoid (around 6370 km), and the synthetic topography shows up as variations of a few kilometers around it. Let’s plot it to see it more clearly:

import verde as vd

surface_grid = vd.make_xarray_grid(
   (longitude, latitude),
   surface,
   data_names="surface",
   dims=("latitude", "longitude"),
)
topography_grid = vd.make_xarray_grid(
   (longitude, latitude),
   topography,
   data_names="topography",
   dims=("latitude", "longitude"),
)

fig = pygmt.Figure()
gmt_projection = "M-60/-30/10c"
title = "Surface boundary of the tesseroid layer"
with pygmt.config(FONT_TITLE="12p"):
   fig.grdimage(
      region=region,
      projection=gmt_projection,
      grid=surface_grid.surface,
      frame=["a", f"+t{title}"],
      cmap="magma",
   )
fig.colorbar(cmap=True, frame=["af", "x+lSurface radius", "y+lmeters"])
fig.coast(shorelines="1p,black")

fig.shift_origin(xshift="w+1.5c")

title = "Topography"
with pygmt.config(FONT_TITLE="12p"):
   fig.grdimage(
      region=region,
      projection=gmt_projection,
      grid=topography_grid.topography,
      frame=["a", f"+t{title}"],
      cmap="magma",
   )
fig.colorbar(cmap=True, frame=["af", "x+lTopography", "y+lmeters"])
fig.coast(shorelines="1p,black")
fig.show()
../../_images/tesseroid_18_0.png

Let’s assign the same density to each tesseroid through a 2D array with the same value: 2670 kg per cubic meter.

density = np.full_like(surface, 2670.0)

Now we can define the tesseroid layer:

import harmonica as hm

tesseroids = hm.tesseroid_layer(
    coordinates=(longitude, latitude),
    surface=surface,
    reference=reference,
    properties={"density": density},
)
tesseroids
<xarray.Dataset> Size: 159kB
Dimensions:    (latitude: 81, longitude: 81)
Coordinates:
  * latitude   (latitude) float64 648B -50.0 -49.5 -49.0 ... -11.0 -10.5 -10.0
  * longitude  (longitude) float64 648B -80.0 -79.5 -79.0 ... -41.0 -40.5 -40.0
    top        (latitude, longitude) float64 52kB 6.367e+06 ... 6.379e+06
    bottom     (latitude, longitude) float64 52kB 6.366e+06 ... 6.377e+06
Data variables:
    density    (latitude, longitude) float64 52kB 2.67e+03 2.67e+03 ... 2.67e+03
Attributes:
    longitude_units:   degrees
    latitude_units:    degrees
    radius_units:      meters
    properties_units:  SI

Let’s define a grid of observation points located 10 km above the reference ellipsoid. Since the radius of the ellipsoid changes with latitude, we compute the radial coordinate of the observation points accordingly:

grid_longitude, grid_latitude = bd.grid_coordinates(region=region, spacing=spacing)
grid_radius = ellipsoid.geocentric_radius(grid_latitude) + 10e3
coordinates = (grid_longitude, grid_latitude, grid_radius)

And compute the downward component of the gravitational acceleration generated by the tesseroid layer on them:

gravity = tesseroids.tesseroid_layer.gravity(coordinates, field="g_z")

Finally, let’s plot the gravitational field:

grid = vd.make_xarray_grid(
   coordinates,
   gravity,
   data_names="gravity",
   dims=("latitude", "longitude"),
   extra_coords_names="radius",
)

fig = pygmt.Figure()
title = "Gravitational acceleration of a layer of tesseroids"
with pygmt.config(FONT_TITLE="12p"):
   fig.grdimage(
      region=region,
      projection="M-60/-30/10c",
      grid=grid.gravity,
      frame=["a", f"+t{title}"],
      cmap="viridis",
   )
fig.colorbar(cmap=True, frame=["a100f50", "x+lmGal"])
fig.coast(shorelines="1p,black")
fig.show()
../../_images/tesseroid_23_0.png