Info, tips & tricks#
There is the usual trade-off between speed, memory, and accuracy. Very generally speaking we can say that the DLF is faster than QWE, but QWE is much easier on memory usage. QWE allows you to control the accuracy. A standard quadrature in the form of QUAD is also provided. QUAD is generally orders of magnitudes slower, and more fragile depending on the input arguments. However, it can provide accurate results where DLF and QWE fail.
Memory#
By default empymod
will try to carry out the computation in one go, without
looping. If your model has many offsets and many frequencies this can be heavy
on memory usage. Even more so if you are computing time-domain responses for
many times. If you are running out of memory, you should use either
loop='off'
or loop='freq'
to loop over offsets or frequencies,
respectively. Use verb=3
to see how many offsets and how many frequencies
are computed internally.
Speed#
Please be aware that the high-level routines empymod.model.bipole
and
empymod.model.loop
are convenience functions, making it easy for the
user to compute arbitrary rotated sources and receivers. However, the
convenience comes at a price, these are certainly not the fastest
implementations for a given scenario. There are simply too many different use
cases, each with its particular layout of sources, receivers, geometrical
factors, required fields, and so on. These convenience functions simply loop
internally over different source and receiver depths, source and receiver
integration points, and required fields. If you are going to model millions and
millions of responses it will be worth to think about it carefully. Often it
will be much faster to collect the same source and receiver depths and call
these functions individually (loop yourself). Or write your own wrapper around
either empymod.model.dipole
, or even around empymod.model.fem
and empymod.model.tem
.
As such, the provided modelling routine can serve as a template to create your own, problem-specific modelling routine!
Depths, Rotation, and Bipole#
Depths: Computation of many source and receiver positions is fastest if they remain at the same depth, as they can be computed in one kernel call. If depths do change, one has to loop over them. Note: Sources or receivers placed on a layer interface are considered in the upper layer.
Rotation: Sources and receivers aligned along the principal axes x, y, and z can be computed in one kernel call. For arbitrary oriented di- or bipoles, 3 kernel calls are required. If source and receiver are arbitrary oriented, 9 (3x3) kernel calls are required.
Bipole: Bipoles increase the computation time by the amount of integration points used. For a source and a receiver bipole with each 5 integration points you need 25 (5x5) kernel calls. You can compute it in 1 kernel call if you set both integration points to 1, and therefore compute the bipole as if they were dipoles at their centre.
Example: For 1 source and 10 receivers, all at the same depth, 1 kernel call is required. If all receivers are at different depths, 10 kernel calls are required. If you make source and receivers bipoles with 5 integration points, 250 kernel calls are required. If you rotate the source arbitrary horizontally, 500 kernel calls are required. If you rotate the receivers too, in the horizontal plane, 1’000 kernel calls are required. If you rotate the receivers also vertically, 1’500 kernel calls are required. If you rotate the source vertically too, 2’250 kernel calls are required. So your computation will take 2’250 times longer! No matter how fast the kernel is, this will take a long time. Therefore carefully plan how precise you want to define your source and receiver bipoles.
source bipole |
receiver bipole |
||||||
---|---|---|---|---|---|---|---|
kernel calls |
intpts |
azimuth |
dip |
intpts |
azimuth |
dip |
diff. z |
1 |
1 |
0/90 |
0/90 |
1 |
0/90 |
0/90 |
1 |
10 |
1 |
0/90 |
0/90 |
1 |
0/90 |
0/90 |
10 |
250 |
5 |
0/90 |
0/90 |
5 |
0/90 |
0/90 |
10 |
500 |
5 |
arb. |
0/90 |
5 |
0/90 |
0/90 |
10 |
1000 |
5 |
arb. |
0/90 |
5 |
arb. |
0/90 |
10 |
1500 |
5 |
arb. |
0/90 |
5 |
arb. |
arb. |
10 |
2250 |
5 |
arb. |
arb. |
5 |
arb. |
arb. |
10 |
Lagged Convolution and Splined Transforms#
Both Hankel and Fourier DLF have three options, which can be controlled via the
htarg['pts_per_dec']
and ftarg['pts_per_dec']
parameters:
pts_per_dec=0
: Standard DLF;
pts_per_dec<0
: Lagged Convolution DLF: Spacing defined by filter base, interpolation is carried out in the input domain;
pts_per_dec>0
: Splined DLF: Spacing defined bypts_per_dec
, interpolation is carried out in the output domain.
Similarly, interpolation can be used for QWE
by setting pts_per_dec
to
a value bigger than 0.
The Lagged Convolution and Splined options should be used with caution, as they
use interpolation and are therefore less precise than the standard version.
However, they can significantly speed up QWE, and massively speed up DLF.
Additionally, the interpolated versions minimizes memory requirements a lot.
Speed-up is greater if all source-receiver angles are identical. Note that
setting pts_per_dec
to something else than 0 to compute only one offset
(Hankel) or only one time (Fourier) will be slower than using the standard
version.
QWE: Good speed-up is also achieved for QWE by setting maxint
as low as
possible. Also, the higher nquad
is, the higher the speed-up will be.
DLF: Big improvements are achieved for long DLF-filters and for many offsets/frequencies (thousands).
Warning
Keep in mind that setting pts_per_dec
to something else than 0 uses
interpolation, and is therefore not as accurate as the standard version.
Use with caution and always compare with the standard version to verify if
you can apply interpolation to your problem at hand!
Be aware that QUAD (Hankel transform) always use the splined version and always loops over offsets. The Fourier transforms FFTlog, QWE, and FFT always use interpolation too, either in the frequency or in the time domain.
The splined versions of QWE check whether the ratio of any two adjacent
intervals is above a certain threshold (steep end of the wavenumber or
frequency spectrum). If it is, it carries out QUAD for this interval instead
of QWE. The threshold is stored in diff_quad
, which can be changed within
the parameter htarg
and ftarg
.
For a graphical explanation of the differences between standard DLF, lagged convolution DLF, and splined DLF for the Hankel and the Fourier transforms see the example Digital Linear Filters.
Looping#
By default, you can compute many offsets and many frequencies all in one go,
vectorized (for the DLF), which is the default. The loop
parameter gives
you the possibility to force looping over frequencies or offsets. This
parameter can have severe effects on both runtime and memory usage. Play around
with this factor to find the fastest version for your problem at hand. It
ALWAYS loops over frequencies if ht = 'QWE'/'QUAD'
or if ht = 'DLF'
and
pts_per_dec!=0
(Lagged Convolution or Splined Hankel DLF). All vectorized
is very fast if there are few offsets or few frequencies. If there are many
offsets and many frequencies, looping over the smaller of the two will be
faster. Choosing the right looping can have a significant influence.
Vertical components and xdirect
#
Computing the direct field in the wavenumber-frequency domain
(xdirect=False
; the default) is generally faster than computing it in the
frequency-space domain (xdirect=True
).
However, using xdirect = True
can improve the result (if source and
receiver are in the same layer) to compute:
the vertical electric field due to a vertical electric source,
configurations that involve vertical magnetic components (source or receiver),
all configurations when source and receiver depth are exactly the same.
The Hankel transforms methods are having sometimes difficulties transforming these functions.
Time-domain land CSEM#
The derivation, as it stands, has a near-singular behaviour in the
wavenumber-frequency domain when \(\kappa^2 = \omega^2\epsilon\mu\). This
can be a problem for land-domain CSEM computations if source and receiver are
located at the surface between air and subsurface. Because most transforms do
not sample the wavenumber-frequency domain sufficiently to catch this
near-singular behaviour (hence not smooth), which then creates noise at early
times where the signal should be zero. To avoid the issue simply set the
relative electric permittivity (epermH
, epermV
) of the air to zero.
This trick obviously uses the diffusive approximation for the air-layer, it
therefore will not work for very high frequencies (e.g., GPR computations).
An example is given in Improve land CSEM computation.
This trick works fine for all horizontal components, but not so much for the
vertical component. But then it is not feasible to have a vertical source or
receiver exactly at the surface. A few tips for these cases: The receiver can
be put pretty close to the surface (a few millimeters), but the source has to
be put down a meter or two, more for the case of vertical source AND receiver,
less for vertical source OR receiver. The results are generally better if the
source is put deeper than the receiver. In either case, the best is to first
test the survey layout against the analytical result (using
empymod.analytical
with solution='dhs'
) for a half-space, and
subsequently model more complex cases.
A common alternative to this trick is to apply a lowpass filter to filter out the unstable high frequencies.
Hook for user-defined computation of \(\eta\) and \(\zeta\)#
In principal it is always best to write your own modelling routine if you want
to adjust something. Just copy empymod.dipole
or empymod.bipole
as a
template, and modify it to your needs. Since empymod v1.7.4
, however, there
is a hook which allows you to modify \(\eta_h, \eta_v, \zeta_h\), and
\(\zeta_v\) quite easily.
The trick is to provide a dictionary (we name it inp
here) instead of the
resistivity vector in res
. This dictionary, inp
, has two mandatory plus
optional entries:
res
: the resistivity vector you would have provided normally (mandatory).A function name, which has to be either or both of (mandatory)
func_eta
: To adjustetaH
andetaV
, orfunc_zeta
: to adjustzetaH
andzetaV
.
In addition, you have to provide all parameters you use in
func_eta
/func_zeta
and are not already provided toempymod
. All additional parameters must have #layers elements.
The functions func_eta
and func_zeta
must have the following
characteristics:
The signature is
func(inp, p_dict)
, whereinp
is the dictionary you provide, andp_dict
is a dictionary that contains all parameters so far computed in empymod [locals()
].
It must return
etaH, etaV
iffunc_eta
, orzetaH, zetaV
iffunc_zeta
.
Dummy example
def my_new_eta(inp, p_dict):
# Your computations, using the parameters you provided
# in `inp` and the parameters from empymod in `p_dict`.
# In the example line below, we provide, e.g., inp['tau']
return etaH, etaV
And then you call empymod
with res={'res': res-array, 'tau': tau,
'func_eta': my_new_eta}
.
Have a look at the corresponding example in the Gallery, where this hook is exploited in the low-frequency range to use the Cole-Cole model for IP computation. It could also be used in the high-frequency range to model dielectricity.
Zero horizontal offset#
By default, empymod
enforces a minimum horizontal offset of 1 mm. The
reason for this lies in the Hankel transform. The digital linear filter method
computes the required wavenumbers via
where \(b_n\) are the base values of the filter, and \(r\) is the horizontal offset. It can be seen from Equation (1) that this breaks down for a zero horizontal offset (something similar applies for the QWE Hankel transform method).
However, the quadrature method for the Hankel transform as well as the analytical solutions do not have this limitation, and both can be used to compute actual zero horizontal offset responses. One can set the minimum (horizontal) offset to zero (or any other value) by running
empymod.set_minimum(min_off=0)
So if you have to compute actual zero horizontal offset data you have to use the quadrature method (ht=’quad’). However, be aware that this method is usually significantly slower than the DLF method, and needs careful adjustments of the htarg-parameters depending on the model and the survey layout.
There exist probably clever workarounds to this limitation of the DLF. However, depending on the source-receiver configuration a minimum offset of one to ten millimeters is generally enough to give a sufficiently precise approximation of the actual zero-offset response, at least for practical purposes.
Here is a script that computes the responses for all possible source-receiver configurations for a fullspace, comparing the analytical space-frequency domain solution with the solutions using the quadrature and using the DLF for the Hankel transform. The analytical solution and the quadrature transform compute the zero offset explicitly, the DLF transform has a minimum offset of 1 mm. You can adjust it to your model and survey layout.
import empymod
import numpy as np
import matplotlib.pyplot as plt
xy = np.arange(1001.)/500-1 # x=y-offsets
off = np.sign(xy)*np.sqrt(2*xy**2) # Offset
res = 1 # Fullspace resistivity
zoff = 1 # Vertical distance
freq = 1 # Frequency
# Collect input
inp = {'src': [0, 0, 0], 'rec': [xy, xy, zoff], 'depth': [],
'res': res, 'freqtime': freq, 'verb': 2}
pab = [11, 12, 13, 14, 15, 16, 21, 22, 23, 24, 25, 26,
31, 32, 33, 34, 35, 36, 41, 42, 43, 44, 45, 46,
51, 52, 53, 54, 55, 56, 61, 62, 63, 64, 65, 66]
# Loop over all source-receiver combinations
for ab in pab:
# Enforce minimum offset
empymod.set_minimum(min_off=1e-3)
print(' --- DLF ---')
num = empymod.dipole(
ab=ab, xdirect=False, htarg={'pts_per_dec': 0}, **inp)
# Remove minimum offset
empymod.set_minimum(min_off=0)
print(' --- QUAD ---')
qua = empymod.dipole(
ab=ab, xdirect=False, ht='quad', **inp,
htarg={'a': 1e-3, 'b': 5e1, 'rtol': 1e-4, 'pts_per_dec': 100})
print(' --- Analytical ---')
ana = empymod.dipole(ab=ab, xdirect=True, **inp)
# Plot the result
plt.figure(num=ab)
plt.suptitle(f"ab = {ab}")
ax1 = plt.subplot(221)
plt.title('Real')
plt.ylabel('E-field (V/m)')
plt.plot(off, ana.real, 'k-')
plt.plot(off, qua.real, 'C0--')
plt.plot(off, num.real, 'C1-.')
plt.xticks([-1, -0.5, 0, 0.5, 1], ())
ax3 = plt.subplot(223)
plt.xlabel('Offset (m)')
plt.ylabel('Rel. Error (%)')
plt.plot(off, 100*abs((qua.real-ana.real)/ana.real), 'C0--')
plt.plot(off, 100*abs((num.real-ana.real)/ana.real), 'C1-.')
plt.yscale('log')
ax2 = plt.subplot(222, sharey=ax1)
plt.title('Imag')
plt.plot(off, ana.imag, 'k-', label='analytical')
plt.plot(off, qua.imag, 'C0--', label='QUAD')
plt.plot(off, num.imag, 'C1-.', label='DLF')
ax2.yaxis.set_label_position("right")
ax2.yaxis.tick_right()
plt.xticks([-1, -0.5, 0, 0.5, 1], ())
plt.legend()
ax4 = plt.subplot(224, sharey=ax3)
plt.xlabel('Offset (m)')
plt.plot(off, 100*abs((qua.imag-ana.imag)/ana.imag), 'C0--')
plt.plot(off, 100*abs((num.imag-ana.imag)/ana.imag), 'C1-.')
ax4.yaxis.set_label_position("right")
ax4.yaxis.tick_right()
plt.yscale('log')
plt.tight_layout()
plt.show()
The result for x-directed source and receiver (ab=11) is shown in the following figure: