From fb37bb9eaa0f04414f43c04b1c50aaa22f02d7da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=A4fer?= Date: Mon, 6 Jul 2026 13:29:43 +0200 Subject: [PATCH 1/2] fix get_words to work with sliding windows --- .../collection/dictionary_based/_sfa.py | 32 +++++++++++-------- .../collection/dictionary_based/_sfa_fast.py | 29 ++++++----------- .../dictionary_based/tests/test_sfa.py | 26 +++++++++++++++ 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/aeon/transformations/collection/dictionary_based/_sfa.py b/aeon/transformations/collection/dictionary_based/_sfa.py index b862c280db..a5eb836c09 100644 --- a/aeon/transformations/collection/dictionary_based/_sfa.py +++ b/aeon/transformations/collection/dictionary_based/_sfa.py @@ -4,7 +4,7 @@ """ __maintainer__ = [] -__all__ = ["SFA"] +__all__ = ["SFA", "get_chars"] import math import os @@ -476,10 +476,15 @@ def get_words(self): ------- Array of words """ - words = np.squeeze(self.words) - return np.array( - [_get_chars(word, self.word_length, self.alphabet_size) for word in words] - ) + if self.save_words: + return np.array( + [ + get_chars(word, self.word_length, self.alphabet_size) + for word in self.words + ] + ) + + raise ValueError("save_words must be set to True when initializing SFA.") def _binning(self, X, y=None): num_windows_per_inst = math.ceil(self.n_timepoints / self.window_size) @@ -1178,16 +1183,17 @@ def _get_test_params(cls, parameter_set="default"): @njit(cache=True, fastmath=True) -def _get_chars(word, word_length, alphabet_size): - chars = np.zeros(word_length, dtype=np.uint32) +def get_chars(words, word_length, alphabet_size): + chars = np.zeros((len(words), word_length), dtype=np.uint32) letter_bits = int(np.log2(alphabet_size)) mask = (1 << letter_bits) - 1 - for i in range(word_length): - # Extract the last bits - char = word & mask - chars[-i - 1] = char + for j in range(chars.shape[0]): + for i in range(word_length): + # Extract the last bits + char = words[j] & mask + chars[j][-i - 1] = char - # Right shift by to move to the next group of bits - word >>= letter_bits + # Right shift by to move to the next group of bits + words[j] >>= letter_bits return chars diff --git a/aeon/transformations/collection/dictionary_based/_sfa_fast.py b/aeon/transformations/collection/dictionary_based/_sfa_fast.py index df32d1100c..fdf79c3de8 100644 --- a/aeon/transformations/collection/dictionary_based/_sfa_fast.py +++ b/aeon/transformations/collection/dictionary_based/_sfa_fast.py @@ -30,6 +30,7 @@ from sklearn.utils import check_random_state from aeon.transformations.collection import BaseCollectionTransformer +from aeon.transformations.collection.dictionary_based._sfa import get_chars from aeon.utils.numba.general import AEON_NUMBA_STD_THRESHOLD from aeon.utils.validation import check_n_jobs @@ -769,10 +770,15 @@ def get_words(self): ------- Array of words """ - words = np.squeeze(self.words) - return np.array( - [_get_chars(word, self.word_length, self.letter_bits) for word in words] - ) + if self.save_words: + return np.array( + [ + get_chars(word, self.word_length, self.alphabet_size) + for word in self.words + ] + ) + + raise ValueError("save_words must be set to True when initializing SFA_FAST.") def transform_words(self, X): """Return the words and dft coefficients generated for each series. @@ -859,21 +865,6 @@ def __setstate__(self, state): self.relevant_features = typed_dict -@njit(cache=True, fastmath=True) -def _get_chars(word, word_length, letter_bits): - chars = np.zeros(word_length, dtype=np.uint32) - for i in range(word_length): - # Extract the last bits - mask = (1 << letter_bits[i]) - 1 - char = word & mask - chars[-i - 1] = char - - # Right shift by to move to the next group of bits - word >>= letter_bits[i] - - return chars - - @njit(fastmath=True, cache=True, parallel=True) def _binning_dft( X, diff --git a/aeon/transformations/collection/dictionary_based/tests/test_sfa.py b/aeon/transformations/collection/dictionary_based/tests/test_sfa.py index 849f91cc93..a5c0ce9557 100644 --- a/aeon/transformations/collection/dictionary_based/tests/test_sfa.py +++ b/aeon/transformations/collection/dictionary_based/tests/test_sfa.py @@ -286,3 +286,29 @@ def test_sfa_dynamic(alphabet_allocation_method): # Check if the budget is correctly allocated assert np.mean(np.log2(p.alphabet_sizes)) <= np.log2(alphabet_size) + + +def test_sfa_get_words_from_sliding_window(): + """Test get_words with sliding window (multiple windows per case).""" + X = np.random.rand(3, 1, 20) + y = np.array([0, 1, 0]) + + # Sliding window: window_size < n_timepoints + word_length = 4 + window_size = 10 + alphabet_size = 8 + + p = SFA( + word_length=word_length, + alphabet_size=alphabet_size, + window_size=window_size, + save_words=True, + ) + p.fit_transform(X, y) + + words = p.get_words() + + # Should return array of shape (n_cases, n_windows, word_length) + assert len(words) == X.shape[0] # n_cases + assert len(words[0]) == X.shape[-1] - window_size + 1 # sliding windows + assert len(words[0][0]) == word_length From c78021e6ebab36c6da8a44dcfbe2aeea1c352a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=A4fer?= Date: Thu, 9 Jul 2026 16:49:01 +0200 Subject: [PATCH 2/2] udpates --- .../collection/dictionary_based/_sfa.py | 5 +++-- .../dictionary_based/tests/test_sfa.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/aeon/transformations/collection/dictionary_based/_sfa.py b/aeon/transformations/collection/dictionary_based/_sfa.py index a5eb836c09..9fb215a047 100644 --- a/aeon/transformations/collection/dictionary_based/_sfa.py +++ b/aeon/transformations/collection/dictionary_based/_sfa.py @@ -1188,12 +1188,13 @@ def get_chars(words, word_length, alphabet_size): letter_bits = int(np.log2(alphabet_size)) mask = (1 << letter_bits) - 1 for j in range(chars.shape[0]): + word = words[j] # local copy for i in range(word_length): # Extract the last bits - char = words[j] & mask + char = word & mask chars[j][-i - 1] = char # Right shift by to move to the next group of bits - words[j] >>= letter_bits + word >>= letter_bits # only changes local copy return chars diff --git a/aeon/transformations/collection/dictionary_based/tests/test_sfa.py b/aeon/transformations/collection/dictionary_based/tests/test_sfa.py index a5c0ce9557..0d9909df4a 100644 --- a/aeon/transformations/collection/dictionary_based/tests/test_sfa.py +++ b/aeon/transformations/collection/dictionary_based/tests/test_sfa.py @@ -7,6 +7,7 @@ from aeon.datasets import load_unit_test from aeon.transformations.collection.dictionary_based import SFA, SFAFast, SFAWhole +from aeon.transformations.collection.dictionary_based._sfa import get_chars @pytest.mark.parametrize( @@ -312,3 +313,20 @@ def test_sfa_get_words_from_sliding_window(): assert len(words) == X.shape[0] # n_cases assert len(words[0]) == X.shape[-1] - window_size + 1 # sliding windows assert len(words[0][0]) == word_length + + +def test_get_chars_does_not_modify_input(): + """Test that get_chars does not modify the input words array.""" + words = np.array( + [ + np.uint32(0b110101), + np.uint32(0b101011), + np.uint32(0b011110), + ], + dtype=np.uint32, + ) + words_original = words.copy() + + _ = get_chars(words, word_length=3, alphabet_size=4) + + np.testing.assert_array_equal(words, words_original)