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
10 changes: 10 additions & 0 deletions docs/sphinx/source/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ PyPI 版本变更与未发布变更;发布日期采用 PyPI 上传日期。完

### Changed / 变更

- Raised the base and optional SuperDex UniSim requirements to
`unisim-core>=1.7.1`. The released packages contain portable MJCF profile
v1 and the IsaacSim worker dependency-isolation fix, so consumer acceptance
no longer uses the provisional local-checkout skip and validates the
robot/object/table/mirror fixture from PyPI dependencies (#1609; UniSim
#154/#155).
将基础与可选 SuperDex 的 UniSim 依赖提升到 `unisim-core>=1.7.1`。已发布
包包含 portable MJCF profile v1 与 IsaacSim worker 依赖隔离修复,因此
消费验收不再使用临时本地 checkout skip,并基于 PyPI 依赖验证
robot/object/table/mirror fixture(#1609;UniSim #154/#155)。
- Raised the base UniSim requirement to `unisim-core>=1.4.1` to consume the
published IsaacGym fixed-variant adapter. The optional `superdex` extra's
own `>=1.4.0` constraint is unchanged: the base requirement already forces
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ A mapped logical root must name its physical entity's declared root exactly. Bin

The existing `ResetStateTransaction` stages one public `SceneResetRequest` for mapped scenes. Missing fields and unselected entities/environments remain unchanged. Per-environment defaults come from `get_entity_default_state`, and `restore_default_controls` restores keyframe controls in the same commit; controls need not equal joint positions. No engine-private tensors or asset parsing enter manager terms. Scalar hinge/slide joints and a common selected environment set per transaction are the current consumer boundary; unsupported mixed DR/mocap or row patterns fail explicitly.

`tests/envs/test_multi_entity_consumer.py` registers one primitive task with the same pickleable EnvFactory for MuJoCo and IsaacSim. It checks observation/action dimensions, passive joints, selected resets, variants and a kinematic mirror. The native IsaacSim cases require `UNILAB_TEST_M2_ISAACSIM=1`. The consumer requires released `unisim-core>=1.5.0`; normal and ROCm lock profiles resolve the PyPI package without a Git source override. `UNILAB_LOCAL_UNISIM` remains an explicit alternative for local development. See [UniSim roadmap #108](https://github.com/unilabsim/unisim/issues/108) for implementation and verification scope.
`tests/envs/test_multi_entity_consumer.py` registers one primitive task with the same pickleable EnvFactory for MuJoCo and IsaacSim. It checks observation/action dimensions, passive joints, selected resets, variants and a kinematic mirror. The portable-profile fixture also combines a robot, passive object, table and collision-free mirror with the non-round-robin N5/K2 assignment `[1,1,0,1,0]`. The native IsaacSim cases require `UNILAB_TEST_M2_ISAACSIM=1`. The consumer requires released `unisim-core>=1.7.1`; normal and ROCm lock profiles resolve the PyPI package without a Git source override. `UNILAB_LOCAL_UNISIM` remains an explicit alternative for local development. See [UniSim roadmap #154](https://github.com/unilabsim/unisim/issues/154) and [UniSim contract #155](https://github.com/unilabsim/unisim/issues/155) for implementation and verification scope.

## Evidence In Repo

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Issue #1599 的 M2 消费层将 UniSim 物理实体声明与 UniLab 逻辑 selec

既有 `ResetStateTransaction` 为 mapped scene 暂存一次公共 `SceneResetRequest`。缺失字段、未选实体和环境保持不变。逐环境默认值来自 `get_entity_default_state`,`restore_default_controls` 在同次提交中恢复 keyframe control,控制值不必等于关节位置。Manager term 不接触引擎私有 tensor 或资产解析。当前消费边界为标量 hinge/slide 和一次事务共用选中环境集合;不支持的 DR/mocap 混写或行模式明确拒绝。

`tests/envs/test_multi_entity_consumer.py` 为 MuJoCo 和 IsaacSim 注册同一个 primitive task,并使用同一个可 pickle 的 EnvFactory。测试检查观测/动作维度、被动关节、局部 reset、variants 和 kinematic mirror。原生 IsaacSim case 通过 `UNILAB_TEST_M2_ISAACSIM=1` 启用。消费层要求已发布的 `unisim-core>=1.5.0`;标准与 ROCm 锁文件均解析 PyPI 包,不使用 Git source 覆盖。`UNILAB_LOCAL_UNISIM` 仍是显式本地开发替代方案。实现和验证边界见 [UniSim roadmap #108](https://github.com/unilabsim/unisim/issues/108)。
`tests/envs/test_multi_entity_consumer.py` 为 MuJoCo 和 IsaacSim 注册同一个 primitive task,并使用同一个可 pickle 的 EnvFactory。测试检查观测/动作维度、被动关节、局部 reset、variants 和 kinematic mirror。portable-profile fixture 还组合 robot、被动 object、table 与 collision-free mirror,并使用非 round-robin 的 N5/K2 assignment `[1,1,0,1,0]`。原生 IsaacSim case 通过 `UNILAB_TEST_M2_ISAACSIM=1` 启用。消费层要求已发布的 `unisim-core>=1.7.1`;标准与 ROCm 锁文件均解析 PyPI 包,不使用 Git source 覆盖。`UNILAB_LOCAL_UNISIM` 仍是显式本地开发替代方案。实现和验证边界见 [UniSim roadmap #154](https://github.com/unilabsim/unisim/issues/154) 与 [UniSim contract #155](https://github.com/unilabsim/unisim/issues/155)。

## 仓库中的证据

Expand Down
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ dependencies = [
# unisim-core package. Version 1.5.0 provides the M2 physical entity,
# immutable variant, layout and selected-reset contracts; 1.5.1 adds
# get_motion_body_ids on the newton/genesis adapters; 1.6.0 adds Newton
# CUDA graph execution.
"unisim-core>=1.6.0",
# CUDA graph execution; 1.7.0 adds the portable MJCF scene profile and
# 1.7.1 isolates the IsaacSim worker from host compiled dependencies.
"unisim-core>=1.7.1",
# 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 @@ -159,7 +160,7 @@ genesis = [
"genesis-world==1.3.3",
]
viser = ["viser>=1.0.26", "trimesh>=3.21.7"]
# SuperDex Physics/Robotics 1.0.0 ships as published wheels; the exact pins and
# SuperDex Physics/Robotics 1.1.0 ships as published wheels; the exact pins and
# the temporary superdex-uni build names are owned by unisim-core's superdex
# extra (they revert to superdex-physics/-robotics once the upstream
# project_superdex release lands). That extra already carries plain `mujoco`
Expand All @@ -171,7 +172,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.6.0 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'",
"unisim-core[superdex]>=1.7.1 ; python_version >= '3.12' and sys_platform == 'linux' and platform_machine == 'x86_64'",
]

[dependency-groups]
Expand Down
6 changes: 6 additions & 0 deletions tests/base/test_backend_pre_step_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ def _fake_motrix_backend(pre_step_control_fn=None):
from unisim.backend.motrix.backend import MotrixBackend

backend = object.__new__(MotrixBackend)
backend._portable_mode = False
backend._pre_step_control_fn = pre_step_control_fn
backend._model = _FakeMotrixModel()
backend._data = SimpleNamespace(
Expand Down Expand Up @@ -383,6 +384,7 @@ def sync(self, *, data, wait=False):
monkeypatch.setattr(mod, "RenderSettings", FakeRenderSettings, raising=False)

backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._model = FakeModel()
backend._data = object()
backend._num_envs = 3
Expand Down Expand Up @@ -468,6 +470,7 @@ def launch(self, model, *, batch, render_offset, render_settings):
monkeypatch.setattr(mod, "RenderSettings", FakeRenderSettings, raising=False)

backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._model = FakeModel()
backend._num_envs = 4
backend._render_app = None
Expand Down Expand Up @@ -561,6 +564,7 @@ def sync(self, *, data, wait=False):
monkeypatch.setattr(mod, "RenderSettings", FakeRenderSettings, raising=False)

backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._model = FakeModel()
backend._data = object()
backend._num_envs = 3
Expand Down Expand Up @@ -652,6 +656,7 @@ def set_main_camera(self, camera):
monkeypatch.setattr(mod, "RenderSettings", FakeRenderSettings, raising=False)

backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._model = FakeModel()
backend._num_envs = 1
backend._render_app = None
Expand Down Expand Up @@ -707,6 +712,7 @@ def launch(self, model, *, batch, render_offset, render_settings):
monkeypatch.setattr(mod, "RenderSettings", FakeRenderSettings, raising=False)

backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._model = FakeModel()
backend._num_envs = 4
backend._render_app = None
Expand Down
2 changes: 2 additions & 0 deletions tests/base/test_entity_scene_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ def test_per_environment_entity_defaults_are_not_broadcast_from_first_variant():
for entity in cfg.scene.entity_assets:
if entity.name == "object":
continue
if entity.source is None:
continue
path = Path(entity.source.model_file)
key = (
'<key name="home" qpos="0.25" ctrl="0.37"/>'
Expand Down
2 changes: 1 addition & 1 deletion tests/base/test_genesis_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
<framezaxis objtype="site" objname="imu_site" name="base_up"/>
<framepos objtype="site" objname="foot_site" name="foot_pos"/>
<framequat objtype="site" objname="foot_site" name="foot_quat"/>
<contact name="foot_contact" geom1="floor" geom2="foot_geom" data="found" num="1" reduce="mindist"/>
<contact name="foot_contact" geom1="floor" geom2="foot_geom" data="found" num="1"/>
</sensor>
<keyframe>
<key name="stand" qpos="0 0 0.8 1 0 0 0 0.1 -0.2"/>
Expand Down
8 changes: 6 additions & 2 deletions tests/base/test_isaacsim_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import pytest
from unisim.backend.base import CameraCfg, RenderClosedError
from unisim.backend.isaacgym.backend import IsaacGymWorkerError
from unisim.backend.isaacsim import dependencies as isaacsim_dependencies
from unisim.backend.isaacsim.backend import (
IsaacSimBackend,
IsaacSimRenderError,
Expand Down Expand Up @@ -165,7 +166,10 @@ def test_dependencies_resolve_default_layout(
env = build_worker_env(runtime)
assert env["LD_LIBRARY_PATH"].split(":")[0] == str(tmp_path / "venv" / "lib")
assert env["PATH"].split(":")[0] == str(tmp_path / "venv" / "bin")
assert env["PYTHONPATH"].split(":")[0] == str(tmp_path / "IsaacLab" / "source")
python_paths = env["PYTHONPATH"].split(":")
host_package_root = Path(isaacsim_dependencies.__file__).resolve().parents[3]
assert python_paths[0] == str(tmp_path / "IsaacLab" / "source")
assert str(host_package_root) not in python_paths
assert env["OMNI_KIT_ACCEPT_EULA"] == "1"


Expand Down Expand Up @@ -220,7 +224,7 @@ def test_contact_sensor_is_explicitly_unsupported(backend: IsaacSimBackend) -> N
metadata = backend._scene_metadata
assert metadata is not None
assert "foot_contact" in metadata.unsupported_sensors
with pytest.raises(NotImplementedError, match="contact-force reporting"):
with pytest.raises(NotImplementedError, match="PhysX collision-pair force reporter"):
backend.get_sensor_data("foot_contact")
# Non-contact sensors remain available through the inherited cached path.
assert backend.get_sensor_data("base_gyro").shape == (NUM_ENVS, 3)
Expand Down
9 changes: 9 additions & 0 deletions tests/base/test_motrix_backend_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ def test_motrix_backend_dr_capabilities_include_pd_gains_when_overrides_availabl
import unisim.backend.motrix.backend as mod

backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._supports_position_actuator_gains = True
backend._supports_geom_friction_override = True
backend._supports_gravity_override = True
Expand All @@ -290,6 +291,7 @@ def test_motrix_backend_uses_cached_batch_link_velocities() -> None:
import unisim.backend.motrix.backend as mod

backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._link_velocities = np.arange(2 * 3 * 6, dtype=np.float32).reshape(2, 3, 6)
backend._link_velocity_cache_valid = True

Expand Down Expand Up @@ -319,6 +321,7 @@ def test_motrix_copy_body_state_uses_cached_state_and_reuses_scratch() -> None:
)

backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._num_envs = num_envs
backend._np_dtype = np.float32
backend._link_poses = poses
Expand Down Expand Up @@ -362,6 +365,7 @@ def test_motrix_backend_get_body_pose_w_slices_cached_poses_once() -> None:
import unisim.backend.motrix.backend as mod

backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._link_poses = np.asarray(
[
[
Expand Down Expand Up @@ -390,6 +394,7 @@ def test_motrix_root_layout_uses_selected_body_floating_base_indices() -> None:
"floating": SimpleNamespace(floatingbase=floating_base),
}
backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._model = SimpleNamespace(get_body=lambda name: bodies.get(name))

layout = backend.get_root_state_layout("floating")
Expand Down Expand Up @@ -427,6 +432,7 @@ def test_motrix_backend_applies_body_mass_and_ipos_reset_payload() -> None:
link1.index = 1
link1.name = "object"
backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend.backend_type = "motrix"
backend._model = SimpleNamespace(num_links=2)
backend._links_by_id = {0: link0, 1: link1}
Expand Down Expand Up @@ -462,6 +468,7 @@ def test_motrix_backend_applies_geom_friction_reset_payload() -> None:
geom1 = _FakeMotrixGeom(name="object")
geom1.index = 1
backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend.backend_type = "motrix"
backend._model = SimpleNamespace(num_links=1, num_geoms=2)
backend._geoms_by_id = {0: geom0, 1: geom1}
Expand Down Expand Up @@ -524,6 +531,7 @@ def test_motrix_backend_applies_gravity_reset_payload() -> None:

fake_model = _FakeMotrixModel()
backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend.backend_type = "motrix"
backend._model = fake_model
backend._supports_gravity_override = True
Expand All @@ -548,6 +556,7 @@ def test_motrix_backend_interval_body_force_uses_link_external_force_delta() ->

link = _FakeMotrixLink()
backend = object.__new__(mod.MotrixBackend)
backend._portable_mode = False
backend._num_envs = 2
backend._data = object()
backend._links_by_id = {0: link}
Expand Down
59 changes: 57 additions & 2 deletions tests/envs/test_multi_entity_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import pytest
from unisim.dr.types import FixedVariantPlan, ModelSourceDescriptor
from unisim.entities import EntityInitialState, EntityVariantBinding, SceneEntitySpec
from unisim.scene_compiler import PORTABLE_MJCF_PROFILE_ID

from unilab.base import registry
from unilab.base.entity import EntityCfg
Expand Down Expand Up @@ -103,7 +104,7 @@ def make_fixture_cfg():
return build_fixture_cfg(passive=True)


def build_fixture_cfg(*, passive, num_envs=2):
def build_fixture_cfg(*, passive: bool, num_envs: int = 2, mirror: bool | None = None):
robot, variants, table = _sources(passive)
entities = [
SceneEntitySpec(
Expand Down Expand Up @@ -139,7 +140,8 @@ def build_fixture_cfg(*, passive, num_envs=2):
root_body_name="table/base", physical_entity="table", joint_names=(), actuator_names=()
),
}
if not passive:
include_mirror = not passive if mirror is None else mirror
if include_mirror:
entities.append(
SceneEntitySpec(
"target",
Expand Down Expand Up @@ -215,6 +217,59 @@ def test_registry_factory_mujoco_multi_entity_reset_isolation(num_envs):
env.close()


def test_registry_factory_mujoco_consumes_portable_profile_operation_fixture():
assert PORTABLE_MJCF_PROFILE_ID == "portable-mjcf-v1"
factory = pickle.loads(pickle.dumps(registry_env_factory(TASK, "mujoco")))
env = factory(
num_envs=5,
env_cfg_override={"scene": build_fixture_cfg(passive=True, num_envs=5, mirror=True).scene},
)
try:
state = env.init_state()
assert state.obs["obs"].shape == (5, 4)
assert env.action_space.shape == (1,)
assert env.scene["object"].data.joint_pos.shape == (5, 1)

mirror_before = env.scene["target"].data.root_link_pose_w.copy()
with env._reset_state.scoped(np.array([3])):
env.scene["object"].write_root_link_pose_to_sim(
np.array([[0.4, -0.2, 1.5, 1.0, 0.0, 0.0, 0.0]]), env_ids=np.array([3])
)
np.testing.assert_allclose(env.scene["object"].data.root_link_pos_w[3], [0.4, -0.2, 1.5])
np.testing.assert_array_equal(env.scene["target"].data.root_link_pose_w, mirror_before)

env.step(np.full((5, 1), 0.25, dtype=np.float32))
np.testing.assert_allclose(env._control, 0.25)
env.reset(env_ids=np.array([2]))
np.testing.assert_allclose(env._control[:, 0], [0.25, 0.25, 0.0, 0.25, 0.25])

report = env._backend.get_import_report()
entity_fields = [field for field in report.fields if field.field.startswith("entity.")]
assert {field.scope.entity for field in entity_fields} == {
"robot",
"object",
"table",
"target",
}
assert all(field.effective is not None for field in entity_fields)

masses = []
for env_index in range(5):
playback = env.get_playback_model(env_index)
masses.append(float(np.asarray(playback.body("object/base").mass).reshape(-1)[0]))
for geom_name in ("target/object_geom", "target/lid_geom"):
mirror = playback.geom(geom_name)
np.testing.assert_array_equal(
(np.asarray(mirror.contype).item(), np.asarray(mirror.conaffinity).item()),
(0, 0),
)
for entity_name in ("robot", "object", "table", "target"):
assert playback.body(f"{entity_name}/base")
np.testing.assert_allclose(masses, [2.0, 2.0, 1.0, 2.0, 1.0])
finally:
env.close()


def test_factory_can_be_unpickled_in_a_fresh_process_without_parent_registry():
encoded = base64.b64encode(pickle.dumps(registry_env_factory(TASK, "mujoco"))).decode()
script = """
Expand Down
2 changes: 2 additions & 0 deletions tests/training/test_training_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ class FakeEnv:
cfg = type("Cfg", (), {"render_spacing": 1.0, "ctrl_dt": 0.02})()

backend = MotrixBackend.__new__(MotrixBackend)
backend._portable_mode = False
backend.init_renderer = lambda **kwargs: None

def _render():
Expand Down Expand Up @@ -566,6 +567,7 @@ class FakeEnv:
cfg = type("Cfg", (), {"render_spacing": 1.0, "ctrl_dt": 0.02})()

backend = MotrixBackend.__new__(MotrixBackend)
backend._portable_mode = False
backend.init_renderer = lambda **kwargs: None

def _capture_video_frame():
Expand Down
Loading
Loading