vla / dovla_cil /models /action_encoder.py
anhtld's picture
Initial commit: DoVLA-CIL codebase (h=16 breakthrough)
adc02fa verified
Raw
History Blame Contribute Delete
11.8 kB
from __future__ import annotations
from typing import Any
from dovla_cil.data.schema import ActionChunk
try:
import torch
from torch import nn
except ImportError: # pragma: no cover - torch is an install dependency, absent in bare smoke envs
torch = None
nn = None
TOY_COMMANDS = (
"noop",
"move_to",
"grasp",
"release",
"push",
"place_at",
"open",
"close",
"delay",
)
TOY_SKILLS = (
"unknown",
"noop",
"delay",
"reach",
"grasp",
"lift",
"push",
"place",
"place_at",
"open",
"close",
"opened",
"closed",
)
def vectorize_toy_action(
action: ActionChunk | dict[str, Any] | list[dict[str, Any]] | list[list[float]],
*,
action_dim: int = 8,
action_horizon: int = 4,
) -> list[list[float]]:
"""Convert an action chunk into a fixed horizon numeric matrix.
Symbolic toy actions use intentionally simple and stable feature slots:
command id, target hash, reference hash, dx, dy, x, y, z, then any extra slots remain zero.
Numeric simulator action chunks, such as ManiSkill control vectors, are preserved directly and
only padded or truncated to the requested shape.
"""
if action_dim <= 0:
raise ValueError("action_dim must be positive")
if action_horizon <= 0:
raise ValueError("action_horizon must be positive")
numeric_rows = _numeric_action_rows(action)
if numeric_rows is not None:
return _pad_numeric_rows(numeric_rows, action_dim=action_dim, action_horizon=action_horizon)
commands = _action_commands(action)
rows: list[list[float]] = []
for command in commands[:action_horizon]:
row = [0.0] * action_dim
if action_dim > 0:
row[0] = _command_id(str(command.get("command") or command.get("type") or "noop"))
if action_dim > 1:
row[1] = _stable_unit_hash(str(command.get("object") or command.get("target") or ""))
if action_dim > 2:
row[2] = _stable_unit_hash(
str(command.get("reference") or command.get("container") or "")
)
if action_dim > 3:
row[3] = float(command.get("dx", 0.0) or 0.0)
if action_dim > 4:
row[4] = float(command.get("dy", 0.0) or 0.0)
if action_dim > 5:
position = _position(command.get("position", [0.0, 0.0, 0.0]))
row[5] = position[0]
if action_dim > 6:
row[6] = position[1]
if action_dim > 7:
row[7] = position[2]
rows.append(row)
while len(rows) < action_horizon:
rows.append([0.0] * action_dim)
return rows
def devectorize_toy_action(
values: list[float] | list[list[float]],
*,
skill_type: str | None = None,
) -> ActionChunk:
"""Best-effort conversion from a numeric policy output to a toy ActionChunk."""
rows = _matrix(values)
commands: list[dict[str, Any]] = []
for row in rows:
if not row or all(abs(float(value)) < 1e-8 for value in row):
continue
command = _nearest_command(float(row[0]))
if command in {"noop", "delay"}:
payload: dict[str, Any] = {"command": command}
if command == "delay":
payload["steps"] = max(1, int(round(abs(row[1]) * 3)) if len(row) > 1 else 1)
elif command == "push":
payload = {
"command": "push",
"object": "predicted_target",
"dx": float(row[3]) if len(row) > 3 else 0.0,
"dy": float(row[4]) if len(row) > 4 else 0.0,
}
elif command in {"move_to", "place_at"}:
payload = {
"command": command,
"object": "predicted_target",
"position": [
float(row[5]) if len(row) > 5 else 0.0,
float(row[6]) if len(row) > 6 else 0.0,
float(row[7]) if len(row) > 7 else 0.03,
],
}
elif command in {"grasp", "open", "close"}:
payload = {"command": command, "object": "predicted_target"}
else:
payload = {"command": command}
commands.append(payload)
if not commands:
commands = [{"command": "noop"}]
return ActionChunk(
representation="semantic",
horizon=len(commands),
values=commands,
skill_type=skill_type,
metadata={"source": "devectorized_toy_policy"},
)
def skill_type_id(skill_type: str | None) -> int:
normalized = str(skill_type or "unknown")
try:
return TOY_SKILLS.index(normalized)
except ValueError:
return 0
if nn is not None:
class ActionEncoder(nn.Module):
def __init__(
self,
action_dim: int,
hidden_dim: int,
*,
action_horizon: int = 4,
skill_vocab_size: int = 32,
) -> None:
super().__init__()
self.action_dim = int(action_dim)
self.action_horizon = int(action_horizon)
self.step_mlp = nn.Sequential(
nn.Linear(self.action_dim, hidden_dim),
nn.LayerNorm(hidden_dim),
nn.GELU(),
nn.Linear(hidden_dim, hidden_dim),
)
self.skill_embedding = nn.Embedding(skill_vocab_size, hidden_dim)
self.out = nn.Sequential(
nn.LayerNorm(hidden_dim),
nn.GELU(),
nn.Linear(hidden_dim, hidden_dim),
)
def forward(self, action, skill_type=None):
action_tensor = coerce_action_tensor(
action,
action_dim=self.action_dim,
action_horizon=self.action_horizon,
device=next(self.parameters()).device,
)
step_features = self.step_mlp(action_tensor)
pooled = step_features.mean(dim=1)
if skill_type is not None:
pooled = pooled + self.skill_embedding(
coerce_skill_ids(skill_type, device=pooled.device)
% self.skill_embedding.num_embeddings
)
return self.out(pooled)
else:
class ActionEncoder: # pragma: no cover
def __init__(self, *args, **kwargs) -> None:
del args, kwargs
raise ImportError("Install torch to use ActionEncoder.")
def coerce_action_tensor(
action,
*,
action_dim: int,
action_horizon: int,
device=None,
):
if torch is None: # pragma: no cover
raise ImportError("Install torch to coerce action tensors.")
if isinstance(action, ActionChunk):
matrix = vectorize_toy_action(action, action_dim=action_dim, action_horizon=action_horizon)
return torch.tensor([matrix], dtype=torch.float32, device=device)
if isinstance(action, list) and action and isinstance(action[0], ActionChunk):
matrices = [
vectorize_toy_action(item, action_dim=action_dim, action_horizon=action_horizon)
for item in action
]
return torch.tensor(matrices, dtype=torch.float32, device=device)
tensor = torch.as_tensor(action, dtype=torch.float32, device=device)
if tensor.ndim == 1:
tensor = tensor.unsqueeze(0)
if tensor.ndim == 2:
if tensor.shape[-1] == action_horizon * action_dim:
tensor = tensor.reshape(tensor.shape[0], action_horizon, action_dim)
else:
tensor = tensor.unsqueeze(1)
if tensor.ndim != 3:
raise ValueError("Action tensor must have shape [B,D], [B,H,D], or [D]")
return _pad_action_tensor(tensor, action_horizon=action_horizon, action_dim=action_dim)
def coerce_skill_ids(skill_type, *, device=None):
if torch is None: # pragma: no cover
raise ImportError("Install torch to coerce skill ids.")
if isinstance(skill_type, torch.Tensor):
return skill_type.to(device=device, dtype=torch.long).flatten()
if isinstance(skill_type, str) or skill_type is None:
ids = [skill_type_id(skill_type)]
else:
ids = [skill_type_id(item) for item in skill_type]
return torch.tensor(ids, dtype=torch.long, device=device)
def _pad_action_tensor(tensor, *, action_horizon: int, action_dim: int):
if tensor.shape[1] > action_horizon:
tensor = tensor[:, :action_horizon, :]
elif tensor.shape[1] < action_horizon:
pad = tensor.new_zeros((tensor.shape[0], action_horizon - tensor.shape[1], tensor.shape[2]))
tensor = torch.cat([tensor, pad], dim=1)
if tensor.shape[2] > action_dim:
tensor = tensor[:, :, :action_dim]
elif tensor.shape[2] < action_dim:
pad = tensor.new_zeros((tensor.shape[0], tensor.shape[1], action_dim - tensor.shape[2]))
tensor = torch.cat([tensor, pad], dim=2)
return tensor
def _action_commands(
action: ActionChunk | dict[str, Any] | list[dict[str, Any]],
) -> list[dict[str, Any]]:
if isinstance(action, ActionChunk):
if isinstance(action.values, list) and all(
isinstance(item, dict) for item in action.values
):
return [dict(item) for item in action.values]
return [{"command": "numeric", "position": action.flat_values[:3]}]
if isinstance(action, dict):
return [dict(action)]
return [dict(item) for item in action]
def _position(value: Any) -> list[float]:
if isinstance(value, dict):
return [
float(value.get("x", 0.0)),
float(value.get("y", 0.0)),
float(value.get("z", 0.0)),
]
if isinstance(value, list | tuple):
items = list(value)
while len(items) < 3:
items.append(0.0)
return [float(items[0]), float(items[1]), float(items[2])]
return [0.0, 0.0, 0.0]
def _matrix(values: list[float] | list[list[float]]) -> list[list[float]]:
if not values:
return []
if all(isinstance(item, int | float) for item in values):
return [[float(item) for item in values]] # type: ignore[arg-type]
return [[float(item) for item in row] for row in values] # type: ignore[union-attr]
def _numeric_action_rows(action: Any) -> list[list[float]] | None:
values = action.values if isinstance(action, ActionChunk) else action
if not isinstance(values, list) or not values:
return None
if all(isinstance(row, list) and _is_numeric_sequence(row) for row in values):
return [[float(item) for item in row] for row in values]
if _is_numeric_sequence(values):
return [[float(item) for item in values]]
return None
def _is_numeric_sequence(values: Any) -> bool:
return isinstance(values, list) and all(isinstance(item, int | float) for item in values)
def _pad_numeric_rows(
rows: list[list[float]], *, action_dim: int, action_horizon: int
) -> list[list[float]]:
output: list[list[float]] = []
for row in rows[:action_horizon]:
clipped = [float(value) for value in row[:action_dim]]
if len(clipped) < action_dim:
clipped.extend([0.0] * (action_dim - len(clipped)))
output.append(clipped)
while len(output) < action_horizon:
output.append([0.0] * action_dim)
return output
def _command_id(command: str) -> float:
try:
return TOY_COMMANDS.index(command) / max(len(TOY_COMMANDS) - 1, 1)
except ValueError:
return 0.0
def _nearest_command(value: float) -> str:
index = round(max(0.0, min(1.0, value)) * max(len(TOY_COMMANDS) - 1, 1))
return TOY_COMMANDS[int(index)]
def _stable_unit_hash(value: str) -> float:
total = 0
for byte in value.encode("utf-8"):
total = (total * 257 + byte) % 1000003
return total / 1000003.0