from __future__ import annotations import pytest torch = pytest.importorskip("torch") from dovla_cil.data.schema import ActionChunk from dovla_cil.models import devectorize_toy_action, vectorize_toy_action from dovla_cil.models.dovla import DoVLAConfig, DoVLAModel, load_model_state def _model_and_inputs(): config = DoVLAConfig( obs_dim=10, lang_dim=16, action_dim=8, hidden_dim=32, action_horizon=3, effect_dim=7, intervention_dim=24, ) model = DoVLAModel(config) observation = torch.randn(4, config.obs_dim) instructions = [ "pick the red mug", "put the mug in the bowl", "open the drawer", "push the cube", ] action = torch.randn(4, config.action_horizon, config.action_dim) return model, config, observation, instructions, action def test_forward_policy_works_and_shapes() -> None: model, config, observation, instructions, _action = _model_and_inputs() predicted = model.forward_policy(observation, instructions) assert predicted.shape == (4, config.action_horizon, config.action_dim) def test_forward_effect_works_and_shapes() -> None: model, config, observation, instructions, action = _model_and_inputs() output = model.forward_effect(observation, instructions, action) assert output["effect_vector"].shape == (4, config.effect_dim) assert output["success_logit"].shape == (4,) assert output["success"].shape == (4,) assert output["progress"].shape == (4,) def test_forward_reward_works_and_shapes() -> None: model, _config, observation, instructions, action = _model_and_inputs() reward = model.forward_reward(observation, instructions, action) regret = model.forward_regret(observation, instructions, action) z = model.encode_intervention(observation, instructions, action) assert reward.shape == (4,) assert regret.shape == (4,) assert z.shape == (4, model.config.intervention_dim) def test_gradients_flow() -> None: model, _config, observation, instructions, action = _model_and_inputs() policy = model.forward_policy(observation, instructions) effect = model.forward_effect(observation, instructions, action) reward = model.forward_reward(observation, instructions, action) loss = policy.square().mean() + effect["effect_vector"].square().mean() + reward.mean() loss.backward() grads = [ parameter.grad for parameter in model.parameters() if parameter.requires_grad and parameter.grad is not None ] assert grads assert sum(float(grad.abs().sum()) for grad in grads) > 0.0 def test_rgb_observation_encoder_drives_policy_and_field_gradients() -> None: config = DoVLAConfig( obs_dim=10, lang_dim=16, action_dim=7, hidden_dim=32, action_horizon=2, effect_dim=6, intervention_dim=24, observation_mode="rgb", ) model = DoVLAModel(config) images = torch.randint(0, 256, (3, 32, 40, 3), dtype=torch.uint8) instructions = ["push the cube", "stack the cubes", "lift the peg"] actions = torch.randn(3, config.action_horizon, config.action_dim) policy = model.forward_policy(images, instructions) field = model.forward_field(images, instructions, actions) (policy.square().mean() + field["potential"].mean()).backward() assert policy.shape == (3, 2, 7) assert field["effect_vector"].shape == (3, 6) assert any( parameter.grad is not None and float(parameter.grad.abs().sum()) > 0 for parameter in model.observation_encoder.image_net.parameters() ) def test_toy_action_vectorize_and_devectorize() -> None: action = ActionChunk( representation="semantic", horizon=2, values=[ {"command": "move_to", "object": "red_mug"}, {"command": "push", "object": "red_mug", "dx": 0.1, "dy": -0.2}, ], skill_type="push", ) matrix = vectorize_toy_action(action, action_dim=8, action_horizon=3) restored = devectorize_toy_action(matrix, skill_type="push") assert len(matrix) == 3 assert len(matrix[0]) == 8 assert restored.representation == "semantic" assert restored.skill_type == "push" def test_numeric_action_chunk_vectorization_preserves_simulator_controls() -> None: action = ActionChunk( representation="maniskill_pd_ee_delta_pose", horizon=2, values=[ [0.1, -0.2, 0.3, 0.4, -0.5, 0.6, 0.7], [0.0, 0.1, -0.1, 0.2, -0.2, 0.3, -0.3], ], skill_type="pick_cube", ) matrix = vectorize_toy_action(action, action_dim=8, action_horizon=3) assert matrix[0] == [0.1, -0.2, 0.3, 0.4, -0.5, 0.6, 0.7, 0.0] assert matrix[1] == [0.0, 0.1, -0.1, 0.2, -0.2, 0.3, -0.3, 0.0] assert matrix[2] == [0.0] * 8 def test_model_state_loader_allows_only_declared_omitted_prefixes() -> None: model, config, _observation, _instructions, _action = _model_and_inputs() state = model.state_dict() prefix = "observation_encoder.net.0." compact = {key: value for key, value in state.items() if not key.startswith(prefix)} restored = DoVLAModel(config) load_model_state( restored, { "model_state_dict": compact, "omitted_state_prefixes": [prefix], }, ) invalid = dict(compact) invalid.pop("language_encoder.net.0.weight") with pytest.raises(RuntimeError, match="invalid_missing"): load_model_state( DoVLAModel(config), { "model_state_dict": invalid, "omitted_state_prefixes": [prefix], }, )