diff --git a/pyproject.toml b/pyproject.toml
index f89213862..827bae001 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -48,7 +48,9 @@ dependencies = [
# and 1.7.3 adds IsaacSim mapped-scene contact sensors, bounded PhysX
# solver configuration, per-entity self-collision and mapped reset
# domain randomization, plus IsaacGym mapped-scene reset randomization.
- "unisim-core>=1.7.4",
+ # 1.7.5 formalizes the physics-state playback rendering contract (layout
+ # split, mocap playback, conformance coverage; unilabsim/unisim#291).
+ "unisim-core>=1.7.5",
# RL algorithms and async runtimes (APPO/SAC runners, collectors,
# IPC, logging) live in the independently released uni-rl package
# (distribution name ``unilab-rl``), consumed via the injected env
@@ -127,8 +129,9 @@ mujoco = [
# mujoco==3.11.0 — switching MuJoCo versions requires an mjbatch rebuild,
# not a UniLab config change.
"mujoco~=3.11.0",
- # The batch engine is the published unilabsim mjbatch fork.
- "mjbatch-uni~=0.2.3",
+ # The batch engine is the published unilabsim mjbatch fork. 0.2.4 adds the
+ # streaming VariantPack builder required by unisim-core 1.7.5.
+ "mjbatch-uni~=0.2.4",
]
mjwarp = [
# Keep the Warp backend on the same MuJoCo minor line as the host backend.
@@ -161,7 +164,7 @@ newton = [
# The Motrix runtime pin lives in the unisim-core ``motrix`` extra, so
# the consumed runtime always matches the version the
# unisim.backend.motrix adapter is tested against.
-motrix = ["unisim-core[motrix]>=1.7.4"]
+motrix = ["unisim-core[motrix]>=1.7.5"]
genesis = [
# Genesis imports as ``genesis`` and owns the GPU physics implementation;
# it is a separate optional extra pinned exactly to the probed release
@@ -185,7 +188,7 @@ uni_rl = ["unilab-rl==1.3.2"]
# required-environments; elsewhere the extra is empty and the CLI reports a
# targeted runtime diagnostic.
superdex = [
- "unisim-core[superdex]>=1.7.4 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'",
+ "unisim-core[superdex]>=1.7.5 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'",
]
[dependency-groups]
diff --git a/src/unilab/base/base.py b/src/unilab/base/base.py
index 490f0d228..c82d3aa88 100644
--- a/src/unilab/base/base.py
+++ b/src/unilab/base/base.py
@@ -24,6 +24,7 @@ class EnvPlayCapabilities:
supports_native_video_capture: bool = False
supports_debug_overlay: bool = False
supports_interactive_debug_overlay: bool = False
+ supports_mocap_playback: bool = False
@dataclass
diff --git a/src/unilab/base/np_env.py b/src/unilab/base/np_env.py
index c9382769c..5b3022aeb 100644
--- a/src/unilab/base/np_env.py
+++ b/src/unilab/base/np_env.py
@@ -614,6 +614,7 @@ def play_capabilities(self) -> EnvPlayCapabilities:
supports_native_video_capture=capabilities.supports_native_video_capture,
supports_debug_overlay=capabilities.supports_debug_overlay,
supports_interactive_debug_overlay=capabilities.supports_interactive_debug_overlay,
+ supports_mocap_playback=capabilities.supports_mocap_playback,
)
def get_playback_model(self, env_index: int | None = None) -> Any:
@@ -627,6 +628,28 @@ def get_playback_model(self, env_index: int | None = None) -> Any:
"""
return self._backend.get_playback_model(env_index)
+ def get_physics_state_layout(self) -> Any:
+ """Return the backend physics-state layout contract.
+
+ The layout describes the ``get_physics_state_snapshot`` columns as
+ ``[time, qpos, qvel, (mocap_pos, mocap_quat)?]`` so render frontends
+ can split snapshots without hardcoding ``1 + nq + nv``. The SimBackend
+ default fails closed for backends without playback support.
+ """
+ return self._backend.get_physics_state_layout()
+
+ def get_playback_mocap_state(self, env_index: int = 0) -> tuple[np.ndarray, np.ndarray]:
+ """Return detached ``(mocap_pos, mocap_quat)`` playback state for one env."""
+ if not self.play_capabilities.supports_mocap_playback:
+ raise NotImplementedError(
+ f"{self._backend.__class__.__name__} does not support mocap playback"
+ )
+ mocap_pos, mocap_quat = self._backend.get_playback_mocap_state(env_index)
+ return (
+ np.asarray(mocap_pos, dtype=np.float64).copy(),
+ np.asarray(mocap_quat, dtype=np.float64).copy(),
+ )
+
def get_scene_visual_model_file(self) -> str | None:
"""Return the backend scene visual model file on the cold path, when available."""
return cast(str | None, self._backend.get_scene_visual_model_file())
diff --git a/src/unilab/scripts/play_interactive.py b/src/unilab/scripts/play_interactive.py
index 7c3d724d4..f477a33fb 100644
--- a/src/unilab/scripts/play_interactive.py
+++ b/src/unilab/scripts/play_interactive.py
@@ -78,6 +78,10 @@
prepare_motion_overlay_selection,
select_torch_device,
)
+from unilab.visualization.playback_state import (
+ PhysicsStateApplier,
+ assert_physics_state_playback_supported,
+)
_KEY_ENTER, _KEY_KP_ENTER = 257, 335
_KEY_BACKSPACE = 259
@@ -927,6 +931,7 @@ def play_interactive(args, cfg: DictConfig | None = None, *, algo: str | None =
return
playback_session = session[0]
env = playback_session.env
+ assert_physics_state_playback_supported(env, entrypoint="play_interactive")
# Discover task-owned playback overlays (command terms implementing
# playback_debug_overlay_getter()); None when the env provides none.
@@ -966,7 +971,7 @@ def play_interactive(args, cfg: DictConfig | None = None, *, algo: str | None =
mj_model = _load_viewer_model(env, use_env_visual_model=use_env_visual_model)
viz_data = mujoco.MjData(mj_model)
- state_spec = mujoco.mjtState.mjSTATE_FULLPHYSICS
+ state_applier = PhysicsStateApplier(env, mj_model, env_index=0)
ctrl_dt = env.cfg.ctrl_dt
playback_session.reset()
@@ -1052,9 +1057,8 @@ def _on_key(keycode: int) -> None:
# Use the reset pose for the initial target only. Updating this
# in the loop would override the user's manual camera movement.
- initial_phys = playback_session.physics_state()[0].astype(np.float64)
- mujoco.mj_setState(mj_model, viz_data, initial_phys, state_spec)
- mujoco.mj_forward(mj_model, viz_data)
+ initial_phys = playback_session.physics_state()[0]
+ state_applier.apply(initial_phys, viz_data)
if bool(getattr(args, "camera_follow_body", True)):
base_pos = viz_data.xpos[focus_body_id]
viewer.cam.lookat[0] = float(base_pos[0])
@@ -1074,9 +1078,8 @@ def _on_key(keycode: int) -> None:
playback_session.advance(controls)
# Push env state[0] into viz_data and refresh scene
- phys = playback_session.physics_state()[0].astype(np.float64)
- mujoco.mj_setState(mj_model, viz_data, phys, state_spec)
- mujoco.mj_forward(mj_model, viz_data)
+ phys = playback_session.physics_state()[0]
+ state_applier.apply(phys, viz_data)
primitives: list[DebugPrimitive] = []
if overlay.enabled:
diff --git a/src/unilab/scripts/play_viser.py b/src/unilab/scripts/play_viser.py
index f05168646..74d8358bc 100644
--- a/src/unilab/scripts/play_viser.py
+++ b/src/unilab/scripts/play_viser.py
@@ -32,6 +32,10 @@
from unilab.training import ensure_registries
from unilab.visualization.interactive_playback import PlaybackControls, PlayInteractiveArgs
+from unilab.visualization.playback_state import (
+ PhysicsStateApplier,
+ assert_physics_state_playback_supported,
+)
from unilab.visualization.render_many import get_grid_offsets
from unilab.visualization.viser_scene import (
VISER_AVAILABLE,
@@ -97,6 +101,7 @@ def _build_scene_entries(
"runtime_env_idx": env_idx,
"model": mj_model,
"data": mujoco.MjData(mj_model),
+ "applier": PhysicsStateApplier(env, mj_model, env_index=env_idx),
"scene": MujocoViserScene(server, mj_model, name_prefix="/mujoco/single"),
}
)
@@ -105,10 +110,12 @@ def _build_scene_entries(
offsets = get_grid_offsets(len(visible_env_indices), spacing=spacing)
models: list[mujoco.MjModel] = []
data: list[mujoco.MjData] = []
+ appliers: list[PhysicsStateApplier] = []
for env_idx in visible_env_indices:
model = _load_env_playback_model(env, int(env_idx))
models.append(model)
data.append(mujoco.MjData(model))
+ appliers.append(PhysicsStateApplier(env, model, env_index=int(env_idx)))
# MuJoCo task instances normally share one model. Viser can then render
# each geom as a batched mesh, reducing per-frame messages from
@@ -130,6 +137,7 @@ def _build_scene_entries(
"models": models,
"model": models[0],
"data": data,
+ "appliers": appliers,
"scene": MujocoViserBatchScene(
server,
models,
@@ -151,6 +159,7 @@ def _build_scene_entries(
"runtime_env_idx": env_idx,
"model": mj_model,
"data": mujoco.MjData(mj_model),
+ "applier": appliers[local_idx],
"scene": MujocoViserScene(
server,
mj_model,
@@ -184,6 +193,7 @@ def log(message: str) -> None:
return
playback_session = session[0]
env = playback_session.env
+ assert_physics_state_playback_supported(env, entrypoint="play_viser")
# --- GUI controls --------------------------------------------------------
max_visible_envs = min(int(OmegaConf.select(cfg, "viser.max_envs", default=16) or 16), num_envs)
@@ -195,7 +205,6 @@ def log(message: str) -> None:
initial_mode = "all"
visible_env_indices = build_visible_env_indices(num_envs, max_visible_envs)
- state_spec = mujoco.mjtState.mjSTATE_FULLPHYSICS
ctrl_dt = env.cfg.ctrl_dt
render_spacing = float(
OmegaConf.select(cfg, "training.render_spacing") or getattr(env.cfg, "render_spacing", 1.0)
@@ -322,20 +331,18 @@ def _on_env_switch(event: Any) -> None:
physics_batch = playback_session.physics_state()
for entry in scene_entries["value"]:
if entry.get("batch", False):
- for runtime_idx, model, data in zip(
+ for runtime_idx, applier, data in zip(
entry["runtime_env_indices"],
- entry["models"],
+ entry["appliers"],
entry["data"],
strict=True,
):
- phys = physics_batch[int(runtime_idx)].astype(np.float64)
- mujoco.mj_setState(model, data, phys, state_spec)
- mujoco.mj_forward(model, data)
+ applier.apply(physics_batch[int(runtime_idx)], data)
entry["scene"].update(entry["data"])
continue
- phys = physics_batch[int(entry["runtime_env_idx"])].astype(np.float64)
- mujoco.mj_setState(entry["model"], entry["data"], phys, state_spec)
- mujoco.mj_forward(entry["model"], entry["data"])
+ entry["applier"].apply(
+ physics_batch[int(entry["runtime_env_idx"])], entry["data"]
+ )
entry["scene"].update(entry["data"])
# Real-time pacing
diff --git a/src/unilab/visualization/playback_state.py b/src/unilab/visualization/playback_state.py
new file mode 100644
index 000000000..9f951cadc
--- /dev/null
+++ b/src/unilab/visualization/playback_state.py
@@ -0,0 +1,112 @@
+"""Contract-driven physics-state application for interactive render frontends.
+
+Interactive entrypoints (``play_interactive``, ``play_viser``) render through a
+MuJoCo playback shell: the physics owner produces physics-state snapshots and
+the shell only renders. This module owns the two contract touchpoints of that
+path (unilabsim/unisim#291):
+
+- :func:`assert_physics_state_playback_supported` fails at startup with an
+ actionable message when the selected backend does not implement the
+ playback contract, instead of surfacing a ``NotImplementedError`` from the
+ first snapshot fetch deep inside the viewer loop.
+- :class:`PhysicsStateApplier` splits snapshot rows through the unisim
+ ``physics_state_layout`` contract (``split_state``) and replays mocap
+ geometry through the contract ``get_playback_mocap_state`` entrypoint, so
+ frontends never hardcode ``1 + nq + nv`` slices or
+ ``mjSTATE_FULLPHYSICS``.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+import mujoco
+import numpy as np
+
+
+def assert_physics_state_playback_supported(env: Any, *, entrypoint: str) -> None:
+ """Fail at startup unless the env's backend supports physics-state playback.
+
+ Raises:
+ NotImplementedError: naming the backend and the rendering paths that
+ remain available for it.
+ """
+ capabilities = getattr(env, "play_capabilities", None)
+ if capabilities is not None and capabilities.supports_physics_state_playback:
+ return
+ backend_name = type(getattr(env, "_backend", env)).__name__
+ if capabilities is not None and (
+ capabilities.supports_native_interactive_renderer
+ or capabilities.supports_native_video_capture
+ ):
+ hint = "use the backend's native interactive/video rendering entrypoints instead"
+ else:
+ hint = (
+ "no unified rendering path exists for this backend yet "
+ "(tracked by unilabsim/unisim#291)"
+ )
+ raise NotImplementedError(
+ f"{entrypoint}: {backend_name} does not support physics-state playback; {hint}."
+ )
+
+
+class PhysicsStateApplier:
+ """Apply contract physics-state rows to one MuJoCo playback ``MjData``.
+
+ Cold-path construction validates the playback model against the backend
+ physics-state layout; :meth:`apply` then only writes arrays and forwards
+ the model. Mocap geometry is replayed through the contract
+ ``get_playback_mocap_state`` entrypoint when the backend declares
+ ``supports_mocap_playback`` (the returned state is aligned with the
+ playback model, which may differ from the physics model); otherwise the
+ snapshot tail is consumed directly.
+ """
+
+ def __init__(self, env: Any, model: mujoco.MjModel, env_index: int = 0) -> None:
+ self._model = model
+ self._env = env
+ self._env_index = int(env_index)
+ self._mocap_via_contract = False
+
+ layout = env.get_physics_state_layout()
+ if model.nq != layout.nq or model.nv != layout.nv:
+ raise ValueError(
+ f"Playback model dimensions (nq={model.nq}, nv={model.nv}) do not match "
+ f"the backend physics-state layout (nq={layout.nq}, nv={layout.nv}); "
+ "the viewer model must share the physics joint structure."
+ )
+ self._layout = layout
+ if model.nmocap == 0:
+ return
+ capabilities = getattr(env, "play_capabilities", None)
+ if capabilities is not None and capabilities.supports_mocap_playback:
+ self._mocap_via_contract = True
+ elif layout.nmocap != model.nmocap:
+ raise NotImplementedError(
+ f"{type(getattr(env, '_backend', env)).__name__} snapshots do not carry the "
+ f"{model.nmocap} mocap bodies of the playback model "
+ f"(layout nmocap={layout.nmocap}) and the backend does not declare "
+ "supports_mocap_playback; mocap geometry cannot be replayed."
+ )
+
+ def apply(self, state: np.ndarray, data: mujoco.MjData) -> None:
+ """Write one snapshot row into ``data`` and forward the playback model."""
+ parts = self._layout.split_state(np.asarray(state, dtype=np.float64))
+ data.time = float(parts.time)
+ data.qpos[:] = parts.qpos
+ data.qvel[:] = parts.qvel
+ if self._model.nmocap:
+ if self._mocap_via_contract:
+ mocap_pos, mocap_quat = self._env.get_playback_mocap_state(self._env_index)
+ else:
+ mocap_pos, mocap_quat = parts.mocap_pos, parts.mocap_quat
+ data.mocap_pos[:] = np.asarray(mocap_pos, dtype=np.float64).reshape(
+ self._model.nmocap, 3
+ )
+ data.mocap_quat[:] = np.asarray(mocap_quat, dtype=np.float64).reshape(
+ self._model.nmocap, 4
+ )
+ mujoco.mj_forward(self._model, data)
+
+
+__all__ = ["PhysicsStateApplier", "assert_physics_state_playback_supported"]
diff --git a/tests/base/test_np_env_playback_contract.py b/tests/base/test_np_env_playback_contract.py
index 2f0723c17..298556e14 100644
--- a/tests/base/test_np_env_playback_contract.py
+++ b/tests/base/test_np_env_playback_contract.py
@@ -239,3 +239,42 @@ def test_play_capabilities_forward_supports_interactive_debug_overlay() -> None:
)
assert env.play_capabilities.supports_interactive_debug_overlay is True
assert EnvPlayCapabilities().supports_interactive_debug_overlay is False
+
+
+def test_play_capabilities_fail_closed_on_mocap_without_backend_declaration() -> None:
+ env = _PlaybackStubEnv()
+ assert env.play_capabilities.supports_mocap_playback is False
+ assert EnvPlayCapabilities().supports_mocap_playback is False
+
+
+def test_play_capabilities_forward_supports_mocap_playback() -> None:
+ capabilities = BackendPlayCapabilities(supports_mocap_playback=True)
+ env = _PlaybackStubEnv(capabilities=capabilities)
+ assert env.play_capabilities.supports_mocap_playback is True
+
+
+def test_get_physics_state_layout_delegates_to_backend() -> None:
+ env = _PlaybackStubEnv()
+ layout = env.get_physics_state_layout()
+ assert layout is env._backend.get_physics_state_layout.return_value
+
+
+def test_get_playback_mocap_state_gated_on_capability() -> None:
+ env = _PlaybackStubEnv()
+ with pytest.raises(NotImplementedError, match="mocap playback"):
+ env.get_playback_mocap_state(0)
+ env._backend.get_playback_mocap_state.assert_not_called()
+
+
+def test_get_playback_mocap_state_returns_detached_copies() -> None:
+ env = _PlaybackStubEnv(capabilities=BackendPlayCapabilities(supports_mocap_playback=True))
+ mocap_pos = np.zeros((1, 3))
+ mocap_quat = np.zeros((1, 4))
+ env._backend.get_playback_mocap_state.return_value = (mocap_pos, mocap_quat)
+
+ out_pos, out_quat = env.get_playback_mocap_state(2)
+
+ env._backend.get_playback_mocap_state.assert_called_once_with(2)
+ assert out_pos.dtype == np.float64 and out_quat.dtype == np.float64
+ out_pos[:] = 1.0
+ assert mocap_pos.sum() == 0.0
diff --git a/tests/visualization/test_playback_state.py b/tests/visualization/test_playback_state.py
new file mode 100644
index 000000000..b4d74c94c
--- /dev/null
+++ b/tests/visualization/test_playback_state.py
@@ -0,0 +1,153 @@
+"""Tests for the contract-driven physics-state applier and startup precheck.
+
+Covers unilabsim/unisim#291 downstream consumption: capability precheck at
+entrypoint startup, layout-contract snapshot splitting, and contract mocap
+replay.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+import mujoco
+import numpy as np
+import pytest
+from unisim.backend.base import PhysicsStateLayout
+
+from unilab.base.base import EnvPlayCapabilities
+from unilab.visualization.playback_state import (
+ PhysicsStateApplier,
+ assert_physics_state_playback_supported,
+)
+
+_FREE_BODY_XML = """
+
+
+
+
+
+"""
+
+_MOCAP_XML = """
+
+
+
+
+
+
+"""
+
+
+class _StubBackend:
+ pass
+
+
+class _StubEnv:
+ """Minimal env surface consumed by the playback-state helpers."""
+
+ def __init__(
+ self,
+ capabilities: EnvPlayCapabilities,
+ layout: PhysicsStateLayout,
+ ) -> None:
+ self.play_capabilities = capabilities
+ self._backend = _StubBackend()
+ self._layout = layout
+ self.mocap_calls: list[int] = []
+
+ def get_physics_state_layout(self) -> Any:
+ return self._layout
+
+ def get_playback_mocap_state(self, env_index: int = 0):
+ self.mocap_calls.append(env_index)
+ return np.full((1, 3), 0.5), np.array([[1.0, 0.0, 0.0, 0.0]])
+
+
+def _model(xml: str) -> mujoco.MjModel:
+ return mujoco.MjModel.from_xml_string(xml)
+
+
+def test_precheck_passes_with_physics_state_playback() -> None:
+ env = _StubEnv(
+ EnvPlayCapabilities(supports_physics_state_playback=True),
+ layout=PhysicsStateLayout(nq=0, nv=0),
+ )
+ assert_physics_state_playback_supported(env, entrypoint="play_viser")
+
+
+def test_precheck_names_entrypoint_backend_and_native_hint() -> None:
+ env = _StubEnv(
+ EnvPlayCapabilities(supports_native_interactive_renderer=True),
+ layout=PhysicsStateLayout(nq=0, nv=0),
+ )
+ with pytest.raises(
+ NotImplementedError, match=r"play_interactive.*_StubBackend.*native interactive"
+ ):
+ assert_physics_state_playback_supported(env, entrypoint="play_interactive")
+
+
+def test_precheck_points_to_issue_when_no_rendering_path() -> None:
+ env = _StubEnv(EnvPlayCapabilities(), layout=PhysicsStateLayout(nq=0, nv=0))
+ with pytest.raises(NotImplementedError, match=r"play_viser.*unisim#291"):
+ assert_physics_state_playback_supported(env, entrypoint="play_viser")
+
+
+def test_applier_splits_time_qpos_qvel() -> None:
+ model = _model(_FREE_BODY_XML)
+ env = _StubEnv(
+ EnvPlayCapabilities(supports_physics_state_playback=True),
+ layout=PhysicsStateLayout(nq=7, nv=6),
+ )
+ applier = PhysicsStateApplier(env, model, env_index=0)
+
+ data = mujoco.MjData(model)
+ row = np.arange(14, dtype=np.float64)
+ applier.apply(row, data)
+
+ assert data.time == pytest.approx(0.0)
+ np.testing.assert_allclose(data.qpos, row[1:8])
+ np.testing.assert_allclose(data.qvel, row[8:14])
+
+
+def test_applier_replays_mocap_through_contract_entrypoint() -> None:
+ model = _model(_MOCAP_XML)
+ assert model.nmocap == 1
+ env = _StubEnv(
+ EnvPlayCapabilities(supports_physics_state_playback=True, supports_mocap_playback=True),
+ layout=PhysicsStateLayout(nq=7, nv=6, nmocap=1),
+ )
+ applier = PhysicsStateApplier(env, model, env_index=3)
+
+ data = mujoco.MjData(model)
+ applier.apply(np.zeros(1 + 7 + 6 + 7, dtype=np.float64), data)
+
+ assert env.mocap_calls == [3]
+ np.testing.assert_allclose(data.mocap_pos, 0.5)
+ np.testing.assert_allclose(data.mocap_quat, [[1.0, 0.0, 0.0, 0.0]])
+
+
+def test_applier_falls_back_to_snapshot_tail_without_mocap_capability() -> None:
+ model = _model(_MOCAP_XML)
+ env = _StubEnv(
+ EnvPlayCapabilities(supports_physics_state_playback=True),
+ layout=PhysicsStateLayout(nq=7, nv=6, nmocap=1),
+ )
+ applier = PhysicsStateApplier(env, model)
+
+ data = mujoco.MjData(model)
+ row = np.arange(21, dtype=np.float64)
+ applier.apply(row, data)
+
+ assert env.mocap_calls == []
+ np.testing.assert_allclose(data.mocap_pos[0], row[14:17])
+ np.testing.assert_allclose(data.mocap_quat[0], row[17:21])
+
+
+def test_applier_rejects_model_layout_mismatch() -> None:
+ model = _model(_FREE_BODY_XML)
+ env = _StubEnv(
+ EnvPlayCapabilities(supports_physics_state_playback=True),
+ layout=PhysicsStateLayout(nq=8, nv=6),
+ )
+ with pytest.raises(ValueError, match="nq=8"):
+ PhysicsStateApplier(env, model)
diff --git a/uv.lock b/uv.lock
index 112b6ec7c..d1c193b46 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2136,27 +2136,27 @@ wheels = [
[[package]]
name = "mjbatch-uni"
-version = "0.2.3"
+version = "0.2.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mujoco" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c8/4b/25045002da76322f335ca5a2fa537e4cc742ad60013db8df0a90227eb2bf/mjbatch_uni-0.2.3.tar.gz", hash = "sha256:06f2b2eac3cca1f9b6e49590d4e5806bcc0f810751388299152e351e8f2304a5", size = 43682, upload-time = "2026-09-20T05:39:41.038Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/dd/c7b5f01ba132ee03ea1412651942709ee64febb08004435afe101fd09485/mjbatch_uni-0.2.4.tar.gz", hash = "sha256:7d11c0b798a5722a03a746029ab8d860bf8d1c11caa008205ed8ddf32cc72bb0", size = 45225, upload-time = "2026-09-23T12:24:02.924Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b4/b6/2d4a56df2e2ea8036fda965a83f6965fda58271cf03dbf625951e6ab6323/mjbatch_uni-0.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6954156646aaf14bfe2850df0e80d15b431912f31f810cabf0dd7feb917daf7", size = 156865, upload-time = "2026-09-20T05:39:24.229Z" },
- { url = "https://files.pythonhosted.org/packages/75/22/a219ab113dcfb4ccd5cea4093c156ae3eb64f99bb949849907834d54bd3a/mjbatch_uni-0.2.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ac6af4c049b7d911351ca1f57ab6459c1e189055069a345d74f11e25f64be2d", size = 186391, upload-time = "2026-09-20T05:39:25.953Z" },
- { url = "https://files.pythonhosted.org/packages/e0/43/193224d78f14ccccb4c0d8a84624468cc7456fbc716f53639437f913b6e7/mjbatch_uni-0.2.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7a3dd8e702e79dac5e65e9b5c641e52fcd40ccbb5e46ab62fab30f3a302716", size = 196617, upload-time = "2026-09-20T05:39:27.392Z" },
- { url = "https://files.pythonhosted.org/packages/a4/d5/31f6b07bcd0d8052f48e85075eae536f4f20780ce21658ed4959a5a87473/mjbatch_uni-0.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9ad7598d0f429bbc0efd8999ba031f8baec0772e97702a87a0090762e821bd54", size = 156505, upload-time = "2026-09-20T05:39:28.663Z" },
- { url = "https://files.pythonhosted.org/packages/2b/fd/b1ca7575f6a7008e5ab95f53c7f65f08f710f828865965a972a831d06eea/mjbatch_uni-0.2.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8be976f40a2f9ec6f5ed0018f89a373ded8c466d0950cdb52023b04293508b86", size = 186238, upload-time = "2026-09-20T05:39:29.928Z" },
- { url = "https://files.pythonhosted.org/packages/eb/3d/5a0f6d9e280e94f57d805e3fa028fe1c716483ec796260d5735b4bfca11f/mjbatch_uni-0.2.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81c6fe9dc66c05f1850088f05605688a7dc2274b047f0841a536f48faa9ad0b5", size = 196369, upload-time = "2026-09-20T05:39:31.428Z" },
- { url = "https://files.pythonhosted.org/packages/c2/73/f36f56b4eb231d2d307f43a71be15107d2742fbcaaa3faddf419b8b12928/mjbatch_uni-0.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc95a5b920b066ecc87c7436416a0b426e21af6331b162d9c38a53e50084e26c", size = 155821, upload-time = "2026-09-20T05:39:32.874Z" },
- { url = "https://files.pythonhosted.org/packages/68/c0/8dcedd090bcfc166453b1632baa37b4437a2cd9436d8239ccaac326924bb/mjbatch_uni-0.2.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:079e415b0f510912a63ccc2589eb0da0aa97af70928b889c92c6df6d21940822", size = 185242, upload-time = "2026-09-20T05:39:34.338Z" },
- { url = "https://files.pythonhosted.org/packages/ce/b5/d37c4355468455aa7d0aa8240ba1b80eb415efb247a7be12bbef59b56f2e/mjbatch_uni-0.2.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e50d39a3393f7c2bc623a85db3426e64807d001b1bc8e373f6e90c3c973484a5", size = 195998, upload-time = "2026-09-20T05:39:35.847Z" },
- { url = "https://files.pythonhosted.org/packages/90/13/59dfa3f147b3c70e15e46c3ed3ca9524fad2443b3d3d45edc2785fae0a66/mjbatch_uni-0.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b9f08e67d4eddcbdbc5b732e2fbead2ba7c203bddbcb3a266216db8d843cf03", size = 155830, upload-time = "2026-09-20T05:39:37.303Z" },
- { url = "https://files.pythonhosted.org/packages/f3/2e/a918d549bd4e16d7def0d4f4ed5d17e927aeb5547816842455796bbd1f92/mjbatch_uni-0.2.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f86430d883a28d6d8890c6b31bdbe227b99179112ea2fd7533ab2e285aea78f8", size = 185198, upload-time = "2026-09-20T05:39:38.488Z" },
- { url = "https://files.pythonhosted.org/packages/eb/af/aed3093b9b60f2a30ff9be3b67d6f413ecf6835e384c3126dcf27a15add9/mjbatch_uni-0.2.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:245854cf34f5e4600e747ef78c4e7986d14aa68a91f34760ae7585dd4dc05b38", size = 195953, upload-time = "2026-09-20T05:39:39.773Z" },
+ { url = "https://files.pythonhosted.org/packages/22/fe/109ae917b9e8372f7b78a14e1b8f73124de9db072cd7168ec555e77be8e7/mjbatch_uni-0.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba400c142bee50d3212e68a27b23049d35c5aa6b47d1f1abb34002203cc0d4d5", size = 158492, upload-time = "2026-09-23T12:23:46.889Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0c/a8a59aea56fdba3cc4b87da5e499b19ec7cadee39c6cfb06716d70b78cb5/mjbatch_uni-0.2.4-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9bb921fc6a91c445e12491988a7f909ba9754063e22a418ce49c31464720e86", size = 188018, upload-time = "2026-09-23T12:23:48.725Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/62/b9445e3ab7879d75ae552aa7be52e4a5fa115aec6eb3161ad56466318a58/mjbatch_uni-0.2.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:160abb9b74053f56dd401dcd7edf0fb3dc6b1414d224b41b71a09749fd5c30aa", size = 198243, upload-time = "2026-09-23T12:23:49.845Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/fa/876813961fd7078906cec76f3fed7c65528d5026cb73b13c19cb894b2c5c/mjbatch_uni-0.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1495abe9d5c559e2412104572b8061eadc1949d3dd889336d3f1d23295ca544b", size = 158130, upload-time = "2026-09-23T12:23:51.374Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/0f/d5ba1ae19e78dda73da98909c884e5ef6cb2cc19e2c0f28a70a934385e94/mjbatch_uni-0.2.4-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:873bf2126886fcc5d044acd67c18eff714434ee68897716099ac9475bb30eaae", size = 187865, upload-time = "2026-09-23T12:23:52.793Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/d0/fe2b7cd3ecb045e00cbad11ce6407862ff8e60dde0304312fed57398055f/mjbatch_uni-0.2.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:254056c4656bdb48a963ad1ccd83b5451ebfa312c38a011fae9fe4e1bfbd85ec", size = 198001, upload-time = "2026-09-23T12:23:54.078Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/0a/a303e3908c07c86bc89bb1c693db68039834ba71b2b4c4f0621edeb4ef9a/mjbatch_uni-0.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c82a3bdc590308966275f6bacdb3f0cd1b1e4360695c53f54b194443c42dbea0", size = 157447, upload-time = "2026-09-23T12:23:55.287Z" },
+ { url = "https://files.pythonhosted.org/packages/49/04/169f7fd6cd31ebf4e3d02321fd419a6ae7da52fb4f88983b4ac6d1633176/mjbatch_uni-0.2.4-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0e22add7c9c9ab72eaaaaf59ade6163a43709e207e12916fd251e8c6372bb799", size = 186869, upload-time = "2026-09-23T12:23:56.58Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/8667b24dc09d4b69456f1f11cb6226f5a9c76b8a7e0688baa45cbe668ebf/mjbatch_uni-0.2.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d41e8fc20f109693cb30a2f30ef5a7ab9077232d5f05a9414e857a558a28adef", size = 197623, upload-time = "2026-09-23T12:23:57.912Z" },
+ { url = "https://files.pythonhosted.org/packages/90/9c/dacdbd7d34030f1eb22d7df28584798ad1cea80be8dfddeb7cc21e629166/mjbatch_uni-0.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ff9c749cc0e338027e14d046555678e9bb4138b66e4d1b6db221dc107665ad4", size = 157456, upload-time = "2026-09-23T12:23:59.131Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/f8/b215d9d87ba9f0af9bc3bded9322eca5a77faaa1a2fe2d65d8714ea69753/mjbatch_uni-0.2.4-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e8003fced3e5d1357b4184e40af99009547caeade7896b0720b0b32abd7034", size = 186825, upload-time = "2026-09-23T12:24:00.408Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/f0/dc5f7508dffc980e26a9d16c964a9f6dedc050d3acfbf70ef9fa48db1864/mjbatch_uni-0.2.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:952194f3143c005653b8b701fc91e56335a6d3998348ffef6211503b49904566", size = 197579, upload-time = "2026-09-23T12:24:01.635Z" },
]
[[package]]
@@ -5213,7 +5213,7 @@ requires-dist = [
{ name = "imgui-bundle", marker = "extra == 'newton'", specifier = ">=1.92.0" },
{ name = "lark", specifier = ">=1.3.1" },
{ name = "mediapy" },
- { name = "mjbatch-uni", marker = "extra == 'mujoco'", specifier = "~=0.2.3" },
+ { name = "mjbatch-uni", marker = "extra == 'mujoco'", specifier = "~=0.2.4" },
{ name = "mujoco", marker = "extra == 'drake'", specifier = ">=3.5" },
{ name = "mujoco", marker = "extra == 'mujoco'", specifier = "~=3.11.0" },
{ name = "mujoco", marker = "extra == 'newton'", specifier = "==3.11.0" },
@@ -5242,9 +5242,9 @@ requires-dist = [
{ name = "trimesh", marker = "extra == 'viser'", specifier = ">=3.21.7" },
{ name = "typing-extensions" },
{ name = "unilab-rl", marker = "extra == 'uni-rl'", specifier = "==1.3.2" },
- { name = "unisim-core", specifier = ">=1.7.4" },
- { name = "unisim-core", extras = ["motrix"], marker = "extra == 'motrix'", specifier = ">=1.7.4" },
- { name = "unisim-core", extras = ["superdex"], marker = "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'superdex'", specifier = ">=1.7.4" },
+ { name = "unisim-core", specifier = ">=1.7.5" },
+ { name = "unisim-core", extras = ["motrix"], marker = "extra == 'motrix'", specifier = ">=1.7.5" },
+ { name = "unisim-core", extras = ["superdex"], marker = "python_full_version >= '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'superdex'", specifier = ">=1.7.5" },
{ name = "viser", marker = "extra == 'viser'", specifier = ">=1.0.26" },
{ name = "wandb" },
{ name = "warp-lang", marker = "extra == 'mjwarp'", specifier = "==1.16.0" },
@@ -5288,13 +5288,13 @@ wheels = [
[[package]]
name = "unisim-core"
-version = "1.7.4"
+version = "1.7.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/60/0b/ade6efb6d382dc0619d2b4d223919bd2a1bb3d4cedce6459e049a2601dd4/unisim_core-1.7.4.tar.gz", hash = "sha256:aa8fe37d95645ea310a6f63cecf1a8ce035acfc5be31d44a4dfdf47c8d21224a", size = 508761, upload-time = "2026-09-21T07:31:56.563Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/2c/8255a60c0260a4dc3b41aa60d9910b417a9ae11a1ef3de2b682252680390/unisim_core-1.7.5.tar.gz", hash = "sha256:7613702bf757a6253691bbcd5ab7ae38bf2ce2b8466cf5e6b0947c59466fbc11", size = 526190, upload-time = "2026-09-23T12:31:03.655Z" }
[package.optional-dependencies]
motrix = [