From 5f4b205758c72e527fecbca272fd412c4e45fa53 Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:18:12 +0800 Subject: [PATCH 1/2] Prevent manifold EMA state aliasing --- .../filters/manifold_exponential_moving_average.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pyrecest/filters/manifold_exponential_moving_average.py b/src/pyrecest/filters/manifold_exponential_moving_average.py index a78172dbf..91f7cb3de 100644 --- a/src/pyrecest/filters/manifold_exponential_moving_average.py +++ b/src/pyrecest/filters/manifold_exponential_moving_average.py @@ -1,5 +1,6 @@ """Exponential moving average for states on manifolds.""" +import copy from typing import Any, Callable import numpy as np @@ -107,16 +108,16 @@ def filter_state(self): @filter_state.setter def filter_state(self, new_state): - self._filter_state = new_state + self._filter_state = copy.deepcopy(new_state) def update(self, sample): """Update the moving average with a new manifold-valued sample.""" if self._filter_state is None: - self._filter_state = sample + self.filter_state = sample return tangent_update = self.alpha * asarray(self.phi_inv(self._filter_state, sample)) - self._filter_state = self.phi(self._filter_state, tangent_update) + self.filter_state = self.phi(self._filter_state, tangent_update) def get_point_estimate(self): """Return the current manifold estimate.""" From 828928dd0dc4842e7aa13a8d8a8aa40fc545719e Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:18:29 +0800 Subject: [PATCH 2/2] Test manifold EMA state ownership --- ...old_exponential_moving_average_aliasing.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/filters/test_manifold_exponential_moving_average_aliasing.py diff --git a/tests/filters/test_manifold_exponential_moving_average_aliasing.py b/tests/filters/test_manifold_exponential_moving_average_aliasing.py new file mode 100644 index 000000000..16bf39eec --- /dev/null +++ b/tests/filters/test_manifold_exponential_moving_average_aliasing.py @@ -0,0 +1,44 @@ +"""Regression tests for manifold EMA state ownership.""" + +import numpy as np +import numpy.testing as npt + +from pyrecest.filters import ManifoldExponentialMovingAverage + + +def _phi_euclidean(state, tangent): + return state + tangent + + +def _phi_inv_euclidean(state_ref, state): + return state - state_ref + + +def test_first_sample_is_copied_into_filter_state(): + sample = np.array([2.0, 3.0]) + ema = ManifoldExponentialMovingAverage( + initial_state=None, + alpha=0.5, + phi=_phi_euclidean, + phi_inv=_phi_inv_euclidean, + ) + + ema.update(sample) + sample[:] = -1.0 + + npt.assert_array_equal(ema.filter_state, np.array([2.0, 3.0])) + + +def test_explicit_state_assignment_does_not_alias_caller_array(): + ema = ManifoldExponentialMovingAverage( + initial_state=np.array([0.0, 0.0]), + alpha=0.5, + phi=_phi_euclidean, + phi_inv=_phi_inv_euclidean, + ) + replacement = np.array([4.0, 5.0]) + + ema.filter_state = replacement + replacement[:] = 0.0 + + npt.assert_array_equal(ema.filter_state, np.array([4.0, 5.0]))