1D Inversion of Frequency Domain EM Data for a Single Sounding
Keywords: frequency-domain EM, 1D sounding, inversion, parametric, sparse norm, wires mapping
Summary: Here, we invert secondary magnetic field data for a single 1D FDEM sounding. We demonstrate 3 approaches for inverting the data:
- A weighted least-squares inversion where the number of layers and their thicknesses are fixed
- An iteratively re-weighted 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 re-weighted 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 Frequency-Domain EM Data for a Single Sounding tutorial before working through this one. For this tutorial, we focus on:
- How to carry out 1D geophysical inversion with SimPEG.
- How to assign appropriate uncertainties to FDEM 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.
Importing 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 frequency domain EM data are imported from the simpeg
# SimPEG functionality
import simpeg.electromagnetics.frequency_domain as fdem
from simpeg.utils import plot_1d_layer_model, download, mkvc
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 and Extract the Tutorial Data¶
For this tutorial, the frequencies and observed data for 1D sounding are stored within a tar-file. Here, we download and extract the data file.
# URL to assets
data_source = "https://github.com/simpeg/user-tutorials/raw/main/assets/07-fdem/inv_fdem_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 + "em1dfm_data.txt"
overwriting /home/ssoler/git/user-tutorials/notebooks/07-fdem/inv_fdem_1d_files.tar.gz
Downloading https://github.com/simpeg/user-tutorials/raw/main/assets/07-fdem/inv_fdem_1d_files.tar.gz
saved to: /home/ssoler/git/user-tutorials/notebooks/07-fdem/inv_fdem_1d_files.tar.gz
Download completed!
Load and Plot the Data¶
Here we load and plot the 1D sounding data. In this case, we have the secondary field response in ppm for a set of frequencies. The columns of the data file are: frequency (Hz), real component (ppm) and imaginary component (ppm).
# Load data
dobs = np.loadtxt(str(data_filename), skiprows=1)
# Extract frequency and observed data columns
frequencies = dobs[:, 0]
dobs = mkvc(dobs[:, 1:].T)
# Plot data
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
ax.loglog(frequencies, np.abs(dobs[0::2]), "k-o", lw=2)
ax.loglog(frequencies, np.abs(dobs[1::2]), "k:o", lw=2)
ax.grid(which="both")
ax.set_xlabel("Frequency (Hz)")
ax.set_ylabel("|Hs/Hp| (ppm)")
ax.set_title("Sounding Data")
ax.legend(["Real", "Imaginary"])
plt.show()
Defining the Survey¶
Here, we define the survey geometry. For a comprehensive description of constructing FDEM surveys in SimPEG, see the 1D Forward Simulation of Frequency-Domain EM Data for a Single Sounding tutorial.
Here, the survey consisted of a vertical magnetic dipole source located 30 m above the surface. The receiver measured the vertical component of the secondary field at a 10 m offset from the source in ppm.
source_location = np.array([0.0, 0.0, 30.0])
source_orientation = "z"
moment = 1.0
receiver_location = np.array([10.0, 0.0, 30.0])
receiver_orientation = "z"
data_type = "ppm"
# Receiver list
receiver_list = []
receiver_list.append(
fdem.receivers.PointMagneticFieldSecondary(
receiver_location,
orientation=receiver_orientation,
data_type=data_type,
component="real",
)
)
receiver_list.append(
fdem.receivers.PointMagneticFieldSecondary(
receiver_location,
orientation=receiver_orientation,
data_type=data_type,
component="imag",
)
)
# Define source list
source_list = []
for freq in frequencies:
source_list.append(
fdem.sources.MagDipole(
receiver_list=receiver_list,
frequency=freq,
location=source_location,
orientation=source_orientation,
moment=moment,
)
)
# Survey
survey = fdem.survey.Survey(source_list)
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 secondary field and ppm data: In this case, we generally apply a percent uncertainty. Depending on many factors, a percent uncertainty between 5% and 20% may be applied. For systems where multiple field directions are measured for the same source, we may not want to apply a uniform percent uncertainty to all data. Doing so may cause the inversion to overfit weaker signals. In this case, the uncertainty may be a percent of the amplitude of the secondary field. If the data contain very small values, a small floor value should be added to ensure stability of the inversion.
For total field data: In this case, the real component contains both the primary and secondary fields, whereas the imaginary component contains only secondary fields. Because the primary field is orders of magnitude larger than the secondary field, applying a simple percent uncertainty to the real component data may result in us underfitting the real component of the EM response. Ideally, we would have defined the survey to perfectly simulate the primary fields, and assign uncertainties based on the secondary fields.
# 5% of the absolute value
uncertainties = 0.05 * np.abs(dobs) * np.ones(np.shape(dobs))
Defining 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, noise_floor=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.
Defining a 1D Layered Earth¶
Let us assume we have a reasonable estimate of the regional conductivity within our area of interest. For the highest frequency and the estimated conductivity, we compute the minimum skin depth:
The minimum layer thickness is some fraction of the minimum skin depth. Next, we use the minimum frequency and the estimated conductivity to compute the maximum skin depth:
Starting from our minimum layer thickness, we continue to add layers with exponentially increasing thicknesses. We do so until the layers extend to some multiple of the maximum skin depth.
# estimated host conductivity (S/m)
estimated_conductivity = 0.1
# minimum skin depth
d_min = 500.0 / np.sqrt(estimated_conductivity * frequencies.max())
print("MINIMUM SKIN DEPTH: {} m".format(d_min))
# maximum skin depth
d_max = 500.0 / np.sqrt(estimated_conductivity * frequencies.min())
print("MAXIMUM SKIN DEPTH: {} m".format(d_max))
MINIMUM SKIN DEPTH: 4.383604418942005 m
MAXIMUM SKIN DEPTH: 80.89810021132169 m
depth_min = 0.5 # top layer thickness
depth_max = 200.0 # depth to lowest layer
geometric_factor = 1.1 # rate of thickness increase
# 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
Model and Mapping to Layer Conductivities¶
Recall from the 1D Forward Simulation of Frequency-Domain EM Data for a Single Sounding 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 conductivities (or resistivities), it is best to use the log-value, as electrical conductivities of rocks span many order of magnitude.
Here, the model defines the log-conductivity values for a defined set of subsurface layers. And we use the simpeg.maps.ExpMap to map from the model parameters to the conductivity values required by the forward simulation.
log_conductivity_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-conductivity values (S/m)
starting_conductivity_model = np.log(1e-1 * np.ones(n_layers))
# Reference model is also log-resistivity values (S/m)
reference_conductivity_model = starting_conductivity_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 Frequency-Domain EM Data for a Single Sounding tutorial. Here, we use the Simulation1DLayered 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-conductivities, we use sigmaMap
to set the mapping from the model to the layer conductivities.
simulation_L2 = fdem.Simulation1DLayered(
survey=survey, thicknesses=layer_thicknesses, sigmaMap=log_conductivity_map
)
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)
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: 40 cells
MESH EXTENT CELL WIDTH FACTOR
dir nC min max min max max
--- --- --------------------------- ------------------ ------
x 40 -219.43 -0.00 0.50 18.70 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=10.0,
reference_model=reference_conductivity_model,
reference_model_in_smooth=False,
)
Optimization¶
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
)
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)
Inversion Directives¶
To understand the role of directives in the inversion, please visit this online resource. Here, we apply common directives for weighted least-squares inversion of gravity data and describe their roles. These are:
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 3 works well for FDEM 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.
update_jacobi = directives.UpdatePreconditioner(update_every_iteration=True)
starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=5)
beta_schedule = directives.BetaSchedule(coolingFactor=2.0, coolingRate=3)
target_misfit = directives.TargetMisfit(chifact=1.0)
directives_list_L2 = [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_conductivity_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 Simulation1DLayered 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.43e+00 2.86e+02 0.00e+00 2.86e+02 5.81e+01 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)
1 1.43e+00 1.17e+02 3.60e+01 1.69e+02 2.11e+01 0
2 1.43e+00 8.17e+01 5.10e+01 1.55e+02 9.07e+00 0 Skip BFGS
3 7.17e-01 7.10e+01 5.66e+01 1.12e+02 2.38e+01 0 Skip BFGS
4 7.17e-01 3.14e+01 9.14e+01 9.69e+01 5.71e+00 0
5 7.17e-01 2.73e+01 9.51e+01 9.54e+01 2.75e+00 0 Skip BFGS
6 3.59e-01 2.59e+01 9.65e+01 6.05e+01 1.47e+01 0 Skip BFGS
7 3.59e-01 1.27e+01 1.21e+02 5.60e+01 1.78e+00 0
8 3.59e-01 1.21e+01 1.22e+02 5.58e+01 7.11e-01 0 Skip BFGS
9 1.79e-01 1.20e+01 1.22e+02 3.39e+01 8.04e+00 0 Skip BFGS
------------------------- STOP! -------------------------
1 : |fc-fOld| = 0.0000e+00 <= tolF*(1+|f0|) = 2.8679e+01
1 : |xc-x_last| = 4.9329e-01 <= tolX*(1+|x0|) = 1.5563e+00
0 : |proj(x-g)-x| = 8.0394e+00 <= tolG = 1.0000e-01
0 : |proj(x-g)-x| = 8.0394e+00 <= 1e3*eps = 1.0000e-02
0 : maxIter = 100 <= iter = 10
------------------------- DONE! -------------------------
Inversion Outputs¶
Data Misfit¶
dpred_L2 = simulation_L2.dpred(recovered_model_L2)
fig = plt.figure(figsize=(10, 5))
ax = [fig.add_axes([0.1 + ii * 0.5, 0.1, 0.37, 0.85]) for ii in range(2)]
for ii in range(2):
ax[ii].loglog(frequencies, np.abs(dobs[ii::2]), "k-o", lw=2)
ax[ii].loglog(frequencies, np.abs(dpred_L2[ii::2]), "b-o", lw=2)
ax[ii].grid(which="both")
ax[ii].set_xlabel("Frequency (Hz)")
ax[ii].set_ylabel("|Hs/Hp| (ppm)")
ax[ii].legend(["Observed", "L2 Inversion"])
if ii == 1:
ax[ii].set_ylabel("")
ax[0].set_title("Real Component")
ax[1].set_title("Imaginary Component")
plt.show()
Recovered Model¶
# true conductivities and layer thicknesses
true_conductivities = np.array([0.1, 1.0, 0.1])
true_layers = np.r_[20.0, 40.0, 160.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_conductivities, ax=ax1, color="k")
plot_1d_layer_model(
layer_thicknesses, log_conductivity_map * recovered_model_L2, ax=ax1, color="b"
)
ax1.grid()
ax1.set_xlabel(r"Resistivity ($\Omega m$)")
x_min, x_max = true_conductivities.min(), true_conductivities.max()
ax1.set_xlim(0.9 * x_min, 1.5 * x_max)
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 = fdem.simulation_1d.Simulation1DLayered(
survey=survey,
sigmaMap=log_conductivity_map,
thicknesses=layer_thicknesses,
)
Define the Data Misfit¶
dmis_irls = data_misfit.L2DataMisfit(simulation=simulation_irls, data=data_object)
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.01,
alpha_x=1,
reference_model_in_smooth=False,
norms=[1.0, 1.0],
)
Optimization¶
opt_irls = optimization.InexactGaussNewton(
maxIter=100, maxIterLS=20, maxIterCG=30, tolCG=1e-3
)
Inverse Problem¶
inv_prob_irls = inverse_problem.BaseInvProblem(dmis_irls, reg_irls, opt_irls)
Directives¶
Here, we create common directives for IRLS inversion of total magnetic intensity data and describe their roles. In additon to the UpdateSensitivityWeights, UpdatePreconditioner and BetaEstimate_ByEig (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.
starting_beta_irls = directives.BetaEstimate_ByEig(beta0_ratio=5)
update_jacobi_irls = directives.UpdatePreconditioner(update_every_iteration=True)
update_irls = directives.UpdateIRLS(
cooling_factor=2,
cooling_rate=3,
f_min_change=1e-4,
max_irls_iterations=30,
chifact_start=1.0,
)
directives_list_irls = [update_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_conductivity_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 Simulation1DLayered 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 3.11e+01 2.86e+02 0.00e+00 2.86e+02 5.81e+01 0
1 3.11e+01 2.94e+01 1.68e+00 8.18e+01 2.24e+01 0
2 3.11e+01 1.72e+01 1.42e+00 6.14e+01 5.54e+00 0
3 1.56e+01 1.40e+01 1.50e+00 3.73e+01 8.92e+00 0 Skip BFGS
Reached starting chifact with l2-norm regularization: Start IRLS steps...
irls_threshold 2.5194963943229616
4 1.56e+01 7.99e+00 1.94e+00 3.82e+01 1.55e+00 0
5 1.56e+01 8.44e+00 1.89e+00 3.78e+01 6.28e-01 0
6 1.97e+01 8.26e+00 1.90e+00 4.57e+01 2.59e+00 0 Skip BFGS
7 1.97e+01 9.69e+00 1.88e+00 4.68e+01 5.89e-01 0
8 1.97e+01 1.00e+01 1.86e+00 4.68e+01 1.82e-01 0 Skip BFGS
9 2.50e+01 9.96e+00 1.87e+00 5.67e+01 3.39e+00 0 Skip BFGS
10 2.50e+01 1.21e+01 1.81e+00 5.74e+01 5.89e-01 0
11 2.50e+01 1.25e+01 1.79e+00 5.74e+01 1.18e-01 0 Skip BFGS
12 1.54e+01 1.24e+01 1.80e+00 4.02e+01 6.15e+00 0 Skip BFGS
13 1.54e+01 8.64e+00 2.04e+00 4.01e+01 7.07e-01 0
14 1.54e+01 8.75e+00 2.03e+00 4.01e+01 2.39e-01 0
15 1.91e+01 8.69e+00 2.03e+00 4.74e+01 2.46e+00 0 Skip BFGS
16 1.91e+01 1.00e+01 2.01e+00 4.84e+01 6.74e-01 0
17 1.91e+01 1.03e+01 1.99e+00 4.83e+01 1.71e-01 0
18 2.36e+01 1.03e+01 1.99e+00 5.72e+01 3.08e+00 0 Skip BFGS
19 2.36e+01 1.22e+01 1.95e+00 5.80e+01 7.71e-01 0
20 2.36e+01 1.26e+01 1.93e+00 5.80e+01 1.59e-01 0 Skip BFGS
21 1.45e+01 1.25e+01 1.93e+00 4.05e+01 6.30e+00 0 Skip BFGS
22 1.45e+01 8.84e+00 2.17e+00 4.02e+01 7.78e-01 0
23 1.45e+01 8.91e+00 2.16e+00 4.02e+01 1.92e-01 0
24 1.77e+01 8.87e+00 2.16e+00 4.71e+01 2.34e+00 0 Skip BFGS
25 1.77e+01 1.01e+01 2.14e+00 4.79e+01 7.81e-01 0
26 1.77e+01 1.04e+01 2.12e+00 4.79e+01 1.69e-01 0
27 2.16e+01 1.04e+01 2.12e+00 5.62e+01 2.88e+00 0 Skip BFGS
28 2.16e+01 1.20e+01 2.08e+00 5.69e+01 8.85e-01 0
29 2.16e+01 1.24e+01 2.06e+00 5.68e+01 1.61e-01 0
30 1.34e+01 1.23e+01 2.06e+00 3.98e+01 6.14e+00 0 Skip BFGS
31 1.34e+01 8.99e+00 2.28e+00 3.94e+01 7.83e-01 0
32 1.34e+01 9.00e+00 2.27e+00 3.94e+01 1.23e-01 0
33 1.62e+01 8.97e+00 2.28e+00 4.59e+01 2.23e+00 0 Skip BFGS
34 1.62e+01 1.00e+01 2.25e+00 4.65e+01 7.95e-01 0
35 1.62e+01 1.03e+01 2.23e+00 4.64e+01 1.27e-01 0
36 1.97e+01 1.03e+01 2.23e+00 5.42e+01 2.71e+00 0 Skip BFGS
37 1.97e+01 1.17e+01 2.18e+00 5.47e+01 9.16e-01 0
38 1.97e+01 1.20e+01 2.17e+00 5.47e+01 1.29e-01 0 Skip BFGS
39 1.23e+01 1.20e+01 2.17e+00 3.87e+01 5.80e+00 0 Skip BFGS
40 1.23e+01 9.06e+00 2.37e+00 3.82e+01 7.66e-01 0
41 1.23e+01 9.01e+00 2.37e+00 3.82e+01 7.90e-02 0
------------------------- STOP! -------------------------
1 : |fc-fOld| = 2.0199e-02 <= tolF*(1+|f0|) = 2.8679e+01
1 : |xc-x_last| = 7.9955e-02 <= tolX*(1+|x0|) = 1.5563e+00
1 : |proj(x-g)-x| = 7.8972e-02 <= tolG = 1.0000e-01
0 : |proj(x-g)-x| = 7.8972e-02 <= 1e3*eps = 1.0000e-02
0 : maxIter = 100 <= iter = 41
------------------------- DONE! -------------------------
Data Misfit and Recovered Model¶
dpred_irls = simulation_irls.dpred(recovered_model_irls)
fig = plt.figure(figsize=(10, 5))
ax = [fig.add_axes([0.1 + ii * 0.5, 0.1, 0.37, 0.85]) for ii in range(2)]
for ii in range(2):
ax[ii].loglog(frequencies, np.abs(dobs[ii::2]), "k-o", lw=2)
ax[ii].loglog(frequencies, np.abs(dpred_L2[ii::2]), "b-o", lw=2)
ax[ii].loglog(frequencies, np.abs(dpred_irls[ii::2]), "r-o", lw=2)
ax[ii].grid(which="both")
ax[ii].set_xlabel("Frequency (Hz)")
ax[ii].set_ylabel("|Hs/Hp| (ppm)")
ax[ii].legend(["True Sounding", "Predicted (L2-model)", "Predicted (IRLS)"])
if ii == 1:
ax[ii].set_ylabel("")
ax[0].set_title("Real Component")
ax[1].set_title("Imaginary Component")
plt.show()
# 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_conductivities, ax=ax1, color="k")
plot_1d_layer_model(
layer_thicknesses, log_conductivity_map * recovered_model_L2, ax=ax1, color="b"
)
plot_1d_layer_model(
layer_thicknesses, log_conductivity_map * recovered_model_irls, ax=ax1, color="r"
)
ax1.grid()
ax1.set_xlabel(r"Conductivity ($S/m$)")
x_min, x_max = true_conductivities.min(), true_conductivities.max()
ax1.set_xlim(0.8 * x_min, 2 * x_max)
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.
Model and Mapping¶
For a 3-layered Earth model, the model consists of 2 log-thicknesses and 3 log-conductivities. Similar to the 1D Forward Simulation of Frequency Domain EM Data for a Single Sounding tutorial, need a mapping that extract log-thicknesses and log-resistivities 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_resistivity", 3))
# Maping for layer thicknesses
log_thicknesses_map = maps.ExpMap() * wire_map.log_thicknesses
# Mapping for conductivities
log_resistivity_map = maps.ExpMap() * wire_map.log_resistivity
Starting and Reference Model¶
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_[30.0, 20.0, 20, 0.5, 5])
reference_parametric_model = starting_parametric_model.copy()
Forward Simulation¶
Because the layer thicknesses are part of the model, we define the thicknessesMap
. Because we are working in terms of electrical resistivity, we must define the rhoMap
.
simulation_parametric = fdem.simulation_1d.Simulation1DLayered(
survey=survey,
rhoMap=log_resistivity_map,
thicknessesMap=log_thicknesses_map,
)
Data Misfit¶
dmis_parametric = data_misfit.L2DataMisfit(
simulation=simulation_parametric, data=data_object
)
(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 emphasize 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_resistivity,
reference_model=reference_parametric_model,
)
reg_parametric = reg_1 + reg_2
reg_parametric.multipliers = [1.0, 0.1]
Optimization¶
opt_parametric = optimization.InexactGaussNewton(
maxIter=100, maxIterLS=20, maxIterCG=20, tolCG=1e-3
)
Inverse Problem¶
inv_prob_parametric = inverse_problem.BaseInvProblem(
dmis_parametric, reg_parametric, opt_parametric
)
Directives¶
update_jacobi = directives.UpdatePreconditioner(update_every_iteration=True)
starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=5)
beta_schedule = directives.BetaSchedule(coolingFactor=2.0, coolingRate=3)
target_misfit = directives.TargetMisfit(chifact=1.0)
directives_list_parametric = [
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 Simulation1DLayered 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 3.18e+03 2.87e+02 0.00e+00 2.87e+02 6.58e+02 0
1 3.18e+03 1.07e+02 1.82e-02 1.65e+02 7.94e+01 0
2 3.18e+03 9.89e+01 2.04e-02 1.64e+02 1.48e+01 0 Skip BFGS
3 1.59e+03 9.77e+01 2.07e-02 1.31e+02 2.04e+02 0 Skip BFGS
4 1.59e+03 6.06e+01 3.68e-02 1.19e+02 1.41e+01 0
5 1.59e+03 5.90e+01 3.78e-02 1.19e+02 3.10e+00 0 Skip BFGS
6 7.96e+02 5.88e+01 3.79e-02 8.90e+01 1.57e+02 0 Skip BFGS
7 7.96e+02 3.59e+01 5.78e-02 8.19e+01 6.20e+00 0
8 7.96e+02 3.52e+01 5.87e-02 8.19e+01 3.78e-01 0 Skip BFGS
9 3.98e+02 3.51e+01 5.87e-02 5.85e+01 1.15e+02 0 Skip BFGS
10 3.98e+02 2.14e+01 8.26e-02 5.43e+01 8.19e+00 0
11 3.98e+02 2.10e+01 8.36e-02 5.42e+01 1.18e+00 0 Skip BFGS
12 1.99e+02 2.09e+01 8.38e-02 3.76e+01 7.98e+01 0 Skip BFGS
13 1.99e+02 1.32e+01 1.10e-01 3.52e+01 7.78e+00 0
14 1.99e+02 1.30e+01 1.11e-01 3.51e+01 1.00e+00 0 Skip BFGS
15 9.95e+01 1.30e+01 1.11e-01 2.41e+01 5.04e+01 0 Skip BFGS
------------------------- STOP! -------------------------
1 : |fc-fOld| = 0.0000e+00 <= tolF*(1+|f0|) = 2.8794e+01
1 : |xc-x_last| = 8.4799e-02 <= tolX*(1+|x0|) = 6.7086e-01
0 : |proj(x-g)-x| = 5.0407e+01 <= tolG = 1.0000e-01
0 : |proj(x-g)-x| = 5.0407e+01 <= 1e3*eps = 1.0000e-02
0 : maxIter = 100 <= iter = 16
------------------------- DONE! -------------------------
Data Misfit and Recovered Model¶
dpred_parametric = simulation_parametric.dpred(recovered_model_parametric)
fig = plt.figure(figsize=(10, 5))
ax = [fig.add_axes([0.1 + ii * 0.5, 0.1, 0.37, 0.85]) for ii in range(2)]
for ii in range(2):
ax[ii].loglog(frequencies, np.abs(dobs[ii::2]), "k-o", lw=2)
ax[ii].loglog(frequencies, np.abs(dpred_L2[ii::2]), "b-o", lw=2)
ax[ii].loglog(frequencies, np.abs(dpred_irls[ii::2]), "r-o", lw=2)
ax[ii].loglog(frequencies, np.abs(dpred_parametric[ii::2]), "g-o", lw=2)
ax[ii].grid(which="both")
ax[ii].set_xlabel("Frequency (Hz)")
ax[ii].set_ylabel("|Hs/Hp| (ppm)")
ax[ii].legend(
[
"True Sounding",
"Predicted (L2-model)",
"Predicted (IRLS)",
"Predicted (Parametric)",
]
)
if ii == 1:
ax[ii].set_ylabel("")
ax[0].set_title("Real Component")
ax[1].set_title("Imaginary Component")
plt.show()
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_conductivities, ax=ax1, color="k")
plot_1d_layer_model(
layer_thicknesses, log_conductivity_map * recovered_model_L2, ax=ax1, color="b"
)
plot_1d_layer_model(
layer_thicknesses, log_conductivity_map * recovered_model_irls, ax=ax1, color="r"
)
plot_1d_layer_model(
log_thicknesses_map * recovered_model_parametric,
1 / (log_resistivity_map * recovered_model_parametric),
ax=ax1,
color="g",
)
ax1.grid()
ax1.set_xlabel(r"Resistivity ($\Omega m$)")
x_min, x_max = true_conductivities.min(), true_conductivities.max()
ax1.set_xlim(0.9 * x_min, 2 * x_max)
ax1.set_ylim([np.sum(layer_thicknesses), 0])
ax1.legend(["True Model", "L2 Model", "IRLS Model", "Parametric Model"])
plt.show()