Keywords: TDEM, forward simulation, waveforms, 1D sounding, wires mapping.
Summary: In this tutorial, we present the fundamentals of simulating TDEM data in SimPEG. We use the module simpeg
The tutorial is organized into 3 parts. In part 1, we focus on the most fundamental aspects of simulating TDEM data. For a step-off waveform, we simulate TDEM data for a conductive and non-permeable layered Earth. In part 2, we demonstrate how to include dispersive electromagnetic properties in the 1D TDEM simulation. And in part 3, we demonstrate how data can be simulated for user-specified transmitter waveforms.
Learning Objectives:
- The fundamentals of simulating TDEM data with SimPEG.
- Understanding the way in which TDEM surveys are created in SimPEG, which includes:
- Defining receivers
- Defining controlled sources
- Organizing sources and receivers into a survey object
- Defining the Earth’s electrical properties in terms of conductivity OR resistivity.
- The ways in which we can define 1D layered Earth models.
- Simulating TDEM data for different transmitter waveforms
- Simulating TDEM data for dispersive electromagnetic properties (i.e. induced polarization, superparamagnetism)
Importing Modules¶
Here, we import all of the functionality required to run the notebook for the tutorial exercise. All of the functionality specific to TDEM is imported from simpeg
# SimPEG functionality
import simpeg.electromagnetics.time_domain as tdem
from simpeg import maps
from simpeg.utils import plot_1d_layer_model
# Common Python functionality
import os
import numpy as np
from scipy.constants import mu_0
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rcParams.update({"font.size": 14})
write_output = False # Optional
Part 1: Step-Off Response for a Conductive Layer¶
In this part of the tutorial, we focus on the most fundamental aspects of simulating TDEM data in SimPEG. Here, the survey consists of a single circular loop located 1 m above the Earth’s surface. The vertical B-field is simulated at the loop’s centre for a step-off waveform.
Defining the Survey¶
TDEM surveys within SimPEG require the user to create and connect four types of objects:
- receivers: There are a multitude of TDEM receiver classes within SimPEG, each of which is used to simulate data corresponding to a different field measurement; e.g. PointMagneticField, PointMagneticFluxTimeDerivative and PointElectricField. The properties for each TDEM receiver object generally include: the orientation of the field being measured (x, y, z, other), the time channels, the data type, and one or more associated observation locations.
- sources: There are a multitude of TDEM source classes within SimPEG, each of which corresponds to a different geometry; e.g. MagDipole and LineCurrent. Source classes generally require the user to define the current waveform, location and geometry.
- waveforms: The waveform defining the time-dependent current in the source is defined by an object in SimPEG. Some waveform classes include StepOffWaveform, VTEMWaveform and RawWaveform
- survey: The object which stores and organizes all of the sources and receivers.
For a full list of source types, waveform types and receiver types, please visit API documentation for simpeg
Topography: When generating the survey for a 1D forward simulation, sources and receivers must be located above the Earth’s surface. By default, the Earth’s surface is at z = 0 m when defining the 1D simulation. So for 1D FDEM problems, it is easiest to define the z-locations of all sources and receivers as flight heights.
For this tutorial, the survey consists of a circular source loop 1 m above the surface that uses a step-off waveform. The vertical component of the magnetic flux density is measured at the loop’s center at a number of time channels.
# Source properties
source_location = np.array([0.0, 0.0, 1.0]) # (3, ) numpy.array_like
source_orientation = "z" # "x", "y" or "z"
source_current = 1.0 # maximum on-time current (A)
source_radius = 10.0 # source loop radius (m)
# Receiver properties
receiver_locations = np.array([0.0, 0.0, 1.0]) # or (N, 3) numpy.ndarray
receiver_orientation = "z" # "x", "y" or "z"
times = np.logspace(-5, -2, 31) # time channels (s)
A waveform must be assigned as a property of each TDEM source object. Here, we define a step-off waveform. By default, the off-time for the step-off waveform begins at 0 s. However, the keyword argument off_time
can be used to change the start of the off-time.
stepoff_waveform = tdem.sources.StepOffWaveform(off_time=0.0)
The most general way to generate TDEM surveys is to loop over all sources. A new source object must be created whenever the source location and/or geometry differs. For each source, we define and assign the associated receivers. A different receiver object must be created whenever geometry, data type and/or time channels differ. However, identical data being collected at a multitude of observation locations can be defined using a single receiver object.
For this tutorial, we only have a single source. And this source only has a single receiver. More complicated survey geometries are used in the 3D Forward Simulation of TDEM Data on Cylindrical Mesh for a Galvanic Source and Forward Simulation of 3D Airborne TDEM Data on a Tree Mesh tutorials. These tutorials demonstrate the more general construction of TDEM surveys.
# Define receiver list. In our case, we have only a single receiver for each source.
# When simulating the response for multiple data types for the same source,
# the list consists of multiple receiver objects.
receiver_list = []
receiver_list.append(
tdem.receivers.PointMagneticFluxDensity(
receiver_locations, times, orientation=receiver_orientation
)
)
# Define source list. In our case, we have only a single source.
source_list = [
tdem.sources.CircularLoop(
receiver_list=receiver_list,
location=source_location,
waveform=stepoff_waveform,
current=source_current,
radius=source_radius,
)
]
# Define the survey
survey = tdem.Survey(source_list)
Defining a 1D Layered Earth and the Model¶
In SimPEG, a 1D layered Earth is defined by the set of layer thicknesses and the physical properties for each layer. Thicknesses and physical property values are defined from the top layer down. If we have N layers, we define N physical property values and N-1 layer thicknesses. The lowest layer is assumed to extend to infinity. In the case of a halfspace, the layer thicknesses would be an empty array.
# Layer conductivities
layer_conductivities = np.r_[0.1, 1.0, 0.1]
# Layer thicknesses
layer_thicknesses = np.r_[40.0, 40.0]
# Number of layers
n_layers = len(layer_conductivities)
fig = plt.figure(figsize=(4, 5))
ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.8])
ax1 = plot_1d_layer_model(layer_thicknesses, layer_conductivities, scale="log", ax=ax1)
ax1.grid(which="both")
ax1.set_xlabel(r"Conductivity ($S/m$)")
plt.show()
Models and Mappings for 1D Simulations¶
In SimPEG, the term ‘model’ is not necessarily synonymous with a set of physical property values. For example, the model may be defined as the logarithms of the physical property values, or be the parameters defining a layered Earth geometry. Models in SimPEG are 1D numpy.ndarray whose lengths are equal to the number of model parameters. For 1D TDEM simulations, we can characterize the Earth’s electric properties according to electrical conductivity or electrical resistivity.
Classes within the simpeg.maps
module are used to define the mapping that connects the model to the parameters required to run the 1D TDEM simulation; e.g. layer conductivities/resistivities, magnetic permeabilities and/or layer thicknesses. In this part of the tutorial, we demonstrate 2 types of mappings and models that may be used for 1D TDEM simulation.
1. Conductivity model: For forward simulation, the easiest approach is to define the model as the layer conductivities and set the layer thicknesses as a static property of the 1D simulation. In this case, the mapping from the model to the conductivities is defined using the simpeg
2. Parametric layered Earth model: In this case, the model parameters are log-resistivities and layer thicknesses. We therefore need a mapping that extracts log-resistivities from the model and converts them into resistivities, and a mapping that extracts layer thicknesses from the model. 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.
# Define model and mapping for a conductivity model.
conductivity_model = layer_conductivities.copy()
conductivity_map = maps.IdentityMap(nP=n_layers)
# Define model and mappings for the parametric model.
# Note the ordering in which you defined the model parameters and the
# order in which you defined the wire mappings matters!!!
parametric_model = np.r_[layer_thicknesses, np.log(1 / layer_conductivities)]
wire_map = maps.Wires(("thicknesses", n_layers - 1), ("log_resistivity", n_layers))
thicknesses_map = wire_map.thicknesses
log_resistivity_map = maps.ExpMap() * wire_map.log_resistivity
Defining the Forward Simulation¶
In SimPEG, the physics of the forward simulation is defined by creating an instance of an appropriate simulation class. Here, we use the Simulation1DLayered which simulates the data according to a 1D Hankel transform solution. To fully define the forward simulation, we need to connect the simulation object to:
- the survey
- the layer thicknesses
- the physical properties of the layers (e.g. conductivity)
This is accomplished by setting each one of the aforementioned items as a property of the simulation object. Since the parameters defining the model in each case are different, we must define a separate simulation object for each case.
1. Conductivity model simulation: Here, the model parameters are the layer conductivities. sigmaMap
is used to define the mapping from the model to the layer conductivities. And thicknessess
is used to set the layer thicknesses as a static property of the simulation.
2. Parametric model simulation: Here, the model consists of the layer thicknesses and log-resistivities. Because we are now working with electric resistivity, rhoMap
is used to define the mapping for the Earth’s electrical properties; i.e. model parameters to layer resistivities. And thicknessesMap
is used to define the mapping from the model to the layer thicknesses.
simulation_conductivity = tdem.simulation_1d.Simulation1DLayered(
survey=survey,
sigmaMap=conductivity_map,
thicknesses=layer_thicknesses,
)
simulation_parametric = tdem.simulation_1d.Simulation1DLayered(
survey=survey,
rhoMap=log_resistivity_map,
thicknessesMap=thicknesses_map,
)
Predict 1D TDEM Data¶
Once any simulation within SimPEG has been properly constructed, simulated data for a given model vector can be computed using the dpred method. Note that despite the difference in how we defined the model, the data predicted for the conductivity and parametric models is the same.
For surveys consisting of multiple sources, multiple receivers per source and multiple observation locations and time channels per receiver, the predicted data vector is organized:
- by source
- by receiver
- by time channel, then
- by observation location
dpred_conductivity = simulation_conductivity.dpred(conductivity_model)
dpred_parametric = simulation_parametric.dpred(parametric_model)
fig = plt.figure(figsize=(5, 6))
ax = fig.add_axes([0.2, 0.15, 0.75, 0.78])
ax.loglog(times, dpred_conductivity, "b-", lw=3)
ax.loglog(times, dpred_parametric, "r--", lw=3)
ax.set_xlim([times.min(), times.max()])
ax.grid()
ax.set_xlabel("Times (s)")
ax.set_ylabel("B (T)")
ax.set_title("Magnetic Flux Density")
ax.legend(["Conductivity model", "Parametric model"])
plt.show()
Part 2: Magnetic Permeability and Dispersive Physical Properties¶
In this part of the tutorial, we simulate TDEM data for 3 cases: 1) a conductive and magnetically permeable layer, 2) a conductive and chargeable layer, and 3) a conductive layer with a magnetically viscous top layer. In all cases, the set of model parameters are the layer conductivities from part 1.
Case 1: the conductive layer is also magnetically permeable¶
In this case, the middle layer is magnetically permeable in addition to being more conductive than the host. For TDEM simulation classes within SimPEG, the Earth’s magnetic properties are defined as magnetic permeabilities (not susceptibilities). Here, the layer magnetic permeabilities are set as a static property of the simulation using the mu
keyword argument. However, one could define a model whose parameters contain the layer magnetic permeabilities; see the 1D Forward Simulation of Frequency Domain EM Data for a Single Sounding tutorial. This would require the user set muMap
.
layer_susceptibilities = np.r_[0.0, 9.0, 0.0]
layer_permeabilities = mu_0 * (1 + layer_susceptibilities)
simulation_permeable = tdem.simulation_1d.Simulation1DLayered(
survey=survey,
sigmaMap=conductivity_map,
thicknesses=layer_thicknesses,
mu=layer_permeabilities,
)
dpred_permeable = simulation_permeable.dpred(conductivity_model)
Case 2: the middle layer is also chargeable (induced polarization)¶
In this case, the middle layer exhibits an induced polarization (IP) response. In SimPEG, we use the Cole-Cole model to characterize the dispersive conductivity, see EM GeoSci:
where:
- ω is the radial frequency (rad/s)
- σ is the electrical conductivity (S/m) at the infinite frequency limit
- is the intrinsic chargeability (unitless)
- is the central time-relaxation constant (s)
- (unitless) defines the distribution of time-relaxation constants. A Debye model is .
When electrical conductivities are model parameters, sigmaMap
sets the mapping from the model to the layer conductivities. In rare instances, electrical conductivities are set directly using the sigma
keyword argument. Unfortunately, SimPEG does not currently have the ability to include the additional parameters used to define dispersive electrical conductivities in the model. As such, these parameters are set as static parameters of the simulation class using the eta
, tau
and C
keyword arguments.
Important: In order for the 1D simulation to model dispersive conductivities correctly, none of the values used to set eta
can be equal to zero, even if the layer is non-chargeable. To model non-chargeable layers, simply set the value to something small.
eta = 0.5 # intrinsic chargeability [0, 1]
tau = 0.001 # central time-relaxation constant in seconds
c = 0.8 # phase constant [0, 1]
layer_eta = np.r_[1e-10, eta, 1e-10]
layer_tau = np.r_[0.0, tau, 0.0]
layer_c = np.r_[0.0, c, 0.0]
simulation_ip = tdem.Simulation1DLayered(
survey=survey,
thicknesses=layer_thicknesses,
sigmaMap=conductivity_map,
eta=layer_eta,
tau=layer_tau,
c=layer_c,
)
dpred_ip = simulation_ip.dpred(conductivity_model)
Case 3: the top layer exhibits superparamagnetism (SPM)¶
In this case, the top layer exhibits superparamagnetism (or viscous remanent magnetization). In SimPEG, we use a log-uniform distribution of time relaxation constants to define the dispersive magnetization, see EM GeoSci:
where:
- ω is the radial frequency (rad/s)
- is the permeability of free space
- μ is the magnetic permeability at the infinite frequency limit
- (unitless) is the DC susceptibility for the SPM effect
- are the lower and upper bounds for the log-uniform distribution of time-relaxation constants
When magnetic permeabilities μ are model parameters, muMap
sets the mapping from the model to the layer permeabilities. In rare instances, magnetic permeabilities are set directly using the mu
keyword argument. Unfortunately, SimPEG does not currently have the ability to include the additional parameters used to define dispersive magnetic permeability in the model. As such, these parameters are set as static parameters of the simulation class using the dchi
, tau1
and tau2
keyword arguments.
chi = 0.01 # infinite susceptibility in SI
dchi = 0.01 # amplitude of frequency-dependent susceptibility contribution
tau1 = 1e-7 # lower limit for time relaxation constants in seconds
tau2 = 1.0 # upper limit for time relaxation constants in seconds
layer_mu = mu_0 * (1 + chi * np.r_[1.0, 0.0, 0.0]) # convert to permeability
layer_dchi = chi * np.r_[1.0, 0.0, 0.0]
layer_tau1 = tau1 * np.r_[1.0, 0.0, 0.0]
layer_tau2 = tau1 * np.r_[1.0, 0.0, 0.0]
simulation_spm = tdem.Simulation1DLayered(
survey=survey,
thicknesses=layer_thicknesses,
sigmaMap=conductivity_map,
mu=mu_0,
dchi=dchi,
tau1=tau1,
tau2=tau2,
)
dpred_spm = simulation_spm.dpred(conductivity_model)
/t40array/ssoler/miniforge3/envs/simpeg-user-tutorials/lib/python3.10/site-packages/numpy/_core/_asarray.py:126: ComplexWarning: Casting complex values to real discards the imaginary part
arr = array(a, dtype=dtype, order=order, copy=None, subok=subok)
fig = plt.figure(figsize=(5, 6))
ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.85])
ax1.loglog(times, np.abs(dpred_conductivity[0 : len(times)]), "k", lw=2)
ax1.loglog(times, np.abs(dpred_permeable[0 : len(times)]), "r", lw=2)
ax1.loglog(times, np.abs(dpred_ip[0 : len(times)]), "b", lw=2)
ax1.loglog(times, np.abs(dpred_spm[0 : len(times)]), "g", lw=2)
ax1.set_xlim([times.min(), times.max()])
ax1.grid()
ax1.legend(
[
"Conductive layer",
"Conductive and permeable",
"Conductive and chargeable",
"Superparamagnetic case",
]
)
ax1.set_xlabel("Times (s)")
ax1.set_ylabel("B (T)")
ax1.set_title("Magnetic Flux Density")
plt.show()
Part 3: Simulation for Different Waveforms¶
In this part of the tutorial, we simulate TDEM data for various transmitter waveforms using a single survey object. As we learned in part 1, each waveform will require the construction of a different source object. For any waveform object, the eval method can be used to evaluate the waveform at the time provided.
Defining Different Waveforms¶
# Rectangular waveform. The user may customize the waveform by setting the start
# time, end time and on time amplitude for the current waveform.
eps = 1e-6
ramp_on = np.r_[-0.004, -0.004 + eps]
ramp_off = np.r_[-eps, 0.0]
rectangular_waveform = tdem.sources.TrapezoidWaveform(
ramp_on=ramp_on, ramp_off=ramp_off
)
# Triangular waveform. The user may customize the waveform by setting the start
# time, peak time, end time and peak amplitude for the current waveform.
eps = 1e-8
start_time = -0.02
peak_time = -0.01
off_time = 0.0
triangle_waveform = tdem.sources.TriangularWaveform(
start_time=start_time, peak_time=peak_time, off_time=off_time
)
# General waveform. This is a fully general way to define the waveform.
# The use simply provides times and the current.
def custom_waveform(t, tmax):
out = np.cos(0.5 * np.pi * (t - tmax) / (tmax + 0.02))
out[t >= tmax] = 1 + (t[t >= tmax] - tmax) / tmax
return out
waveform_times = np.r_[np.linspace(-0.02, -0.011, 10), -np.logspace(-2, -6, 61), 0.0]
waveform_current = custom_waveform(waveform_times, -0.0055)
general_waveform = tdem.sources.PiecewiseLinearWaveform(
times=waveform_times, currents=waveform_current
)
fig = plt.figure(figsize=(6, 4))
ax = fig.add_axes([0.1, 0.1, 0.85, 0.8])
ax.plot(np.r_[-2e-2, 0.0, 1e-10, 1e-3], np.r_[1.0, 1.0, 0.0, 0.0], "k", lw=2)
plotting_current = [rectangular_waveform.eval(t) for t in waveform_times]
ax.plot(waveform_times, plotting_current, "b", lw=2)
plotting_current = [triangle_waveform.eval(t) for t in waveform_times]
ax.plot(waveform_times, plotting_current, "r", lw=2)
plotting_current = [general_waveform.eval(t) for t in waveform_times]
ax.plot(waveform_times, plotting_current, "g", lw=2)
ax.grid()
ax.set_xlim([waveform_times.min(), 1e-3])
ax.set_xlabel("Time (s)")
ax.set_ylabel("Current (A)")
ax.set_title("Waveforms")
ax.legend(["Step-off", "Rectangular", "Triangle", "General"], loc="lower left")
plt.show()
Designing a Survey with Multiple Sources¶
waveforms_list = [
stepoff_waveform,
rectangular_waveform,
triangle_waveform,
general_waveform,
]
source_list_multi = []
for w in waveforms_list:
receiver_list_multi = [
tdem.receivers.PointMagneticFluxDensity(
receiver_locations, times, orientation=receiver_orientation
)
]
source_list_multi.append(
tdem.sources.CircularLoop(
receiver_list=receiver_list_multi,
location=source_location,
waveform=w,
current=source_current,
radius=source_radius,
)
)
# Define the survey
survey_multi = tdem.Survey(source_list_multi)
simulation_waveforms = tdem.Simulation1DLayered(
survey=survey_multi, thicknesses=layer_thicknesses, sigmaMap=conductivity_map
)
dpred_waveforms = simulation_waveforms.dpred(conductivity_model)
fig = plt.figure(figsize=(5, 6))
d = np.reshape(dpred_waveforms, (len(source_list_multi), len(times))).T
ax = fig.add_axes([0.15, 0.15, 0.8, 0.75])
colorlist = ["k", "b", "r", "g"]
for ii, k in enumerate(colorlist):
ax.loglog(times, np.abs(d[:, ii]), k, lw=2)
ax.set_xlim([times.min(), times.max()])
ax.grid()
ax.legend(["Step-off", "Rectangular", "Triangle", "General"])
ax.set_xlabel("Times (s)")
ax.set_ylabel("B (T)")
ax.set_title("Magnetic Flux Density")
plt.show()
Optional: Write data
if write_output:
dir_path = os.path.sep.join([".", "fwd_tdem_1d_outputs"]) + os.path.sep
if not os.path.exists(dir_path):
os.mkdir(dir_path)
rng = np.random.default_rng(seed=347)
noise = rng.normal(
scale=0.05 * np.abs(dpred_conductivity),
size=len(dpred_conductivity),
)
dpred_conductivity += noise
fname = dir_path + "em1dtm_data.txt"
np.savetxt(fname, np.c_[times, dpred_conductivity], fmt="%.4e", header="TIME BZ")