diff --git a/shared/tests/test_force_filters.py b/shared/tests/test_force_filters.py index 2538225..98ff3c7 100644 --- a/shared/tests/test_force_filters.py +++ b/shared/tests/test_force_filters.py @@ -95,7 +95,41 @@ def test_reset_clears_the_memory_between_sessions(): ) -def test_settings_round_trip_through_a_preferences_mapping(): +def test_the_chain_reproduces_the_sdk_filter_exactly(): + """Replacing the SDK's low-pass with ours must not move a single number. + + ``Sensor._apply_force_filter`` filters ``_force_raw`` with + ``orisys.signal_processing.LowpassFilter`` (Butterworth, ``sosfilt`` with + persistent state, steady-state reset) and then applies the device + calibration. Our chain uses the same recipe, so at the same settings the + recorded values stay identical — this test is what makes that a guarantee + rather than a hope. + """ + try: + from orisys.signal_processing import LowpassFilter + except Exception: # pragma: no cover - the SDK is not installed here + return + + rng = np.random.default_rng(3) + signal = np.cumsum(rng.normal(0.0, 1.0, (400, 3)), axis=0) * 10.0 + 500.0 + + sdk = LowpassFilter(cutoff_hz=10.0, sample_rate_hz=30.0, order=2, n_channels=3) + sdk.configure(cutoff_hz=10.0, sample_rate_hz=30.0, order=2) + sdk.reset(signal[0]) + sdk_out = np.array([sdk.process(row) for row in signal]) + + chain = ForceFilterChain( + FilterSettings(lowpass_enabled=True, lowpass_hz=10.0, sample_rate_hz=30.0) + ) + # The chain carries six channels (raw + newtons); the first three are the + # raw counts the SDK filters, so give all six the same steady state. + chain.reset(list(signal[0]) * 2) + chain_out = np.array([chain.process(list(row) * 2)[:3] for row in signal]) + + assert np.array_equal(sdk_out, chain_out), ( + f"the chain differs from the SDK filter by {np.abs(sdk_out - chain_out).max()}" + ) + settings = settings_from_mapping( { "median_enabled": True, @@ -111,3 +145,14 @@ def test_settings_round_trip_through_a_preferences_mapping(): # An empty mapping is the documented default, not a disabled chain. default = settings_from_mapping(None) assert default.as_dict()["lowpass_hz"] == FilterSettings().lowpass_hz + + +def test_settings_round_trip_through_a_preferences_mapping(): + settings = settings_from_mapping({ + "median_enabled": True, "median_window": 7, + "lowpass_enabled": True, "lowpass_hz": 12.5, "show_unfiltered": True, + }) + assert settings.median_enabled and settings.median_window == 7 + assert settings.lowpass_hz == 12.5 and settings.show_unfiltered + assert settings.describe() == "median(7) + lowpass(12.5Hz,order2)" + assert settings_from_mapping(None).as_dict()["lowpass_hz"] == FilterSettings().lowpass_hz