Source code for empymod.kernel

r"""

:mod:`kernel` -- Kernel calculation
===================================

Kernel of ``empymod``, calculates the wavenumber-domain electromagnetic
response. Plus analytical full- and half-space solutions.

The functions ``wavenumber``, ``angle_factor``, ``fullspace``, ``greenfct``,
``reflections``, and ``fields`` are based on source files (specified in each
function) from the source code distributed with [HuTS15]_, which can be found
at `software.seg.org/2015/0001 <http://software.seg.org/2015/0001>`_.  These
functions are (c) 2015 by Hunziker et al. and the Society of Exploration
Geophysicists, http://software.seg.org/disclaimer.txt.  Please read the
NOTICE-file in the root directory for more information regarding the involved
licenses.

"""
# Copyright 2016-2019 Dieter Werthmüller
#
# This file is part of empymod.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License.  You may obtain a copy
# of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations under
# the License.


import numpy as np
from scipy import special  # Only used for halfspace solution

np.seterr(all='ignore')

__all__ = ['wavenumber', 'angle_factor', 'fullspace', 'greenfct',
           'reflections', 'fields', 'halfspace']


# Wavenumber-frequency domain kernel

[docs]def wavenumber(zsrc, zrec, lsrc, lrec, depth, etaH, etaV, zetaH, zetaV, lambd, ab, xdirect, msrc, mrec, use_ne_eval): r"""Calculate wavenumber domain solution. Return the wavenumber domain solutions ``PJ0``, ``PJ1``, and ``PJ0b``, which have to be transformed with a Hankel transform to the frequency domain. ``PJ0``/``PJ0b`` and ``PJ1`` have to be transformed with Bessel functions of order 0 (:math:`J_0`) and 1 (:math:`J_1`), respectively. This function corresponds loosely to equations 105--107, 111--116, 119--121, and 123--128 in [HuTS15]_, and equally loosely to the file ``kxwmod.c``. [HuTS15]_ uses Bessel functions of orders 0, 1, and 2 (:math:`J_0, J_1, J_2`). The implementations of the *Fast Hankel Transform* and the *Quadrature-with-Extrapolation* in ``transform`` are set-up with Bessel functions of order 0 and 1 only. This is achieved by applying the recurrence formula .. math:: J_2(kr) = \frac{2}{kr} J_1(kr) - J_0(kr) \ . .. note:: ``PJ0`` and ``PJ0b`` could theoretically be added here into one, and then be transformed in one go. However, ``PJ0b`` has to be multiplied by ``factAng`` later. This has to be done after the Hankel transform for methods which make use of spline interpolation, in order to work for offsets that are not in line with each other. This function is called from one of the Hankel functions in :mod:`transform`. Consult the modelling routines in :mod:`model` for a description of the input and output parameters. If you are solely interested in the wavenumber-domain solution you can call this function directly. However, you have to make sure all input arguments are correct, as no checks are carried out here. """ # ** CALCULATE GREEN'S FUNCTIONS # Shape of PTM, PTE: (nfreq, noffs, nfilt) PTM, PTE = greenfct(zsrc, zrec, lsrc, lrec, depth, etaH, etaV, zetaH, zetaV, lambd, ab, xdirect, msrc, mrec, use_ne_eval) # ** AB-SPECIFIC COLLECTION OF PJ0, PJ1, AND PJ0b # Pre-allocate output PJ0 = None PJ1 = None PJ0b = None # Calculate Ptot which is used in all cases Ptot = (PTM + PTE)/(4*np.pi) # If rec is magnetic switch sign (reciprocity MM/ME => EE/EM). if mrec: sign = -1 else: sign = 1 # Group into PJ0 and PJ1 for J0/J1 Hankel Transform if ab in [11, 12, 21, 22, 14, 24, 15, 25]: # Eqs 105, 106, 111, 112, # J2(kr) = 2/(kr)*J1(kr) - J0(kr) # 119, 120, 123, 124 if ab in [14, 22]: sign *= -1 PJ0b = sign/2*Ptot*lambd PJ1 = -sign*Ptot if ab in [11, 22, 24, 15]: if ab in [22, 24]: sign *= -1 PJ0 = sign*(PTM - PTE)/(8*np.pi)*lambd elif ab in [13, 23, 31, 32, 34, 35, 16, 26]: # Eqs 107, 113, 114, 115, PJ1 = sign*Ptot*lambd*lambd # . 121, 125, 126, 127 if ab in [34, 26]: PJ1 *= -1 elif ab in [33, ]: # Eq 116 PJ0 = sign*Ptot*lambd*lambd*lambd # Return PJ0, PJ1, PJ0b return PJ0, PJ1, PJ0b
[docs]def greenfct(zsrc, zrec, lsrc, lrec, depth, etaH, etaV, zetaH, zetaV, lambd, ab, xdirect, msrc, mrec, use_ne_eval): r"""Calculate Green's function for TM and TE. .. math:: \tilde{g}^{tm}_{hh}, \tilde{g}^{tm}_{hz}, \tilde{g}^{tm}_{zh}, \tilde{g}^{tm}_{zz}, \tilde{g}^{te}_{hh}, \tilde{g}^{te}_{zz} This function corresponds to equations 108--110, 117/118, 122; 89--94, A18--A23, B13--B15; 97--102 A26--A31, and B16--B18 in [HuTS15]_, and loosely to the corresponding files ``Gamma.F90``, ``Wprop.F90``, ``Ptotalx.F90``, ``Ptotalxm.F90``, ``Ptotaly.F90``, ``Ptotalym.F90``, ``Ptotalz.F90``, and ``Ptotalzm.F90``. The Green's functions are multiplied according to Eqs 105-107, 111-116, 119-121, 123-128; with the factors inside the integrals. This function is called from the function :mod:`kernel.wavenumber`. """ # GTM/GTE have shape (frequency, offset, lambda). # gamTM/gamTE have shape (frequency, offset, layer, lambda): # Reciprocity switches for magnetic receivers if mrec: if msrc: # If src is also magnetic, switch eta and zeta (MM => EE). # G^mm_ab(s, r, e, z) = -G^ee_ab(s, r, -z, -e) etaH, zetaH = -zetaH, -etaH etaV, zetaV = -zetaV, -etaV else: # If src is electric, swap src and rec (ME => EM). # G^me_ab(s, r, e, z) = -G^em_ba(r, s, e, z) zsrc, zrec = zrec, zsrc lsrc, lrec = lrec, lsrc for TM in [True, False]: # Continue if Green's function not required if TM and ab in [16, 26]: continue elif not TM and ab in [13, 23, 31, 32, 33, 34, 35]: continue # Define eta/zeta depending if TM or TE if TM: e_zH, e_zV, z_eH = etaH, etaV, zetaH # TM: zetaV not used else: e_zH, e_zV, z_eH = zetaH, zetaV, etaH # TE: etaV not used # Uppercase gamma if use_ne_eval: ez_ratio = (e_zH/e_zV)[:, None, :, None] # NOQA ez_prod = (z_eH*e_zH)[:, None, :, None] # NOQA lambd2 = use_ne_eval("lambd*lambd")[None, :, None, :] # NOQA Gam = use_ne_eval("sqrt(ez_ratio*lambd2 + ez_prod)") else: Gam = np.sqrt((e_zH/e_zV)[:, None, :, None] * (lambd*lambd)[None, :, None, :] + (z_eH*e_zH)[:, None, :, None]) # Gamma in receiver layer lrecGam = Gam[:, :, lrec, :] # Reflection (coming from below (Rp) and above (Rm) rec) if depth.size > 1: # Only if more than 1 layer Rp, Rm = reflections(depth, e_zH, Gam, lrec, lsrc, use_ne_eval) # Field propagators # (Up- (Wu) and downgoing (Wd), in rec layer); Eq 74 if lrec != depth.size-1: # No upgoing field prop. if rec in last ddepth = depth[lrec + 1] - zrec if use_ne_eval: Wu = use_ne_eval("exp(-lrecGam*ddepth)") else: Wu = np.exp(-lrecGam*ddepth) else: Wu = np.zeros_like(lrecGam) if lrec != 0: # No downgoing field propagator if rec in first ddepth = zrec - depth[lrec] if use_ne_eval: Wd = use_ne_eval("exp(-lrecGam*ddepth)") else: Wd = np.exp(-lrecGam*ddepth) else: Wd = np.zeros_like(lrecGam) # Field at rec level (coming from below (Pu) and above (Pd) rec) Pu, Pd = fields(depth, Rp, Rm, Gam, lrec, lsrc, zsrc, ab, TM, use_ne_eval) # Green's functions if lsrc == lrec: # Rec in src layer; Eqs 108, 109, 110, 117, 118, 122 # Green's function depending on <ab> if depth.size == 1: # If only one layer, no reflections/fields green = np.zeros_like(lrecGam) elif ab in [13, 23, 31, 32, 14, 24, 15, 25]: green = Pu*Wu - Pd*Wd else: green = Pu*Wu + Pd*Wd # Direct field, if it is computed in the wavenumber domain if not xdirect: # Direct field directf = np.exp(-lrecGam*abs(zsrc - zrec)) # Swap TM for certain <ab> if TM and ab in [11, 12, 13, 14, 15, 21, 22, 23, 24, 25]: directf *= -1 # Multiply by zrec-zsrc-sign for certain <ab> if ab in [13, 14, 15, 23, 24, 25, 31, 32]: directf *= np.sign(zrec - zsrc) # Add direct field to Green's function green += directf else: # Calculate exponential factor if lrec == depth.size-1: ddepth = 0 else: ddepth = depth[lrec+1] - depth[lrec] if use_ne_eval: fexp = use_ne_eval("exp(-lrecGam*ddepth)") else: fexp = np.exp(-lrecGam*ddepth) # Sign-switch for Green calculation if TM and ab in [11, 12, 13, 21, 22, 23, 14, 24, 15, 25]: pmw = -1 else: pmw = 1 if lrec < lsrc: # Rec above src layer: Pd not used # Eqs 89-94, A18-A23, B13-B15 green = Pu*(Wu + pmw*Rm[:, :, 0, :]*fexp*Wd) elif lrec > lsrc: # rec below src layer: Pu not used # Eqs 97-102 A26-A30, B16-B18 green = Pd*(pmw*Wd + Rp[:, :, abs(lsrc-lrec), :]*fexp*Wu) # Store in corresponding variable if TM: gamTM, GTM = Gam, green else: gamTE, GTE = Gam, green # ** AB-SPECIFIC FACTORS AND CALCULATION OF PTOT'S # These are the factors inside the integrals # Eqs 105-107, 111-116, 119-121, 123-128 # PTM, PTE if ab in [11, 12, 21, 22]: PTM = GTM*gamTM[:, :, lrec, :]/etaH[:, None, lrec, None] PTE = zetaH[:, None, lsrc, None]*GTE/gamTE[:, :, lsrc, :] elif ab in [14, 15, 24, 25]: PTM = ((etaH[:, lsrc]/etaH[:, lrec])[:, None, None] * GTM*gamTM[:, :, lrec, :]/gamTM[:, :, lsrc, :]) PTE = GTE elif ab in [13, 23]: PTM = -((etaH[:, lsrc]/etaH[:, lrec]/etaV[:, lsrc])[:, None, None] * GTM*gamTM[:, :, lrec, :]/gamTM[:, :, lsrc, :]) PTE = 0 elif ab in [31, 32]: PTM = GTM/etaV[:, None, lrec, None] PTE = 0 elif ab in [34, 35]: PTM = ((etaH[:, lsrc]/etaV[:, lrec])[:, None, None] * GTM/gamTM[:, :, lsrc, :]) PTE = 0 elif ab in [16, 26]: PTM = 0 PTE = ((zetaH[:, lsrc]/zetaV[:, lsrc])[:, None, None] * GTE/gamTE[:, :, lsrc, :]) elif ab in [33, ]: PTM = ((etaH[:, lsrc]/etaV[:, lsrc]/etaV[:, lrec])[:, None, None] * GTM/gamTM[:, :, lsrc, :]) PTE = 0 # Return Green's functions return PTM, PTE
[docs]def reflections(depth, e_zH, Gam, lrec, lsrc, use_ne_eval): r"""Calculate Rp, Rm. .. math:: R^\pm_n, \bar{R}^\pm_n This function corresponds to equations 64/65 and A-11/A-12 in [HuTS15]_, and loosely to the corresponding files ``Rmin.F90`` and ``Rplus.F90``. This function is called from the function :mod:`kernel.greenfct`. """ # Loop over Rp, Rm for plus in [True, False]: # Switches depending if plus or minus if plus: pm = 1 layer_count = np.arange(depth.size-2, min(lrec, lsrc)-1, -1) izout = abs(lsrc-lrec) minmax = max(lrec, lsrc) else: pm = -1 layer_count = np.arange(1, max(lrec, lsrc)+1, 1) izout = 0 minmax = -min(lrec, lsrc) # If rec in last and rec below src (plus) or # if rec in first and rec above src (minus), shift izout shiftplus = lrec < lsrc and lrec == 0 and not plus shiftminus = lrec > lsrc and lrec == depth.size-1 and plus if shiftplus or shiftminus: izout -= pm # Pre-allocate Ref Ref = np.zeros((Gam.shape[0], Gam.shape[1], abs(lsrc-lrec)+1, Gam.shape[3]), dtype=Gam.dtype) # Calculate the reflection for iz in layer_count: # Eqs 65, A-12 e_zHa = e_zH[:, None, iz+pm, None] Gama = Gam[:, :, iz, :] e_zHb = e_zH[:, None, iz, None] Gamb = Gam[:, :, iz+pm, :] if use_ne_eval: rlocstr = "(e_zHa*Gama - e_zHb*Gamb)/(e_zHa*Gama + e_zHb*Gamb)" rloc = use_ne_eval(rlocstr) else: rloca = e_zHa*Gama rlocb = e_zHb*Gamb rloc = (rloca - rlocb)/(rloca + rlocb) # In first layer tRef = rloc if iz == layer_count[0]: tRef = rloc.copy() else: ddepth = depth[iz+1+pm]-depth[iz+pm] # Eqs 64, A-11 if use_ne_eval: term = use_ne_eval("tRef*exp(-2*Gamb*ddepth)") tRef = use_ne_eval("(rloc + term)/(1 + rloc*term)") else: term = tRef*np.exp(-2*Gamb*ddepth) # NOQA tRef = (rloc + term)/(1 + rloc*term) # The global reflection coefficient is given back for all layers # between and including src- and rec-layer if lrec != lsrc and pm*iz <= minmax: Ref[:, :, izout, :] = tRef[:] izout -= pm # If lsrc = lrec, we just store the last values if lsrc == lrec and layer_count.size > 0: Ref = tRef # Store Ref in Rm/Rp if plus: Rm = Ref else: Rp = Ref # Return reflections (minus and plus) return Rm, Rp
[docs]def fields(depth, Rp, Rm, Gam, lrec, lsrc, zsrc, ab, TM, use_ne_eval): r"""Calculate Pu+, Pu-, Pd+, Pd-. .. math:: P^{u\pm}_s, P^{d\pm}_s, \bar{P}^{u\pm}_s, \bar{P}^{d\pm}_s; P^{u\pm}_{s-1}, P^{u\pm}_n, \bar{P}^{u\pm}_{s-1}, \bar{P}^{u\pm}_n; P^{d\pm}_{s+1}, P^{d\pm}_n, \bar{P}^{d\pm}_{s+1}, \bar{P}^{d\pm}_n This function corresponds to equations 81/82, 95/96, 103/104, A-8/A-9, A-24/A-25, and A-32/A-33 in [HuTS15]_, and loosely to the corresponding files ``Pdownmin.F90``, ``Pdownplus.F90``, ``Pupmin.F90``, and ``Pdownmin.F90``. This function is called from the function :mod:`kernel.greenfct`. """ # Variables nlsr = abs(lsrc-lrec)+1 # nr of layers btw and incl. src and rec layer rsrcl = 0 # src-layer in reflection (Rp/Rm), first if down izrange = range(2, nlsr) isr = lsrc last = depth.size-1 # Booleans if src in first or last layer; swapped if up=True first_layer = lsrc == 0 last_layer = lsrc == depth.size-1 # Depths; dp and dm are swapped if up=True if lsrc != depth.size-1: ds = depth[lsrc+1]-depth[lsrc] dp = depth[lsrc+1]-zsrc dm = zsrc-depth[lsrc] # Rm and Rp; swapped if up=True Rmp = Rm Rpm = Rp # Boolean if plus or minus has to be calculated plusset = [13, 23, 33, 14, 24, 34, 15, 25, 35] if TM: plus = ab in plusset else: plus = ab not in plusset # Sign-switches pm = 1 # + if plus=True, - if plus=False if not plus: pm = -1 pup = -1 # + if up=True, - if up=False mupm = 1 # + except if up=True and plus=False # Gamma of source layer iGam = Gam[:, :, lsrc, :] # Calculate down- and up-going fields for up in [False, True]: # No upgoing field if rec is in last layer or below src if up and (lrec == depth.size-1 or lrec > lsrc): Pu = np.zeros_like(iGam) continue # No downgoing field if rec is in first layer or above src if not up and (lrec == 0 or lrec < lsrc): Pd = np.zeros_like(iGam) continue # Swaps if up=True if up: if not last_layer: dp, dm = dm, dp else: dp = dm Rmp, Rpm = Rpm, Rmp first_layer, last_layer = last_layer, first_layer rsrcl = nlsr-1 # src-layer in refl. (Rp/Rm), last (nlsr-1) if up izrange = range(nlsr-2) isr = lrec last = 0 pup = 1 if not plus: mupm = -1 # Calculate Pu+, Pu-, Pd+, Pd- if lsrc == lrec: # rec in src layer; Eqs 81/82, A-8/A-9 if last_layer: # If src/rec are in top (up) or bottom (down) layer if use_ne_eval: P = use_ne_eval("Rmp*exp(-iGam*dm)") else: P = Rmp*np.exp(-iGam*dm) else: # If src and rec are in any layer in between if use_ne_eval: Pstr = "(exp(-iGam*dm) + pm*Rpm*exp(-iGam*(ds+dp)))" Pstr += "* Rmp/(1-Rmp*Rpm*exp(-2*iGam*ds))" P = use_ne_eval(Pstr) else: P = np.exp(-iGam*dm) + pm*Rpm*np.exp(-iGam*(ds+dp)) P *= Rmp/(1 - Rmp*Rpm*np.exp(-2*iGam*ds)) else: # rec above (up) / below (down) src layer # # Eqs 95/96, A-24/A-25 for rec above src layer # # Eqs 103/104, A-32/A-33 for rec below src layer # First compute P_{s-1} (up) / P_{s+1} (down) iRpm = Rpm[:, :, rsrcl, :] if first_layer: # If src is in bottom (up) / top (down) layer if use_ne_eval: P = use_ne_eval("(1 + iRpm)*mupm*exp(-iGam*dp)") else: P = (1 + iRpm)*mupm*np.exp(-iGam*dp) else: iRmp = Rmp[:, :, rsrcl, :] if use_ne_eval: Pstr = "(mupm*exp(-iGam*dp) + " Pstr += "pm*mupm*iRmp*exp(-iGam*(ds+dm)))" Pstr += "*(1 + iRpm)/(1 - iRmp*iRpm * exp(-2*iGam*ds))" P = use_ne_eval(Pstr) else: P = mupm*np.exp(-iGam*dp) P += pm*mupm*iRmp*np.exp(-iGam * (ds+dm)) P *= (1 + iRpm)/(1 - iRmp*iRpm * np.exp(-2*iGam*ds)) # If up or down and src is in last but one layer if up or (not up and lsrc+1 < depth.size-1): ddepth = depth[lsrc+1-1*pup]-depth[lsrc-1*pup] iRpm = Rpm[:, :, rsrcl-1*pup, :] miGam = Gam[:, :, lsrc-1*pup, :] if use_ne_eval: P = use_ne_eval("P/(1 + iRpm*exp(-2*miGam * ddepth))") else: P /= (1 + iRpm*np.exp(-2*miGam * ddepth)) # Second compute P for all other layers if nlsr > 2: for iz in izrange: ddepth = depth[isr+iz+pup+1]-depth[isr+iz+pup] iRpm = Rpm[:, :, iz+pup, :] piGam = Gam[:, :, isr+iz+pup, :] if use_ne_eval: P = use_ne_eval("P*(1 + iRpm)*exp(-piGam * ddepth)") else: P *= (1 + iRpm)*np.exp(-piGam * ddepth) # If rec/src NOT in first/last layer (up/down) if isr+iz != last: ddepth = depth[isr+iz+1] - depth[isr+iz] iRpm = Rpm[:, :, iz, :] piGam2 = Gam[:, :, isr+iz, :] if use_ne_eval: Pstr = "P/(1 + iRpm*exp(-2*piGam2 * ddepth))" P = use_ne_eval(Pstr) else: P /= 1 + iRpm*np.exp(-2*piGam2 * ddepth) # Store P in Pu/Pd if up: Pu = P else: Pd = P # Return fields (up- and downgoing) return Pu, Pd
# Angle Factor
[docs]def angle_factor(angle, ab, msrc, mrec): r"""Return the angle-dependent factor. The whole calculation in the wavenumber domain is only a function of the distance between the source and the receiver, it is independent of the angel. The angle-dependency is this factor, which can be applied to the corresponding parts in the wavenumber or in the frequency domain. The ``angle_factor`` corresponds to the sine and cosine-functions in Eqs 105-107, 111-116, 119-121, 123-128. This function is called from one of the Hankel functions in :mod:`transform`. Consult the modelling routines in :mod:`model` for a description of the input and output parameters. """ # 33/66 are completely symmetric and hence independent of angle if ab in [33, ]: return np.ones(angle.size) # Evaluation angle eval_angle = angle.copy() # Add pi if receiver is magnetic (reciprocity), but not if source is # electric, because then source and receiver are swapped, ME => EM: # G^me_ab(s, r, e, z) = -G^em_ba(r, s, e, z). if mrec and not msrc: eval_angle += np.pi # Define fct (cos/sin) and angles to be tested if ab in [11, 22, 15, 24, 13, 31, 26, 35]: fct = np.cos test_ang_1 = np.pi/2 test_ang_2 = 3*np.pi/2 else: fct = np.sin test_ang_1 = np.pi test_ang_2 = 2*np.pi if ab in [11, 22, 15, 24, 12, 21, 14, 25]: eval_angle *= 2 # Get factor factAng = fct(eval_angle) # Ensure cos([pi/2, 3pi/2]) and sin([pi, 2pi]) are zero (floating pt issue) factAng[np.isclose(np.abs(eval_angle), test_ang_1, 1e-10, 1e-14)] = 0 factAng[np.isclose(np.abs(eval_angle), test_ang_2, 1e-10, 1e-14)] = 0 return factAng
# Analytical solutions
[docs]def fullspace(off, angle, zsrc, zrec, etaH, etaV, zetaH, zetaV, ab, msrc, mrec): r"""Analytical full-space solutions in the frequency domain. .. math:: \hat{G}^{ee}_{\alpha\beta}, \hat{G}^{ee}_{3\alpha}, \hat{G}^{ee}_{33}, \hat{G}^{em}_{\alpha\beta}, \hat{G}^{em}_{\alpha 3} This function corresponds to equations 45--50 in [HuTS15]_, and loosely to the corresponding files ``Gin11.F90``, ``Gin12.F90``, ``Gin13.F90``, ``Gin22.F90``, ``Gin23.F90``, ``Gin31.F90``, ``Gin32.F90``, ``Gin33.F90``, ``Gin41.F90``, ``Gin42.F90``, ``Gin43.F90``, ``Gin51.F90``, ``Gin52.F90``, ``Gin53.F90``, ``Gin61.F90``, and ``Gin62.F90``. This function is called from one of the modelling routines in :mod:`model`. Consult these modelling routines for a description of the input and output parameters. """ xco = np.cos(angle)*off yco = np.sin(angle)*off # Reciprocity switches for magnetic receivers if mrec: if msrc: # If src is also magnetic, switch eta and zeta (MM => EE). # G^mm_ab(s, r, e, z) = -G^ee_ab(s, r, -z, -e) etaH, zetaH = -zetaH, -etaH etaV, zetaV = -zetaV, -etaV else: # If src is electric, swap src and rec (ME => EM). # G^me_ab(s, r, e, z) = -G^em_ba(r, s, e, z) xco *= -1 yco *= -1 zsrc, zrec = zrec, zsrc # Calculate TE/TM-variables if ab not in [16, 26]: # Calc TM lGamTM = np.sqrt(zetaH*etaV) RTM = np.sqrt(off*off + ((zsrc-zrec)*(zsrc-zrec)*etaH/etaV)[:, None]) uGamTM = np.exp(-lGamTM[:, None]*RTM)/(4*np.pi*RTM * np.sqrt(etaH/etaV)[:, None]) if ab not in [13, 23, 31, 32, 33, 34, 35]: # Calc TE lGamTE = np.sqrt(zetaV*etaH) RTE = np.sqrt(off*off+(zsrc-zrec)*(zsrc-zrec)*(zetaH/zetaV)[:, None]) uGamTE = np.exp(-lGamTE[:, None]*RTE)/(4*np.pi*RTE * np.sqrt(zetaH/zetaV)[:, None]) # Calculate responses if ab in [11, 12, 21, 22]: # Eqs 45, 46 # Define coo1, coo2, and delta if ab in [11, 22]: if ab in [11, ]: coo1 = xco coo2 = xco else: coo1 = yco coo2 = yco delta = 1 else: coo1 = xco coo2 = yco delta = 0 # Calculate response term1 = uGamTM*(3*coo1*coo2/(RTM*RTM) - delta) term1 *= 1/(etaV[:, None]*RTM*RTM) + (lGamTM/etaV)[:, None]/RTM term1 += uGamTM*zetaH[:, None]*coo1*coo2/(RTM*RTM) term2 = -delta*zetaH[:, None]*uGamTE term3 = -zetaH[:, None]*coo1*coo2/(off*off)*(uGamTM - uGamTE) term4 = -np.sqrt(zetaH)[:, None]*(2*coo1*coo2/(off*off) - delta) if np.any(zetaH.imag < 0): # We need the sqrt where Im > 0. term4 *= -1 # This if-statement corrects for it. term4 *= np.exp(-lGamTM[:, None]*RTM) - np.exp(-lGamTE[:, None]*RTE) term4 /= 4*np.pi*np.sqrt(etaH)[:, None]*off*off gin = term1 + term2 + term3 + term4 elif ab in [13, 23, 31, 32]: # Eq 47 # Define coo if ab in [13, 31]: coo = xco elif ab in [23, 32]: coo = yco # Calculate response term1 = (etaH/etaV)[:, None]*(zrec - zsrc)*coo/(RTM*RTM) term2 = 3/(RTM*RTM) + 3*lGamTM[:, None]/RTM + (lGamTM*lGamTM)[:, None] gin = term1*term2*uGamTM/etaV[:, None] elif ab in [33, ]: # Eq 48 # Calculate response term1 = (((etaH/etaV)[:, None]*(zsrc - zrec)/RTM) * ((etaH/etaV)[:, None]*(zsrc - zrec)/RTM) * (3/(RTM*RTM) + 3*lGamTM[:, None]/RTM + (lGamTM*lGamTM)[:, None])) term2 = (-(etaH/etaV)[:, None]/RTM*(1/RTM + lGamTM[:, None]) - (etaH*zetaH)[:, None]) gin = (term1 + term2)*uGamTM/etaV[:, None] elif ab in [14, 24, 15, 25]: # Eq 49 # Define coo1, coo2, coo3, coo4, delta, and pm if ab in [14, 25]: coo1, coo2 = xco, yco coo3, coo4 = xco, yco delta = 0 pm = -1 elif ab in [24, 15]: coo1, coo2 = yco, yco coo3, coo4 = xco, xco delta = 1 pm = 1 # 15/25: Swap x/y if ab in[15, 25]: coo1, coo3 = coo3, coo1 coo2, coo4 = coo4, coo2 # 24/25: Swap src/rec if ab in[24, 25]: zrec, zsrc = zsrc, zrec # Calculate response def term(lGam, z_eH, z_eV, R, off, co1, co2): fac = (lGam*z_eH/z_eV)[:, None]/R*np.exp(-lGam[:, None]*R) term = 2/(off*off) + lGam[:, None]/R + 1/(R*R) return fac*(co1*co2*term - delta) termTM = term(lGamTM, etaH, etaV, RTM, off, coo1, coo2) termTE = term(lGamTE, zetaH, zetaV, RTE, off, coo3, coo4) mult = (zrec - zsrc)/(4*np.pi*np.sqrt(etaH*zetaH)[:, None]*off*off) gin = -mult*(pm*termTM + termTE) elif ab in [34, 35, 16, 26]: # Eqs 50, 51 # Define coo if ab in [34, 16]: coo = yco else: coo = -xco # Define R, lGam, uGam, e_zH, and e_zV if ab in [34, 35]: coo *= -1 R = RTM lGam = lGamTM uGam = uGamTM e_zH = etaH e_zV = etaV else: R = RTE lGam = lGamTE uGam = uGamTE e_zH = zetaH e_zV = zetaV # Calculate response gin = coo*(e_zH/e_zV)[:, None]/R*(lGam[:, None] + 1/R)*uGam # If rec is magnetic switch sign (reciprocity MM/ME => EE/EM). if mrec: gin *= -1 return gin
[docs]def halfspace(off, angle, zsrc, zrec, etaH, etaV, freqtime, ab, signal, solution='dhs'): r"""Return frequency- or time-space domain VTI half-space solution. Calculates the frequency- or time-space domain electromagnetic response for a half-space below air using the diffusive approximation, as given in [SlHM10]_, where the electric source is located at [0, 0, zsrc], and the electric receiver at [xco, yco, zrec]. It can also be used to calculate the fullspace solution or the separate fields: direct field, reflected field, and airwave; always using the diffusive approximation. See ``solution``-parameter. This function is called from one of the modelling routines in :mod:`model`. Consult these modelling routines for a description of the input and solution parameters. """ xco = np.cos(angle)*off yco = np.sin(angle)*off res = np.real(1/etaH[0, 0]) aniso = 1/np.sqrt(np.real(etaV[0, 0])*res) # Define sval/time and dtype depending on signal. if signal is None: sval = freqtime dtype = etaH.dtype else: time = freqtime if signal == -1: # Calculate DC time = np.r_[time[:, 0], 1e4][:, None] freqtime = time dtype = float # Other defined parameters rh = np.sqrt(xco**2 + yco**2) # Horizontal distance in space hp = abs(zrec + zsrc) # Physical vertical distance hm = abs(zrec - zsrc) hsp = hp*aniso # Scaled vertical distance hsm = hm*aniso rp = np.sqrt(xco**2 + yco**2 + hp**2) # Physical distance rm = np.sqrt(xco**2 + yco**2 + hm**2) rsp = np.sqrt(xco**2 + yco**2 + hsp**2) # Scaled distance rsm = np.sqrt(xco**2 + yco**2 + hsm**2) # mu_0 = 4e-7*np.pi # Magn. perm. of free space [H/m] tp = mu_0*rp**2/(res*4) # Diffusion time tm = mu_0*rm**2/(res*4) tsp = mu_0*rsp**2/(res*aniso**2*4) # Scaled diffusion time tsm = mu_0*rsm**2/(res*aniso**2*4) # delta-fct delta_\alpha\beta if ab in [11, 22, 33]: delta = 1 else: delta = 0 # Define alpha/beta; swap if necessary x = xco y = yco if ab == 11: y = x elif ab in [22, 23, 32]: x = y elif ab == 21: x, y = y, x # Define rev for 3\alpha->\alpha3 reciprocity if ab in [13, 23]: rev = -1 elif ab in [31, 32]: rev = 1 # Exponential diffusion functions for m=0,1,2 if signal is None: # Frequency-domain f0p = np.exp(-2*np.sqrt(sval*tp)) f0m = np.exp(-2*np.sqrt(sval*tm)) fs0p = np.exp(-2*np.sqrt(sval*tsp)) fs0m = np.exp(-2*np.sqrt(sval*tsm)) f1p = np.sqrt(sval)*f0p f1m = np.sqrt(sval)*f0m fs1p = np.sqrt(sval)*fs0p fs1m = np.sqrt(sval)*fs0m f2p = sval*f0p f2m = sval*f0m fs2p = sval*fs0p fs2m = sval*fs0m elif abs(signal) == 1: # Time-domain step response # Replace F(m) with F(m-2) f0p = special.erfc(np.sqrt(tp/time)) f0m = special.erfc(np.sqrt(tm/time)) fs0p = special.erfc(np.sqrt(tsp/time)) fs0m = special.erfc(np.sqrt(tsm/time)) f1p = np.exp(-tp/time)/np.sqrt(np.pi*time) f1m = np.exp(-tm/time)/np.sqrt(np.pi*time) fs1p = np.exp(-tsp/time)/np.sqrt(np.pi*time) fs1m = np.exp(-tsm/time)/np.sqrt(np.pi*time) f2p = f1p*np.sqrt(tp)/time f2m = f1m*np.sqrt(tm)/time fs2p = fs1p*np.sqrt(tsp)/time fs2m = fs1m*np.sqrt(tsm)/time else: # Time-domain impulse response f0p = np.sqrt(tp/(np.pi*time**3))*np.exp(-tp/time) f0m = np.sqrt(tm/(np.pi*time**3))*np.exp(-tm/time) fs0p = np.sqrt(tsp/(np.pi*time**3))*np.exp(-tsp/time) fs0m = np.sqrt(tsm/(np.pi*time**3))*np.exp(-tsm/time) f1p = (tp/time - 0.5)/np.sqrt(tp)*f0p f1m = (tm/time - 0.5)/np.sqrt(tm)*f0m fs1p = (tsp/time - 0.5)/np.sqrt(tsp)*fs0p fs1m = (tsm/time - 0.5)/np.sqrt(tsm)*fs0m f2p = (tp/time - 1.5)/time*f0p f2m = (tm/time - 1.5)/time*f0m fs2p = (tsp/time - 1.5)/time*fs0p fs2m = (tsm/time - 1.5)/time*fs0m # Pre-allocate arrays gs0m = np.zeros(np.shape(x), dtype=dtype) gs0p = np.zeros(np.shape(x), dtype=dtype) gs1m = np.zeros(np.shape(x), dtype=dtype) gs1p = np.zeros(np.shape(x), dtype=dtype) gs2m = np.zeros(np.shape(x), dtype=dtype) gs2p = np.zeros(np.shape(x), dtype=dtype) g0p = np.zeros(np.shape(x), dtype=dtype) g1m = np.zeros(np.shape(x), dtype=dtype) g1p = np.zeros(np.shape(x), dtype=dtype) g2m = np.zeros(np.shape(x), dtype=dtype) g2p = np.zeros(np.shape(x), dtype=dtype) air = np.zeros(np.shape(f0p), dtype=dtype) if ab in [11, 12, 21, 22]: # 1. {alpha, beta} # Get indices for singularities izr = rh == 0 # index where rh = 0 iir = np.invert(izr) # invert of izr izh = hm == 0 # index where hm = 0 iih = np.invert(izh) # invert of izh # fab fab = rh**2*delta-x*y # TM-mode coefficients gs0p = res*aniso*(3*x*y - rsp**2*delta)/(4*np.pi*rsp**5) gs0m = res*aniso*(3*x*y - rsm**2*delta)/(4*np.pi*rsm**5) gs1p[iir] = (((3*x[iir]*y[iir] - rsp[iir]**2*delta)/rsp[iir]**4 - (x[iir]*y[iir] - fab[iir])/rh[iir]**4) * np.sqrt(mu_0*res)/(4*np.pi)) gs1m[iir] = (((3*x[iir]*y[iir] - rsm[iir]**2*delta)/rsm[iir]**4 - (x[iir]*y[iir] - fab[iir])/rh[iir]**4) * np.sqrt(mu_0*res)/(4*np.pi)) gs2p[iir] = ((mu_0*x[iir]*y[iir])/(4*np.pi*aniso*rsp[iir]) * (1/rsp[iir]**2 - 1/rh[iir]**2)) gs2m[iir] = ((mu_0*x[iir]*y[iir])/(4*np.pi*aniso*rsm[iir]) * (1/rsm[iir]**2 - 1/rh[iir]**2)) # TM-mode for numerical singularities rh=0 (hm!=0) gs1p[izr*iih] = -np.sqrt(mu_0*res)*delta/(4*np.pi*hsp**2) gs1m[izr*iih] = -np.sqrt(mu_0*res)*delta/(4*np.pi*hsm**2) gs2p[izr*iih] = -mu_0*delta/(8*np.pi*aniso*hsp) gs2m[izr*iih] = -mu_0*delta/(8*np.pi*aniso*hsm) # TE-mode coefficients g0p = res*(3*fab - rp**2*delta)/(2*np.pi*rp**5) g1m[iir] = (np.sqrt(mu_0*res)*(x[iir]*y[iir] - fab[iir]) / (4*np.pi*rh[iir]**4)) g1p[iir] = (g1m[iir] + np.sqrt(mu_0*res)*(3*fab[iir] - rp[iir]**2*delta)/(2*np.pi*rp[iir]**4)) g2p[iir] = mu_0*fab[iir]/(4*np.pi*rp[iir])*(2/rp[iir]**2 - 1/rh[iir]**2) g2m[iir] = -mu_0*fab[iir]/(4*np.pi*rh[iir]**2*rm[iir]) # TE-mode for numerical singularities rh=0 (hm!=0) g1m[izr*iih] = np.zeros(np.shape(g1m[izr*iih]), dtype=dtype) g1p[izr*iih] = -np.sqrt(mu_0*res)*delta/(2*np.pi*hp**2) g2m[izr*iih] = mu_0*delta/(8*np.pi*hm) g2p[izr*iih] = mu_0*delta/(8*np.pi*hp) # Bessel functions for airwave def BI(gamH, hp, nr, xim): r"""Return BI_nr.""" return np.exp(-np.real(gamH)*hp)*special.ive(nr, xim) def BK(xip, nr): r"""Return BK_nr.""" if np.isrealobj(xip): # To keep it real in Laplace-domain [exp(-1j*0) = 1-0j]. return special.kve(nr, xip) else: return np.exp(-1j*np.imag(xip))*special.kve(nr, xip) # Airwave calculation def airwave(sval, hp, rp, res, fab, delta): r"""Return airwave.""" # Parameters zeta = sval*mu_0 gamH = np.sqrt(zeta/res) xip = gamH*(rp + hp)/2 xim = gamH*(rp - hp)/2 # Bessel functions BI0 = BI(gamH, hp, 0, xim) BI1 = BI(gamH, hp, 1, xim) BI2 = BI(gamH, hp, 2, xim) BK0 = BK(xip, 0) BK1 = BK(xip, 1) # Calculation P1 = (sval*mu_0)**(3/2)*fab*hp/(4*np.sqrt(res)) P2 = 4*BI1*BK0 - (3*BI0 - 4*np.sqrt(res)*BI1/(np.sqrt(sval*mu_0) * (rp + hp)) + BI2)*BK1 P3 = 3*fab/rp**2 - delta P4 = (sval*mu_0*hp*rp*(BI0*BK0 - BI1*BK1) + np.sqrt(res*sval*mu_0)*BI0*BK1 * (rp + hp) + np.sqrt(res*sval*mu_0)*BI1*BK0*(rp - hp)) return (P1*P2 - P3*P4)/(4*np.pi*rp**3) # Airwave depending on signal if signal is None: # Frequency-domain air = airwave(sval, hp, rp, res, fab, delta) elif abs(signal) == 1: # Time-domain step response # Solution for step-response air-wave is not analytical, but uses # the Gaver-Stehfest method. K = 16 # Coefficients Dk fn = np.vectorize(np.math.factorial) def coeff_dk(k, K): r"""Return coefficients Dk for k, K.""" n = np.arange(int((k+1)/2), np.min([k, K/2])+.5, 1, dtype=int) Dk = n**(K/2)*fn(2*n) Dk /= fn(n)*fn(n-1)*fn(k-n)*fn(2*n-k)*fn(K/2-n) return Dk.sum()*(-1)**(k+K/2) for k in range(1, K+1): sval = k*np.log(2)/time cair = airwave(sval, hp, rp, res, fab, delta) air += coeff_dk(k, K)*cair.real/k else: # Time-domain impulse response thp = mu_0*hp**2/(4*res) trh = mu_0*rh**2/(8*res) P1 = (mu_0**2*hp*np.exp(-thp/time))/(res*32*np.pi*time**3) P2 = 2*(delta - (x*y)/rh**2)*special.ive(1, trh/time) P3 = mu_0/(2*res*time)*(rh**2*delta - x*y)-delta P4 = special.ive(0, trh/time) - special.ive(1, trh/time) air = P1*(P2 - P3*P4) elif ab in [13, 23, 31, 32]: # 2. {3, alpha}, {alpha, 3} # TM-mode gs0m = 3*x*res*aniso**3*(zrec - zsrc)/(4*np.pi*rsm**5) gs0p = rev*3*x*res*aniso**3*hp/(4*np.pi*rsp**5) gs1m = (np.sqrt(mu_0*res)*3*aniso**2*x*(zrec - zsrc) / (4*np.pi*rsm**4)) gs1p = rev*np.sqrt(mu_0*res)*3*aniso**2*x*hp/(4*np.pi*rsp**4) gs2m = mu_0*x*aniso*(zrec - zsrc)/(4*np.pi*rsm**3) gs2p = rev*mu_0*x*aniso*hp/(4*np.pi*rsp**3) elif ab == 33: # 3. {3, 3} # TM-mode gs0m = res*aniso**3*(3*hsm**2 - rsm**2)/(4*np.pi*rsm**5) gs0p = -res*aniso**3*(3*hsp**2 - rsp**2)/(4*np.pi*rsp**5) gs1m = np.sqrt(mu_0*res)*aniso**2*(3*hsm**2 - rsm**2)/(4*np.pi*rsm**4) gs1p = -np.sqrt(mu_0*res)*aniso**2*(3*hsp**2 - rsp**2)/(4*np.pi*rsp**4) gs2m = mu_0*aniso*(hsm**2 - rsm**2)/(4*np.pi*rsm**3) gs2p = -mu_0*aniso*(hsp**2 - rsp**2)/(4*np.pi*rsp**3) # Direct field direct_TM = gs0m*fs0m + gs1m*fs1m + gs2m*fs2m direct_TE = g1m*f1m + g2m*f2m direct = direct_TM + direct_TE # Reflection reflect_TM = gs0p*fs0p + gs1p*fs1p + gs2p*fs2p reflect_TE = g0p*f0p + g1p*f1p + g2p*f2p reflect = reflect_TM + reflect_TE # If switch-off, subtract switch-on from DC value if signal == -1: direct_TM = direct_TM[-1]-direct_TM[:-1] direct_TE = direct_TE[-1]-direct_TE[:-1] direct = direct[-1]-direct[:-1] reflect_TM = reflect_TM[-1]-reflect_TM[:-1] reflect_TE = reflect_TE[-1]-reflect_TE[:-1] reflect = reflect[-1]-reflect[:-1] air = air[-1]-air[:-1] # Return, depending on 'solution' if solution == 'dfs': return direct elif solution == 'dsplit': return direct, reflect, air elif solution == 'dtetm': return direct_TE, direct_TM, reflect_TE, reflect_TM, air else: return direct + reflect + air