From 6af005fca923af8ec21a53b2949f6648f2efe3ba Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:53:12 +0200 Subject: [PATCH 1/3] Add MEM-RBPF log-weight underflow regression --- .../test_mem_rbpf_log_weight_stability.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/filters/test_mem_rbpf_log_weight_stability.py diff --git a/tests/filters/test_mem_rbpf_log_weight_stability.py b/tests/filters/test_mem_rbpf_log_weight_stability.py new file mode 100644 index 0000000000..1e2e591d1b --- /dev/null +++ b/tests/filters/test_mem_rbpf_log_weight_stability.py @@ -0,0 +1,38 @@ +import numpy as np +import pytest +from pyrecest import backend +from pyrecest.backend import array, diag, eye, zeros +from pyrecest.filters.mem_rbpf_tracker import MEMRBPFTracker + + +pytestmark = pytest.mark.skipif( + backend.__backend_name__ != "numpy", + reason="MEM-RBPF particle-weight stability is currently exercised on NumPy only", +) + + +def test_mem_rbpf_particle_weights_preserve_tiny_covariance_information(): + tracker = MEMRBPFTracker( + kinematic_state=array([0.0, 0.0, 0.0, 0.0]), + covariance=eye(4), + shape_state=array([0.0, 1.0, 1.0]), + shape_covariance=diag(array([1e-3, 1e-3, 1e-3])), + meas_noise_cov=zeros((2, 2)), + sys_noise=zeros((4, 4)), + shape_sys_noise=zeros((3, 3)), + multiplicative_noise_cov=1e-200 * eye(2), + n_particles=2, + resampling_threshold=0, + ) + tracker.theta = array([0.0, 0.0]) + tracker.axis = array([[1.0, 1.0], [np.sqrt(2.0), np.sqrt(2.0)]]) + tracker.axis_covariances = zeros((2, 2, 2)) + tracker.weights = array([0.5, 0.5]) + + tracker._update_particle_weights( + centered=zeros((1, 2)), + meas_noise_cov=zeros((2, 2)), + mult_var=1e-200, + ) + + np.testing.assert_allclose(np.asarray(tracker.weights), [2.0 / 3.0, 1.0 / 3.0]) From da46c18b7952d4cb9691f2e294672b1fe152d373 Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Tue, 21 Jul 2026 21:55:15 +0200 Subject: [PATCH 2/3] Stabilize MEM-RBPF particle log likelihoods --- src/pyrecest/filters/mem_rbpf_tracker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyrecest/filters/mem_rbpf_tracker.py b/src/pyrecest/filters/mem_rbpf_tracker.py index 056ec5f9c9..890ccf3e66 100644 --- a/src/pyrecest/filters/mem_rbpf_tracker.py +++ b/src/pyrecest/filters/mem_rbpf_tracker.py @@ -463,13 +463,13 @@ def _update_particle_weights(self, centered, meas_noise_cov, mult_var): marginal_cov = self._symmetrize(marginal_cov) if self.covariance_regularization > 0.0: marginal_cov = marginal_cov + self.covariance_regularization * eye(2) - determinant = linalg.det(marginal_cov) - if float(determinant) <= 0.0: + determinant_sign, log_determinant = linalg.slogdet(marginal_cov) + if float(determinant_sign) <= 0.0 or not bool(isfinite(log_determinant)): log_likelihoods.append(array(-float("inf"))) continue inverse_cov = linalg.pinv(marginal_cov) quad = einsum("ma,ab,mb->m", centered, inverse_cov, centered) - log_likelihoods.append(-0.5 * backend_sum(log(determinant) + quad)) + log_likelihoods.append(-0.5 * backend_sum(log_determinant + quad)) log_likelihoods = array(log_likelihoods) log_weights = log(maximum(self.weights, 1e-300)) self.weights = self._normalize_log_weights(log_weights + log_likelihoods) From 1a1c2a14ebab2bd7b1622f2ca490f9682c3946cb Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:01:13 +0200 Subject: [PATCH 3/3] Use backend-supported stable MEM-RBPF log determinants --- src/pyrecest/filters/mem_rbpf_tracker.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pyrecest/filters/mem_rbpf_tracker.py b/src/pyrecest/filters/mem_rbpf_tracker.py index 890ccf3e66..47e94d2c54 100644 --- a/src/pyrecest/filters/mem_rbpf_tracker.py +++ b/src/pyrecest/filters/mem_rbpf_tracker.py @@ -463,10 +463,13 @@ def _update_particle_weights(self, centered, meas_noise_cov, mult_var): marginal_cov = self._symmetrize(marginal_cov) if self.covariance_regularization > 0.0: marginal_cov = marginal_cov + self.covariance_regularization * eye(2) - determinant_sign, log_determinant = linalg.slogdet(marginal_cov) - if float(determinant_sign) <= 0.0 or not bool(isfinite(log_determinant)): + covariance_eigenvalues = linalg.eigvalsh(marginal_cov) + if bool(backend_any(~isfinite(covariance_eigenvalues))) or bool( + backend_any(covariance_eigenvalues <= 0.0) + ): log_likelihoods.append(array(-float("inf"))) continue + log_determinant = backend_sum(log(covariance_eigenvalues)) inverse_cov = linalg.pinv(marginal_cov) quad = einsum("ma,ab,mb->m", centered, inverse_cov, centered) log_likelihoods.append(-0.5 * backend_sum(log_determinant + quad))