Skip to article frontmatterSkip to article content

1D Inversion of Frequency Domain EM Data for a Single Sounding

University of British Columbia

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:

  1. A weighted least-squares inversion where the number of layers and their thicknesses are fixed
  2. An iteratively re-weighted least-squares (IRLS) inversion to recover sparse and/or blocky structures
  3. 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.electromagnetics.frequency_domain module. Classes required to define the data misfit, regularization, optimization, etc... are imported from elsewhere within SimPEG. We also import some useful utility functions from simpeg.utils. To generate the mesh used for the inversion, we use the discretize package.

# 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()
<Figure size 500x500 with 1 Axes>

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:

dmin5001σfmaxd_{min} \approx 500 \sqrt{\dfrac{1}{\sigma \, f_{max}}}

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:

dmax5001σfmind_{max} \approx 500 \sqrt{\dfrac{1}{\sigma \, f_{min}}}

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 Ωm\Omega m. 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, and coolingRate 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()
<Figure size 1000x500 with 2 Axes>

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()
<Figure size 600x600 with 1 Axes>

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()
<Figure size 1000x500 with 2 Axes>
# 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()
<Figure size 600x600 with 1 Axes>

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()
<Figure size 1000x500 with 2 Axes>
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()
<Figure size 600x600 with 1 Axes>