from __future__ import annotations import csv import subprocess import sys from pathlib import Path from dovla_cil.data.schema import ( CIL_VERSION, ActionChunk, CILRecord, FailureInfo, RewardInfo, StructuredEffect, make_record_id, ) from dovla_cil.data.sharding import write_cil_shards from dovla_cil.experiments.reports import ( compute_candidate_stats, compute_failure_stats, generate_dataset_report, load_dataset_summary, ) from dovla_cil.utils.io import read_json def test_dataset_report_script_runs_and_creates_files(tmp_path: Path) -> None: dataset_dir = tmp_path / "dataset" out_dir = tmp_path / "report" _write_tiny_dataset(dataset_dir) result = subprocess.run( [ sys.executable, "scripts/report_dataset.py", "--dataset", str(dataset_dir), "--out", str(out_dir), "--sample-groups", "2", ], check=True, text=True, capture_output=True, ) expected = { "summary.json", "candidate_type_counts.csv", "reward_histogram.png", "success_by_candidate_type.csv", "success_by_candidate_type.png", "regret_histogram.png", "group_size_distribution.png", "failure_type_counts.csv", "failure_type_counts.png", "examples.md", } assert "num records: 3" in result.stdout assert expected.issubset({path.name for path in out_dir.iterdir()}) def test_report_stats_match_expected_toy_dataset(tmp_path: Path) -> None: dataset_dir = tmp_path / "dataset" out_dir = tmp_path / "report" records = _write_tiny_dataset(dataset_dir) summary = generate_dataset_report(dataset_dir, out_dir, sample_groups=2, seed=4) candidate_stats = { row["candidate_type"]: row for row in compute_candidate_stats(records) } failure_stats = {row["failure_type"]: row for row in compute_failure_stats(records)} summary_file = read_json(out_dir / "summary.json") candidate_counts = _read_csv(out_dir / "candidate_type_counts.csv") assert summary["num_records"] == 3 assert summary["num_groups"] == 2 assert summary["candidate_type_counts"] == {"expert": 2, "wrong_target": 1} assert summary["failure_type_counts"] == {"success": 2, "wrong_target": 1} assert summary_file["success_rate"] == 2 / 3 assert candidate_stats["expert"]["success_rate"] == 1.0 assert candidate_stats["wrong_target"]["success_rate"] == 0.0 assert failure_stats["wrong_target"]["count"] == 1 assert candidate_counts == [ {"candidate_type": "expert", "count": "2"}, {"candidate_type": "wrong_target", "count": "1"}, ] assert "| record_id | candidate_type | reward.progress |" in ( out_dir / "examples.md" ).read_text(encoding="utf-8") def test_load_dataset_summary_function(tmp_path: Path) -> None: dataset_dir = tmp_path / "dataset" _write_tiny_dataset(dataset_dir) summary = load_dataset_summary(dataset_dir) assert summary["num_records"] == 3 assert summary["reward"]["min"] == 0.2 assert summary["reward"]["max"] == 1.0 assert summary["group_size"]["max"] == 2 def _write_tiny_dataset(dataset_dir: Path) -> list[CILRecord]: records = [ _record("group-a", 0, "expert", 1.0, True, "success", regret=0.0, rank=0), _record("group-a", 1, "wrong_target", 0.2, False, "wrong_target", regret=0.8, rank=1), _record("group-b", 0, "expert", 1.0, True, "success", regret=0.0, rank=0), ] write_cil_shards( records, output_dir=dataset_dir, max_records_per_shard=10, dataset_name="report_toy", backend="toy", k=2, task_count=1, seed=5, ) return records def _record( group_id: str, index: int, candidate_type: str, reward_progress: float, success: bool, failure_type: str, *, regret: float, rank: int, ) -> CILRecord: action = ActionChunk( action_id=f"{candidate_type}-{index}", representation="semantic", horizon=1, values=[{"command": "noop" if not success else "grasp", "object": "red_mug"}], skill_type="grasp", metadata={"candidate_type": candidate_type, "intended_target": "red_mug"}, ) return CILRecord( version=CIL_VERSION, record_id=make_record_id(group_id, action.action_id, seed=9), group_id=group_id, state_hash=f"state-{group_id}", task_id="toy_report_task", scene_id=f"scene-{group_id}", instruction="Pick the red mug.", instruction_family={"family": "pick"}, observation_ref=None, observation_inline={"symbolic_state": {"objects": {}}}, action_chunk=action, next_observation_ref=None, next_observation_inline={"symbolic_state": {"objects": {}}}, structured_effect=StructuredEffect( object_pose_delta={"red_mug": [reward_progress, 0.0, 0.0]}, relation_after={"grasped(red_mug)": success}, moved_objects=["red_mug"] if reward_progress > 0 else [], ), reward=RewardInfo( progress=reward_progress, success=success, terminal_success=success, dense_components={"progress": reward_progress}, ), regret=regret, rank_within_group=rank, candidate_type=candidate_type, failure=FailureInfo( type=failure_type, symbolic_reason=failure_type, language_explanation=failure_type, ), ) def _read_csv(path: Path) -> list[dict[str, str]]: with path.open("r", encoding="utf-8", newline="") as handle: return list(csv.DictReader(handle))