Keywords: DC resistivity, 1D sounding, inversion, parametric, sparse norm, apparent resistivity, wires mapping.
Summary: Here we invert DC resistivity data for a single Wenner sounding. We demonstrate 3 approaches that can be used to invert 1D data in SimPEG:
- A weighted least-squares inversion where the number of layers and their thicknesses are fixed.
- An iteratively reweighted least-squares (IRLS) inversion to recover sparse and/or blocky structures.
- A parametric inversion where we invert for the layer thicknesses and electrical properties assuming a 3-layered Earth.
The weighted least-squares approach is a great introduction to geophysical inversion with SimPEG. One drawback however, is that it recovers smooth structures which may not be representative of the true model. To recover sparse and/or blocky 1D structures, we demonstrate an iteratively reweighted least-squares approach. If the number of layers is known, but their depths, thicknesses and conductivities are not, we can use a parametric inversion approach.
Learning Objectives: Because this tutorial focusses primarily on inversion-related functionality, we urge the reader to become familiar with functionality explained in the 1D Forward Simulation of DC Resistivity Data tutorial before working through this one. For this tutorial, we focus on:
- General approaches for 1D inversion with SimPEG.
- How to assign appropriate uncertainties to apparent resistivity or normalized voltage data.
- Choosing suitable parameters for the inversion.
- Specifying directives that are applied throughout the inversion.
- Weighted least-squares, sparse-norm and parametric inversion.
- Analyzing inversion outputs.
Import Modules¶
Here, we import all of the functionality required to run the notebook for the tutorial exercise.
All of the functionality specific to the forward simulation of 1D DC resistivity data are imported from the simpeg
# SimPEG functionality
from simpeg.electromagnetics.static import resistivity as dc
from simpeg.utils import plot_1d_layer_model, download
from simpeg import (
maps,
data,
data_misfit,
regularization,
optimization,
inverse_problem,
inversion,
directives,
)
# discretize functionality
from discretize import TensorMesh
# Basic Python functionality
import os
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import tarfile
mpl.rcParams.update({"font.size": 14})
Download Tutorial Files¶
For this tutorial, the observed data for the Wenner sounding are stored within a tar-file. Here, we download and extract the data file.
# URL to download from repository assets
data_source = "https://github.com/simpeg/user-tutorials/raw/main/assets/05-dcr/inv_dcr_1d_files.tar.gz"
# download the data
downloaded_data = 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/ssoler/git/user-tutorials/notebooks/05-dcr/inv_dcr_1d_files.tar.gz
Downloading https://github.com/simpeg/user-tutorials/raw/main/assets/05-dcr/inv_dcr_1d_files.tar.gz
saved to: /home/ssoler/git/user-tutorials/notebooks/05-dcr/inv_dcr_1d_files.tar.gz
Download completed!
Load and Plot the Data¶
Here, we load and parse the data file containing observed data and survey geometry. The data file contains the xyz locations for the A, B, M and N electrodes, in order, as well as the observed data. We then compute the AB/2 (half separation distance) and plot the Wenner sounding data.
# Load data
dobs = np.loadtxt(str(data_filename))
# Extract A, B, M and N 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]
# Compute the AB/2 separation for the 1D Wenner array
AB_separations = np.sqrt(np.sum((A_electrodes - B_electrodes) ** 2, axis=1))
# Plot apparent resistivity as a function of AB/2
fig = plt.figure(figsize=(8, 4))
mpl.rcParams.update({"font.size": 14})
ax1 = fig.add_axes([0.15, 0.1, 0.7, 0.85])
ax1.semilogy(AB_separations / 2, dobs, "b-o")
ax1.grid(which="both")
ax1.set_xlabel("AB/2 (m)")
ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)")
plt.show()
Assign Uncertainties¶
Inversion with SimPEG requires that we define the uncertainties on our data; that is, an estimate of the standard deviation of the noise on our data assuming it is uncorrelated Gaussian with zero mean. An online resource explaining uncertainties and their role in the inversion can be found here.
For 1D apparent resistivity data, we generally apply a percent uncertainty to all data. A percent uncertainty will fit conductive and resistive structures equally; unlike a floor uncertainty which will prioritize fitting more resistive structures. For this tutorial, we apply a 2% uncertainty to all data. Depending on the quality of the data, we may choose a percent uncertainty between 2% - 10%.
For 1D normalized voltage data, we also tend to apply a percent uncertainty to all data. Differences in electrode spacing and subsurface conductivity result in measured voltages that span many orders of magnitude. A percent uncertainty ensures all data are fit equally. Like apparent resistivity, a percent uncertainty of 2% - 10% is a good initial guess.
NOTE: if the data contain very small values that you feel could be erroneous, a small floor value can be added to ensure stability of the inversion.
# Add 2.5% uncertainties to all data.
uncertainties = 0.025 * np.abs(dobs)
Define the Survey¶
Here, we define the survey geometry. Although the survey consists of a single 1D Wenner sounding, the approach is generalized for 2D and 3D surveys where the electrode locations may not be sorted. For a comprehensive description of constructing DC resistivity surveys in SimPEG, see the 1D Forward Simulation of DC Resistivity Data tutorial.
# Sort by unique current electrode locations
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]
# Define empty list for sources to live
source_list = []
# Loop over all sources
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)
Define the Data¶
The SimPEG Data class is required for inversion and connects the observed data, uncertainties and survey geometry.
data_object = data.Data(survey, dobs=dobs, standard_deviation=uncertainties)
Weighted Least-Squares Inversion¶
Here, we use the weighted least-squares inversion approach to recover the log-conductivities on a 1D layered Earth. We impose no a-priori information about the number of layers (geological units) or their thicknesses. Instead, we define a large number of layers with exponentially increasing thicknesses. And the depth, thickness and electrical properties of the Earth are inferred from the recovered model.
Design a 1D Layered Earth¶
When the spacings between the electrodes are small, we are obtaining higher resolution information about the conductivity near the Earth’s surface. As a result, we define much thinner layers near the Earth’s surface. When the spacings between electrodes are large, we are obtaining lower resolution information about the conductivity at depth. So at depth, we can define layers that are much thicker. The thickness defining the top layer is ultimately determined by the minimum electrode spacing. And the thickness and depth to the lower layer is determined by the largest electrode spacing. The rate at which the layer thicknesses increase with depth is determined by the user.
For Wenner soundings: For the 1D Wenner array, the layer thicknesses and largest depth are determined by the AB/2 spacing. A reasonable minimum layer thickness is 10% the minimum AB/2 spacing. And the depth to the bottom layer should be roughly equal to the largest AB/2 spacing.
For dipole-dipole, pole-dipole, etc...: For these surveys, the source electrodes AB and receiver electrodes MN are offset from one another. For a particular set of electrodes ABMN, we consider the distance between the average source location and average receiver location. For a dipole-dipole survey, this would be:
For a pole-dipole survey, this would be:
Pole sources penetrate the Earth more deeply and produce smoother fields within the source region, so we can get away with a larger layer thickness for the top layer but we need to discretize deeper. But in general, it is easy to make your your top layer equal to 10% of your smallest distance. And to make the depth to the lower layer equal to the largest distance.
# Use Wenner electrode spacings to set discretization parameters
print("AB separations: {}".format(AB_separations))
depth_min = 5.0 # top layer thickness
depth_max = np.max(AB_separations / 2) # depth to lowest layer
geometric_factor = 1.1 # rate of thickness increase
AB separations: [ 60. 120. 180. 240. 300. 360. 420. 480. 540. 600. 660. 720.
780. 840. 900. 960. 1020. 1080. 1140. 1200. 1260. 1320. 1380. 1440.
1500.]
# Increase subsequent layer thicknesses by the geometric factors until
# it reaches the maximum layer depth.
layer_thicknesses = [depth_min]
while np.sum(layer_thicknesses) < depth_max:
layer_thicknesses.append(geometric_factor * layer_thicknesses[-1])
n_layers = len(layer_thicknesses) + 1 # Number of layers
Define a Model and Mapping¶
Recall from the Forward Simulation of 1D Sounding DC Resistivity Data tutorial that the ‘model’ is not necessarily synonymous with physical property values. And that we need to define a mapping from the model to the set of input parameters required by the forward simulation. When inverting to recover electrical resistivities (or conductivities), it is best to use the log-value, as electrical resistivities (or conductivities) of rocks span many order of magnitude.
Here, the model defines the log-resistivity values for a defined set of subsurface layers. And we use the simpeg.maps.ExpMap to map from the model parameters to the resistivity values required by the forward simulation.
log_resistivity_map = maps.ExpMap(nP=n_layers)
Starting/Reference Models¶
The starting model defines a reasonable starting point for the inversion. Because electromagnetic problems are non-linear, your choice in starting model does have an impact on the recovered model. For DC resistivity inversion, we generally choose our starting model based on apparent resistivities. For the tutorial example, the apparent resistivities were near 1000 . It should be noted that the starting model cannot be vector of zeros, otherwise the inversion will be unable to compute a gradient direction at the first iteration.
The reference model is used to include a-priori information. The impact of the reference model on the inversion will be discussed in another tutorial. The reference model for basic inversion approaches is either zero or equal to the starting model.
Notice that the length of the starting and reference models is equal to the number of model parameters!!!
# Starting model is log-resistivity values (Ohmm)
starting_resistivity_model = np.log(1e3 * np.ones(n_layers))
# Reference model is also log-resistivity values (Ohmm)
reference_resistivity_model = starting_resistivity_model.copy()
Define the Forward Simulation¶
A simulation object defining the forward problem is required in order to predict data and calculate misfits for recovered models. A comprehensive description of the simulation object for 1D DC resistivity was discussed in the 1D Forward Simulation of DC Resistivity Data for a Single Sounding tutorial. Here, we use the Simulation1DLayers which simulates the data according to a 1D Hankel transform solution.
The layer thicknesses are a static property of the simulation, and we set them using the thicknessess
keyword argument. Since our model consists of log-resistivities, we use rhoMap
to set the mapping from the model to the layer resistivities.
simulation_L2 = dc.simulation_1d.Simulation1DLayers(
survey=survey,
rhoMap=log_resistivity_map,
thicknesses=layer_thicknesses,
)
Define the Data Misfit¶
To understand the role of the data misfit in the inversion, please visit this online resource. Here, we use the L2DataMisfit class to define the data misfit. In this case, the data misfit is the L2 norm of the weighted residual between the observed data and the data predicted for a given model. When instantiating the data misfit object within SimPEG, we must assign an appropriate data object and simulation object as properties.
dmis_L2 = data_misfit.L2DataMisfit(simulation=simulation_L2, data=data_object)
Define the Regularization¶
To understand the role of the regularization in the inversion, please visit this online resource.
To define the regularization within SimPEG, we must define a 1D tensor mesh. Meshes are designed using the discretize package. Whereas layer thicknesses and our model are defined from our top-layer down, tensor meshes are defined from the bottom up. So to define a 1D tensor mesh for the regularization, we:
- add an extra layer to the end of our thicknesses so that the number of cells in the 1D mesh equals the number of model parameters
- reverse the order so that the model parameters in the regularization match up with the appropriate cell
- define the tensor mesh from the cell widths
# Define 1D cell widths
h = np.r_[layer_thicknesses, layer_thicknesses[-1]]
h = np.flipud(h)
# Create regularization mesh
regularization_mesh = TensorMesh([h], "N")
print(regularization_mesh)
TensorMesh: 31 cells
MESH EXTENT CELL WIDTH FACTOR
dir nC min max min max max
--- --- --------------------------- ------------------ ------
x 31 -901.79 -0.00 5.00 79.32 1.10
By default, the regularization acts on the model parameters. In this case, the model parameters are the log-resistivities, not the electric resistivities!!! Here, we use the WeightedLeastSquares regularization class to constrain the inversion result. Here, length scale along x are used to balance the smallness and smoothness terms; yes, x is smoothness along the vertical direction. And the reference model is only applied to the smallness term. If we wanted to apply the regularization to a function of the model parameters, we would need to set an approprate mapping object using the mapping
keyword argument.
reg_L2 = regularization.WeightedLeastSquares(
regularization_mesh,
length_scale_x=1.0,
reference_model=reference_resistivity_model,
reference_model_in_smooth=False,
)
Define the Optimization Algorithm¶
Here, we use the InexactGaussNewton class to solve the optimization problem using the inexact Gauss Newton with conjugate gradient solver. Reasonable default values have generally been set for the properties of each optimization class. However, the user may choose to set custom values; e.g. the accuracy tolerance for the conjugate gradient solver or the number of line searches.
opt_L2 = optimization.InexactGaussNewton(
maxIter=100, maxIterLS=20, maxIterCG=20, tolCG=1e-3
)
Define the Inverse Problem¶
We use the BaseInvProblem class to fully define the inverse problem that is solved at each beta (trade-off parameter) iteration. The inverse problem requires appropriate data misfit, regularization and optimization objects.
inv_prob_L2 = inverse_problem.BaseInvProblem(dmis_L2, reg_L2, opt_L2)
Provide Inversion Directives¶
Directives represent operations that are carried out during the inversion. Here, we apply common directives for weighted least-squares inversion of DC resistivity data and describe their roles. These are:
UpdateSensitivityWeights: Apply sensitivity weighting to counteract the natural tendency of DC resistivity inversion to place materials near the electrodes. Since the problem is non-linear and the sensitivities are updated with every model, we set
every_iteration=True
.UpdatePreconditioner: Apply Jacobi preconditioner when solving optimization problem to reduce the number of conjugate gradient iterations. We set
update_every_iteration=True
because the ideal preconditioner is model-dependent.BetaEstimate_ByEig: Compute and set starting trade-off parameter (beta) based on largest eigenvalues.
BetaSchedule: Size reduction of the trade-off parameter at every beta iteration, and the number of Gauss-Newton iterations for each beta. In general, a
coolingFactor
between 1.5 and 2.5, andcoolingRate
of 2 or 3 works well for DC resistivity inversion. Cooling beta too quickly will result in portions of the model getting trapped in local minima. And we will not be finding the solution that minimizes the optimization problem if the cooling rate is too small.TargetMisfit: Terminates the inversion when the data misfit equals the target misfit. A
chifact=1
terminates the inversion when the data misfit equals the number of data.
The directive objects are organized in a list
. Upon starting the inversion or updating the recovered model at each iteration, the inversion will call each directive within the list in order. The order of the directives matters, and SimPEG will throw an error if directives are organized into an improper order. Some directives, like the BetaEstimate_ByEig
are only used when starting the inversion. Other directives, like UpdatePreconditionner
, are used whenever the model is updated.
sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=True)
update_jacobi = directives.UpdatePreconditioner(update_every_iteration=True)
starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=5)
beta_schedule = directives.BetaSchedule(coolingFactor=2.0, coolingRate=2)
target_misfit = directives.TargetMisfit(chifact=1.0)
directives_list_L2 = [
sensitivity_weights,
update_jacobi,
starting_beta,
beta_schedule,
target_misfit,
]
Define and Run the Inversion¶
We define the inversion using the BaseInversion class. The inversion class must be instantiated with an appropriate inverse problem object and directives list. The run
method, along with a starting model, is respondible for running the inversion. The output is a 1D numpy.ndarray containing the recovered model parameters
# Here we combine the inverse problem and the set of directives
inv_L2 = inversion.BaseInversion(inv_prob_L2, directives_list_L2)
# Run the inversion
recovered_model_L2 = inv_L2.run(starting_resistivity_model)
Running inversion with SimPEG v0.23.0
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
============================ Inexact Gauss Newton ============================
# beta phi_d phi_m f |proj(x-g)-x| LS Comment
-----------------------------------------------------------------------------
x0 has any nan: 0
/t40array/ssoler/miniforge3/envs/simpeg-user-tutorials/lib/python3.10/site-packages/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.
return get_default_solver(warn=True)
0 4.30e+02 1.42e+03 0.00e+00 1.42e+03 1.88e+03 0
1 4.30e+02 1.07e+03 3.57e-01 1.22e+03 4.74e+01 0
2 2.15e+02 1.06e+03 3.73e-01 1.14e+03 7.50e+02 0 Skip BFGS
3 2.15e+02 8.74e+02 1.00e+00 1.09e+03 2.34e+01 0
4 1.07e+02 8.70e+02 1.02e+00 9.80e+02 6.25e+02 0 Skip BFGS
5 1.07e+02 6.78e+02 2.33e+00 9.29e+02 1.91e+01 0
6 5.37e+01 6.75e+02 2.36e+00 8.02e+02 4.79e+02 0 Skip BFGS
7 5.37e+01 5.03e+02 4.79e+00 7.60e+02 1.87e+01 0
8 2.69e+01 5.05e+02 4.76e+00 6.32e+02 3.44e+02 0
9 2.69e+01 3.56e+02 9.18e+00 6.03e+02 2.50e+01 0
10 1.34e+01 3.64e+02 8.85e+00 4.83e+02 2.38e+02 0
11 1.34e+01 2.41e+02 1.66e+01 4.64e+02 3.32e+01 0
12 6.71e+00 2.54e+02 1.55e+01 3.58e+02 1.62e+02 0
13 6.71e+00 1.59e+02 2.84e+01 3.49e+02 4.06e+01 0
14 3.36e+00 1.73e+02 2.57e+01 2.59e+02 1.09e+02 0
15 3.36e+00 1.04e+02 4.56e+01 2.57e+02 4.64e+01 0
16 1.68e+00 1.17e+02 4.02e+01 1.85e+02 7.27e+01 0
17 1.68e+00 7.04e+01 6.94e+01 1.87e+02 5.07e+01 0
18 8.39e-01 8.10e+01 6.00e+01 1.31e+02 4.72e+01 0
19 8.39e-01 5.04e+01 1.01e+02 1.35e+02 5.38e+01 0
20 4.20e-01 5.81e+01 8.64e+01 9.44e+01 2.95e+01 0
21 4.20e-01 3.85e+01 1.34e+02 9.46e+01 5.26e+01 0
22 2.10e-01 4.21e+01 1.24e+02 6.82e+01 1.63e+01 0
23 2.10e-01 3.14e+01 1.52e+02 6.32e+01 3.91e+01 0
24 1.05e-01 3.12e+01 1.50e+02 4.69e+01 9.05e+00 0
25 1.05e-01 2.64e+01 1.73e+02 4.45e+01 2.93e+01 0
26 5.24e-02 2.62e+01 1.72e+02 3.52e+01 4.18e+00 0
------------------------- STOP! -------------------------
1 : |fc-fOld| = 0.0000e+00 <= tolF*(1+|f0|) = 1.4241e+02
1 : |xc-x_last| = 4.8633e-01 <= tolX*(1+|x0|) = 3.9461e+00
0 : |proj(x-g)-x| = 4.1800e+00 <= tolG = 1.0000e-01
0 : |proj(x-g)-x| = 4.1800e+00 <= 1e3*eps = 1.0000e-02
0 : maxIter = 100 <= iter = 27
------------------------- DONE! -------------------------
Plot Observed and Predicted Data¶
# 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(AB_separations / 2, dobs, "k")
ax1.semilogy(AB_separations / 2, simulation_L2.dpred(recovered_model_L2), "b")
ax1.grid(which="both")
ax1.set_xlabel("AB/2 (m)")
ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)")
ax1.legend(["Observed", "L2 Inversion"])
plt.show()
Plot the Recovered Model¶
# Define true model and layer thicknesses
true_resistivities = np.r_[1e3, 4e3, 2e2]
true_layers = np.r_[100.0, 100.0]
# Plot true model and recovered model
fig = plt.figure(figsize=(6, 6))
ax1 = fig.add_axes([0.2, 0.15, 0.7, 0.7])
plot_1d_layer_model(true_layers, true_resistivities, ax=ax1, color="k")
plot_1d_layer_model(
layer_thicknesses, log_resistivity_map * recovered_model_L2, ax=ax1, color="b"
)
x_min, x_max = true_resistivities.min(), true_resistivities.max()
ax1.set_xlim(0.9 * x_min, 1.1 * x_max)
ax1.grid()
ax1.set_xlabel(r"Resistivity ($\Omega m$)")
ax1.legend(["True Model", "L2-Model"])
plt.show()
Iteratively Re-weighted Least-Squares Inversion¶
Here, we use the iteratively reweighted least-squares (IRLS) inversion approach to recover sparse and/or blocky models on the set layers.
Define the Forward Simulation¶
simulation_irls = dc.simulation_1d.Simulation1DLayers(
survey=survey,
rhoMap=log_resistivity_map,
thicknesses=layer_thicknesses,
)
Define the Data Misfit¶
dmis_irls = data_misfit.L2DataMisfit(simulation=simulation_irls, data=data_object)
Define the Regularization¶
Here, we use the Sparse regularization class to constrain the inversion result using an IRLS approach. Here, the scaling constants that balance the smallness and smoothness terms are set directly. Equal emphasis on smallness and smoothness is generally applied by using the inverse square of the smallest cell dimension. The reference model is only applied to the smallness term; which is redundant for the tutorial example since we have set the reference model to an array of zeros. Here, we apply a 1-norm to the smallness term and a 1-norm to first-order smoothness along the x (vertical direction).
reg_irls = regularization.Sparse(
regularization_mesh,
alpha_s=0.1,
alpha_x=1,
reference_model_in_smooth=False,
norms=[1.0, 1.0],
)
Define the Optimization Algorithm¶
opt_irls = optimization.InexactGaussNewton(
maxIter=100, maxIterLS=20, maxIterCG=30, tolCG=1e-3
)
Define the Inverse Problem¶
inv_prob_irls = inverse_problem.BaseInvProblem(dmis_irls, reg_irls, opt_irls)
Provide Inversion Directives¶
Here, we create common directives for IRLS inversion of DC resistivity data and describe their roles. In additon to the UpdateSensitivityWeights, UpdatePreconditioner and BetaEstimate_ByEig directives (described before), inversion with sparse-norms requires the UpdateIRLS
directive.
You will notice that we don’t use the BetaSchedule and TargetMisfit directives. Here, the beta cooling schedule is set in the UpdateIRLS
directive using the coolingFactor
and coolingRate
properties. The target misfit for the L2 portion of the IRLS approach is set with the chifact_start
property.
sensitivity_weights_irls = directives.UpdateSensitivityWeights(every_iteration=True)
starting_beta_irls = directives.BetaEstimate_ByEig(beta0_ratio=1)
update_jacobi_irls = directives.UpdatePreconditioner(update_every_iteration=True)
update_irls = directives.UpdateIRLS(
cooling_factor=2,
cooling_rate=2,
f_min_change=1e-4,
max_irls_iterations=30,
chifact_start=1.0,
)
directives_list_irls = [
update_irls,
sensitivity_weights_irls,
starting_beta_irls,
update_jacobi_irls,
]
Define and Run the Inversion¶
# Here we combine the inverse problem and the set of directives
inv_irls = inversion.BaseInversion(inv_prob_irls, directives_list_irls)
# Run the inversion
recovered_model_irls = inv_irls.run(starting_resistivity_model)
Running inversion with SimPEG v0.23.0
simpeg.InvProblem will set Regularization.reference_model to m0.
simpeg.InvProblem will set Regularization.reference_model to m0.
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
============================ Inexact Gauss Newton ============================
# beta phi_d phi_m f |proj(x-g)-x| LS Comment
-----------------------------------------------------------------------------
x0 has any nan: 0
0 8.47e+02 1.42e+03 0.00e+00 1.42e+03 1.88e+03 0
1 8.47e+02 6.18e+02 2.98e-01 8.70e+02 4.38e+01 0
2 4.24e+02 6.13e+02 3.04e-01 7.42e+02 4.29e+02 0 Skip BFGS
3 4.24e+02 4.49e+02 6.03e-01 7.04e+02 2.02e+01 0
4 2.12e+02 4.53e+02 5.93e-01 5.78e+02 3.04e+02 0
5 2.12e+02 3.13e+02 1.13e+00 5.53e+02 2.79e+01 0
6 1.06e+02 3.23e+02 1.08e+00 4.37e+02 2.09e+02 0
7 1.06e+02 2.09e+02 2.01e+00 4.22e+02 3.61e+01 0
8 5.30e+01 2.23e+02 1.85e+00 3.21e+02 1.42e+02 0
9 5.30e+01 1.37e+02 3.36e+00 3.15e+02 4.29e+01 0
10 2.65e+01 1.51e+02 3.01e+00 2.31e+02 9.52e+01 0
11 2.65e+01 9.05e+01 5.30e+00 2.31e+02 4.81e+01 0
12 1.32e+01 1.03e+02 4.63e+00 1.64e+02 6.29e+01 0
13 1.32e+01 6.23e+01 7.93e+00 1.67e+02 5.20e+01 0
14 6.62e+00 7.19e+01 6.82e+00 1.17e+02 4.04e+01 0
15 6.62e+00 4.56e+01 1.14e+01 1.21e+02 5.45e+01 0
16 3.31e+00 5.24e+01 9.73e+00 8.46e+01 2.48e+01 0
17 3.31e+00 3.57e+01 1.40e+01 8.20e+01 5.08e+01 0
18 1.66e+00 3.74e+01 1.33e+01 5.94e+01 1.35e+01 0
19 1.66e+00 2.92e+01 1.59e+01 5.55e+01 3.61e+01 0
20 8.28e-01 2.90e+01 1.58e+01 4.20e+01 6.96e+00 0
21 8.28e-01 2.54e+01 1.80e+01 4.02e+01 2.68e+01 0
22 4.14e-01 2.53e+01 1.78e+01 3.27e+01 3.09e+00 0
Reached starting chifact with l2-norm regularization: Start IRLS steps...
irls_threshold 2.266062153902932
23 4.14e-01 2.38e+01 2.66e+01 3.48e+01 2.65e+01 0
24 4.14e-01 2.47e+01 2.45e+01 3.49e+01 3.66e+01 0
25 4.14e-01 2.43e+01 2.98e+01 3.66e+01 4.05e+00 0
26 4.14e-01 2.48e+01 2.84e+01 3.66e+01 1.18e+01 0
27 4.14e-01 2.47e+01 3.42e+01 3.89e+01 2.12e+00 0
28 4.14e-01 2.53e+01 3.25e+01 3.88e+01 9.72e+00 0
29 4.14e-01 2.52e+01 3.96e+01 4.16e+01 2.36e+00 0
30 4.14e-01 2.60e+01 3.74e+01 4.15e+01 1.01e+01 0
31 4.14e-01 2.59e+01 4.60e+01 4.49e+01 2.86e+00 0
32 4.14e-01 2.69e+01 4.33e+01 4.48e+01 1.03e+01 0
33 4.14e-01 2.67e+01 5.34e+01 4.88e+01 3.44e+00 0
34 2.66e-01 2.79e+01 5.03e+01 4.13e+01 6.33e+00 0
35 2.66e-01 2.57e+01 6.87e+01 4.40e+01 1.22e+01 0
36 1.70e-01 2.68e+01 6.42e+01 3.77e+01 7.88e+00 0
37 1.70e-01 2.51e+01 8.63e+01 3.98e+01 1.10e+01 0
38 1.09e-01 2.58e+01 8.15e+01 3.47e+01 6.97e+00 0
39 1.09e-01 2.46e+01 1.04e+02 3.60e+01 9.16e+00 0
40 7.02e-02 2.50e+01 9.86e+01 3.19e+01 5.85e+00 0
41 7.02e-02 2.41e+01 1.17e+02 3.23e+01 7.82e+00 0
42 4.51e-02 2.43e+01 1.12e+02 2.93e+01 5.27e+00 0
43 4.51e-02 2.37e+01 1.20e+02 2.91e+01 7.27e+00 0
44 2.89e-02 2.37e+01 1.17e+02 2.71e+01 4.60e+00 0
45 2.89e-02 2.34e+01 9.78e+01 2.62e+01 1.31e+01 0
46 1.86e-02 2.33e+01 9.99e+01 2.52e+01 1.56e+01 1 Skip BFGS
47 1.86e-02 2.33e+01 7.65e+01 2.47e+01 1.61e+01 3 Skip BFGS
48 1.19e-02 2.32e+01 7.84e+01 2.42e+01 1.98e+01 3 Skip BFGS
49 1.19e-02 2.32e+01 6.78e+01 2.40e+01 2.14e+01 4 Skip BFGS
50 7.65e-03 2.32e+01 6.92e+01 2.37e+01 2.33e+01 4 Skip BFGS
51 7.65e-03 2.31e+01 6.33e+01 2.36e+01 2.66e+01 4 Skip BFGS
52 4.91e-03 2.30e+01 6.52e+01 2.34e+01 2.95e+01 4 Skip BFGS
53 4.91e-03 2.30e+01 5.96e+01 2.33e+01 3.30e+01 4 Skip BFGS
54 3.15e-03 2.29e+01 6.20e+01 2.31e+01 3.62e+01 4 Skip BFGS
55 3.15e-03 2.29e+01 5.63e+01 2.31e+01 4.03e+01 4 Skip BFGS
56 2.02e-03 2.28e+01 5.97e+01 2.30e+01 4.52e+01 4 Skip BFGS
57 2.02e-03 2.28e+01 5.43e+01 2.29e+01 4.64e+01 5 Skip BFGS
58 1.30e-03 2.28e+01 5.65e+01 2.28e+01 4.81e+01 5 Skip BFGS
59 1.30e-03 2.28e+01 5.29e+01 2.28e+01 5.13e+01 5 Skip BFGS
60 8.33e-04 2.27e+01 5.43e+01 2.28e+01 5.18e+01 6 Skip BFGS
61 8.33e-04 2.27e+01 5.20e+01 2.27e+01 5.23e+01 6 Skip BFGS
62 5.35e-04 2.27e+01 5.37e+01 2.27e+01 5.28e+01 6 Skip BFGS
63 5.35e-04 2.27e+01 5.04e+01 2.27e+01 5.38e+01 6 Skip BFGS
64 3.43e-04 2.27e+01 5.25e+01 2.27e+01 5.51e+01 6 Skip BFGS
65 3.43e-04 2.26e+01 5.26e+01 2.27e+01 5.54e+01 7 Skip BFGS
66 2.20e-04 2.26e+01 5.38e+01 2.26e+01 5.59e+01 7 Skip BFGS
67 2.20e-04 2.26e+01 5.51e+01 2.26e+01 5.67e+01 7 Skip BFGS
68 1.41e-04 2.26e+01 5.58e+01 2.26e+01 5.69e+01 8 Skip BFGS
69 1.41e-04 2.26e+01 5.68e+01 2.26e+01 5.72e+01 8 Skip BFGS
70 9.08e-05 2.26e+01 5.75e+01 2.26e+01 5.75e+01 8 Skip BFGS
71 9.08e-05 2.26e+01 5.88e+01 2.26e+01 5.79e+01 8 Skip BFGS
72 5.82e-05 2.26e+01 5.97e+01 2.26e+01 5.85e+01 8 Skip BFGS
73 5.82e-05 2.26e+01 6.13e+01 2.26e+01 5.91e+01 8 Skip BFGS
74 3.74e-05 2.26e+01 6.18e+01 2.26e+01 5.92e+01 9 Skip BFGS
75 3.74e-05 2.26e+01 6.27e+01 2.26e+01 5.94e+01 9 Skip BFGS
76 2.40e-05 2.26e+01 6.32e+01 2.26e+01 5.96e+01 9 Skip BFGS
77 2.40e-05 2.26e+01 6.44e+01 2.26e+01 5.99e+01 9 Skip BFGS
78 1.54e-05 2.26e+01 6.50e+01 2.26e+01 6.02e+01 9 Skip BFGS
79 1.54e-05 2.26e+01 6.85e+01 2.26e+01 6.05e+01 9 Skip BFGS
80 9.89e-06 2.26e+01 6.90e+01 2.26e+01 6.09e+01 9 Skip BFGS
81 9.89e-06 2.26e+01 7.20e+01 2.26e+01 6.10e+01 10 Skip BFGS
82 6.35e-06 2.26e+01 7.23e+01 2.26e+01 6.11e+01 10 Skip BFGS
Reach maximum number of IRLS cycles: 30
------------------------- STOP! -------------------------
1 : |fc-fOld| = 0.0000e+00 <= tolF*(1+|f0|) = 1.4241e+02
1 : |xc-x_last| = 6.1065e-02 <= tolX*(1+|x0|) = 3.9461e+00
0 : |proj(x-g)-x| = 6.1085e+01 <= tolG = 1.0000e-01
0 : |proj(x-g)-x| = 6.1085e+01 <= 1e3*eps = 1.0000e-02
0 : maxIter = 100 <= iter = 83
------------------------- DONE! -------------------------
Plot Observed and Predicted Data¶
# 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(AB_separations / 2, dobs, "k")
ax1.semilogy(AB_separations / 2, simulation_L2.dpred(recovered_model_L2), "b")
ax1.semilogy(AB_separations / 2, simulation_irls.dpred(recovered_model_irls), "r")
ax1.grid(which="both")
ax1.set_xlabel("AB/2 (m)")
ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)")
ax1.legend(["Observed", "L2 Inversion", "IRLS Inversion"])
plt.show()
Plot Recovered Models¶
# Plot true model and recovered model
fig = plt.figure(figsize=(6, 6))
ax1 = fig.add_axes([0.2, 0.15, 0.7, 0.7])
plot_1d_layer_model(true_layers, true_resistivities, ax=ax1, color="k")
plot_1d_layer_model(
layer_thicknesses, log_resistivity_map * recovered_model_L2, ax=ax1, color="b"
)
plot_1d_layer_model(
layer_thicknesses, log_resistivity_map * recovered_model_irls, ax=ax1, color="r"
)
x_min, x_max = true_resistivities.min(), true_resistivities.max()
ax1.set_xlim(0.9 * x_min, 1.1 * x_max)
ax1.grid()
ax1.set_xlabel(r"Resistivity ($\Omega m$)")
ax1.legend(["True Model", "L2 Model", "IRLS Model"])
plt.show()
Parametric Inversion¶
Here, we assume the subsurface is defined by a 3-layered Earth. However, the electrical properties and thicknesses of the layers are unknown. Here, we define our model to include log-conductivities and log-thicknesses. When including quantities that span different scales, it is frequently best to define the model in terms of log-values so that each quantity influences the predicted data evenly.
Models and Mappings¶
For a 3-layered Earth model, the model consists of 2 log-thicknesses and 3 log-conductivities. Similar to the 1D Forward Simulation of DC Resistivity Data tutorial, need a mapping that extract log-thicknesses and log-conductivities from the model, and mappings that convert log-values to property values. For this, we require the simpeg.maps.Wires mapping and simpeg.maps.ExpMap mapping classes. Note that successive mappings can be chained together using the operator.
# Wire maps to extract log-thicknesses and log-conductivities
wire_map = maps.Wires(("log_thicknesses", 2), ("log_conductivity", 3))
# Maping for layer thicknesses
log_thicknesses_map = maps.ExpMap() * wire_map.log_thicknesses
# Mapping for conductivities
log_conductivity_map = maps.ExpMap() * wire_map.log_conductivity
Starting and Reference Models¶
This problem is highly non-linear so it is important to have a reasonable estimate of the true model.
starting_parametric_model = np.log(np.r_[125.0, 50.0, 1e-3, 2e-3, 5e-2])
reference_parametric_model = starting_parametric_model.copy()
Define the Forward Simulation¶
Because the layer thicknesses are part of the model, we define the thicknessesMap
. Because we are working in terms of electrical conductivity, we must define the sigmaMap
.
simulation_parametric = dc.simulation_1d.Simulation1DLayers(
survey=survey,
sigmaMap=log_conductivity_map,
thicknessesMap=log_thicknesses_map,
)
Define the Data Misfit¶
dmis_parametric = data_misfit.L2DataMisfit(
simulation=simulation_parametric, data=data_object
)
Define a (Combo) Regularization¶
We need to define a regularization for each model parameter type. In this case, we have log-thicknesses and log-conductivities. For each model parameter type, we create a 1D tensor mesh with length equal to the number of parameters. In the mapping
keyword argument, we used the wire map that extracts the specific model parameters from the model.
Using the operator, separate regularizations can be summed to form a regularization that is also a ComboObjectiveFunction. By setting the multipliers
property, we can emphasis the relative contributions of the log-thicknesses and log-conductivities regularizations.
reg_1 = regularization.Smallness(
TensorMesh([(np.ones(2))], "0"),
mapping=wire_map.log_thicknesses,
reference_model=reference_parametric_model,
)
reg_2 = regularization.Smallness(
TensorMesh([(np.ones(3))], "0"),
mapping=wire_map.log_conductivity,
reference_model=reference_parametric_model,
)
reg_parametric = reg_1 + reg_2
reg_parametric.multipliers = [1.0, 0.1]
Define the Optimization Algorithm¶
opt_parametric = optimization.InexactGaussNewton(
maxIter=100, maxIterLS=20, maxIterCG=20, tolCG=1e-3
)
Define the Inverse Problem¶
inv_prob_parametric = inverse_problem.BaseInvProblem(
dmis_parametric, reg_parametric, opt_parametric
)
Provide Inversion Directives¶
sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=True)
update_jacobi = directives.UpdatePreconditioner(update_every_iteration=True)
starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=10)
beta_schedule = directives.BetaSchedule(coolingFactor=2.0, coolingRate=2)
target_misfit = directives.TargetMisfit(chifact=1.0)
directives_list_parametric = [
sensitivity_weights,
update_jacobi,
starting_beta,
beta_schedule,
target_misfit,
]
Define and Run Inversion¶
inv_parametric = inversion.BaseInversion(
inv_prob_parametric, directives_list_parametric
)
recovered_model_parametric = inv_parametric.run(starting_parametric_model)
Running inversion with SimPEG v0.23.0
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
============================ Inexact Gauss Newton ============================
# beta phi_d phi_m f |proj(x-g)-x| LS Comment
-----------------------------------------------------------------------------
x0 has any nan: 0
0 1.36e+05 1.95e+04 0.00e+00 1.95e+04 1.72e+04 0
1 1.36e+05 1.35e+04 2.04e-02 1.63e+04 3.54e+03 0
2 6.81e+04 1.24e+04 2.92e-02 1.43e+04 8.80e+03 0 Skip BFGS
3 6.81e+04 7.15e+03 9.98e-02 1.39e+04 1.25e+03 0
4 3.41e+04 6.84e+03 1.04e-01 1.04e+04 7.29e+03 0 Skip BFGS
5 3.41e+04 2.56e+03 2.27e-01 1.03e+04 1.31e+03 0
6 1.70e+04 2.75e+03 2.17e-01 6.45e+03 4.96e+03 0
7 1.70e+04 8.86e+02 3.22e-01 6.37e+03 9.63e+02 0
8 8.51e+03 9.84e+02 3.26e-01 3.76e+03 2.94e+03 0
9 8.51e+03 3.41e+02 4.05e-01 3.79e+03 5.31e+02 0
10 4.26e+03 3.56e+02 4.29e-01 2.18e+03 1.71e+03 0
11 4.26e+03 1.37e+02 4.99e-01 2.26e+03 2.65e+02 0
12 2.13e+03 1.38e+02 5.17e-01 1.24e+03 9.96e+02 0
13 2.13e+03 6.26e+01 5.76e-01 1.29e+03 1.34e+02 0
14 1.06e+03 6.34e+01 5.82e-01 6.82e+02 5.64e+02 0
15 1.06e+03 3.67e+01 6.28e-01 7.06e+02 6.75e+01 0
16 5.32e+02 3.72e+01 6.29e-01 3.72e+02 3.08e+02 0
17 5.32e+02 2.79e+01 6.64e-01 3.81e+02 2.69e+01 0
18 2.66e+02 2.80e+01 6.64e-01 2.05e+02 1.63e+02 0
------------------------- STOP! -------------------------
1 : |fc-fOld| = 0.0000e+00 <= tolF*(1+|f0|) = 1.9497e+03
1 : |xc-x_last| = 4.6199e-02 <= tolX*(1+|x0|) = 1.2573e+00
0 : |proj(x-g)-x| = 1.6254e+02 <= tolG = 1.0000e-01
0 : |proj(x-g)-x| = 1.6254e+02 <= 1e3*eps = 1.0000e-02
0 : maxIter = 100 <= iter = 19
------------------------- DONE! -------------------------
Plot Observed and Predicted Data¶
# 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(AB_separations / 2, dobs, "k")
ax1.semilogy(AB_separations / 2, simulation_L2.dpred(recovered_model_L2), "b")
ax1.semilogy(AB_separations / 2, simulation_irls.dpred(recovered_model_irls), "r")
ax1.semilogy(
AB_separations / 2, simulation_parametric.dpred(recovered_model_parametric), "g"
)
ax1.grid(which="both")
ax1.set_xlabel("AB/2 (m)")
ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)")
ax1.legend(["Observed", "L2 Inversion", "IRLS Inversion", "Parametric Inversion"])
plt.show()
Plot Recovered Models¶
fig = plt.figure(figsize=(6, 6))
ax1 = fig.add_axes([0.2, 0.15, 0.7, 0.7])
plot_1d_layer_model(true_layers, true_resistivities, ax=ax1, color="k")
plot_1d_layer_model(
layer_thicknesses, log_resistivity_map * recovered_model_L2, ax=ax1, color="b"
)
plot_1d_layer_model(
layer_thicknesses, log_resistivity_map * recovered_model_irls, ax=ax1, color="r"
)
plot_1d_layer_model(
log_thicknesses_map * recovered_model_parametric,
1 / (log_conductivity_map * recovered_model_parametric),
ax=ax1,
color="g",
)
x_min, x_max = true_resistivities.min(), true_resistivities.max()
ax1.set_xlim(0.8 * x_min, 2 * x_max)
ax1.set_ylim([np.sum(layer_thicknesses), 0])
ax1.grid()
ax1.set_xlabel(r"Resistivity ($\Omega m$)")
ax1.legend(["True Model", "L2 Model", "IRLS Model", "Parametric Model"])
plt.show()