Skip to content

Commit 8222ed7

Browse files
authored
Forward modelling for tesseroids and point masses (fatiando#60)
Add functions for computing gravitational fields generated by tesseroids and point masses in geocentric spherical coordinates. Forward model for tesseroids only admit constant densities. Implemented 2D and 3D adaptive discretization and editable GLQ orders. Tesseroids are converted to point masses following GLQ and then make use of point mass forward models to compute the gravitational fields. Add two pytest runs: one with Numba jit disabled to check coverage, and another one with Numba jit enabled. Add a decorator for test functions that must be run only if Numba jit is enabled. Add a simple example on forward modelling with tesseroids.
1 parent cc4b9fb commit 8222ed7

14 files changed

+1562
-3
lines changed

Makefile

+11-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
PROJECT=harmonica
33
TESTDIR=tmp-test-dir-with-unique-name
44
PYTEST_ARGS=--cov-config=../.coveragerc --cov-report=term-missing --cov=$(PROJECT) --doctest-modules -v --pyargs
5+
NUMBATEST_ARGS=--doctest-modules -v --pyargs -m use_numba
56
LINT_FILES=setup.py $(PROJECT)
67
BLACK_FILES=setup.py $(PROJECT) examples data/examples
78
FLAKE8_FILES=setup.py $(PROJECT) examples data/examples
@@ -20,13 +21,21 @@ help:
2021
install:
2122
pip install --no-deps -e .
2223

23-
test:
24+
test: test_coverage test_numba
25+
26+
test_coverage:
2427
# Run a tmp folder to make sure the tests are run on the installed version
2528
mkdir -p $(TESTDIR)
26-
cd $(TESTDIR); MPLBACKEND='agg' pytest $(PYTEST_ARGS) $(PROJECT)
29+
cd $(TESTDIR); NUMBA_DISABLE_JIT=1 MPLBACKEND='agg' pytest $(PYTEST_ARGS) $(PROJECT)
2730
cp $(TESTDIR)/.coverage* .
2831
rm -rvf $(TESTDIR)
2932

33+
test_numba:
34+
# Run a tmp folder to make sure the tests are run on the installed version
35+
mkdir -p $(TESTDIR)
36+
cd $(TESTDIR); NUMBA_DISABLE_JIT=0 MPLBACKEND='agg' pytest $(NUMBATEST_ARGS) $(PROJECT)
37+
rm -rvf $(TESTDIR)
38+
3039
format:
3140
black $(BLACK_FILES)
3241

doc/api/index.rst

+9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ Gravity Corrections
1616
normal_gravity
1717
bouguer_correction
1818

19+
Forward modelling
20+
-----------------
21+
22+
.. autosummary::
23+
:toctree: generated/
24+
25+
point_mass_gravity
26+
tesseroid_gravity
27+
1928
Isostasy
2029
--------
2130

doc/install.rst

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Dependencies
2222
* `numpy <http://www.numpy.org/>`__
2323
* `scipy <https://docs.scipy.org/doc/scipy/reference/>`__
2424
* `pandas <http://pandas.pydata.org/>`__
25+
* `numba <https://numba.pydata.org/>`__
2526
* `xarray <https://xarray.pydata.org/>`__
2627
* `attrs <https://www.attrs.org/>`__
2728
* `pooch <http://www.fatiando.org/pooch/>`__

environment.yml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies:
88
- numpy
99
- scipy
1010
- pandas
11+
- numba
1112
- pooch
1213
- verde
1314
- attrs

examples/tesseroid.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
Tesseroid Forward Modelling
3+
===========================
4+
5+
Computing the gravitational fields generated by regional or global scale structures
6+
require to take into account the curvature of the Earth.
7+
One common approach is to use spherical prisms also known as tesseroids.
8+
We will compute the gravitational potential and the radial component of the acceleration
9+
generated by a single tesseroid on a computation grid through the
10+
:func:`harmonica.tesseroid_gravity` function.
11+
"""
12+
import harmonica as hm
13+
import verde as vd
14+
import matplotlib.pyplot as plt
15+
import cartopy.crs as ccrs
16+
17+
18+
# Get default ellipsoid (WGS84) to obtain the mean Earth radius
19+
ellipsoid = hm.get_ellipsoid()
20+
21+
# Define computation points as a regular grid at 1km above the mean Earth radius
22+
region = [-10, 10, -10, 10]
23+
spacing = 0.1
24+
radius = 1e3 + ellipsoid.mean_radius
25+
coordinates = vd.grid_coordinates(region, spacing=spacing, extra_coords=radius)
26+
27+
# Define tesseroid with top surface at the mean Earth radius, thickness of 10km and
28+
# density of 2670kg/m^3
29+
west, east, south, north = -5, 5, -5, 5
30+
thickness = 10e3
31+
top = ellipsoid.mean_radius
32+
bottom = top - thickness
33+
tesseroid = [west, east, south, north, bottom, top]
34+
density = 2670
35+
36+
# Compute the potential and the radial component of the acceleration
37+
fields = "potential g_r".split()
38+
results = {}
39+
for field in fields:
40+
results[field] = hm.tesseroid_gravity(coordinates, tesseroid, density, field=field)
41+
42+
# Plot the gravitational fields
43+
fig = plt.figure(figsize=(8, 5))
44+
units = {"potential": "J/kg", "g_r": "mGal"}
45+
titles = {
46+
"potential": "Potential gravitational field",
47+
"g_r": "Radial component of gravitational gradient",
48+
}
49+
for i, field in enumerate(fields):
50+
# Add subplot
51+
ax = fig.add_subplot(1, len(fields), i + 1, projection=ccrs.PlateCarree())
52+
# Plot gravitational field and colorbar
53+
img = ax.pcolormesh(*coordinates[:2], results[field])
54+
plt.colorbar(
55+
img, ax=ax, pad=0.1, aspect=50, orientation="horizontal", label=units[field]
56+
)
57+
# Add grid lines
58+
grid_lines = ax.gridlines(
59+
ccrs.PlateCarree(),
60+
draw_labels=[True, True, False, False],
61+
linestyle="--",
62+
alpha=0.4,
63+
)
64+
grid_lines.xlabels_top = False
65+
grid_lines.ylabels_right = False
66+
ax.set_title(titles[field])
67+
plt.show()

harmonica/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from .isostasy import isostasy_airy
1313
from .gravity_corrections import normal_gravity, bouguer_correction
1414
from .coordinates import geodetic_to_spherical, spherical_to_geodetic
15+
from .forward.point_mass import point_mass_gravity
16+
from .forward.tesseroid import tesseroid_gravity
1517

1618

1719
# Get the version number through versioneer

harmonica/forward/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Forward modeling functions for the gravity and magnetic fields of geometric shapes.
3+
"""

harmonica/forward/point_mass.py

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""
2+
Forward modelling for point masses
3+
"""
4+
import numpy as np
5+
from numba import jit
6+
7+
from ..constants import GRAVITATIONAL_CONST
8+
9+
10+
def point_mass_gravity(coordinates, points, masses, field, dtype="float64"):
11+
"""
12+
Compute gravitational fields of a point mass on spherical coordinates.
13+
14+
Parameters
15+
----------
16+
coordinates : list or array
17+
List or array containing `longitude`, `latitude` and `radius` of computation
18+
points defined on a spherical geocentric coordinate system.
19+
Both `longitude` and `latitude` should be in degrees and `radius` in meters.
20+
points : list or array
21+
List or array containing the coordinates of the point masses `longitude_p`,
22+
`latitude_p`, `radius_p` defined on a spherical geocentric coordinate system.
23+
Both `longitude_p` and `latitude_p` should be in degrees and `radius` in meters.
24+
masses : list or array
25+
List or array containing the mass of each point mass in kg.
26+
field : str
27+
Gravitational field that wants to be computed.
28+
The available fields are:
29+
30+
- Gravitational potential: ``potential``
31+
- Radial acceleration: ``g_r``
32+
dtype : data-type (optional)
33+
Data type assigned to resulting gravitational field, and coordinates of point
34+
masses and computation points. Default to ``np.float64``.
35+
36+
37+
Returns
38+
-------
39+
result : array
40+
Gravitational field generated by the `point_mass` on the computation points
41+
defined in `coordinates`.
42+
The potential is given in SI units, the accelerations in mGal and the Marussi
43+
tensor components in Eotvos.
44+
"""
45+
kernels = {"potential": kernel_potential, "g_r": kernel_g_r}
46+
if field not in kernels:
47+
raise ValueError("Gravity field {} not recognized".format(field))
48+
# Figure out the shape and size of the output array
49+
cast = np.broadcast(*coordinates[:3])
50+
result = np.zeros(cast.size, dtype=dtype)
51+
# Prepare arrays to be passed to the jitted functions
52+
longitude, latitude, radius = (
53+
np.atleast_1d(i).ravel().astype(dtype) for i in coordinates[:3]
54+
)
55+
longitude_p, latitude_p, radius_p = (
56+
np.atleast_1d(i).ravel().astype(dtype) for i in points[:3]
57+
)
58+
masses = np.atleast_1d(masses).astype(dtype).ravel()
59+
# Compute gravitational field
60+
jit_point_mass_gravity(
61+
longitude,
62+
latitude,
63+
radius,
64+
longitude_p,
65+
latitude_p,
66+
radius_p,
67+
masses,
68+
result,
69+
kernels[field],
70+
)
71+
result *= GRAVITATIONAL_CONST
72+
# Convert to more convenient units
73+
if field == "g_r":
74+
result *= 1e5 # SI to mGal
75+
return result.reshape(cast.shape)
76+
77+
78+
@jit(nopython=True)
79+
def jit_point_mass_gravity(
80+
longitude, latitude, radius, longitude_p, latitude_p, radius_p, masses, out, kernel
81+
): # pylint: disable=invalid-name
82+
"""
83+
Compute gravity field of point masses on computation points.
84+
85+
Parameters
86+
----------
87+
longitude, latitude, radius : 1d-arrays
88+
Coordinates of computation points in spherical geocentric coordinate system.
89+
longitude_p, latitude_p, radius_p : 1d-arrays
90+
Coordinates of point masses in spherical geocentric coordinate system.
91+
masses : 1d-array
92+
Mass of each point mass in SI units.
93+
out : 1d-array
94+
Array where the gravitational field on each computation point will be appended.
95+
It must have the same size of ``longitude``, ``latitude`` and ``radius``.
96+
kernel : func
97+
Kernel function that will be used to compute the gravity field on the
98+
computation points.
99+
"""
100+
# Compute quantities related to computation point
101+
longitude = np.radians(longitude)
102+
latitude = np.radians(latitude)
103+
cosphi = np.cos(latitude)
104+
sinphi = np.sin(latitude)
105+
# Compute quantities related to point masses
106+
longitude_p = np.radians(longitude_p)
107+
latitude_p = np.radians(latitude_p)
108+
cosphi_p = np.cos(latitude_p)
109+
sinphi_p = np.sin(latitude_p)
110+
# Compute gravity field
111+
for l in range(longitude.size):
112+
for m in range(longitude_p.size):
113+
out[l] += masses[m] * kernel(
114+
longitude[l],
115+
cosphi[l],
116+
sinphi[l],
117+
radius[l],
118+
longitude_p[m],
119+
cosphi_p[m],
120+
sinphi_p[m],
121+
radius_p[m],
122+
)
123+
124+
125+
@jit(nopython=True)
126+
def kernel_potential(
127+
longitude, cosphi, sinphi, radius, longitude_p, cosphi_p, sinphi_p, radius_p
128+
):
129+
"""
130+
Kernel function for potential gravity field
131+
"""
132+
coslambda = np.cos(longitude_p - longitude)
133+
cospsi = sinphi_p * sinphi + cosphi_p * cosphi * coslambda
134+
distance_sq = (radius - radius_p) ** 2 + 2 * radius * radius_p * (1 - cospsi)
135+
return 1 / np.sqrt(distance_sq)
136+
137+
138+
@jit(nopython=True)
139+
def kernel_g_r(
140+
longitude, cosphi, sinphi, radius, longitude_p, cosphi_p, sinphi_p, radius_p
141+
):
142+
"""
143+
Kernel function for radial component of gravity gradient
144+
"""
145+
coslambda = np.cos(longitude_p - longitude)
146+
cospsi = sinphi_p * sinphi + cosphi_p * cosphi * coslambda
147+
distance_sq = (radius - radius_p) ** 2 + 2 * radius * radius_p * (1 - cospsi)
148+
delta_z = radius_p * cospsi - radius
149+
return delta_z / distance_sq ** (3 / 2)

0 commit comments

Comments
 (0)