pywavelet 0.0.1b0__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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__()
|