Module pycurious.grid
This PyCurious module contains the CurieGrid
class,
which can be initialised with a magnetic grid of equal spacing in the x and y direction.
It contains methods for the following functionality:
- Decomposition of subgrids for processing square windows of the magnetic anomaly
- Removing linear trends from the magnetic anomaly
- Upward continuation
- Reduction to the pole
Other functions within this module are useful to compute analytical solutions of the radial power spectrum, according to Bouligand et al. (2009), Maus and Dimri (1995), and the decomposition of from the magnetic anomaly according to Tanaka et al. (1999):
bouligand2009()
: analytic solution used inCurieOptimise
maus1995()
: simplified version ofbouligand2009()
without higher order integration.tanaka1999()
: to be used in conjunction withComputeTanaka()
Source code
# Copyright 2018-2019 Ben Mather, Robert Delhaye
#
# This file is part of PyCurious.
#
# PyCurious is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or any later version.
#
# PyCurious is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCurious. If not, see <http://www.gnu.org/licenses/>.
"""
This PyCurious module contains the `pycurious.grid.CurieGrid` class,
which can be initialised with a magnetic grid of equal spacing in the x and y direction.
It contains methods for the following functionality:
- Decomposition of subgrids for processing square windows of the magnetic anomaly
- Removing linear trends from the magnetic anomaly
- Upward continuation
- Reduction to the pole
Other functions within this module are useful to compute analytical solutions
of the radial power spectrum, \\( \\Phi \\) according to Bouligand *et al.* (2009),
Maus and Dimri (1995), and the decomposition of \\( \\Phi \\) from the magnetic
anomaly according to Tanaka *et al.* (1999):
- `bouligand2009`: analytic solution used in `pycurious.optimise.CurieOptimise`
- `maus1995`: simplified version of `bouligand2009` without higher order integration.
- `tanaka1999`: to be used in conjunction with `ComputeTanaka`
"""
# -*- coding: utf-8 -*-
import numpy as np
from scipy.special import gamma, kv
import warnings
try:
range = xrange
except:
pass
class CurieGrid(object):
"""
Accepts a 2D array and Cartesian coordinates specifying the
bounding box of the array
Grid must be projected in metres.
Args:
grid : 2D numpy array
2D array of magnetic data
xmin : float
minimum x bound in metres
xmax : float
maximum x bound in metres
ymin : float
minimum y bound in metres
ymax : float
maximum y bound in metres
Attributes:
grid : 2D numpy array
2D array of magnetic data
xmin : float
minimum x bound in metres
xmax : float
maximum x bound in metres
ymin : float
minimum y bound in metres
ymax : float
maximum y bound in metres
dx : float
grid spacing in the x-direction in metres
dy : float
grid spacing in the y-direction in metres
nx : int
number of nodes in the x-direction
ny : int
number of nodes in the y-direction
xcoords : 1D numpy array
1D numpy array of coordinates in the x-direction
ycoords : 1D numpy array
1D numpy array of coordinates in the y-direction
Notes:
In all instances `x` indicates eastings in metres and `y` indicates northings.
Using a grid of longitude / latitudinal coordinates (degrees) will result
in incorrect Curie depth calculations.
"""
def __init__(self, grid, xmin, xmax, ymin, ymax):
self.data = np.array(grid)
ny, nx = self.data.shape
self.xmin, self.xmax = xmin, xmax
self.ymin, self.ymax = ymin, ymax
self.xcoords, dx = np.linspace(xmin, xmax, nx, retstep=True)
self.ycoords, dy = np.linspace(ymin, ymax, ny, retstep=True)
self.nx, self.ny = nx, ny
self.dx, self.dy = dx, dy
if not np.allclose(dx, dy, 1.0):
raise ValueError("node spacing should be identical {}".format((dx, dy)))
def subgrid(self, window, xc, yc):
"""
Extract a subgrid from the data at a window around
the point (xc,yc)
Args:
xc : float
x coordinate
yc : float
y coordinate
window : float
size of window in metres
Returns:
data : 2D array
subgrid encompassing window size
"""
# check whether coordinate is inside grid
if xc < self.xmin or xc > self.xmax or yc < self.ymin or yc > self.ymax:
raise ValueError("Point {} outside data range".format((xc, yc)))
# find nearest index to xc,yc
ix = np.abs(self.xcoords - xc).argmin()
iy = np.abs(self.ycoords - yc).argmin()
nw = int(round(window / self.dx))
n2w = nw // 2
# extract a square window from the data
imin = ix - n2w
imax = ix + n2w + 1
jmin = iy - n2w
jmax = iy + n2w + 1
# check whether window fits inside grid
if imin < 0 or imax > self.nx or jmin < 0 or jmax > self.ny:
raise ValueError(
"Window size {} at centroid {} exceeds the data range".format(
window, (xc, yc)
)
)
data = self.data[jmin:jmax, imin:imax]
return data
def create_centroid_list(self, window, spacingX=None, spacingY=None):
"""
Create a list of xc,yc values to extract subgrids.
Args:
window : float
size of the windows in metres
spacingX : float (optional)
specify spacing in metres in the X direction
will default to maximum X resolution
spacingY : float (optional)
specify spacing in metres in the Y direction
will default to maximum Y resolution
Returns:
xc_list : 1D array
array of x coordinates
yc_list : 1D array
array of y coordinates
"""
xcoords = self.xcoords
ycoords = self.ycoords
nw = int(round(window / self.dx))
n2w = nw // 2
# this is the densest spacing possible given the data
xc = xcoords[n2w:-n2w]
yc = ycoords[n2w:-n2w]
# but we can alter it if required
if spacingX is not None:
xc = np.arange(xc.min(), xc.max(), spacingX)
if spacingY is not None:
yc = np.arange(yc.min(), yc.max(), spacingY)
xq, yq = np.meshgrid(xc, yc)
return xq.ravel(), yq.ravel()
def remove_trend_linear(self, data):
"""
Remove the best-fitting linear trend from the data
This may come in handy if the magnetic data has not been
reduced to the pole.
Args:
data : 2D numpy array
Returns:
data : 2D numpy array
"""
nr, nc = data.shape
yq, xq = np.mgrid[0:nc, 0:nr]
A = np.c_[xq.ravel(), yq.ravel(), np.ones(xq.size)]
c, resid, rank, sigma = np.linalg.lstsq(A, data.ravel(), rcond=None)
return data - np.dot(A, c).reshape(data.shape)
def _taper_spectrum(self, subgrid, taper=np.hanning, scale=0.001, **kwargs):
"""
Template for tapering the power spectrum used in:
- `radial_spectrum`
- `radial_spectrum_log`
- `azimuthal_spectrum`
"""
data = subgrid
nr, nc = data.shape
if nr != nc:
warnings.warn("subgrid is not square {}".format((nr, nc)), RuntimeWarning)
# control taper
if taper is None:
vtaper = 1.0
else:
rt = taper(nr, **kwargs)
ct = taper(nc, **kwargs)
xq, yq = np.meshgrid(ct, rt)
vtaper = xq * yq
# scaling factor to transform wavenumber into units of rad/km
dx_scale = self.dx * scale
dk = 2.0 * np.pi / (nr - 1) / dx_scale
kbins = np.arange(dk, dk * nr / 2, dk)
return vtaper, dk, kbins
def _FFT_spectrum(self, subgrid, vtaper, dk, kbins, const):
"""
Template for computing the (fast) Fourier transform used in:
- `radial_spectrum`
- `radial_spectrum_log`
A constant `const` should be applied to the FFT of the magnetic anomaly
to convert `S` and `sigma` to specific units for further analysis.
It is useful to remember that:
```python
2*log(FFT) == log(FFT**2)
```
"""
data = subgrid
nr, nc = data.shape
nbins = kbins.size - 1
# fast Fourier transform and shift
FT = np.abs(np.fft.fft2(data * vtaper))
FT = np.fft.fftshift(FT)
S = np.empty(nbins)
k = np.empty(nbins)
sigma = np.empty(nbins)
i0 = int((nr - 1) // 2)
ix, iy = np.mgrid[0:nr, 0:nr]
kk = np.hypot((ix - i0) * dk, (iy - i0) * dk)
for i in range(nbins):
mask = np.logical_and(kk >= kbins[i], kk <= kbins[i + 1])
rr = const * np.log(FT[mask])
S[i] = rr.mean()
k[i] = kk[mask].mean()
sigma[i] = np.std(rr)
return k, S, sigma
def radial_spectrum(self, subgrid, taper=np.hanning, power=2.0, **kwargs):
"""
Compute the radial spectrum for a square grid.
> Wavenumber is returned in values of __rad/km__
Args:
subgrid : 2D array
window of the original data (see subgrid method)
taper : function (default=np.hanning)
taper function, set to None for no taper function
power : float
raise the FFT of the magnetic anomaly to the power.
- 2.0 for Bouligand _et al._ (2009) use cases
- 0.5 for Tanaka _et al.__ (1999) use cases
kwargs : keyword arguments
keyword arguments to pass to `taper`
Returns:
k : 1D array shape (n,)
wavenumber in rad/km
Phi : 1D array shape (n,)
Radial power spectrum
sigma_Phi : 1D array shape (n,)
Standard deviation of Phi
Notes:
While `subgrid` is projected in eastings / northings (in metres),
the wavenumber, \\( k \\), is returned in units of rad/km.
This is because both Bouligand *et al.* (2009) and Tanaka *et al.*
(1999) require the computation of Curie depth in these units.
References:
Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie
temperature depth in the western United States with a fractal model for
crustal magnetization, J. Geophys. Res., 114, B11104,
doi:10.1029/2009JB006494
Tanaka, A., Okubo, Y., & Matsubayashi, O. (1999). Curie point depth
based on spectrum analysis of the magnetic anomaly data in East and
Southeast Asia. Tectonophysics, 306(3–4), 461–470.
doi:10.1016/S0040-1951(99)00072-4
"""
# bin the spectrum and compute the taper
vtaper, dk, kbins = self._taper_spectrum(subgrid, taper, **kwargs)
# calculate the Fourier transform and apply scaling constant to retrieve
# values compatible with Bouligand or Tanaka analysis
return self._FFT_spectrum(subgrid, vtaper, dk, kbins, power)
def azimuthal_spectrum(
self, subgrid, taper=np.hanning, power=2.0, theta=5.0, **kwargs
):
"""
Compute azimuthal spectrum for a square grid.
> Wavenumber is returned in values of __rad/km__
Args:
subgrid : 2D array
window of the original data (see subgrid method)
taper : function (default=np.hanning)
taper function, set to None for no taper function
theta : float
angle increment in degrees
args : arguments
arguments o pass to taper
Returns:
k : 1D array shape (n,)
wavenumber in rad/km
Phi : 1D array shape (n,)
Radial power spectrum
sigma_Phi : 1D array shape (n,)
Standard deviation of Phi
Notes:
While `subgrid` is projected in eastings / northings (in metres),
the wavenumber, \\( k \\), is returned in units of rad/km.
This is because both Bouligand *et al.* (2009) and Tanaka *et al.*
(1999) require the computation of Curie depth in these units.
References:
Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie
temperature depth in the western United States with a fractal model for
crustal magnetization, J. Geophys. Res., 114, B11104,
doi:10.1029/2009JB006494
Tanaka, A., Okubo, Y., & Matsubayashi, O. (1999). Curie point depth
based on spectrum analysis of the magnetic anomaly data in East and
Southeast Asia. Tectonophysics, 306(3–4), 461–470.
doi:10.1016/S0040-1951(99)00072-4
"""
from pycurious import radon
vtaper, dk, kbins = self._taper_spectrum(subgrid, taper, **kwargs)
dtheta = np.arange(0.0, 180.0, theta)
sinogram = radon.radon2d(subgrid, np.pi * dtheta / 180.0)
S = np.zeros((dtheta.size, kbins.size))
# control taper
if taper is None:
vtaper = 1.0
else:
vtaper = taper(sinogram.shape[0], **kwargs)
nk = 1 + 2 * kbins.size
for i in range(0, dtheta.size):
PSD = np.abs(np.fft.fft(vtaper * sinogram[:, i], n=nk))
S[i, :] = power * np.log(PSD[1 : kbins.size + 1])
return kbins, S, dtheta
def reduce_to_pole(self, data, inc, dec, sinc=None, sdec=None):
"""
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 (Blakely, 1996).
Args:
data : 1D array
the total field anomaly data at each point.
inc : float / 1D array
inclination of the inducing Geomagnetic field
dec : float / 1D array
declination of the inducing Geomagnetic field
sinc : float / 1D array (optional)
inclination of the total magnetization of the anomaly source
sdec : float / 1D array (optional)
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 : 2D array
the data reduced to the pole.
References:
Blakely, R. J. (1996), Potential Theory in Gravity and Magnetic
Applications, Cambridge University Press.
Notes:
This functions performs the reduction in the frequency domain
(using the FFT). The transform filter is (in the freq domain):
\\( 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 \\( k_x, k_y \\) are the wave-numbers in the x and y
directions and
\\( |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 \\)
\\( \\mathbf{m} = (m_x, m_y, m_z) \\) is the unit-vector of the total
magnetization of the source and
\\( \\mathbf{f} = (f_x, f_y, f_z) \\) is the unit-vector of the
Geomagnetic field.
"""
nr, nc = data.shape
if nr != nc:
warnings.warn("subgrid is not square {}".format((nr, nc)), RuntimeWarning)
fx, fy, fz = ang2vec(1.0, inc, dec)
if sinc is None or sdec is None:
mx, my, mz = fx, fy, fz
else:
mx, my, mz = ang2vec(1.0, sinc, sdec)
kx, ky = [k for k in _fftfreqs(self.dx, self.dy, data.shape)]
kz = np.hypot(kx, ky)
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 np.errstate(divide="ignore", invalid="ignore"):
rtp = (kz) / (
a1 * kx ** 2
+ a2 * ky ** 2
+ a3 * kx * ky
+ 1j * np.sqrt(kz) * (b1 * kx + b2 * ky)
)
rtp[0, 0] = 0
ft_pole = rtp * np.fft.fft2(data)
return np.real(np.fft.ifft2(ft_pole))
def upward_continuation(self, data, height):
"""
Upward continuation of potential field data.
Calculates the continuation through the Fast Fourier Transform in
the wavenumber domain (Blakely, 1996):
\\( F\\{h_{up}\\} = F\\{h\\} e^{-\\Delta z |k|} \\)
and then transformed back to the space domain. \\( h_{up} \\) is the
upward continue data, \\( \\Delta z \\) is the height increase,
\\( F \\) denotes the Fourier Transform,
\\( |k| \\) is the wavenumber modulus.
Args:
data : 2D array
potential field at the grid points
height : float
height increase (delta z) in meters.
Returns:
cont : array
upward continued data
References:
Blakely, R. J. (1996), Potential Theory in Gravity and Magnetic
Applications, Cambridge University Press.
"""
nr, nc = data.shape
if nr != nc:
warnings.warn("subgrid is not square {}".format((nr, nc)), RuntimeWarning)
if height <= 0:
warnings.warn(
"Using 'height' <= 0 means downward continuation, "
+ "which is known to be unstable."
)
fx = 2.0 * np.pi * np.fft.fftfreq(nr, self.dx)
fy = 2.0 * np.pi * np.fft.fftfreq(nc, self.dy)
kx, ky = np.meshgrid(fy, fx)[::-1]
kz = np.hypot(kx, ky)
upcont_ft = np.fft.fft2(data) * np.exp(-height * kz)
cont = np.real(np.fft.ifft2(upcont_ft))
return cont
# Helper functions to calculate Curie depth
def bouligand2009(kh, beta, zt, dz, C):
"""
Calculate the synthetic radial power spectrum of
magnetic anomalies
Equation (4) of Bouligand et al. (2009)
Args:
kh : float / 1D array
wavenumber in rad/km
beta : float / 1D array
fractal parameter
zt : float / 1D array
top of magnetic sources
dz : float / 1D array
thickness of magnetic sources
C : float 1D array
field constant (Maus et al., 1997)
Returns:
Phi : float / 1D array
radial power spectrum of magnetic anomalies
References:
Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie
temperature depth in the western United States with a fractal model for
crustal magnetization, J. Geophys. Res., 114, B11104,
doi:10.1029/2009JB006494
Maus, S., D. Gordon, and D. Fairhead (1997), Curie temperature depth
estimation using a self-similar magnetization model, Geophys. J. Int.,
129, 163-168, doi:10.1111/j.1365-246X.1997.tb00945.x
"""
# from scipy.special import kv
khdz = kh * dz
coshkhdz = np.cosh(khdz)
Phi1d = C - 2.0 * kh * zt - (beta - 1.0) * np.log(kh) - khdz
A = (
np.sqrt(np.pi)
/ gamma(1.0 + 0.5 * beta)
* (
0.5 * coshkhdz * gamma(0.5 * (1.0 + beta))
- kv((-0.5 * (1.0 + beta)), khdz)
* np.power(0.5 * khdz, (0.5 * (1.0 + beta)))
)
)
Phi1d += np.log(A)
return Phi1d
def tanaka1999(k, lnPhi, sigma_lnPhi, kmin_range=(0.05, 0.2), kmax_range=(0.05, 0.2)):
"""
Compute weighted linear fit of Phi over spatial frequency window kmin:kmax
Args:
k : float / 1D-array
wavenumber in rad/km
lnPhi : float / 1D array
log of the radial power spectrum (see power_spectrum_log)
expected in ln(sqrt(S)) form
sigma_lnPhi : standard deviation of lnPhi
kmin_range : tuple (default:(0.05, 0.2))
minimum and maximum range of spatial frequencies to fit for the
top of magnetic sources - ideally low frequency, straight line
kmax_range : tuple (default:(0.05, 0.2))
minimum and maximum range of spatial frequencies to fit for the
bottom of magnetic source - ideally low frequency, straight line
Returns:
upper_source : tuple
(Ztr,btr,dZtr) gradient, intercept, error for the top of magnetic sources
lower_source : tuple
(Zor,bor,dZor) gradient, intercept, error for the bottom of magnetic sources
"""
# for now...
S = lnPhi
sigma2 = sigma_lnPhi ** 2
def compute_coefficients(X, Y, E):
X2 = X ** 2
Y2 = Y ** 2
E2 = E ** 2
XY = np.multiply(X, Y)
XE2sum = np.sum(X / E2)
YE2sum = np.sum(Y / E2)
rE2sum = np.sum(1.0 / E2)
X2E2sum = np.sum(X2 / E2)
# TL = XE2sum*YE2sum - np.sum(XY/E2*rE2sum)
# I think summation in second TL term needed to be split
TL = XE2sum * YE2sum - np.sum(XY / E2) * rE2sum
BL = XE2sum ** 2 - X2E2sum * rE2sum
Z = TL / BL
b = (np.sum(XY / E2) - Z * X2E2sum) / XE2sum
# dZ = np.sqrt( rE2sum/(X2E2sum*rE2sum - XE2sum) )
## There was a missing **2 term at end of error term.
dZ = np.sqrt(rE2sum / (X2E2sum * rE2sum - XE2sum ** 2))
return Z, b, dZ
sf = k / (2.0 * np.pi)
# mask low wavenumbers
kmin, kmax = kmin_range
mask1 = np.logical_and(sf >= kmin, sf <= kmax)
X1 = sf[mask1]
Y1 = S[mask1]
E1 = sigma2[mask1]
# mask high wavenumbers
kmin, kmax = kmax_range
mask2 = np.logical_and(sf >= kmin, sf <= kmax)
X2 = sf[mask2]
Y2 = np.log(np.exp(S[mask2]) / (X2 * 2 * np.pi))
E2 = np.log(np.exp(sigma2[mask2]) / (X2 * 2 * np.pi))
# compute top and bottom of magnetic layer
Ztr, btr, dZtr = compute_coefficients(X1, Y1, E1)
Zor, bor, dZor = compute_coefficients(X2, Y2, E2)
return (Ztr, btr, dZtr), (Zor, bor, dZor)
def ComputeTanaka(Ztr, dZtr, Zor, dZor):
"""
Compute the Curie depth from the results of tanaka1999
Args:
Ztr : float / 1D array
top of the magnetic source
dZtr : float / 1D array
error of Ztr
Zor : float / 1D array
centroid depth of the magnetic source
dZor : float / 1D array
error of Zor
Returns:
Zb : float / 1D array
estimated Curie point depth at bottom of magnetic source
eZb : float / 1D array
error of `Zb`
"""
Zb = 2.0 * Zor - Ztr
dZb = 2.0 * dZor + dZtr
return abs(Zb), dZb
def maus1995(beta, zt, kh, C=0.0):
"""
Calculate the synthetic radial power spectrum of
magnetic anomalies (Maus and Dimri; 1995)
This is not all that useful except when testing
overflow errors which occur for the second term
in Bouligand et al. (2009).
Args:
beta : float / 1D array
fractal parameter
zt : float / 1D array
top of magnetic sources
kh : float / 1D array
norm of the wave number in the horizontal plane
C : float / 1D array
field constant (Maus et al., 1997)
Returns:
Phi : float / 1D array
radial power spectrum of magnetic anomalies
References:
Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie
temperature depth in the western United States with a fractal model for
crustal magnetization, J. Geophys. Res., 114, B11104,
doi:10.1029/2009JB006494
Maus, S., D. Gordon, and D. Fairhead (1997), Curie temperature depth
estimation using a self-similar magnetization model, Geophys. J. Int.,
129, 163-168, doi:10.1111/j.1365-246X.1997.tb00945.x
"""
return C - 2.0 * kh * zt - (beta - 1.0) * np.log(kh)
def _fftfreqs(dx, dy, shape):
"""
Get two 2D-arrays with the wave numbers in the x and y directions.
"""
fx = 2.0 * np.pi * np.fft.fftfreq(shape[0], dx)
fy = 2.0 * np.pi * np.fft.fftfreq(shape[1], dy)
return np.meshgrid(fy, fx)[::-1]
def ang2vec(intensity, inc, dec):
"""
Convert intensity, inclination and declination to a 3-component vector
Args:
intensity : float or 1D array
The intensity (norm) of the vector
inc : float
The inclination of the vector (in degrees)
dec : float
The declination of the vector (in degrees)
Returns:
vec : array = [x, y, z]
3-component vector
Notes:
Coordinate system is assumed to be x->North, y->East, z->Down.
Inclination is positive down and declination is measured with respect
to x (North).
Examples:
>>> import numpy
>>> print ang2vec(3, 45, 45)
[ 1.5 1.5 2.12132034]
>>> print ang2vec(numpy.arange(4), 45, 45)
[[ 0. 0. 0. ]
[ 0.5 0.5 0.70710678]
[ 1. 1. 1.41421356]
[ 1.5 1.5 2.12132034]]
"""
return np.transpose([intensity * i for i in dircos(inc, dec)])
def dircos(inc, dec):
"""
Returns the 3 coordinates of a unit vector given its inclination and
declination.
Args:
inc : float
The inclination of the vector (in degrees)
dec : float
The declination of the vector (in degrees)
Returns:
vect : list
The unit vector = [x, y, z]
Notes:
Coordinate system is assumed to be x->North, y->East, z->Down.
Inclination is positive down and declination is measured with respect
to x (North).
"""
d2r = np.pi / 180.0
vect = [
np.cos(d2r * inc) * np.cos(d2r * dec),
np.cos(d2r * inc) * np.sin(d2r * dec),
np.sin(d2r * inc),
]
return vect
Functions
def ComputeTanaka(Ztr, dZtr, Zor, dZor)
-
Compute the Curie depth from the results of tanaka1999
Args
Ztr
:float
/1D
array
- top of the magnetic source
dZtr
:float
/1D
array
- error of Ztr
Zor
:float
/1D
array
- centroid depth of the magnetic source
dZor
:float
/1D
array
- error of Zor
Returns
Zb
:float
/1D
array
- estimated Curie point depth at bottom of magnetic source
eZb
:float
/1D
array
- error of
Zb
Source code
def ComputeTanaka(Ztr, dZtr, Zor, dZor): """ Compute the Curie depth from the results of tanaka1999 Args: Ztr : float / 1D array top of the magnetic source dZtr : float / 1D array error of Ztr Zor : float / 1D array centroid depth of the magnetic source dZor : float / 1D array error of Zor Returns: Zb : float / 1D array estimated Curie point depth at bottom of magnetic source eZb : float / 1D array error of `Zb` """ Zb = 2.0 * Zor - Ztr dZb = 2.0 * dZor + dZtr return abs(Zb), dZb
def ang2vec(intensity, inc, dec)
-
Convert intensity, inclination and declination to a 3-component vector
Args
intensity
:float
or1D
array
- The intensity (norm) of the vector
inc
:float
- The inclination of the vector (in degrees)
dec
:float
- The declination of the vector (in degrees)
Returns
vec
:array
= [x
,y
,z
]- 3-component vector
Notes
Coordinate system is assumed to be x->North, y->East, z->Down. Inclination is positive down and declination is measured with respect to x (North).
Examples
>>> import numpy >>> print ang2vec(3, 45, 45) [ 1.5 1.5 2.12132034] >>> print ang2vec(numpy.arange(4), 45, 45) [[ 0. 0. 0. ]
[ 0.5 0.5 0.70710678] [ 1. 1. 1.41421356] [ 1.5 1.5 2.12132034]]
Source code
def ang2vec(intensity, inc, dec): """ Convert intensity, inclination and declination to a 3-component vector Args: intensity : float or 1D array The intensity (norm) of the vector inc : float The inclination of the vector (in degrees) dec : float The declination of the vector (in degrees) Returns: vec : array = [x, y, z] 3-component vector Notes: Coordinate system is assumed to be x->North, y->East, z->Down. Inclination is positive down and declination is measured with respect to x (North). Examples: >>> import numpy >>> print ang2vec(3, 45, 45) [ 1.5 1.5 2.12132034] >>> print ang2vec(numpy.arange(4), 45, 45) [[ 0. 0. 0. ] [ 0.5 0.5 0.70710678] [ 1. 1. 1.41421356] [ 1.5 1.5 2.12132034]] """ return np.transpose([intensity * i for i in dircos(inc, dec)])
def bouligand2009(kh, beta, zt, dz, C)
-
Calculate the synthetic radial power spectrum of magnetic anomalies
Equation (4) of Bouligand et al. (2009)
Args
kh
:float
/1D
array
- wavenumber in rad/km
beta
:float
/1D
array
- fractal parameter
zt
:float
/1D
array
- top of magnetic sources
dz
:float
/1D
array
- thickness of magnetic sources
C
:float
1D
array
- field constant (Maus et al., 1997)
Returns
Phi
:float
/1D
array
- radial power spectrum of magnetic anomalies
References
Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie temperature depth in the western United States with a fractal model for crustal magnetization, J. Geophys. Res., 114, B11104, doi:10.1029/2009JB006494
Maus, S., D. Gordon, and D. Fairhead (1997), Curie temperature depth estimation using a self-similar magnetization model, Geophys. J. Int., 129, 163-168, doi:10.1111/j.1365-246X.1997.tb00945.x
Source code
def bouligand2009(kh, beta, zt, dz, C): """ Calculate the synthetic radial power spectrum of magnetic anomalies Equation (4) of Bouligand et al. (2009) Args: kh : float / 1D array wavenumber in rad/km beta : float / 1D array fractal parameter zt : float / 1D array top of magnetic sources dz : float / 1D array thickness of magnetic sources C : float 1D array field constant (Maus et al., 1997) Returns: Phi : float / 1D array radial power spectrum of magnetic anomalies References: Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie temperature depth in the western United States with a fractal model for crustal magnetization, J. Geophys. Res., 114, B11104, doi:10.1029/2009JB006494 Maus, S., D. Gordon, and D. Fairhead (1997), Curie temperature depth estimation using a self-similar magnetization model, Geophys. J. Int., 129, 163-168, doi:10.1111/j.1365-246X.1997.tb00945.x """ # from scipy.special import kv khdz = kh * dz coshkhdz = np.cosh(khdz) Phi1d = C - 2.0 * kh * zt - (beta - 1.0) * np.log(kh) - khdz A = ( np.sqrt(np.pi) / gamma(1.0 + 0.5 * beta) * ( 0.5 * coshkhdz * gamma(0.5 * (1.0 + beta)) - kv((-0.5 * (1.0 + beta)), khdz) * np.power(0.5 * khdz, (0.5 * (1.0 + beta))) ) ) Phi1d += np.log(A) return Phi1d
def dircos(inc, dec)
-
Returns the 3 coordinates of a unit vector given its inclination and declination.
Args
inc
:float
- The inclination of the vector (in degrees)
dec
:float
- The declination of the vector (in degrees)
Returns
vect
:list
- The unit vector = [x, y, z]
Notes
Coordinate system is assumed to be x->North, y->East, z->Down. Inclination is positive down and declination is measured with respect to x (North).
Source code
def dircos(inc, dec): """ Returns the 3 coordinates of a unit vector given its inclination and declination. Args: inc : float The inclination of the vector (in degrees) dec : float The declination of the vector (in degrees) Returns: vect : list The unit vector = [x, y, z] Notes: Coordinate system is assumed to be x->North, y->East, z->Down. Inclination is positive down and declination is measured with respect to x (North). """ d2r = np.pi / 180.0 vect = [ np.cos(d2r * inc) * np.cos(d2r * dec), np.cos(d2r * inc) * np.sin(d2r * dec), np.sin(d2r * inc), ] return vect
def maus1995(beta, zt, kh, C=0.0)
-
Calculate the synthetic radial power spectrum of magnetic anomalies (Maus and Dimri; 1995)
This is not all that useful except when testing overflow errors which occur for the second term in Bouligand et al. (2009).
Args
beta
:float
/1D
array
- fractal parameter
zt
:float
/1D
array
- top of magnetic sources
kh
:float
/1D
array
- norm of the wave number in the horizontal plane
C
:float
/1D
array
- field constant (Maus et al., 1997)
Returns
Phi
:float
/1D
array
- radial power spectrum of magnetic anomalies
References
Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie temperature depth in the western United States with a fractal model for crustal magnetization, J. Geophys. Res., 114, B11104, doi:10.1029/2009JB006494
Maus, S., D. Gordon, and D. Fairhead (1997), Curie temperature depth estimation using a self-similar magnetization model, Geophys. J. Int., 129, 163-168, doi:10.1111/j.1365-246X.1997.tb00945.x
Source code
def maus1995(beta, zt, kh, C=0.0): """ Calculate the synthetic radial power spectrum of magnetic anomalies (Maus and Dimri; 1995) This is not all that useful except when testing overflow errors which occur for the second term in Bouligand et al. (2009). Args: beta : float / 1D array fractal parameter zt : float / 1D array top of magnetic sources kh : float / 1D array norm of the wave number in the horizontal plane C : float / 1D array field constant (Maus et al., 1997) Returns: Phi : float / 1D array radial power spectrum of magnetic anomalies References: Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie temperature depth in the western United States with a fractal model for crustal magnetization, J. Geophys. Res., 114, B11104, doi:10.1029/2009JB006494 Maus, S., D. Gordon, and D. Fairhead (1997), Curie temperature depth estimation using a self-similar magnetization model, Geophys. J. Int., 129, 163-168, doi:10.1111/j.1365-246X.1997.tb00945.x """ return C - 2.0 * kh * zt - (beta - 1.0) * np.log(kh)
def tanaka1999(k, lnPhi, sigma_lnPhi, kmin_range=(0.05, 0.2), kmax_range=(0.05, 0.2))
-
Compute weighted linear fit of Phi over spatial frequency window kmin:kmax
Args
k
:float
/1D
-array
- wavenumber in rad/km
lnPhi
:float
/1D
array
- log of the radial power spectrum (see power_spectrum_log) expected in ln(sqrt(S)) form
sigma_lnPhi
:standard
deviation
oflnPhi
kmin_range
:tuple
(default:(0.05
,0.2
))- minimum and maximum range of spatial frequencies to fit for the top of magnetic sources - ideally low frequency, straight line
kmax_range
:tuple
(default:(0.05
,0.2
))- minimum and maximum range of spatial frequencies to fit for the bottom of magnetic source - ideally low frequency, straight line
Returns
upper_source
:tuple
- (Ztr,btr,dZtr) gradient, intercept, error for the top of magnetic sources
lower_source
:tuple
- (Zor,bor,dZor) gradient, intercept, error for the bottom of magnetic sources
Source code
def tanaka1999(k, lnPhi, sigma_lnPhi, kmin_range=(0.05, 0.2), kmax_range=(0.05, 0.2)): """ Compute weighted linear fit of Phi over spatial frequency window kmin:kmax Args: k : float / 1D-array wavenumber in rad/km lnPhi : float / 1D array log of the radial power spectrum (see power_spectrum_log) expected in ln(sqrt(S)) form sigma_lnPhi : standard deviation of lnPhi kmin_range : tuple (default:(0.05, 0.2)) minimum and maximum range of spatial frequencies to fit for the top of magnetic sources - ideally low frequency, straight line kmax_range : tuple (default:(0.05, 0.2)) minimum and maximum range of spatial frequencies to fit for the bottom of magnetic source - ideally low frequency, straight line Returns: upper_source : tuple (Ztr,btr,dZtr) gradient, intercept, error for the top of magnetic sources lower_source : tuple (Zor,bor,dZor) gradient, intercept, error for the bottom of magnetic sources """ # for now... S = lnPhi sigma2 = sigma_lnPhi ** 2 def compute_coefficients(X, Y, E): X2 = X ** 2 Y2 = Y ** 2 E2 = E ** 2 XY = np.multiply(X, Y) XE2sum = np.sum(X / E2) YE2sum = np.sum(Y / E2) rE2sum = np.sum(1.0 / E2) X2E2sum = np.sum(X2 / E2) # TL = XE2sum*YE2sum - np.sum(XY/E2*rE2sum) # I think summation in second TL term needed to be split TL = XE2sum * YE2sum - np.sum(XY / E2) * rE2sum BL = XE2sum ** 2 - X2E2sum * rE2sum Z = TL / BL b = (np.sum(XY / E2) - Z * X2E2sum) / XE2sum # dZ = np.sqrt( rE2sum/(X2E2sum*rE2sum - XE2sum) ) ## There was a missing **2 term at end of error term. dZ = np.sqrt(rE2sum / (X2E2sum * rE2sum - XE2sum ** 2)) return Z, b, dZ sf = k / (2.0 * np.pi) # mask low wavenumbers kmin, kmax = kmin_range mask1 = np.logical_and(sf >= kmin, sf <= kmax) X1 = sf[mask1] Y1 = S[mask1] E1 = sigma2[mask1] # mask high wavenumbers kmin, kmax = kmax_range mask2 = np.logical_and(sf >= kmin, sf <= kmax) X2 = sf[mask2] Y2 = np.log(np.exp(S[mask2]) / (X2 * 2 * np.pi)) E2 = np.log(np.exp(sigma2[mask2]) / (X2 * 2 * np.pi)) # compute top and bottom of magnetic layer Ztr, btr, dZtr = compute_coefficients(X1, Y1, E1) Zor, bor, dZor = compute_coefficients(X2, Y2, E2) return (Ztr, btr, dZtr), (Zor, bor, dZor)
Classes
class CurieGrid (grid, xmin, xmax, ymin, ymax)
-
Accepts a 2D array and Cartesian coordinates specifying the bounding box of the array
Grid must be projected in metres.
Args
grid
:2D
numpy
array
- 2D array of magnetic data
xmin
:float
- minimum x bound in metres
xmax
:float
- maximum x bound in metres
ymin
:float
- minimum y bound in metres
ymax
:float
- maximum y bound in metres
Attributes
grid
:2D
numpy
array
- 2D array of magnetic data
xmin
:float
- minimum x bound in metres
xmax
:float
- maximum x bound in metres
ymin
:float
- minimum y bound in metres
ymax
:float
- maximum y bound in metres
dx
:float
- grid spacing in the x-direction in metres
dy
:float
- grid spacing in the y-direction in metres
nx
:int
- number of nodes in the x-direction
ny
:int
- number of nodes in the y-direction
xcoords
:1D
numpy
array
- 1D numpy array of coordinates in the x-direction
ycoords
:1D
numpy
array
- 1D numpy array of coordinates in the y-direction
Notes
In all instances
x
indicates eastings in metres andy
indicates northings. Using a grid of longitude / latitudinal coordinates (degrees) will result in incorrect Curie depth calculations.Source code
class CurieGrid(object): """ Accepts a 2D array and Cartesian coordinates specifying the bounding box of the array Grid must be projected in metres. Args: grid : 2D numpy array 2D array of magnetic data xmin : float minimum x bound in metres xmax : float maximum x bound in metres ymin : float minimum y bound in metres ymax : float maximum y bound in metres Attributes: grid : 2D numpy array 2D array of magnetic data xmin : float minimum x bound in metres xmax : float maximum x bound in metres ymin : float minimum y bound in metres ymax : float maximum y bound in metres dx : float grid spacing in the x-direction in metres dy : float grid spacing in the y-direction in metres nx : int number of nodes in the x-direction ny : int number of nodes in the y-direction xcoords : 1D numpy array 1D numpy array of coordinates in the x-direction ycoords : 1D numpy array 1D numpy array of coordinates in the y-direction Notes: In all instances `x` indicates eastings in metres and `y` indicates northings. Using a grid of longitude / latitudinal coordinates (degrees) will result in incorrect Curie depth calculations. """ def __init__(self, grid, xmin, xmax, ymin, ymax): self.data = np.array(grid) ny, nx = self.data.shape self.xmin, self.xmax = xmin, xmax self.ymin, self.ymax = ymin, ymax self.xcoords, dx = np.linspace(xmin, xmax, nx, retstep=True) self.ycoords, dy = np.linspace(ymin, ymax, ny, retstep=True) self.nx, self.ny = nx, ny self.dx, self.dy = dx, dy if not np.allclose(dx, dy, 1.0): raise ValueError("node spacing should be identical {}".format((dx, dy))) def subgrid(self, window, xc, yc): """ Extract a subgrid from the data at a window around the point (xc,yc) Args: xc : float x coordinate yc : float y coordinate window : float size of window in metres Returns: data : 2D array subgrid encompassing window size """ # check whether coordinate is inside grid if xc < self.xmin or xc > self.xmax or yc < self.ymin or yc > self.ymax: raise ValueError("Point {} outside data range".format((xc, yc))) # find nearest index to xc,yc ix = np.abs(self.xcoords - xc).argmin() iy = np.abs(self.ycoords - yc).argmin() nw = int(round(window / self.dx)) n2w = nw // 2 # extract a square window from the data imin = ix - n2w imax = ix + n2w + 1 jmin = iy - n2w jmax = iy + n2w + 1 # check whether window fits inside grid if imin < 0 or imax > self.nx or jmin < 0 or jmax > self.ny: raise ValueError( "Window size {} at centroid {} exceeds the data range".format( window, (xc, yc) ) ) data = self.data[jmin:jmax, imin:imax] return data def create_centroid_list(self, window, spacingX=None, spacingY=None): """ Create a list of xc,yc values to extract subgrids. Args: window : float size of the windows in metres spacingX : float (optional) specify spacing in metres in the X direction will default to maximum X resolution spacingY : float (optional) specify spacing in metres in the Y direction will default to maximum Y resolution Returns: xc_list : 1D array array of x coordinates yc_list : 1D array array of y coordinates """ xcoords = self.xcoords ycoords = self.ycoords nw = int(round(window / self.dx)) n2w = nw // 2 # this is the densest spacing possible given the data xc = xcoords[n2w:-n2w] yc = ycoords[n2w:-n2w] # but we can alter it if required if spacingX is not None: xc = np.arange(xc.min(), xc.max(), spacingX) if spacingY is not None: yc = np.arange(yc.min(), yc.max(), spacingY) xq, yq = np.meshgrid(xc, yc) return xq.ravel(), yq.ravel() def remove_trend_linear(self, data): """ Remove the best-fitting linear trend from the data This may come in handy if the magnetic data has not been reduced to the pole. Args: data : 2D numpy array Returns: data : 2D numpy array """ nr, nc = data.shape yq, xq = np.mgrid[0:nc, 0:nr] A = np.c_[xq.ravel(), yq.ravel(), np.ones(xq.size)] c, resid, rank, sigma = np.linalg.lstsq(A, data.ravel(), rcond=None) return data - np.dot(A, c).reshape(data.shape) def _taper_spectrum(self, subgrid, taper=np.hanning, scale=0.001, **kwargs): """ Template for tapering the power spectrum used in: - `radial_spectrum` - `radial_spectrum_log` - `azimuthal_spectrum` """ data = subgrid nr, nc = data.shape if nr != nc: warnings.warn("subgrid is not square {}".format((nr, nc)), RuntimeWarning) # control taper if taper is None: vtaper = 1.0 else: rt = taper(nr, **kwargs) ct = taper(nc, **kwargs) xq, yq = np.meshgrid(ct, rt) vtaper = xq * yq # scaling factor to transform wavenumber into units of rad/km dx_scale = self.dx * scale dk = 2.0 * np.pi / (nr - 1) / dx_scale kbins = np.arange(dk, dk * nr / 2, dk) return vtaper, dk, kbins def _FFT_spectrum(self, subgrid, vtaper, dk, kbins, const): """ Template for computing the (fast) Fourier transform used in: - `radial_spectrum` - `radial_spectrum_log` A constant `const` should be applied to the FFT of the magnetic anomaly to convert `S` and `sigma` to specific units for further analysis. It is useful to remember that: ```python 2*log(FFT) == log(FFT**2) ``` """ data = subgrid nr, nc = data.shape nbins = kbins.size - 1 # fast Fourier transform and shift FT = np.abs(np.fft.fft2(data * vtaper)) FT = np.fft.fftshift(FT) S = np.empty(nbins) k = np.empty(nbins) sigma = np.empty(nbins) i0 = int((nr - 1) // 2) ix, iy = np.mgrid[0:nr, 0:nr] kk = np.hypot((ix - i0) * dk, (iy - i0) * dk) for i in range(nbins): mask = np.logical_and(kk >= kbins[i], kk <= kbins[i + 1]) rr = const * np.log(FT[mask]) S[i] = rr.mean() k[i] = kk[mask].mean() sigma[i] = np.std(rr) return k, S, sigma def radial_spectrum(self, subgrid, taper=np.hanning, power=2.0, **kwargs): """ Compute the radial spectrum for a square grid. > Wavenumber is returned in values of __rad/km__ Args: subgrid : 2D array window of the original data (see subgrid method) taper : function (default=np.hanning) taper function, set to None for no taper function power : float raise the FFT of the magnetic anomaly to the power. - 2.0 for Bouligand _et al._ (2009) use cases - 0.5 for Tanaka _et al.__ (1999) use cases kwargs : keyword arguments keyword arguments to pass to `taper` Returns: k : 1D array shape (n,) wavenumber in rad/km Phi : 1D array shape (n,) Radial power spectrum sigma_Phi : 1D array shape (n,) Standard deviation of Phi Notes: While `subgrid` is projected in eastings / northings (in metres), the wavenumber, \\( k \\), is returned in units of rad/km. This is because both Bouligand *et al.* (2009) and Tanaka *et al.* (1999) require the computation of Curie depth in these units. References: Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie temperature depth in the western United States with a fractal model for crustal magnetization, J. Geophys. Res., 114, B11104, doi:10.1029/2009JB006494 Tanaka, A., Okubo, Y., & Matsubayashi, O. (1999). Curie point depth based on spectrum analysis of the magnetic anomaly data in East and Southeast Asia. Tectonophysics, 306(3–4), 461–470. doi:10.1016/S0040-1951(99)00072-4 """ # bin the spectrum and compute the taper vtaper, dk, kbins = self._taper_spectrum(subgrid, taper, **kwargs) # calculate the Fourier transform and apply scaling constant to retrieve # values compatible with Bouligand or Tanaka analysis return self._FFT_spectrum(subgrid, vtaper, dk, kbins, power) def azimuthal_spectrum( self, subgrid, taper=np.hanning, power=2.0, theta=5.0, **kwargs ): """ Compute azimuthal spectrum for a square grid. > Wavenumber is returned in values of __rad/km__ Args: subgrid : 2D array window of the original data (see subgrid method) taper : function (default=np.hanning) taper function, set to None for no taper function theta : float angle increment in degrees args : arguments arguments o pass to taper Returns: k : 1D array shape (n,) wavenumber in rad/km Phi : 1D array shape (n,) Radial power spectrum sigma_Phi : 1D array shape (n,) Standard deviation of Phi Notes: While `subgrid` is projected in eastings / northings (in metres), the wavenumber, \\( k \\), is returned in units of rad/km. This is because both Bouligand *et al.* (2009) and Tanaka *et al.* (1999) require the computation of Curie depth in these units. References: Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie temperature depth in the western United States with a fractal model for crustal magnetization, J. Geophys. Res., 114, B11104, doi:10.1029/2009JB006494 Tanaka, A., Okubo, Y., & Matsubayashi, O. (1999). Curie point depth based on spectrum analysis of the magnetic anomaly data in East and Southeast Asia. Tectonophysics, 306(3–4), 461–470. doi:10.1016/S0040-1951(99)00072-4 """ from pycurious import radon vtaper, dk, kbins = self._taper_spectrum(subgrid, taper, **kwargs) dtheta = np.arange(0.0, 180.0, theta) sinogram = radon.radon2d(subgrid, np.pi * dtheta / 180.0) S = np.zeros((dtheta.size, kbins.size)) # control taper if taper is None: vtaper = 1.0 else: vtaper = taper(sinogram.shape[0], **kwargs) nk = 1 + 2 * kbins.size for i in range(0, dtheta.size): PSD = np.abs(np.fft.fft(vtaper * sinogram[:, i], n=nk)) S[i, :] = power * np.log(PSD[1 : kbins.size + 1]) return kbins, S, dtheta def reduce_to_pole(self, data, inc, dec, sinc=None, sdec=None): """ 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 (Blakely, 1996). Args: data : 1D array the total field anomaly data at each point. inc : float / 1D array inclination of the inducing Geomagnetic field dec : float / 1D array declination of the inducing Geomagnetic field sinc : float / 1D array (optional) inclination of the total magnetization of the anomaly source sdec : float / 1D array (optional) 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 : 2D array the data reduced to the pole. References: Blakely, R. J. (1996), Potential Theory in Gravity and Magnetic Applications, Cambridge University Press. Notes: This functions performs the reduction in the frequency domain (using the FFT). The transform filter is (in the freq domain): \\( 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 \\( k_x, k_y \\) are the wave-numbers in the x and y directions and \\( |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 \\) \\( \\mathbf{m} = (m_x, m_y, m_z) \\) is the unit-vector of the total magnetization of the source and \\( \\mathbf{f} = (f_x, f_y, f_z) \\) is the unit-vector of the Geomagnetic field. """ nr, nc = data.shape if nr != nc: warnings.warn("subgrid is not square {}".format((nr, nc)), RuntimeWarning) fx, fy, fz = ang2vec(1.0, inc, dec) if sinc is None or sdec is None: mx, my, mz = fx, fy, fz else: mx, my, mz = ang2vec(1.0, sinc, sdec) kx, ky = [k for k in _fftfreqs(self.dx, self.dy, data.shape)] kz = np.hypot(kx, ky) 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 np.errstate(divide="ignore", invalid="ignore"): rtp = (kz) / ( a1 * kx ** 2 + a2 * ky ** 2 + a3 * kx * ky + 1j * np.sqrt(kz) * (b1 * kx + b2 * ky) ) rtp[0, 0] = 0 ft_pole = rtp * np.fft.fft2(data) return np.real(np.fft.ifft2(ft_pole)) def upward_continuation(self, data, height): """ Upward continuation of potential field data. Calculates the continuation through the Fast Fourier Transform in the wavenumber domain (Blakely, 1996): \\( F\\{h_{up}\\} = F\\{h\\} e^{-\\Delta z |k|} \\) and then transformed back to the space domain. \\( h_{up} \\) is the upward continue data, \\( \\Delta z \\) is the height increase, \\( F \\) denotes the Fourier Transform, \\( |k| \\) is the wavenumber modulus. Args: data : 2D array potential field at the grid points height : float height increase (delta z) in meters. Returns: cont : array upward continued data References: Blakely, R. J. (1996), Potential Theory in Gravity and Magnetic Applications, Cambridge University Press. """ nr, nc = data.shape if nr != nc: warnings.warn("subgrid is not square {}".format((nr, nc)), RuntimeWarning) if height <= 0: warnings.warn( "Using 'height' <= 0 means downward continuation, " + "which is known to be unstable." ) fx = 2.0 * np.pi * np.fft.fftfreq(nr, self.dx) fy = 2.0 * np.pi * np.fft.fftfreq(nc, self.dy) kx, ky = np.meshgrid(fy, fx)[::-1] kz = np.hypot(kx, ky) upcont_ft = np.fft.fft2(data) * np.exp(-height * kz) cont = np.real(np.fft.ifft2(upcont_ft)) return cont
Subclasses
Methods
def azimuthal_spectrum(self, subgrid, taper=
, power=2.0, theta=5.0, **kwargs) -
Compute azimuthal spectrum for a square grid.
Wavenumber is returned in values of rad/km
Args
subgrid
:2D
array
- window of the original data (see subgrid method)
taper
:function
(default=np.hanning
)- taper function, set to None for no taper function
theta
:float
- angle increment in degrees
args
:arguments
- arguments o pass to taper
Returns
k
:1D
array
shape
(n
,)- wavenumber in rad/km
Phi
:1D
array
shape
(n
,)- Radial power spectrum
sigma_Phi
:1D
array
shape
(n
,)- Standard deviation of Phi
Notes
While
subgrid
is projected in eastings / northings (in metres), the wavenumber, , is returned in units of rad/km. This is because both Bouligand et al. (2009) and Tanaka et al. (1999) require the computation of Curie depth in these units.References
Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie temperature depth in the western United States with a fractal model for crustal magnetization, J. Geophys. Res., 114, B11104, doi:10.1029/2009JB006494
Tanaka, A., Okubo, Y., & Matsubayashi, O. (1999). Curie point depth based on spectrum analysis of the magnetic anomaly data in East and Southeast Asia. Tectonophysics, 306(3–4), 461–470. doi:10.1016/S0040-1951(99)00072-4
Source code
def azimuthal_spectrum( self, subgrid, taper=np.hanning, power=2.0, theta=5.0, **kwargs ): """ Compute azimuthal spectrum for a square grid. > Wavenumber is returned in values of __rad/km__ Args: subgrid : 2D array window of the original data (see subgrid method) taper : function (default=np.hanning) taper function, set to None for no taper function theta : float angle increment in degrees args : arguments arguments o pass to taper Returns: k : 1D array shape (n,) wavenumber in rad/km Phi : 1D array shape (n,) Radial power spectrum sigma_Phi : 1D array shape (n,) Standard deviation of Phi Notes: While `subgrid` is projected in eastings / northings (in metres), the wavenumber, \\( k \\), is returned in units of rad/km. This is because both Bouligand *et al.* (2009) and Tanaka *et al.* (1999) require the computation of Curie depth in these units. References: Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie temperature depth in the western United States with a fractal model for crustal magnetization, J. Geophys. Res., 114, B11104, doi:10.1029/2009JB006494 Tanaka, A., Okubo, Y., & Matsubayashi, O. (1999). Curie point depth based on spectrum analysis of the magnetic anomaly data in East and Southeast Asia. Tectonophysics, 306(3–4), 461–470. doi:10.1016/S0040-1951(99)00072-4 """ from pycurious import radon vtaper, dk, kbins = self._taper_spectrum(subgrid, taper, **kwargs) dtheta = np.arange(0.0, 180.0, theta) sinogram = radon.radon2d(subgrid, np.pi * dtheta / 180.0) S = np.zeros((dtheta.size, kbins.size)) # control taper if taper is None: vtaper = 1.0 else: vtaper = taper(sinogram.shape[0], **kwargs) nk = 1 + 2 * kbins.size for i in range(0, dtheta.size): PSD = np.abs(np.fft.fft(vtaper * sinogram[:, i], n=nk)) S[i, :] = power * np.log(PSD[1 : kbins.size + 1]) return kbins, S, dtheta
def create_centroid_list(self, window, spacingX=None, spacingY=None)
-
Create a list of xc,yc values to extract subgrids.
Args
window
:float
- size of the windows in metres
spacingX
:float
(optional)- specify spacing in metres in the X direction will default to maximum X resolution
spacingY
:float
(optional)- specify spacing in metres in the Y direction will default to maximum Y resolution
Returns
xc_list
:1D
array
- array of x coordinates
yc_list
:1D
array
- array of y coordinates
Source code
def create_centroid_list(self, window, spacingX=None, spacingY=None): """ Create a list of xc,yc values to extract subgrids. Args: window : float size of the windows in metres spacingX : float (optional) specify spacing in metres in the X direction will default to maximum X resolution spacingY : float (optional) specify spacing in metres in the Y direction will default to maximum Y resolution Returns: xc_list : 1D array array of x coordinates yc_list : 1D array array of y coordinates """ xcoords = self.xcoords ycoords = self.ycoords nw = int(round(window / self.dx)) n2w = nw // 2 # this is the densest spacing possible given the data xc = xcoords[n2w:-n2w] yc = ycoords[n2w:-n2w] # but we can alter it if required if spacingX is not None: xc = np.arange(xc.min(), xc.max(), spacingX) if spacingY is not None: yc = np.arange(yc.min(), yc.max(), spacingY) xq, yq = np.meshgrid(xc, yc) return xq.ravel(), yq.ravel()
def radial_spectrum(self, subgrid, taper=
, power=2.0, **kwargs) -
Compute the radial spectrum for a square grid.
Wavenumber is returned in values of rad/km
Args
subgrid
:2D
array
- window of the original data (see subgrid method)
taper
:function
(default=np.hanning
)- taper function, set to None for no taper function
power
:float
- raise the FFT of the magnetic anomaly to the power. - 2.0 for Bouligand et al. (2009) use cases - 0.5 for Tanaka _et al.__ (1999) use cases
kwargs
:keyword
arguments
- keyword arguments to pass to
taper
Returns
k
:1D
array
shape
(n
,)- wavenumber in rad/km
Phi
:1D
array
shape
(n
,)- Radial power spectrum
sigma_Phi
:1D
array
shape
(n
,)- Standard deviation of Phi
Notes
While
subgrid
is projected in eastings / northings (in metres), the wavenumber, , is returned in units of rad/km. This is because both Bouligand et al. (2009) and Tanaka et al. (1999) require the computation of Curie depth in these units.References
Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie temperature depth in the western United States with a fractal model for crustal magnetization, J. Geophys. Res., 114, B11104, doi:10.1029/2009JB006494
Tanaka, A., Okubo, Y., & Matsubayashi, O. (1999). Curie point depth based on spectrum analysis of the magnetic anomaly data in East and Southeast Asia. Tectonophysics, 306(3–4), 461–470. doi:10.1016/S0040-1951(99)00072-4
Source code
def radial_spectrum(self, subgrid, taper=np.hanning, power=2.0, **kwargs): """ Compute the radial spectrum for a square grid. > Wavenumber is returned in values of __rad/km__ Args: subgrid : 2D array window of the original data (see subgrid method) taper : function (default=np.hanning) taper function, set to None for no taper function power : float raise the FFT of the magnetic anomaly to the power. - 2.0 for Bouligand _et al._ (2009) use cases - 0.5 for Tanaka _et al.__ (1999) use cases kwargs : keyword arguments keyword arguments to pass to `taper` Returns: k : 1D array shape (n,) wavenumber in rad/km Phi : 1D array shape (n,) Radial power spectrum sigma_Phi : 1D array shape (n,) Standard deviation of Phi Notes: While `subgrid` is projected in eastings / northings (in metres), the wavenumber, \\( k \\), is returned in units of rad/km. This is because both Bouligand *et al.* (2009) and Tanaka *et al.* (1999) require the computation of Curie depth in these units. References: Bouligand, C., J. M. G. Glen, and R. J. Blakely (2009), Mapping Curie temperature depth in the western United States with a fractal model for crustal magnetization, J. Geophys. Res., 114, B11104, doi:10.1029/2009JB006494 Tanaka, A., Okubo, Y., & Matsubayashi, O. (1999). Curie point depth based on spectrum analysis of the magnetic anomaly data in East and Southeast Asia. Tectonophysics, 306(3–4), 461–470. doi:10.1016/S0040-1951(99)00072-4 """ # bin the spectrum and compute the taper vtaper, dk, kbins = self._taper_spectrum(subgrid, taper, **kwargs) # calculate the Fourier transform and apply scaling constant to retrieve # values compatible with Bouligand or Tanaka analysis return self._FFT_spectrum(subgrid, vtaper, dk, kbins, power)
def reduce_to_pole(self, data, inc, dec, sinc=None, sdec=None)
-
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 (Blakely, 1996).
Args
data
:1D
array
- the total field anomaly data at each point.
inc
:float
/1D
array
- inclination of the inducing Geomagnetic field
dec
:float
/1D
array
- declination of the inducing Geomagnetic field
sinc
:float
/1D
array
(optional)- inclination of the total magnetization of the anomaly source
sdec
:float
/1D
array
(optional)- 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
:2D
array
- the data reduced to the pole.
References
Blakely, R. J. (1996), Potential Theory in Gravity and Magnetic Applications, Cambridge University Press.
Notes
This functions performs the reduction in the frequency domain (using the FFT). The transform filter is (in the freq domain):
in which are the wave-numbers in the x and y directions and
is the unit-vector of the total magnetization of the source and is the unit-vector of the Geomagnetic field.
Source code
def reduce_to_pole(self, data, inc, dec, sinc=None, sdec=None): """ 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 (Blakely, 1996). Args: data : 1D array the total field anomaly data at each point. inc : float / 1D array inclination of the inducing Geomagnetic field dec : float / 1D array declination of the inducing Geomagnetic field sinc : float / 1D array (optional) inclination of the total magnetization of the anomaly source sdec : float / 1D array (optional) 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 : 2D array the data reduced to the pole. References: Blakely, R. J. (1996), Potential Theory in Gravity and Magnetic Applications, Cambridge University Press. Notes: This functions performs the reduction in the frequency domain (using the FFT). The transform filter is (in the freq domain): \\( 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 \\( k_x, k_y \\) are the wave-numbers in the x and y directions and \\( |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 \\) \\( \\mathbf{m} = (m_x, m_y, m_z) \\) is the unit-vector of the total magnetization of the source and \\( \\mathbf{f} = (f_x, f_y, f_z) \\) is the unit-vector of the Geomagnetic field. """ nr, nc = data.shape if nr != nc: warnings.warn("subgrid is not square {}".format((nr, nc)), RuntimeWarning) fx, fy, fz = ang2vec(1.0, inc, dec) if sinc is None or sdec is None: mx, my, mz = fx, fy, fz else: mx, my, mz = ang2vec(1.0, sinc, sdec) kx, ky = [k for k in _fftfreqs(self.dx, self.dy, data.shape)] kz = np.hypot(kx, ky) 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 np.errstate(divide="ignore", invalid="ignore"): rtp = (kz) / ( a1 * kx ** 2 + a2 * ky ** 2 + a3 * kx * ky + 1j * np.sqrt(kz) * (b1 * kx + b2 * ky) ) rtp[0, 0] = 0 ft_pole = rtp * np.fft.fft2(data) return np.real(np.fft.ifft2(ft_pole))
def remove_trend_linear(self, data)
-
Remove the best-fitting linear trend from the data
This may come in handy if the magnetic data has not been reduced to the pole.
Args
data
:2D
numpy
array
Returns
data
:2D
numpy
array
Source code
def remove_trend_linear(self, data): """ Remove the best-fitting linear trend from the data This may come in handy if the magnetic data has not been reduced to the pole. Args: data : 2D numpy array Returns: data : 2D numpy array """ nr, nc = data.shape yq, xq = np.mgrid[0:nc, 0:nr] A = np.c_[xq.ravel(), yq.ravel(), np.ones(xq.size)] c, resid, rank, sigma = np.linalg.lstsq(A, data.ravel(), rcond=None) return data - np.dot(A, c).reshape(data.shape)
def subgrid(self, window, xc, yc)
-
Extract a subgrid from the data at a window around the point (xc,yc)
Args
xc
:float
- x coordinate
yc
:float
- y coordinate
window
:float
- size of window in metres
Returns
data
:2D
array
- subgrid encompassing window size
Source code
def subgrid(self, window, xc, yc): """ Extract a subgrid from the data at a window around the point (xc,yc) Args: xc : float x coordinate yc : float y coordinate window : float size of window in metres Returns: data : 2D array subgrid encompassing window size """ # check whether coordinate is inside grid if xc < self.xmin or xc > self.xmax or yc < self.ymin or yc > self.ymax: raise ValueError("Point {} outside data range".format((xc, yc))) # find nearest index to xc,yc ix = np.abs(self.xcoords - xc).argmin() iy = np.abs(self.ycoords - yc).argmin() nw = int(round(window / self.dx)) n2w = nw // 2 # extract a square window from the data imin = ix - n2w imax = ix + n2w + 1 jmin = iy - n2w jmax = iy + n2w + 1 # check whether window fits inside grid if imin < 0 or imax > self.nx or jmin < 0 or jmax > self.ny: raise ValueError( "Window size {} at centroid {} exceeds the data range".format( window, (xc, yc) ) ) data = self.data[jmin:jmax, imin:imax] return data
def upward_continuation(self, data, height)
-
Upward continuation of potential field data.
Calculates the continuation through the Fast Fourier Transform in the wavenumber domain (Blakely, 1996):
and then transformed back to the space domain. is the upward continue data, is the height increase, denotes the Fourier Transform, is the wavenumber modulus.
Args
data
:2D
array
- potential field at the grid points
height
:float
- height increase (delta z) in meters.
Returns
cont
:array
- upward continued data
References
Blakely, R. J. (1996), Potential Theory in Gravity and Magnetic Applications, Cambridge University Press.
Source code
def upward_continuation(self, data, height): """ Upward continuation of potential field data. Calculates the continuation through the Fast Fourier Transform in the wavenumber domain (Blakely, 1996): \\( F\\{h_{up}\\} = F\\{h\\} e^{-\\Delta z |k|} \\) and then transformed back to the space domain. \\( h_{up} \\) is the upward continue data, \\( \\Delta z \\) is the height increase, \\( F \\) denotes the Fourier Transform, \\( |k| \\) is the wavenumber modulus. Args: data : 2D array potential field at the grid points height : float height increase (delta z) in meters. Returns: cont : array upward continued data References: Blakely, R. J. (1996), Potential Theory in Gravity and Magnetic Applications, Cambridge University Press. """ nr, nc = data.shape if nr != nc: warnings.warn("subgrid is not square {}".format((nr, nc)), RuntimeWarning) if height <= 0: warnings.warn( "Using 'height' <= 0 means downward continuation, " + "which is known to be unstable." ) fx = 2.0 * np.pi * np.fft.fftfreq(nr, self.dx) fy = 2.0 * np.pi * np.fft.fftfreq(nc, self.dy) kx, ky = np.meshgrid(fy, fx)[::-1] kz = np.hypot(kx, ky) upcont_ft = np.fft.fft2(data) * np.exp(-height * kz) cont = np.real(np.fft.ifft2(upcont_ft)) return cont