def spline(values, points, new): """Return `values` at `points` interpolated in log10 at `new`.""" out = iuSpline(np.log10(points), values.real)(np.log10(new)) if hankel: out = out + 1j * iuSpline(np.log10(points), values.imag)( np.log10(new)) return out
def spline(values, points, int_pts): r"""Return `values` at `points` interpolated in log at `int_pts`.""" out = iuSpline(np.log(points), values.real)(np.log(int_pts)) if hankel: out = out + 1j * iuSpline(np.log(points), values.imag)( np.log(int_pts)) return out
def ffht(fEM, time, freq, ftarg): """Fourier Transform using a Cosine- or a Sine-filter. It follows the Filter methodology [Anderson_1975]_, see `fht` for more information. The 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. This function is based on `get_CSEM1D_TD_FHT.m` from the source code distributed with [Key_2012]_. Returns ------- tEM : array Returns time-domain EM response of `fEM` for given `time`. conv : bool Only relevant for QWE/QUAD. """ # Get ftarg values fftfilt, pts_per_dec, ftkind = ftarg # Settings depending if cos/sin plus scaling if ftkind == 'sin': fEM = -fEM.imag else: fEM = fEM.real if pts_per_dec: # Use pts_per_dec frequencies per decade # 1. Interpolate in frequency domain sfEM = iuSpline(np.log10(2*np.pi*freq), fEM) ifEM = sfEM(np.log10(fftfilt.base/time[:, None])) # 2. Filter tEM = np.dot(ifEM, getattr(fftfilt, ftkind)) else: # Standard FHT procedure # Get new times in frequency domain _, itime = get_spline_values(fftfilt, time) # Re-arranged fEM with shape (ntime, nfreq). Each row starts one # 'freq' higher. fEM = np.concatenate((np.tile(fEM, itime.size).squeeze(), np.zeros(itime.size))) fEM = fEM.reshape(itime.size, -1)[:, :fftfilt.base.size] # 1. Filter stEM = np.dot(fEM, getattr(fftfilt, ftkind)) # 2. Interpolate in time domain itEM = iuSpline(np.log10((itime)[::-1]), stEM[::-1]) tEM = itEM(np.log10(time)) # Return the electromagnetic time domain field # (Second argument is only for QWE) return tEM/time, True
def fourier_fft(fEM, time, freq, ftarg): r"""Fourier Transform using the Fast Fourier Transform. The function is called from one of the modelling routines in :mod:`empymod.model`. Consult these modelling routines for a description of the input and output parameters. Returns ------- tEM : array Returns time-domain EM response of `fEM` for given `time`. conv : bool Only relevant for QWE/QUAD. """ # Get ftarg values dfreq = ftarg['dfreq'] nfreq = ftarg['nfreq'] ntot = ftarg['ntot'] pts_per_dec = ftarg['pts_per_dec'] # If pts_per_dec, we have first to interpolate fEM to required freqs if pts_per_dec: sfEMr = iuSpline(np.log(freq), fEM.real) sfEMi = iuSpline(np.log(freq), fEM.imag) freq = np.arange(1, nfreq+1)*dfreq fEM = sfEMr(np.log(freq)) + 1j*sfEMi(np.log(freq)) # Pad the frequency result fEM = np.pad(fEM, (0, ntot-nfreq), 'linear_ramp') # Carry out FFT ifftEM = fftpack.ifft(np.r_[fEM[1:], 0, fEM[::-1].conj()]).real stEM = 2*ntot*fftpack.fftshift(ifftEM*dfreq, 0) # Interpolate in time domain dt = 1/(2*ntot*dfreq) ifEM = iuSpline(np.linspace(-ntot, ntot-1, 2*ntot)*dt, stEM) tEM = ifEM(time)/2*np.pi # (Multiplication of 2/pi in model.tem) # Return the electromagnetic time domain field # (Second argument is only for QWE) return tEM, True
def tinker_params_spline(delta, z=None): global tinker_splines if tinker_splines is None: tinker_splines = [] D, data = np.log(tinker_data[0]), tinker_data[1:] for y in data: # Extend to large Delta p = np.polyfit(D[-2:], y[-2:], 1) x = np.hstack((D, D[-1] + 3.)) y = np.hstack((y, np.polyval(p, x[-1]))) tinker_splines.append(iuSpline(x, y, k=2)) A0, a0, b0, c0 = [ts(np.log(delta)) for ts in tinker_splines] if z is None: return A0, a0, b0, c0 z = np.asarray(z) A = A0 * (1 + z)**-.14 a = a0 * (1 + z)**-.06 alpha = 10.**(-(((.75 / np.log10(delta / 75.)))**1.2)) b = b0 * (1 + z)**-alpha c = np.zeros(np.shape(z)) + c0 return A, a, b, c
fEM = test_freq(res, off, f) fft0 = {'fEM': fEM, 'f': f, 'ftarg': ftarg} # # F -- QWE - FQWE # # nquad = fqwe0['ftarg'][2] maxint = fqwe0['ftarg'][3] fEM = fqwe0['fEM'] freq = fqwe0['f'] # The following is a condensed version of transform.fqwe, without doqwe-part xint = np.concatenate((np.array([1e-20]), np.arange(1, maxint + 1) * np.pi)) intervals = xint / t[:, None] g_x, g_w = special.p_roots(nquad) dx = np.repeat(np.diff(xint) / 2, nquad) Bx = dx * (np.tile(g_x, maxint) + 1) + np.repeat(xint[:-1], nquad) SS = np.sin(Bx) * np.tile(g_w, maxint) tEM_iint = iuSpline(np.log(2 * np.pi * freq), fEM.imag) sEM = tEM_iint(np.log(Bx / t[:, None])) * SS fqwe0['sEM'] = sEM fqwe0['intervals'] = intervals # # G -- QWE - HQWE # # # Model model = utils.check_model([], 10, 2, 2, 5, 1, 10, True, 0) depth, res, aniso, epermH, epermV, mpermH, mpermV, isfullspace = model frequency = utils.check_frequency(1, res, aniso, epermH, epermV, mpermH, mpermV, 0) freq, etaH, etaV, zetaH, zetaV = frequency src, nsrc = utils.check_dipole([0, 0, 0], 'src', 0) ab, msrc, mrec = utils.check_ab(11, 0) ht, htarg = utils.check_hankel('qwe', None, 0) rec = [np.arange(1, 11) * 500, np.zeros(10), 300]
def waveform(times, resp, times_wanted, wave_time, wave_amp, nquad=3): """Apply a source waveform to the signal. Parameters ---------- times : ndarray Times of computed input response; should start before and end after `times_wanted`. resp : ndarray EM-response corresponding to `times`. times_wanted : ndarray Wanted times. wave_time : ndarray Time steps of the wave. wave_amp : ndarray Amplitudes of the wave corresponding to `wave_time`, usually in the range of [0, 1]. nquad : int Number of Gauss-Legendre points for the integration. Default is 3. Returns ------- resp_wanted : ndarray EM field for `times_wanted`. """ # Interpolate on log. PP = iuSpline(np.log10(times), resp) # Wave time steps. dt = np.diff(wave_time) dI = np.diff(wave_amp) dIdt = dI / dt # Gauss-Legendre Quadrature; 3 is generally good enough. # (Roots/weights could be cached.) g_x, g_w = roots_legendre(nquad) # Pre-allocate output. resp_wanted = np.zeros_like(times_wanted) # Loop over wave segments. for i, cdIdt in enumerate(dIdt): # We only have to consider segments with a change of current. if cdIdt == 0.0: continue # If wanted time is before a wave element, ignore it. ind_a = wave_time[i] < times_wanted if ind_a.sum() == 0: continue # If wanted time is within a wave element, we cut the element. ind_b = wave_time[i + 1] > times_wanted[ind_a] # Start and end for this wave-segment for all times. ta = times_wanted[ind_a] - wave_time[i] tb = times_wanted[ind_a] - wave_time[i + 1] tb[ind_b] = 0.0 # Cut elements # Gauss-Legendre for this wave segment. See # https://en.wikipedia.org/wiki/Gaussian_quadrature#Change_of_interval # for the change of interval, which makes this a bit more complex. logt = np.log10(np.outer((tb - ta) / 2, g_x) + (ta + tb)[:, None] / 2) fact = (tb - ta) / 2 * cdIdt resp_wanted[ind_a] += fact * np.sum(np.array(PP(logt) * g_w), axis=1) return resp_wanted
def fftlog(fEM, time, freq, ftarg): r"""Fourier Transform using FFTLog. FFTLog is the logarithmic analogue to the Fast Fourier Transform FFT. FFTLog was presented in Appendix B of [Hami00]_ and published at <http://casa.colorado.edu/~ajsh/FFTLog>. This function uses a simplified version of ``pyfftlog``, which is a python-version of ``FFTLog``. For more details regarding ``pyfftlog`` see <https://github.com/prisae/pyfftlog>. Not the full flexibility of ``FFTLog`` is available here: Only the logarithmic FFT (``fftl`` in ``FFTLog``), not the Hankel transform (``fht`` in ``FFTLog``). Furthermore, the following parameters are fixed: - ``kr`` = 1 (initial value) - ``kropt`` = 1 (silently adjusts ``kr``) - ``dir`` = 1 (forward) Furthermore, ``q`` is restricted to -1 <= q <= 1. The 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. Returns ------- tEM : array Returns time-domain EM response of ``fEM`` for given ``time``. conv : bool Only relevant for QWE/QUAD. """ # Get tcalc, dlnr, kr, rk, q; a and n _, _, q, mu, tcalc, dlnr, kr, rk = ftarg if mu > 0: # Sine a = -fEM.imag else: # Cosine a = fEM.real n = a.size # 1. Amplitude and Argument of kr^(-2 i y) U_mu(q + 2 i y) ln2kr = np.log(2.0 / kr) d = np.pi / (n * dlnr) m = np.arange(1, (n + 1) / 2) y = m * d # y = m*pi/(n*dlnr) if q == 0: # unbiased case (q = 0) zp = special.loggamma((mu + 1) / 2.0 + 1j * y) arg = 2.0 * (ln2kr * y + zp.imag) else: # biased case (q != 0) xp = (mu + 1.0 + q) / 2.0 xm = (mu + 1.0 - q) / 2.0 zp = special.loggamma(xp + 0j) zm = special.loggamma(xm + 0j) # Amplitude and Argument of U_mu(q) amp = np.exp(np.log(2.0) * q + zp.real - zm.real) # note +Im(zm) to get conjugate value below real axis arg = zp.imag + zm.imag # first element: cos(arg) = ±1, sin(arg) = 0 argcos1 = amp * np.cos(arg) # remaining elements zp = special.loggamma(xp + 1j * y) zm = special.loggamma(xm + 1j * y) argamp = np.exp(np.log(2.0) * q + zp.real - zm.real) arg = 2 * ln2kr * y + zp.imag + zm.imag argcos = np.cos(arg) argsin = np.sin(arg) # 2. Centre point of array jc = np.array((n + 1) / 2.0) j = np.arange(n) + 1 # 3. a(r) = A(r) (r/rc)^[-dir*(q-.5)] a *= np.exp(-(q - 0.5) * (j - jc) * dlnr) # 4. transform a(r) -> ã(k) # 4.a normal FFT a = fftpack.rfft(a) # 4.b m = np.arange(1, n / 2, dtype=int) # index variable if q == 0: # unbiased (q = 0) transform # multiply by (kr)^[- i 2 m pi/(n dlnr)] U_mu[i 2 m pi/(n dlnr)] ar = a[2 * m - 1] ai = a[2 * m] a[2 * m - 1] = ar * argcos[:-1] - ai * argsin[:-1] a[2 * m] = ar * argsin[:-1] + ai * argcos[:-1] # problematical last element, for even n if np.mod(n, 2) == 0: ar = argcos[-1] a[-1] *= ar else: # biased (q != 0) transform # multiply by (kr)^[- i 2 m pi/(n dlnr)] U_mu[q + i 2 m pi/(n dlnr)] # phase ar = a[2 * m - 1] ai = a[2 * m] a[2 * m - 1] = ar * argcos[:-1] - ai * argsin[:-1] a[2 * m] = ar * argsin[:-1] + ai * argcos[:-1] a[0] *= argcos1 a[2 * m - 1] *= argamp[:-1] a[2 * m] *= argamp[:-1] # problematical last element, for even n if np.mod(n, 2) == 0: m = int(n / 2) - 3 ar = argcos[m - 1] * argamp[m - 1] a[-1] *= ar # 4.c normal FFT back a = fftpack.irfft(a) # Ã(k) = ã(k) k^[-dir*(q+.5)] rc^[-dir*(q-.5)] # = ã(k) (k/kc)^[-dir*(q+.5)] (kc rc)^(-dir*q) (rc/kc)^(dir*.5) a = a[::-1] * np.exp(-( (q + 0.5) * (j - jc) * dlnr + q * np.log(kr) - np.log(rk) / 2.0)) # Interpolate for the desired times ttEM = iuSpline(np.log(tcalc), a) tEM = ttEM(np.log(time)) # (Second argument is only for QWE) return tEM, True
def fqwe(fEM, time, freq, qweargs): r"""Fourier Transform using Quadrature-With-Extrapolation. It follows the QWE methodology [Key12]_ for the Hankel transform, see ``hqwe`` for more information. The 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. This function is based on ``get_CSEM1D_TD_QWE.m`` from the source code distributed with [Key12]_. ``fqwe`` checks how steep the decay of the frequency-domain result is, and calls QUAD for the very steep interval, for which QWE is not suited. Returns ------- tEM : array Returns time-domain EM response of ``fEM`` for given ``time``. conv : bool If true, QWE/QUAD converged. If not, <ftarg> might have to be adjusted. """ # Get rtol, atol, nquad, maxint, diff_quad, a, b, and limit rtol, atol, nquad, maxint, _, diff_quad, a, b, limit, sincos = qweargs # Calculate quadrature intervals for all offset xint = np.concatenate( (np.array([1e-20]), np.arange(1, maxint + 1) * np.pi)) if sincos == np.cos: # Adjust zero-crossings if cosine-transform xint[1:] -= np.pi / 2 intervals = xint / time[:, None] # Get Gauss Quadrature Weights g_x, g_w = special.p_roots(nquad) # Pre-compute the Bessel functions at fixed quadrature points, multiplied # by the corresponding Gauss quadrature weight. dx = np.repeat(np.diff(xint) / 2, nquad) Bx = dx * (np.tile(g_x, maxint) + 1) + np.repeat(xint[:-1], nquad) SS = sincos(Bx) * np.tile(g_w, maxint) # Interpolate in frequency domain tEM_rint = iuSpline(np.log(2 * np.pi * freq), fEM.real) tEM_iint = iuSpline(np.log(2 * np.pi * freq), -fEM.imag) # Check if we use QWE or SciPy's QUAD # If there are any steep decays within an interval we have to use QUAD, as # QWE is not designed for these intervals. check0 = np.log(intervals[:, :-1]) check1 = np.log(intervals[:, 1:]) doqwe = np.all( (np.abs(tEM_rint(check0) + 1j * tEM_iint(check0)) / np.abs(tEM_rint(check1) + 1j * tEM_iint(check1)) < diff_quad), 1) # Choose imaginary part if sine-transform, else real part if sincos == np.sin: tEM_int = tEM_iint else: tEM_int = tEM_rint # Set quadargs if not given: if not limit: limit = maxint if not a: a = intervals[:, 0] else: a = a * np.ones(time.shape) if not b: b = intervals[:, -1] else: b = b * np.ones(time.shape) # Pre-allocate output array tEM = np.zeros(time.size) conv = True # Carry out SciPy's Quad if required if np.any(~doqwe): def sEMquad(w, t): r"""Return scaled, interpolated value of tEM_int for ``w``.""" return tEM_int(np.log(w)) * sincos(w * t) # Loop over times that require QUAD for i in np.where(~doqwe)[0]: out = integrate.quad(sEMquad, a[i], b[i], (time[i], ), 1, atol, rtol, limit) tEM[i] = out[0] # If there is a fourth output from QUAD, it means it did not conv. if len(out) > 3: conv *= False # Carry out QWE for 'well-behaved' intervals if np.any(doqwe): sEM = tEM_int(np.log(Bx / time[doqwe, None])) * SS tEM[doqwe], _, tc = qwe(rtol, atol, maxint, sEM, intervals[doqwe, :]) conv *= tc return tEM, conv
def hquad(zsrc, zrec, lsrc, lrec, off, factAng, depth, ab, etaH, etaV, zetaH, zetaV, xdirect, quadargs, use_ne_eval, msrc, mrec): r"""Hankel Transform using the ``QUADPACK`` library. This routine uses the ``scipy.integrate.quad`` module, which in turn makes use of the Fortran library ``QUADPACK`` (``qagse``). It is massively (orders of magnitudes) slower than either ``fht`` or ``hqwe``, and is mainly here for completeness and comparison purposes. It always uses interpolation in the wavenumber domain, hence it generally will not be as precise as the other methods. However, it might work in some areas where the others fail. The 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. Returns ------- fEM : array Returns frequency-domain EM response. kcount : int Kernel count. For HQUAD, this is 1. conv : bool If true, QUAD converged. If not, <htarg> might have to be adjusted. """ # Get quadargs rtol, atol, limit, a, b, pts_per_dec = quadargs # Get required lambdas la = np.log10(a) lb = np.log10(b) ilambd = np.logspace(la, lb, (lb - la) * pts_per_dec + 1) # Call the kernel PJ0, PJ1, PJ0b = kernel.wavenumber(zsrc, zrec, lsrc, lrec, depth, etaH, etaV, zetaH, zetaV, np.atleast_2d(ilambd), ab, xdirect, msrc, mrec, use_ne_eval) # Interpolation in wavenumber domain: Has to be done separately on each PJ, # in order to work with multiple offsets which have different angles. # We check if the kernels are zero, to avoid unnecessary calculations. if PJ0 is not None: sPJ0r = iuSpline(np.log(ilambd), PJ0.real) sPJ0i = iuSpline(np.log(ilambd), PJ0.imag) else: sPJ0r = None sPJ0i = None if PJ1 is not None: sPJ1r = iuSpline(np.log(ilambd), PJ1.real) sPJ1i = iuSpline(np.log(ilambd), PJ1.imag) else: sPJ1r = None sPJ1i = None if PJ0b is not None: sPJ0br = iuSpline(np.log(ilambd), PJ0b.real) sPJ0bi = iuSpline(np.log(ilambd), PJ0b.imag) else: sPJ0br = None sPJ0bi = None # Pre-allocate output array fEM = np.zeros(off.size, dtype=complex) conv = True # Input-dictionary for quad iinp = {'a': a, 'b': b, 'epsabs': atol, 'epsrel': rtol, 'limit': limit} # Loop over offsets for i in range(off.size): fEM[i], tc = quad(sPJ0r, sPJ0i, sPJ1r, sPJ1i, sPJ0br, sPJ0bi, ab, off[i], factAng[i], iinp) conv *= tc # Return the electromagnetic field # Second argument (1) is the kernel count, last argument is only for QWE. return fEM, 1, conv
def hqwe(zsrc, zrec, lsrc, lrec, off, factAng, depth, ab, etaH, etaV, zetaH, zetaV, xdirect, qweargs, use_ne_eval, msrc, mrec): r"""Hankel Transform using Quadrature-With-Extrapolation. *Quadrature-With-Extrapolation* was introduced to geophysics by [Key12]_. It is one of many so-called *ISE* methods to solve Hankel Transforms, where *ISE* stands for Integration, Summation, and Extrapolation. Following [Key12]_, but without going into the mathematical details here, the QWE method rewrites the Hankel transform of the form .. math:: F(r) = \int^\infty_0 f(\lambda)J_v(\lambda r)\ \mathrm{d}\lambda as a quadrature sum which form is similar to the DLF (equation 15), .. math:: F_i \approx \sum^m_{j=1} f(x_j/r)w_j g(x_j) = \sum^m_{j=1} f(x_j/r)\hat{g}(x_j) \ , but with various bells and whistles applied (using the so-called Shanks transformation in the form of a routine called :math:`\epsilon`-algorithm ([Shan55]_, [Wynn56]_; implemented with algorithms from [Tref00]_ and [Weni89]_). This function is based on ``get_CSEM1D_FD_QWE.m``, ``qwe.m``, and ``getBesselWeights.m`` from the source code distributed with [Key12]_. In the spline-version, ``hqwe`` checks how steep the decay of the wavenumber-domain result is, and calls QUAD for the very steep interval, for which QWE is not suited. The 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. Returns ------- fEM : array Returns frequency-domain EM response. kcount : int Kernel count. conv : bool If true, QWE/QUAD converged. If not, <htarg> might have to be adjusted. """ # Input params have an additional dimension for frequency, reduce here etaH = etaH[0, :] etaV = etaV[0, :] zetaH = zetaH[0, :] zetaV = zetaV[0, :] # Get rtol, atol, nquad, maxint, and pts_per_dec rtol, atol, nquad, maxint, pts_per_dec = qweargs[:5] # 1. PRE-COMPUTE THE BESSEL FUNCTIONS # at fixed quadrature points for each interval and multiply by the # corresponding Gauss quadrature weights # Get Gauss quadrature weights g_x, g_w = special.p_roots(nquad) # Compute n zeros of the Bessel function of the first kind of order 1 using # the Newton-Raphson method, which is fast enough for our purposes. Could # be done with a loop for (but it is slower): # b_zero[i] = optimize.newton(special.j1, b_zero[i]) # Initial guess using asymptotic zeros b_zero = np.pi * np.arange(1.25, maxint + 1) # Newton-Raphson iterations for i in range(10): # 10 is more than enough, usually stops in 5 # Evaluate b_x0 = special.j1(b_zero) # j0 and j1 have faster versions b_x1 = special.jv(2, b_zero) # j2 does not have a faster version # The step length b_h = -b_x0 / (b_x0 / b_zero - b_x1) # Take the step b_zero += b_h # Check for convergence if all(np.abs(b_h) < 8 * np.finfo(float).eps * b_zero): break # 2. COMPUTE THE QUADRATURE INTERVALS AND BESSEL FUNCTION WEIGHTS # Lower limit of integrand, a small but non-zero value xint = np.concatenate((np.array([1e-20]), b_zero)) # Assemble the output arrays dx = np.repeat(np.diff(xint) / 2, nquad) Bx = dx * (np.tile(g_x, maxint) + 1) + np.repeat(xint[:-1], nquad) BJ0 = special.j0(Bx) * np.tile(g_w, maxint) BJ1 = special.j1(Bx) * np.tile(g_w, maxint) # 3. START QWE # Intervals and lambdas for all offset intervals = xint / off[:, None] lambd = Bx / off[:, None] # The following lines until # "Call and return QWE, depending if spline or not" # are part of the splined routine. However, we calculate it here to get # the non-zero kernels, `k_used`. # New lambda, from min to max required lambda with pts_per_dec start = np.log10(lambd.min()) stop = np.log10(lambd.max()) # If not spline, we just calculate three lambdas to check if pts_per_dec == 0: ilambd = np.logspace(start, stop, 3) else: ilambd = np.logspace(start, stop, (stop - start) * pts_per_dec + 1) # Call the kernel PJ0, PJ1, PJ0b = kernel.wavenumber(zsrc, zrec, lsrc, lrec, depth, etaH[None, :], etaV[None, :], zetaH[None, :], zetaV[None, :], np.atleast_2d(ilambd), ab, xdirect, msrc, mrec, use_ne_eval) # Check which kernels have information k_used = [True, True, True] for i, val in enumerate((PJ0, PJ1, PJ0b)): if val is None: k_used[i] = False # Call and return QWE, depending if spline or not if pts_per_dec != 0: # If spline, we calculate all kernels here # Interpolation : Has to be done separately on each PJ, # in order to work with multiple offsets which have different angles. if k_used[0]: sPJ0r = iuSpline(np.log(ilambd), PJ0.real) sPJ0i = iuSpline(np.log(ilambd), PJ0.imag) else: sPJ0r = None sPJ0i = None if k_used[1]: sPJ1r = iuSpline(np.log(ilambd), PJ1.real) sPJ1i = iuSpline(np.log(ilambd), PJ1.imag) else: sPJ1r = None sPJ1i = None if k_used[2]: sPJ0br = iuSpline(np.log(ilambd), PJ0b.real) sPJ0bi = iuSpline(np.log(ilambd), PJ0b.imag) else: sPJ0br = None sPJ0bi = None # Get quadargs: diff_quad, a, b, limit diff_quad, a, b, limit = qweargs[5:] # Set quadargs if not given: if not limit: limit = maxint if not a: a = intervals[:, 0] else: a = a * np.ones(off.shape) if not b: b = intervals[:, -1] else: b = b * np.ones(off.shape) # Check if we use QWE or SciPy's QUAD # If there are any steep decays within an interval we have to use QUAD, # as QWE is not designed for these intervals. check0 = np.log(intervals[:, :-1]) check1 = np.log(intervals[:, 1:]) numerator = np.zeros((off.size, maxint), dtype=complex) denominator = np.zeros((off.size, maxint), dtype=complex) if k_used[0]: numerator += sPJ0r(check0) + 1j * sPJ0i(check0) denominator += sPJ0r(check1) + 1j * sPJ0i(check1) if k_used[1]: numerator += sPJ1r(check0) + 1j * sPJ1i(check0) denominator += sPJ1r(check1) + 1j * sPJ1i(check1) if k_used[2]: numerator += sPJ0br(check0) + 1j * sPJ0bi(check0) denominator += sPJ0br(check1) + 1j * sPJ0bi(check1) doqwe = np.all((np.abs(numerator) / np.abs(denominator) < diff_quad), 1) # Pre-allocate output array fEM = np.zeros(off.size, dtype=complex) conv = True # Carry out SciPy's Quad if required if np.any(~doqwe): # Loop over offsets that require Quad for i in np.where(~doqwe)[0]: # Input-dictionary for quad iinp = { 'a': a[i], 'b': b[i], 'epsabs': atol, 'epsrel': rtol, 'limit': limit } fEM[i], tc = quad(sPJ0r, sPJ0i, sPJ1r, sPJ1i, sPJ0br, sPJ0bi, ab, off[i], factAng[i], iinp) # Update conv conv *= tc # Return kcount=1 in case no QWE is calculated kcount = 1 if np.any(doqwe): # Get EM-field at required offsets if k_used[0]: sPJ0 = sPJ0r(np.log(lambd)) + 1j * sPJ0i(np.log(lambd)) if k_used[1]: sPJ1 = sPJ1r(np.log(lambd)) + 1j * sPJ1i(np.log(lambd)) if k_used[2]: sPJ0b = sPJ0br(np.log(lambd)) + 1j * sPJ0bi(np.log(lambd)) # Carry out and return the Hankel transform for this interval sEM = np.zeros_like(numerator, dtype=complex) if k_used[1]: sEM += np.sum( np.reshape(sPJ1 * BJ1, (off.size, nquad, -1), order='F'), 1) if ab in [11, 12, 21, 22, 14, 24, 15, 25]: # Because of J2 # J2(kr) = 2/(kr)*J1(kr) - J0(kr) sEM /= np.atleast_1d(off[:, np.newaxis]) if k_used[2]: sEM += np.sum( np.reshape(sPJ0b * BJ0, (off.size, nquad, -1), order='F'), 1) if k_used[1] or k_used[2]: sEM *= factAng[:, np.newaxis] if k_used[0]: sEM += np.sum( np.reshape(sPJ0 * BJ0, (off.size, nquad, -1), order='F'), 1) getkernel = sEM[doqwe, :] # Get QWE fEM[doqwe], kcount, tc = qwe(rtol, atol, maxint, getkernel, intervals[doqwe, :], None, None, None) conv *= tc else: # If not spline, we define the wavenumber-kernel here def getkernel(i, inplambd, inpoff, inpfang): r"""Return wavenumber-domain-kernel as a fct of interval i.""" # Indices and factor for this interval iB = i * nquad + np.arange(nquad) # PJ0 and PJ1 for this interval PJ0, PJ1, PJ0b = kernel.wavenumber(zsrc, zrec, lsrc, lrec, depth, etaH[None, :], etaV[None, :], zetaH[None, :], zetaV[None, :], np.atleast_2d(inplambd)[:, iB], ab, xdirect, msrc, mrec, use_ne_eval) # Carry out and return the Hankel transform for this interval gEM = np.zeros_like(inpoff, dtype=complex) if k_used[1]: gEM += inpfang * np.dot(PJ1[0, :], BJ1[iB]) if ab in [11, 12, 21, 22, 14, 24, 15, 25]: # Because of J2 # J2(kr) = 2/(kr)*J1(kr) - J0(kr) gEM /= np.atleast_1d(inpoff) if k_used[2]: gEM += inpfang * np.dot(PJ0b[0, :], BJ0[iB]) if k_used[0]: gEM += np.dot(PJ0[0, :], BJ0[iB]) return gEM # Get QWE fEM, kcount, conv = qwe(rtol, atol, maxint, getkernel, intervals, lambd, off, factAng) return fEM, kcount, conv
def fht(zsrc, zrec, lsrc, lrec, off, angle, depth, ab, etaH, etaV, zetaH, zetaV, xdirect, fhtarg, use_spline, use_ne_eval, msrc, mrec): """Hankel Transform using the Fast Hankel Transform. The *Fast Hankel Transform* is a *Digital Filter Method*, introduced to geophysics by [Gosh_1971]_, and made popular and wide-spread by [Anderson_1975]_, [Anderson_1979]_, [Anderson_1982]_. This implementation of the FHT follows [Key_2012]_, equation 6. Without going into the mathematical details (which can be found in any of the above papers) and following [Key_2012]_, the FHT method rewrites the Hankel transform of the form .. math:: F(r) = \int^\infty_0 f(\lambda)J_v(\lambda r)\ \mathrm{d}\lambda as .. math:: F(r) = \sum^n_{i=1} f(b_i/r)h_i/r \ , where :math:`h` is the digital filter.The Filter abscissae b is given by .. math:: b_i = \lambda_ir = e^{ai}, \qquad i = -l, -l+1, \cdots, l \ , with :math:`l=(n-1)/2`, and :math:`a` is the spacing coefficient. This function is loosely based on `get_CSEM1D_FD_FHT.m` from the source code distributed with [Key_2012]_. The 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. Returns ------- fEM : array Returns frequency-domain EM response. kcount : int Kernel count. For FHT, this is 1. conv : bool Only relevant for QWE/QUAD. """ # Get fhtargs fhtfilt = fhtarg[0] pts_per_dec = fhtarg[1] # For FHT, spline for one offset is equals no spline if use_spline and off.size == 1: use_spline = False # 1. COMPUTE REQUIRED LAMBDAS for given hankel-filter-base if use_spline: # Use interpolation # Get lambda from offset and filter lambd, ioff = get_spline_values(fhtfilt, off, pts_per_dec) else: # df.base/off lambd = fhtfilt.base/off[:, None] # 2. CALL THE KERNEL PJ0, PJ1, PJ0b = kernel.wavenumber(zsrc, zrec, lsrc, lrec, depth, etaH, etaV, zetaH, zetaV, lambd, ab, xdirect, msrc, mrec, use_ne_eval) if use_spline and pts_per_dec: # If spline in wnr-domain, interpolate PJ's # Interpolate in wavenumber domain PJ0real = iuSpline(np.log10(lambd), PJ0.real) PJ0imag = iuSpline(np.log10(lambd), PJ0.imag) PJ1real = iuSpline(np.log10(lambd), PJ1.real) PJ1imag = iuSpline(np.log10(lambd), PJ1.imag) PJ0breal = iuSpline(np.log10(lambd), PJ0b.real) PJ0bimag = iuSpline(np.log10(lambd), PJ0b.imag) # Overwrite lambd with non-spline lambd lambd = fhtfilt.base/off[:, None] # Get fEM-field at required non-spline lambdas PJ0 = PJ0real(np.log10(lambd)) + 1j*PJ0imag(np.log10(lambd)) PJ1 = PJ1real(np.log10(lambd)) + 1j*PJ1imag(np.log10(lambd)) PJ0b = PJ0breal(np.log10(lambd)) + 1j*PJ0bimag(np.log10(lambd)) # Set spline to false use_spline = False elif use_spline: # If spline in frequency domain, re-arrange PJ's def rearrange_PJ(PJ, noff, nfilt): """Return re-arranged PJ with shape (noff, nlambd). Each row starts one 'lambda' higher.""" outarr = np.concatenate((np.tile(PJ, noff).squeeze(), np.zeros(noff))) return outarr.reshape(noff, -1)[:, :nfilt] PJ0 = rearrange_PJ(PJ0, ioff.size, fhtfilt.base.size) PJ1 = rearrange_PJ(PJ1, ioff.size, fhtfilt.base.size) PJ0b = rearrange_PJ(PJ0b, ioff.size, fhtfilt.base.size) # 3. ANGLE DEPENDENT FACTORS factAng = kernel.angle_factor(angle, ab, msrc, mrec) one_angle = (factAng - factAng[0] == 0).all() # 4. CARRY OUT THE FHT if use_spline and one_angle: # SPLINE, ALL ANGLES ARE EQUAL # If all offsets are in one line from the source, hence have the same # angle, we can combine PJ0 and PJ0b and save one FHT, and combine both # into one function to interpolate. # 1. FHT EM_int = factAng[0]*np.dot(PJ1, fhtfilt.j1) if ab in [11, 12, 21, 22, 14, 24, 15, 25]: # Because of J2 # J2(kr) = 2/(kr)*J1(kr) - J0(kr) EM_int /= ioff EM_int += np.dot(PJ0 + factAng[0]*PJ0b, fhtfilt.j0) # 2. Interpolation real_EM = iuSpline(np.log10(ioff[::-1]), EM_int.real[::-1]) imag_EM = iuSpline(np.log10(ioff[::-1]), EM_int.imag[::-1]) fEM = real_EM(np.log10(off)) + 1j*imag_EM(np.log10(off)) elif use_spline: # SPLINE, VARYING ANGLES # If not all offsets are in one line from the source, hence do not have # the same angle, the whole process has to be done separately for # angle-dependent and angle-independent parts. This means one FHT more, # and two (instead of one) functions to interpolate. # 1. FHT # Separated in an angle-dependent and a non-dependent part EM_noang = np.dot(PJ0, fhtfilt.j0) EM_angle = np.dot(PJ1, fhtfilt.j1) if ab in [11, 12, 21, 22, 14, 24, 15, 25]: # Because of J2 # J2(kr) = 2/(kr)*J1(kr) - J0(kr) EM_angle /= ioff EM_angle += np.dot(PJ0b, fhtfilt.j0) # 2. Interpolation # Separately on EM_noang and EM_angle real_noang = iuSpline(np.log10(ioff[::-1]), EM_noang.real[::-1]) imag_noang = iuSpline(np.log10(ioff[::-1]), EM_noang.imag[::-1]) real_angle = iuSpline(np.log10(ioff[::-1]), EM_angle.real[::-1]) imag_angle = iuSpline(np.log10(ioff[::-1]), EM_angle.imag[::-1]) # Get fEM-field at required offsets EM_noang = real_noang(np.log10(off)) + 1j*imag_noang(np.log10(off)) EM_angle = real_angle(np.log10(off)) + 1j*imag_angle(np.log10(off)) # Angle dependency fEM = (factAng*EM_angle + EM_noang) else: # NO SPLINE # Without spline, we can combine PJ0 and PJ0b to save one FHT, even if # all offsets have a different angle. fEM = factAng*np.dot(PJ1, fhtfilt.j1) if ab in [11, 12, 21, 22, 14, 24, 15, 25]: # Because of J2 # J2(kr) = 2/(kr)*J1(kr) - J0(kr) fEM /= off fEM += np.dot(PJ0 + factAng[:, np.newaxis]*PJ0b, fhtfilt.j0) # Return the electromagnetic field, normalize by offset # Second argument (1) is the kernel count # (Last argument is only for QWE) return fEM/off, 1, True
def projectFields(self, u): """ Transform frequency domain responses to time domain responses """ # Compute frequency domain reponses right at filter coefficient values # Src waveform: Step-off if self.use_lowpass_filter: factor = self.lowpass_filter.copy() else: factor = np.ones_like(self.frequency, dtype=complex) if self.rx_type == 'Bz': factor *= 1./(2j*np.pi*self.frequency) if self.wave_type == 'stepoff': # Compute EM responses if u.size == self.n_frequency: resp, _ = fourier_dlf( u.flatten()*factor, self.time, self.frequency, self.ftarg ) # Compute EM sensitivities else: resp = np.zeros( (self.n_time, self.n_layer), dtype=np.float64, order='F') # ) # TODO: remove for loop for i in range(self.n_layer): resp_i, _ = fourier_dlf( u[:, i]*factor, self.time, self.frequency, self.ftarg ) resp[:, i] = resp_i # Evaluate piecewise linear input current waveforms # Using Fittermann's approach (19XX) with Gaussian Quadrature elif self.wave_type == 'general': # Compute EM responses if u.size == self.n_frequency: resp_int, _ = fourier_dlf( u.flatten()*factor, self.time_int, self.frequency, self.ftarg ) # step_func = interp1d( # self.time_int, resp_int # ) step_func = iuSpline( np.log10(self.time_int), resp_int ) resp = piecewise_pulse_fast( step_func, self.time, self.time_input_currents, self.input_currents, self.period, n_pulse=self.n_pulse ) # Compute response for the dual moment if self.moment_type == "dual": resp_dual_moment = piecewise_pulse_fast( step_func, self.time_dual_moment, self.time_input_currents_dual_moment, self.input_currents_dual_moment, self.period_dual_moment, n_pulse=self.n_pulse ) # concatenate dual moment response # so, ordering is the first moment data # then the second moment data. resp = np.r_[resp, resp_dual_moment] # Compute EM sensitivities else: if self.moment_type == "single": resp = np.zeros( (self.n_time, self.n_layer), dtype=np.float64, order='F' ) else: # For dual moment resp = np.zeros( (self.n_time+self.n_time_dual_moment, self.n_layer), dtype=np.float64, order='F') # TODO: remove for loop (?) for i in range(self.n_layer): resp_int_i, _ = fourier_dlf( u[:, i]*factor, self.time_int, self.frequency, self.ftarg ) # step_func = interp1d( # self.time_int, resp_int_i # ) step_func = iuSpline( np.log10(self.time_int), resp_int_i ) resp_i = piecewise_pulse_fast( step_func, self.time, self.time_input_currents, self.input_currents, self.period, n_pulse=self.n_pulse ) if self.moment_type == "single": resp[:, i] = resp_i else: resp_dual_moment_i = piecewise_pulse_fast( step_func, self.time_dual_moment, self.time_input_currents_dual_moment, self.input_currents_dual_moment, self.period_dual_moment, n_pulse=self.n_pulse ) resp[:, i] = np.r_[resp_i, resp_dual_moment_i] return resp * (-2.0/np.pi) * mu_0