Skip to content

Commit 843e5f9

Browse files
committed
whenxuan: create the unittest for synchroextracting transform
1 parent 4ad30ba commit 843e5f9

6 files changed

Lines changed: 253 additions & 0 deletions

File tree

‎pysdkit/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@
133133
# Synchrosqueezing Transform
134134
from ._tfa import SST, sst
135135

136+
# Synchroextracting Transform
137+
from ._tfa import SET, set
138+
136139
# Adaptive Polymorphic Mode Decomposition / Impulsive Mode Decomposition
137140
from ._imd import APMD, IMD, imd
138141

@@ -235,6 +238,7 @@ def print_functions():
235238
Adaptive Local Iterative Filtering | ALIF
236239
Variational TF Mode Tracking Decomposition | VTFMTD
237240
Synchrosqueezing Transform | SST
241+
Synchroextracting Transform | SET
238242
Adaptive Polymorphic Mode Decomposition | APMD
239243
Impulsive Mode Decomposition | IMD
240244
Empirical Wavelet Transform | EWT
@@ -314,6 +318,8 @@ def print_functions():
314318
"vtfmtd",
315319
"SST",
316320
"sst",
321+
"SET",
322+
"set",
317323
"APMD",
318324
"IMD",
319325
"imd",

‎pysdkit/data/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
"load_sst_float",
5151
"load_sst_doppler",
5252
"load_fast_kurtogram_x",
53+
"load_set_batdata2",
54+
"load_set_vibdata",
5355
]
5456

5557
# A series of functions for generating 1D NumPy signals
@@ -139,4 +141,6 @@
139141
load_sst_float,
140142
load_sst_doppler,
141143
load_fast_kurtogram_x,
144+
load_set_batdata2,
145+
load_set_vibdata,
142146
)

‎pysdkit/data/_loaders.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,38 @@ def load_sst_doppler() -> Dict[str, np.ndarray]:
306306
return {"signal": signal}
307307

308308

309+
def load_set_batdata2() -> Dict[str, Union[np.ndarray, float]]:
310+
"""
311+
Load the packaged bat echolocation record used by SET Example 3.
312+
313+
MATLAB ``batdata2.mat`` (variable ``data``, 400 samples) from Yu,
314+
Yu and Xu, IEEE TIE 64(10) 2017, ``Example_3.m``. Sampling rate
315+
``Fs = 1e6 / 7``. Stored as ``set_batdata2.npy``.
316+
317+
:return: dict with ``signal``, ``fs``, ``t``
318+
"""
319+
signal = _load_float1d("set_batdata2.npy")
320+
fs = 1.0e6 / 7.0
321+
t = np.arange(signal.size, dtype=float) / fs
322+
return {"signal": signal, "fs": fs, "t": t}
323+
324+
325+
def load_set_vibdata() -> Dict[str, Union[np.ndarray, float]]:
326+
"""
327+
Load the packaged vibration record used by SET Example 4.
328+
329+
MATLAB ``vibdata.mat`` (variable ``data``, 1024 samples) from Yu,
330+
Yu and Xu, IEEE TIE 64(10) 2017, ``Example_4.m``. Sampling rate
331+
2000 Hz. Stored as ``set_vibdata.npy``.
332+
333+
:return: dict with ``signal``, ``fs``, ``t``
334+
"""
335+
signal = _load_float1d("set_vibdata.npy")
336+
fs = 2000.0
337+
t = np.arange(signal.size, dtype=float) / fs
338+
return {"signal": signal, "fs": fs, "t": t}
339+
340+
309341
def load_memd_taichi_hex() -> Dict[str, np.ndarray]:
310342
"""
311343
Load the packaged MATLAB ``taichi_hex_inp.mat`` demo (``taichi_final``).

‎pysdkit/data/set_batdata2.npy‎

3.25 KB
Binary file not shown.

‎pysdkit/data/set_vibdata.npy‎

8.13 KB
Binary file not shown.

‎pysdkit/tests/test_set.py‎

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Unit tests for the Synchroextracting Transform (SET).
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import unittest
9+
10+
import numpy as np
11+
12+
from pysdkit import SET, set
13+
from pysdkit._tfa.set import SET as ModuleSET
14+
from pysdkit._tfa.set import (
15+
brevridge,
16+
brevridge_mult,
17+
frequency_axis_set,
18+
gaussian_window,
19+
odd_window_length,
20+
reconstruct_from_ridges,
21+
set_transform,
22+
stft_gaussian_pair,
23+
)
24+
from pysdkit.data import load_set_batdata2, load_set_vibdata
25+
26+
27+
def _fm_tone(n_samples: int = 256, freq: float = 0.12) -> np.ndarray:
28+
time = np.arange(n_samples, dtype=float)
29+
return np.cos(2.0 * np.pi * freq * time)
30+
31+
32+
class SETHelperTest(unittest.TestCase):
33+
"""Window, STFT and ridge helpers."""
34+
35+
def test_odd_window_length(self) -> None:
36+
self.assertEqual(odd_window_length(40), 41)
37+
self.assertEqual(odd_window_length(41), 41)
38+
39+
def test_odd_window_rejects_tiny(self) -> None:
40+
with self.assertRaises(ValueError):
41+
odd_window_length(2)
42+
43+
def test_gaussian_window_peak_and_derivative(self) -> None:
44+
window, deriv = gaussian_window(41)
45+
self.assertEqual(window.size, 41)
46+
self.assertEqual(int(np.argmax(window)), 20)
47+
self.assertAlmostEqual(
48+
float(np.max(np.abs(deriv + deriv[::-1]))), 0.0, places=12
49+
)
50+
self.assertGreater(float(window[20]), float(window[0]))
51+
52+
def test_frequency_axis_set(self) -> None:
53+
freq = frequency_axis_set(400, fs=100.0)
54+
self.assertEqual(freq.size, 200)
55+
self.assertAlmostEqual(freq[0], 0.0)
56+
self.assertAlmostEqual(freq[1], 100.0 / 400.0)
57+
self.assertEqual(frequency_axis_set(401, fs=1.0).size, 201)
58+
59+
def test_stft_shape(self) -> None:
60+
samples = _fm_tone(128)
61+
tfr, tfr_d, window, va = stft_gaussian_pair(samples, 17)
62+
self.assertEqual(tfr.shape, (64, 128))
63+
self.assertEqual(tfr_d.shape, (64, 128))
64+
self.assertEqual(window.size, 17)
65+
self.assertAlmostEqual(va, 128 / 17.0)
66+
67+
def test_set_transform_shape_and_extraction(self) -> None:
68+
samples = _fm_tone(128)
69+
result = set_transform(samples, hlength=17, fs=1.0)
70+
self.assertEqual(result["te"].shape, (64, 128))
71+
self.assertEqual(result["tfr"].shape, (64, 128))
72+
self.assertTrue(np.all(np.isfinite(result["te"])))
73+
n_stft = int(np.count_nonzero(np.abs(result["tfr"]) > 1e-12))
74+
n_set = int(np.count_nonzero(np.abs(result["te"]) > 1e-12))
75+
self.assertLess(n_set, n_stft)
76+
self.assertTrue(np.all((result["seo"] == 0.0) | (result["seo"] == 1.0)))
77+
78+
def test_rejects_2d_and_short(self) -> None:
79+
with self.assertRaises(ValueError):
80+
set_transform(np.ones((4, 4)))
81+
with self.assertRaises(ValueError):
82+
set_transform(np.ones(5))
83+
84+
85+
class SETPublicTest(unittest.TestCase):
86+
"""Public ``SET`` TFR, IF peak, and modal reconstruction."""
87+
88+
def test_public_alias(self) -> None:
89+
self.assertIs(SET, ModuleSET)
90+
91+
def test_transform_tone_peaks_near_carrier(self) -> None:
92+
freq_c = 0.12
93+
samples = _fm_tone(256, freq=freq_c)
94+
te, freq = SET(hlength=33, fs=1.0).transform(samples)
95+
peak_bin = int(np.argmax(np.mean(np.abs(te), axis=1)))
96+
self.assertAlmostEqual(float(freq[peak_bin]), freq_c, delta=0.03)
97+
98+
def test_example1_if_track(self) -> None:
99+
fs = 100.0
100+
time = np.arange(0.0, 4.0, 1.0 / fs)
101+
phase = 25.0 * time + 10.0 * np.sin(1.5 * time)
102+
samples = np.exp(-0.5 * time) * np.sin(2.0 * np.pi * phase)
103+
inst_freq = 25.0 + 15.0 * np.cos(1.5 * time)
104+
engine = SET(hlength=40, fs=fs)
105+
te, freq = engine.transform(samples)
106+
peak_bin = np.argmax(np.abs(te), axis=0)
107+
est_hz = freq[peak_bin]
108+
mid = slice(40, -40)
109+
self.assertLess(float(np.median(np.abs(est_hz[mid] - inst_freq[mid]))), 3.0)
110+
111+
def test_fit_transform_shape_and_residue(self) -> None:
112+
samples = _fm_tone(192, freq=0.1)
113+
imfs = SET(hlength=25, n_imfs=1, fs=1.0).fit_transform(samples)
114+
self.assertEqual(imfs.shape, (2, samples.size))
115+
recon = np.sum(imfs, axis=0)
116+
np.testing.assert_allclose(recon, samples, atol=1e-10)
117+
self.assertTrue(np.all(np.isfinite(imfs)))
118+
119+
def test_call_matches_fit_transform(self) -> None:
120+
samples = _fm_tone(96, freq=0.08)
121+
engine = SET(hlength=17, n_imfs=1)
122+
np.testing.assert_allclose(engine(samples), engine.fit_transform(samples))
123+
124+
def test_functional_set(self) -> None:
125+
samples = _fm_tone(96, freq=0.09)
126+
imfs = set(samples, hlength=17, n_imfs=1)
127+
self.assertEqual(imfs.shape[0], 2)
128+
self.assertEqual(imfs.shape[1], samples.size)
129+
130+
def test_two_tones_modes_recover_sources(self) -> None:
131+
n_samples = 256
132+
time = np.arange(n_samples, dtype=float)
133+
s1 = np.sin(2.0 * np.pi * 0.06 * time)
134+
s2 = np.sin(2.0 * np.pi * 0.18 * time)
135+
mix = s1 + s2
136+
imfs = SET(hlength=41, n_imfs=2, clear_win=8).fit_transform(mix)
137+
modes = imfs[:2]
138+
corr = np.array(
139+
[
140+
[np.abs(np.corrcoef(modes[i], src)[0, 1]) for src in (s1, s2)]
141+
for i in range(2)
142+
]
143+
)
144+
self.assertGreater(float(np.max(corr[:, 0])), 0.75)
145+
self.assertGreater(float(np.max(corr[:, 1])), 0.75)
146+
147+
def test_multicomponent_example_energy(self) -> None:
148+
fs = 120.0
149+
time = np.arange(0.0, 4.0, 1.0 / fs)
150+
s1 = np.sin(
151+
2.0 * np.pi * (10.0 * time + 2.0 * np.arctan((2.0 * time - 2.0) ** 2))
152+
)
153+
s2 = np.sin(2.0 * np.pi * (32.0 * time + 10.0 * np.sin(time)))
154+
s3 = np.sin(2.0 * np.pi * (44.0 * time + 10.0 * np.sin(time)))
155+
mix = s1 + s2 + s3
156+
imfs = SET(hlength=55, fs=fs, n_imfs=3, clear_win=5).fit_transform(mix)
157+
self.assertEqual(imfs.shape, (4, mix.size))
158+
recon = np.sum(imfs[:3], axis=0)
159+
err = float(np.linalg.norm(mix - recon) / np.linalg.norm(mix))
160+
self.assertLess(err, 0.55)
161+
162+
def test_batdata_smoke(self) -> None:
163+
record = load_set_batdata2()
164+
samples = np.asarray(record["signal"])
165+
engine = SET(hlength=45, fs=float(record["fs"]), n_imfs=4)
166+
te, freq = engine.transform(samples)
167+
self.assertEqual(te.shape[1], samples.size)
168+
self.assertEqual(freq.size, te.shape[0])
169+
imfs = engine.fit_transform(samples, n_imfs=4)
170+
self.assertEqual(imfs.shape, (5, samples.size))
171+
np.testing.assert_allclose(np.sum(imfs, axis=0), samples, atol=1e-10)
172+
173+
def test_vibdata_tfr_finite(self) -> None:
174+
record = load_set_vibdata()
175+
te, freq = SET(hlength=50, fs=float(record["fs"])).transform(record["signal"])
176+
self.assertTrue(np.all(np.isfinite(te)))
177+
self.assertEqual(te.shape[1], np.asarray(record["signal"]).size)
178+
self.assertGreater(float(np.max(np.abs(te))), 0.0)
179+
180+
def test_ridge_helpers(self) -> None:
181+
n_freq, n_time = 40, 60
182+
tx = np.zeros((n_freq, n_time))
183+
ridge = np.clip(
184+
np.round(10 + 4 * np.sin(np.linspace(0, 2 * np.pi, n_time))).astype(int),
185+
0,
186+
n_freq - 1,
187+
)
188+
tx[ridge, np.arange(n_time)] = 3.0
189+
tx += 0.05 * np.random.default_rng(0).random((n_freq, n_time))
190+
freq = np.arange(n_freq, dtype=float)
191+
curve, energy = brevridge(tx, freq, ridge_lambda=1.0)
192+
self.assertEqual(curve.size, n_time)
193+
self.assertGreater(energy, -np.inf)
194+
self.assertLess(float(np.mean(np.abs(curve - ridge))), 3.0)
195+
ridges, energies = brevridge_mult(tx, freq, n_ridges=1, clear_win=3)
196+
self.assertEqual(ridges.shape, (1, n_time))
197+
self.assertEqual(energies.size, 1)
198+
modes = reconstruct_from_ridges(tx.astype(np.complex128), ridges)
199+
self.assertEqual(modes.shape, (1, n_time))
200+
201+
def test_invalid_init(self) -> None:
202+
with self.assertRaises(ValueError):
203+
SET(hlength=1)
204+
with self.assertRaises(ValueError):
205+
SET(fs=0.0)
206+
with self.assertRaises(ValueError):
207+
SET(n_imfs=0)
208+
209+
210+
if __name__ == "__main__":
211+
unittest.main()

0 commit comments

Comments
 (0)