Beispiel #1
0
 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
Beispiel #2
0
 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
Beispiel #3
0
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
Beispiel #4
0
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
Beispiel #5
0
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
Beispiel #6
0
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]
Beispiel #7
0
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
Beispiel #8
0
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
Beispiel #9
0
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
Beispiel #10
0
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
Beispiel #11
0
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
Beispiel #12
0
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
Beispiel #13
0
    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