from __future__ import annotations import subprocess import sys from pathlib import Path from dovla_cil.eval.causalstress import ( CAUSALSTRESS_CATEGORIES, CausalStressConfig, compute_causalstress_metrics, generate_causalstress_groups, ) from dovla_cil.generation.pipeline import generate_cil_dataset from dovla_cil.tasks.library import built_in_toy_tasks from dovla_cil.training.trainer import DoVLATrainer, TrainerConfig from dovla_cil.utils.io import read_json def test_causalstress_generation_works() -> None: groups = generate_causalstress_groups( CausalStressConfig(num_tasks=len(CAUSALSTRESS_CATEGORIES), k=4, seed=1) ) assert len(groups) == len(CAUSALSTRESS_CATEGORIES) assert {group.category for group in groups} == set(CAUSALSTRESS_CATEGORIES) assert all(len(group.records) == 4 for group in groups) assert all(record.group_id == group.group_id for group in groups for record in group.records) def test_each_causalstress_category_generates_one_group() -> None: for category in CAUSALSTRESS_CATEGORIES: groups = generate_causalstress_groups( CausalStressConfig(num_tasks=1, k=3, seed=4, categories=(category,)) ) assert len(groups) == 1 assert groups[0].category == category assert groups[0].records assert groups[0].task.success_predicates def test_hard_causalstress_categories_cycle_named_variants() -> None: expected_counts = { "similar_distractors": 4, "spatial_relation_minimal_pairs": 3, "negation_and_avoidance": 2, "sequential_tasks": 3, "irreversible_failure": 2, "physics_perturbation_placeholders": 3, } for category, count in expected_counts.items(): groups = generate_causalstress_groups( CausalStressConfig(num_tasks=count, k=2, seed=5, categories=(category,)) ) assert len({group.task.task_id for group in groups}) == count def test_causalstress_metrics_on_synthetic_predictions() -> None: groups = generate_causalstress_groups(CausalStressConfig(num_tasks=3, k=4, seed=2)) predictions = {} for group in groups: predictions[group.group_id] = { "scores": [record.reward.score for record in group.records], "success": [1.0 if record.reward.terminal_success else 0.0 for record in group.records], "progress": [record.reward.progress for record in group.records], "regret": [float(record.regret or 0.0) for record in group.records], "effects": [ [0.0] * 32 for _record in group.records ], } # Use target effect vectors as predictions to make the MAE exact zero. from dovla_cil.eval.causalstress import _effect_vector for group in groups: predictions[group.group_id]["effects"] = [ _effect_vector(record, dim=32) for record in group.records ] metrics = compute_causalstress_metrics(groups, predictions) assert metrics["pairwise_ranking_accuracy"] == 1.0 assert metrics["top1_action_selection"] == 1.0 assert metrics["success_prediction_accuracy"] == 1.0 assert metrics["effect_prediction_mae"] == 0.0 assert metrics["regret_calibration_error"] == 0.0 assert "per_category" in metrics assert "target_confusion_matrix" in metrics def test_eval_causalstress_script_runs_on_smoke_checkpoint(tmp_path: Path) -> None: dataset_dir = tmp_path / "cil" run_dir = tmp_path / "run" out_path = tmp_path / "causalstress.json" generate_cil_dataset( backend="toy", tasks=built_in_toy_tasks()[:2], out_dir=dataset_dir, num_states_per_task=1, k=4, seed=3, shard_size=8, inline_observations=True, ) DoVLATrainer( TrainerConfig( dataset_dir=dataset_dir, output_dir=run_dir, epochs=1, batch_groups=1, records_per_group=4, hidden_dim=32, seed=3, device="cpu", ) ).train() subprocess.run( [ sys.executable, "scripts/eval_causalstress.py", "--checkpoint", str(run_dir / "best.pt"), "--backend", "toy", "--out", str(out_path), "--num-tasks", "3", "--k", "4", "--seed", "3", ], check=True, capture_output=True, text=True, ) metrics = read_json(out_path) assert metrics["num_groups"] == 3 assert "pairwise_ranking_accuracy" in metrics assert "task_success_rate" in metrics