Skip to content

Commit f64eebd

Browse files
Copilotalexarje
andauthored
Replace stub test_ssm.py with 19 unit tests for SSM helper functions
Agent-Logs-Url: https://github.com/fourMs/MGT-python/sessions/dda917bb-b265-4678-a561-da54642b9ad6 Co-authored-by: alexarje <114316+alexarje@users.noreply.github.com>
1 parent b387361 commit f64eebd

1 file changed

Lines changed: 186 additions & 5 deletions

File tree

tests/test_ssm.py

Lines changed: 186 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,188 @@
1-
# test_with_pytest.py
1+
"""Unit tests for musicalgestures._ssm helper functions.
22
3-
def test_always_passes():
4-
assert True
3+
These tests cover the pure-Python/NumPy helpers that do not require FFmpeg or
4+
a real video file, so they run in every environment including CI without
5+
additional system dependencies.
6+
"""
7+
from __future__ import annotations
58

6-
def test_always_fails():
7-
assert False
9+
import numpy as np
10+
import pytest
11+
from scipy import signal
12+
13+
from musicalgestures._ssm import smooth_downsample_feature_sequence, slow_dot
14+
15+
16+
# ---------------------------------------------------------------------------
17+
# smooth_downsample_feature_sequence
18+
# ---------------------------------------------------------------------------
19+
20+
class TestSmoothDownsampleFeatureSequence:
21+
"""Tests for smooth_downsample_feature_sequence."""
22+
23+
def _make_X(self, n_features=3, n_frames=100):
24+
rng = np.random.default_rng(0)
25+
return rng.random((n_features, n_frames)).astype(np.float64)
26+
27+
# --- output shape -------------------------------------------------------
28+
29+
def test_output_shape_default_params(self):
30+
X = self._make_X(n_features=4, n_frames=100)
31+
X_smooth, sr_feat, _ = smooth_downsample_feature_sequence(X, sr=10)
32+
# with down_sampling=10: columns become ceil/floor of 100/10 = 10
33+
assert X_smooth.shape[0] == 4
34+
assert X_smooth.shape[1] == len(range(0, 100, 10))
35+
36+
def test_output_shape_custom_downsampling(self):
37+
X = self._make_X(n_features=2, n_frames=50)
38+
X_smooth, _, _ = smooth_downsample_feature_sequence(X, sr=5, down_sampling=5)
39+
assert X_smooth.shape == (2, len(range(0, 50, 5)))
40+
41+
def test_output_shape_single_feature(self):
42+
X = self._make_X(n_features=1, n_frames=80)
43+
X_smooth, _, _ = smooth_downsample_feature_sequence(X, sr=8, down_sampling=4)
44+
assert X_smooth.shape[0] == 1
45+
assert X_smooth.shape[1] == len(range(0, 80, 4))
46+
47+
# --- sampling rate ------------------------------------------------------
48+
49+
def test_sampling_rate_reduced(self):
50+
_, sr_feat, _ = smooth_downsample_feature_sequence(
51+
self._make_X(), sr=100, down_sampling=10
52+
)
53+
assert sr_feat == pytest.approx(10.0)
54+
55+
def test_sampling_rate_custom(self):
56+
_, sr_feat, _ = smooth_downsample_feature_sequence(
57+
self._make_X(), sr=60, down_sampling=4
58+
)
59+
assert sr_feat == pytest.approx(15.0)
60+
61+
def test_sampling_rate_no_downsampling(self):
62+
_, sr_feat, _ = smooth_downsample_feature_sequence(
63+
self._make_X(), sr=30, down_sampling=1
64+
)
65+
assert sr_feat == pytest.approx(30.0)
66+
67+
# --- smoothing effect ---------------------------------------------------
68+
69+
def test_constant_signal_unchanged_by_smoothing(self):
70+
"""A constant-valued feature sequence should remain constant after smoothing."""
71+
X = np.ones((2, 100))
72+
X_smooth, _, _ = smooth_downsample_feature_sequence(
73+
X, sr=10, filt_len=11, down_sampling=1, w_type='boxcar'
74+
)
75+
# Interior samples should be very close to 1.0 (edge effects excluded)
76+
interior = X_smooth[:, 20:-20]
77+
np.testing.assert_allclose(interior, 1.0, atol=1e-10)
78+
79+
def test_smoothing_reduces_variance(self):
80+
"""Smoothing should reduce the variance of a noisy signal."""
81+
rng = np.random.default_rng(42)
82+
X = rng.random((1, 500))
83+
X_smooth, _, _ = smooth_downsample_feature_sequence(
84+
X, sr=50, filt_len=41, down_sampling=1
85+
)
86+
assert X_smooth.var() < X.var()
87+
88+
# --- formatter ----------------------------------------------------------
89+
90+
def test_formatter_is_callable(self):
91+
_, _, formatter = smooth_downsample_feature_sequence(
92+
self._make_X(), sr=10
93+
)
94+
# FuncFormatter wraps our inner function; calling it should return a string
95+
result = formatter(5.0, 0)
96+
assert isinstance(result, str)
97+
98+
def test_formatter_output_value(self):
99+
_, _, formatter = smooth_downsample_feature_sequence(
100+
self._make_X(), sr=10
101+
)
102+
# The inner function multiplies x by the default down_sampling (10)
103+
# and rounds to 1 decimal place. With a float input, round() returns
104+
# a float, so str(round(3.5 * 10, 1)) == "35.0".
105+
assert formatter(3.5, 0) == str(round(3.5 * 10, 1))
106+
107+
# --- window types -------------------------------------------------------
108+
109+
def test_different_window_types_produce_output(self):
110+
X = self._make_X(n_features=2, n_frames=80)
111+
for w in ('boxcar', 'hann', 'hamming', 'blackman'):
112+
X_smooth, sr_feat, _ = smooth_downsample_feature_sequence(
113+
X, sr=10, w_type=w
114+
)
115+
assert X_smooth.shape[0] == 2
116+
assert sr_feat == pytest.approx(1.0)
117+
118+
# --- dtype / value range ------------------------------------------------
119+
120+
def test_output_is_float(self):
121+
X = self._make_X()
122+
X_smooth, _, _ = smooth_downsample_feature_sequence(X, sr=10)
123+
assert np.issubdtype(X_smooth.dtype, np.floating)
124+
125+
126+
# ---------------------------------------------------------------------------
127+
# slow_dot
128+
# ---------------------------------------------------------------------------
129+
130+
class TestSlowDot:
131+
"""Tests for slow_dot (low-memory dot product wrapper)."""
132+
133+
# --- correctness --------------------------------------------------------
134+
135+
def test_result_matches_numpy_dot(self):
136+
rng = np.random.default_rng(7)
137+
X = rng.random((10, 20))
138+
Y = rng.random((20, 15))
139+
S = slow_dot(X, Y, length=10)
140+
np.testing.assert_allclose(S, np.dot(X, Y), atol=1e-12)
141+
142+
def test_square_identity_matrix(self):
143+
n = 8
144+
X = np.eye(n)
145+
S = slow_dot(X, X, length=n)
146+
np.testing.assert_allclose(S, np.eye(n), atol=1e-12)
147+
148+
def test_result_shape(self):
149+
rng = np.random.default_rng(11)
150+
m, k, n = 5, 12, 7
151+
X = rng.random((m, k))
152+
Y = rng.random((k, n))
153+
S = slow_dot(X, Y, length=m)
154+
assert S.shape == (m, n)
155+
156+
def test_single_row(self):
157+
X = np.array([[1.0, 2.0, 3.0]])
158+
Y = np.array([[4.0], [5.0], [6.0]])
159+
S = slow_dot(X, Y, length=1)
160+
assert S.shape == (1, 1)
161+
assert S[0, 0] == pytest.approx(32.0)
162+
163+
def test_zero_matrices(self):
164+
X = np.zeros((6, 10))
165+
Y = np.zeros((10, 6))
166+
S = slow_dot(X, Y, length=6)
167+
np.testing.assert_array_equal(S, np.zeros((6, 6)))
168+
169+
# --- self-similarity matrix properties ---------------------------------
170+
171+
def test_ssm_symmetry(self):
172+
"""X @ X.T should be symmetric (a common SSM construction)."""
173+
rng = np.random.default_rng(99)
174+
X = rng.random((15, 8))
175+
S = slow_dot(X, X.T, length=15)
176+
np.testing.assert_allclose(S, S.T, atol=1e-12)
177+
178+
def test_ssm_diagonal_is_max(self):
179+
"""In an SSM built from normalised rows, diagonal >= off-diagonal."""
180+
rng = np.random.default_rng(3)
181+
X = rng.random((10, 5))
182+
# Normalise rows
183+
norms = np.linalg.norm(X, axis=1, keepdims=True) + 1e-8
184+
X_norm = X / norms
185+
S = slow_dot(X_norm, X_norm.T, length=10)
186+
diag = np.diag(S)
187+
for i in range(len(diag)):
188+
assert diag[i] >= S[i, :].max() - 1e-10

0 commit comments

Comments
 (0)