The fatiando package has been deprecated. Please check out the new tools in the Fatiando a Terra website: www.fatiando.org

Source code for fatiando.gravmag.transform

"""
Potential field transformations, like upward continuation and derivatives.

.. note:: Most, if not all, functions here required gridded data.

**Transformations**

* :func:`~fatiando.gravmag.transform.upcontinue`: Upward continuation of
  gridded potential field data on a level surface.
* :func:`~fatiando.gravmag.transform.reduce_to_pole`: Reduce the total field
  magnetic anomaly to the pole.
* :func:`~fatiando.gravmag.transform.tga`: Calculate the amplitude of the
  total gradient (also called the analytic signal)
* :func:`~fatiando.gravmag.transform.tilt`: Calculates the tilt angle
* :func:`~fatiando.gravmag.transform.power_density_spectra`: Calculates
  the Power Density Spectra of a gridded potential field data.
* :func:`~fatiando.gravmag.transform.radial_average`: Calculates the
  the radial average of a Power Density Spectra using concentring rings.

**Derivatives**

* :func:`~fatiando.gravmag.transform.derivx`: Calculate the n-th order
  derivative of a potential field in the x-direction (North-South)
* :func:`~fatiando.gravmag.transform.derivy`: Calculate the n-th order
  derivative of a potential field in the y-direction (East-West)
* :func:`~fatiando.gravmag.transform.derivz`: Calculate the n-th order
  derivative of a potential field in the z-direction

----

"""
from __future__ import division
import warnings
import numpy

from .. import utils


[docs]def reduce_to_pole(x, y, data, shape, inc, dec, sinc, sdec): r""" Reduce total field magnetic anomaly data to the pole. The reduction to the pole if a phase transformation that can be applied to total field magnetic anomaly data. It "simulates" how the data would be if **both** the Geomagnetic field and the magnetization of the source were vertical (:math:`90^\circ` inclination) (Blakely, 1996). This functions performs the reduction in the frequency domain (using the FFT). The transform filter is (in the frequency domain): .. math:: RTP(k_x, k_y) = \frac{|k|}{ a_1 k_x^2 + a_2 k_y^2 + a_3 k_x k_y + i|k|(b_1 k_x + b_2 k_y)} in which :math:`k_x` and :math:`k_y` are the wave-numbers in the x and y directions and .. math:: |k| = \sqrt{k_x^2 + k_y^2} \\ a_1 = m_z f_z - m_x f_x \\ a_2 = m_z f_z - m_y f_y \\ a_3 = -m_y f_x - m_x f_y \\ b_1 = m_x f_z + m_z f_x \\ b_2 = m_y f_z + m_z f_y :math:`\mathbf{m} = (m_x, m_y, m_z)` is the unit-vector of the total magnetization of the source and :math:`\mathbf{f} = (f_x, f_y, f_z)` is the unit-vector of the Geomagnetic field. .. note:: Requires gridded data. .. warning:: The magnetization direction of the anomaly source is crucial to the reduction-to-the-pole. **Wrong values of *sinc* and *sdec* will lead to a wrong reduction.** Parameters: * x, y : 1d-arrays The x, y, z coordinates of each data point. * data : 1d-array The total field anomaly data at each point. * shape : tuple = (nx, ny) The shape of the data grid * inc, dec : floats The inclination and declination of the inducing Geomagnetic field * sinc, sdec : floats The inclination and declination of the total magnetization of the anomaly source. The total magnetization is the vector sum of the induced and remanent magnetization. If there is only induced magnetization, use the *inc* and *dec* of the Geomagnetic field. Returns: * rtp : 1d-array The data reduced to the pole. References: Blakely, R. J. (1996), Potential Theory in Gravity and Magnetic Applications, Cambridge University Press. """ fx, fy, fz = utils.ang2vec(1, inc, dec) if sinc is None or sdec is None: mx, my, mz = fx, fy, fz else: mx, my, mz = utils.ang2vec(1, sinc, sdec) kx, ky = [k for k in _fftfreqs(x, y, shape, shape)] kz_sqr = kx**2 + ky**2 a1 = mz*fz - mx*fx a2 = mz*fz - my*fy a3 = -my*fx - mx*fy b1 = mx*fz + mz*fx b2 = my*fz + mz*fy # The division gives a RuntimeWarning because of the zero frequency term. # This suppresses the warning. with numpy.errstate(divide='ignore', invalid='ignore'): rtp = (kz_sqr)/(a1*kx**2 + a2*ky**2 + a3*kx*ky + 1j*numpy.sqrt(kz_sqr)*(b1*kx + b2*ky)) rtp[0, 0] = 0 ft_pole = rtp*numpy.fft.fft2(numpy.reshape(data, shape)) return numpy.real(numpy.fft.ifft2(ft_pole)).ravel()
[docs]def upcontinue(x, y, data, shape, height): r""" Upward continuation of potential field data. Calculates the continuation through the Fast Fourier Transform in the wavenumber domain (Blakely, 1996): .. math:: F\{h_{up}\} = F\{h\} e^{-\Delta z |k|} and then transformed back to the space domain. :math:`h_{up}` is the upward continue data, :math:`\Delta z` is the height increase, :math:`F` denotes the Fourier Transform, and :math:`|k|` is the wavenumber modulus. .. note:: Requires gridded data. .. note:: x, y, z and height should be in meters. .. note:: It is not possible to get the FFT of a masked grid. The default :func:`fatiando.gridder.interp` call using minimum curvature will not be suitable. Use ``extrapolate=True`` or ``algorithm='nearest'`` to get an unmasked grid. Parameters: * x, y : 1D-arrays The x and y coordinates of the grid points * data : 1D-array The potential field at the grid points * shape : tuple = (nx, ny) The shape of the grid * height : float The height increase (delta z) in meters. Returns: * cont : array The upward continued data References: Blakely, R. J. (1996), Potential Theory in Gravity and Magnetic Applications, Cambridge University Press. """ assert x.shape == y.shape, \ "x and y arrays must have same shape" if height <= 0: warnings.warn("Using 'height' <= 0 means downward continuation, " + "which is known to be unstable.") nx, ny = shape # Pad the array with the edge values to avoid instability padded, padx, pady = _pad_data(data, shape) kx, ky = _fftfreqs(x, y, shape, padded.shape) kz = numpy.sqrt(kx**2 + ky**2) upcont_ft = numpy.fft.fft2(padded)*numpy.exp(-height*kz) cont = numpy.real(numpy.fft.ifft2(upcont_ft)) # Remove padding cont = cont[padx: padx + nx, pady: pady + ny].ravel() return cont
def _upcontinue_space(x, y, data, shape, height): """ Upward continuation using the space-domain formula. DEPRECATED. Use the better implementation using FFT. Kept here for historical reasons. """ nx, ny = shape dx = (x.max() - x.min())/(nx - 1) dy = (y.max() - y.min())/(ny - 1) area = dx*dy deltaz_sqr = (height)**2 cont = numpy.zeros_like(data) for i, j, g in zip(x, y, data): cont += g*area*((x - i)**2 + (y - j)**2 + deltaz_sqr)**(-1.5) cont *= abs(height)/(2*numpy.pi) return cont
[docs]def tga(x, y, data, shape, method='fd'): r""" Calculate the total gradient amplitude (TGA). This the same as the `3D analytic signal` of Roest et al. (1992), but we prefer the newer, more descriptive nomenclature suggested by Reid (2012). The TGA is defined as the amplitude of the gradient vector of a potential field :math:`T` (e.g. the magnetic total field anomaly): .. math:: TGA = \sqrt{ \left(\frac{\partial T}{\partial x}\right)^2 + \left(\frac{\partial T}{\partial y}\right)^2 + \left(\frac{\partial T}{\partial z}\right)^2 } .. note:: Requires gridded data. .. warning:: If the data is not in SI units, the derivatives will be in strange units and so will the total gradient amplitude! I strongly recommend converting the data to SI **before** calculating the TGA is you need the gradient in Eotvos (use one of the unit conversion functions of :mod:`fatiando.utils`). Parameters: * x, y : 1D-arrays The x and y coordinates of the grid points * data : 1D-array The potential field at the grid points * shape : tuple = (nx, ny) The shape of the grid * method : string The method used to calculate the horizontal derivatives. Options are: ``'fd'`` for finite-difference (more stable) or ``'fft'`` for the Fast Fourier Transform. The z derivative is always calculated by FFT. Returns: * tga : 1D-array The amplitude of the total gradient References: Reid, A. (2012), Forgotten truths, myths and sacred cows of Potential Fields Geophysics - II, in SEG Technical Program Expanded Abstracts 2012, pp. 1-3, Society of Exploration Geophysicists. Roest, W., J. Verhoef, and M. Pilkington (1992), Magnetic interpretation using the 3-D analytic signal, GEOPHYSICS, 57(1), 116-125, doi:10.1190/1.1443174. """ dx = derivx(x, y, data, shape, method=method) dy = derivy(x, y, data, shape, method=method) dz = derivz(x, y, data, shape) res = numpy.sqrt(dx ** 2 + dy ** 2 + dz ** 2) return res
[docs]def tilt(x, y, data, shape, xderiv=None, yderiv=None, zderiv=None): r""" Calculates the potential field tilt, as defined by Miller and Singh (1994) .. math:: tilt(f) = tan^{-1}\left( \frac{ \frac{\partial T}{\partial z}}{ \sqrt{\frac{\partial T}{\partial x}^2 + \frac{\partial T}{\partial y}^2}} \right) When used on magnetic total field anomaly data, works best if the data is reduced to the pole. It's useful to plot the zero contour line of the tilt to represent possible outlines of the source bodies. Use matplotlib's ``pyplot.contour`` or ``pyplot.tricontour`` for this. .. note:: Requires gridded data if ``xderiv``, ``yderiv`` and ``zderiv`` are not given. Parameters: * x, y : 1D-arrays The x and y coordinates of the grid points * data : 1D-array The potential field at the grid points * shape : tuple = (nx, ny) The shape of the grid. Ignored if *xderiv*, *yderiv* and *zderiv* are given. * xderiv : 1D-array or None Optional. Values of the derivative in the x direction. If ``None``, will calculated using the default options of :func:`~fatiando.gravmag.transform.derivx` * yderiv : 1D-array or None Optional. Values of the derivative in the y direction. If ``None``, will calculated using the default options of :func:`~fatiando.gravmag.transform.derivy` * zderiv : 1D-array or None Optional. Values of the derivative in the z direction. If ``None``, will calculated using the default options of :func:`~fatiando.gravmag.transform.derivz` Returns: * tilt : 1D-array The tilt angle of the total field in radians. References: Miller, Hugh G, and Vijay Singh. 1994. "Potential Field Tilt --- a New Concept for Location of Potential Field Sources." Journal of Applied Geophysics 32 (2--3): 213-17. doi:10.1016/0926-9851(94)90022-1. """ if xderiv is None: xderiv = derivx(x, y, data, shape) if yderiv is None: yderiv = derivy(x, y, data, shape) if zderiv is None: zderiv = derivz(x, y, data, shape) horiz_deriv = numpy.sqrt(xderiv**2 + yderiv**2) tilt = numpy.arctan2(zderiv, horiz_deriv) return tilt
[docs]def derivx(x, y, data, shape, order=1, method='fd'): """ Calculate the derivative of a potential field in the x direction. .. note:: Requires gridded data. .. warning:: If the data is not in SI units, the derivative will be in strange units! I strongly recommend converting the data to SI **before** calculating the derivative (use one of the unit conversion functions of :mod:`fatiando.utils`). This way the derivative will be in SI units and can be easily converted to what unit you want. Parameters: * x, y : 1D-arrays The x and y coordinates of the grid points * data : 1D-array The potential field at the grid points * shape : tuple = (nx, ny) The shape of the grid * order : int The order of the derivative * method : string The method used to calculate the derivatives. Options are: ``'fd'`` for central finite-differences (more stable) or ``'fft'`` for the Fast Fourier Transform. Returns: * deriv : 1D-array The derivative """ nx, ny = shape assert method in ['fft', 'fd'], \ 'Invalid method "{}".'.format(method) if method == 'fft': # Pad the array with the edge values to avoid instability padded, padx, pady = _pad_data(data, shape) kx, _ = _fftfreqs(x, y, shape, padded.shape) deriv_ft = numpy.fft.fft2(padded)*(kx*1j)**order deriv_pad = numpy.real(numpy.fft.ifft2(deriv_ft)) # Remove padding from derivative deriv = deriv_pad[padx: padx + nx, pady: pady + ny] elif method == 'fd': datamat = data.reshape(shape) dx = (x.max() - x.min())/(nx - 1) deriv = numpy.empty_like(datamat) deriv[1:-1, :] = (datamat[2:, :] - datamat[:-2, :])/(2*dx) deriv[0, :] = deriv[1, :] deriv[-1, :] = deriv[-2, :] if order > 1: deriv = derivx(x, y, deriv, shape, order=order - 1, method='fd') return deriv.ravel()
[docs]def derivy(x, y, data, shape, order=1, method='fd'): """ Calculate the derivative of a potential field in the y direction. .. note:: Requires gridded data. .. warning:: If the data is not in SI units, the derivative will be in strange units! I strongly recommend converting the data to SI **before** calculating the derivative (use one of the unit conversion functions of :mod:`fatiando.utils`). This way the derivative will be in SI units and can be easily converted to what unit you want. Parameters: * x, y : 1D-arrays The x and y coordinates of the grid points * data : 1D-array The potential field at the grid points * shape : tuple = (nx, ny) The shape of the grid * order : int The order of the derivative * method : string The method used to calculate the derivatives. Options are: ``'fd'`` for central finite-differences (more stable) or ``'fft'`` for the Fast Fourier Transform. Returns: * deriv : 1D-array The derivative """ nx, ny = shape assert method in ['fft', 'fd'], \ 'Invalid method "{}".'.format(method) if method == 'fft': # Pad the array with the edge values to avoid instability padded, padx, pady = _pad_data(data, shape) _, ky = _fftfreqs(x, y, shape, padded.shape) deriv_ft = numpy.fft.fft2(padded)*(ky*1j)**order deriv_pad = numpy.real(numpy.fft.ifft2(deriv_ft)) # Remove padding from derivative deriv = deriv_pad[padx: padx + nx, pady: pady + ny] elif method == 'fd': datamat = data.reshape(shape) dy = (y.max() - y.min())/(ny - 1) deriv = numpy.empty_like(datamat) deriv[:, 1:-1] = (datamat[:, 2:] - datamat[:, :-2])/(2*dy) deriv[:, 0] = deriv[:, 1] deriv[:, -1] = deriv[:, -2] if order > 1: deriv = derivy(x, y, deriv, shape, order=order - 1, method='fd') return deriv.ravel()
[docs]def derivz(x, y, data, shape, order=1, method='fft'): """ Calculate the derivative of a potential field in the z direction. .. note:: Requires gridded data. .. warning:: If the data is not in SI units, the derivative will be in strange units! I strongly recommend converting the data to SI **before** calculating the derivative (use one of the unit conversion functions of :mod:`fatiando.utils`). This way the derivative will be in SI units and can be easily converted to what unit you want. Parameters: * x, y : 1D-arrays The x and y coordinates of the grid points * data : 1D-array The potential field at the grid points * shape : tuple = (nx, ny) The shape of the grid * order : int The order of the derivative * method : string The method used to calculate the derivatives. Options are: ``'fft'`` for the Fast Fourier Transform. Returns: * deriv : 1D-array The derivative """ assert method == 'fft', \ "Invalid method '{}'".format(method) nx, ny = shape # Pad the array with the edge values to avoid instability padded, padx, pady = _pad_data(data, shape) kx, ky = _fftfreqs(x, y, shape, padded.shape) deriv_ft = numpy.fft.fft2(padded)*numpy.sqrt(kx**2 + ky**2)**order deriv = numpy.real(numpy.fft.ifft2(deriv_ft)) # Remove padding from derivative return deriv[padx: padx + nx, pady: pady + ny].ravel()
[docs]def power_density_spectra(x, y, data, shape): r""" Calculates the Power Density Spectra of a 2D gridded potential field through the FFT: .. math:: \Phi_{\Delta T}(k_x, k_y) = | F\left{\Delta T \right}(k_x, k_y) |^2 .. note:: Requires gridded data. .. note:: x, y, z and height should be in meters. Parameters: * x, y : 1D-arrays The x and y coordinates of the grid points * data : 1D-array The potential field at the grid points * shape : tuple = (nx, ny) The shape of the grid Returns: * kx, ky : 2D-arrays The wavenumbers of each Power Density Spectra point * pds : 2D-array The Power Density Spectra of the data """ kx, ky = _fftfreqs(x, y, shape, shape) pds = abs(numpy.fft.fft2(numpy.reshape(data, shape)))**2 return kx, ky, pds
[docs]def radial_average_spectrum(kx, ky, pds, max_radius=None, ring_width=None): r""" Calculates the average of the Power Density Spectra points that falls inside concentric rings built around the origin of the wavenumber coordinate system with constant width. The width of the rings and the inner radius of the biggest ring can be changed by setting the optional parameters ring_width and max_radius, respectively. .. note:: To calculate the radially averaged power density spectra use the outputs of the function power_density_spectra as input of this one. Parameters: * kx, ky : 2D-arrays The x and y coordinates of the grid points * data : 1D-array The potential field at the grid points * shape : tuple = (nx, ny) The shape of the grid * max_radius : float (optional) Inner radius of the biggest ring. By default it's set as the minimum of kx.max() and ky.max(). Making it smaller leaves points outside of the averaging, and making it bigger includes points nearer to the boundaries. * ring_width : float (optional) Width of the rings. By default it's set as the largest value of :math:`\Delta k_x` and :math:`\Delta k_y`, being them the equidistances of the kx and ky arrays. Making it bigger gives more populated averages, and making it smaller lowers the ammount of points per ring (use it carefully). Returns: * k_radial : 1D-array Wavenumbers of each Radially Averaged Power Spectrum point. Also, the inner radius of the rings. * pds_radial : 1D array Radially Averaged Power Spectrum """ nx, ny = pds.shape if max_radius is None: max_radius = min(kx.max(), ky.max()) if ring_width is None: ring_width = max(kx[1, 0], ky[0, 1]) k = numpy.sqrt(kx**2 + ky**2) pds_radial = [] k_radial = [] radius_i = -1 while True: radius_i += 1 if radius_i*ring_width > max_radius: break else: if radius_i == 0: inside = k <= 0.5*ring_width else: inside = numpy.logical_and(k > (radius_i - 0.5)*ring_width, k <= (radius_i + 0.5)*ring_width) pds_radial.append(pds[inside].mean()) k_radial.append(radius_i*ring_width) return numpy.array(k_radial), numpy.array(pds_radial)
def _pad_data(data, shape): n = _nextpow2(numpy.max(shape)) nx, ny = shape padx = (n - nx)//2 pady = (n - ny)//2 padded = numpy.pad(data.reshape(shape), ((padx, padx), (pady, pady)), mode='edge') return padded, padx, pady def _nextpow2(i): buf = numpy.ceil(numpy.log(i)/numpy.log(2)) return int(2**buf) def _fftfreqs(x, y, shape, padshape): """ Get two 2D-arrays with the wave numbers in the x and y directions. """ nx, ny = shape dx = (x.max() - x.min())/(nx - 1) fx = 2*numpy.pi*numpy.fft.fftfreq(padshape[0], dx) dy = (y.max() - y.min())/(ny - 1) fy = 2*numpy.pi*numpy.fft.fftfreq(padshape[1], dy) return numpy.meshgrid(fy, fx)[::-1]