Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/sphinx/source/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,29 @@ PyPI 版本变更与未发布变更;发布日期采用 PyPI 上传日期。完
Reward manager 不再在 reset extras 中重复输出 `Episode_Reward/*`;既有
episode log 仍是这些指标的数据源(#1570)。

### Changed / 变更

- The MuJoCo executor dependency is pinned to `mjbatch-uni~=0.2.1` and
`unisim-core` moves to `>=1.4.0`
([unisim#71](https://github.com/unilabsim/unisim/issues/71),
[mjbatch_uni#28](https://github.com/unilabsim/mjbatch_uni/issues/28)).
unisim-core 1.4.0 adds per-environment gravity reset, the cross-backend
body wrench/torque contract, and per-substep callback wrenches through
`PreStepControlOutput`; on the MuJoCo pre-step control path the executor's
split-substep sensor copyout now refreshes tracked body state at every
substep boundary (about 14x faster than the previous host-side recompute,
with bit-identical trajectories). Tests that pinned the previous executor
callback protocol and Isaac worker diagnostic text were updated to the
published contracts.
MuJoCo 执行器依赖固定为 `mjbatch-uni~=0.2.1`,`unisim-core` 升级到
`>=1.4.0`(unisim#71、mjbatch_uni#28)。unisim-core 1.4.0 新增逐环境
重力 reset、跨后端 body wrench/torque 契约,以及通过
`PreStepControlOutput` 的逐子步 callback wrench;MuJoCo pre-step control
路径改用执行器的 split-substep 传感器增量拷出,在每个子步边界刷新
tracked body state(相比此前主机端重算约 14 倍加速,轨迹逐位一致)。
原先固定旧执行器 callback 协议与 Isaac worker 诊断文案的测试已更新到
已发布契约。

## 1.2.0 (2026-09-10)

### Breaking changes / 破坏性变更
Expand Down
9 changes: 5 additions & 4 deletions pyproject.rocm.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ requires-python = ">=3.10,<3.14"
dependencies = [
"numpy",
# Physics implementations are provided by the independently released
# unisim-core package. The 1.3.0 release carries the fixed model variant
# and per-world reset-default contracts.
"unisim-core>=1.3.0",
# unisim-core package. The 1.4.0 release carries the fixed model variant,
# per-world reset-default, per-env gravity, and substep body-wrench
# contracts.
"unisim-core>=1.4.0",
# RL algorithms and async runtimes live in the independently released
# uni-rl package (distribution name ``unilab-rl``); see pyproject.toml.
"unilab-rl==1.2.0",
Expand Down Expand Up @@ -80,7 +81,7 @@ mujoco = [
# time, so isolated builds are correct and no compiler preflight is
# needed.
"mujoco~=3.11.0",
"mjbatch-uni~=0.2.0",
"mjbatch-uni~=0.2.1",
]
motrix = ["motrixsim-core==0.8.2"]
viser = ["viser>=1.0.26", "trimesh>=3.21.7"]
Expand Down
11 changes: 6 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ requires-python = ">=3.10,<3.14"
dependencies = [
"numpy",
# Physics implementations are provided by the independently released
# unisim-core package. The 1.3.0 release carries the fixed model variant
# and per-world reset-default contracts.
"unisim-core>=1.3.0",
# unisim-core package. The 1.4.0 release carries the fixed model variant,
# per-world reset-default, per-env gravity, and substep body-wrench
# contracts.
"unisim-core>=1.4.0",
# RL algorithms and async runtimes (PPO/APPO/SAC/TD3 runners,
# collectors, IPC, logging) live in the independently released uni-rl
# package (distribution name ``unilab-rl``), consumed via the injected
Expand Down Expand Up @@ -122,7 +123,7 @@ mujoco = [
# not a UniLab config change.
"mujoco~=3.11.0",
# The batch engine is the published unilabsim mjbatch fork.
"mjbatch-uni~=0.2.0",
"mjbatch-uni~=0.2.1",
]
mjwarp = [
# Keep the Warp backend on the same MuJoCo minor line as the host backend.
Expand Down Expand Up @@ -172,7 +173,7 @@ viser = ["viser>=1.0.26", "trimesh>=3.21.7"]
# required-environments; elsewhere the extra is empty and the CLI reports a
# targeted runtime diagnostic.
superdex = [
"unisim-core[superdex]>=1.3.0 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'",
"unisim-core[superdex]>=1.4.0 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'",
]

[dependency-groups]
Expand Down
41 changes: 34 additions & 7 deletions tests/base/test_backend_pre_step_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@
from unisim.backend.base import SimBackend


def _contract_backend(fn=None):
"""A minimal owner object exposing the pre-step-control contract methods."""
backend = SimpleNamespace(_pre_step_control_fn=fn)
backend._convert_pre_step_control = lambda ctrl: SimBackend._convert_pre_step_control(
backend, ctrl
)
return backend


def test_pre_step_control_default_noop() -> None:
backend = SimpleNamespace(_pre_step_control_fn=None)
backend = _contract_backend()
ctrl = np.zeros((2, 3), dtype=np.float32)

out = SimBackend._apply_pre_step_control(backend, ctrl) # type: ignore[arg-type]
Expand All @@ -17,7 +26,7 @@ def test_pre_step_control_default_noop() -> None:


def test_pre_step_control_applies_registered_converter() -> None:
backend = SimpleNamespace(_pre_step_control_fn=None)
backend = _contract_backend()
ctrl = np.array([[1.0, 2.0, 3.0]], dtype=np.float32)

SimBackend.set_pre_step_control( # type: ignore[arg-type]
Expand All @@ -34,7 +43,7 @@ def test_pre_step_control_applies_registered_converter() -> None:


def test_pre_step_control_rejects_shape_mismatch() -> None:
backend = SimpleNamespace(_pre_step_control_fn=lambda current_backend, ctrl: ctrl[:, :1])
backend = _contract_backend(lambda current_backend, ctrl: ctrl[:, :1])
ctrl = np.zeros((2, 3), dtype=np.float32)

with pytest.raises(ValueError, match="pre-step control must return shape"):
Expand Down Expand Up @@ -70,6 +79,7 @@ def __init__(self, nq: int = 1, nv: int = 1, nu: int = 2, nbody: int = 1) -> Non
}
self.step_calls: list[dict] = []
self.callback_controls: list[np.ndarray] = []
self.callback_channels: list[np.ndarray] = []

def bind(self, name: str, dtype=None) -> np.ndarray:
return self._bufs[name]
Expand All @@ -81,19 +91,28 @@ def step(
history=None,
*,
callback=None,
substep_sensor_copyout=None,
) -> None:
self.step_calls.append(
{
"nstep": nstep,
"callback": callback,
"substep_sensor_copyout": substep_sensor_copyout,
}
)
state = self._state
ctrl_buf = self._bufs["ctrl"]
for k in range(nstep):
if callback is not None:
callback(k, state, ctrl_buf)
if substep_sensor_copyout is None:
callback(k, state, ctrl_buf)
else:
start, stop = substep_sensor_copyout
callback(k, state, ctrl_buf, self._bufs["sensordata"][:, start:stop])
self.callback_controls.append(ctrl_buf.copy())
# The persistent wrench channel as the substep's physics
# sees it: composed after the callback, before integration.
self.callback_channels.append(self._bufs["xfrc_applied"].copy())
state += 1.0
# End-of-call copy-out of the bound input/derived fields.
self._bufs["qpos"][:] = state[:, self.qpos_slice]
Expand All @@ -118,6 +137,7 @@ def _fake_mujoco_backend(pre_step_control_fn=None):
pool = _FakeMjBatch()
backend = object.__new__(MuJoCoBackend)
backend._pre_step_control_fn = pre_step_control_fn
backend._tracked_sensor_copyout_range = None
backend._num_envs = 1
backend._np_dtype = np.float32
backend.nq = pool._nq
Expand Down Expand Up @@ -219,7 +239,7 @@ def test_mujoco_step_writes_xfrc_absolutely_and_clears_pending() -> None:
np.testing.assert_allclose(backend._pending_xfrc_applied, [[0.0] * 6])


def test_mujoco_step_with_pre_step_control_stages_xfrc_before_dispatch() -> None:
def test_mujoco_step_with_pre_step_control_applies_staged_xfrc_each_substep() -> None:
backend = _fake_mujoco_backend()
backend._pending_xfrc_applied = np.full((1, 6), 7.0, dtype=np.float64)

Expand All @@ -230,11 +250,18 @@ def test_mujoco_step_with_pre_step_control_stages_xfrc_before_dispatch() -> None

pool = backend._pool
assert len(pool.step_calls) == 1
# The callback protocol writes ctrl only; the wrench rides its own channel.
# The staged wrench is recomposed onto the persistent channel before every
# substep; each recorded channel is the wrench that substep's physics
# actually consumed.
assert len(pool.callback_controls) == 2
for cb_control in pool.callback_controls:
np.testing.assert_allclose(cb_control, [[1.5, 0.5]])
np.testing.assert_allclose(backend._xfrc_view.reshape(1, -1), [[7.0] * 6])
assert len(pool.callback_channels) == 2
for channel in pool.callback_channels:
np.testing.assert_allclose(channel.reshape(1, -1), [[7.0] * 6])
# The call finishes by clearing the persistent channel and the staging
# buffer so nothing leaks into the next control step.
np.testing.assert_allclose(backend._xfrc_view.reshape(1, -1), [[0.0] * 6])
np.testing.assert_allclose(backend._pending_xfrc_applied, [[0.0] * 6])


Expand Down
4 changes: 3 additions & 1 deletion tests/base/test_isaacgym_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,9 @@ def test_dependencies_missing_runtime_is_actionable(
) -> None:
monkeypatch.setenv(ENV_HOME, str(tmp_path / "empty"))
monkeypatch.delenv(ENV_PYTHON, raising=False)
with pytest.raises(IsaacGymDependencyError, match="setup_isaacgym_env.sh"):
with pytest.raises(
IsaacGymDependencyError, match="could not find the Python 3.8 worker interpreter"
):
resolve_isaacgym_runtime()


Expand Down
4 changes: 3 additions & 1 deletion tests/base/test_isaacsim_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ def test_dependencies_override_and_missing_layout(
empty = tmp_path / "empty"
monkeypatch.setenv(ENV_HOME, str(empty))
monkeypatch.delenv(ENV_PYTHON, raising=False)
with pytest.raises(IsaacSimDependencyError, match="setup_isaacsim_env.sh"):
with pytest.raises(
IsaacSimDependencyError, match="could not find the Python 3.11 worker interpreter"
):
resolve_isaacsim_runtime()


Expand Down
Loading
Loading