| from __future__ import annotations |
|
|
| from typing import Any |
|
|
| from dovla_cil.data.schema import ActionChunk |
|
|
| try: |
| import torch |
| from torch import nn |
| except ImportError: |
| 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: |
| 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: |
| 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: |
| 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]] |
| return [[float(item) for item in row] for row in values] |
|
|
|
|
| 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 |
|
|