nullcal 0.2.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.
- nullcal/__init__.py +16 -0
- nullcal/calibration.py +189 -0
- nullcal/clustering/__init__.py +0 -0
- nullcal/clustering/base.py +39 -0
- nullcal/clustering/injection.py +63 -0
- nullcal/clustering/precompute.py +30 -0
- nullcal/clustering/single.py +208 -0
- nullcal/clustering/time_frequency_map.py +42 -0
- nullcal/data.py +93 -0
- nullcal/likelihood/__init__.py +9 -0
- nullcal/likelihood/recalibration_likelihood.py +227 -0
- nullcal/metadata/__init__.py +0 -0
- nullcal/metadata/yaml.py +30 -0
- nullcal/null_stream/__init__.py +0 -0
- nullcal/null_stream/calibration.py +40 -0
- nullcal/null_stream/null_stream.py +252 -0
- nullcal/null_stream/projector.py +48 -0
- nullcal/null_stream/whiten.py +106 -0
- nullcal/result/__init__.py +5 -0
- nullcal/result/result.py +50 -0
- nullcal/result/utils.py +13 -0
- nullcal/sampler.py +113 -0
- nullcal/studies/__init__.py +1 -0
- nullcal/studies/lwa_leakage.py +513 -0
- nullcal/studies/spline_resolution.py +148 -0
- nullcal/time_frequency_transform/README.md +7 -0
- nullcal/time_frequency_transform/__init__.py +23 -0
- nullcal/time_frequency_transform/inverse_wavelet_freq_funcs.py +42 -0
- nullcal/time_frequency_transform/inverse_wavelet_time_funcs.py +49 -0
- nullcal/time_frequency_transform/stft.py +45 -0
- nullcal/time_frequency_transform/transform_freq_funcs.py +180 -0
- nullcal/time_frequency_transform/transform_time_funcs.py +60 -0
- nullcal/time_frequency_transform/utils.py +21 -0
- nullcal/time_frequency_transform/wavelet_transforms.py +249 -0
- nullcal/utils/__init__.py +0 -0
- nullcal/utils/log.py +72 -0
- nullcal/utils/snr.py +29 -0
- nullcal/version.py +9 -0
- nullcal-0.2.0.dist-info/METADATA +222 -0
- nullcal-0.2.0.dist-info/RECORD +42 -0
- nullcal-0.2.0.dist-info/WHEEL +4 -0
- nullcal-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""JAX kernels for inverse WDM transforms in the frequency domain."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
|
|
7
|
+
jax.config.update("jax_enable_x64", True)
|
|
8
|
+
|
|
9
|
+
import jax.numpy as jnp # noqa: E402
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def inverse_wavelet_freq_helper_fast(wave_in, phif, n_f, n_t):
|
|
13
|
+
"""Return the inverse frequency-domain transform."""
|
|
14
|
+
wave_in = jnp.asarray(wave_in)
|
|
15
|
+
phif = jnp.asarray(phif, dtype=wave_in.dtype)
|
|
16
|
+
rows = jnp.arange(n_t)
|
|
17
|
+
modes = jnp.arange(1, n_f)[:, None]
|
|
18
|
+
interior = wave_in[:, 1:n_f].T
|
|
19
|
+
factors = jnp.where((rows[None, :] + modes) % 2 == 1, -1j, 1.0)
|
|
20
|
+
|
|
21
|
+
prefactors = jnp.zeros((n_f + 1, n_t), dtype=jnp.result_type(wave_in, 1j))
|
|
22
|
+
prefactors = prefactors.at[0].set(wave_in[(2 * rows) % n_t, 0] / jnp.sqrt(2.0))
|
|
23
|
+
prefactors = prefactors.at[n_f].set(wave_in[(2 * rows) % n_t + 1, 0] / jnp.sqrt(2.0))
|
|
24
|
+
prefactors = prefactors.at[1:n_f].set(interior * factors)
|
|
25
|
+
transformed = jnp.fft.fft(prefactors, axis=1)
|
|
26
|
+
|
|
27
|
+
res = jnp.zeros(n_f * n_t // 2 + 1, dtype=transformed.dtype)
|
|
28
|
+
half = n_t // 2
|
|
29
|
+
|
|
30
|
+
lower_offsets = jnp.arange(half)
|
|
31
|
+
res = res.at[lower_offsets].add(transformed[0, (2 * lower_offsets) % n_t] * phif[lower_offsets])
|
|
32
|
+
|
|
33
|
+
upper_offsets = jnp.arange(half + 1)
|
|
34
|
+
upper_indices = n_f * half - upper_offsets
|
|
35
|
+
res = res.at[upper_indices].add(transformed[n_f, (-2 * upper_offsets) % n_t] * phif[upper_offsets])
|
|
36
|
+
|
|
37
|
+
offsets = jnp.arange(1 - half, half)
|
|
38
|
+
centres = jnp.arange(1, n_f)[:, None] * half
|
|
39
|
+
indices = centres + offsets[None, :]
|
|
40
|
+
fft_indices = indices % n_t
|
|
41
|
+
contributions = transformed[1:n_f][jnp.arange(n_f - 1)[:, None], fft_indices] * phif[jnp.abs(offsets)][None, :]
|
|
42
|
+
return res.at[indices].add(contributions)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""JAX kernels for inverse WDM transforms in the time domain."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
|
|
7
|
+
jax.config.update("jax_enable_x64", True)
|
|
8
|
+
|
|
9
|
+
import jax.numpy as jnp # noqa: E402
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _compact_packets(wave_in, n_f, n_t):
|
|
13
|
+
"""Pack adjacent WDM time rows into complex FFT inputs."""
|
|
14
|
+
wave_in = jnp.asarray(wave_in)
|
|
15
|
+
even_rows = jnp.arange(0, n_t, 2)
|
|
16
|
+
even = wave_in[even_rows]
|
|
17
|
+
odd = wave_in[even_rows + 1]
|
|
18
|
+
packets = jnp.zeros((n_t // 2, 2 * n_f), dtype=jnp.result_type(wave_in, 1j))
|
|
19
|
+
packets = packets.at[:, 0].set(jnp.sqrt(2.0) * even[:, 0])
|
|
20
|
+
packets = packets.at[:, n_f].set(jnp.sqrt(2.0) * odd[:, 0])
|
|
21
|
+
|
|
22
|
+
even_modes = jnp.arange(2, n_f - 1, 2)
|
|
23
|
+
packets = packets.at[:, even_modes].set(even[:, even_modes] - odd[:, even_modes])
|
|
24
|
+
packets = packets.at[:, 2 * n_f - even_modes].set(even[:, even_modes] + odd[:, even_modes])
|
|
25
|
+
|
|
26
|
+
odd_modes = jnp.arange(1, n_f, 2)
|
|
27
|
+
packets = packets.at[:, odd_modes].set(1j * (even[:, odd_modes] - odd[:, odd_modes]))
|
|
28
|
+
return packets.at[:, 2 * n_f - odd_modes].set(-1j * (even[:, odd_modes] + odd[:, odd_modes]))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def inverse_wavelet_time_helper_fast(wave_in, phi, n_f, n_t, mult):
|
|
32
|
+
"""Return the inverse time-domain transform using batched JAX FFTs."""
|
|
33
|
+
wave_in = jnp.asarray(wave_in)
|
|
34
|
+
phi = jnp.asarray(phi, dtype=wave_in.dtype)
|
|
35
|
+
n_d = n_f * n_t
|
|
36
|
+
k_cutoff = mult * 2 * n_f
|
|
37
|
+
transformed = jnp.fft.fft(_compact_packets(wave_in, n_f, n_t), axis=1)
|
|
38
|
+
|
|
39
|
+
pair_rows = jnp.arange(0, n_t, 2)[:, None]
|
|
40
|
+
offsets = jnp.arange(k_cutoff)[None, :]
|
|
41
|
+
fft_indices = (-k_cutoff // 2 + pair_rows * n_f + n_d + offsets) % (2 * n_f)
|
|
42
|
+
result_indices = (-k_cutoff // 2 + pair_rows * n_f + offsets) % n_d
|
|
43
|
+
real_values = jnp.take_along_axis(jnp.real(transformed), fft_indices, axis=1)
|
|
44
|
+
imag_indices = (fft_indices + n_f) % (2 * n_f)
|
|
45
|
+
imag_values = jnp.take_along_axis(jnp.imag(transformed), imag_indices, axis=1)
|
|
46
|
+
|
|
47
|
+
result = jnp.zeros(n_d, dtype=wave_in.dtype)
|
|
48
|
+
result = result.at[result_indices].add(phi[None, :] * real_values)
|
|
49
|
+
return result.at[(result_indices + n_f) % n_d].add(phi[None, :] * imag_values)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Short-time Fourier transform.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def stft(data: np.ndarray, sampling_frequency: float, frequency_resolution: float, window: np.ndarray):
|
|
11
|
+
"""Short-time Fourier transform.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
data (np.ndarray): Data.
|
|
15
|
+
sampling_frequency (float): Sampling frequency in Hz.
|
|
16
|
+
frequency_resolution (float): Frequency resolution in Jz.
|
|
17
|
+
window (np.ndarray, optional): Window function. Defaults to None.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: data must be real-valued.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
np.ndarray: Short-time Fourier transform of data.
|
|
24
|
+
"""
|
|
25
|
+
data = np.asarray(data)
|
|
26
|
+
if not np.isrealobj(data):
|
|
27
|
+
raise ValueError("data must be real-valued")
|
|
28
|
+
|
|
29
|
+
# Compute number of frames
|
|
30
|
+
n_samples = data.shape[0]
|
|
31
|
+
window_size = int(sampling_frequency / frequency_resolution)
|
|
32
|
+
n_frames = int(n_samples // window_size)
|
|
33
|
+
|
|
34
|
+
# Reshape signal into frames
|
|
35
|
+
# Shape: (n_frames, window_size)
|
|
36
|
+
frames = data.reshape(n_frames, window_size)
|
|
37
|
+
|
|
38
|
+
# Apply window function
|
|
39
|
+
windowed_frames = frames * window
|
|
40
|
+
|
|
41
|
+
# Compute rFFT for each frame
|
|
42
|
+
# Shape: (n_frames, window_size // 2 + 1), complex-valued
|
|
43
|
+
r_stft_matrix = np.fft.rfft(windowed_frames, n=window_size, axis=-1) / sampling_frequency
|
|
44
|
+
|
|
45
|
+
return r_stft_matrix
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""helper functions for transform_freq"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
import numpy as np
|
|
7
|
+
import scipy.special
|
|
8
|
+
|
|
9
|
+
jax.config.update("jax_enable_x64", True)
|
|
10
|
+
|
|
11
|
+
import jax.numpy as jnp # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def phitilde_vec(om, n_f, nx=4.0):
|
|
15
|
+
"""Compute phitilde, om i array, nx is filter steepness, defaults to 4.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
om (1D numpy array): om.
|
|
19
|
+
n_f (int): Number of frequency bins.
|
|
20
|
+
nx (float, optional): Filter steepness. Defaults to 4.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
1D numpy array: z.
|
|
24
|
+
"""
|
|
25
|
+
omega = np.pi
|
|
26
|
+
d_omega = omega / n_f
|
|
27
|
+
ins_d_omega = 1.0 / np.sqrt(d_omega)
|
|
28
|
+
b = omega / (2 * n_f)
|
|
29
|
+
a = (d_omega - b) / 2
|
|
30
|
+
z = np.zeros(om.size)
|
|
31
|
+
|
|
32
|
+
mask = (np.abs(om) >= a) & (np.abs(om) < a + b)
|
|
33
|
+
|
|
34
|
+
x = (np.abs(om[mask]) - a) / b
|
|
35
|
+
y = scipy.special.betainc(nx, nx, x)
|
|
36
|
+
z[mask] = ins_d_omega * np.cos(np.pi / 2.0 * y)
|
|
37
|
+
|
|
38
|
+
z[np.abs(om) < a] = ins_d_omega
|
|
39
|
+
return z
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def phitilde_vec_norm(n_f, n_t, nx):
|
|
43
|
+
"""Normalize phitilde as needed for inverse frequency domain transform.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
n_f (int): Number of frequency bins.
|
|
47
|
+
n_t (int): Number of time bins.
|
|
48
|
+
nx (float): Filter steepness
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
phif: Wavelet.
|
|
52
|
+
"""
|
|
53
|
+
n_d = n_f * n_t
|
|
54
|
+
oms = 2 * np.pi / n_d * np.arange(0, n_t // 2 + 1)
|
|
55
|
+
phif = phitilde_vec(oms, n_f, nx)
|
|
56
|
+
# nrm should be 1
|
|
57
|
+
nrm = np.sqrt((2 * np.sum(phif[1:] ** 2) + phif[0] ** 2) * 2 * np.pi / n_d)
|
|
58
|
+
nrm /= np.pi ** (3 / 2) / np.pi
|
|
59
|
+
phif /= nrm
|
|
60
|
+
return phif
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def tukey(data, alpha, n_sample):
|
|
64
|
+
"""Apply Tukey window function to data.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
data (1D numpy array): Data.
|
|
68
|
+
alpha (float): Rolling parameter.
|
|
69
|
+
n_sample (int): Length of data
|
|
70
|
+
"""
|
|
71
|
+
imin = int(alpha * (n_sample - 1) / 2)
|
|
72
|
+
imax = int((n_sample - 1) * (1 - alpha / 2))
|
|
73
|
+
n_win = n_sample - imax
|
|
74
|
+
values = jnp.asarray(data)
|
|
75
|
+
indices = jnp.arange(n_sample)
|
|
76
|
+
safe_imin = max(imin, 1)
|
|
77
|
+
rising_values = 0.5 * (1.0 + jnp.cos(jnp.pi * (indices / safe_imin - 1.0)))
|
|
78
|
+
falling_values = 0.5 * (1.0 + jnp.cos(jnp.pi / n_win * (indices - imax)))
|
|
79
|
+
window = jnp.where(indices < imin, rising_values, 1.0)
|
|
80
|
+
window = jnp.where(indices > imax, falling_values, window)
|
|
81
|
+
result = values * window
|
|
82
|
+
if isinstance(data, np.ndarray):
|
|
83
|
+
data[...] = np.asarray(result)
|
|
84
|
+
return None
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _frequency_packets(data, n_f, n_t, phif):
|
|
89
|
+
"""Build every windowed frequency packet in one JAX gather."""
|
|
90
|
+
data = jnp.asarray(data)
|
|
91
|
+
phif = jnp.asarray(phif, dtype=data.real.dtype)
|
|
92
|
+
modes = jnp.arange(n_f + 1)[:, None]
|
|
93
|
+
offsets = jnp.arange(1 - n_t // 2, n_t // 2)[None, :]
|
|
94
|
+
frequency_indices = modes * (n_t // 2) + offsets
|
|
95
|
+
safe_indices = jnp.clip(frequency_indices, 0, data.size - 1)
|
|
96
|
+
valid = ~(((modes == 0) & (offsets < 0)) | ((modes == n_f) & (offsets > 0)))
|
|
97
|
+
values = phif[jnp.abs(offsets)] * data[safe_indices]
|
|
98
|
+
|
|
99
|
+
packets = jnp.zeros((n_f + 1, n_t), dtype=data.dtype)
|
|
100
|
+
packets = packets.at[:, 1:].set(jnp.where(valid, values, 0.0))
|
|
101
|
+
endpoint_scale = jnp.where((jnp.arange(n_f + 1) == 0) | (jnp.arange(n_f + 1) == n_f), 0.5, 1.0)
|
|
102
|
+
centres = phif[0] * data[jnp.arange(n_f + 1) * (n_t // 2)] * endpoint_scale
|
|
103
|
+
return packets.at[:, n_t // 2].set(centres)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _unpack_frequency_packets(transformed, n_f, n_t, *, quadrature=False, frequency_filter=None):
|
|
107
|
+
"""Map packet FFT components to the real WDM coefficient array."""
|
|
108
|
+
rows = jnp.arange(n_t)[:, None]
|
|
109
|
+
modes = jnp.arange(1, n_f)[None, :]
|
|
110
|
+
interior = transformed[1:n_f].T
|
|
111
|
+
parity = (rows + modes) % 2 == 1
|
|
112
|
+
odd_mode = modes % 2 == 1
|
|
113
|
+
if quadrature:
|
|
114
|
+
values = jnp.where(parity, jnp.real(interior), jnp.where(odd_mode, -jnp.imag(interior), jnp.imag(interior)))
|
|
115
|
+
else:
|
|
116
|
+
values = jnp.where(parity, jnp.where(odd_mode, -jnp.imag(interior), jnp.imag(interior)), jnp.real(interior))
|
|
117
|
+
|
|
118
|
+
if frequency_filter is not None:
|
|
119
|
+
selected = jnp.asarray(frequency_filter, dtype=bool)
|
|
120
|
+
values = jnp.where(selected[None, 1:n_f], values, 0.0)
|
|
121
|
+
else:
|
|
122
|
+
selected = jnp.ones(n_f + 1, dtype=bool)
|
|
123
|
+
|
|
124
|
+
wave = jnp.zeros((n_t, n_f), dtype=transformed.real.dtype)
|
|
125
|
+
wave = wave.at[:, 1:].set(values)
|
|
126
|
+
packet_indices = jnp.arange(1 if quadrature else 0, n_t, 2)
|
|
127
|
+
wave = wave.at[::2, 0].set(jnp.where(selected[0], jnp.real(transformed[0, packet_indices]) * jnp.sqrt(2.0), 0.0))
|
|
128
|
+
wave = wave.at[1::2, 0].set(
|
|
129
|
+
jnp.where(selected[n_f], jnp.real(transformed[n_f, packet_indices]) * jnp.sqrt(2.0), 0.0)
|
|
130
|
+
)
|
|
131
|
+
return wave
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def transform_wavelet_freq_helper(data, n_f, n_t, phif):
|
|
135
|
+
"""Helper to do the wavelet transform using the fast wavelet domain transform.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
data (1D numpy array): Data.
|
|
139
|
+
n_f (int): Number of frequency bins.
|
|
140
|
+
n_t (int): Number of time bins.
|
|
141
|
+
phif (1D numpy array): Wavelet.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
2D numpy array: Data in wavelet domain.
|
|
145
|
+
"""
|
|
146
|
+
packets = _frequency_packets(data, n_f, n_t, phif)
|
|
147
|
+
return _unpack_frequency_packets(jnp.fft.ifft(packets, axis=1), n_f, n_t)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def transform_wavelet_freq_partial_helper(data, n_f, n_t, phif, frequency_filter):
|
|
151
|
+
"""Helper to do the wavelet transform using the fast wavelet domain transform.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
data (1D numpy array): Data.
|
|
155
|
+
n_f (int): Number of frequency bins.
|
|
156
|
+
n_t (int): Number of time bins.
|
|
157
|
+
phif (1D numpy array): Wavelet.
|
|
158
|
+
frequency_filter (1D numpy numpy): An array to indicate which frequency bins to evaluate.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
2D numpy array: Data in wavelet domain.
|
|
162
|
+
"""
|
|
163
|
+
packets = _frequency_packets(data, n_f, n_t, phif)
|
|
164
|
+
return _unpack_frequency_packets(jnp.fft.ifft(packets, axis=1), n_f, n_t, frequency_filter=frequency_filter)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def transform_wavelet_freq_quadrature_helper(data, n_f, n_t, phif):
|
|
168
|
+
"""Helper to do the wavelet transform using the fast wavelet domain quadrature transform.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
data (1D numpy array): Data.
|
|
172
|
+
n_f (int): Number of frequency bins.
|
|
173
|
+
n_t (int): Number of time bins.
|
|
174
|
+
phif (1D numpy array): Wavelet.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
2D numpy array: Data in wavelet domain.
|
|
178
|
+
"""
|
|
179
|
+
packets = _frequency_packets(data, n_f, n_t, phif)
|
|
180
|
+
return _unpack_frequency_packets(jnp.fft.ifft(packets, axis=1), n_f, n_t, quadrature=True)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""JAX kernels for the direct time-domain WDM transform."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
|
|
7
|
+
jax.config.update("jax_enable_x64", True)
|
|
8
|
+
|
|
9
|
+
import jax.numpy as jnp # noqa: E402
|
|
10
|
+
import numpy as np # noqa: E402
|
|
11
|
+
|
|
12
|
+
from .transform_freq_funcs import phitilde_vec # noqa: E402
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def transform_wavelet_time_helper(data, n_f, n_t, phi, mult):
|
|
16
|
+
"""Transform a time series into WDM coefficients with batched JAX FFTs.
|
|
17
|
+
|
|
18
|
+
Computation uses the common floating dtype of ``data`` and ``phi``: two
|
|
19
|
+
float32 inputs stay float32, while float64 or integer data paired with the
|
|
20
|
+
float64 window use float64.
|
|
21
|
+
"""
|
|
22
|
+
data = jnp.asarray(data)
|
|
23
|
+
phi = jnp.asarray(phi)
|
|
24
|
+
working_dtype = jnp.result_type(data, phi)
|
|
25
|
+
data = data.astype(working_dtype)
|
|
26
|
+
phi = phi.astype(working_dtype)
|
|
27
|
+
n_d = n_f * n_t
|
|
28
|
+
n_k = mult * 2 * n_f
|
|
29
|
+
rows = jnp.arange(n_t)[:, None]
|
|
30
|
+
offsets = jnp.arange(n_k)[None, :]
|
|
31
|
+
sample_indices = (rows * n_f - n_k // 2 + offsets) % n_d
|
|
32
|
+
packets = data[sample_indices] * phi[None, :]
|
|
33
|
+
transformed = jnp.fft.rfft(packets, axis=1)
|
|
34
|
+
|
|
35
|
+
modes = jnp.arange(1, n_f)[None, :]
|
|
36
|
+
selected = transformed[:, (jnp.arange(1, n_f) * mult)]
|
|
37
|
+
interior = jnp.where((rows + modes) % 2 == 1, -jnp.imag(selected), jnp.real(selected))
|
|
38
|
+
|
|
39
|
+
wave = jnp.zeros((n_t, n_f), dtype=transformed.real.dtype)
|
|
40
|
+
wave = wave.at[:, 1:].set(interior)
|
|
41
|
+
wave = wave.at[::2, 0].set(jnp.real(transformed[::2, 0]) / jnp.sqrt(2.0))
|
|
42
|
+
return wave.at[1::2, 0].set(jnp.real(transformed[::2, n_f * mult]) / jnp.sqrt(2.0))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def phi_vec(n_f, nx=4.0, mult=16):
|
|
46
|
+
"""Return the time-domain WDM window."""
|
|
47
|
+
omega = np.pi
|
|
48
|
+
d_omega = omega / n_f
|
|
49
|
+
inverse_sqrt_d_omega = 1.0 / np.sqrt(d_omega)
|
|
50
|
+
k_cutoff = mult * 2 * n_f
|
|
51
|
+
half_k_cutoff = mult * n_f
|
|
52
|
+
dom = 2 * np.pi / k_cutoff
|
|
53
|
+
|
|
54
|
+
spectrum = np.zeros(k_cutoff, dtype=np.complex128)
|
|
55
|
+
spectrum[0] = inverse_sqrt_d_omega
|
|
56
|
+
spectrum[1 : half_k_cutoff + 1] = phitilde_vec(dom * np.arange(1, half_k_cutoff + 1), n_f, nx)
|
|
57
|
+
spectrum[half_k_cutoff + 1 :] = phitilde_vec(-dom * np.arange(half_k_cutoff - 1, 0, -1), n_f, nx)
|
|
58
|
+
transformed = k_cutoff * jnp.fft.ifft(jnp.asarray(spectrum), k_cutoff)
|
|
59
|
+
phi = jnp.concatenate((jnp.real(transformed[half_k_cutoff:]), jnp.real(transformed[:half_k_cutoff])))
|
|
60
|
+
return phi * (jnp.sqrt(2.0) / jnp.sqrt(k_cutoff / dom))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for the time-frequency transform.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_shape_of_wavelet_transform(t_length: int, sampling_frequency: float, frequency_resolution: float) -> tuple:
|
|
9
|
+
"""Get the shape of wavelet transform.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
t_length (int): Length of the time series.
|
|
13
|
+
sampling_frequency (float): Sampling frequency in Hz.
|
|
14
|
+
frequency_resolution (float): Frequency resolution in Hz.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
tuple: n_t, n_f. Number of time points and frequency points.
|
|
18
|
+
"""
|
|
19
|
+
n_f = int(sampling_frequency / 2 / frequency_resolution)
|
|
20
|
+
n_t = int(t_length / n_f)
|
|
21
|
+
return n_t, n_f
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""helper functions for transform_time.py"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .inverse_wavelet_freq_funcs import inverse_wavelet_freq_helper_fast
|
|
10
|
+
from .inverse_wavelet_time_funcs import inverse_wavelet_time_helper_fast
|
|
11
|
+
from .transform_freq_funcs import (
|
|
12
|
+
phitilde_vec_norm,
|
|
13
|
+
transform_wavelet_freq_helper,
|
|
14
|
+
transform_wavelet_freq_quadrature_helper,
|
|
15
|
+
)
|
|
16
|
+
from .transform_time_funcs import phi_vec, transform_wavelet_time_helper
|
|
17
|
+
from .utils import get_shape_of_wavelet_transform
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def inverse_wavelet_time(wave_in, n_f, n_t, nx=4.0, mult=32) -> jax.Array:
|
|
21
|
+
"""Fast inverse wavelet transform to time domain.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
wave_in (2D numpy array): Data in wavelet domain.
|
|
25
|
+
n_f (int): Number of frequency bins.
|
|
26
|
+
n_t (int): Number of time bins.
|
|
27
|
+
nx (float, optional): Steepness of the filter. Defaults to 4..
|
|
28
|
+
mult (int, optional): mult. Defaults to 32.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
jax.Array: Data in time domain.
|
|
32
|
+
"""
|
|
33
|
+
mult = min(mult, n_t // 2) # make sure K isn't bigger than n_d
|
|
34
|
+
phi = phi_vec(n_f, nx=nx, mult=mult) / 2
|
|
35
|
+
output = inverse_wavelet_time_helper_fast(wave_in, phi, n_f, n_t, mult)
|
|
36
|
+
return output / jnp.sqrt(output.shape[0])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def inverse_wavelet_freq_time(wave_in, n_f, n_t, nx=4.0) -> jax.Array:
|
|
40
|
+
"""Inverse wavelet transform to time domain via Fourier transform of frequency domain.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
wave_in (2D numpy array): Data in wavelet domain.
|
|
44
|
+
n_f (int): Number of frequency bins.
|
|
45
|
+
n_t (int): Number of time bins.
|
|
46
|
+
nx (float, optional): Steepness of filter. Defaults to 4..
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
jax.Array: Data in time domain.
|
|
50
|
+
"""
|
|
51
|
+
res_f = inverse_wavelet_freq(wave_in, n_f, n_t, nx)
|
|
52
|
+
return jnp.fft.irfft(res_f)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def inverse_wavelet_freq(wave_in, n_f, n_t, nx=4.0) -> jax.Array:
|
|
56
|
+
"""Inverse wavelet transform to frequency domain signal.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
wave_in (2D numpy array): Data in wavelet domain.
|
|
60
|
+
n_f (int): Number of frequency bins.
|
|
61
|
+
n_t (int): Number of time bins.
|
|
62
|
+
nx (float, optional): Steepness of filter. Defaults to 4..
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
jax.Array: Data in frequency domain.
|
|
66
|
+
"""
|
|
67
|
+
phif = phitilde_vec_norm(n_f, n_t, nx)
|
|
68
|
+
output = inverse_wavelet_freq_helper_fast(wave_in, phif, n_f, n_t)
|
|
69
|
+
return output / jnp.sqrt((output.shape[0] - 1) * 2)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def transform_wavelet_time(data, n_f, n_t, nx=4.0, mult=32) -> jax.Array:
|
|
73
|
+
"""Do the wavelet transform in the time domain,
|
|
74
|
+
note there can be significant leakage if mult is too small and the
|
|
75
|
+
transform is only approximately exact if mult=n_t/2.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
data (1D numpy array): Data in time domain.
|
|
79
|
+
n_f (int): Number of frequency bins.
|
|
80
|
+
n_t (int): Number of time bins.
|
|
81
|
+
nx (float, optional): Steepness of filter. Defaults to 4..
|
|
82
|
+
mult (int, optional): mult. Defaults to 32.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
jax.Array: Data in wavelet domain.
|
|
86
|
+
"""
|
|
87
|
+
mult = min(mult, n_t // 2) # make sure K isn't bigger than n_d
|
|
88
|
+
phi = phi_vec(n_f, nx, mult)
|
|
89
|
+
wave = transform_wavelet_time_helper(data, n_f, n_t, phi, mult) * jnp.sqrt(data.shape[0])
|
|
90
|
+
|
|
91
|
+
return wave
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def transform_wavelet_freq_time(data, n_f, n_t, nx=4.0) -> jax.Array:
|
|
95
|
+
"""Transform time domain data into wavelet domain via FFT and then frequency transform.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
data (1D numpy array): Data in time domain.
|
|
99
|
+
n_f (int): Number of frequency bins.
|
|
100
|
+
n_t (int): Number of time bins.
|
|
101
|
+
nx (float, optional): Steepness of filter. Defaults to 4..
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
jax.Array: Data in wavelet domain.
|
|
105
|
+
"""
|
|
106
|
+
data_fft = jnp.fft.rfft(data)
|
|
107
|
+
|
|
108
|
+
return transform_wavelet_freq(data_fft, n_f, n_t, nx)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def transform_wavelet_freq_time_quadrature(data, n_f, n_t, nx=4.0) -> jax.Array:
|
|
112
|
+
"""Transform time domain data into wavelet quadrature domain via FFT and then frequency transform.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
data (1D numpy array): Data in time domain.
|
|
116
|
+
n_f (int): Number of frequency bins.
|
|
117
|
+
n_t (int): Number of time bins.
|
|
118
|
+
nx (float, optional): Steepness of filter. Defaults to 4..
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
jax.Array: Data in wavelet domain.
|
|
122
|
+
"""
|
|
123
|
+
data_fft = jnp.fft.rfft(data)
|
|
124
|
+
|
|
125
|
+
return transform_wavelet_freq_quadrature(data_fft, n_f, n_t, nx)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def transform_wavelet_freq_quadrature(data, n_f, n_t, nx=4.0) -> jax.Array:
|
|
129
|
+
"""Do the wavelet quadrature transform using the fast wavelet domain transform.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
data (1D complex numpy array): Data in frequency domain.
|
|
133
|
+
n_f (int): Number of frequency bins.
|
|
134
|
+
n_t (int): Number of time bins.
|
|
135
|
+
nx (float, optional): Steepness of filter. Defaults to 4..
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
jax.Array: Data in wavelet domain.
|
|
139
|
+
"""
|
|
140
|
+
phif = 2 / n_f * phitilde_vec_norm(n_f, n_t, nx)
|
|
141
|
+
return transform_wavelet_freq_quadrature_helper(data, n_f, n_t, phif) * jnp.sqrt((data.shape[0] - 1) * 2)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def transform_wavelet_freq(data, n_f, n_t, nx=4.0) -> jax.Array:
|
|
145
|
+
"""Do the wavelet transform using the fast wavelet domain transform.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
data (1D complex numpy array): Data in frequency domain.
|
|
149
|
+
n_f (int): Number of frequency bins.
|
|
150
|
+
n_t (int): Number of time bins.
|
|
151
|
+
nx (float, optional): Steepness of filter. Defaults to 4..
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
jax.Array: Data in wavelet domain.
|
|
155
|
+
"""
|
|
156
|
+
phif = 2 / n_f * phitilde_vec_norm(n_f, n_t, nx)
|
|
157
|
+
return transform_wavelet_freq_helper(data, n_f, n_t, phif) * jnp.sqrt((data.shape[0] - 1) * 2)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class WaveletTransform:
|
|
161
|
+
"""A class to handle wavelet transform."""
|
|
162
|
+
|
|
163
|
+
def __init__(self, duration: float, sampling_frequency: float, frequency_resolution: float, nx: float = 4.0):
|
|
164
|
+
"""Wavelet transform.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
duration (float): Duration in second.
|
|
168
|
+
sampling_frequency (float): Sampling frequency in Hz.
|
|
169
|
+
frequency_resolution (float): Frequency resolution in Hz.
|
|
170
|
+
nx (float): Sharpness of wavelet.
|
|
171
|
+
Defaults to 4.0.
|
|
172
|
+
"""
|
|
173
|
+
self.duration = duration
|
|
174
|
+
self.sampling_frequency = sampling_frequency
|
|
175
|
+
self.frequency_resolution = frequency_resolution
|
|
176
|
+
self.nx = nx
|
|
177
|
+
self._t_length = int(duration * sampling_frequency)
|
|
178
|
+
self._f_length = self._t_length // 2 + 1
|
|
179
|
+
self.shape = get_shape_of_wavelet_transform(
|
|
180
|
+
t_length=int(duration * sampling_frequency),
|
|
181
|
+
sampling_frequency=sampling_frequency,
|
|
182
|
+
frequency_resolution=frequency_resolution,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def frequency_to_wavelet(self, frequency_domain_data: np.ndarray) -> jax.Array:
|
|
186
|
+
"""Transform from frequency domain to wavelet domain.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
frequency_domain_data (np.ndarray): Frequency-domain data.
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
ValueError: The length of frequency_domain_data does not match
|
|
193
|
+
the expected length.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
jax.Array: Wavelet-domain data.
|
|
197
|
+
"""
|
|
198
|
+
# Check whether the length of frequency-domain data
|
|
199
|
+
# matches the expected length.
|
|
200
|
+
if len(frequency_domain_data) != self._f_length:
|
|
201
|
+
raise ValueError(
|
|
202
|
+
f"The length of frequency_domain_data: {len(frequency_domain_data)}"
|
|
203
|
+
f"does not match the expected length: {self._f_length}."
|
|
204
|
+
)
|
|
205
|
+
return transform_wavelet_freq(data=frequency_domain_data, n_f=self.shape[1], n_t=self.shape[0])
|
|
206
|
+
|
|
207
|
+
def wavelet_to_frequency(self, wavelet_domain_data: np.ndarray) -> jax.Array:
|
|
208
|
+
"""Transform from wavelet domain to frequency domain.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
wavelet_domain_data (np.ndarray): Wavelet-domain data.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ValueError: The shape of wavelet_domain_data does not match
|
|
215
|
+
the expected shape.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
jax.Array: Frequency-domain data.
|
|
219
|
+
"""
|
|
220
|
+
# Check whether the shape of wavelet-domain strain
|
|
221
|
+
# matches the expected shape.
|
|
222
|
+
if wavelet_domain_data.shape != self.shape:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"The shape of wavelet_domain_data: {wavelet_domain_data.shape}"
|
|
225
|
+
f"does not match the expected shape: {self.shape}."
|
|
226
|
+
)
|
|
227
|
+
return inverse_wavelet_freq(wave_in=wavelet_domain_data, n_f=self.shape[1], n_t=self.shape[0], nx=self.nx)
|
|
228
|
+
|
|
229
|
+
def frequency_to_wavelet_quadrature(self, frequency_domain_data: np.ndarray) -> jax.Array:
|
|
230
|
+
"""Transform from frequency domain to wavelet quadrature domain.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
frequency_domain_data (np.ndarray): Frequency-domain data.
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
ValueError: The length of frequency_domain_data does not match
|
|
237
|
+
the expected length.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
jax.Array: Wavelet quadrature domain data.
|
|
241
|
+
"""
|
|
242
|
+
# Check whether the length of frequency-domain data
|
|
243
|
+
# matches the expected length.
|
|
244
|
+
if len(frequency_domain_data) != self._f_length:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
f"The length of frequency_domain_data: {len(frequency_domain_data)}"
|
|
247
|
+
f"does not match the expected length: {self._f_length}."
|
|
248
|
+
)
|
|
249
|
+
return transform_wavelet_freq_quadrature(data=frequency_domain_data, n_f=self.shape[1], n_t=self.shape[0])
|
|
File without changes
|