vla / workspace /tests /test_metrics.py
anhtld's picture
auto-sync 2026-07-04T04:28:21Z workspace (part 8)
52f42ba verified
Raw
History Blame Contribute Delete
12.7 kB
from __future__ import annotations
import json
import math
from pathlib import Path
import pytest
from cil.metrics import (
MetricInputError,
any_unsafe,
candidate_diversity,
collapse_rate,
mean_nearest_distance_to_set,
normalized_causal_action_regret,
outcome_safety_violation,
outcome_ptr_at_k,
proxy_positive_tangent_coverage_at_k,
proxy_support_distance,
selector_regret_at_k,
selected_unsafe,
safety_label_coverage,
unsafe_rate,
)
from scripts import eval_metrics
def test_outcome_ptr_refuses_proxy_inputs() -> None:
with pytest.raises(MetricInputError, match="OutcomePTR"):
outcome_ptr_at_k([0.5, 0.7], 0.6, candidates_evaluated=False)
assert outcome_ptr_at_k([0.5, 0.7], 0.6, candidates_evaluated=True) == 1.0
def test_selector_regret_refuses_unevaluated_candidates() -> None:
with pytest.raises(MetricInputError, match="SelectorRegret"):
selector_regret_at_k([0.5, 0.7], candidates_evaluated=False)
def test_normalized_causal_action_regret() -> None:
assert normalized_causal_action_regret(2.0, 1.25, 1.0) == pytest.approx(0.75)
assert normalized_causal_action_regret(2.0, 0.5, 1.0) > 1.0
def test_proxy_positive_tangent_coverage_is_not_outcome_ptr() -> None:
generated = [[0.0, 0.1], [2.0, 2.0]]
positives = [[0.0, 0.0]]
assert proxy_positive_tangent_coverage_at_k(
generated,
positives,
threshold=0.08,
k=2,
) == 1.0
assert math.isclose(proxy_support_distance(generated, positives, k=2), math.sqrt(0.005))
assert math.isclose(
mean_nearest_distance_to_set(generated, positives, k=2),
(math.sqrt(0.005) + 2.0) / 2.0,
)
assert candidate_diversity(generated, k=2) > 0.0
assert collapse_rate([[1.0, 1.0], [1.0, 1.0], [2.0, 2.0]], k=3) == pytest.approx(
2.0 / 3.0
)
def test_safety_metrics_preserve_unknown_labels() -> None:
outcomes = [
{"safety_violation": False},
{"safety_violation": "yes"},
{"safety_violation": None},
]
assert outcome_safety_violation(outcomes[0]) is False
assert outcome_safety_violation(outcomes[1]) is True
assert outcome_safety_violation(outcomes[2]) is None
assert safety_label_coverage(outcomes, k=3) == pytest.approx(2.0 / 3.0)
assert unsafe_rate(outcomes, k=3) == pytest.approx(0.5)
assert any_unsafe(outcomes, k=3) == 1.0
assert selected_unsafe(outcomes, selected_index=1, k=3) == 1.0
assert unsafe_rate([{"safety_violation": None}], k=1) is None
def test_eval_metrics_measured_mode_requires_evaluated_candidates(tmp_path: Path) -> None:
input_path = tmp_path / "rows.json"
input_path.write_text(
json.dumps(
[
{
"task_id": "pick",
"seed": 0,
"candidates_evaluated": False,
"base_utility": 0.2,
"generated_utilities": [0.3, 0.4],
}
]
)
)
with pytest.raises(MetricInputError, match="candidates_evaluated=true"):
eval_metrics.main(["--input", str(input_path), "--out-dir", str(tmp_path / "out"), "--mode", "measured"])
def test_eval_metrics_measured_exports_success_decomposition(tmp_path: Path) -> None:
input_path = tmp_path / "rows.json"
input_path.write_text(
json.dumps(
{
"rows": [
{
"chart_id": "c0",
"task_id": "pick",
"seed": 0,
"candidates_evaluated": True,
"base_utility": 0.3,
"base_success": False,
"base_outcome": {"success": False, "safety_violation": False},
"generated_utilities": [0.2, 1.1, 0.5],
"candidate_success": [False, True, False],
"candidate_outcomes": [
{"success": False, "safety_violation": True},
{"success": True, "safety_violation": False},
{"success": False, "safety_violation": None},
],
"selected_index": 0,
"hidden_chart_utilities": [0.4, 1.5],
"predicted_scores": [0.9, 0.8, 0.1],
}
]
}
)
)
out_dir = tmp_path / "measured_metrics"
assert (
eval_metrics.main(
[
"--input",
str(input_path),
"--out-dir",
str(out_dir),
"--mode",
"measured",
"--k",
"3",
"--bootstrap-samples",
"20",
]
)
== 0
)
metrics = json.loads((out_dir / "metrics.json").read_text())
row = metrics["rows"][0]
assert row["selected_success_at_3"] == 0.0
assert row["proposal_oracle_success_at_3"] == 1.0
assert row["hidden_chart_oracle_success_at_3"] == 1.0
assert row["success_support_gap_at_3"] == 0.0
assert row["success_selector_gap_at_3"] == 1.0
assert row["ncar_to_proposal_oracle_at_3"] == pytest.approx(1.125)
assert row["ncar_to_hidden_chart_oracle_at_3"] == pytest.approx(1.0833333333)
assert row["support_gap_fraction_to_hidden_at_3"] == pytest.approx(0.3333333333)
assert row["selector_gap_fraction_to_hidden_at_3"] == pytest.approx(0.75)
assert row["proposal_oracle_utility_gain_over_base_at_3"] == pytest.approx(0.8)
assert row["base_safety_label_known"] == 1.0
assert row["base_unsafe_known"] == 0.0
assert row["generated_safety_label_coverage_at_3"] == pytest.approx(2.0 / 3.0)
assert row["generated_unsafe_rate_known_at_3"] == pytest.approx(0.5)
assert row["any_generated_unsafe_known_at_3"] == 1.0
assert row["selected_safety_label_known_at_3"] == 1.0
assert row["selected_unsafe_known_at_3"] == 1.0
assert row["proposal_oracle_safety_label_known_at_3"] == 1.0
assert row["proposal_oracle_unsafe_known_at_3"] == 0.0
def test_eval_metrics_measured_exports_safety_coverage_without_false_zero(
tmp_path: Path,
) -> None:
input_path = tmp_path / "rows.json"
input_path.write_text(
json.dumps(
{
"rows": [
{
"chart_id": "c0",
"task_id": "pick",
"seed": 0,
"candidates_evaluated": True,
"base_utility": 0.3,
"base_success": False,
"base_outcome": {"success": False, "safety_violation": None},
"generated_utilities": [0.2, 0.4],
"candidate_success": [False, False],
"candidate_outcomes": [
{"success": False, "safety_violation": None},
{"success": False, "safety_violation": None},
],
"selected_index": 0,
}
]
}
)
)
out_dir = tmp_path / "measured_metrics_null_safety"
assert (
eval_metrics.main(
[
"--input",
str(input_path),
"--out-dir",
str(out_dir),
"--mode",
"measured",
"--k",
"2",
"--bootstrap-samples",
"20",
]
)
== 0
)
metrics = json.loads((out_dir / "metrics.json").read_text())
row = metrics["rows"][0]
assert row["base_safety_label_known"] == 0.0
assert row["generated_safety_label_coverage_at_2"] == 0.0
assert row["selected_safety_label_known_at_2"] == 0.0
assert row["proposal_oracle_safety_label_known_at_2"] == 0.0
assert "base_unsafe_known" not in row
assert "generated_unsafe_rate_known_at_2" not in row
assert "any_generated_unsafe_known_at_2" not in row
assert "selected_unsafe_known_at_2" not in row
assert "proposal_oracle_unsafe_known_at_2" not in row
def test_eval_metrics_omits_unstable_ncar_when_oracle_matches_base(tmp_path: Path) -> None:
input_path = tmp_path / "rows.json"
input_path.write_text(
json.dumps(
{
"rows": [
{
"chart_id": "c0",
"task_id": "pick",
"seed": 0,
"candidates_evaluated": True,
"base_utility": 0.5,
"generated_utilities": [0.4, 0.5],
"selected_index": 0,
}
]
}
)
)
out_dir = tmp_path / "measured_metrics_unstable_ncar"
assert (
eval_metrics.main(
[
"--input",
str(input_path),
"--out-dir",
str(out_dir),
"--mode",
"measured",
"--k",
"2",
"--bootstrap-samples",
"20",
]
)
== 0
)
row = json.loads((out_dir / "metrics.json").read_text())["rows"][0]
assert "ncar_to_proposal_oracle_at_2" not in row
def test_eval_metrics_proxy_exports_json_and_latex(tmp_path: Path) -> None:
input_path = tmp_path / "rows.json"
input_path.write_text(
json.dumps(
{
"rows": [
{
"chart_id": "c0",
"task_id": "pick",
"seed": 0,
"generated_tangents": [[0.0, 0.1], [1.0, 1.0]],
"positive_tangents": [[0.0, 0.0]],
"negative_tangents": [[2.0, 2.0]],
}
]
}
)
)
out_dir = tmp_path / "proxy_metrics"
assert (
eval_metrics.main(
[
"--input",
str(input_path),
"--out-dir",
str(out_dir),
"--mode",
"proxy",
"--k",
"2",
"--thresholds",
"0.20,0.40",
"--bootstrap-samples",
"20",
]
)
== 0
)
metrics = json.loads((out_dir / "metrics.json").read_text())
assert metrics["rows"][0]["pptc_at_2_thr_0p20"] == 1.0
assert "proxy_support_distance_at_2" in metrics["rows"][0]
assert "mean_positive_distance_at_2" in metrics["rows"][0]
assert "mean_negative_distance_at_2" in metrics["rows"][0]
assert (out_dir / "table.tex").exists()
assert (out_dir / "report.md").exists()
for filename in (
"config.yaml",
"command.txt",
"git_hash.txt",
"data_hash.txt",
"split_hash.txt",
"train.log",
"eval.log",
"metrics_by_task.json",
"metrics_by_seed.json",
):
assert (out_dir / filename).exists()
def test_eval_metrics_no_markdown_report_removes_stale_report(tmp_path: Path) -> None:
input_path = tmp_path / "rows.json"
input_path.write_text(
json.dumps(
{
"split_hash": "split-demo",
"rows": [
{
"chart_id": "c0",
"task_id": "pick",
"seed": 0,
"generated_tangents": [[0.0, 0.1]],
"positive_tangents": [[0.0, 0.0]],
}
],
}
)
)
out_dir = tmp_path / "proxy_metrics"
(out_dir).mkdir()
(out_dir / "report.md").write_text("stale\n")
assert (
eval_metrics.main(
[
"--input",
str(input_path),
"--out-dir",
str(out_dir),
"--mode",
"proxy",
"--k",
"1",
"--bootstrap-samples",
"20",
"--no-markdown-report",
]
)
== 0
)
assert not (out_dir / "report.md").exists()
assert (out_dir / "config.yaml").exists()
assert (out_dir / "command.txt").exists()
assert (out_dir / "git_hash.txt").exists()
assert (out_dir / "data_hash.txt").exists()
assert (out_dir / "split_hash.txt").read_text().strip() == "split-demo"