Skip to content

Commit 7dde2a7

Browse files
aignerlukasprisae
authored andcommitted
temfast example
init commit of the temfast example, flake8 test done
1 parent 510d9ed commit 7dde2a7

File tree

1 file changed

+387
-0
lines changed

1 file changed

+387
-0
lines changed
Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
"""
2+
TEM: AEMR TEM-FAST 48 system
3+
=================
4+
**In this example we compute the TEM response from the TEM-FAST 48 system.
5+
6+
This example was contributed by Lukas Aigner (@aignerlukas), who was interested
7+
in modelling the TEM-FAST system, which is used at the TU Wien.
8+
If you are interested and want to use this work please have a look at the
9+
corresponding paper: https://doi.org/10.1016/j.jappgeo.2024.105334
10+
11+
The modeller ``empymod`` models the electromagnetic (EM) full wavefield Greens
12+
function for electric and magnetic point sources and receivers. As such, it can
13+
model any EM method from DC to GPR. However, how to actually implement a
14+
particular EM method and survey layout can be tricky, as there are many more
15+
things involved than just computing the EM Greens function.
16+
17+
What is not included in ``empymod`` at this moment (but hopefully in the
18+
future), but is required to model TEM data, is to **account for arbitrary
19+
source waveform**, and to apply a **lowpass filter**. So we generate these two
20+
things here, and create our own wrapper to model TEM data.
21+
22+
"""
23+
import empymod
24+
import numpy as np
25+
import matplotlib.pyplot as plt
26+
from scipy.special import roots_legendre
27+
from matplotlib.ticker import LogLocator, NullFormatter
28+
from scipy.interpolate import InterpolatedUnivariateSpline as iuSpline
29+
plt.style.use('ggplot')
30+
31+
32+
###############################################################################
33+
# 1. TEM-FAST 48 Waveform and other characteristics
34+
# The TEM-FASt system uses a "time-key" value to determine the number of gates,
35+
# the front ramp and the length of the current pulse.
36+
# We are using values that correspond to a time-key of 5
37+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
38+
turn_on_ramp = -3.0E-06
39+
turn_off_ramp = 0.95E-06
40+
on_time = 3.75E-03
41+
42+
injected_current = 4.1 # Ampere
43+
time_gates = np.r_[4.060e+00, 5.070e+00, 6.070e+00, 7.080e+00,
44+
8.520e+00, 1.053e+01, 1.255e+01, 1.456e+01,
45+
1.744e+01, 2.146e+01, 2.549e+01, 2.950e+01,
46+
3.528e+01, 4.330e+01, 5.140e+01, 5.941e+01, # time-key 1
47+
7.160e+01, 8.760e+01, 1.036e+02, 1.196e+02, # time-key 2
48+
1.436e+02, 1.756e+02, 2.076e+02, 2.396e+02, # time-key 3
49+
2.850e+02, 3.500e+02, 4.140e+02, 4.780e+02, # time-key 4
50+
5.700e+02, 6.990e+02, 8.280e+02, 9.560e+02, # time-key 5
51+
] * 1e-6 # from us to s
52+
53+
waveform_times = np.r_[turn_on_ramp - on_time, -on_time,
54+
0.000E+00, turn_off_ramp]
55+
waveform_current = np.r_[0.0, injected_current, injected_current, 0.0]
56+
57+
plt.figure()
58+
plt.title('Waveform')
59+
plt.plot(np.r_[-9, waveform_times*1e3, 2], np.r_[0, waveform_current, 0])
60+
plt.xlabel('Time (ms)')
61+
plt.xlim([-4, 0.5])
62+
plt.legend()
63+
64+
65+
###############################################################################
66+
# 2. ``empymod`` implementation
67+
# -----------------------------
68+
def waveform(times, resp, times_wanted, wave_time, wave_amp, nquad=3):
69+
"""Apply a source waveform to the signal.
70+
71+
Parameters
72+
----------
73+
times : ndarray
74+
Times of computed input response; should start before and end after
75+
`times_wanted`.
76+
77+
resp : ndarray
78+
EM-response corresponding to `times`.
79+
80+
times_wanted : ndarray
81+
Wanted times.
82+
83+
wave_time : ndarray
84+
Time steps of the wave.
85+
86+
wave_amp : ndarray
87+
Amplitudes of the wave corresponding to `wave_time`, usually
88+
in the range of [0, 1].
89+
90+
nquad : int
91+
Number of Gauss-Legendre points for the integration. Default is 3.
92+
93+
Returns
94+
-------
95+
resp_wanted : ndarray
96+
EM field for `times_wanted`.
97+
98+
"""
99+
100+
# Interpolate on log.
101+
PP = iuSpline(np.log10(times), resp)
102+
103+
# Wave time steps.
104+
dt = np.diff(wave_time)
105+
dI = np.diff(wave_amp)
106+
dIdt = dI/dt
107+
108+
# Gauss-Legendre Quadrature; 3 is generally good enough.
109+
# (Roots/weights could be cached.)
110+
g_x, g_w = roots_legendre(nquad)
111+
112+
# Pre-allocate output.
113+
resp_wanted = np.zeros_like(times_wanted)
114+
115+
# Loop over wave segments.
116+
for i, cdIdt in enumerate(dIdt):
117+
118+
# We only have to consider segments with a change of current.
119+
if cdIdt == 0.0:
120+
continue
121+
122+
# If wanted time is before a wave element, ignore it.
123+
ind_a = wave_time[i] < times_wanted
124+
if ind_a.sum() == 0:
125+
continue
126+
127+
# If wanted time is within a wave element, we cut the element.
128+
ind_b = wave_time[i+1] > times_wanted[ind_a]
129+
130+
# Start and end for this wave-segment for all times.
131+
ta = times_wanted[ind_a]-wave_time[i]
132+
tb = times_wanted[ind_a]-wave_time[i+1]
133+
tb[ind_b] = 0.0 # Cut elements
134+
135+
# Gauss-Legendre for this wave segment. See
136+
# https://en.wikipedia.org/wiki/Gaussian_quadrature#Change_of_interval
137+
# for the change of interval, which makes this a bit more complex.
138+
logt = np.log10(np.outer((tb-ta)/2, g_x)+(ta+tb)[:, None]/2)
139+
fact = (tb-ta)/2*cdIdt
140+
resp_wanted[ind_a] += fact*np.sum(np.array(PP(logt)*g_w), axis=1)
141+
142+
return resp_wanted
143+
144+
145+
###############################################################################
146+
def get_time(time, r_time):
147+
"""Additional time for ramp.
148+
149+
Because of the arbitrary waveform, we need to compute some times before and
150+
after the actually wanted times for interpolation of the waveform.
151+
152+
Some implementation details: The actual times here don't really matter. We
153+
create a vector of time.size+2, so it is similar to the input times and
154+
accounts that it will require a bit earlier and a bit later times. Really
155+
important are only the minimum and maximum times. The Fourier DLF, with
156+
`pts_per_dec=-1`, computes times from minimum to at least the maximum,
157+
where the actual spacing is defined by the filter spacing. It subsequently
158+
interpolates to the wanted times. Afterwards, we interpolate those again to
159+
compute the actual waveform response.
160+
161+
Note: We could first call `waveform`, and get the actually required times
162+
from there. This would make this function obsolete. It would also
163+
avoid the double interpolation, first in `empymod.model.time` for the
164+
Fourier DLF with `pts_per_dec=-1`, and second in `waveform`. Doable.
165+
Probably not or marginally faster. And the code would become much
166+
less readable.
167+
168+
Parameters
169+
----------
170+
time : ndarray
171+
Desired times
172+
173+
r_time : ndarray
174+
Waveform times
175+
176+
Returns
177+
-------
178+
time_req : ndarray
179+
Required times
180+
"""
181+
tmin = np.log10(max(time.min()-r_time.max(), 1e-10))
182+
tmax = np.log10(time.max()-r_time.min())
183+
return np.logspace(tmin, tmax, time.size+2)
184+
185+
186+
###############################################################################
187+
def temfast(off_time, waveform_times, model, square_side=12.5):
188+
"""Custom wrapper of empymod.model.bipole.
189+
190+
Here, we compute TEM-FAST data using the ``empymod.model.bipole`` routine
191+
as an example. This function is based upon the Walk TEM example.
192+
193+
We model the big source square loop by computing only half of one side of
194+
the electric square loop and approximating the finite length dipole with 3
195+
point dipole sources. The result is then multiplied by 8, to account for
196+
all eight half-sides of the square loop.
197+
198+
The implementation here assumes a central loop configuration, where the
199+
receiver (1 m2 area) is at the origin, and the source is a
200+
square_side x square_side m electric loop, centered around the origin.
201+
202+
Note: This approximation of only using half of one of the four sides
203+
obviously only works for central, horizontal square loops. If your
204+
loop is arbitrary rotated, then you have to model all four sides of
205+
the loop and sum it up.
206+
207+
208+
Parameters
209+
----------
210+
off_time : ndarray
211+
times at which the secondary magnetic field will be measured
212+
213+
waveform_times : ndarray
214+
Depths of the resistivity model (see ``empymod.model.bipole`` for more
215+
info.)
216+
217+
depth : ndarray
218+
Depths of the resistivity model (see ``empymod.model.bipole`` for more
219+
info.)
220+
221+
res : ndarray
222+
Resistivities of the resistivity model (see ``empymod.model.bipole``
223+
for more info.)
224+
225+
square_side : float
226+
sige length of the square loop in meter.
227+
228+
Returns
229+
-------
230+
TEM-FAST waveform : EMArray
231+
TEM-FAST response (dB/dt).
232+
233+
"""
234+
235+
if 'm' in model:
236+
depth = model['depth']
237+
res = model
238+
del res['depth']
239+
else:
240+
res = model['res']
241+
depth = model['depth']
242+
243+
# === GET REQUIRED TIMES ===
244+
time = get_time(off_time, waveform_times)
245+
246+
# === GET REQUIRED FREQUENCIES ===
247+
time, freq, ft, ftarg = empymod.utils.check_time(
248+
time=time, # Required times
249+
signal=1, # Switch-on response
250+
ft='dlf', # Use DLF
251+
ftarg={'dlf': 'key_601_2009'},
252+
verb=2,
253+
)
254+
255+
# === COMPUTE FREQUENCY-DOMAIN RESPONSE ===
256+
# We only define a few parameters here. You could extend this for any
257+
# parameter possible to provide to empymod.model.bipole.
258+
hs = square_side / 2 # half side length
259+
EM = empymod.model.bipole(
260+
src=[hs, hs, 0, hs, 0, 0], # El. bipole source; half of one side.
261+
rec=[0, 0, 0, 0, 90], # Receiver at the origin, vertical.
262+
depth=depth, # Depth-model, including air-interface.
263+
res=res, # if with IP, res is a dictionary with
264+
# all params and the function
265+
freqtime=freq, # Required frequencies.
266+
mrec=True, # It is an el. source, but a magn. rec.
267+
strength=8, # To account for 4 sides of square loop.
268+
srcpts=3, # Approx. the finite dip. with 3 points.
269+
htarg={'dlf': 'key_401_2009'}, # Short filter, so fast.
270+
)
271+
272+
# Multiply the frequecny-domain result with
273+
# \mu for H->B, and i\omega for B->dB/dt.
274+
EM *= 2j*np.pi*freq*4e-7*np.pi
275+
276+
# === Butterworth-type filter (implemented from simpegEM1D.Waveforms.py)===
277+
cutofffreq = 1e8 # determined empirically for TEM-FAST
278+
h = (1+1j*freq/cutofffreq)**-1 # First order type
279+
h *= (1+1j*freq/3e5)**-1
280+
EM *= h
281+
282+
# === CONVERT TO TIME DOMAIN ===
283+
delay_rst = 0 # unknown for TEM-FAST, therefore 0
284+
EM, _ = empymod.model.tem(EM[:, None], np.array([1]),
285+
freq, time+delay_rst, 1, ft, ftarg)
286+
EM = np.squeeze(EM)
287+
288+
# === APPLY WAVEFORM ===
289+
return waveform(time, EM, off_time, waveform_times, waveform_current)
290+
291+
292+
###############################################################################
293+
def pelton_res(inp, p_dict):
294+
""" Pelton et al. (1978).
295+
code from: https://empymod.emsig.xyz/en/stable/examples/time_domain/
296+
cole_cole_ip.html#sphx-glr-examples-time-domain-cole-cole-ip-py
297+
"""
298+
299+
# Compute complex resistivity from Pelton et al.
300+
# print('\n shape: p_dict["freq"]\n', p_dict['freq'].shape)
301+
iwtc = np.outer(2j*np.pi*p_dict['freq'], inp['tau'])**inp['c']
302+
303+
rhoH = inp['rho_0'] * (1 - inp['m']*(1 - 1/(1 + iwtc)))
304+
rhoV = rhoH*p_dict['aniso']**2
305+
306+
# Add electric permittivity contribution
307+
etaH = 1/rhoH + 1j*p_dict['etaH'].imag
308+
etaV = 1/rhoV + 1j*p_dict['etaV'].imag
309+
310+
return etaH, etaV
311+
312+
313+
###############################################################################
314+
# 3. Computation non-IP
315+
# --------------
316+
317+
depths = [8, 20]
318+
rhos = [25, 5, 50]
319+
model = {'depth': np.r_[0, depths],
320+
'res': np.r_[2e14, rhos]}
321+
322+
# Compute conductive model
323+
response = temfast(off_time=time_gates, waveform_times=waveform_times,
324+
model=model)
325+
326+
327+
###############################################################################
328+
# 4. Computation with IP
329+
# --------------
330+
depths = [8, 20]
331+
rhos = [25, 5, 50]
332+
charg = np.r_[0, 0.9, 0]
333+
taus = np.r_[1e-6, 5e-4, 1e-6]
334+
cs = np.r_[0, 0.9, 0]
335+
336+
eta_func = pelton_res
337+
depth = np.r_[0, depths]
338+
model = {'depth': depth,
339+
'res': np.r_[2e14, rhos],
340+
'rho_0': np.r_[2e14, rhos],
341+
'm': np.r_[0, charg],
342+
'tau': np.r_[1e-7, taus],
343+
'c': np.r_[0.01, cs],
344+
'func_eta': eta_func}
345+
346+
347+
# Compute conductive model
348+
response_ip = temfast(off_time=time_gates, waveform_times=waveform_times,
349+
model=model)
350+
351+
352+
###############################################################################
353+
# 5. Comparison
354+
# -------------
355+
356+
plt.figure(figsize=(5, 5), constrained_layout=True)
357+
358+
# Plot result of model 1
359+
ax1 = plt.subplot(111)
360+
plt.title('TEM-FAST responses')
361+
362+
# empymod
363+
plt.plot(time_gates, response, 'r.--', ms=7, label="response")
364+
plt.plot(time_gates, abs(response_ip), 'kx:', ms=7, label="response with IP")
365+
366+
sub0 = response_ip[response_ip < 0]
367+
tg_sub0 = time_gates[response_ip < 0]
368+
plt.plot(tg_sub0, abs(sub0), marker='s', ls='none',
369+
mfc='none', ms=8, mew=1,
370+
mec='c', label="negative readings")
371+
372+
# Plot settings
373+
plt.xscale('log')
374+
plt.yscale('log')
375+
plt.xlabel("Time(s)")
376+
plt.ylabel(r"$\mathrm{d}\mathrm{B}_\mathrm{z}\,/\,\mathrm{d}t$")
377+
plt.grid(which='both', c='w')
378+
plt.legend(title='Data', loc=1)
379+
380+
381+
# Force minor ticks on logscale
382+
ax1.yaxis.set_minor_locator(LogLocator(subs='all', numticks=20))
383+
ax1.yaxis.set_minor_formatter(NullFormatter())
384+
plt.grid(which='both', c='w')
385+
386+
###############################################################################
387+
empymod.Report()

0 commit comments

Comments
 (0)