Source: tests/test_v2_selectors.py · Plain text

tests/test_v2_selectors.py

"""Nested protocol, fold isolation, reward baseline and conservative gate tests."""

import copy
import inspect

import numpy as np
import pytest

from mcss.vision_probe.v2_selectors import (
    TRAJECTORIES,
    _choice,
    _fit,
    _winner,
    fit_v2,
    predict_v2,
    select_v2,
)


def rows(zero=False):
    result = []
    for seq in range(3):
        for slot in range(1 + (seq == 2)):
            for i, t in enumerate(TRAJECTORIES):
                reward = 0.0 if zero else (0.02 if i == 5 else -0.01 if i else 0.0)
                result.append(
                    {
                        "split": "discovery",
                        "sequence": str(seq),
                        "target_slot": slot,
                        "trajectory": list(t),
                        "J": 0.4 + reward,
                        "visible": {
                            "quality": reward,
                            "sequence_signal": float(seq),
                            "f_cycle": reward / 2,
                        },
                    }
                )
    return result


def test_nested_isolation_weighted_scaler_and_final_model():
    data = rows()
    fitted = fit_v2(data, ["quality", "sequence_signal"], {})
    for name, cv in fitted["nested_cv_results"].items():
        assert cv["outer_oof_score_before_fallback"]["net_delta_J"] > 0
        assert cv["final_policy"] == "RIDGE_GATE"
        for outer in cv["outer_folds"]:
            held = set(outer["held_out_sequences"])
            assert held.isdisjoint(outer["training_sequences"])
            for fold in outer["inner_cv"]["folds"]:
                model = fold["model"]
                train = set(model["training_sequences"])
                assert train.isdisjoint(held | set(fold["held_out_sequences"]))
                index = model["feature_names"].index("sequence_signal")
                assert model["mean"][index] == pytest.approx(np.mean([float(s) for s in train]))
                totals = list(model["scaler_provenance"]["sequence_total_weights"].values())
                assert max(totals) == pytest.approx(min(totals))
                assert model["scaler_provenance"]["weight_sum"] == pytest.approx(
                    model["scaler_provenance"]["row_count"]
                )
            for decision in outer["oof_decisions"]:
                assert held.isdisjoint(decision["fitted_sequences"])
        artifact = fitted["selectors"][name]
        group = data[:16]
        assert (
            select_v2([r["visible"] for r in group], [r["trajectory"] for r in group], artifact)
            == 5
        )


def test_zero_reward_forces_always_off_and_no_final_tuning():
    data = rows(zero=True)
    fitted = fit_v2(data, ["quality", "sequence_signal"], {})
    for name, artifact in fitted["selectors"].items():
        assert artifact["policy"] == "ALWAYS_OFF"
        assert artifact["outer_nonpositive_forced_off"] is True
        assert fitted["nested_cv_results"][name]["final_tuning"] is None
        group = data[:16][::-1]
        features = [r["visible"] for r in group]
        assert select_v2(features, [r["trajectory"] for r in group], artifact) == 15
        assert predict_v2(features, artifact) == [0.0] * 16


def test_no_validation_rows_and_no_inference_label_injection():
    data = rows(zero=True)
    bad = copy.deepcopy(data)
    bad[0]["split"] = "validation"
    with pytest.raises(ValueError, match="discovery"):
        fit_v2(bad, ["quality", "sequence_signal"], {})
    artifact = fit_v2(data, ["quality", "sequence_signal"], {})["selectors"]["CycleGate"]
    features = [{**r["visible"], "target_J": r["J"]} for r in data[:16]]
    with pytest.raises(ValueError, match="visible"):
        predict_v2(features, artifact)
    assert set(inspect.signature(select_v2).parameters) == {"features", "trajectories", "artifact"}


def test_off_never_competes_and_strict_threshold_and_ties():
    prediction = [-1.0] * 16
    prediction[0] = 100.0
    prediction[2] = 0.001
    prediction[1] = 0.001
    assert _choice(prediction, TRAJECTORIES, 0.001) == 0
    assert _choice(prediction, TRAJECTORIES, 0.0005) == 1
    prediction[1] = 0.0009999999999
    assert _choice(prediction, TRAJECTORIES, 0.0005) == 1


def test_weighted_reward_fit_pair_baseline_invariance():
    data = rows()
    changed = copy.deepcopy(data)
    for row in changed:
        row["J"] += 7 * float(row["sequence"]) + float(row["target_slot"])
    first = _fit(data, ["quality", "sequence_signal"], 1.0)
    second = _fit(changed, ["quality", "sequence_signal"], 1.0)
    assert first["coefficients"] == pytest.approx(second["coefficients"], abs=1e-12)
    assert first["mean"][1] == pytest.approx(1.0)


def test_tuning_tie_break_low_writes_then_threshold_then_regularization():
    def c(reward, write, threshold, alpha):
        return {
            "score": {"net_delta_J": reward, "write_rate": write},
            "threshold": threshold,
            "alpha": alpha,
        }

    candidates = [
        c(0.001, 0.5, 0.005, 100),
        c(0.001 + 1e-13, 0.1, 0.0, 0.1),
        c(0.001, 0.1, 0.002, 1),
        c(0.001, 0.1, 0.002, 100),
    ]
    assert _winner(candidates) is candidates[-1]


def test_nested_results_are_deterministic_and_json_serializable():
    import json

    data = rows()
    first = fit_v2(data, ["quality", "sequence_signal"], {"seed": 20260926})
    second = fit_v2(data, ["quality", "sequence_signal"], {"seed": 20260926})
    assert json.dumps(first, sort_keys=True, allow_nan=False) == json.dumps(
        second, sort_keys=True, allow_nan=False
    )
    assert first["selectors"]["GateOnly"]["feature_names"] == ["quality", "sequence_signal"]
    assert first["selectors"]["CycleGate"]["feature_names"] == [
        "quality",
        "sequence_signal",
        "f_cycle",
    ]