From 27f0f03d78dbb9db9a52993061717d2d99a61e8c Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 27 Jul 2026 15:37:49 +0200 Subject: [PATCH] fix(quickadapter): guard filtfilt padding length (#143) Derive the SciPy default pad length from the FIR coefficient lengths and use the same no-op boundary for smoothing and label lookahead. --- quickadapter/user_data/strategies/Utils.py | 30 +++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index bf3be75..0337dce 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -797,6 +797,14 @@ assert SMOOTHING_KERNELS == ( ) +def _filtfilt_default_padlen( + numerator_length: int, + denominator_length: int, +) -> int: + """Return SciPy's default ``filtfilt`` pad length.""" + return 3 * max(numerator_length, denominator_length) + + def get_smoothing_kernel_half_width( config: dict[str, Any], *, @@ -819,8 +827,7 @@ def get_smoothing_kernel_half_width( ``int(4.0 * sigma + 0.5)`` (scipy's ``round`` form). Returns 0 for ``method == "none"``, for ``series_length < max(window_candles, 3)`` (``smooth()`` top-level no-op), and for the filtfilt/savgol routes - when ``series_length < effective_window`` (downstream short-series - no-op in ``zero_phase_filter`` / ``savgol_filter``). + when their downstream short-series guards make smoothing a no-op. """ method = config.get("method", SMOOTHING_METHODS[0]) if method == SMOOTHING_METHODS[0]: # "none" @@ -844,13 +851,12 @@ def get_smoothing_kernel_half_width( effective_window = get_even_window(raw_window) else: effective_window = get_odd_window(raw_window) - # ``zero_phase_filter`` / ``savgol_filter`` short-series gate - if ( - method in SMOOTHING_KERNELS or method == SMOOTHING_METHODS[7] - ) and series_length < effective_window: - return 0 if method in SMOOTHING_KERNELS: + if series_length <= _filtfilt_default_padlen(effective_window, 1): + return 0 return effective_window - 1 + if method == SMOOTHING_METHODS[7] and series_length < effective_window: + return 0 return effective_window // 2 @@ -1747,14 +1753,14 @@ def zero_phase_filter( if len(series) < window: return series - values = series.to_numpy(dtype=float) - if values.size <= 1: - return series - b = _calculate_coeffs(window=window, win_type=win_type, std=std, beta=beta) a = np.array([1.0], dtype=float) + padlen = _filtfilt_default_padlen(len(b), len(a)) + if len(series) <= padlen: + return series - filtered_values = sp.signal.filtfilt(b, a, values) + values = series.to_numpy(dtype=float) + filtered_values = sp.signal.filtfilt(b, a, values, padlen=padlen) return pd.Series(filtered_values, index=series.index) -- 2.53.0