from __future__ import annotations import pytest from dovla_cil.data.schema import ActionChunk from dovla_cil.effects.extractors import extract_structured_effect from dovla_cil.effects.failure_classifier import classify_failure, classify_toy_failure from dovla_cil.effects.rewards import best_action_index, compute_reward, distance_to_target_reward from dovla_cil.tasks.library import ToyTaskLibrary def test_distance_reward_prefers_near_actions() -> None: assert distance_to_target_reward(0.1, success=False) > distance_to_target_reward( 0.5, success=False ) assert distance_to_target_reward(0.01, success=True) > 0.0 def test_distance_reward_rejects_negative() -> None: with pytest.raises(ValueError): distance_to_target_reward(-1.0, success=False) def test_success_predicate_gives_success_reward() -> None: task = ToyTaskLibrary().get_by_id("toy_put_red_mug_in_blue_bowl") before = _state(red_mug=[0.0, 0.0, 0.03], blue_bowl=[1.0, 0.0, 0.03]) after = _state( red_mug=[1.0, 0.0, 0.04], blue_bowl=[1.0, 0.0, 0.03], red_mug_extra={"inside": "blue_bowl"}, ) effect = extract_structured_effect(before, after, task=task) reward = compute_reward(task, effect) assert reward.success is True assert reward.terminal_success is True assert reward.progress == 1.0 def test_wrong_target_classified() -> None: task = ToyTaskLibrary().get_by_id("toy_pick_object_among_distractors") before = _state(red_mug=[0, 0, 0.03], blue_mug=[0.5, 0, 0.03], green_bowl=[1, 0, 0.03]) after = _state(red_mug=[0, 0, 0.03], blue_mug=[0.8, 0, 0.03], green_bowl=[1, 0, 0.03]) effect = extract_structured_effect(before, after, task=task) reward = compute_reward(task, effect) action = ActionChunk( representation="semantic", values=[{"command": "grasp", "object": "blue_mug"}], skill_type="grasp", metadata={"candidate_type": "wrong_target", "intended_target": "blue_mug"}, ) failure = classify_failure(task, action, effect, reward) assert failure.type == "wrong_target" def test_no_motion_classified() -> None: task = ToyTaskLibrary().get_by_id("toy_lift_can") before = _state(can=[0, 0, 0.03]) after = _state(can=[0, 0, 0.03]) effect = extract_structured_effect(before, after, task=task) reward = compute_reward(task, effect) action = ActionChunk( representation="semantic", values=[{"command": "noop"}], skill_type="noop", metadata={"candidate_type": "noop", "intended_target": "can"}, ) failure = classify_failure(task, action, effect, reward) assert failure.type == "no_motion" def test_partial_pick_place_classified() -> None: task = ToyTaskLibrary().get_by_id("toy_put_red_mug_in_blue_bowl") before = _state(red_mug=[0.0, 0.0, 0.03], blue_bowl=[1.0, 0.0, 0.03]) after = _state( red_mug=[0.5, 0.0, 0.2], blue_bowl=[1.0, 0.0, 0.03], red_mug_extra={"grasped": True, "lifted": True}, ) effect = extract_structured_effect( before, after, task=task, rollout_info={"grasp_success": True} ) reward = compute_reward(task, effect) action = ActionChunk( representation="semantic", values=[{"command": "grasp", "object": "red_mug"}], skill_type="grasp", metadata={"candidate_type": "near_miss", "intended_target": "red_mug"}, ) failure = classify_failure(task, action, effect, reward) assert reward.success is False assert 0.0 < reward.progress < 1.0 assert failure.type == "partial_success" def test_progress_is_bounded() -> None: task = ToyTaskLibrary().get_by_id("toy_push_cube_to_target_zone") before = _state(cube=[0, 0, 0.03], target_zone=[10, 0, 0.0]) after = _state(cube=[100, 0, 0.03], target_zone=[10, 0, 0.0]) effect = extract_structured_effect(before, after, task=task) reward = compute_reward(task, effect) assert 0.0 <= reward.progress <= 1.0 def test_effect_extraction_compatibility_and_toy_failure() -> None: effect = extract_structured_effect( {"position": 0.0}, {"position": 0.5}, info={"success": False} ) assert effect.metrics["delta_position"] == 0.5 assert effect.relation_after["success"] is False assert classify_toy_failure(distance=0.2, tolerance=0.05) == "missed_target" assert classify_toy_failure(distance=0.01, tolerance=0.05) is None def test_best_action_index() -> None: assert best_action_index([0.0, 2.0, 1.0]) == 1 def _state( *, red_mug=None, blue_bowl=None, blue_mug=None, green_bowl=None, can=None, cube=None, target_zone=None, red_mug_extra=None, ) -> dict: objects = {} for name, position in { "red_mug": red_mug, "blue_bowl": blue_bowl, "blue_mug": blue_mug, "green_bowl": green_bowl, "can": can, "cube": cube, "target_zone": target_zone, }.items(): if position is not None: objects[name] = {"position": list(position), "grasped": False, "lifted": False} if red_mug_extra and "red_mug" in objects: objects["red_mug"].update(red_mug_extra) return {"objects": objects, "near_threshold": 0.25, "lifted_z": 0.1}