Copy mjwarp viewer s.t. MJX users can debug models effectively.
PiperOrigin-RevId: 792258628 Change-Id: I3b2b5d10414934866d5c647f30fe88e9cd1a8756
This commit is contained in:
committed by
Copybara-Service
parent
9e5fdd6983
commit
9b9b7030e7
+227
@@ -0,0 +1,227 @@
|
|||||||
|
# Copyright 2025 The Newton Developers
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
"""mjwarp-viewer: load and simulate an MJCF with MuJoCo Warp.
|
||||||
|
|
||||||
|
Usage: mjwarp-viewer <mjcf XML path> [flags]
|
||||||
|
|
||||||
|
Example:
|
||||||
|
mjwarp-viewer benchmark/humanoid/humanoid.xml -o "opt.solver=cg"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import enum
|
||||||
|
import logging
|
||||||
|
import pickle
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import mujoco
|
||||||
|
import mujoco.viewer
|
||||||
|
import numpy as np
|
||||||
|
import warp as wp
|
||||||
|
from absl import app
|
||||||
|
from absl import flags
|
||||||
|
from etils import epath
|
||||||
|
|
||||||
|
import mujoco_warp as mjw
|
||||||
|
|
||||||
|
|
||||||
|
class EngineOptions(enum.IntEnum):
|
||||||
|
WARP = 0
|
||||||
|
C = 1
|
||||||
|
|
||||||
|
|
||||||
|
_CLEAR_KERNEL_CACHE = flags.DEFINE_bool("clear_kernel_cache", False, "Clear kernel cache (to calculate full JIT time)")
|
||||||
|
_ENGINE = flags.DEFINE_enum_class("engine", EngineOptions.WARP, EngineOptions, "Simulation engine")
|
||||||
|
_NCONMAX = flags.DEFINE_integer("nconmax", None, "Maximum number of contacts.")
|
||||||
|
_NJMAX = flags.DEFINE_integer("njmax", None, "Maximum number of constraints per world.")
|
||||||
|
_OVERRIDE = flags.DEFINE_multi_string("override", [], "Model overrides (notation: foo.bar = baz)", short_name="o")
|
||||||
|
_DEVICE = flags.DEFINE_string("device", None, "override the default Warp device")
|
||||||
|
|
||||||
|
|
||||||
|
_VIEWER_GLOBAL_STATE = {"running": True, "step_once": False}
|
||||||
|
|
||||||
|
|
||||||
|
def key_callback(key: int) -> None:
|
||||||
|
if key == 32: # Space bar
|
||||||
|
_VIEWER_GLOBAL_STATE["running"] = not _VIEWER_GLOBAL_STATE["running"]
|
||||||
|
logging.info("RUNNING = %s", _VIEWER_GLOBAL_STATE["running"])
|
||||||
|
elif key == 46: # period
|
||||||
|
_VIEWER_GLOBAL_STATE["step_once"] = True
|
||||||
|
|
||||||
|
|
||||||
|
def _load_model(path: epath.Path) -> mujoco.MjModel:
|
||||||
|
if not path.exists():
|
||||||
|
resource_path = epath.resource_path("mujoco_warp") / path
|
||||||
|
if not resource_path.exists():
|
||||||
|
raise FileNotFoundError(f"file not found: {path}\nalso tried: {resource_path}")
|
||||||
|
path = resource_path
|
||||||
|
|
||||||
|
print(f"Loading model from: {path}...")
|
||||||
|
if path.suffix == ".mjb":
|
||||||
|
return mujoco.MjModel.from_binary_path(path.as_posix())
|
||||||
|
|
||||||
|
spec = mujoco.MjSpec.from_file(path.as_posix())
|
||||||
|
# check if the file has any mujoco.sdf test plugins
|
||||||
|
if any(p.plugin_name.startswith("mujoco.sdf") for p in spec.plugins):
|
||||||
|
from mujoco.mjx.third_party.mujoco_warp.test_data.collision_sdf.utils import register_sdf_plugins as register_sdf_plugins
|
||||||
|
|
||||||
|
register_sdf_plugins(mjw)
|
||||||
|
return spec.compile()
|
||||||
|
|
||||||
|
|
||||||
|
def _override(model: Union[mjw.Model, mujoco.MjModel]):
|
||||||
|
enum_fields = {
|
||||||
|
"opt.integrator": mjw.IntegratorType,
|
||||||
|
"opt.cone": mjw.ConeType,
|
||||||
|
"opt.solver": mjw.SolverType,
|
||||||
|
"opt.broadphase": mjw.BroadphaseType,
|
||||||
|
"opt.broadphase_filter": mjw.BroadphaseFilter,
|
||||||
|
}
|
||||||
|
for override in _OVERRIDE.value:
|
||||||
|
if "=" not in override:
|
||||||
|
raise app.UsageError(f"Invalid override format: {override}")
|
||||||
|
key, val = override.split("=", 1)
|
||||||
|
key, val = key.strip(), val.strip()
|
||||||
|
|
||||||
|
if key in enum_fields:
|
||||||
|
try:
|
||||||
|
val = str(enum_fields[key][val.upper()])
|
||||||
|
except KeyError:
|
||||||
|
raise app.UsageError(f"Unrecognized enum value: {val}")
|
||||||
|
|
||||||
|
obj, attrs = model, key.split(".")
|
||||||
|
for i, attr in enumerate(attrs):
|
||||||
|
if not hasattr(obj, attr):
|
||||||
|
raise app.UsageError(f"Unrecognized model field: {key}")
|
||||||
|
if i < len(attrs) - 1:
|
||||||
|
obj = getattr(obj, attr)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
val = type(getattr(obj, attr))(ast.literal_eval(val))
|
||||||
|
except (SyntaxError, ValueError):
|
||||||
|
raise app.UsageError(f"Unrecognized value for field: {key}")
|
||||||
|
|
||||||
|
setattr(obj, attr, val)
|
||||||
|
|
||||||
|
|
||||||
|
def _compile_step(m, d):
|
||||||
|
mjw.step(m, d)
|
||||||
|
# double warmup to work around issues with compilation during graph capture:
|
||||||
|
mjw.step(m, d)
|
||||||
|
# capture the whole step function as a CUDA graph
|
||||||
|
with wp.ScopedCapture() as capture:
|
||||||
|
mjw.step(m, d)
|
||||||
|
return capture.graph
|
||||||
|
|
||||||
|
|
||||||
|
def _main(argv: Sequence[str]) -> None:
|
||||||
|
"""Runs viewer app."""
|
||||||
|
if len(argv) < 2:
|
||||||
|
raise app.UsageError("Missing required input: mjcf path.")
|
||||||
|
elif len(argv) > 2:
|
||||||
|
raise app.UsageError("Too many command-line arguments.")
|
||||||
|
|
||||||
|
mjm = _load_model(epath.Path(argv[1]))
|
||||||
|
mjd = mujoco.MjData(mjm)
|
||||||
|
mujoco.mj_forward(mjm, mjd)
|
||||||
|
|
||||||
|
if _ENGINE.value == EngineOptions.C:
|
||||||
|
_override(mjm)
|
||||||
|
print(
|
||||||
|
f" nbody: {mjm.nbody} nv: {mjm.nv} ngeom: {mjm.ngeom} nu: {mjm.nu}\n"
|
||||||
|
f" solver: {mujoco.mjtSolver(mjm.opt.solver).name} cone: {mujoco.mjtCone(mjm.opt.cone).name}"
|
||||||
|
f" iterations: {mjm.opt.iterations} ls_iterations: {mjm.opt.ls_iterations}\n"
|
||||||
|
f" integrator: {mujoco.mjtIntegrator(mjm.opt.integrator).name}\n"
|
||||||
|
)
|
||||||
|
print(f"MuJoCo C simulating with dt = {mjm.opt.timestep:.3f}...")
|
||||||
|
else:
|
||||||
|
wp.config.quiet = flags.FLAGS["verbosity"].value < 1
|
||||||
|
wp.init()
|
||||||
|
if _CLEAR_KERNEL_CACHE.value:
|
||||||
|
wp.clear_kernel_cache()
|
||||||
|
|
||||||
|
with wp.ScopedDevice(_DEVICE.value):
|
||||||
|
m = mjw.put_model(mjm)
|
||||||
|
_override(m)
|
||||||
|
mjm_hash = pickle.dumps(mjm)
|
||||||
|
broadphase, filter = mjw.BroadphaseType(m.opt.broadphase).name, mjw.BroadphaseFilter(m.opt.broadphase_filter).name
|
||||||
|
solver, cone = mjw.SolverType(m.opt.solver).name, mjw.ConeType(m.opt.cone).name
|
||||||
|
integrator = mjw.IntegratorType(m.opt.integrator).name
|
||||||
|
iterations, ls_iterations, ls_parallel = m.opt.iterations, m.opt.ls_iterations, m.opt.ls_parallel
|
||||||
|
print(
|
||||||
|
f" nbody: {m.nbody} nv: {m.nv} ngeom: {m.ngeom} nu: {m.nu} is_sparse: {m.opt.is_sparse}\n"
|
||||||
|
f" broadphase: {broadphase} broadphase_filter: {filter}\n"
|
||||||
|
f" solver: {solver} cone: {cone} iterations: {iterations} ls_iterations: {ls_iterations} ls_parallel: {ls_parallel}\n"
|
||||||
|
f" integrator: {integrator} graph_conditional: {m.opt.graph_conditional}"
|
||||||
|
)
|
||||||
|
d = mjw.put_data(mjm, mjd, nconmax=_NCONMAX.value, njmax=_NJMAX.value)
|
||||||
|
print(f"Data\n nworld: {d.nworld} nconmax: {d.nconmax} njmax: {d.njmax}\n")
|
||||||
|
print("Compiling physics step...", end="")
|
||||||
|
start = time.time()
|
||||||
|
graph = _compile_step(m, d)
|
||||||
|
elapsed = time.time() - start
|
||||||
|
print(f"done ({elapsed:0.2}s).")
|
||||||
|
print(f"MuJoCo Warp simulating with dt = {m.opt.timestep.numpy()[0]:.3f}...")
|
||||||
|
|
||||||
|
with mujoco.viewer.launch_passive(mjm, mjd, key_callback=key_callback) as viewer:
|
||||||
|
while True:
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
if _ENGINE.value == EngineOptions.C:
|
||||||
|
mujoco.mj_step(mjm, mjd)
|
||||||
|
else: # mjwarp
|
||||||
|
wp.copy(d.ctrl, wp.array([mjd.ctrl.astype(np.float32)]))
|
||||||
|
wp.copy(d.act, wp.array([mjd.act.astype(np.float32)]))
|
||||||
|
wp.copy(d.xfrc_applied, wp.array([mjd.xfrc_applied.astype(np.float32)]))
|
||||||
|
wp.copy(d.qpos, wp.array([mjd.qpos.astype(np.float32)]))
|
||||||
|
wp.copy(d.qvel, wp.array([mjd.qvel.astype(np.float32)]))
|
||||||
|
wp.copy(d.time, wp.array([mjd.time], dtype=wp.float32))
|
||||||
|
|
||||||
|
hash = pickle.dumps(mjm)
|
||||||
|
if hash != mjm_hash:
|
||||||
|
mjm_hash = hash
|
||||||
|
m = mjw.put_model(mjm)
|
||||||
|
graph = _compile_step(m, d)
|
||||||
|
|
||||||
|
if _VIEWER_GLOBAL_STATE["running"]:
|
||||||
|
wp.capture_launch(graph)
|
||||||
|
wp.synchronize()
|
||||||
|
elif _VIEWER_GLOBAL_STATE["step_once"]:
|
||||||
|
_VIEWER_GLOBAL_STATE["step_once"] = False
|
||||||
|
wp.capture_launch(graph)
|
||||||
|
wp.synchronize()
|
||||||
|
|
||||||
|
mjw.get_data_into(mjd, mjm, d)
|
||||||
|
|
||||||
|
viewer.sync()
|
||||||
|
|
||||||
|
elapsed = time.time() - start
|
||||||
|
if elapsed < mjm.opt.timestep:
|
||||||
|
time.sleep(mjm.opt.timestep - elapsed)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# absl flags assumes __main__ is the main running module for printing usage documentation
|
||||||
|
# pyproject bin scripts break this assumption, so manually set argv and docstring
|
||||||
|
sys.argv[0] = "mujoco_warp.viewer"
|
||||||
|
sys.modules["__main__"].__doc__ = __doc__
|
||||||
|
app.run(_main)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user