File size: 8,394 Bytes
168e232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
"""Pick and place in sim with MuJoCo as the world model (oracle). Milestone 0.

    # batch: metrics.csv + summary.txt + compute.txt (+ an mp4 per episode with --video: room view with the purple
    # plan and the sample spaghetti | gripper camera with the plan projected into it)
    uv run python scripts/pick_sim.py --episodes 12
    uv run python scripts/pick_sim.py --episodes 3 --video
    # live, watch it think (macOS: mjpython for the viewer)
    uv run mjpython scripts/pick_sim.py --viewer

Episodes cycle through the (object, container) pairs of configs/tasks/pick_place.yaml.
"""
from __future__ import annotations

import argparse
import sys
import time
from datetime import datetime
from pathlib import Path

import mujoco
import numpy as np
import yaml

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from cablepilot.compute import ComputeMeter  # noqa: E402
from cablepilot.runner import Sim, run_episode, save_episode, write_summary  # noqa: E402
from cablepilot.sim.scene import load_config  # noqa: E402
from cablepilot.viz import draw_scene, overlay_camera  # noqa: E402


def hud(f) -> list[str]:
    tg = f.extra["target"]
    lines = [f"t {f.time_s:5.1f}s  phase {f.phase.upper()}",
             f"target conf {tg.confidence:.2f}{'  SEEN' if tg.visible else ''}",
             f"plan {f.extra['plan_ms']:.0f} ms  ESS {f.info['ess']:.0f}"]
    top = sorted(f.info["task_costs"].items(), key=lambda kv: -abs(kv[1]))[:3]
    lines.append("  ".join(f"{k} {v:.0f}" for k, v in top))
    return lines


class Camera:
    """Projection for an offscreen render of a MuJoCo camera (the gripper camera) at W x H."""

    def __init__(self, model, data, cam_id, W, H):
        self.d, self.cam, self.W, self.H = data, cam_id, W, H
        self.fy = 0.5 * H / np.tan(np.radians(model.cam_fovy[cam_id]) / 2)

    def __call__(self, pts):
        R = self.d.cam_xmat[self.cam].reshape(3, 3)
        rel = (np.atleast_2d(pts) - self.d.cam_xpos[self.cam]) @ R
        z = -rel[:, 2]
        return (self.W / 2 + self.fy * rel[:, 0] / np.maximum(z, 1e-6),
                self.H / 2 - self.fy * rel[:, 1] / np.maximum(z, 1e-6), z)


class VideoSink:
    def __init__(self, sim: Sim, path: Path, fps: int, W=640, H=360):
        import imageio.v2 as imageio

        m, d = sim.plant.model, sim.plant.data
        self.sim = sim
        self.r_room = mujoco.Renderer(m, H, W)
        self.r_grip = mujoco.Renderer(m, H, W)
        self.cam = mujoco.MjvCamera()
        self.cam.distance, self.cam.azimuth, self.cam.elevation = 2.6, 225.0, -28.0
        self.cam.lookat[:] = [0, 0, 0.6]
        self.gcam = m.camera("gripper_cam").id
        self.proj = Camera(m, d, self.gcam, W, H)
        self.opt = mujoco.MjvOption()
        self.opt.geomgroup[2] = 0                    # hide the walls in the room view
        self.gopt = mujoco.MjvOption()               # gripper camera: what the real camera would see
        for o in (self.opt, self.gopt):
            o.sitegroup[:] = 0                       # sites (the grasp point marker) are bookkeeping, not objects
            o.flags[mujoco.mjtVisFlag.mjVIS_RANGEFINDER] = 0
        self.writer = imageio.get_writer(path, fps=fps, codec="libx264", quality=7, macro_block_size=8)

    def __call__(self, f):
        d = self.sim.plant.data
        target = 0.5 * (f.nominal[0] + f.nominal[-1])
        self.cam.lookat[:] = 0.9 * self.cam.lookat + 0.1 * target
        self.r_room.update_scene(d, camera=self.cam, scene_option=self.opt)
        draw_scene(self.r_room.scene, f.nominal, f.samples, f.extra["target"], f.phase, reset=False)
        room = self.r_room.render()
        self.r_grip.update_scene(d, camera=self.gcam, scene_option=self.gopt)
        grip = overlay_camera(self.r_grip.render(), self.proj, f.nominal, f.samples, hud(f))
        self.writer.append_data(np.concatenate([room, grip], axis=1))

    def close(self):
        self.writer.close()
        self.r_room.close()
        self.r_grip.close()


class ViewerSink:
    def __init__(self, sim: Sim, viewer, realtime: bool):
        self.sim, self.v, self.realtime = sim, viewer, realtime
        self.t_wall0 = self.t_sim0 = None

    def __call__(self, f):
        v = self.v
        if not v.is_running():
            raise KeyboardInterrupt
        with v.lock():
            draw_scene(v.user_scn, f.nominal, f.samples, f.extra["target"], f.phase)
        v.set_texts([(mujoco.mjtFontScale.mjFONTSCALE_150, mujoco.mjtGridPos.mjGRID_TOPLEFT, "\n".join(hud(f)), None)])
        v.sync()
        if self.realtime:
            now = time.perf_counter()
            if self.t_wall0 is None or f.time_s < self.t_sim0:
                self.t_wall0, self.t_sim0 = now, f.time_s
            ahead = (f.time_s - self.t_sim0) - (now - self.t_wall0)
            if ahead > 0:
                time.sleep(ahead)


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--episodes", type=int, default=1)
    ap.add_argument("--seed", type=int, default=0)
    ap.add_argument("--planner", default=str(ROOT / "configs/planner.yaml"))
    ap.add_argument("--task", default=str(ROOT / "configs/tasks/pick_place.yaml"))
    ap.add_argument("--object", default=None, help="only this object")
    ap.add_argument("--container", default=None, help="only this container")
    ap.add_argument("--viewer", action="store_true")
    ap.add_argument("--fast", action="store_true")
    ap.add_argument("--video", action="store_true")
    ap.add_argument("--out", default=None)
    ap.add_argument("--duration", type=float, default=None)
    args = ap.parse_args()

    planner = yaml.safe_load(Path(args.planner).read_text())
    task_cfg = yaml.safe_load(Path(args.task).read_text())
    if args.duration:
        task_cfg["episode"]["duration_s"] = args.duration
    cfg = load_config()
    sim = Sim(cfg, planner, task_cfg)
    stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    out = Path(args.out or ROOT / "runs" / f"{stamp}_pick_place_oracle")
    out.mkdir(parents=True, exist_ok=True)
    (out / "config.yaml").write_text(yaml.safe_dump({"args": vars(args), "planner": planner, "task": task_cfg}))
    pairs = [p for p in task_cfg["episode"]["pairs"]
             if (args.object in (None, p[0])) and (args.container in (None, p[1]))] or [[args.object, args.container]]

    viewer = None
    if args.viewer:
        import mujoco.viewer
        viewer = mujoco.viewer.launch_passive(sim.plant.model, sim.plant.data, show_left_ui=False, show_right_ui=False)
    meter = ComputeMeter(budget_s=sim.pcfg.dt, log_path=out / "compute.jsonl")
    all_metrics = []
    try:
        for ep in range(args.episodes):
            obj, box = pairs[ep % len(pairs)]
            sink = None
            if viewer is not None:
                sink = ViewerSink(sim, viewer, realtime=not args.fast)
            elif args.video:
                sink = VideoSink(sim, out / f"episode_{ep:02d}_{obj}_{box}.mp4", fps=round(3 / sim.pcfg.dt))
            t0 = time.perf_counter()
            log, mt = run_episode(sim, seed=args.seed + 1000 * ep, obj=obj, container=box, on_frame=sink, meter=meter)
            if isinstance(sink, VideoSink):
                sink.close()
            save_episode(log, out / f"episode_{ep:02d}.npz")
            all_metrics.append(mt)
            events = " | ".join(f"{e['time_s']:.1f}s {e['why']}" for e in log["events"])
            print(f"episode {ep:02d} {obj:6s}->{box:3s}: success {mt['success']:.0f}  grasp {mt['time_to_grasp_s']:.1f}s  "
                  f"place {mt['time_to_place_s']:.1f}s  closes {mt['close_attempts']}  plan {mt['plan_ms_mean']:.0f} ms  "
                  f"tilt max {mt['max_tilt_deg']:.1f}  T max {mt['max_tension_N']:.0f} N  ({time.perf_counter() - t0:.0f} s wall)"
                  f"\n    [{events}]", flush=True)
    except KeyboardInterrupt:
        print("stopped")
    finally:
        if viewer is not None:
            viewer.close()
    if all_metrics:
        header = f"task: pick_place  model: oracle (MuJoCo)  episodes: {len(all_metrics)}  seed: {args.seed}"
        print(write_summary(out, all_metrics, header))
        table = meter.table()
        (out / "compute.txt").write_text(table + "\n")
        print(table)
        print(f"-> {out}")
    meter.close()


if __name__ == "__main__":
    main()