From fb0347e0c43806bc7c9330b3b8b3d4fc45599e30 Mon Sep 17 00:00:00 2001 From: TATP-233 Date: Wed, 23 Sep 2026 20:47:31 +0800 Subject: [PATCH] feat: consume unisim 1.7.5 physics-state playback contract in interactive rendering Downstream adaptation for unilabsim/unisim#291: - play_interactive/play_viser precheck supports_physics_state_playback at startup and name the available rendering paths in the error instead of failing on the first snapshot fetch inside the viewer loop - new unilab.visualization.playback_state owns the contract touchpoints: PhysicsStateApplier splits snapshots through the physics-state layout contract (no more hardcoded mjSTATE_FULLPHYSICS / 1+nq+nv) and replays mocap geometry through the contract get_playback_mocap_state entrypoint - EnvPlayCapabilities gains supports_mocap_playback; NpEnv exposes get_physics_state_layout/get_playback_mocap_state passthroughs - bump unisim-core to >=1.7.5 (playback contract release) and mjbatch-uni to ~=0.2.4 (streaming VariantPack builder required by unisim 1.7.5) Affects the MuJoCo playback-shell rendering path only; physics ownership is unchanged. --- pyproject.toml | 13 +- src/unilab/base/base.py | 1 + src/unilab/base/np_env.py | 23 +++ src/unilab/scripts/play_interactive.py | 17 ++- src/unilab/scripts/play_viser.py | 25 ++-- src/unilab/visualization/playback_state.py | 112 ++++++++++++++ tests/base/test_np_env_playback_contract.py | 39 +++++ tests/visualization/test_playback_state.py | 153 ++++++++++++++++++++ uv.lock | 40 ++--- 9 files changed, 382 insertions(+), 41 deletions(-) create mode 100644 src/unilab/visualization/playback_state.py create mode 100644 tests/visualization/test_playback_state.py 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 = [