from __future__ import annotations import pytest from dovla_cil.data.schema import ( CIL_VERSION, ActionChunk, CILBenchBranch, CILBenchGroup, CILGroup, CILRecord, FailureInfo, OutcomeVector, RewardInfo, StructuredEffect, compute_regret_and_ranks, compute_state_hash, make_record_id, validate_group, ) def make_record( group_id: str, action_id: str, progress: float, *, state_hash: str = "s" ) -> CILRecord: return CILRecord( version=CIL_VERSION, record_id=make_record_id(group_id, action_id, seed=7), group_id=group_id, state_hash=state_hash, task_id="task", scene_id=None, instruction="move the mug", instruction_family={"family": "place"}, observation_ref=None, observation_inline={"symbolic": True}, action_chunk=ActionChunk( action_id=action_id, representation="delta_xy", horizon=1, values=[[progress, 0.0]], skill_type="push", ), next_observation_ref=None, next_observation_inline={"symbolic": True, "next": True}, structured_effect=StructuredEffect( object_pose_delta={"mug": [progress, 0.0, 0.0]}, relation_before={"inside(mug,bowl)": False}, relation_after={"inside(mug,bowl)": progress > 0.5}, moved_objects=["mug"] if progress else [], symbolic_before={"objects": {"mug": {"position": [0, 0, 0]}}}, symbolic_after={"objects": {"mug": {"position": [progress, 0, 0]}}}, ), reward=RewardInfo( progress=progress, success=progress > 0.5, terminal_success=progress > 0.5, dense_components={"progress": progress}, ), regret=None, rank_within_group=None, candidate_type="sampled", failure=None if progress > 0 else FailureInfo(type="no_motion", symbolic_reason="object did not move"), ) def test_schema_roundtrip() -> None: record = make_record("g", "a", 1.0) record.validate() assert CILRecord.from_dict(record.to_dict()) == record group = CILGroup.from_records([record]) assert group.group_id == "g" def test_group_validation() -> None: records = [make_record("g", "a", 0.0), make_record("g", "b", 1.0)] validate_group(records) with pytest.raises(ValueError): validate_group([make_record("g", "a", 0.0), make_record("other", "b", 1.0)]) with pytest.raises(ValueError): validate_group([make_record("g", "a", 0.0), make_record("g", "b", 1.0, state_hash="x")]) def test_regret_and_rank_computation() -> None: ranked = compute_regret_and_ranks( [make_record("g", "low", 0.0), make_record("g", "high", 1.0)] ) by_action = {record.action_chunk.action_id: record for record in ranked} assert by_action["high"].rank_within_group == 0 assert by_action["high"].regret == 0.0 assert by_action["low"].rank_within_group == 1 assert by_action["low"].regret == 2.0 def test_deterministic_record_ids_and_state_hashes() -> None: assert make_record_id("g", "a", 1) == make_record_id("g", "a", 1) assert make_record_id("g", "a", 1) != make_record_id("g", "a", 2) assert compute_state_hash(b"state") == compute_state_hash(b"state") assert compute_state_hash(b"state") != compute_state_hash(b"other") def test_outcome_vector_from_reward_and_utility() -> None: reward = RewardInfo( progress=0.6, success=True, terminal_success=True, dense_components={ "contact_quality": 0.5, "task_stage_quality": 0.25, "smoothness": 0.8, "recovery": 1.0, }, ) outcome = OutcomeVector.from_reward(reward) assert outcome.success == 1.0 assert outcome.progress == 0.6 assert outcome.contact_quality == 0.5 assert outcome.safety_violation == 0.0 assert outcome.lexicographic_utility() > reward.score def test_cilbench_group_roundtrip_and_same_state_contrast() -> None: base_action = ActionChunk(action_id="base", horizon=1, values=[[0.0, 0.0]]) repair_action = ActionChunk(action_id="repair", horizon=1, values=[[0.1, 0.0]]) group = CILBenchGroup( group_id="chart-0", task_id="PickCube-v1", split_id="train", simulator_state_hash="hash", instruction="pick the cube", observation_ref=None, observation_inline={"rgb": "obs/000.png"}, scene_metadata={"target_object": "cube"}, anchor_policy="h16_bc", branches=[ CILBenchBranch( branch_id="base", action=base_action, branch_family="anchor", outcome=OutcomeVector(success=0.0, progress=0.2), ), CILBenchBranch( branch_id="repair", action=repair_action, branch_family="recovery_tangent", outcome=OutcomeVector(success=1.0, progress=0.8, recovery=1.0), ), ], ) restored = CILBenchGroup.from_dict(group.to_dict()) assert restored == group assert restored.same_state_causal_contrast("repair", "base") > 0.0 with pytest.raises(KeyError): restored.same_state_causal_contrast("repair", "missing")