From 70b6b50c062c251b771d6253efedb466f3eeabc3 Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:03:30 +0800 Subject: [PATCH 1/2] Add one-shot calibrated association feature fix --- ...hot-reject-complex-calibrated-features.yml | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .github/workflows/one-shot-reject-complex-calibrated-features.yml diff --git a/.github/workflows/one-shot-reject-complex-calibrated-features.yml b/.github/workflows/one-shot-reject-complex-calibrated-features.yml new file mode 100644 index 0000000000..dea27fd560 --- /dev/null +++ b/.github/workflows/one-shot-reject-complex-calibrated-features.yml @@ -0,0 +1,100 @@ +name: Apply calibrated association feature validation fix + +on: + push: + branches: + - agent/reject-complex-calibrated-features + +permissions: + contents: write + +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: agent/reject-complex-calibrated-features + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Apply focused source and regression patch + run: | + python - <<'PY' + from pathlib import Path + + source_path = Path('src/pyrecest/utils/association_features.py') + source = source_path.read_text(encoding='utf-8') + old = '''def _flatten_prediction_features(features: Any) -> tuple[Any, tuple[int, ...]]: + features = asarray(features, dtype=float64) + ''' + new = '''def _flatten_prediction_features(features: Any) -> tuple[Any, tuple[int, ...]]: + features = _as_real_numeric_backend_array( + features, + message="features must contain real numeric values", + ) + ''' + if old not in source: + raise SystemExit('Expected _flatten_prediction_features implementation not found') + source_path.write_text(source.replace(old, new, 1), encoding='utf-8') + + test_path = Path('tests/utils/test_calibrated_association_feature_validation.py') + test_path.write_text( + '''"""Regression tests for calibrated association feature validation.""" + + import pytest + + from pyrecest.backend import array + from pyrecest.utils import CalibratedPairwiseAssociationModel + + + class _RecordingPredictProbaModel: + classes_ = array([0, 1]) + + def __init__(self): + self.called = False + + def predict_proba(self, features): + self.called = True + return array([[0.25, 0.75]]) + + + def test_predict_proba_rejects_complex_direct_features_before_model_call(): + model = _RecordingPredictProbaModel() + calibrated_model = CalibratedPairwiseAssociationModel( + model, + feature_names=("distance", "similarity"), + ) + complex_features = array([[1.0 + 2.0j, 0.5]]) + + with pytest.raises(ValueError, match="real numeric"): + calibrated_model.predict_match_probability(complex_features) + + assert not model.called + ''', + encoding='utf-8', + ) + + Path('.github/workflows/one-shot-reject-complex-calibrated-features.yml').unlink() + PY + + - name: Check changed Python syntax + run: | + python -m py_compile \ + src/pyrecest/utils/association_features.py \ + tests/utils/test_calibrated_association_feature_validation.py + + - name: Commit patch + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + git add \ + src/pyrecest/utils/association_features.py \ + tests/utils/test_calibrated_association_feature_validation.py \ + .github/workflows/one-shot-reject-complex-calibrated-features.yml + git commit -m "Reject complex calibrated association features" + git push origin HEAD:agent/reject-complex-calibrated-features From 084f92aeaca8c9e8807a505c302fa0163855e675 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 06:03:42 +0000 Subject: [PATCH 2/2] Reject complex calibrated association features --- ...hot-reject-complex-calibrated-features.yml | 100 ------------------ src/pyrecest/utils/association_features.py | 5 +- ...librated_association_feature_validation.py | 31 ++++++ 3 files changed, 35 insertions(+), 101 deletions(-) delete mode 100644 .github/workflows/one-shot-reject-complex-calibrated-features.yml create mode 100644 tests/utils/test_calibrated_association_feature_validation.py diff --git a/.github/workflows/one-shot-reject-complex-calibrated-features.yml b/.github/workflows/one-shot-reject-complex-calibrated-features.yml deleted file mode 100644 index dea27fd560..0000000000 --- a/.github/workflows/one-shot-reject-complex-calibrated-features.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: Apply calibrated association feature validation fix - -on: - push: - branches: - - agent/reject-complex-calibrated-features - -permissions: - contents: write - -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: agent/reject-complex-calibrated-features - fetch-depth: 0 - - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Apply focused source and regression patch - run: | - python - <<'PY' - from pathlib import Path - - source_path = Path('src/pyrecest/utils/association_features.py') - source = source_path.read_text(encoding='utf-8') - old = '''def _flatten_prediction_features(features: Any) -> tuple[Any, tuple[int, ...]]: - features = asarray(features, dtype=float64) - ''' - new = '''def _flatten_prediction_features(features: Any) -> tuple[Any, tuple[int, ...]]: - features = _as_real_numeric_backend_array( - features, - message="features must contain real numeric values", - ) - ''' - if old not in source: - raise SystemExit('Expected _flatten_prediction_features implementation not found') - source_path.write_text(source.replace(old, new, 1), encoding='utf-8') - - test_path = Path('tests/utils/test_calibrated_association_feature_validation.py') - test_path.write_text( - '''"""Regression tests for calibrated association feature validation.""" - - import pytest - - from pyrecest.backend import array - from pyrecest.utils import CalibratedPairwiseAssociationModel - - - class _RecordingPredictProbaModel: - classes_ = array([0, 1]) - - def __init__(self): - self.called = False - - def predict_proba(self, features): - self.called = True - return array([[0.25, 0.75]]) - - - def test_predict_proba_rejects_complex_direct_features_before_model_call(): - model = _RecordingPredictProbaModel() - calibrated_model = CalibratedPairwiseAssociationModel( - model, - feature_names=("distance", "similarity"), - ) - complex_features = array([[1.0 + 2.0j, 0.5]]) - - with pytest.raises(ValueError, match="real numeric"): - calibrated_model.predict_match_probability(complex_features) - - assert not model.called - ''', - encoding='utf-8', - ) - - Path('.github/workflows/one-shot-reject-complex-calibrated-features.yml').unlink() - PY - - - name: Check changed Python syntax - run: | - python -m py_compile \ - src/pyrecest/utils/association_features.py \ - tests/utils/test_calibrated_association_feature_validation.py - - - name: Commit patch - run: | - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - git add \ - src/pyrecest/utils/association_features.py \ - tests/utils/test_calibrated_association_feature_validation.py \ - .github/workflows/one-shot-reject-complex-calibrated-features.yml - git commit -m "Reject complex calibrated association features" - git push origin HEAD:agent/reject-complex-calibrated-features diff --git a/src/pyrecest/utils/association_features.py b/src/pyrecest/utils/association_features.py index 447592c5dc..ebc30076ce 100644 --- a/src/pyrecest/utils/association_features.py +++ b/src/pyrecest/utils/association_features.py @@ -403,7 +403,10 @@ def _finite_feature_plane(values: Any, feature_name: str) -> Any: def _flatten_prediction_features(features: Any) -> tuple[Any, tuple[int, ...]]: - features = asarray(features, dtype=float64) + features = _as_real_numeric_backend_array( + features, + message="features must contain real numeric values", + ) if features.ndim == 0: raise ValueError("features must be at least one-dimensional") if features.ndim == 1: diff --git a/tests/utils/test_calibrated_association_feature_validation.py b/tests/utils/test_calibrated_association_feature_validation.py new file mode 100644 index 0000000000..ab4e8d89ef --- /dev/null +++ b/tests/utils/test_calibrated_association_feature_validation.py @@ -0,0 +1,31 @@ +"""Regression tests for calibrated association feature validation.""" + +import pytest + +from pyrecest.backend import array +from pyrecest.utils import CalibratedPairwiseAssociationModel + + +class _RecordingPredictProbaModel: + classes_ = array([0, 1]) + + def __init__(self): + self.called = False + + def predict_proba(self, features): + self.called = True + return array([[0.25, 0.75]]) + + +def test_predict_proba_rejects_complex_direct_features_before_model_call(): + model = _RecordingPredictProbaModel() + calibrated_model = CalibratedPairwiseAssociationModel( + model, + feature_names=("distance", "similarity"), + ) + complex_features = array([[1.0 + 2.0j, 0.5]]) + + with pytest.raises(ValueError, match="real numeric"): + calibrated_model.predict_match_probability(complex_features) + + assert not model.called