Gravity Inversion#

Let’s start this gravity inversion example by generating a synthetic gravity dataset that consists in two prisms with opposite density constrast:

import bordado as bd
import harmonica as hm
import numpy as np
import xarray as xr

# Create some synthetic data
# ==========================
# Define a set of observation points
region = (-100, 100, -100, 100)
shape = (31, 31)
height = 0
coordinates = bd.grid_coordinates(
    region, shape=shape, non_dimensional_coords=height
)

# Define two rectangular prisms with different densities
prisms = [
    (-60, -40, -10, 10, -70, -50),
    (40, 60, -10, 10, -70, -50),
]
densities = np.array([-200, 200])  # SI units

# Compute the gz field they generate on the observation points
gz = hm.prism_gravity(coordinates, prisms, densities, field="g_z")
gz *= -1  # Invert sign to work with upward component

# Add noise
stderr = np.max(np.abs(gz)) * 0.01
gz += np.random.default_rng(seed=51).normal(scale=stderr, size=gz.shape)
gz_std = stderr * np.ones_like(gz)

# Build an xr.DataArray with the synthetic data
dims = ("northing", "easting")
easting, northing, upward = coordinates
easting, northing = easting[0, :], northing[:, 0]
coords = {"easting": easting, "northing": northing, "upward": (dims, upward)}
dataset = xr.Dataset({"gz": (dims, gz), "gz_std": (dims, gz_std)}, coords=coords)
dataset
<xarray.Dataset> Size: 24kB
Dimensions:   (northing: 31, easting: 31)
Coordinates:
  * northing  (northing) float64 248B -100.0 -93.33 -86.67 ... 86.67 93.33 100.0
  * easting   (easting) float64 248B -100.0 -93.33 -86.67 ... 86.67 93.33 100.0
    upward    (northing, easting) float64 8kB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0
Data variables:
    gz        (northing, easting) float64 8kB 0.0002054 0.0002152 ... -0.0002245
    gz_std    (northing, easting) float64 8kB 2.575e-05 2.575e-05 ... 2.575e-05
# Plot synthetic data
import matplotlib.pyplot as plt

dataset.gz.plot.pcolormesh(cbar_kwargs={"label": "gz [mGal]"})
plt.show()
../../_images/gravity-inversion_1_0.png

Start by defining SimPEG’s gravity simulation for the inversion:

import discretize
import simpeg
from simpeg.potential_fields import gravity

# Define the mesh
h = [(5.0, 40)]
mesh = discretize.TensorMesh(h=[h, h, h], origin="CCN")

# Define gravity survey based on the location of the observation points
easting, northing = dataset.easting, dataset.northing
easting, northing = np.meshgrid(easting, northing)
coordinates = (easting, northing, dataset.upward.values)

locations = np.vstack(tuple(c.ravel() for c in coordinates)).T
receivers = gravity.Point(locations, components="gz")
source = gravity.SourceField(receiver_list=[receivers])
survey = gravity.Survey(source)

# Define SimPEG's gravity simulation
simulation_simpeg = gravity.Simulation3DIntegral(
    mesh, survey=survey, rhoMap=simpeg.maps.IdentityMap(mesh)
)

We need to wrap the simulation to make it compatible with the new inversion framework:

import inversion_ideas as ii

simulation = ii.WrappedSimulation(simulation_simpeg, store_jacobian=True)

Now we can start defining the objective function we want to invert. Let’s start by defining the data misfit using the synthetic data, its errors, and the wrapped simulation:

# Define the data misfit
data = dataset.gz.values.ravel()
uncertainties = dataset.gz_std.values.ravel()
data_misfit = ii.DataMisfit(data, uncertainties, simulation)

Now let’s continue with the regularization:

initial_model = np.zeros(mesh.n_cells)
depth_weights = simpeg.utils.depth_weighting(mesh, 0) ** 2
smallness = ii.Smallness(mesh=mesh, cell_weights=depth_weights)
flatness_x, flatness_y, flatness_z = tuple(
    ii.Flatness(mesh=mesh, direction=direction, cell_weights=depth_weights)
    for direction in ("x", "y", "z")
)
alpha_x, alpha_y, alpha_z = tuple(
    ii.utils.estimate_tradeoff_parameter(smallness, flatness, initial_model)
    for flatness in (flatness_x, flatness_y, flatness_z)
)
model_norm = (
    smallness
    + alpha_x * flatness_x
    + alpha_y * flatness_y
    + alpha_z * flatness_z
).flatten()
model_norm
\[\phi_\text{s} (\mathbf{m}) + 6.635 \, \phi_\text{x} (\mathbf{m}) + 6.621 \, \phi_\text{y} (\mathbf{m}) + 20.218 \, \phi_\text{z} (\mathbf{m})\]

Estimate a trade-off parameter between the data_misfit and the model_norm, and use it as an initial value for the hyperparams.CooledMultiplier:

# Define objective function
beta_0 = 4 * ii.utils.estimate_tradeoff_parameter(data_misfit, model_norm, initial_model)
beta = ii.hyperparams.CooledMultiplier(beta_0, cooling_factor=2)
beta
\[\text{CooledMultiplier}(2.585 \cdot 10^{6})\]

And finally, define the objective function:

phi = data_misfit + beta * model_norm
phi
\[\phi_\text{d} (\mathbf{m}) + \text{CooledMultiplier}(2.585 \cdot 10^{6}) \, [\phi_\text{s} (\mathbf{m}) + 6.635 \, \phi_\text{x} (\mathbf{m}) + 6.621 \, \phi_\text{y} (\mathbf{m}) + 20.218 \, \phi_\text{z} (\mathbf{m})]\]

Build the inversion#

Define a stopping condition: stop the inversion if the \(\chi\) factor falls below 1:

stopping_criterion = ii.conditions.ChiTarget(data_misfit, chi_target=1.0)

Create a dynamic Jacobi preconditioner that will get updated throughout the inversion. Since we’ll need the Inversion to pass it to the minimizer, we’ll pass it to it through the minimizer_kwargs:

preconditioner = ii.JacobiPreconditioner(phi)
minimizer_kwargs = {"preconditioner": preconditioner}

Now we can define the inversion with:

  • the objective function we want to invert (phi),

  • the initial model we’ll use to start the inversion process,

  • a minimize.conjugate_gradient as the minimizer,

  • the beta.update method as the only directive,

  • the ChiTarget condition as a stopping criterion,

  • and the preconditioner inside the minimizer_kwargs dictionary.

inversion = ii.Inversion(
    phi,
    initial_model,
    ii.minimize.conjugate_gradient,
    directives=[beta.update],
    stopping_criterion=stopping_criterion,
    minimizer_kwargs=minimizer_kwargs,
)

And run it!

inverted_model = inversion.run()

We can access the inversion.log and convert it to a pandas.DataFrame, and also use it to plot convergence curves:

log = inversion.log.to_pandas()
log
beta phi_d phi_m beta * phi_m phi chi
iter
0 2.585145e+06 1.667546e+06 0.000000 0.000000 1.667546e+06 1735.219131
1 2.585145e+06 2.899614e+04 0.048352 124997.124329 1.539933e+05 30.172888
2 1.292572e+06 1.103149e+04 0.057869 74799.776763 8.583127e+04 11.479182
3 6.462862e+05 4.166759e+03 0.065094 42069.293002 4.623605e+04 4.335858
4 3.231431e+05 1.772969e+03 0.070118 22658.162433 2.443113e+04 1.844921
5 1.615716e+05 9.318052e+02 0.073670 11902.945813 1.283475e+04 0.969620
fig, axes = plt.subplots(nrows=3, ncols=1, sharex=True, figsize=(6, 8))
axes[0].plot(log.index, log.beta, "o-")
axes[0].set_ylabel("Beta")

axes[1].plot(log.index, log.phi_d, "o-")
axes[1].axhline(data_misfit.n_data, linestyle="--", color="grey")
axes[1].set_ylabel("Data misfit")
axes[1].set_yscale("log")

axes[2].plot(log.index, log.phi_m, "o-")
axes[2].set_ylabel("Model norm")
plt.show()
../../_images/gravity-inversion_13_0.png

Finally, let’s plot the model:

(tmp,) = mesh.plot_slice(inverted_model, normal="Y", slice_loc=0)
plt.colorbar(tmp, label="g/cc")
plt.show()
../../_images/gravity-inversion_14_0.png