# Source code for astroML.fourier

import numpy as np

try:
# use scipy if available: it's faster
from scipy.fftpack import fft, ifft, fftshift
except ImportError:
from numpy.fft import fft, ifft, fftshift

[docs]def FT_continuous(t, h, axis=-1, method=1):
r"""Approximate a continuous 1D Fourier Transform with sampled data.

This function uses the Fast Fourier Transform to approximate
the continuous fourier transform of a sampled function, using
the convention

.. math::

H(f) = \int h(t) exp(-2 \pi i f t) dt

It returns f and H, which approximate H(f).

Parameters
----------
t : array_like
regularly sampled array of times
t is assumed to be regularly spaced, i.e.
t = t0 + Dt * np.arange(N)
h : array_like
real or complex signal at each time
axis : int
axis along which to perform fourier transform.
This axis must be the same length as t.

Returns
-------
f : ndarray
frequencies of result.  Units are the same as 1/t
H : ndarray
Fourier coefficients at each frequency.
"""
assert t.ndim == 1
assert h.shape[axis] == t.shape
N = len(t)
if N % 2 != 0:
raise ValueError("number of samples must be even")

Dt = t - t
Df = 1. / (N * Dt)
t0 = t[N // 2]

f = Df * (np.arange(N) - N // 2)

shape = np.ones(h.ndim, dtype=int)
shape[axis] = N

phase = np.ones(N)
phase[1::2] = -1
phase = phase.reshape(shape)

if method == 1:
H = Dt * fft(h * phase, axis=axis)
else:
H = Dt * fftshift(fft(h, axis=axis), axes=axis)

H *= phase
H *= np.exp(-2j * np.pi * t0 * f.reshape(shape))
H *= np.exp(-1j * np.pi * N / 2)

return f, H

[docs]def IFT_continuous(f, H, axis=-1, method=1):
"""Approximate a continuous 1D Inverse Fourier Transform with sampled data.

This function uses the Fast Fourier Transform to approximate
the continuous fourier transform of a sampled function, using
the convention

.. math::

H(f) = integral[ h(t) exp(-2 pi i f t) dt]

h(t) = integral[ H(f) exp(2 pi i f t) dt]

It returns t and h, which approximate h(t).

Parameters
----------
f : array_like
regularly sampled array of times
t is assumed to be regularly spaced, i.e.
f = f0 + Df * np.arange(N)
H : array_like
real or complex signal at each time
axis : int
axis along which to perform fourier transform.
This axis must be the same length as t.

Returns
-------
f : ndarray
frequencies of result.  Units are the same as 1/t
H : ndarray
Fourier coefficients at each frequency.
"""
assert f.ndim == 1
assert H.shape[axis] == f.shape
N = len(f)
if N % 2 != 0:
raise ValueError("number of samples must be even")

f0 = f
Df = f - f

t0 = -0.5 / Df
Dt = 1. / (N * Df)
t = t0 + Dt * np.arange(N)

shape = np.ones(H.ndim, dtype=int)
shape[axis] = N

t_calc = t.reshape(shape)
f_calc = f.reshape(shape)

H_prime = H * np.exp(2j * np.pi * t0 * f_calc)
h_prime = ifft(H_prime, axis=axis)
h = N * Df * np.exp(2j * np.pi * f0 * (t_calc - t0)) * h_prime

return t, h

[docs]def PSD_continuous(t, h, axis=-1, method=1):
r"""Approximate a continuous 1D Power Spectral Density of sampled data.

This function uses the Fast Fourier Transform to approximate
the continuous fourier transform of a sampled function, using
the convention

.. math::

H(f) = \int h(t) \exp(-2 \pi i f t) dt

It returns f and PSD, which approximate PSD(f) where

.. math::

PSD(f) = |H(f)|^2 + |H(-f)|^2

Parameters
----------
t : array_like
regularly sampled array of times
t is assumed to be regularly spaced, i.e.
t = t0 + Dt * np.arange(N)
h : array_like
real or complex signal at each time
axis : int
axis along which to perform fourier transform.
This axis must be the same length as t.

Returns
-------
f : ndarray
frequencies of result.  Units are the same as 1/t
PSD : ndarray
Fourier coefficients at each frequency.
"""
assert t.ndim == 1
assert h.shape[axis] == t.shape
N = len(t)
if N % 2 != 0:
raise ValueError("number of samples must be even")

ax = axis % h.ndim

if method == 1:
# use FT_continuous
f, Hf = FT_continuous(t, h, axis)
Hf = np.rollaxis(Hf, ax)
f = -f[N // 2::-1]
PSD = abs(Hf[N // 2::-1]) ** 2
PSD[:-1] += abs(Hf[N // 2:]) ** 2
PSD = np.rollaxis(PSD, 0, ax + 1)
else:
# A faster way to do it is with fftshift
# take advantage of the fact that phases go away
Dt = t - t
Df = 1. / (N * Dt)
f = Df * np.arange(N // 2 + 1)
Hf = fft(h, axis=axis)
Hf = np.rollaxis(Hf, ax)
PSD = abs(Hf[:N // 2 + 1]) ** 2
PSD[-1] = 0
PSD[1:] += abs(Hf[N // 2:][::-1]) ** 2
PSD *= 2
PSD = Dt ** 2 * np.rollaxis(PSD, 0, ax + 1)

return f, PSD

[docs]def sinegauss(t, t0, f0, Q):
"""Sine-gaussian wavelet"""
a = (f0 * 1. / Q) ** 2
return (np.exp(-a * (t - t0) ** 2)
* np.exp(2j * np.pi * f0 * (t - t0)))

[docs]def sinegauss_FT(f, t0, f0, Q):
"""Fourier transform of the sine-gaussian wavelet.

This uses the convention

.. math::

H(f) = integral[ h(t) exp(-2pi i f t) dt]
"""
a = (f0 * 1. / Q) ** 2
return (np.sqrt(np.pi / a)
* np.exp(-2j * np.pi * f * t0)
* np.exp(-np.pi ** 2 * (f - f0) ** 2 / a))

[docs]def sinegauss_PSD(f, t0, f0, Q):
"""Compute the PSD of the sine-gaussian function at frequency f

.. math::

PSD(f) = |H(f)|^2 + |H(-f)|^2
"""
a = (f0 * 1. / Q) ** 2
Pf = np.pi / a * np.exp(-2 * np.pi ** 2 * (f - f0) ** 2 / a)
Pmf = np.pi / a * np.exp(-2 * np.pi ** 2 * (-f - f0) ** 2 / a)
return Pf + Pmf

[docs]def wavelet_PSD(t, h, f0, Q=1.0):
"""Compute the wavelet PSD as a function of f0 and t

Parameters
----------
t : array_like
array of times, length N
h : array_like
array of observed values, length N
f0 : array_like
array of candidate frequencies, length Nf
Q : float
Q-parameter for wavelet

Returns
-------
PSD : ndarray
The 2-dimensional PSD, of shape (Nf, N), corresponding with
frequencies f0 and times t.
"""
t, h, f0 = map(np.asarray, (t, h, f0))
if (t.ndim != 1) or (t.shape != h.shape):
raise ValueError('t and h must be one dimensional and the same shape')

if f0.ndim != 1:
raise ValueError('f0 must be one dimensional')

Q = Q + np.zeros_like(f0)

f, H = FT_continuous(t, h)
W = np.conj(sinegauss_FT(f, 0, f0[:, None], Q[:, None]))
_, HW = IFT_continuous(f, H * W)

return abs(HW) ** 2