pywavelet 0.0.1b0__py3-none-any.whl → 0.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- pywavelet/__init__.py +1 -1
- pywavelet/_version.py +2 -2
- pywavelet/logger.py +6 -7
- pywavelet/transforms/__init__.py +10 -10
- pywavelet/transforms/forward/__init__.py +4 -0
- pywavelet/transforms/forward/from_freq.py +80 -0
- pywavelet/transforms/forward/from_time.py +66 -0
- pywavelet/transforms/forward/main.py +128 -0
- pywavelet/transforms/forward/wavelet_bins.py +58 -0
- pywavelet/transforms/inverse/__init__.py +3 -0
- pywavelet/transforms/inverse/main.py +96 -0
- pywavelet/transforms/{from_wavelets/inverse_wavelet_freq_funcs.py → inverse/to_freq.py} +43 -32
- pywavelet/transforms/{from_wavelets/inverse_wavelet_time_funcs.py → inverse/to_time.py} +49 -21
- pywavelet/transforms/phi_computer.py +152 -0
- pywavelet/transforms/types/__init__.py +4 -0
- pywavelet/transforms/types/common.py +53 -0
- pywavelet/transforms/types/frequencyseries.py +237 -0
- pywavelet/transforms/types/plotting.py +341 -0
- pywavelet/transforms/types/timeseries.py +280 -0
- pywavelet/transforms/types/wavelet.py +374 -0
- pywavelet/transforms/types/wavelet_mask.py +34 -0
- pywavelet/utils.py +76 -0
- pywavelet-0.1.0.dist-info/METADATA +35 -0
- pywavelet-0.1.0.dist-info/RECORD +26 -0
- {pywavelet-0.0.1b0.dist-info → pywavelet-0.1.0.dist-info}/WHEEL +1 -1
- pywavelet/fft_funcs.py +0 -16
- pywavelet/likelihood/__init__.py +0 -0
- pywavelet/likelihood/likelihood_base.py +0 -9
- pywavelet/likelihood/whittle.py +0 -24
- pywavelet/transforms/common.py +0 -77
- pywavelet/transforms/from_wavelets/__init__.py +0 -25
- pywavelet/transforms/to_wavelets/__init__.py +0 -52
- pywavelet/transforms/to_wavelets/transform_freq_funcs.py +0 -84
- pywavelet/transforms/to_wavelets/transform_time_funcs.py +0 -63
- pywavelet/utils/__init__.py +0 -0
- pywavelet/utils/fisher_matrix.py +0 -6
- pywavelet/utils/snr.py +0 -37
- pywavelet/waveform_generator/__init__.py +0 -0
- pywavelet/waveform_generator/build_lookup_table.py +0 -0
- pywavelet/waveform_generator/generators/__init__.py +0 -2
- pywavelet/waveform_generator/generators/functional_waveform_generator.py +0 -33
- pywavelet/waveform_generator/generators/lookuptable_waveform_generator.py +0 -15
- pywavelet/waveform_generator/generators/rom_waveform_generator.py +0 -0
- pywavelet/waveform_generator/waveform_generator.py +0 -14
- pywavelet-0.0.1b0.dist-info/METADATA +0 -35
- pywavelet-0.0.1b0.dist-info/RECORD +0 -29
- {pywavelet-0.0.1b0.dist-info → pywavelet-0.1.0.dist-info}/top_level.txt +0 -0
@@ -1,39 +1,48 @@
|
|
1
1
|
"""functions for computing the inverse wavelet transforms"""
|
2
2
|
import numpy as np
|
3
3
|
from numba import njit
|
4
|
+
from numpy import fft
|
4
5
|
|
5
|
-
from ... import fft_funcs as fft
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
def inverse_wavelet_time_helper_fast(
|
8
|
+
wave_in: np.ndarray, phi: np.ndarray, Nf: int, Nt: int, mult: int
|
9
|
+
) -> np.ndarray:
|
9
10
|
"""helper loop for fast inverse wavelet transform"""
|
10
11
|
ND = Nf * Nt
|
11
12
|
K = mult * 2 * Nf
|
12
|
-
# res = np.zeros(ND)
|
13
13
|
|
14
14
|
# extend this array, we can use wrapping boundary conditions at end
|
15
15
|
res = np.zeros(ND + K + Nf)
|
16
16
|
|
17
17
|
afins = np.zeros(2 * Nf, dtype=np.complex128)
|
18
18
|
|
19
|
+
__core(Nf, Nt, K, ND, wave_in, phi, res, afins)
|
20
|
+
|
21
|
+
return res[:ND]
|
22
|
+
|
23
|
+
|
24
|
+
def __core(Nf: int, Nt: int, K: int, ND: int, wave_in: np.ndarray, phi: np.ndarray, res: np.ndarray, afins:np.ndarray) -> None:
|
19
25
|
for n in range(0, Nt):
|
20
26
|
if n % 2 == 0:
|
21
27
|
pack_wave_time_helper_compact(n, Nf, Nt, wave_in, afins)
|
22
|
-
ffts_fin = fft.fft(afins)
|
28
|
+
ffts_fin = np.fft.fft(afins)
|
23
29
|
unpack_time_wave_helper_compact(n, Nf, Nt, K, phi, ffts_fin, res)
|
24
30
|
|
25
31
|
# wrap boundary conditions
|
26
|
-
res[: min(K + Nf, ND)] += res[ND
|
32
|
+
res[: min(K + Nf, ND)] += res[ND: min(ND + K + Nf, 2 * ND)]
|
27
33
|
if K + Nf > ND:
|
28
|
-
res[: K + Nf - ND] += res[2 * ND
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
res[: K + Nf - ND] += res[2 * ND: ND + K * Nf]
|
35
|
+
|
36
|
+
|
37
|
+
def unpack_time_wave_helper(
|
38
|
+
n: int,
|
39
|
+
Nf: int,
|
40
|
+
Nt: int,
|
41
|
+
K: int,
|
42
|
+
phis: np.ndarray,
|
43
|
+
fft_fin_real: np.ndarray,
|
44
|
+
res: np.ndarray,
|
45
|
+
) -> None:
|
37
46
|
"""helper for time domain wavelet transform to unpack wavelet domain coefficients"""
|
38
47
|
ND = Nf * Nt
|
39
48
|
|
@@ -52,10 +61,19 @@ def unpack_time_wave_helper(n, Nf, Nt, K, phis, fft_fin_real, res):
|
|
52
61
|
k = 0
|
53
62
|
|
54
63
|
|
55
|
-
|
56
|
-
|
64
|
+
def unpack_time_wave_helper_compact(
|
65
|
+
n: int,
|
66
|
+
Nf: int,
|
67
|
+
Nt: int,
|
68
|
+
K: int,
|
69
|
+
phis: np.ndarray,
|
70
|
+
fft_fin: np.ndarray,
|
71
|
+
res: np.ndarray,
|
72
|
+
):
|
57
73
|
"""helper for time domain wavelet transform to unpack wavelet domain coefficients
|
58
74
|
in compact representation where cosine and sine parts are real and imaginary parts
|
75
|
+
|
76
|
+
IN-PLACE EDITS res
|
59
77
|
"""
|
60
78
|
ND = Nf * Nt
|
61
79
|
fft_fin_real = np.zeros(4 * Nf)
|
@@ -79,8 +97,13 @@ def unpack_time_wave_helper_compact(n, Nf, Nt, K, phis, fft_fin, res):
|
|
79
97
|
|
80
98
|
|
81
99
|
@njit()
|
82
|
-
def pack_wave_time_helper(
|
83
|
-
|
100
|
+
def pack_wave_time_helper(
|
101
|
+
n: int, Nf: int, Nt: int, wave_in: np.ndarray, afins: np.ndarray
|
102
|
+
) -> None:
|
103
|
+
"""helper for time domain transform to pack wavelet domain coefficients
|
104
|
+
|
105
|
+
IN-PLACE EDITS afins
|
106
|
+
"""
|
84
107
|
if n % 2 == 0:
|
85
108
|
# assign highest and lowest bin correctly
|
86
109
|
afins[0] = np.sqrt(2) * wave_in[n, 0]
|
@@ -108,9 +131,14 @@ def pack_wave_time_helper(n, Nf, Nt, wave_in, afins):
|
|
108
131
|
|
109
132
|
|
110
133
|
@njit()
|
111
|
-
def pack_wave_time_helper_compact(
|
112
|
-
|
134
|
+
def pack_wave_time_helper_compact(
|
135
|
+
n: int, Nf: int, Nt: int, wave_in: np.ndarray, afins: np.ndarray
|
136
|
+
) -> None:
|
137
|
+
"""
|
138
|
+
Helper for time domain transform to pack wavelet domain coefficients
|
113
139
|
in packed representation with odd and even coefficients in real and imaginary pars
|
140
|
+
|
141
|
+
IN-PLACE EDITS afins
|
114
142
|
"""
|
115
143
|
afins[0] = np.sqrt(2) * wave_in[n, 0]
|
116
144
|
if n + 1 < Nt:
|
@@ -0,0 +1,152 @@
|
|
1
|
+
import numpy as np
|
2
|
+
from numpy import fft
|
3
|
+
from scipy.special import betainc
|
4
|
+
|
5
|
+
PI = np.pi
|
6
|
+
|
7
|
+
|
8
|
+
def phitilde_vec(
|
9
|
+
omega: np.ndarray, Nf: int, dt: float, d: float = 4.0
|
10
|
+
) -> np.ndarray:
|
11
|
+
"""Compute phi_tilde(omega_i) array, nx is filter steepness, defaults to 4.
|
12
|
+
|
13
|
+
Eq 11 of https://arxiv.org/pdf/2009.00043.pdf (Cornish et al. 2020)
|
14
|
+
|
15
|
+
phi(omega_i) =
|
16
|
+
1/sqrt(2π∆F) if |omega_i| < A
|
17
|
+
1/sqrt(2π∆F) cos(nu_d π/2 * |omega|-A / B) if A < |omega_i| < A + B
|
18
|
+
|
19
|
+
Where nu_d = normalized incomplete beta function
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
Parameters
|
24
|
+
----------
|
25
|
+
ω : np.ndarray
|
26
|
+
Array of angular frequencies
|
27
|
+
Nf : int
|
28
|
+
Number of frequency bins
|
29
|
+
d : float, optional
|
30
|
+
Number of standard deviations for the gaussian wavelet, by default 4.
|
31
|
+
|
32
|
+
Returns
|
33
|
+
-------
|
34
|
+
np.ndarray
|
35
|
+
Array of phi_tilde(omega_i) values
|
36
|
+
|
37
|
+
"""
|
38
|
+
dF = 1.0 / (2 * Nf) # NOTE: missing 1/dt?
|
39
|
+
dOmega = 2 * PI * dF # Near Eq 10 # 2 pi times DF
|
40
|
+
inverse_sqrt_dOmega = 1.0 / np.sqrt(dOmega)
|
41
|
+
|
42
|
+
A = dOmega / 4
|
43
|
+
B = dOmega - 2 * A # Cannot have B \leq 0.
|
44
|
+
if B <= 0:
|
45
|
+
raise ValueError("B must be greater than 0")
|
46
|
+
|
47
|
+
phi = np.zeros(omega.size)
|
48
|
+
mask = (A <= np.abs(omega)) & (np.abs(omega) < A + B) # Minor changes
|
49
|
+
vd = (PI / 2.0) * __nu_d(omega[mask], A, B, d=d) # different from paper
|
50
|
+
phi[mask] = inverse_sqrt_dOmega * np.cos(vd)
|
51
|
+
phi[np.abs(omega) < A] = inverse_sqrt_dOmega
|
52
|
+
return phi
|
53
|
+
|
54
|
+
|
55
|
+
def __nu_d(
|
56
|
+
omega: np.ndarray, A: float, B: float, d: float = 4.0
|
57
|
+
) -> np.ndarray:
|
58
|
+
"""Compute the normalized incomplete beta function.
|
59
|
+
|
60
|
+
Parameters
|
61
|
+
----------
|
62
|
+
ω : np.ndarray
|
63
|
+
Array of angular frequencies
|
64
|
+
A : float
|
65
|
+
Lower bound for the beta function
|
66
|
+
B : float
|
67
|
+
Upper bound for the beta function
|
68
|
+
d : float, optional
|
69
|
+
Number of standard deviations for the gaussian wavelet, by default 4.
|
70
|
+
|
71
|
+
Returns
|
72
|
+
-------
|
73
|
+
np.ndarray
|
74
|
+
Array of ν_d values
|
75
|
+
|
76
|
+
scipy.special.betainc
|
77
|
+
https://docs.scipy.org/doc/scipy-1.7.1/reference/reference/generated/scipy.special.betainc.html
|
78
|
+
|
79
|
+
"""
|
80
|
+
x = (np.abs(omega) - A) / B
|
81
|
+
return betainc(d, d, x) / betainc(d, d, 1)
|
82
|
+
|
83
|
+
|
84
|
+
def phitilde_vec_norm(Nf: int, Nt: int, dt: float, d: float) -> np.ndarray:
|
85
|
+
"""Normalize phitilde for inverse frequency domain transform."""
|
86
|
+
|
87
|
+
# Calculate the frequency values
|
88
|
+
ND = Nf * Nt
|
89
|
+
omegas = 2 * np.pi / ND * np.arange(0, Nt // 2 + 1)
|
90
|
+
|
91
|
+
# Calculate the unnormalized phitilde (u_phit)
|
92
|
+
u_phit = phitilde_vec(omegas, Nf, dt, d)
|
93
|
+
|
94
|
+
# Normalize the phitilde
|
95
|
+
normalising_factor = np.pi ** (-1 / 2) # Ollie's normalising factor
|
96
|
+
|
97
|
+
# Notes: this is the overall normalising factor that is different from Cornish's paper
|
98
|
+
# It is the only way I can force this code to be consistent with our work in the
|
99
|
+
# frequency domain. First note that
|
100
|
+
|
101
|
+
# old normalising factor -- This factor is absolutely ridiculous. Why!?
|
102
|
+
# Matt_normalising_factor = np.sqrt(
|
103
|
+
# (2 * np.sum(u_phit[1:] ** 2) + u_phit[0] ** 2) * 2 * PI / ND
|
104
|
+
# )
|
105
|
+
# Matt_normalising_factor /= PI**(3/2)/PI
|
106
|
+
|
107
|
+
# The expression above is equal to np.pi**(-1/2) after working through the maths.
|
108
|
+
# I have pulled (2/Nf) from __init__.py (from freq to wavelet) into the normalsiing
|
109
|
+
# factor here. I thnk it's cleaner to have ONE normalising constant. Avoids confusion
|
110
|
+
# and it is much easier to track.
|
111
|
+
|
112
|
+
# TODO: understand the following:
|
113
|
+
# (2 * np.sum(u_phit[1:] ** 2) + u_phit[0] ** 2) = 0.5 * Nt / dOmega
|
114
|
+
# Matt_normalising_factor is equal to 1/sqrt(pi)... why is this computed?
|
115
|
+
# in such a stupid way?
|
116
|
+
|
117
|
+
return u_phit / (normalising_factor)
|
118
|
+
|
119
|
+
|
120
|
+
def phi_vec(Nf: int, dt, d: float = 4.0, q: int = 16) -> np.ndarray:
|
121
|
+
"""get time domain phi as fourier transform of phitilde_vec"""
|
122
|
+
insDOM = 1.0 / np.sqrt(PI / Nf)
|
123
|
+
K = q * 2 * Nf
|
124
|
+
half_K = q * Nf # np.int64(K/2)
|
125
|
+
|
126
|
+
dom = 2 * PI / K # max frequency is K/2*dom = pi/dt = OM
|
127
|
+
|
128
|
+
DX = np.zeros(K, dtype=np.complex128)
|
129
|
+
|
130
|
+
# zero frequency
|
131
|
+
DX[0] = insDOM
|
132
|
+
|
133
|
+
DX = DX.copy()
|
134
|
+
# postive frequencies
|
135
|
+
DX[1 : half_K + 1] = phitilde_vec(
|
136
|
+
dom * np.arange(1, half_K + 1), Nf, dt, d
|
137
|
+
)
|
138
|
+
# negative frequencies
|
139
|
+
DX[half_K + 1 :] = phitilde_vec(
|
140
|
+
-dom * np.arange(half_K - 1, 0, -1), Nf, dt, d
|
141
|
+
)
|
142
|
+
DX = K * fft.ifft(DX, K)
|
143
|
+
|
144
|
+
phi = np.zeros(K)
|
145
|
+
phi[0:half_K] = np.real(DX[half_K:K])
|
146
|
+
phi[half_K:] = np.real(DX[0:half_K])
|
147
|
+
|
148
|
+
nrm = np.sqrt(K / dom) # *np.linalg.norm(phi)
|
149
|
+
|
150
|
+
fac = np.sqrt(2.0) / nrm
|
151
|
+
phi *= fac
|
152
|
+
return phi
|
@@ -0,0 +1,53 @@
|
|
1
|
+
from typing import Literal, Tuple
|
2
|
+
|
3
|
+
import numpy as xp
|
4
|
+
from numpy.fft import irfft, fft, rfft, rfftfreq
|
5
|
+
|
6
|
+
from ...logger import logger
|
7
|
+
|
8
|
+
|
9
|
+
def _len_check(d):
|
10
|
+
if not xp.log2(len(d)).is_integer():
|
11
|
+
logger.warning(f"Data length {len(d)} is suggested to be a power of 2")
|
12
|
+
|
13
|
+
|
14
|
+
def is_documented_by(original):
|
15
|
+
def wrapper(target):
|
16
|
+
target.__doc__ = original.__doc__
|
17
|
+
return target
|
18
|
+
|
19
|
+
return wrapper
|
20
|
+
|
21
|
+
|
22
|
+
def fmt_time(seconds: float, units=False) -> Tuple[str, str]:
|
23
|
+
"""Returns formatted time and units [ms, s, min, hr, day]"""
|
24
|
+
t, u = "", ""
|
25
|
+
if seconds < 1e-3:
|
26
|
+
t, u = f"{seconds * 1e6:.2f}", "µs"
|
27
|
+
elif seconds < 1:
|
28
|
+
t, u = f"{seconds * 1e3:.2f}", "ms"
|
29
|
+
elif seconds < 60:
|
30
|
+
t, u = f"{seconds:.2f}", "s"
|
31
|
+
elif seconds < 60 * 60:
|
32
|
+
t, u = f"{seconds / 60:.2f}", "min"
|
33
|
+
elif seconds < 60 * 60 * 24:
|
34
|
+
t, u = f"{seconds / 3600:.2f}", "hr"
|
35
|
+
else:
|
36
|
+
t, u = f"{seconds / 86400:.2f}", "day"
|
37
|
+
|
38
|
+
if units:
|
39
|
+
return t, u
|
40
|
+
return t
|
41
|
+
|
42
|
+
|
43
|
+
def fmt_timerange(trange):
|
44
|
+
t0 = fmt_time(trange[0])
|
45
|
+
tend, units = fmt_time(trange[1], units = True)
|
46
|
+
return f"[{t0}, {tend}] {units}"
|
47
|
+
|
48
|
+
|
49
|
+
def fmt_pow2(n:float)->str:
|
50
|
+
pow2 = xp.log2(n)
|
51
|
+
if pow2.is_integer():
|
52
|
+
return f"2^{int(pow2)}"
|
53
|
+
return f"{n:,}"
|
@@ -0,0 +1,237 @@
|
|
1
|
+
import matplotlib.pyplot as plt
|
2
|
+
from typing import Tuple, Union, Optional
|
3
|
+
|
4
|
+
from .common import is_documented_by, xp, irfft, fmt_time, fmt_pow2
|
5
|
+
from .plotting import plot_freqseries, plot_periodogram
|
6
|
+
|
7
|
+
__all__ = ["FrequencySeries"]
|
8
|
+
|
9
|
+
class FrequencySeries:
|
10
|
+
"""
|
11
|
+
A class to represent a one-sided frequency series, with various methods for
|
12
|
+
plotting and converting the series to a time-domain representation.
|
13
|
+
|
14
|
+
Attributes
|
15
|
+
----------
|
16
|
+
data : xp.ndarray
|
17
|
+
Frequency domain data.
|
18
|
+
freq : xp.ndarray
|
19
|
+
Corresponding frequencies (must be non-negative).
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, data: xp.ndarray, freq: xp.ndarray, t0: float = 0):
|
23
|
+
"""
|
24
|
+
Initialize the FrequencySeries with data and frequencies.
|
25
|
+
|
26
|
+
Parameters
|
27
|
+
----------
|
28
|
+
data : xp.ndarray
|
29
|
+
Frequency domain data.
|
30
|
+
freq : xp.ndarray
|
31
|
+
Array of frequencies. Must be non-negative.
|
32
|
+
t0 : float, optional
|
33
|
+
Initial time of the time domain signal (default is 0).
|
34
|
+
(This is not used in this class, but is included for compatibility with TimeSeries.)
|
35
|
+
|
36
|
+
Raises
|
37
|
+
------
|
38
|
+
ValueError
|
39
|
+
If any frequency is negative or if `data` and `freq` do not have the same length.
|
40
|
+
"""
|
41
|
+
if xp.any(freq < 0):
|
42
|
+
raise ValueError("FrequencySeries must be one-sided (only non-negative frequencies)")
|
43
|
+
if len(data) != len(freq):
|
44
|
+
raise ValueError(f"data and freq must have the same length ({len(data)} != {len(freq)})")
|
45
|
+
self.data = data
|
46
|
+
self.freq = freq
|
47
|
+
self.t0 = t0
|
48
|
+
|
49
|
+
@is_documented_by(plot_freqseries)
|
50
|
+
def plot(self, ax=None, **kwargs) -> Tuple[plt.Figure, plt.Axes]:
|
51
|
+
return plot_freqseries(
|
52
|
+
self.data, self.freq, self.nyquist_frequency, ax=ax, **kwargs
|
53
|
+
)
|
54
|
+
|
55
|
+
@is_documented_by(plot_periodogram)
|
56
|
+
def plot_periodogram(self, ax=None, **kwargs) -> Tuple[plt.Figure, plt.Axes]:
|
57
|
+
return plot_periodogram(
|
58
|
+
self.data, self.freq, self.fs, ax=ax, **kwargs
|
59
|
+
)
|
60
|
+
|
61
|
+
def __len__(self):
|
62
|
+
"""Return the length of the frequency series."""
|
63
|
+
return len(self.data)
|
64
|
+
|
65
|
+
def __getitem__(self, item):
|
66
|
+
"""Return the data point at the specified index."""
|
67
|
+
return self.data[item]
|
68
|
+
|
69
|
+
@property
|
70
|
+
def df(self) -> float:
|
71
|
+
"""Return the frequency resolution (Δf)."""
|
72
|
+
return float(self.freq[1] - self.freq[0])
|
73
|
+
|
74
|
+
@property
|
75
|
+
def dt(self) -> float:
|
76
|
+
"""Return the time resolution (Δt)."""
|
77
|
+
return 1 / self.fs
|
78
|
+
|
79
|
+
@property
|
80
|
+
def sample_rate(self) -> float:
|
81
|
+
"""Return the sample rate (fs)."""
|
82
|
+
return 2 * float(self.freq[-1])
|
83
|
+
|
84
|
+
@property
|
85
|
+
def fs(self) -> float:
|
86
|
+
"""Return the sample rate (fs)."""
|
87
|
+
return self.sample_rate
|
88
|
+
|
89
|
+
@property
|
90
|
+
def nyquist_frequency(self) -> float:
|
91
|
+
"""Return the Nyquist frequency (fs/2)."""
|
92
|
+
return self.sample_rate / 2
|
93
|
+
|
94
|
+
@property
|
95
|
+
def duration(self) -> float:
|
96
|
+
"""Return the duration of the time domain signal."""
|
97
|
+
return (2 * (len(self) - 1)) / self.fs
|
98
|
+
|
99
|
+
@property
|
100
|
+
def minimum_frequency(self) -> float:
|
101
|
+
"""Return the minimum frequency in the frequency series."""
|
102
|
+
return float(xp.abs(self.freq).min())
|
103
|
+
|
104
|
+
@property
|
105
|
+
def maximum_frequency(self) -> float:
|
106
|
+
"""Return the maximum frequency in the frequency series."""
|
107
|
+
return float(xp.abs(self.freq).max())
|
108
|
+
|
109
|
+
@property
|
110
|
+
def range(self) -> Tuple[float, float]:
|
111
|
+
"""Return the frequency range (minimum and maximum frequencies)."""
|
112
|
+
return (self.minimum_frequency, self.maximum_frequency)
|
113
|
+
|
114
|
+
@property
|
115
|
+
def shape(self) -> Tuple[int, ...]:
|
116
|
+
"""Return the shape of the data array."""
|
117
|
+
return self.data.shape
|
118
|
+
|
119
|
+
@property
|
120
|
+
def ND(self) -> int:
|
121
|
+
"""Return the number of data points in the time domain signal."""
|
122
|
+
return 2 * (len(self) - 1)
|
123
|
+
|
124
|
+
def __repr__(self) -> str:
|
125
|
+
"""Return a string representation of the FrequencySeries."""
|
126
|
+
dur = fmt_time(self.duration)
|
127
|
+
n = fmt_pow2(len(self))
|
128
|
+
return f"FrequencySeries(n={n}, frange=[{self.range[0]:.2f}, {self.range[1]:.2f}] Hz, T={dur}, fs={self.fs:.2f} Hz)"
|
129
|
+
|
130
|
+
def noise_weighted_inner_product(self, other: "FrequencySeries", psd:"FrequencySeries") -> float:
|
131
|
+
"""
|
132
|
+
Compute the noise-weighted inner product of two FrequencySeries.
|
133
|
+
|
134
|
+
Parameters
|
135
|
+
----------
|
136
|
+
other : FrequencySeries
|
137
|
+
The other FrequencySeries.
|
138
|
+
psd : FrequencySeries
|
139
|
+
The power spectral density (PSD) of the noise.
|
140
|
+
|
141
|
+
Returns
|
142
|
+
-------
|
143
|
+
float
|
144
|
+
The noise-weighted inner product of the two FrequencySeries.
|
145
|
+
"""
|
146
|
+
integrand = xp.real(xp.conj(self.data) * other.data / psd.data)
|
147
|
+
return (4 * self.dt/self.ND) * xp.nansum(integrand)
|
148
|
+
|
149
|
+
def matched_filter_snr(self, other: "FrequencySeries", psd: "FrequencySeries") -> float:
|
150
|
+
"""
|
151
|
+
Compute the signal-to-noise ratio (SNR) of a matched filter.
|
152
|
+
|
153
|
+
Parameters
|
154
|
+
----------
|
155
|
+
other : FrequencySeries
|
156
|
+
The other FrequencySeries.
|
157
|
+
psd : FrequencySeries
|
158
|
+
The power spectral density (PSD) of the noise.
|
159
|
+
|
160
|
+
Returns
|
161
|
+
-------
|
162
|
+
float
|
163
|
+
The SNR of the matched filter.
|
164
|
+
"""
|
165
|
+
return xp.sqrt(self.noise_weighted_inner_product(other, psd))
|
166
|
+
|
167
|
+
def optimal_snr(self, psd: "FrequencySeries") -> float:
|
168
|
+
"""
|
169
|
+
Compute the optimal signal-to-noise ratio (SNR) of a FrequencySeries.
|
170
|
+
|
171
|
+
Parameters
|
172
|
+
----------
|
173
|
+
psd : FrequencySeries
|
174
|
+
The power spectral density (PSD) of the noise.
|
175
|
+
|
176
|
+
Returns
|
177
|
+
-------
|
178
|
+
float
|
179
|
+
The optimal SNR of the FrequencySeries.
|
180
|
+
"""
|
181
|
+
return xp.sqrt(self.noise_weighted_inner_product(self, psd))
|
182
|
+
|
183
|
+
def to_timeseries(self) -> "TimeSeries":
|
184
|
+
"""
|
185
|
+
Convert the frequency series to a time series using inverse Fourier transform.
|
186
|
+
|
187
|
+
Returns
|
188
|
+
-------
|
189
|
+
TimeSeries
|
190
|
+
The corresponding time domain signal.
|
191
|
+
"""
|
192
|
+
# Perform the inverse FFT
|
193
|
+
time_data = irfft(self.data, n=2 * (len(self) - 1))
|
194
|
+
|
195
|
+
# Calculate the time array
|
196
|
+
dt = 1 / (2 * self.nyquist_frequency)
|
197
|
+
time = xp.arange(len(time_data)) * dt
|
198
|
+
time += self.t0
|
199
|
+
|
200
|
+
# Create and return a TimeSeries object
|
201
|
+
from .timeseries import TimeSeries
|
202
|
+
return TimeSeries(time_data, time)
|
203
|
+
|
204
|
+
|
205
|
+
def to_wavelet(
|
206
|
+
self,
|
207
|
+
Nf: Union[int, None] = None,
|
208
|
+
Nt: Union[int, None] = None,
|
209
|
+
nx: Optional[float] = 4.0,
|
210
|
+
)->"Wavelet":
|
211
|
+
"""
|
212
|
+
Convert the frequency series to a wavelet using inverse Fourier transform.
|
213
|
+
|
214
|
+
Returns
|
215
|
+
-------
|
216
|
+
Wavelet
|
217
|
+
The corresponding wavelet.
|
218
|
+
"""
|
219
|
+
from ..forward import from_freq_to_wavelet
|
220
|
+
return from_freq_to_wavelet(self, Nf=Nf, Nt=Nt, nx=nx)
|
221
|
+
|
222
|
+
|
223
|
+
def __eq__(self, other):
|
224
|
+
"""Check if two FrequencySeries objects are equal."""
|
225
|
+
data_same = xp.allclose(self.data, other.data)
|
226
|
+
freq_same = xp.allclose(self.freq, other.freq)
|
227
|
+
return data_same and freq_same
|
228
|
+
|
229
|
+
def __copy__(self):
|
230
|
+
return FrequencySeries(
|
231
|
+
xp.copy(self.data),
|
232
|
+
xp.copy(self.freq),
|
233
|
+
t0=self.t0
|
234
|
+
)
|
235
|
+
|
236
|
+
def copy(self):
|
237
|
+
return self.__copy__()
|