simrig 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simrig/__init__.py +82 -0
- simrig/_version.py +3 -0
- simrig/browser_render.py +145 -0
- simrig/browser_shell.py +130 -0
- simrig/cli.py +412 -0
- simrig/core.py +144 -0
- simrig/custom_env.py +109 -0
- simrig/huggingface.py +78 -0
- simrig/io.py +49 -0
- simrig/live_view.py +553 -0
- simrig/model_view.py +944 -0
- simrig/mujoco_backend.py +197 -0
- simrig/paths.py +54 -0
- simrig/playground_backend.py +603 -0
- simrig/presets.py +93 -0
- simrig/preview.py +956 -0
- simrig/rendering.py +127 -0
- simrig/scaffold.py +127 -0
- simrig/three_scene.py +107 -0
- simrig/validate_env.py +211 -0
- simrig-0.2.2.dist-info/METADATA +238 -0
- simrig-0.2.2.dist-info/RECORD +26 -0
- simrig-0.2.2.dist-info/WHEEL +5 -0
- simrig-0.2.2.dist-info/entry_points.txt +2 -0
- simrig-0.2.2.dist-info/licenses/LICENSE +21 -0
- simrig-0.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""MuJoCo Playground environment backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from simrig.core import BackendInfo, EnvInspectionReport, RunConfig, SmokeResult, TrainabilityStatus
|
|
13
|
+
from simrig.custom_env import is_env_module_path, load_custom_env, resolve_env_label
|
|
14
|
+
from simrig.io import default_run_dir, save_json
|
|
15
|
+
from simrig.paths import find_menagerie
|
|
16
|
+
from simrig.presets import hidden_sizes, preset, resolve_small_network
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _import_registry():
|
|
20
|
+
try:
|
|
21
|
+
from mujoco_playground._src import registry # type: ignore
|
|
22
|
+
except ImportError as exc:
|
|
23
|
+
raise RuntimeError(
|
|
24
|
+
"MuJoCo Playground is not installed. Install SimRig with the playground "
|
|
25
|
+
"extra, or install the `playground` package used by MuJoCo Playground."
|
|
26
|
+
) from exc
|
|
27
|
+
return registry
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _import_training_deps():
|
|
31
|
+
try:
|
|
32
|
+
import jax # type: ignore
|
|
33
|
+
import jax.numpy as jp # type: ignore
|
|
34
|
+
_patch_jax_brax_compat(jax)
|
|
35
|
+
from brax.io import model as brax_model # type: ignore
|
|
36
|
+
from brax.training.acme import running_statistics # type: ignore
|
|
37
|
+
from brax.training.agents.ppo import networks as ppo_networks # type: ignore
|
|
38
|
+
from brax.training.agents.ppo import train as ppo # type: ignore
|
|
39
|
+
from mujoco_playground import wrapper # type: ignore
|
|
40
|
+
except ImportError as exc:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
"Training dependencies are not installed. Install JAX, Brax, and "
|
|
43
|
+
"MuJoCo Playground before running train/eval commands."
|
|
44
|
+
) from exc
|
|
45
|
+
return jax, jp, brax_model, running_statistics, ppo_networks, ppo, wrapper
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _patch_jax_brax_compat(jax: Any) -> None:
|
|
49
|
+
"""Restore APIs Brax still calls that newer JAX hid behind AttributeError.
|
|
50
|
+
|
|
51
|
+
JAX 0.10+ makes ``jax.device_put_replicated`` raise, while Brax 0.14 PPO
|
|
52
|
+
still calls it. Re-bind the private implementation when missing.
|
|
53
|
+
"""
|
|
54
|
+
if "device_put_replicated" not in jax.__dict__:
|
|
55
|
+
try:
|
|
56
|
+
from jax._src.api import device_put_replicated as _device_put_replicated # type: ignore
|
|
57
|
+
except ImportError:
|
|
58
|
+
pass
|
|
59
|
+
else:
|
|
60
|
+
jax.device_put_replicated = _device_put_replicated # type: ignore[attr-defined]
|
|
61
|
+
|
|
62
|
+
if "device_put_sharded" not in jax.__dict__:
|
|
63
|
+
try:
|
|
64
|
+
from jax._src.api import device_put_sharded as _device_put_sharded # type: ignore
|
|
65
|
+
except ImportError:
|
|
66
|
+
pass
|
|
67
|
+
else:
|
|
68
|
+
jax.device_put_sharded = _device_put_sharded # type: ignore[attr-defined]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def backend_info() -> BackendInfo:
|
|
72
|
+
try:
|
|
73
|
+
registry = _import_registry()
|
|
74
|
+
except RuntimeError as exc:
|
|
75
|
+
return BackendInfo(name="mujoco-playground", available=False, detail=str(exc))
|
|
76
|
+
return BackendInfo(
|
|
77
|
+
name="mujoco-playground",
|
|
78
|
+
available=True,
|
|
79
|
+
detail=f"{len(registry.ALL_ENVS)} registered envs",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def list_envs(backend: str = "mujoco-playground") -> list[dict[str, Any]]:
|
|
84
|
+
"""List MuJoCo Playground environments."""
|
|
85
|
+
_validate_backend(backend)
|
|
86
|
+
registry = _import_registry()
|
|
87
|
+
return [{"name": name, "backend": backend} for name in sorted(registry.ALL_ENVS)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def load_env(env_name: str, config_overrides: dict[str, Any] | None = None):
|
|
91
|
+
"""Load a Playground registry env or a custom ``*.py`` env module."""
|
|
92
|
+
if is_env_module_path(env_name):
|
|
93
|
+
return load_custom_env(env_name, config_overrides=config_overrides)
|
|
94
|
+
|
|
95
|
+
_configure_menagerie()
|
|
96
|
+
registry = _import_registry()
|
|
97
|
+
config_overrides = _default_overrides(registry, env_name, config_overrides)
|
|
98
|
+
return registry.load(env_name, config_overrides=config_overrides)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def inspect_env(env_name: str, *, backend: str = "mujoco-playground") -> EnvInspectionReport:
|
|
102
|
+
"""Load a Playground or custom env and report trainability metadata."""
|
|
103
|
+
_validate_backend(backend)
|
|
104
|
+
label = resolve_env_label(env_name)
|
|
105
|
+
|
|
106
|
+
if is_env_module_path(env_name):
|
|
107
|
+
return _inspect_custom_env(env_name, label=label, backend=backend)
|
|
108
|
+
|
|
109
|
+
registry = _import_registry()
|
|
110
|
+
if env_name not in registry.ALL_ENVS:
|
|
111
|
+
return EnvInspectionReport(
|
|
112
|
+
name=label,
|
|
113
|
+
backend=backend,
|
|
114
|
+
status=TrainabilityStatus.FAILED,
|
|
115
|
+
available=False,
|
|
116
|
+
loaded=False,
|
|
117
|
+
errors=[f"Unknown environment: {env_name}"],
|
|
118
|
+
notes=[
|
|
119
|
+
f"Available environments: {', '.join(sorted(registry.ALL_ENVS))}",
|
|
120
|
+
"Or pass a custom env module path ending in .py",
|
|
121
|
+
],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
env = load_env(env_name)
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
return EnvInspectionReport(
|
|
128
|
+
name=label,
|
|
129
|
+
backend=backend,
|
|
130
|
+
status=TrainabilityStatus.FAILED,
|
|
131
|
+
available=True,
|
|
132
|
+
loaded=False,
|
|
133
|
+
errors=[str(exc)],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
mj_model = getattr(env, "mj_model", None)
|
|
137
|
+
xml_path = getattr(env, "xml_path", None)
|
|
138
|
+
try:
|
|
139
|
+
randomizer = registry.get_domain_randomizer(env_name)
|
|
140
|
+
except Exception:
|
|
141
|
+
randomizer = None
|
|
142
|
+
return EnvInspectionReport(
|
|
143
|
+
name=label,
|
|
144
|
+
backend=backend,
|
|
145
|
+
status=TrainabilityStatus.TRAINABLE_EXISTING_ENV,
|
|
146
|
+
available=True,
|
|
147
|
+
loaded=True,
|
|
148
|
+
observation_size=getattr(env, "observation_size", None),
|
|
149
|
+
action_size=int(getattr(env, "action_size", 0) or 0),
|
|
150
|
+
xml_path=str(xml_path) if xml_path else None,
|
|
151
|
+
model_bodies=int(mj_model.nbody) if mj_model is not None else None,
|
|
152
|
+
model_actuators=int(mj_model.nu) if mj_model is not None else None,
|
|
153
|
+
has_domain_randomizer=randomizer is not None,
|
|
154
|
+
notes=[
|
|
155
|
+
"Existing MuJoCo Playground envs are trainable because they define "
|
|
156
|
+
"reset, step, reward, observations, and termination."
|
|
157
|
+
],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _inspect_custom_env(env_name: str, *, label: str, backend: str) -> EnvInspectionReport:
|
|
162
|
+
try:
|
|
163
|
+
env = load_env(env_name)
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
return EnvInspectionReport(
|
|
166
|
+
name=label,
|
|
167
|
+
backend=backend,
|
|
168
|
+
status=TrainabilityStatus.NEEDS_CUSTOM_ENV,
|
|
169
|
+
available=True,
|
|
170
|
+
loaded=False,
|
|
171
|
+
errors=[str(exc)],
|
|
172
|
+
notes=[
|
|
173
|
+
"Custom env module failed to load. Finish implementation, then "
|
|
174
|
+
"run `simrig validate-env PATH --runtime` and `simrig smoke PATH`."
|
|
175
|
+
],
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
mj_model = getattr(env, "mj_model", None)
|
|
179
|
+
xml_path = getattr(env, "xml_path", None)
|
|
180
|
+
warnings = _obs_key_warnings(getattr(env, "observation_size", None))
|
|
181
|
+
return EnvInspectionReport(
|
|
182
|
+
name=label,
|
|
183
|
+
backend=backend,
|
|
184
|
+
status=TrainabilityStatus.TRAINABLE_EXISTING_ENV,
|
|
185
|
+
available=True,
|
|
186
|
+
loaded=True,
|
|
187
|
+
observation_size=getattr(env, "observation_size", None),
|
|
188
|
+
action_size=int(getattr(env, "action_size", 0) or 0),
|
|
189
|
+
xml_path=str(xml_path) if xml_path else None,
|
|
190
|
+
model_bodies=int(mj_model.nbody) if mj_model is not None else None,
|
|
191
|
+
model_actuators=int(mj_model.nu) if mj_model is not None else None,
|
|
192
|
+
has_domain_randomizer=False,
|
|
193
|
+
warnings=warnings,
|
|
194
|
+
notes=[
|
|
195
|
+
f"Custom env module: {env_name}",
|
|
196
|
+
"Run `simrig smoke` before `simrig train`.",
|
|
197
|
+
"SimRig PPO defaults expect observation keys `state` and `privileged_state`.",
|
|
198
|
+
],
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _obs_key_warnings(observation_size: Any) -> list[str]:
|
|
203
|
+
warnings: list[str] = []
|
|
204
|
+
if observation_size is None:
|
|
205
|
+
warnings.append("observation_size is missing.")
|
|
206
|
+
return warnings
|
|
207
|
+
if isinstance(observation_size, dict):
|
|
208
|
+
if "state" not in observation_size:
|
|
209
|
+
warnings.append("observation_size is missing key `state` (policy obs).")
|
|
210
|
+
if "privileged_state" not in observation_size:
|
|
211
|
+
warnings.append(
|
|
212
|
+
"observation_size is missing key `privileged_state` (value obs). "
|
|
213
|
+
"SimRig PPO defaults expect it."
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
warnings.append(
|
|
217
|
+
"observation_size is flat; SimRig PPO defaults expect dict keys "
|
|
218
|
+
"`state` and `privileged_state`."
|
|
219
|
+
)
|
|
220
|
+
return warnings
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def smoke_env(
|
|
224
|
+
env_name: str,
|
|
225
|
+
*,
|
|
226
|
+
steps: int = 10,
|
|
227
|
+
backend: str = "mujoco-playground",
|
|
228
|
+
seed: int = 0,
|
|
229
|
+
) -> SmokeResult:
|
|
230
|
+
"""Run a short reset/zero-action step test."""
|
|
231
|
+
_validate_backend(backend)
|
|
232
|
+
label = resolve_env_label(env_name)
|
|
233
|
+
jax, jp, *_ = _import_training_deps()
|
|
234
|
+
try:
|
|
235
|
+
env = load_env(env_name)
|
|
236
|
+
reset = jax.jit(env.reset)
|
|
237
|
+
step = jax.jit(env.step)
|
|
238
|
+
state = reset(jax.random.PRNGKey(seed))
|
|
239
|
+
completed = 0
|
|
240
|
+
for completed in range(1, steps + 1):
|
|
241
|
+
state = step(state, jp.zeros(env.action_size))
|
|
242
|
+
return SmokeResult(
|
|
243
|
+
env_name=label,
|
|
244
|
+
backend=backend,
|
|
245
|
+
steps_requested=steps,
|
|
246
|
+
steps_completed=completed,
|
|
247
|
+
passed=True,
|
|
248
|
+
action_size=int(env.action_size),
|
|
249
|
+
observation_size=getattr(env, "observation_size", None),
|
|
250
|
+
final_reward=float(state.reward),
|
|
251
|
+
final_done=bool(state.done),
|
|
252
|
+
)
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
return SmokeResult(
|
|
255
|
+
env_name=label,
|
|
256
|
+
backend=backend,
|
|
257
|
+
steps_requested=steps,
|
|
258
|
+
steps_completed=0,
|
|
259
|
+
passed=False,
|
|
260
|
+
errors=[str(exc)],
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def train_ppo(
|
|
265
|
+
env_name: str,
|
|
266
|
+
*,
|
|
267
|
+
preset_name: str = "smoke",
|
|
268
|
+
output: Path | str | None = None,
|
|
269
|
+
backend: str = "mujoco-playground",
|
|
270
|
+
overrides: dict[str, Any] | None = None,
|
|
271
|
+
) -> RunConfig:
|
|
272
|
+
"""Train a Playground or custom env module with Brax PPO."""
|
|
273
|
+
_validate_backend(backend)
|
|
274
|
+
jax, jp, brax_model, _, ppo_networks, ppo, wrapper = _import_training_deps()
|
|
275
|
+
del jax, jp
|
|
276
|
+
label = resolve_env_label(env_name)
|
|
277
|
+
config = preset(preset_name)
|
|
278
|
+
config.update(overrides or {})
|
|
279
|
+
output_dir = Path(output) if output is not None else default_run_dir(label, preset_name)
|
|
280
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
# Orbax requires absolute checkpoint paths.
|
|
282
|
+
output_dir = output_dir.resolve()
|
|
283
|
+
checkpoint_dir = output_dir / "checkpoints"
|
|
284
|
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
285
|
+
env = load_env(env_name, config_overrides={"impl": config.get("impl", "jax")})
|
|
286
|
+
sizes = hidden_sizes(bool(config["small_network"]))
|
|
287
|
+
network_factory = functools.partial(
|
|
288
|
+
ppo_networks.make_ppo_networks,
|
|
289
|
+
policy_hidden_layer_sizes=sizes,
|
|
290
|
+
value_hidden_layer_sizes=sizes,
|
|
291
|
+
policy_obs_key="state",
|
|
292
|
+
value_obs_key="privileged_state",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def progress(num_steps: int, metrics: dict[str, Any]) -> None:
|
|
296
|
+
reward = float(metrics.get("eval/episode_reward", 0.0))
|
|
297
|
+
length = float(metrics.get("eval/avg_episode_length", 0.0))
|
|
298
|
+
print(f"steps={num_steps:,} eval_reward={reward:.3f} eval_length={length:.1f}")
|
|
299
|
+
|
|
300
|
+
run_config = RunConfig(
|
|
301
|
+
env_name=label,
|
|
302
|
+
backend=backend,
|
|
303
|
+
preset=preset_name,
|
|
304
|
+
output_dir=str(output_dir),
|
|
305
|
+
config={**config, "env_ref": str(env_name)},
|
|
306
|
+
command=[
|
|
307
|
+
sys.executable,
|
|
308
|
+
"-m",
|
|
309
|
+
"simrig.cli",
|
|
310
|
+
"train",
|
|
311
|
+
str(env_name),
|
|
312
|
+
"--preset",
|
|
313
|
+
preset_name,
|
|
314
|
+
"--output",
|
|
315
|
+
str(output_dir),
|
|
316
|
+
],
|
|
317
|
+
)
|
|
318
|
+
save_json(output_dir / "config.json", run_config)
|
|
319
|
+
_, params, metrics = ppo.train(
|
|
320
|
+
environment=env,
|
|
321
|
+
wrap_env_fn=wrapper.wrap_for_brax_training,
|
|
322
|
+
num_timesteps=config["timesteps"],
|
|
323
|
+
num_evals=config["num_evals"],
|
|
324
|
+
num_eval_envs=config["num_eval_envs"],
|
|
325
|
+
episode_length=config["episode_length"],
|
|
326
|
+
normalize_observations=True,
|
|
327
|
+
action_repeat=1,
|
|
328
|
+
unroll_length=config["unroll_length"],
|
|
329
|
+
num_minibatches=config["num_minibatches"],
|
|
330
|
+
num_updates_per_batch=config["num_updates_per_batch"],
|
|
331
|
+
discounting=config["discounting"],
|
|
332
|
+
learning_rate=config["learning_rate"],
|
|
333
|
+
entropy_cost=config["entropy_cost"],
|
|
334
|
+
num_envs=config["num_envs"],
|
|
335
|
+
batch_size=config["batch_size"],
|
|
336
|
+
network_factory=network_factory,
|
|
337
|
+
progress_fn=progress,
|
|
338
|
+
save_checkpoint_path=str(checkpoint_dir),
|
|
339
|
+
)
|
|
340
|
+
brax_model.save_params(str(output_dir / "policy.params"), params)
|
|
341
|
+
save_json(output_dir / "final_metrics.json", metrics)
|
|
342
|
+
return run_config
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def eval_policy(
|
|
346
|
+
checkpoint: Path | str,
|
|
347
|
+
*,
|
|
348
|
+
env_name: str,
|
|
349
|
+
steps: int = 500,
|
|
350
|
+
backend: str = "mujoco-playground",
|
|
351
|
+
small_network: bool | None = None,
|
|
352
|
+
seed: int = 0,
|
|
353
|
+
command: tuple[float, ...] | None = None,
|
|
354
|
+
) -> dict[str, Any]:
|
|
355
|
+
"""Headless deterministic policy rollout."""
|
|
356
|
+
_validate_backend(backend)
|
|
357
|
+
jax, jp, brax_model, running_statistics, ppo_networks, *_ = _import_training_deps()
|
|
358
|
+
env = load_env(env_name)
|
|
359
|
+
sizes = hidden_sizes(resolve_small_network(checkpoint, small_network=small_network))
|
|
360
|
+
network_factory = functools.partial(
|
|
361
|
+
ppo_networks.make_ppo_networks,
|
|
362
|
+
policy_hidden_layer_sizes=sizes,
|
|
363
|
+
value_hidden_layer_sizes=sizes,
|
|
364
|
+
policy_obs_key="state",
|
|
365
|
+
value_obs_key="privileged_state",
|
|
366
|
+
)
|
|
367
|
+
networks = network_factory(
|
|
368
|
+
env.observation_size,
|
|
369
|
+
env.action_size,
|
|
370
|
+
preprocess_observations_fn=running_statistics.normalize,
|
|
371
|
+
)
|
|
372
|
+
params = brax_model.load_params(str(checkpoint))
|
|
373
|
+
policy = jax.jit(ppo_networks.make_inference_fn(networks)(params, deterministic=True))
|
|
374
|
+
reset = jax.jit(env.reset)
|
|
375
|
+
step = jax.jit(env.step)
|
|
376
|
+
rng = jax.random.PRNGKey(seed)
|
|
377
|
+
state = reset(rng)
|
|
378
|
+
command_applied = False
|
|
379
|
+
if command is not None:
|
|
380
|
+
state, command_applied = _apply_command(env, state, jp.asarray(command))
|
|
381
|
+
if not command_applied:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f"Environment {resolve_env_label(env_name)} does not expose command-like state."
|
|
384
|
+
)
|
|
385
|
+
total_reward = 0.0
|
|
386
|
+
completed = 0
|
|
387
|
+
for completed in range(1, steps + 1):
|
|
388
|
+
if command is not None:
|
|
389
|
+
state, command_applied = _apply_command(env, state, jp.asarray(command))
|
|
390
|
+
rng, action_rng = jax.random.split(rng)
|
|
391
|
+
action, _ = policy(state.obs, action_rng)
|
|
392
|
+
state = step(state, action)
|
|
393
|
+
total_reward += float(state.reward)
|
|
394
|
+
if bool(state.done):
|
|
395
|
+
break
|
|
396
|
+
return {
|
|
397
|
+
"env_name": resolve_env_label(env_name),
|
|
398
|
+
"backend": backend,
|
|
399
|
+
"checkpoint": str(checkpoint),
|
|
400
|
+
"seed": seed,
|
|
401
|
+
"command": list(command) if command is not None else None,
|
|
402
|
+
"command_applied": command_applied,
|
|
403
|
+
"steps_requested": steps,
|
|
404
|
+
"steps_completed": completed,
|
|
405
|
+
"total_reward": total_reward,
|
|
406
|
+
"average_reward": total_reward / max(completed, 1),
|
|
407
|
+
"terminated": bool(state.done),
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def demo_policy(
|
|
412
|
+
checkpoint: Path | str,
|
|
413
|
+
*,
|
|
414
|
+
env_name: str,
|
|
415
|
+
steps: int = 5000,
|
|
416
|
+
backend: str = "mujoco-playground",
|
|
417
|
+
small_network: bool | None = None,
|
|
418
|
+
seed: int = 0,
|
|
419
|
+
command: tuple[float, ...] | None = None,
|
|
420
|
+
speed: float = 1.0,
|
|
421
|
+
camera_distance: float | None = None,
|
|
422
|
+
) -> dict[str, Any]:
|
|
423
|
+
"""Run a trained policy in a desktop MuJoCo viewer."""
|
|
424
|
+
_validate_backend(backend)
|
|
425
|
+
jax, jp, brax_model, running_statistics, ppo_networks, *_ = _import_training_deps()
|
|
426
|
+
try:
|
|
427
|
+
import mujoco # type: ignore
|
|
428
|
+
from gymnasium.envs.mujoco.mujoco_rendering import WindowViewer # type: ignore
|
|
429
|
+
except ImportError as exc:
|
|
430
|
+
raise RuntimeError(
|
|
431
|
+
"Interactive demo requires MuJoCo and Gymnasium's MuJoCo viewer."
|
|
432
|
+
) from exc
|
|
433
|
+
|
|
434
|
+
env = load_env(env_name)
|
|
435
|
+
sizes = hidden_sizes(resolve_small_network(checkpoint, small_network=small_network))
|
|
436
|
+
network_factory = functools.partial(
|
|
437
|
+
ppo_networks.make_ppo_networks,
|
|
438
|
+
policy_hidden_layer_sizes=sizes,
|
|
439
|
+
value_hidden_layer_sizes=sizes,
|
|
440
|
+
policy_obs_key="state",
|
|
441
|
+
value_obs_key="privileged_state",
|
|
442
|
+
)
|
|
443
|
+
networks = network_factory(
|
|
444
|
+
env.observation_size,
|
|
445
|
+
env.action_size,
|
|
446
|
+
preprocess_observations_fn=running_statistics.normalize,
|
|
447
|
+
)
|
|
448
|
+
params = brax_model.load_params(str(checkpoint))
|
|
449
|
+
policy = jax.jit(ppo_networks.make_inference_fn(networks)(params, deterministic=True))
|
|
450
|
+
reset = jax.jit(env.reset)
|
|
451
|
+
step = jax.jit(env.step)
|
|
452
|
+
rng = jax.random.PRNGKey(seed)
|
|
453
|
+
state = reset(rng)
|
|
454
|
+
command_applied = False
|
|
455
|
+
if command is not None:
|
|
456
|
+
state, command_applied = _apply_command(env, state, jp.asarray(command))
|
|
457
|
+
|
|
458
|
+
mj_data = mujoco.MjData(env.mj_model)
|
|
459
|
+
viewer = WindowViewer(env.mj_model, mj_data, width=None, height=None, max_geom=1000)
|
|
460
|
+
if camera_distance is not None:
|
|
461
|
+
viewer.cam.distance = camera_distance
|
|
462
|
+
|
|
463
|
+
total_reward = 0.0
|
|
464
|
+
completed = 0
|
|
465
|
+
try:
|
|
466
|
+
for completed in range(1, steps + 1):
|
|
467
|
+
if command is not None:
|
|
468
|
+
state, command_applied = _apply_command(env, state, jp.asarray(command))
|
|
469
|
+
rng, action_rng = jax.random.split(rng)
|
|
470
|
+
action, _ = policy(state.obs, action_rng)
|
|
471
|
+
state = step(state, action)
|
|
472
|
+
total_reward += float(state.reward)
|
|
473
|
+
|
|
474
|
+
mj_data.qpos = state.data.qpos
|
|
475
|
+
mj_data.qvel = state.data.qvel
|
|
476
|
+
_copy_mocap_state(state.data, mj_data)
|
|
477
|
+
mujoco.mj_forward(env.mj_model, mj_data)
|
|
478
|
+
viewer.data = mj_data
|
|
479
|
+
viewer.add_overlay(
|
|
480
|
+
mujoco.mjtGridPos.mjGRID_TOPRIGHT,
|
|
481
|
+
"SimRig demo\nEnv\nReward\nSteps",
|
|
482
|
+
f"\n{resolve_env_label(env_name)}\n{float(state.reward):.4f}\n{completed}",
|
|
483
|
+
)
|
|
484
|
+
viewer.render()
|
|
485
|
+
time.sleep(max(0.0, env.dt / max(speed, 1e-6) - 0.001))
|
|
486
|
+
if viewer.window is None or bool(state.done):
|
|
487
|
+
break
|
|
488
|
+
finally:
|
|
489
|
+
viewer.close()
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
"env_name": resolve_env_label(env_name),
|
|
493
|
+
"backend": backend,
|
|
494
|
+
"checkpoint": str(checkpoint),
|
|
495
|
+
"steps_requested": steps,
|
|
496
|
+
"steps_completed": completed,
|
|
497
|
+
"total_reward": total_reward,
|
|
498
|
+
"average_reward": total_reward / max(completed, 1),
|
|
499
|
+
"terminated": bool(state.done),
|
|
500
|
+
"command": list(command) if command is not None else None,
|
|
501
|
+
"command_applied": command_applied,
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _validate_backend(backend: str) -> None:
|
|
506
|
+
if backend != "mujoco-playground":
|
|
507
|
+
raise ValueError(
|
|
508
|
+
f"Unsupported backend for v0: {backend}. "
|
|
509
|
+
"SimRig v0 supports only mujoco-playground training envs."
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _copy_mocap_state(source_data: Any, target_data: Any) -> None:
|
|
514
|
+
"""Copy optional MJX mocap state into native MuJoCo render data."""
|
|
515
|
+
for name in ("mocap_pos", "mocap_quat"):
|
|
516
|
+
source = getattr(source_data, name, None)
|
|
517
|
+
target = getattr(target_data, name, None)
|
|
518
|
+
if source is not None and target is not None:
|
|
519
|
+
target[:] = source
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _apply_command(env: Any, state: Any, command: Any) -> tuple[Any, bool]:
|
|
523
|
+
if hasattr(env, "set_command"):
|
|
524
|
+
return env.set_command(state, command), True
|
|
525
|
+
info = dict(getattr(state, "info", {}))
|
|
526
|
+
if "command" not in info:
|
|
527
|
+
return state, False
|
|
528
|
+
info["command"] = command
|
|
529
|
+
for key in ("steps_until_next_cmd", "steps_until_next_command"):
|
|
530
|
+
if key in info:
|
|
531
|
+
try:
|
|
532
|
+
import jax.numpy as jp # type: ignore
|
|
533
|
+
|
|
534
|
+
info[key] = jp.iinfo(jp.int32).max
|
|
535
|
+
except Exception:
|
|
536
|
+
pass
|
|
537
|
+
obs = _rebuild_observation(env, state, info)
|
|
538
|
+
if obs is None:
|
|
539
|
+
return state.replace(info=info), True
|
|
540
|
+
return state.replace(info=info, obs=obs), True
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _rebuild_observation(env: Any, state: Any, info: dict[str, Any]) -> Any | None:
|
|
544
|
+
target = getattr(env, "unwrapped", env)
|
|
545
|
+
get_obs = getattr(target, "_get_obs", None)
|
|
546
|
+
if get_obs is None:
|
|
547
|
+
get_obs = getattr(env, "_get_obs", None)
|
|
548
|
+
if get_obs is None:
|
|
549
|
+
return None
|
|
550
|
+
try:
|
|
551
|
+
return get_obs(state.data, info)
|
|
552
|
+
except TypeError:
|
|
553
|
+
# Some locomotion envs require contact flags as a third argument.
|
|
554
|
+
try:
|
|
555
|
+
import jax.numpy as jp # type: ignore
|
|
556
|
+
|
|
557
|
+
foot_sensor_ids = getattr(target, "_feet_floor_found_sensor", None)
|
|
558
|
+
mj_model = getattr(target, "_mj_model", getattr(target, "mj_model", None))
|
|
559
|
+
if foot_sensor_ids is None or mj_model is None:
|
|
560
|
+
return None
|
|
561
|
+
contact = jp.array(
|
|
562
|
+
[
|
|
563
|
+
state.data.sensordata[mj_model.sensor_adr[sensor_id]] > 0
|
|
564
|
+
for sensor_id in foot_sensor_ids
|
|
565
|
+
]
|
|
566
|
+
)
|
|
567
|
+
return get_obs(state.data, info, contact)
|
|
568
|
+
except Exception:
|
|
569
|
+
return None
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _default_overrides(
|
|
573
|
+
registry: Any,
|
|
574
|
+
env_name: str,
|
|
575
|
+
config_overrides: dict[str, Any] | None,
|
|
576
|
+
) -> dict[str, Any] | None:
|
|
577
|
+
overrides = dict(config_overrides or {})
|
|
578
|
+
if "impl" in overrides:
|
|
579
|
+
return overrides
|
|
580
|
+
try:
|
|
581
|
+
config = registry.get_default_config(env_name)
|
|
582
|
+
except Exception:
|
|
583
|
+
return overrides or None
|
|
584
|
+
if "impl" in config:
|
|
585
|
+
overrides["impl"] = "jax"
|
|
586
|
+
return overrides or None
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _configure_menagerie() -> None:
|
|
590
|
+
"""Point MuJoCo Playground at an existing local Menagerie when possible."""
|
|
591
|
+
try:
|
|
592
|
+
menagerie = find_menagerie()
|
|
593
|
+
except FileNotFoundError:
|
|
594
|
+
return
|
|
595
|
+
os.environ.setdefault("MUJOCO_MENAGERIE_PATH", str(menagerie))
|
|
596
|
+
try:
|
|
597
|
+
from etils import epath # type: ignore
|
|
598
|
+
from mujoco_playground._src import mjx_env # type: ignore
|
|
599
|
+
|
|
600
|
+
mjx_env.MENAGERIE_PATH = epath.Path(menagerie)
|
|
601
|
+
except Exception:
|
|
602
|
+
# Loading may still work if the backend uses the environment variable.
|
|
603
|
+
return
|
simrig/presets.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Training presets shared by CLI and library calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
PRESETS: dict[str, dict[str, Any]] = {
|
|
11
|
+
"smoke": {
|
|
12
|
+
"timesteps": 4096,
|
|
13
|
+
"num_envs": 16,
|
|
14
|
+
"num_eval_envs": 1,
|
|
15
|
+
"num_evals": 2,
|
|
16
|
+
"episode_length": 100,
|
|
17
|
+
"batch_size": 16,
|
|
18
|
+
"unroll_length": 10,
|
|
19
|
+
"num_minibatches": 2,
|
|
20
|
+
"num_updates_per_batch": 1,
|
|
21
|
+
"discounting": 0.97,
|
|
22
|
+
"learning_rate": 3e-4,
|
|
23
|
+
"entropy_cost": 1e-2,
|
|
24
|
+
"small_network": True,
|
|
25
|
+
},
|
|
26
|
+
"local": {
|
|
27
|
+
"timesteps": 1_000_000,
|
|
28
|
+
"num_envs": 128,
|
|
29
|
+
"num_eval_envs": 4,
|
|
30
|
+
"num_evals": 10,
|
|
31
|
+
"episode_length": 1000,
|
|
32
|
+
"batch_size": 128,
|
|
33
|
+
"unroll_length": 20,
|
|
34
|
+
"num_minibatches": 32,
|
|
35
|
+
"num_updates_per_batch": 4,
|
|
36
|
+
"discounting": 0.97,
|
|
37
|
+
"learning_rate": 3e-4,
|
|
38
|
+
"entropy_cost": 1e-2,
|
|
39
|
+
"small_network": False,
|
|
40
|
+
},
|
|
41
|
+
"cloud": {
|
|
42
|
+
"timesteps": 200_000_000,
|
|
43
|
+
"num_envs": 8192,
|
|
44
|
+
"num_eval_envs": 128,
|
|
45
|
+
"num_evals": 20,
|
|
46
|
+
"episode_length": 1000,
|
|
47
|
+
"batch_size": 256,
|
|
48
|
+
"unroll_length": 20,
|
|
49
|
+
"num_minibatches": 32,
|
|
50
|
+
"num_updates_per_batch": 4,
|
|
51
|
+
"discounting": 0.97,
|
|
52
|
+
"learning_rate": 3e-4,
|
|
53
|
+
"entropy_cost": 1e-2,
|
|
54
|
+
"small_network": False,
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def preset(name: str) -> dict[str, Any]:
|
|
60
|
+
"""Return a mutable copy of a named preset."""
|
|
61
|
+
if name not in PRESETS:
|
|
62
|
+
choices = ", ".join(sorted(PRESETS))
|
|
63
|
+
raise ValueError(f"Unknown preset: {name}. Choose one of: {choices}")
|
|
64
|
+
return dict(PRESETS[name])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def hidden_sizes(small_network: bool) -> tuple[int, ...]:
|
|
68
|
+
return (64, 64) if small_network else (512, 256, 128)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_small_network(
|
|
72
|
+
checkpoint: Path | str,
|
|
73
|
+
*,
|
|
74
|
+
small_network: bool | None = None,
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""Resolve network size from an explicit flag or a sibling run config.json."""
|
|
77
|
+
if small_network is not None:
|
|
78
|
+
return small_network
|
|
79
|
+
|
|
80
|
+
config_path = Path(checkpoint).resolve().parent / "config.json"
|
|
81
|
+
if not config_path.exists():
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
86
|
+
except (OSError, json.JSONDecodeError):
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
config = data.get("config")
|
|
90
|
+
if isinstance(config, dict) and "small_network" in config:
|
|
91
|
+
return bool(config["small_network"])
|
|
92
|
+
return False
|
|
93
|
+
|