diff --git a/src/pyrecest/filters/hyperspherical_ukf.py b/src/pyrecest/filters/hyperspherical_ukf.py index de26c48215..de2caa9e76 100644 --- a/src/pyrecest/filters/hyperspherical_ukf.py +++ b/src/pyrecest/filters/hyperspherical_ukf.py @@ -201,10 +201,13 @@ def predict_nonlinear_arbitrary_noise( # pylint: disable=too-many-locals "noise_samples and noise_weights must contain the same number " "of samples." ) + if noise_weights.shape[0] == 0: + raise ValueError("noise_weights must contain at least one sample.") if not all(isfinite(noise_weights)): raise ValueError("noise_weights must be finite.") if not all(noise_weights > 0): raise ValueError("noise_weights must be strictly positive.") + noise_weights = noise_weights / noise_weights.max() noise_weights = noise_weights / noise_weights.sum() mu = reshape(asarray(self._filter_state.mu, dtype=float64), (-1,)) diff --git a/tests/filters/test_hyperspherical_ukf_arbitrary_noise_normalization.py b/tests/filters/test_hyperspherical_ukf_arbitrary_noise_normalization.py index d65a6b4e75..a5de6079da 100644 --- a/tests/filters/test_hyperspherical_ukf_arbitrary_noise_normalization.py +++ b/tests/filters/test_hyperspherical_ukf_arbitrary_noise_normalization.py @@ -8,11 +8,11 @@ from pyrecest.filters.hyperspherical_ukf import HypersphericalUKF +@unittest.skipIf( + pyrecest.backend.__backend_name__ in ("pytorch", "jax"), + reason="Arbitrary-noise prediction is not supported on this backend", +) class HypersphericalUKFArbitraryNoiseNormalizationTest(unittest.TestCase): - @unittest.skipIf( - pyrecest.backend.__backend_name__ in ("pytorch", "jax"), - reason="Arbitrary-noise prediction is not supported on this backend", - ) def test_radial_model_scale_does_not_create_spurious_covariance(self): ukf = HypersphericalUKF(dim=2, alpha=1.0) noise_samples = np.array([[0.0, 1.0]]) @@ -36,3 +36,37 @@ def scaled_same_direction(_x, v): np.zeros((2, 2)), atol=1e-12, ) + + def test_maximum_finite_weights_do_not_overflow(self): + ukf = HypersphericalUKF(dim=2, alpha=1.0) + noise_samples = np.array([[0.0, 1.0]]) + noise_weights = np.full(2, np.finfo(float).max) + + def fixed_direction(_x, _v): + return array([1.0, 0.0]) + + with np.errstate(over="raise", invalid="raise", divide="raise"): + ukf.predict_nonlinear_arbitrary_noise( + fixed_direction, noise_samples, noise_weights + ) + + npt.assert_allclose( + np.asarray(ukf.filter_state.mu, dtype=float), + np.array([1.0, 0.0]), + atol=1e-12, + ) + npt.assert_allclose( + np.asarray(ukf.filter_state.C, dtype=float), + np.zeros((2, 2)), + atol=1e-12, + ) + + def test_rejects_empty_noise_support(self): + ukf = HypersphericalUKF(dim=2, alpha=1.0) + + with self.assertRaisesRegex(ValueError, "at least one sample"): + ukf.predict_nonlinear_arbitrary_noise( + lambda x, _v: x, + np.empty((1, 0)), + np.empty((0,)), + )