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.
Files changed (42) hide show
  1. nullcal/__init__.py +16 -0
  2. nullcal/calibration.py +189 -0
  3. nullcal/clustering/__init__.py +0 -0
  4. nullcal/clustering/base.py +39 -0
  5. nullcal/clustering/injection.py +63 -0
  6. nullcal/clustering/precompute.py +30 -0
  7. nullcal/clustering/single.py +208 -0
  8. nullcal/clustering/time_frequency_map.py +42 -0
  9. nullcal/data.py +93 -0
  10. nullcal/likelihood/__init__.py +9 -0
  11. nullcal/likelihood/recalibration_likelihood.py +227 -0
  12. nullcal/metadata/__init__.py +0 -0
  13. nullcal/metadata/yaml.py +30 -0
  14. nullcal/null_stream/__init__.py +0 -0
  15. nullcal/null_stream/calibration.py +40 -0
  16. nullcal/null_stream/null_stream.py +252 -0
  17. nullcal/null_stream/projector.py +48 -0
  18. nullcal/null_stream/whiten.py +106 -0
  19. nullcal/result/__init__.py +5 -0
  20. nullcal/result/result.py +50 -0
  21. nullcal/result/utils.py +13 -0
  22. nullcal/sampler.py +113 -0
  23. nullcal/studies/__init__.py +1 -0
  24. nullcal/studies/lwa_leakage.py +513 -0
  25. nullcal/studies/spline_resolution.py +148 -0
  26. nullcal/time_frequency_transform/README.md +7 -0
  27. nullcal/time_frequency_transform/__init__.py +23 -0
  28. nullcal/time_frequency_transform/inverse_wavelet_freq_funcs.py +42 -0
  29. nullcal/time_frequency_transform/inverse_wavelet_time_funcs.py +49 -0
  30. nullcal/time_frequency_transform/stft.py +45 -0
  31. nullcal/time_frequency_transform/transform_freq_funcs.py +180 -0
  32. nullcal/time_frequency_transform/transform_time_funcs.py +60 -0
  33. nullcal/time_frequency_transform/utils.py +21 -0
  34. nullcal/time_frequency_transform/wavelet_transforms.py +249 -0
  35. nullcal/utils/__init__.py +0 -0
  36. nullcal/utils/log.py +72 -0
  37. nullcal/utils/snr.py +29 -0
  38. nullcal/version.py +9 -0
  39. nullcal-0.2.0.dist-info/METADATA +222 -0
  40. nullcal-0.2.0.dist-info/RECORD +42 -0
  41. nullcal-0.2.0.dist-info/WHEEL +4 -0
  42. 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