| """YamTaskEnv: the environment a task runs in. |
| |
| Lifecycle mirrors ManiSkill's, so a task file reads the same way: |
| |
| _load_scene() build the props and objects this task needs |
| _initialize_episode() per-episode randomization + settling |
| solve() run the task's solver (scripted, not a policy) |
| evaluate() return {"success": bool, ...} |
| |
| The env owns the simulator, both arms and the recorder. A task subclass owns *what* is in the |
| scene and *what counts as done* -- it never touches action vectors or IK. |
| """ |
| from __future__ import annotations |
|
|
| import os |
| from pathlib import Path |
|
|
| import numpy as np |
|
|
| from .scene import SceneBuilder, Randomizer, TABLE_TOP |
|
|
| REPO = Path(__file__).resolve().parents[4] |
|
|
|
|
| class YamTaskEnv: |
| |
| task_name = "unnamed" |
| title = "" |
| tags: list[str] = [] |
| instruction: dict = {} |
| bimanual = False |
| gripper_effort: float | None = None |
| gripper_damping: float | None = None |
| viewer_eye = (0.9, -0.9, 1.15) |
| viewer_lookat = (0.05, 0.0, 0.5) |
| rw_objects: dict = {} |
| rw_articulations: dict = {} |
|
|
| def __init__(self, seed: int = 0, randomize: bool = True, episode: int = -1, video: str = "", |
| overrides: dict | None = None): |
| from . import config as cfg |
| |
| cfg.apply(self, cfg.load(self.task_name), source=f"configs/{self.task_name}.yaml") |
| cfg.apply(self, overrides or {}, source="--set") |
| self.seed = seed |
| self.episode = episode |
| self.video_path = video or f"outputs/tasks/{self.task_name}.mp4" |
| self.rand = Randomizer(seed=seed, enabled=randomize) |
| self.env = None |
| self.scene = None |
| self.arms = {} |
| self.recorder = None |
| self._start_z, self._peak_z, self._peak_tilt = {}, {}, {} |
|
|
| |
| @classmethod |
| def env_vars(cls, overrides: dict | None = None) -> dict: |
| """Vars that must be set BEFORE the simulator starts (asset registration, clamp force). |
| |
| `overrides` is the already-merged YAML + --set mapping. It is read here rather than on the |
| instance because these decide what the simulator loads, and the instance does not exist |
| until after the simulator is up. |
| """ |
| o = overrides or {} |
| rw = o.get("rw_objects", cls.rw_objects) |
| artic = o.get("rw_articulations", cls.rw_articulations) |
| out = {} |
| if rw: |
| out["YAM_RW_OBJECTS"] = ",".join(f"{k}={v}" for k, v in rw.items()) |
| if artic: |
| out["YAM_RW_ARTIC"] = ",".join(f"{k}={v}" for k, v in artic.items()) |
| if o.get("gripper_effort", cls.gripper_effort): |
| out["YAM_GRIP_EFFORT"] = str(o.get("gripper_effort", cls.gripper_effort)) |
| if o.get("gripper_damping", cls.gripper_damping): |
| out["YAM_GRIP_DAMPING"] = str(o.get("gripper_damping", cls.gripper_damping)) |
| return out |
|
|
| |
| def build(self, env, origin): |
| from ..motion import ArmController, Recorder |
| from ..motion.arm import grasp_quat |
|
|
| self.env = env |
| u = env.unwrapped |
| self.origin = np.asarray(origin, float) |
| self.scene = SceneBuilder(env, origin, self.rand) |
|
|
| R, L = u.scene["right_robot"], u.scene["left_robot"] |
| self._R, self._L = R, L |
| self._dev = R.data.root_pos_w.device |
| self._cmd = {"l": None, "r": None} |
| self._grip = {"l": 1.0, "r": 1.0} |
| self._quat = {"l": grasp_quat("y"), "r": grasp_quat("y")} |
|
|
| def step_fn(arm, pos, quat, grip): |
| side = "r" if arm.name == "right" else "l" |
| self._cmd[side] = np.asarray(pos, np.float32) |
| self._quat[side] = np.asarray(quat, np.float32) |
| self._grip[side] = float(grip) |
| self.step() |
|
|
| for side, art, nm in (("right", R, "right"), ("left", L, "left")): |
| self.arms[side] = ArmController( |
| art, art.data.body_names, |
| art.data.root_pos_w[0].cpu().numpy()-self.origin, |
| art.data.root_quat_w[0].cpu().numpy(), self.origin, |
| step_fn, on_step=self._on_step, name=nm) |
| self._cmd["r"] = self.arms["right"].eef() |
| self._cmd["l"] = self.arms["left"].eef() |
| self._lhome = L.data.joint_pos[0].clone() |
|
|
| self.recorder = Recorder(env, title=self.title or self.task_name, episode=self.episode) |
| self.recorder.lines_fn = self._hud_lines |
|
|
| self._load_scene() |
| self._initialize_episode() |
| self._boost_friction() |
| self._start_quat = {} |
| for n in self.scene.objects: |
| z = float(self.scene.object_pos(n)[2]) |
| self._start_z[n], self._peak_z[n] = z, z |
| |
| self._start_quat[n] = self.scene.objects[n].data.root_quat_w[0].cpu().numpy().copy() |
| |
| self._start_joints = self._joint_state() |
| print(f"[env] {self.task_name}: built (seed={self.seed}, randomize={self.rand.enabled}) " |
| f"placements={ {k: np.round(v, 4).tolist() if hasattr(v, '__len__') else round(v, 4) for k, v in self.rand.log.items()} }", |
| flush=True) |
|
|
| |
| def _load_scene(self): |
| raise NotImplementedError |
|
|
| def _initialize_episode(self): |
| """Default: settle everything and re-seat objects at their measured height.""" |
| self.scene.reseat_objects(self._placed, self.step) |
|
|
| def solve(self): |
| raise NotImplementedError |
|
|
| def evaluate(self) -> dict: |
| raise NotImplementedError |
|
|
| |
| def step(self): |
| import torch |
| act = np.concatenate([self._cmd["l"], self._quat["l"], [self._grip["l"]], |
| self._cmd["r"], self._quat["r"], [self._grip["r"]]]) |
| self.env.step(torch.tensor(act, dtype=torch.float32, device=self._dev).view(1, -1)) |
| if not self.bimanual: |
| self._L.write_joint_state_to_sim( |
| self._lhome.view(1, -1), torch.zeros((1, self._lhome.shape[0]), device=self._dev)) |
|
|
| def drive_both(self, l_pos, r_pos, l_grip, r_grip): |
| """Command BOTH arms and step once. |
| |
| Driving them one after the other makes the first arm park in the second one's path, and |
| it also steps the sim twice per waypoint. A two-arm task must move on a single profile. |
| """ |
| L, R = self.arms["left"], self.arms["right"] |
| for arm, pos, side in ((L, l_pos, "l"), (R, r_pos, "r")): |
| arm._cmd = np.asarray(pos, np.float32) |
| self._cmd[side] = (np.asarray(pos, np.float32)+arm._corr).astype(np.float32) |
| self._quat[side] = arm.quat |
| self._grip["l"], self._grip["r"] = float(l_grip), float(r_grip) |
| self.step() |
| for arm in (L, R): |
| e = arm._cmd-arm.eef() |
| e = np.where(np.abs(e) > 0.008, e, 0.0) |
| arm._corr = np.clip(arm._corr+0.08*e, -0.10, 0.10) |
| arm._corr[2] = max(float(arm._corr[2]), -0.06) |
| self._on_step() |
|
|
| def move_both(self, l_target, r_target, l_grip, r_grip, steps=120): |
| """Eased simultaneous move of both arms to their targets.""" |
| from ..motion.arm import ease |
| L, R = self.arms["left"], self.arms["right"] |
| ls = L._seg_start() if l_target is not None else None |
| rs = R._seg_start() if r_target is not None else None |
| lt = np.asarray(l_target, np.float32) if l_target is not None else ls |
| rt = np.asarray(r_target, np.float32) if r_target is not None else rs |
| for k in range(steps): |
| a = ease((k+1)/float(steps)) |
| self.drive_both((1-a)*ls+a*lt, (1-a)*rs+a*rt, l_grip, r_grip) |
| return (float(np.linalg.norm(L.eef()-lt)), float(np.linalg.norm(R.eef()-rt))) |
|
|
| capture_every = 2 |
|
|
| def _on_step(self): |
| for n in self._peak_z: |
| self._peak_z[n] = max(self._peak_z[n], float(self.scene.object_pos(n)[2])) |
| |
| |
| for n, q0 in getattr(self, "_start_quat", {}).items(): |
| q = self.scene.objects[n].data.root_quat_w[0].cpu().numpy() |
| d = abs(float(np.dot(q/np.linalg.norm(q), q0/np.linalg.norm(q0)))) |
| ang = float(np.degrees(2*np.arccos(min(1.0, d)))) |
| self._peak_tilt[n] = max(self._peak_tilt.get(n, 0.0), ang) |
| |
| |
| self._tick = getattr(self, "_tick", 0)+1 |
| if self.recorder is not None and self._tick % self.capture_every == 0: |
| self.recorder.capture() |
|
|
| def _boost_friction(self, s=1.6, d=1.4): |
| import torch |
| def boost(view, tag): |
| try: |
| m = view.get_material_properties().clone(); m[..., 0] = s; m[..., 1] = d |
| view.set_material_properties(m, torch.arange(m.shape[0], dtype=torch.int32, device=m.device)) |
| except Exception as e: |
| print(f"[env] friction set failed on {tag}: {e}", flush=True) |
| boost(self._R.root_physx_view, "right_robot"); boost(self._L.root_physx_view, "left_robot") |
| for n, o in self.scene.objects.items(): |
| boost(o.root_physx_view, n) |
|
|
| def _hud_lines(self): |
| return [f"{n}=({p[0]:+.2f},{p[1]:+.2f},{p[2]:.2f})" |
| for n, p in ((n, self.scene.object_pos(n)) for n in list(self.scene.objects)[:3])] |
|
|
| |
| def state(self): |
| """The dict the condition predicates read.""" |
| return {"objects": {n: self.scene.object_pos(n) for n in self.scene.objects}, |
| "quats": {n: self.scene.objects[n].data.root_quat_w[0].cpu().numpy() |
| for n in self.scene.objects}, |
| "regions": self.scene.regions, "start_quats": getattr(self, "_start_quat", {}), |
| "start_z": self._start_z, "peak_z": self._peak_z, |
| "peak_tilt": self._peak_tilt, |
| "joints": self._joint_state(), "start_joints": getattr(self, "_start_joints", {}), |
| "joint_delta": getattr(self, "_joint_delta", {}), |
| "links": self._link_state(), |
| "joint_sane": {n: bool( |
| (art.data.joint_pos[0] >= art.data.joint_pos_limits[0, :, 0]-0.05).all() |
| and (art.data.joint_pos[0] <= art.data.joint_pos_limits[0, :, 1]+0.05).all()) |
| for n, art in getattr(self.scene, "articulations", {}).items()}} |
|
|
| def _link_state(self): |
| """(fixture, link index) -> world xyz, for chains whose shape is the thing being judged.""" |
| out = {} |
| for n, art in getattr(self.scene, "articulations", {}).items(): |
| p = art.data.body_pos_w[0].cpu().numpy()-self.origin |
| for i in range(len(p)): |
| out[(n, i)] = p[i] |
| return out |
|
|
| def _joint_state(self): |
| """(fixture, joint index) -> value, for every jointed prop in the scene.""" |
| out = {} |
| for n, art in getattr(self.scene, "articulations", {}).items(): |
| q = art.data.joint_pos[0].cpu().numpy() |
| for i in range(len(q)): |
| out[(n, i)] = float(q[i]) |
| return out |
|
|
| def check(self, *conditions) -> dict: |
| """Run condition predicates against the current state; returns {"success": ..., per-cond}.""" |
| from .. import conditions as C |
| st = self.state() |
| per = {} |
| for c in conditions: |
| label = getattr(c, "label", getattr(c, "__name__", "cond")) |
| try: |
| per[label] = bool(c(st)) |
| except Exception as e: |
| print(f"[env] condition {label} errored: {e}", flush=True) |
| per[label] = False |
| ok = all(per.values()) if per else False |
| self.recorder.result = "SUCCESS" if ok else "FAIL" |
| for _ in range(14): |
| self.recorder.capture() |
| print(f"[env] EPISODE_RESULT: {self.recorder.result}", flush=True) |
| for k, v in per.items(): |
| print(f"[env] {'PASS' if v else 'FAIL'} {k}", flush=True) |
| return {"success": ok, **per} |
|
|
| def save_video(self, path=None): |
| p = path or self.video_path |
| return self.recorder.save(str(REPO/p) if not os.path.isabs(p) else p) |
|
|