From ef57f5c98c1616c1a5677211132e0f17db2ee760 Mon Sep 17 00:00:00 2001 From: Aleksandr Sinitca Date: Thu, 7 May 2026 12:03:42 +0300 Subject: [PATCH 1/5] Revert "125 prediction support for skipping measurements in the kalman filter" --- StatTools/experimental/augmentation/perturbations.py | 8 ++------ StatTools/filters/kalman_filter.py | 10 +++------- StatTools/generators/kasdin_generator.py | 6 +----- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/StatTools/experimental/augmentation/perturbations.py b/StatTools/experimental/augmentation/perturbations.py index 1ac6354..1909758 100644 --- a/StatTools/experimental/augmentation/perturbations.py +++ b/StatTools/experimental/augmentation/perturbations.py @@ -6,9 +6,7 @@ from scipy.special import gamma as gamma_func -def add_noise( - signal: Sequence, ratio: float, noise_seed: int | None = None -) -> Tuple[np.ndarray, np.ndarray]: +def add_noise(signal: Sequence, ratio: float) -> Tuple[np.ndarray, np.ndarray]: """ Adds noise with a specified ratio of signal to noise ratio (sigma_signal / sigma_noise). @@ -16,7 +14,6 @@ def add_noise( signal (Sequence): The original signal. ratio (float): The desired sigma_signal / sigma_noise ratio (for example, 10 = noise 10 times weaker). - noise_seed (int): Seed for the random number generator. Returns: A noisy signal, noise. @@ -24,8 +21,7 @@ def add_noise( signal = np.array(signal) sigma_signal = np.std(signal, ddof=1) sigma_noise = sigma_signal / ratio - rng = np.random.default_rng(seed=noise_seed) - noise = rng.normal(0, sigma_noise, size=signal.shape) + noise = np.random.normal(0, sigma_noise, size=signal.shape) return signal + noise, noise diff --git a/StatTools/filters/kalman_filter.py b/StatTools/filters/kalman_filter.py index 26b94a0..07004c9 100644 --- a/StatTools/filters/kalman_filter.py +++ b/StatTools/filters/kalman_filter.py @@ -99,21 +99,17 @@ def adjust(self, z: np.ndarray) -> None: Parameters ---------- - z : np.ndarray or None + z : np.ndarray New system measurement, shape (dim_z, 1). - If None, the correction step is skipped and the filter propagates - the predicted state forward (IIR / dead-reckoning behaviour). Raises ------ ValueError - If z has wrong shape. + If None is passed instead of a measurement, or if z has wrong shape. """ if z is None: - return + raise ValueError("Do not pass None as a measurement") z = np.atleast_2d(np.asarray(z, dtype=float)) - if np.any(np.isnan(z)): - return if z.shape != (self._H.shape[0], 1): raise ValueError(f"Expected z shape ({self._H.shape[0]}, 1), got {z.shape}") # y = z - Hx diff --git a/StatTools/generators/kasdin_generator.py b/StatTools/generators/kasdin_generator.py index 4787d1b..cb4a7d9 100644 --- a/StatTools/generators/kasdin_generator.py +++ b/StatTools/generators/kasdin_generator.py @@ -161,15 +161,11 @@ def get_h(self): def create_kasdin_generator( h: float, length: int, - random_generator: Optional[Iterator[float]] = None, + random_generator: Optional[Iterator[float]] = iter(np.random.randn, None), normalize=True, filter_coefficients_length=None, - seed: int | None = None, ) -> KasdinGenerator | ERKasdinGenerator: """Fabric for creating a Kasdin generator.""" - if random_generator is None: - rng = np.random.default_rng(seed) - random_generator = iter(rng.standard_normal, None) if 0.5 <= h <= 1.5: return KasdinGenerator( h, length, random_generator, normalize, filter_coefficients_length From 494ff67473e00b84216f9fcf54f4cd470407236a Mon Sep 17 00:00:00 2001 From: Asya Lyanova <64890333+pipipyau@users.noreply.github.com> Date: Thu, 7 May 2026 13:24:21 +0300 Subject: [PATCH 2/5] fix: fix the seed, remove None test --- tests/test_dpcca.py | 8 ++++---- tests/test_kalman_filter.py | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/test_dpcca.py b/tests/test_dpcca.py index 197cecd..4cc941f 100644 --- a/tests/test_dpcca.py +++ b/tests/test_dpcca.py @@ -224,11 +224,11 @@ def test_dpcca_chol2d_correlation(hurst, des_r0): Three independent fBn tracks are generated with the given Hurst exponent and then correlated by multiplying with the Cholesky factor of R0. """ - length = 2**14 + length = 2**15 s_list = [512, 1024, 2048] - sig_1 = generate_fbn(hurst=hurst, length=length) - sig_2 = generate_fbn(hurst=hurst, length=length) - sig_3 = generate_fbn(hurst=hurst, length=length) + sig_1 = generate_fbn(hurst=hurst, length=length, seed=4) + sig_2 = generate_fbn(hurst=hurst, length=length, seed=44) + sig_3 = generate_fbn(hurst=hurst, length=length, seed=14) np.random.seed(42) signal_triplet = np.vstack((sig_1, sig_2, sig_3)).T diff --git a/tests/test_kalman_filter.py b/tests/test_kalman_filter.py index b163045..7165874 100644 --- a/tests/test_kalman_filter.py +++ b/tests/test_kalman_filter.py @@ -175,11 +175,6 @@ def test_adjust_covariance_stays_symmetric(self, kf_2x1): kf_2x1.adjust(np.array([[np.random.randn()]])) assert np.allclose(kf_2x1._P, kf_2x1._P.T, atol=1e-10) - def test_adjust_none_raises(self, kf_2x1): - """ValueError raised when None is passed as measurement.""" - with pytest.raises(ValueError, match="Do not pass None as a measurement"): - kf_2x1.adjust(None) - def test_adjust_wrong_shape_raises(self, kf_2x1): """ValueError raised when measurement has wrong shape.""" with pytest.raises(ValueError, match="Expected z shape \\(1, 1\\), got"): From 503b52aec9f234d336b391f9e50da1bcb9fc263d Mon Sep 17 00:00:00 2001 From: Asya Lyanova <64890333+pipipyau@users.noreply.github.com> Date: Thu, 7 May 2026 13:25:40 +0300 Subject: [PATCH 3/5] feat: add seed, update random generator, update doc --- StatTools/generators/kasdin_generator.py | 75 ++++++++++++++++++------ 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/StatTools/generators/kasdin_generator.py b/StatTools/generators/kasdin_generator.py index cb4a7d9..f0b5bf4 100644 --- a/StatTools/generators/kasdin_generator.py +++ b/StatTools/generators/kasdin_generator.py @@ -15,32 +15,44 @@ class KasdinGenerator: doi:10.1109/5.381848 Args: - h (float): Hurst exponent (0.5 < H < 1.5) # TODO: update docs - length (int): Maximum length of the sequence. - random_generator (Iterator[float], optional): Iterator providing random values. - Defaults is iter(np.random.randn(), None). + h (float): Hurst exponent, 0.5 <= H <= 1.5. + length (int): Length of the generated sequence (must be >= 1). + random_generator (Iterator[float], optional): Iterator providing i.i.d. normal + random values. If None, one is created from ``seed`` via + ``np.random.default_rng``. + normalize (bool): If True, the output is zero-mean unit-variance. + filter_coefficients_length (int, optional): Number of filter coefficients to use. + Defaults to ``length``. + seed (int | None): Seed for the internal RNG. Ignored when + ``random_generator`` is provided explicitly. Raises: - ValueError: If length is less than 1 - StopIteration('Sequence exhausted') : If maximum sequence length has been reached. + ValueError: If ``length`` is less than 1 or ``h`` is out of range. + StopIteration('Sequence exhausted'): If the iterator is advanced past the end. Example usage: >>> generator = KasdinGenerator(h, length) >>> trj = list(generator) + >>> generator = KasdinGenerator(h, length, seed=42) + >>> trj = list(generator) """ def __init__( self, h: float, length: int, - random_generator: Optional[Iterator[float]] = iter(np.random.randn, None), + random_generator: Optional[Iterator[float]] = None, normalize=True, filter_coefficients_length=None, + seed: Optional[int] = None, ) -> None: if length is not None and length < 1: raise ValueError("Length must be more than 1") self.validate_h(h) self._h = h self.length = length + if random_generator is None: + rng = np.random.default_rng(seed) + random_generator = iter(rng.standard_normal, None) self.random_generator = random_generator self.filter_coefficients_length = filter_coefficients_length @@ -104,15 +116,32 @@ def get_h(self): class ERKasdinGenerator(KasdinGenerator): - """Extended range version of Kasdin generator, which can be used for H < 0.5 and H > 1.5""" + """ + Extended-range Kasdin generator supporting H < 0.5 and H > 1.5. + + Values outside [0.5, 1.5] are reached by repeatedly differencing (H > 1.5) + or integrating (H < 0.5) a standard KasdinGenerator sequence. + + Args: + h (float): Hurst exponent. Any real value is accepted. + length (int): Length of the generated sequence. + random_generator (Iterator[float], optional): Iterator providing i.i.d. normal + random values. If None, one is created from ``seed`` via + ``np.random.default_rng``. + normalize (bool): If True, the output is zero-mean unit-variance. + filter_coefficients_length (int, optional): Number of filter coefficients. + seed (int | None): Seed for the internal RNG. Ignored when + ``random_generator`` is provided explicitly. + """ def __init__( self, h: float, length: int, - random_generator: Optional[Iterator[float]] = iter(np.random.randn, None), + random_generator: Optional[Iterator[float]] = None, normalize=True, filter_coefficients_length=None, + seed: Optional[int] = None, ) -> None: self._effective_h = h self.steps_count = 0 @@ -136,6 +165,7 @@ def __init__( random_generator, normalize, filter_coefficients_length, + seed, ) if self.steps_count > 0: @@ -161,15 +191,24 @@ def get_h(self): def create_kasdin_generator( h: float, length: int, - random_generator: Optional[Iterator[float]] = iter(np.random.randn, None), + random_generator: Optional[Iterator[float]] = None, normalize=True, filter_coefficients_length=None, + seed: Optional[int] = None, ) -> KasdinGenerator | ERKasdinGenerator: - """Fabric for creating a Kasdin generator.""" - if 0.5 <= h <= 1.5: - return KasdinGenerator( - h, length, random_generator, normalize, filter_coefficients_length - ) - return ERKasdinGenerator( - h, length, random_generator, normalize, filter_coefficients_length - ) + """Factory for creating a Kasdin generator. + + Args: + h (float): Hurst exponent. + length (int): Length of the generated sequence. + random_generator (Iterator[float], optional): Custom RNG iterator. + If None, one is built from ``seed``. + normalize (bool): Zero-mean unit-variance normalisation. + filter_coefficients_length (int, optional): Filter order. + seed (int | None): RNG seed. Ignored when ``random_generator`` is given. + + Returns: + KasdinGenerator for 0.5 <= h <= 1.5, ERKasdinGenerator otherwise. + """ + cls = KasdinGenerator if 0.5 <= h <= 1.5 else ERKasdinGenerator + return cls(h, length, random_generator, normalize, filter_coefficients_length, seed) From 00dead1937d24800bbeebadfc9c3227d9a708919 Mon Sep 17 00:00:00 2001 From: Asya Lyanova <64890333+pipipyau@users.noreply.github.com> Date: Thu, 7 May 2026 13:31:58 +0300 Subject: [PATCH 4/5] feat: support none in adjust --- StatTools/filters/kalman_filter.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/StatTools/filters/kalman_filter.py b/StatTools/filters/kalman_filter.py index 07004c9..26b94a0 100644 --- a/StatTools/filters/kalman_filter.py +++ b/StatTools/filters/kalman_filter.py @@ -99,17 +99,21 @@ def adjust(self, z: np.ndarray) -> None: Parameters ---------- - z : np.ndarray + z : np.ndarray or None New system measurement, shape (dim_z, 1). + If None, the correction step is skipped and the filter propagates + the predicted state forward (IIR / dead-reckoning behaviour). Raises ------ ValueError - If None is passed instead of a measurement, or if z has wrong shape. + If z has wrong shape. """ if z is None: - raise ValueError("Do not pass None as a measurement") + return z = np.atleast_2d(np.asarray(z, dtype=float)) + if np.any(np.isnan(z)): + return if z.shape != (self._H.shape[0], 1): raise ValueError(f"Expected z shape ({self._H.shape[0]}, 1), got {z.shape}") # y = z - Hx From 9c03bb5211b160771fc5291765eb04402ef3bd83 Mon Sep 17 00:00:00 2001 From: Asya Lyanova <64890333+pipipyau@users.noreply.github.com> Date: Thu, 7 May 2026 13:32:37 +0300 Subject: [PATCH 5/5] feat: add seed, update random gen --- StatTools/experimental/augmentation/perturbations.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/StatTools/experimental/augmentation/perturbations.py b/StatTools/experimental/augmentation/perturbations.py index 1909758..1ac6354 100644 --- a/StatTools/experimental/augmentation/perturbations.py +++ b/StatTools/experimental/augmentation/perturbations.py @@ -6,7 +6,9 @@ from scipy.special import gamma as gamma_func -def add_noise(signal: Sequence, ratio: float) -> Tuple[np.ndarray, np.ndarray]: +def add_noise( + signal: Sequence, ratio: float, noise_seed: int | None = None +) -> Tuple[np.ndarray, np.ndarray]: """ Adds noise with a specified ratio of signal to noise ratio (sigma_signal / sigma_noise). @@ -14,6 +16,7 @@ def add_noise(signal: Sequence, ratio: float) -> Tuple[np.ndarray, np.ndarray]: signal (Sequence): The original signal. ratio (float): The desired sigma_signal / sigma_noise ratio (for example, 10 = noise 10 times weaker). + noise_seed (int): Seed for the random number generator. Returns: A noisy signal, noise. @@ -21,7 +24,8 @@ def add_noise(signal: Sequence, ratio: float) -> Tuple[np.ndarray, np.ndarray]: signal = np.array(signal) sigma_signal = np.std(signal, ddof=1) sigma_noise = sigma_signal / ratio - noise = np.random.normal(0, sigma_noise, size=signal.shape) + rng = np.random.default_rng(seed=noise_seed) + noise = rng.normal(0, sigma_noise, size=signal.shape) return signal + noise, noise