From 88db9f5e56fb89cbb3a42362b297bf5a6ce70632 Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:38:45 +0200 Subject: [PATCH 1/5] Add one-shot tracklet cost validation patch --- .../one-shot-tracklet-cost-validation.yml | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 .github/workflows/one-shot-tracklet-cost-validation.yml diff --git a/.github/workflows/one-shot-tracklet-cost-validation.yml b/.github/workflows/one-shot-tracklet-cost-validation.yml new file mode 100644 index 0000000000..f18593fb59 --- /dev/null +++ b/.github/workflows/one-shot-tracklet-cost-validation.yml @@ -0,0 +1,247 @@ +name: One-shot tracklet cost validation patch + +on: + push: + branches: + - agent/reject-nonfinite-tracklet-costs + +permissions: + contents: write + +jobs: + patch: + runs-on: ubuntu-latest + steps: + - name: Check out patch branch + uses: actions/checkout@v4 + with: + ref: agent/reject-nonfinite-tracklet-costs + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Apply validation fix and regression tests + run: | + python - <<'PY' + from pathlib import Path + + def replace_once(text: str, old: str, new: str, label: str) -> str: + count = text.count(old) + if count != 1: + raise RuntimeError(f"expected exactly one {label} anchor, found {count}") + return text.replace(old, new, 1) + + source_path = Path("src/pyrecest/filters/tracklet_viterbi.py") + source = source_path.read_text(encoding="utf-8") + + source = replace_once( + source, + ''' metadata: Mapping[str, Any] = field(default_factory=dict) + + + @dataclass(frozen=True) + class TrackletViterbiConfig: + ''', + ''' metadata: Mapping[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + object.__setattr__( + self, + "unary_cost", + _as_scalar_float(self.unary_cost, "unary_cost"), + ) + + + @dataclass(frozen=True) + class TrackletViterbiConfig: + ''', + "candidate validation", + ) + + source = replace_once( + source, + ''' transition_value = float( + transition( + previous_node.candidate, + current_node.candidate, + previous_miss_streak, + ) + ) + ''', + ''' transition_value = _as_scalar_float( + transition( + previous_node.candidate, + current_node.candidate, + previous_miss_streak, + ), + "transition_cost", + ) + ''', + "dynamic-program transition validation", + ) + + source = replace_once( + source, + ''' unary_cost = 0.0 if selected is None else float(selected.unary_cost) + transition = transition_cost or ( + lambda previous, current, miss_streak: default_tracklet_transition_cost( + previous, + current, + miss_streak, + config, + ) + ) + if previous_committed is None: + ''', + ''' unary_cost = 0.0 if selected is None else float(selected.unary_cost) + transition = transition_cost or ( + lambda previous, current, miss_streak: default_tracklet_transition_cost( + previous, + current, + miss_streak, + config, + ) + ) + + def evaluated_transition(previous, current, miss_streak): + return _as_scalar_float( + transition(previous, current, miss_streak), + "transition_cost", + ) + + if previous_committed is None: + ''', + "fixed-lag transition helper", + ) + + source = replace_once( + source, + ''' return float(unary_cost + transition(None, selected, committed_miss_streak)) + ''', + ''' return float( + unary_cost + + evaluated_transition(None, selected, committed_miss_streak) + ) + ''', + "leading-gap transition call", + ) + + source = replace_once( + source, + ''' unary_cost + transition(previous_committed, selected, committed_miss_streak) + ''', + ''' unary_cost + + evaluated_transition( + previous_committed, + selected, + committed_miss_streak, + ) + ''', + "committed transition call", + ) + + source_path.write_text(source, encoding="utf-8") + + tests_path = Path("tests/filters/test_tracklet_viterbi.py") + tests = tests_path.read_text(encoding="utf-8") + marker = "def test_tracklet_candidate_rejects_nonfinite_unary_costs" + if marker in tests: + raise RuntimeError("regression tests already present") + tests += ''' + + + @pytest.mark.parametrize("invalid_cost", [np.nan, np.inf, -np.inf]) + def test_tracklet_candidate_rejects_nonfinite_unary_costs(invalid_cost): + with pytest.raises(ValueError, match="unary_cost must be finite"): + TrackletAssociationCandidate("invalid", unary_cost=invalid_cost) + + + @pytest.mark.parametrize("invalid_cost", [np.nan, np.inf, -np.inf]) + def test_tracklet_viterbi_solvers_reject_nonfinite_transition_costs(invalid_cost): + frames = [ + [TrackletAssociationCandidate("a", unary_cost=0.0)], + [TrackletAssociationCandidate("b", unary_cost=0.0)], + ] + + def transition(_previous, _current, _miss_streak): + return invalid_cost + + with pytest.raises(ValueError, match="transition_cost must be finite"): + solve_tracklet_viterbi(frames, transition_cost=transition) + + with pytest.raises(ValueError, match="transition_cost must be finite"): + solve_fixed_lag_tracklet_viterbi( + frames, + lag_s=0.1, + transition_cost=transition, + ) + ''' + tests_path.write_text(tests, encoding="utf-8") + PY + + - name: Run focused standalone validation + run: | + python -m pip install --upgrade pip + python -m pip install "numpy>=2.0,<2.5" + python -m compileall -q \ + src/pyrecest/filters/tracklet_viterbi.py \ + tests/filters/test_tracklet_viterbi.py + python - <<'PY' + import importlib.util + import math + import sys + from pathlib import Path + + module_path = Path("src/pyrecest/filters/tracklet_viterbi.py") + spec = importlib.util.spec_from_file_location("tracklet_viterbi_validation", module_path) + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + + for invalid in (math.nan, math.inf, -math.inf): + try: + module.TrackletAssociationCandidate("invalid", unary_cost=invalid) + except ValueError as exc: + assert "unary_cost must be finite" in str(exc) + else: + raise AssertionError("non-finite unary cost was accepted") + + frames = [ + [module.TrackletAssociationCandidate("a")], + [module.TrackletAssociationCandidate("b")], + ] + + def transition(_previous, _current, _miss_streak, value=invalid): + return value + + for solve in ( + lambda: module.solve_tracklet_viterbi( + frames, + transition_cost=transition, + ), + lambda: module.solve_fixed_lag_tracklet_viterbi( + frames, + lag_s=0.1, + transition_cost=transition, + ), + ): + try: + solve() + except ValueError as exc: + assert "transition_cost must be finite" in str(exc) + else: + raise AssertionError("non-finite transition cost was accepted") + PY + + - name: Commit the fix and remove this workflow + run: | + rm .github/workflows/one-shot-tracklet-cost-validation.yml + 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 "Reject non-finite tracklet Viterbi costs" + git push origin HEAD:agent/reject-nonfinite-tracklet-costs From 68976c622f9ecd7e0f01b0c2e6ac13b510d2956d Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:41:22 +0200 Subject: [PATCH 2/5] Trigger one-shot patch from pull request --- .github/workflows/one-shot-tracklet-cost-validation.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/one-shot-tracklet-cost-validation.yml b/.github/workflows/one-shot-tracklet-cost-validation.yml index f18593fb59..81e78f2c0f 100644 --- a/.github/workflows/one-shot-tracklet-cost-validation.yml +++ b/.github/workflows/one-shot-tracklet-cost-validation.yml @@ -4,6 +4,10 @@ on: push: branches: - agent/reject-nonfinite-tracklet-costs + pull_request: + branches: + - main + types: [opened, synchronize, reopened] permissions: contents: write From 05f0a113738e989ef0d8cfcdb342755ed9272022 Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:44:05 +0200 Subject: [PATCH 3/5] Make one-shot tracklet patch robust --- .../one-shot-tracklet-cost-validation.yml | 140 +++++------------- 1 file changed, 41 insertions(+), 99 deletions(-) diff --git a/.github/workflows/one-shot-tracklet-cost-validation.yml b/.github/workflows/one-shot-tracklet-cost-validation.yml index 81e78f2c0f..5f50e830d4 100644 --- a/.github/workflows/one-shot-tracklet-cost-validation.yml +++ b/.github/workflows/one-shot-tracklet-cost-validation.yml @@ -1,9 +1,6 @@ name: One-shot tracklet cost validation patch on: - push: - branches: - - agent/reject-nonfinite-tracklet-costs pull_request: branches: - main @@ -41,112 +38,57 @@ jobs: source_path = Path("src/pyrecest/filters/tracklet_viterbi.py") source = source_path.read_text(encoding="utf-8") - source = replace_once( - source, - ''' metadata: Mapping[str, Any] = field(default_factory=dict) - - - @dataclass(frozen=True) - class TrackletViterbiConfig: - ''', - ''' metadata: Mapping[str, Any] = field(default_factory=dict) - - def __post_init__(self) -> None: - object.__setattr__( - self, - "unary_cost", - _as_scalar_float(self.unary_cost, "unary_cost"), - ) - - - @dataclass(frozen=True) - class TrackletViterbiConfig: - ''', - "candidate validation", + candidate_anchor = ( + " metadata: Mapping[str, Any] = field(default_factory=dict)\n" + "\n\n" + "@dataclass(frozen=True)\n" + "class TrackletViterbiConfig:\n" ) - - source = replace_once( - source, - ''' transition_value = float( - transition( - previous_node.candidate, - current_node.candidate, - previous_miss_streak, - ) - ) - ''', - ''' transition_value = _as_scalar_float( - transition( - previous_node.candidate, - current_node.candidate, - previous_miss_streak, - ), - "transition_cost", - ) - ''', - "dynamic-program transition validation", + candidate_replacement = ( + " metadata: Mapping[str, Any] = field(default_factory=dict)\n" + "\n" + " def __post_init__(self) -> None:\n" + " object.__setattr__(\n" + " self,\n" + " \"unary_cost\",\n" + " _as_scalar_float(self.unary_cost, \"unary_cost\"),\n" + " )\n" + "\n\n" + "@dataclass(frozen=True)\n" + "class TrackletViterbiConfig:\n" ) - source = replace_once( source, - ''' unary_cost = 0.0 if selected is None else float(selected.unary_cost) - transition = transition_cost or ( - lambda previous, current, miss_streak: default_tracklet_transition_cost( - previous, - current, - miss_streak, - config, - ) - ) - if previous_committed is None: - ''', - ''' unary_cost = 0.0 if selected is None else float(selected.unary_cost) - transition = transition_cost or ( - lambda previous, current, miss_streak: default_tracklet_transition_cost( - previous, - current, - miss_streak, - config, - ) - ) - - def evaluated_transition(previous, current, miss_streak): - return _as_scalar_float( - transition(previous, current, miss_streak), - "transition_cost", - ) - - if previous_committed is None: - ''', - "fixed-lag transition helper", + candidate_anchor, + candidate_replacement, + "candidate validation", ) - source = replace_once( - source, - ''' return float(unary_cost + transition(None, selected, committed_miss_streak)) - ''', - ''' return float( - unary_cost - + evaluated_transition(None, selected, committed_miss_streak) - ) - ''', - "leading-gap transition call", + transition_anchor = ( + " transition_value = float(\n" + " transition(\n" + " previous_node.candidate,\n" + " current_node.candidate,\n" + " previous_miss_streak,\n" + " )\n" + " )\n" + ) + transition_replacement = ( + " transition_value = _as_scalar_float(\n" + " transition(\n" + " previous_node.candidate,\n" + " current_node.candidate,\n" + " previous_miss_streak,\n" + " ),\n" + " \"transition_cost\",\n" + " )\n" ) - source = replace_once( source, - ''' unary_cost + transition(previous_committed, selected, committed_miss_streak) - ''', - ''' unary_cost - + evaluated_transition( - previous_committed, - selected, - committed_miss_streak, - ) - ''', - "committed transition call", + transition_anchor, + transition_replacement, + "transition validation", ) - source_path.write_text(source, encoding="utf-8") tests_path = Path("tests/filters/test_tracklet_viterbi.py") From f8883a817a54aa7bcf9bf85203e73457b2594d21 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:44:27 +0000 Subject: [PATCH 4/5] Reject non-finite tracklet Viterbi costs --- .../one-shot-tracklet-cost-validation.yml | 193 ------------------ src/pyrecest/filters/tracklet_viterbi.py | 12 +- tests/filters/test_tracklet_viterbi.py | 28 +++ 3 files changed, 38 insertions(+), 195 deletions(-) delete mode 100644 .github/workflows/one-shot-tracklet-cost-validation.yml diff --git a/.github/workflows/one-shot-tracklet-cost-validation.yml b/.github/workflows/one-shot-tracklet-cost-validation.yml deleted file mode 100644 index 5f50e830d4..0000000000 --- a/.github/workflows/one-shot-tracklet-cost-validation.yml +++ /dev/null @@ -1,193 +0,0 @@ -name: One-shot tracklet cost validation patch - -on: - pull_request: - branches: - - main - types: [opened, synchronize, reopened] - -permissions: - contents: write - -jobs: - patch: - runs-on: ubuntu-latest - steps: - - name: Check out patch branch - uses: actions/checkout@v4 - with: - ref: agent/reject-nonfinite-tracklet-costs - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Apply validation fix and regression tests - run: | - python - <<'PY' - from pathlib import Path - - def replace_once(text: str, old: str, new: str, label: str) -> str: - count = text.count(old) - if count != 1: - raise RuntimeError(f"expected exactly one {label} anchor, found {count}") - return text.replace(old, new, 1) - - source_path = Path("src/pyrecest/filters/tracklet_viterbi.py") - source = source_path.read_text(encoding="utf-8") - - candidate_anchor = ( - " metadata: Mapping[str, Any] = field(default_factory=dict)\n" - "\n\n" - "@dataclass(frozen=True)\n" - "class TrackletViterbiConfig:\n" - ) - candidate_replacement = ( - " metadata: Mapping[str, Any] = field(default_factory=dict)\n" - "\n" - " def __post_init__(self) -> None:\n" - " object.__setattr__(\n" - " self,\n" - " \"unary_cost\",\n" - " _as_scalar_float(self.unary_cost, \"unary_cost\"),\n" - " )\n" - "\n\n" - "@dataclass(frozen=True)\n" - "class TrackletViterbiConfig:\n" - ) - source = replace_once( - source, - candidate_anchor, - candidate_replacement, - "candidate validation", - ) - - transition_anchor = ( - " transition_value = float(\n" - " transition(\n" - " previous_node.candidate,\n" - " current_node.candidate,\n" - " previous_miss_streak,\n" - " )\n" - " )\n" - ) - transition_replacement = ( - " transition_value = _as_scalar_float(\n" - " transition(\n" - " previous_node.candidate,\n" - " current_node.candidate,\n" - " previous_miss_streak,\n" - " ),\n" - " \"transition_cost\",\n" - " )\n" - ) - source = replace_once( - source, - transition_anchor, - transition_replacement, - "transition validation", - ) - source_path.write_text(source, encoding="utf-8") - - tests_path = Path("tests/filters/test_tracklet_viterbi.py") - tests = tests_path.read_text(encoding="utf-8") - marker = "def test_tracklet_candidate_rejects_nonfinite_unary_costs" - if marker in tests: - raise RuntimeError("regression tests already present") - tests += ''' - - - @pytest.mark.parametrize("invalid_cost", [np.nan, np.inf, -np.inf]) - def test_tracklet_candidate_rejects_nonfinite_unary_costs(invalid_cost): - with pytest.raises(ValueError, match="unary_cost must be finite"): - TrackletAssociationCandidate("invalid", unary_cost=invalid_cost) - - - @pytest.mark.parametrize("invalid_cost", [np.nan, np.inf, -np.inf]) - def test_tracklet_viterbi_solvers_reject_nonfinite_transition_costs(invalid_cost): - frames = [ - [TrackletAssociationCandidate("a", unary_cost=0.0)], - [TrackletAssociationCandidate("b", unary_cost=0.0)], - ] - - def transition(_previous, _current, _miss_streak): - return invalid_cost - - with pytest.raises(ValueError, match="transition_cost must be finite"): - solve_tracklet_viterbi(frames, transition_cost=transition) - - with pytest.raises(ValueError, match="transition_cost must be finite"): - solve_fixed_lag_tracklet_viterbi( - frames, - lag_s=0.1, - transition_cost=transition, - ) - ''' - tests_path.write_text(tests, encoding="utf-8") - PY - - - name: Run focused standalone validation - run: | - python -m pip install --upgrade pip - python -m pip install "numpy>=2.0,<2.5" - python -m compileall -q \ - src/pyrecest/filters/tracklet_viterbi.py \ - tests/filters/test_tracklet_viterbi.py - python - <<'PY' - import importlib.util - import math - import sys - from pathlib import Path - - module_path = Path("src/pyrecest/filters/tracklet_viterbi.py") - spec = importlib.util.spec_from_file_location("tracklet_viterbi_validation", module_path) - module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module - spec.loader.exec_module(module) - - for invalid in (math.nan, math.inf, -math.inf): - try: - module.TrackletAssociationCandidate("invalid", unary_cost=invalid) - except ValueError as exc: - assert "unary_cost must be finite" in str(exc) - else: - raise AssertionError("non-finite unary cost was accepted") - - frames = [ - [module.TrackletAssociationCandidate("a")], - [module.TrackletAssociationCandidate("b")], - ] - - def transition(_previous, _current, _miss_streak, value=invalid): - return value - - for solve in ( - lambda: module.solve_tracklet_viterbi( - frames, - transition_cost=transition, - ), - lambda: module.solve_fixed_lag_tracklet_viterbi( - frames, - lag_s=0.1, - transition_cost=transition, - ), - ): - try: - solve() - except ValueError as exc: - assert "transition_cost must be finite" in str(exc) - else: - raise AssertionError("non-finite transition cost was accepted") - PY - - - name: Commit the fix and remove this workflow - run: | - rm .github/workflows/one-shot-tracklet-cost-validation.yml - 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 "Reject non-finite tracklet Viterbi costs" - git push origin HEAD:agent/reject-nonfinite-tracklet-costs diff --git a/src/pyrecest/filters/tracklet_viterbi.py b/src/pyrecest/filters/tracklet_viterbi.py index 9c2318cfa7..6d0e0a9376 100644 --- a/src/pyrecest/filters/tracklet_viterbi.py +++ b/src/pyrecest/filters/tracklet_viterbi.py @@ -71,6 +71,13 @@ class TrackletAssociationCandidate: velocity: Any | None = None metadata: Mapping[str, Any] = field(default_factory=dict) + def __post_init__(self) -> None: + object.__setattr__( + self, + "unary_cost", + _as_scalar_float(self.unary_cost, "unary_cost"), + ) + @dataclass(frozen=True) class TrackletViterbiConfig: @@ -283,12 +290,13 @@ def _solve_tracklet_viterbi( for previous_miss_streak, previous_cost in state_costs[-1][ previous_index ].items(): - transition_value = float( + transition_value = _as_scalar_float( transition( previous_node.candidate, current_node.candidate, previous_miss_streak, - ) + ), + "transition_cost", ) current_miss_streak = ( previous_miss_streak + 1 if current_node.is_miss else 0 diff --git a/tests/filters/test_tracklet_viterbi.py b/tests/filters/test_tracklet_viterbi.py index 69f6f92d74..67759e6f40 100644 --- a/tests/filters/test_tracklet_viterbi.py +++ b/tests/filters/test_tracklet_viterbi.py @@ -268,3 +268,31 @@ def transition(previous, current, miss_streak): (1,), (2,), ] + + + +@pytest.mark.parametrize("invalid_cost", [np.nan, np.inf, -np.inf]) +def test_tracklet_candidate_rejects_nonfinite_unary_costs(invalid_cost): + with pytest.raises(ValueError, match="unary_cost must be finite"): + TrackletAssociationCandidate("invalid", unary_cost=invalid_cost) + + +@pytest.mark.parametrize("invalid_cost", [np.nan, np.inf, -np.inf]) +def test_tracklet_viterbi_solvers_reject_nonfinite_transition_costs(invalid_cost): + frames = [ + [TrackletAssociationCandidate("a", unary_cost=0.0)], + [TrackletAssociationCandidate("b", unary_cost=0.0)], + ] + + def transition(_previous, _current, _miss_streak): + return invalid_cost + + with pytest.raises(ValueError, match="transition_cost must be finite"): + solve_tracklet_viterbi(frames, transition_cost=transition) + + with pytest.raises(ValueError, match="transition_cost must be finite"): + solve_fixed_lag_tracklet_viterbi( + frames, + lag_s=0.1, + transition_cost=transition, + ) From d8563451c891c39aefb82cfe0ea36d0f3671f3c2 Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:46:48 +0200 Subject: [PATCH 5/5] Tidy tracklet cost regression spacing --- tests/filters/test_tracklet_viterbi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/filters/test_tracklet_viterbi.py b/tests/filters/test_tracklet_viterbi.py index 67759e6f40..028edc4aa3 100644 --- a/tests/filters/test_tracklet_viterbi.py +++ b/tests/filters/test_tracklet_viterbi.py @@ -270,7 +270,6 @@ def transition(previous, current, miss_streak): ] - @pytest.mark.parametrize("invalid_cost", [np.nan, np.inf, -np.inf]) def test_tracklet_candidate_rejects_nonfinite_unary_costs(invalid_cost): with pytest.raises(ValueError, match="unary_cost must be finite"):