Spaces:
Sleeping
Sleeping
File size: 6,049 Bytes
9510f61 8b1d746 9510f61 8b1d746 9510f61 e58984a 9510f61 e58984a 9510f61 8b1d746 9510f61 e58984a 8b1d746 9510f61 8b1d746 e58984a 9510f61 8b1d746 9510f61 8b1d746 9510f61 | 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 | """Watch a checkpoint walk: rebuild the Brax PPO policy from a saved params
pickle, run it on one Himalaya/snow environment, and encode the frames to MP4.
A reward curve says whether training is moving; only a rollout says whether the
robot walks. `ppo.train` returns `(make_inference_fn, params, metrics)`, and the
checkpoints written by `policy_params_fn` are the same `params` tuple
`(normalizer_params, policy_params, value_params)`, so the policy is rebuilt
exactly the way `ppo.train` builds it: the same `network_factory` config on the
same env, then `make_inference_fn(params, deterministic=True)`.
"""
from __future__ import annotations
import os
import pickle
from pathlib import Path
import numpy as np
def make_inference_fn(env, net_cfg: dict | None):
"""The `make_inference_fn` that `ppo.train(...)` returns, without training."""
from brax.training.acme import running_statistics
from brax.training.agents.ppo import networks as ppo_networks
ppo_network = ppo_networks.make_ppo_networks(
observation_size=env.observation_size,
action_size=env.action_size,
preprocess_observations_fn=running_statistics.normalize,
**dict(net_cfg or {}),
)
return ppo_networks.make_inference_fn(ppo_network)
def load_params(path: Path):
with open(path, "rb") as f:
params = pickle.load(f)
# policy_params_fn and the final return both give (normalizer, policy, value);
# the inference fn wants the first two.
return tuple(params)[:2] if len(params) == 3 else params
def apply_snow(env, friction: float, depth: float) -> None:
"""Fixed snow on a single (non-randomized) Playground env: same foot-floor
contact-pair parameters `snow_randomizer` draws per env, then rebuild the
MJX model like `apply_terrain` does."""
from mujoco import mjx
from himalaya_terrain import snow_params
mj = env._mj_model
solref, solimp = snow_params(float(depth))
mj.pair_friction[0:2, 0:2] = friction
mj.pair_solref[0:2] = np.asarray(solref)
mj.pair_solimp[0:2] = np.asarray(solimp)
floor = mj.geom("floor").id # make it look like snow too
mj.geom_matid[floor] = -1
mj.geom_rgba[floor] = (0.92, 0.94, 0.98, 1.0)
env._mjx_model = mjx.put_model(mj, impl=env._config.impl)
def rollout(env, inference_fn, seconds: float = 8.0, command=(0.5, 0.0, 0.0), seed: int = 0):
"""Run the policy for `seconds` with a fixed joystick command. Returns
(qpos, info): qpos is a (T, nq) trajectory at the env's control rate, info
has distance walked and when (if) it fell."""
import jax
import jax.numpy as jnp
jit_reset = jax.jit(env.reset)
jit_step = jax.jit(env.step)
jit_policy = jax.jit(inference_fn)
rng = jax.random.PRNGKey(seed)
state = jit_reset(rng)
# The joystick task steers by a "command" in info; the get-up task has no
# such key, and adding one would change the State pytree the step function
# was traced against.
steerable = "command" in state.info
cmd = jnp.asarray(command, dtype=jnp.float32)
if steerable:
state.info["command"] = cmd
n_steps = int(seconds / env.dt)
qpos = [np.asarray(state.data.qpos)]
fell_at = None
for i in range(n_steps):
rng, key = jax.random.split(rng)
act, _ = jit_policy(state.obs, key)
state = jit_step(state, act)
if steerable:
state.info["command"] = cmd # joystick envs resample on reset only
qpos.append(np.asarray(state.data.qpos))
if fell_at is None and float(state.done) > 0.5:
fell_at = (i + 1) * env.dt
break
qpos = np.stack(qpos)
info = {"seconds": len(qpos) * env.dt,
"distance_m": float(np.linalg.norm((qpos[-1, :2] - qpos[0, :2]))),
"start_height_m": float(qpos[0, 2]),
"end_height_m": float(qpos[-1, 2]),
"peak_height_m": float(qpos[:, 2].max()),
"fell_at": fell_at}
return qpos, info
def offscreen_gl_backend() -> str | None:
"""Which MuJoCo offscreen backend this machine can actually load. The
backend is fixed when `mujoco` is imported, so rendering happens in a
subprocess with MUJOCO_GL set to this."""
import ctypes.util
import sys
forced = os.environ.get("ROLLOUT_GL")
if forced:
return forced
if sys.platform == "darwin":
return "glfw"
if ctypes.util.find_library("EGL"):
return "egl"
if ctypes.util.find_library("OSMesa"):
return "osmesa"
return None
def write_video(env, qpos: np.ndarray, path: Path, fps: float,
width: int = 640, height: int = 480, camera: str = "track") -> Path:
"""Render a qpos trajectory of `env` to MP4 in a subprocess (see
render_worker.py), so a missing or different GL backend can never take the
training process down."""
import subprocess
import sys
import tempfile
backend = offscreen_gl_backend()
if backend is None:
raise RuntimeError("no offscreen GL backend (libEGL / libOSMesa) in this container -- "
"add `libegl1 libosmesa6` to packages.txt")
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory() as tmp:
import mujoco
mjb = Path(tmp) / "model.mjb"
mujoco.mj_saveModel(env._mj_model, str(mjb), None)
traj = Path(tmp) / "qpos.npy"
np.save(traj, qpos)
worker = Path(__file__).with_name("render_worker.py")
env_vars = dict(os.environ, MUJOCO_GL=backend, PYOPENGL_PLATFORM=backend)
r = subprocess.run([sys.executable, str(worker), str(mjb), str(traj), str(path),
str(fps), str(width), str(height), camera],
env=env_vars, capture_output=True, text=True, timeout=600)
if r.returncode != 0:
tail = (r.stderr or r.stdout).strip().splitlines()[-6:]
raise RuntimeError(f"render ({backend}) failed: " + " | ".join(tail))
return path
|