From e365aa2802f155d7b96e19123cb10aaaf5824c6e Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:03:10 +0800 Subject: [PATCH 1/5] Add one-shot CircularUKF state-copy patch workflow --- .../one-shot-circular-ukf-state-copy.yml | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 .github/workflows/one-shot-circular-ukf-state-copy.yml diff --git a/.github/workflows/one-shot-circular-ukf-state-copy.yml b/.github/workflows/one-shot-circular-ukf-state-copy.yml new file mode 100644 index 0000000000..d1f0dfd8ed --- /dev/null +++ b/.github/workflows/one-shot-circular-ukf-state-copy.yml @@ -0,0 +1,101 @@ +name: One-shot CircularUKF state ownership patch + +on: + push: + branches: + - agent/circular-ukf-state-copy + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v7 + with: + python-version: "3.13" + + - name: Apply focused fix and regression + run: | + python - <<'PY' + from pathlib import Path + + source_path = Path("src/pyrecest/filters/circular_ukf.py") + source = source_path.read_text() + old = ''' @filter_state.setter + def filter_state(self, new_state): + new_state = _as_circular_gaussian(new_state, "filter_state") + self._filter_state = new_state + '''.replace(" ", "") + new = ''' @filter_state.setter + def filter_state(self, new_state): + new_state = _as_circular_gaussian(new_state, "filter_state") + AbstractFilter.filter_state.fset(self, new_state) + '''.replace(" ", "") + if source.count(old) != 1: + raise RuntimeError("expected exactly one CircularUKF state setter") + source_path.write_text(source.replace(old, new)) + + test_path = Path("tests/filters/test_circular_ukf_state_ownership.py") + test_path.write_text( + '''import unittest + +import numpy.testing as npt + +import pyrecest.backend +from pyrecest.backend import array +from pyrecest.distributions import GaussianDistribution +from pyrecest.filters.circular_ukf import CircularUKF + + +class CircularUKFStateOwnershipTest(unittest.TestCase): + @unittest.skipUnless( + pyrecest.backend.__backend_name__ == "numpy", + "mutable NumPy arrays required for aliasing regression", + ) + def test_assignment_copies_state(self): + filt = CircularUKF() + assigned = GaussianDistribution(array([0.5]), array([[0.7]])) + filt.filter_state = assigned + + self.assertIsNot(filt.filter_state, assigned) + assigned.mu[0] = 1.5 + assigned.C[0, 0] = 2.0 + + npt.assert_equal(filt.filter_state.mu, array([0.5])) + npt.assert_equal(filt.filter_state.C, array([[0.7]])) + + +if __name__ == "__main__": + unittest.main() +''' + ) + + Path( + ".github/workflows/one-shot-circular-ukf-state-copy.yml" + ).unlink() + PY + + - name: Validate changed Python syntax + run: | + python -m compileall -q \ + src/pyrecest/filters/circular_ukf.py \ + tests/filters/test_circular_ukf_state_ownership.py + git diff --check + + - name: Commit focused patch + run: | + git config user.name "github-actions[bot]" + git config user.email \ + "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git diff --cached --check + git commit -m "Prevent CircularUKF state aliasing" + git push From a00aeea9e711487362992fb7d3f6e4da7270e3a2 Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:03:56 +0800 Subject: [PATCH 2/5] Make one-shot patch observable on pull requests --- .github/workflows/one-shot-circular-ukf-state-copy.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/one-shot-circular-ukf-state-copy.yml b/.github/workflows/one-shot-circular-ukf-state-copy.yml index d1f0dfd8ed..1bc14c0a07 100644 --- a/.github/workflows/one-shot-circular-ukf-state-copy.yml +++ b/.github/workflows/one-shot-circular-ukf-state-copy.yml @@ -4,6 +4,9 @@ on: push: branches: - agent/circular-ukf-state-copy + pull_request: + branches: + - main permissions: contents: write @@ -12,10 +15,11 @@ jobs: patch: runs-on: ubuntu-latest steps: - - name: Check out repository + - name: Check out patch branch uses: actions/checkout@v7 with: fetch-depth: 0 + ref: agent/circular-ukf-state-copy - name: Set up Python uses: actions/setup-python@v7 From a7f6d1339a6e4c56beac36eba9fe8f283e6cb4b3 Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:05:21 +0800 Subject: [PATCH 3/5] Prevent CircularUKF state aliasing --- src/pyrecest/filters/circular_ukf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyrecest/filters/circular_ukf.py b/src/pyrecest/filters/circular_ukf.py index 066ef921d4..253756e132 100644 --- a/src/pyrecest/filters/circular_ukf.py +++ b/src/pyrecest/filters/circular_ukf.py @@ -245,7 +245,7 @@ def filter_state(self) -> GaussianDistribution: @filter_state.setter def filter_state(self, new_state): new_state = _as_circular_gaussian(new_state, "filter_state") - self._filter_state = new_state + AbstractFilter.filter_state.fset(self, new_state) # ------------------------------------------------------------------ # Prediction From 3635739ef7538efbf8d6302206cc3ecdc4d549dc Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:05:29 +0800 Subject: [PATCH 4/5] Test CircularUKF state ownership --- .../test_circular_ukf_state_ownership.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/filters/test_circular_ukf_state_ownership.py diff --git a/tests/filters/test_circular_ukf_state_ownership.py b/tests/filters/test_circular_ukf_state_ownership.py new file mode 100644 index 0000000000..e462ebfece --- /dev/null +++ b/tests/filters/test_circular_ukf_state_ownership.py @@ -0,0 +1,30 @@ +import unittest + +import numpy.testing as npt + +import pyrecest.backend +from pyrecest.backend import array +from pyrecest.distributions import GaussianDistribution +from pyrecest.filters.circular_ukf import CircularUKF + + +class CircularUKFStateOwnershipTest(unittest.TestCase): + @unittest.skipUnless( + pyrecest.backend.__backend_name__ == "numpy", + "mutable NumPy arrays required for aliasing regression", + ) + def test_assignment_copies_state(self): + filt = CircularUKF() + assigned = GaussianDistribution(array([0.5]), array([[0.7]])) + filt.filter_state = assigned + + self.assertIsNot(filt.filter_state, assigned) + assigned.mu[0] = 1.5 + assigned.C[0, 0] = 2.0 + + npt.assert_equal(filt.filter_state.mu, array([0.5])) + npt.assert_equal(filt.filter_state.C, array([[0.7]])) + + +if __name__ == "__main__": + unittest.main() From 4f3ef2f1fc33c24e04c6e9469d40c50309da956d Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:05:47 +0800 Subject: [PATCH 5/5] Remove temporary patch workflow --- .../one-shot-circular-ukf-state-copy.yml | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 .github/workflows/one-shot-circular-ukf-state-copy.yml diff --git a/.github/workflows/one-shot-circular-ukf-state-copy.yml b/.github/workflows/one-shot-circular-ukf-state-copy.yml deleted file mode 100644 index 1bc14c0a07..0000000000 --- a/.github/workflows/one-shot-circular-ukf-state-copy.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: One-shot CircularUKF state ownership patch - -on: - push: - branches: - - agent/circular-ukf-state-copy - pull_request: - branches: - - main - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - name: Check out patch branch - uses: actions/checkout@v7 - with: - fetch-depth: 0 - ref: agent/circular-ukf-state-copy - - - name: Set up Python - uses: actions/setup-python@v7 - with: - python-version: "3.13" - - - name: Apply focused fix and regression - run: | - python - <<'PY' - from pathlib import Path - - source_path = Path("src/pyrecest/filters/circular_ukf.py") - source = source_path.read_text() - old = ''' @filter_state.setter - def filter_state(self, new_state): - new_state = _as_circular_gaussian(new_state, "filter_state") - self._filter_state = new_state - '''.replace(" ", "") - new = ''' @filter_state.setter - def filter_state(self, new_state): - new_state = _as_circular_gaussian(new_state, "filter_state") - AbstractFilter.filter_state.fset(self, new_state) - '''.replace(" ", "") - if source.count(old) != 1: - raise RuntimeError("expected exactly one CircularUKF state setter") - source_path.write_text(source.replace(old, new)) - - test_path = Path("tests/filters/test_circular_ukf_state_ownership.py") - test_path.write_text( - '''import unittest - -import numpy.testing as npt - -import pyrecest.backend -from pyrecest.backend import array -from pyrecest.distributions import GaussianDistribution -from pyrecest.filters.circular_ukf import CircularUKF - - -class CircularUKFStateOwnershipTest(unittest.TestCase): - @unittest.skipUnless( - pyrecest.backend.__backend_name__ == "numpy", - "mutable NumPy arrays required for aliasing regression", - ) - def test_assignment_copies_state(self): - filt = CircularUKF() - assigned = GaussianDistribution(array([0.5]), array([[0.7]])) - filt.filter_state = assigned - - self.assertIsNot(filt.filter_state, assigned) - assigned.mu[0] = 1.5 - assigned.C[0, 0] = 2.0 - - npt.assert_equal(filt.filter_state.mu, array([0.5])) - npt.assert_equal(filt.filter_state.C, array([[0.7]])) - - -if __name__ == "__main__": - unittest.main() -''' - ) - - Path( - ".github/workflows/one-shot-circular-ukf-state-copy.yml" - ).unlink() - PY - - - name: Validate changed Python syntax - run: | - python -m compileall -q \ - src/pyrecest/filters/circular_ukf.py \ - tests/filters/test_circular_ukf_state_ownership.py - git diff --check - - - name: Commit focused patch - run: | - git config user.name "github-actions[bot]" - git config user.email \ - "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git diff --cached --check - git commit -m "Prevent CircularUKF state aliasing" - git push