from __future__ import annotations import pytest from dovla_cil.transfercritic.eval import compare_selection_strategies from dovla_cil.transfercritic.labeling import make_utility_labels, toy_utility_value from dovla_cil.transfercritic.schema import DataAtom, TransferContext from dovla_cil.transfercritic.selection import greedy_marginal_selection def _atom( atom_id: str, *, score: float, success: bool = False, candidate_type: str = "near_miss", task_id: str = "task_a", cost: float = 1.0, ) -> DataAtom: return DataAtom( record_id=atom_id, embedding=[score, 1.0 if success else 0.0, cost, 0.5], candidate_type=candidate_type, task_metadata={"task_id": task_id, "family": "pick"}, reward_summary={ "progress": score, "score": score + (1.0 if success else 0.0), "success": 1.0 if success else 0.0, "regret": max(0.0, 1.0 - score), }, effect_summary={"moved_object_count": 1.0, "true_relation_count": score}, cost=cost, ) def test_data_atom_and_context_schema_roundtrip() -> None: atom = _atom("r1", score=0.5, success=True) context = TransferContext( benchmark_name="CausalStress", task_family="pick", target_objects=["red_mug"], ood_factor="wrong_target", validation_ref="val://small", ) restored = DataAtom.from_dict(atom.to_dict()) context_restored = TransferContext.from_dict(context.to_dict()) assert restored == atom assert context_restored == context assert restored.atom_id == "r1" def test_greedy_selection_uses_score_over_cost() -> None: atoms = [ _atom("cheap_good", score=0.8, success=True, cost=1.0), _atom("expensive_best", score=1.0, success=True, cost=3.0), _atom("bad", score=0.1, success=False, cost=1.0), ] context = TransferContext(benchmark_name="CausalStress", task_family="pick") result = greedy_marginal_selection( atoms, context, budget=2.0, score_fn=lambda atom, _selected, _context: float(atom.reward_summary["score"]), ) assert result.name == "transfercritic" assert result.total_cost <= 2.0 assert result.atom_ids[0] == "cheap_good" assert "expensive_best" not in result.atom_ids def test_toy_utility_labels_prefer_successful_useful_atoms() -> None: context = TransferContext(benchmark_name="CausalStress", task_family="pick") good = _atom("good", score=0.9, success=True, candidate_type="near_miss") poor = _atom("poor", score=0.1, success=False, candidate_type="noop") labels = make_utility_labels([good, poor], context, method="toy_retraining_delta") assert labels[0].utility == toy_utility_value(good, context) assert labels[0].utility > labels[1].utility assert labels[0].metadata["approximate"] is True def test_selection_experiments_return_all_baselines() -> None: atoms = [ _atom("a", score=0.6, task_id="task_a"), _atom("b", score=0.9, success=True, task_id="task_b"), _atom("c", score=0.2, task_id="task_a"), ] context = TransferContext(benchmark_name="CausalStress", task_family="pick") rows = compare_selection_strategies(atoms, context, budget=2.0, seed=0) assert {row["name"] for row in rows} == { "random_subset", "top_reward_subset", "task_balanced_subset", "transfercritic", } assert all(float(row["total_cost"]) <= 2.0 for row in rows) def test_transfercritic_model_shapes() -> None: torch = pytest.importorskip("torch") from dovla_cil.transfercritic.model import SetConditionedTransferCritic, TransferCriticConfig config = TransferCriticConfig(atom_dim=4, set_dim=4, context_dim=4, hidden_dim=8) model = SetConditionedTransferCritic(config) atom = torch.randn(3, 4) current_set = torch.zeros(3, 4) context = torch.randn(3, 4) score = model(atom, current_set, context) assert score.shape == (3,) score.mean().backward() assert any(parameter.grad is not None for parameter in model.parameters())