Sparse 1D Inversion of Sounding Data#

Here we use the module simpeg.electromangetics.static.resistivity to invert DC resistivity sounding data and recover a 1D electrical resistivity model. In this tutorial, we focus on the following:

  • How to define sources and receivers from a survey file

  • How to define the survey

  • 1D inversion of DC resistivity data with iteratively re-weighted least-squares

For this tutorial, we will invert sounding data collected over a layered Earth using a Wenner array. The end product is layered Earth model which explains the data.

Import modules#

import os
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import tarfile

from discretize import TensorMesh

from simpeg import (
    maps,
    data,
    data_misfit,
    regularization,
    optimization,
    inverse_problem,
    inversion,
    directives,
    utils,
)
from simpeg.electromagnetics.static import resistivity as dc
from simpeg.utils import plot_1d_layer_model

mpl.rcParams.update({"font.size": 16})

# sphinx_gallery_thumbnail_number = 2

Define File Names#

Here we provide the file paths to assets we need to run the inversion. The Path to the true model is also provided for comparison with the inversion results. These files are stored as a tar-file on our google cloud bucket: “https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz

# storage bucket where we have the data
data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz"

# download the data
downloaded_data = utils.download(data_source, overwrite=True)

# unzip the tarfile
tar = tarfile.open(downloaded_data, "r")
tar.extractall()
tar.close()

# path to the directory containing our data
dir_path = downloaded_data.split(".")[0] + os.path.sep

# files to work with
data_filename = dir_path + "app_res_1d_data.dobs"
overwriting /home/vsts/work/1/s/tutorials/05-dcr/dcr1d.tar.gz
Downloading https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz
   saved to: /home/vsts/work/1/s/tutorials/05-dcr/dcr1d.tar.gz
Download completed!

Load Data, Define Survey and Plot#

Here we load the observed data, define the DC survey geometry and plot the data values.

# Load data
dobs = np.loadtxt(str(data_filename))

# Extract source and receiver electrode locations and the observed data
A_electrodes = dobs[:, 0:3]
B_electrodes = dobs[:, 3:6]
M_electrodes = dobs[:, 6:9]
N_electrodes = dobs[:, 9:12]
dobs = dobs[:, -1]

# Define survey
unique_tx, k = np.unique(np.c_[A_electrodes, B_electrodes], axis=0, return_index=True)
n_sources = len(k)
k = np.sort(k)
k = np.r_[k, len(k) + 1]

source_list = []
for ii in range(0, n_sources):
    # MN electrode locations for receivers. Each is an (N, 3) numpy array
    M_locations = M_electrodes[k[ii] : k[ii + 1], :]
    N_locations = N_electrodes[k[ii] : k[ii + 1], :]
    receiver_list = [
        dc.receivers.Dipole(
            M_locations,
            N_locations,
            data_type="apparent_resistivity",
        )
    ]

    # AB electrode locations for source. Each is a (1, 3) numpy array
    A_location = A_electrodes[k[ii], :]
    B_location = B_electrodes[k[ii], :]
    source_list.append(dc.sources.Dipole(receiver_list, A_location, B_location))

# Define survey
survey = dc.Survey(source_list)

# Plot apparent resistivities on sounding curve as a function of Wenner separation
# parameter.
electrode_separations = 0.5 * np.sqrt(
    np.sum((survey.locations_a - survey.locations_b) ** 2, axis=1)
)

fig = plt.figure(figsize=(11, 5))
mpl.rcParams.update({"font.size": 14})
ax1 = fig.add_axes([0.15, 0.1, 0.7, 0.85])
ax1.semilogy(electrode_separations, dobs, "b")
ax1.set_xlabel("AB/2 (m)")
ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)")
plt.show()
plot inv 1 dcr sounding irls

Assign Uncertainties#

Inversion with SimPEG requires that we define standard deviation on our data. This represents our estimate of the noise in our data. For DC sounding data, a relative error is applied to each datum. For this tutorial, the relative error on each datum will be 2%.

std = 0.02 * np.abs(dobs)

Define Data#

Here is where we define the data that are inverted. The data are defined by the survey, the observation values and the standard deviation.

data_object = data.Data(survey, dobs=dobs, standard_deviation=std)

Defining a 1D Layered Earth (1D Tensor Mesh)#

Here, we define the layer thicknesses for our 1D simulation. To do this, we use the TensorMesh class.

# Define layer thicknesses
layer_thicknesses = 5 * np.logspace(0, 1, 25)

# Define a mesh for plotting and regularization.
mesh = TensorMesh([(np.r_[layer_thicknesses, layer_thicknesses[-1]])], "0")

print(mesh)
TensorMesh: 26 cells

                    MESH EXTENT             CELL WIDTH      FACTOR
dir    nC        min           max         min       max      max
---   ---  ---------------------------  ------------------  ------
 x     26          0.00        546.90      5.00     50.00    1.10

Define a Starting and Reference Model#

Here, we create starting and/or reference models for the inversion as well as the mapping from the model space to the active cells. Starting and reference models can be a constant background value or contain a-priori structures. Here, the starting model is log(1000) Ohm meters.

Define log-resistivity values for each layer since our model is the log-resistivity. Don’t make the values 0! Otherwise the gradient for the 1st iteration is zero and the inversion will not converge.

# Define model. A resistivity (Ohm meters) or conductivity (S/m) for each layer.
starting_model = np.log(2e2 * np.ones((len(layer_thicknesses) + 1)))

# Define mapping from model to active cells.
model_map = maps.IdentityMap(nP=len(starting_model)) * maps.ExpMap()

Define the Physics#

Here we define the physics of the problem using the Simulation1DLayers class.

simulation = dc.simulation_1d.Simulation1DLayers(
    survey=survey,
    rhoMap=model_map,
    thicknesses=layer_thicknesses,
)

Define Inverse Problem#

The inverse problem is defined by 3 things:

  1. Data Misfit: a measure of how well our recovered model explains the field data

  2. Regularization: constraints placed on the recovered model and a priori information

  3. Optimization: the numerical approach used to solve the inverse problem

# Define the data misfit. Here the data misfit is the L2 norm of the weighted
# residual between the observed data and the data predicted for a given model.
# Within the data misfit, the residual between predicted and observed data are
# normalized by the data's standard deviation.
dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object)

# Define the regularization (model objective function). Here, 'p' defines the
# the norm of the smallness term and 'q' defines the norm of the smoothness
# term.
reg = regularization.Sparse(mesh, mapping=model_map)
reg.reference_model = starting_model
p = 0
q = 0
reg.norms = [p, q]

# Define how the optimization problem is solved. Here we will use an inexact
# Gauss-Newton approach that employs the conjugate gradient solver.
opt = optimization.ProjectedGNCG(maxIter=100, maxIterLS=20, maxIterCG=20, tolCG=1e-3)

# Define the inverse problem
inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt)

Define Inversion Directives#

Here we define any directives that are carried out during the inversion. This includes the cooling schedule for the trade-off parameter (beta), stopping criteria for the inversion and saving inversion results at each iteration.

# Apply and update sensitivity weighting as the model updates
update_sensitivity_weights = directives.UpdateSensitivityWeights()

# Reach target misfit for L2 solution, then use IRLS until model stops changing.
IRLS = directives.UpdateIRLS(max_irls_iterations=40, f_min_change=1e-5)

# Defining a starting value for the trade-off parameter (beta) between the data
# misfit and the regularization.
starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=20)

# Update the preconditionner
update_Jacobi = directives.UpdatePreconditioner()

# Options for outputting recovered models and predicted data for each beta.
save_iteration = directives.SaveOutputEveryIteration(save_txt=False)

# The directives are defined as a list.
directives_list = [
    update_sensitivity_weights,
    IRLS,
    starting_beta,
    update_Jacobi,
    save_iteration,
]

Running the Inversion#

To define the inversion object, we need to define the inversion problem and the set of directives. We can then run the inversion.

# Here we combine the inverse problem and the set of directives
inv = inversion.BaseInversion(inv_prob, directives_list)

# Run the inversion
recovered_model = inv.run(starting_model)
Running inversion with SimPEG v0.23.1.dev1+g60e0c3a5d
/home/vsts/work/1/s/simpeg/simulation.py:197: DefaultSolverWarning:

Using the default solver: Pardiso.

If you would like to suppress this notification, add
warnings.filterwarnings('ignore', simpeg.utils.solver_utils.DefaultSolverWarning)
 to your script.


                        simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv.
                        ***Done using same Solver, and solver_opts as the Simulation1DLayers problem***

model has any nan: 0
=============================== Projected GNCG ===============================
  #     beta     phi_d     phi_m       f      |proj(x-g)-x|  LS    Comment
-----------------------------------------------------------------------------
x0 has any nan: 0
   0  1.42e-03  4.32e+04  6.45e+02  4.32e+04    3.57e+03      0
   1  7.12e-04  1.10e+04  1.27e+05  1.11e+04    6.18e+03      2
   2  3.56e-04  2.60e+03  4.11e+05  2.74e+03    6.06e+03      0
   3  1.78e-04  7.97e+01  3.79e+05  1.47e+02    4.25e+02      0
   4  8.90e-05  3.22e+01  4.90e+05  7.58e+01    1.30e+02      0   Skip BFGS
Reached starting chifact with l2-norm regularization: Start IRLS steps...
irls_threshold 9.901287661981076
   5  8.90e-05  2.13e+01  7.29e+04  2.78e+01    1.94e+02      0   Skip BFGS
   6  1.48e-04  1.14e+01  1.43e+05  3.25e+01    2.49e+02      0   Skip BFGS
   7  2.41e-04  1.19e+01  1.55e+05  4.93e+01    2.64e+02      2   Skip BFGS
   8  3.45e-04  1.61e+01  1.31e+05  6.13e+01    2.69e+02      0   Skip BFGS
   9  5.70e-04  1.15e+01  1.37e+05  8.98e+01    1.68e+02      0   Skip BFGS
  10  8.56e-04  1.46e+01  1.29e+05  1.25e+02    4.67e+01      0   Skip BFGS
  11  1.11e-03  2.00e+01  1.27e+05  1.61e+02    4.09e+01      0   Skip BFGS
  12  1.43e-03  2.44e+01  1.32e+05  2.13e+02    5.43e+01      0   Skip BFGS
  13  8.93e-04  2.99e+01  1.38e+05  1.53e+02    5.07e+01      0
  14  5.59e-04  2.44e+01  1.77e+05  1.23e+02    2.38e+01      0
  15  6.93e-04  2.16e+01  2.21e+05  1.75e+02    5.74e+01      0   Skip BFGS
  16  4.45e-04  2.78e+01  2.28e+05  1.29e+02    4.15e+01      0
  17  2.86e-04  2.27e+01  2.98e+05  1.08e+02    1.93e+01      0
  18  3.68e-04  2.01e+01  3.75e+05  1.58e+02    5.64e+01      0   Skip BFGS
  19  4.73e-04  2.63e+01  3.85e+05  2.08e+02    5.34e+01      0
  20  2.91e-04  3.13e+01  4.11e+05  1.51e+02    5.46e+01      0   Skip BFGS
  21  1.79e-04  2.54e+01  5.27e+05  1.20e+02    3.14e+01      0
  22  2.23e-04  2.14e+01  6.64e+05  1.69e+02    5.57e+01      0   Skip BFGS
  23  2.78e-04  2.60e+01  7.01e+05  2.20e+02    5.44e+01      0
  24  1.73e-04  3.01e+01  7.38e+05  1.58e+02    5.96e+01      0
  25  1.08e-04  2.46e+01  8.78e+05  1.19e+02    4.72e+01      0
  26  1.41e-04  1.97e+01  1.05e+06  1.67e+02    8.36e+01      0
  27  1.74e-04  2.17e+01  1.09e+06  2.10e+02    1.02e+02      0
  28  2.15e-04  2.37e+01  1.08e+06  2.56e+02    1.33e+02      0
  29  2.65e-04  2.59e+01  1.03e+06  2.99e+02    1.62e+02      0
  30  1.69e-04  2.86e+01  9.20e+05  1.84e+02    1.09e+02      0
  31  1.07e-04  2.48e+01  8.40e+05  1.15e+02    6.50e+01      0   Skip BFGS
  32  1.31e-04  2.23e+01  7.50e+05  1.20e+02    7.40e+01      0   Skip BFGS
  33  1.59e-04  2.32e+01  6.07e+05  1.20e+02    8.10e+01      0
  34  1.93e-04  2.40e+01  4.86e+05  1.18e+02    8.38e+01      0   Skip BFGS
  35  2.36e-04  2.43e+01  3.91e+05  1.16e+02    8.20e+01      0   Skip BFGS
  36  2.87e-04  2.43e+01  3.19e+05  1.16e+02    7.90e+01      0   Skip BFGS
  37  3.49e-04  2.41e+01  2.62e+05  1.16e+02    7.58e+01      0   Skip BFGS
  38  4.25e-04  2.39e+01  2.17e+05  1.16e+02    7.24e+01      0   Skip BFGS
  39  5.17e-04  2.38e+01  1.80e+05  1.17e+02    6.86e+01      0   Skip BFGS
  40  6.29e-04  2.36e+01  1.50e+05  1.18e+02    6.61e+01      0   Skip BFGS
  41  7.66e-04  2.34e+01  1.25e+05  1.19e+02    6.37e+01      0   Skip BFGS
  42  9.32e-04  2.33e+01  1.04e+05  1.20e+02    6.31e+01      0   Skip BFGS
  43  1.13e-03  2.32e+01  8.64e+04  1.21e+02    6.30e+01      0   Skip BFGS
  44  1.38e-03  2.32e+01  7.19e+04  1.22e+02    6.55e+01      0   Skip BFGS
Reach maximum number of IRLS cycles: 40
------------------------- STOP! -------------------------
1 : |fc-fOld| = 0.0000e+00 <= tolF*(1+|f0|) = 4.3194e+03
1 : |xc-x_last| = 3.9892e-02 <= tolX*(1+|x0|) = 2.8016e+00
0 : |proj(x-g)-x|    = 6.5510e+01 <= tolG          = 1.0000e-01
0 : |proj(x-g)-x|    = 6.5510e+01 <= 1e3*eps       = 1.0000e-02
0 : maxIter   =     100    <= iter          =     45
------------------------- DONE! -------------------------

Examining the Results#

# Define true model and layer thicknesses
true_model = np.r_[1e3, 4e3, 2e2]
true_layers = np.r_[100.0, 100.0]

# Extract Least-Squares model
l2_model = inv_prob.l2model

# Plot true model and recovered model
fig = plt.figure(figsize=(6, 4))
x_min = np.min(np.r_[model_map * recovered_model, model_map * l2_model, true_model])
x_max = np.max(np.r_[model_map * recovered_model, model_map * l2_model, true_model])

ax1 = fig.add_axes([0.2, 0.15, 0.7, 0.7])
plot_1d_layer_model(true_layers, true_model, ax=ax1, color="k")
plot_1d_layer_model(layer_thicknesses, model_map * l2_model, ax=ax1, color="b")
plot_1d_layer_model(layer_thicknesses, model_map * recovered_model, ax=ax1, color="r")
ax1.set_xlabel(r"Resistivity ($\Omega m$)")
ax1.set_xlim(0.9 * x_min, 1.1 * x_max)
ax1.legend(["True Model", "L2-Model", "Sparse Model"])

# Plot the true and apparent resistivities on a sounding curve
fig = plt.figure(figsize=(11, 5))
ax1 = fig.add_axes([0.2, 0.1, 0.6, 0.8])
ax1.semilogy(electrode_separations, dobs, "k")
ax1.semilogy(electrode_separations, simulation.dpred(l2_model), "b")
ax1.semilogy(electrode_separations, simulation.dpred(recovered_model), "r")
ax1.set_xlabel("AB/2 (m)")
ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)")
ax1.legend(["True Sounding Curve", "Predicted (L2-Model)", "Predicted (Sparse)"])
plt.show()
  • plot inv 1 dcr sounding irls
  • plot inv 1 dcr sounding irls
/home/vsts/work/1/s/simpeg/utils/plot_utils.py:354: UserWarning:

Attempt to set non-positive xlim on a log-scaled axis will be ignored.

/home/vsts/work/1/s/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py:315: UserWarning:

Attempt to set non-positive xlim on a log-scaled axis will be ignored.

Total running time of the script: (0 minutes 33.551 seconds)

Estimated memory usage: 288 MB

Gallery generated by Sphinx-Gallery