inspect-robots-isaacsim 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inspect_robots_isaacsim-0.1.0/.gitignore +9 -0
- inspect_robots_isaacsim-0.1.0/PKG-INFO +158 -0
- inspect_robots_isaacsim-0.1.0/README.md +133 -0
- inspect_robots_isaacsim-0.1.0/pyproject.toml +63 -0
- inspect_robots_isaacsim-0.1.0/src/inspect_robots_isaacsim/__init__.py +36 -0
- inspect_robots_isaacsim-0.1.0/src/inspect_robots_isaacsim/embodiment.py +327 -0
- inspect_robots_isaacsim-0.1.0/src/inspect_robots_isaacsim/py.typed +0 -0
- inspect_robots_isaacsim-0.1.0/tests/test_isaacsim_embodiment.py +246 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: inspect-robots-isaacsim
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: IsaacSim / Isaac Lab embodiment adapter for Inspect Robots — run Inspect Robots evals against an Isaac Lab simulation.
|
|
5
|
+
Project-URL: Homepage, https://github.com/robocurve/inspect-robots/tree/main/plugins/inspect-robots-isaacsim
|
|
6
|
+
Project-URL: Repository, https://github.com/robocurve/inspect-robots
|
|
7
|
+
Author: Inspect Robots contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: evaluation,inspect_robots,isaac-lab,isaac-sim,robotics,vla
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: inspect-robots>=0.2
|
|
18
|
+
Requires-Dist: numpy>=1.24
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# inspect-robots-isaacsim
|
|
27
|
+
|
|
28
|
+
An [Isaac Lab](https://isaac-sim.github.io/IsaacLab/) (Isaac Sim) **embodiment**
|
|
29
|
+
plugin for [Inspect Robots](https://github.com/robocurve/inspect-robots) — the "Inspect AI
|
|
30
|
+
for robotics".
|
|
31
|
+
|
|
32
|
+
Inspect Robots factors a robotics eval into two swappable inputs: a `Policy` (the VLA
|
|
33
|
+
"brain") and an `Embodiment` (the "body + world"). This package supplies the
|
|
34
|
+
second one, backed by a real Isaac Lab physics simulation, so you can run any
|
|
35
|
+
compatible VLA against your Isaac Sim setup.
|
|
36
|
+
|
|
37
|
+
The default profile is a **7-DoF Franka Panda under joint-position control** with
|
|
38
|
+
a binary gripper (action dimension `= num_arm_joints + 1 = 8`).
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
This package installs and registers on any machine, but `reset()`/`step()` need
|
|
43
|
+
a working **Isaac Lab** environment (NVIDIA Omniverse + GPU) — Isaac Sim is not a
|
|
44
|
+
PyPI dependency and is imported lazily.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# inside the conda/venv that already has isaaclab + isaacsim available
|
|
48
|
+
pip install inspect-robots inspect-robots-isaacsim
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Use it
|
|
52
|
+
|
|
53
|
+
The embodiment is discovered through the `inspect_robots.embodiments` entry point, so
|
|
54
|
+
it appears in the CLI without any import:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
inspect-robots list embodiments # -> includes "isaacsim"
|
|
58
|
+
|
|
59
|
+
inspect-robots run \
|
|
60
|
+
--task my-benchmark \
|
|
61
|
+
--policy my-vla \
|
|
62
|
+
--embodiment isaacsim \
|
|
63
|
+
-E task_id=Isaac-Lift-Cube-Franka-v0 \
|
|
64
|
+
-E headless=true
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Or programmatically:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from inspect_robots import eval
|
|
71
|
+
|
|
72
|
+
logs = eval("my-benchmark", "my-vla", "isaacsim")
|
|
73
|
+
print(logs[0].results.metrics)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Constructing directly (e.g. for a non-Franka arm or extra cameras):
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from inspect_robots_isaacsim import IsaacSimEmbodiment
|
|
80
|
+
|
|
81
|
+
emb = IsaacSimEmbodiment(
|
|
82
|
+
task_id="Isaac-Open-Drawer-Franka-v0",
|
|
83
|
+
num_arm_joints=7,
|
|
84
|
+
cameras=[("base_rgb", 224, 224), ("wrist_rgb", 224, 224)],
|
|
85
|
+
control_hz=30.0,
|
|
86
|
+
headless=True,
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Compatibility
|
|
91
|
+
|
|
92
|
+
Inspect Robots fail-fast-checks the `(policy, embodiment)` pair before any rollout. To
|
|
93
|
+
run against this embodiment your policy must emit **8-D `joint_pos` actions**
|
|
94
|
+
(`control_mode="joint_pos"`, `gripper="binary"`) and require only observation
|
|
95
|
+
keys this task provides. The mock `cubepick` policies (2-D `eef_delta_pos`) are
|
|
96
|
+
intentionally **incompatible** — bring a Franka-trained VLA.
|
|
97
|
+
|
|
98
|
+
### Mapping your task
|
|
99
|
+
|
|
100
|
+
Isaac Lab tasks vary in their observation-dict layout and success signal. The
|
|
101
|
+
constructor exposes hooks so you don't edit the adapter:
|
|
102
|
+
|
|
103
|
+
| Argument | Purpose |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `obs_group` | top-level obs-dict group to read (default `"policy"`) |
|
|
106
|
+
| `image_keys` / `state_keys` | map Inspect Robots keys → your task's raw dict keys |
|
|
107
|
+
| `success_info_key` | where the task reports success in `info` (default `"success"`) |
|
|
108
|
+
| `num_arm_joints` | arm DoF; action dim is this `+ 1` for the gripper |
|
|
109
|
+
|
|
110
|
+
## Memory & GPU hygiene
|
|
111
|
+
|
|
112
|
+
Long unattended evals should not creep in RAM or VRAM. This adapter is built to
|
|
113
|
+
hold nothing per step, but a few usage rules keep a full run leak-free:
|
|
114
|
+
|
|
115
|
+
- **Free the simulator when done.** `eval()` closes what it resolves: an
|
|
116
|
+
embodiment looked up by **registry name** (e.g. `embodiment="isaacsim"`) is
|
|
117
|
+
closed when the run finishes. An embodiment object *you* construct is yours
|
|
118
|
+
to close — an open `SimulationApp` holds GPU memory until the process exits,
|
|
119
|
+
so use it as a context manager (or `close()` in a `finally`):
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
with IsaacSimEmbodiment() as emb:
|
|
123
|
+
eval("my-bench", "my-vla", emb)
|
|
124
|
+
# GPU + sim torn down here
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`close()` is idempotent and safe to call before launch or twice.
|
|
128
|
+
|
|
129
|
+
- **One simulator per process.** Isaac Sim is a hard process singleton; if you
|
|
130
|
+
construct several embodiments the adapter reuses the one live `SimulationApp`
|
|
131
|
+
rather than launching (and leaking) a second.
|
|
132
|
+
|
|
133
|
+
- **Stream frames to disk for long episodes.** Inspect Robots keeps each step's
|
|
134
|
+
observation — including camera frames — in the per-trial record. Pass
|
|
135
|
+
`store_frames=True` to `eval()` so frames go to disk side-cars instead of RAM.
|
|
136
|
+
(Per-trial records are released after each scene is scored, so there is no
|
|
137
|
+
cross-trial accumulation regardless.)
|
|
138
|
+
|
|
139
|
+
- **Skip rendering you don't need.** If your scorer is state/oracle based, build
|
|
140
|
+
the embodiment with `cameras=()` to avoid allocating images at all.
|
|
141
|
+
|
|
142
|
+
The adapter never stores tensors on `self`, and copies observations off Isaac's
|
|
143
|
+
reused buffers (`.astype`), so a step loop holds no growing references — a
|
|
144
|
+
`tracemalloc` regression test asserts flat RAM over thousands of steps.
|
|
145
|
+
|
|
146
|
+
## Develop / test
|
|
147
|
+
|
|
148
|
+
The test suite runs **without Isaac** (it checks spaces, semantics, protocol
|
|
149
|
+
conformance, compatibility, and registry wiring):
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install -e ".[dev]"
|
|
153
|
+
pytest -q
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# inspect-robots-isaacsim
|
|
2
|
+
|
|
3
|
+
An [Isaac Lab](https://isaac-sim.github.io/IsaacLab/) (Isaac Sim) **embodiment**
|
|
4
|
+
plugin for [Inspect Robots](https://github.com/robocurve/inspect-robots) — the "Inspect AI
|
|
5
|
+
for robotics".
|
|
6
|
+
|
|
7
|
+
Inspect Robots factors a robotics eval into two swappable inputs: a `Policy` (the VLA
|
|
8
|
+
"brain") and an `Embodiment` (the "body + world"). This package supplies the
|
|
9
|
+
second one, backed by a real Isaac Lab physics simulation, so you can run any
|
|
10
|
+
compatible VLA against your Isaac Sim setup.
|
|
11
|
+
|
|
12
|
+
The default profile is a **7-DoF Franka Panda under joint-position control** with
|
|
13
|
+
a binary gripper (action dimension `= num_arm_joints + 1 = 8`).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
This package installs and registers on any machine, but `reset()`/`step()` need
|
|
18
|
+
a working **Isaac Lab** environment (NVIDIA Omniverse + GPU) — Isaac Sim is not a
|
|
19
|
+
PyPI dependency and is imported lazily.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# inside the conda/venv that already has isaaclab + isaacsim available
|
|
23
|
+
pip install inspect-robots inspect-robots-isaacsim
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Use it
|
|
27
|
+
|
|
28
|
+
The embodiment is discovered through the `inspect_robots.embodiments` entry point, so
|
|
29
|
+
it appears in the CLI without any import:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
inspect-robots list embodiments # -> includes "isaacsim"
|
|
33
|
+
|
|
34
|
+
inspect-robots run \
|
|
35
|
+
--task my-benchmark \
|
|
36
|
+
--policy my-vla \
|
|
37
|
+
--embodiment isaacsim \
|
|
38
|
+
-E task_id=Isaac-Lift-Cube-Franka-v0 \
|
|
39
|
+
-E headless=true
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or programmatically:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from inspect_robots import eval
|
|
46
|
+
|
|
47
|
+
logs = eval("my-benchmark", "my-vla", "isaacsim")
|
|
48
|
+
print(logs[0].results.metrics)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Constructing directly (e.g. for a non-Franka arm or extra cameras):
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from inspect_robots_isaacsim import IsaacSimEmbodiment
|
|
55
|
+
|
|
56
|
+
emb = IsaacSimEmbodiment(
|
|
57
|
+
task_id="Isaac-Open-Drawer-Franka-v0",
|
|
58
|
+
num_arm_joints=7,
|
|
59
|
+
cameras=[("base_rgb", 224, 224), ("wrist_rgb", 224, 224)],
|
|
60
|
+
control_hz=30.0,
|
|
61
|
+
headless=True,
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Compatibility
|
|
66
|
+
|
|
67
|
+
Inspect Robots fail-fast-checks the `(policy, embodiment)` pair before any rollout. To
|
|
68
|
+
run against this embodiment your policy must emit **8-D `joint_pos` actions**
|
|
69
|
+
(`control_mode="joint_pos"`, `gripper="binary"`) and require only observation
|
|
70
|
+
keys this task provides. The mock `cubepick` policies (2-D `eef_delta_pos`) are
|
|
71
|
+
intentionally **incompatible** — bring a Franka-trained VLA.
|
|
72
|
+
|
|
73
|
+
### Mapping your task
|
|
74
|
+
|
|
75
|
+
Isaac Lab tasks vary in their observation-dict layout and success signal. The
|
|
76
|
+
constructor exposes hooks so you don't edit the adapter:
|
|
77
|
+
|
|
78
|
+
| Argument | Purpose |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `obs_group` | top-level obs-dict group to read (default `"policy"`) |
|
|
81
|
+
| `image_keys` / `state_keys` | map Inspect Robots keys → your task's raw dict keys |
|
|
82
|
+
| `success_info_key` | where the task reports success in `info` (default `"success"`) |
|
|
83
|
+
| `num_arm_joints` | arm DoF; action dim is this `+ 1` for the gripper |
|
|
84
|
+
|
|
85
|
+
## Memory & GPU hygiene
|
|
86
|
+
|
|
87
|
+
Long unattended evals should not creep in RAM or VRAM. This adapter is built to
|
|
88
|
+
hold nothing per step, but a few usage rules keep a full run leak-free:
|
|
89
|
+
|
|
90
|
+
- **Free the simulator when done.** `eval()` closes what it resolves: an
|
|
91
|
+
embodiment looked up by **registry name** (e.g. `embodiment="isaacsim"`) is
|
|
92
|
+
closed when the run finishes. An embodiment object *you* construct is yours
|
|
93
|
+
to close — an open `SimulationApp` holds GPU memory until the process exits,
|
|
94
|
+
so use it as a context manager (or `close()` in a `finally`):
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
with IsaacSimEmbodiment() as emb:
|
|
98
|
+
eval("my-bench", "my-vla", emb)
|
|
99
|
+
# GPU + sim torn down here
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`close()` is idempotent and safe to call before launch or twice.
|
|
103
|
+
|
|
104
|
+
- **One simulator per process.** Isaac Sim is a hard process singleton; if you
|
|
105
|
+
construct several embodiments the adapter reuses the one live `SimulationApp`
|
|
106
|
+
rather than launching (and leaking) a second.
|
|
107
|
+
|
|
108
|
+
- **Stream frames to disk for long episodes.** Inspect Robots keeps each step's
|
|
109
|
+
observation — including camera frames — in the per-trial record. Pass
|
|
110
|
+
`store_frames=True` to `eval()` so frames go to disk side-cars instead of RAM.
|
|
111
|
+
(Per-trial records are released after each scene is scored, so there is no
|
|
112
|
+
cross-trial accumulation regardless.)
|
|
113
|
+
|
|
114
|
+
- **Skip rendering you don't need.** If your scorer is state/oracle based, build
|
|
115
|
+
the embodiment with `cameras=()` to avoid allocating images at all.
|
|
116
|
+
|
|
117
|
+
The adapter never stores tensors on `self`, and copies observations off Isaac's
|
|
118
|
+
reused buffers (`.astype`), so a step loop holds no growing references — a
|
|
119
|
+
`tracemalloc` regression test asserts flat RAM over thousands of steps.
|
|
120
|
+
|
|
121
|
+
## Develop / test
|
|
122
|
+
|
|
123
|
+
The test suite runs **without Isaac** (it checks spaces, semantics, protocol
|
|
124
|
+
conformance, compatibility, and registry wiring):
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install -e ".[dev]"
|
|
128
|
+
pytest -q
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "inspect-robots-isaacsim"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "IsaacSim / Isaac Lab embodiment adapter for Inspect Robots — run Inspect Robots evals against an Isaac Lab simulation."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Inspect Robots contributors" }]
|
|
13
|
+
keywords = ["robotics", "isaac-sim", "isaac-lab", "inspect_robots", "vla", "evaluation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
# NOTE: `isaacsim` / `isaaclab` are intentionally NOT declared here. Isaac Sim and
|
|
23
|
+
# Isaac Lab are not installable from PyPI as ordinary wheels — they are provided by
|
|
24
|
+
# the user's NVIDIA Omniverse / Isaac Lab environment (and need a GPU). This adapter
|
|
25
|
+
# imports them lazily so the package installs, registers, and is discoverable
|
|
26
|
+
# (`inspect-robots list embodiments`) on any machine; only `reset()`/`step()` require Isaac.
|
|
27
|
+
dependencies = ["inspect-robots>=0.2", "numpy>=1.24"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = ["pytest>=8.0", "pytest-cov>=5.0", "mypy>=1.11", "ruff>=0.6"]
|
|
31
|
+
|
|
32
|
+
# Entry-point discovery: an installed inspect-robots-isaacsim appears in `inspect-robots list`
|
|
33
|
+
# without being imported first. The registry calls this factory by the name `isaacsim`.
|
|
34
|
+
[project.entry-points."inspect_robots.embodiments"]
|
|
35
|
+
isaacsim = "inspect_robots_isaacsim:isaacsim_embodiment"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/robocurve/inspect-robots/tree/main/plugins/inspect-robots-isaacsim"
|
|
39
|
+
Repository = "https://github.com/robocurve/inspect-robots"
|
|
40
|
+
|
|
41
|
+
# In the Inspect Robots monorepo this resolves the `inspect_robots` dependency to the in-repo
|
|
42
|
+
# core package instead of PyPI. Harmless in a standalone checkout (uv ignores a
|
|
43
|
+
# workspace source when there is no workspace root).
|
|
44
|
+
[tool.uv.sources]
|
|
45
|
+
inspect-robots = { workspace = true }
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["src/inspect_robots_isaacsim"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
include = ["src/inspect_robots_isaacsim", "tests", "README.md"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 100
|
|
55
|
+
|
|
56
|
+
[tool.mypy]
|
|
57
|
+
strict = true
|
|
58
|
+
|
|
59
|
+
# Isaac Sim / Isaac Lab / torch are not installed in lint/test environments and
|
|
60
|
+
# ship no stubs; they are imported lazily inside methods. Don't fail strict on them.
|
|
61
|
+
[[tool.mypy.overrides]]
|
|
62
|
+
module = ["isaaclab.*", "isaaclab_tasks.*", "gymnasium.*", "torch.*"]
|
|
63
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""inspect-robots-isaacsim — an Isaac Lab (Isaac Sim) embodiment for Inspect Robots.
|
|
2
|
+
|
|
3
|
+
Install this alongside a working Isaac Lab environment and the ``isaacsim``
|
|
4
|
+
embodiment becomes available to Inspect Robots::
|
|
5
|
+
|
|
6
|
+
inspect-robots list embodiments # -> includes "isaacsim"
|
|
7
|
+
inspect-robots run --task my-task --policy my-vla --embodiment isaacsim \
|
|
8
|
+
-E task_id=Isaac-Lift-Cube-Franka-v0
|
|
9
|
+
|
|
10
|
+
or programmatically::
|
|
11
|
+
|
|
12
|
+
from inspect_robots import eval
|
|
13
|
+
eval("my-task", "my-vla", "isaacsim")
|
|
14
|
+
|
|
15
|
+
The embodiment is discovered via the ``inspect_robots.embodiments`` entry point, so it
|
|
16
|
+
shows up without being imported first.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from inspect_robots_isaacsim.embodiment import IsaacSimEmbodiment
|
|
24
|
+
|
|
25
|
+
__all__ = ["IsaacSimEmbodiment", "isaacsim_embodiment"]
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def isaacsim_embodiment(**kwargs: Any) -> IsaacSimEmbodiment:
|
|
31
|
+
"""Factory the Inspect Robots registry calls (entry point ``isaacsim``).
|
|
32
|
+
|
|
33
|
+
Accepts the same keyword arguments as :class:`IsaacSimEmbodiment`; the CLI
|
|
34
|
+
forwards ``-E key=value`` pairs here.
|
|
35
|
+
"""
|
|
36
|
+
return IsaacSimEmbodiment(**kwargs)
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""An ``Embodiment`` adapter that wraps an Isaac Lab (Isaac Sim) environment.
|
|
2
|
+
|
|
3
|
+
This is the "body + world" half of a Inspect Robots eval, backed by a real physics
|
|
4
|
+
simulation. It conforms to :class:`inspect_robots.Embodiment` (the runtime-checkable
|
|
5
|
+
protocol), so once installed it can be paired with any compatible
|
|
6
|
+
:class:`inspect_robots.Policy` and run through ``inspect_robots.eval``.
|
|
7
|
+
|
|
8
|
+
Design mirrors Inspect Robots's own ``RerunSink``: the heavy, GPU-bound, non-PyPI
|
|
9
|
+
dependencies (``isaacsim`` / ``isaaclab`` / ``torch``) are imported **lazily,
|
|
10
|
+
inside methods**. Constructing the adapter and reading ``.info`` therefore work
|
|
11
|
+
on any machine (so ``inspect-robots list embodiments`` and the fail-fast compatibility
|
|
12
|
+
check run without Isaac); only :meth:`reset` / :meth:`step` actually launch the
|
|
13
|
+
simulator.
|
|
14
|
+
|
|
15
|
+
Default profile: a 7-DoF **Franka Panda** under **joint-position** control with a
|
|
16
|
+
binary gripper (action dim ``= num_arm_joints + 1``). Everything is configurable
|
|
17
|
+
so any Isaac Lab manipulation task can be wrapped.
|
|
18
|
+
|
|
19
|
+
Version note: Isaac Lab's gym wrapper returns a dict observation grouped by
|
|
20
|
+
``"policy"`` and batched over ``num_envs``. The obs/action *group keys* and the
|
|
21
|
+
``success`` signal differ across tasks; the constructor exposes ``obs_group``,
|
|
22
|
+
``image_keys``, ``state_keys`` and ``success_info_key`` so you can map your task
|
|
23
|
+
without editing this file.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from collections.abc import Mapping, Sequence
|
|
29
|
+
from typing import TYPE_CHECKING, Any
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
from inspect_robots import (
|
|
34
|
+
ActionSemantics,
|
|
35
|
+
Box,
|
|
36
|
+
CameraSpec,
|
|
37
|
+
EmbodimentInfo,
|
|
38
|
+
ObservationSpace,
|
|
39
|
+
Observation,
|
|
40
|
+
Scene,
|
|
41
|
+
StateField,
|
|
42
|
+
StateSpec,
|
|
43
|
+
StepResult,
|
|
44
|
+
)
|
|
45
|
+
from inspect_robots.spaces import CANONICAL_STATE_UNITS
|
|
46
|
+
|
|
47
|
+
if TYPE_CHECKING:
|
|
48
|
+
from inspect_robots import Action
|
|
49
|
+
|
|
50
|
+
# Isaac Lab simulators expose a privileged success oracle, are resettable and
|
|
51
|
+
# seedable, and can render — but they are NOT self-paced (they step as fast as the
|
|
52
|
+
# GPU allows; Inspect Robots owns wall-clock pacing).
|
|
53
|
+
_DEFAULT_CAPABILITIES = frozenset({"seedable", "resettable", "privileged_success", "renderable"})
|
|
54
|
+
|
|
55
|
+
# Isaac Sim allows exactly ONE SimulationApp per process. Track it module-side so a
|
|
56
|
+
# second embodiment instance reuses the live app instead of launching a duplicate
|
|
57
|
+
# (which crashes) or leaking one. Cleared by close().
|
|
58
|
+
_ACTIVE_APP: Any | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _missing_isaac(exc: ImportError) -> RuntimeError:
|
|
62
|
+
return RuntimeError(
|
|
63
|
+
"Isaac Sim / Isaac Lab is not importable in this environment "
|
|
64
|
+
f"({exc}). The inspect-robots-isaacsim adapter needs a working Isaac Lab "
|
|
65
|
+
"install (NVIDIA Omniverse + GPU). Constructing the embodiment and "
|
|
66
|
+
"reading .info work without it, but reset()/step() require it. "
|
|
67
|
+
"See: https://isaac-sim.github.io/IsaacLab/"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _default_state_fields(num_arm_joints: int) -> tuple[StateField, ...]:
|
|
72
|
+
"""Canonical proprioception for a Franka-like arm (keys/units from Inspect Robots)."""
|
|
73
|
+
|
|
74
|
+
def unit(key: str) -> str:
|
|
75
|
+
return CANONICAL_STATE_UNITS.get(key, "")
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
StateField("joint_pos", (num_arm_joints,), unit("joint_pos")),
|
|
79
|
+
StateField("joint_vel", (num_arm_joints,), unit("joint_vel")),
|
|
80
|
+
StateField("eef_pos", (3,), unit("eef_pos")),
|
|
81
|
+
StateField("eef_quat", (4,), unit("eef_quat")),
|
|
82
|
+
StateField("gripper", (1,), unit("gripper")),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class IsaacSimEmbodiment:
|
|
87
|
+
"""Wrap an Isaac Lab gym task as a Inspect Robots :class:`~inspect_robots.Embodiment`.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
task_id:
|
|
92
|
+
The Isaac Lab / gymnasium task id, e.g. ``"Isaac-Lift-Cube-Franka-v0"``.
|
|
93
|
+
num_arm_joints:
|
|
94
|
+
Number of actuated arm joints (Franka Panda = 7). The action is
|
|
95
|
+
``num_arm_joints`` joint targets plus one binary gripper command, so the
|
|
96
|
+
action dimension is ``num_arm_joints + 1``.
|
|
97
|
+
cameras:
|
|
98
|
+
Camera streams the task renders, as ``(name, height, width[, channels])``
|
|
99
|
+
tuples. Match the names your policy requires (Inspect Robots remaps if needed).
|
|
100
|
+
headless / device:
|
|
101
|
+
Forwarded to Isaac's ``AppLauncher`` / env. ``headless=True`` is required
|
|
102
|
+
on machines without a display (the usual eval box).
|
|
103
|
+
obs_group / image_keys / state_keys / success_info_key:
|
|
104
|
+
How to read the task's observation dict and success flag (see module docs).
|
|
105
|
+
name / supported_setups / supported_target_kinds:
|
|
106
|
+
Surfaced on ``EmbodimentInfo`` for logging and R7 scene-realizability
|
|
107
|
+
checks. Empty ``supported_*`` means "unconstrained".
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
task_id: str = "Isaac-Lift-Cube-Franka-v0",
|
|
113
|
+
*,
|
|
114
|
+
num_arm_joints: int = 7,
|
|
115
|
+
cameras: Sequence[tuple[str, int, int] | tuple[str, int, int, int]] = (
|
|
116
|
+
("base_rgb", 224, 224, 3),
|
|
117
|
+
),
|
|
118
|
+
control_hz: float = 30.0,
|
|
119
|
+
headless: bool = True,
|
|
120
|
+
device: str = "cuda:0",
|
|
121
|
+
obs_group: str = "policy",
|
|
122
|
+
image_keys: Mapping[str, str] | None = None,
|
|
123
|
+
state_keys: Mapping[str, str] | None = None,
|
|
124
|
+
success_info_key: str = "success",
|
|
125
|
+
name: str = "isaacsim",
|
|
126
|
+
supported_setups: Sequence[str] = (),
|
|
127
|
+
supported_target_kinds: Sequence[str] = (),
|
|
128
|
+
) -> None:
|
|
129
|
+
if num_arm_joints < 1:
|
|
130
|
+
raise ValueError("num_arm_joints must be >= 1")
|
|
131
|
+
|
|
132
|
+
self.task_id = task_id
|
|
133
|
+
self.num_arm_joints = num_arm_joints
|
|
134
|
+
self.headless = headless
|
|
135
|
+
self.device = device
|
|
136
|
+
self.obs_group = obs_group
|
|
137
|
+
# Map Inspect Robots obs keys -> the task's raw dict keys. Identity by default.
|
|
138
|
+
self.image_keys = dict(image_keys or {})
|
|
139
|
+
self.state_keys = dict(state_keys or {})
|
|
140
|
+
self.success_info_key = success_info_key
|
|
141
|
+
|
|
142
|
+
camera_specs = tuple(CameraSpec(c[0], c[1], c[2], *(c[3:] or (3,))) for c in cameras)
|
|
143
|
+
action_dim = num_arm_joints + 1 # arm joint targets + 1 binary gripper
|
|
144
|
+
|
|
145
|
+
self.info = EmbodimentInfo(
|
|
146
|
+
name=name,
|
|
147
|
+
action_space=Box(
|
|
148
|
+
shape=(action_dim,),
|
|
149
|
+
semantics=ActionSemantics(
|
|
150
|
+
control_mode="joint_pos",
|
|
151
|
+
rotation_repr="none",
|
|
152
|
+
gripper="binary",
|
|
153
|
+
frame="base",
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
observation_space=ObservationSpace(
|
|
157
|
+
cameras=camera_specs,
|
|
158
|
+
state=StateSpec(fields=_default_state_fields(num_arm_joints)),
|
|
159
|
+
),
|
|
160
|
+
control_hz=control_hz,
|
|
161
|
+
is_simulated=True,
|
|
162
|
+
capabilities=_DEFAULT_CAPABILITIES,
|
|
163
|
+
supported_setups=frozenset(supported_setups),
|
|
164
|
+
supported_target_kinds=frozenset(supported_target_kinds),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Lazily-initialised Isaac handles (None until first reset()).
|
|
168
|
+
self._app: Any | None = None
|
|
169
|
+
self._env: Any | None = None
|
|
170
|
+
self._torch: Any | None = None
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------ #
|
|
173
|
+
# Lazy Isaac bring-up (app launch and env creation are separable so the
|
|
174
|
+
# simulator boot can be validated independently of any task registry)
|
|
175
|
+
# ------------------------------------------------------------------ #
|
|
176
|
+
def _ensure_app(self) -> Any:
|
|
177
|
+
"""Launch the Isaac Sim ``SimulationApp`` on first use and return it.
|
|
178
|
+
|
|
179
|
+
If another instance already launched the app this process, reuse it —
|
|
180
|
+
Isaac Sim is a hard process singleton, so launching twice would crash.
|
|
181
|
+
"""
|
|
182
|
+
global _ACTIVE_APP
|
|
183
|
+
if self._app is not None:
|
|
184
|
+
return self._app
|
|
185
|
+
try:
|
|
186
|
+
import torch
|
|
187
|
+
except ImportError as exc: # pragma: no cover - exercised only without Isaac
|
|
188
|
+
raise _missing_isaac(exc) from exc
|
|
189
|
+
self._torch = torch
|
|
190
|
+
|
|
191
|
+
if _ACTIVE_APP is not None:
|
|
192
|
+
self._app = _ACTIVE_APP
|
|
193
|
+
return self._app
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
from isaaclab.app import AppLauncher
|
|
197
|
+
except ImportError as exc: # pragma: no cover - exercised only without Isaac
|
|
198
|
+
raise _missing_isaac(exc) from exc
|
|
199
|
+
self._app = AppLauncher(headless=self.headless, device=self.device).app
|
|
200
|
+
_ACTIVE_APP = self._app
|
|
201
|
+
return self._app
|
|
202
|
+
|
|
203
|
+
def _ensure_env(self) -> Any:
|
|
204
|
+
"""Build the gym env on first use (boots the app if needed)."""
|
|
205
|
+
if self._env is not None:
|
|
206
|
+
return self._env
|
|
207
|
+
self._ensure_app()
|
|
208
|
+
# Importing the tasks registers the gym ids; must happen AFTER the app boots.
|
|
209
|
+
import gymnasium as gym
|
|
210
|
+
import isaaclab_tasks # noqa: F401 (registers Isaac-* gym ids)
|
|
211
|
+
|
|
212
|
+
self._env = gym.make(self.task_id, num_envs=1, render_mode="rgb_array")
|
|
213
|
+
return self._env
|
|
214
|
+
|
|
215
|
+
# ------------------------------------------------------------------ #
|
|
216
|
+
# Embodiment protocol
|
|
217
|
+
# ------------------------------------------------------------------ #
|
|
218
|
+
def reset(self, scene: Scene, *, seed: int | None = None) -> Observation:
|
|
219
|
+
env = self._ensure_env()
|
|
220
|
+
obs, _info = env.reset(seed=seed)
|
|
221
|
+
return self._to_observation(obs, scene.instruction)
|
|
222
|
+
|
|
223
|
+
def step(self, action: Action) -> StepResult:
|
|
224
|
+
env = self._ensure_env()
|
|
225
|
+
torch = self._torch
|
|
226
|
+
assert torch is not None # set alongside _env
|
|
227
|
+
tensor = torch.as_tensor(
|
|
228
|
+
np.asarray(action.data, dtype=np.float32), device=self.device
|
|
229
|
+
).reshape(1, -1)
|
|
230
|
+
obs, reward, terminated, truncated, info = env.step(tensor)
|
|
231
|
+
|
|
232
|
+
success = self._read_success(info, terminated)
|
|
233
|
+
term = bool(_scalar(terminated))
|
|
234
|
+
return StepResult(
|
|
235
|
+
observation=self._to_observation(obs, None),
|
|
236
|
+
reward=float(_scalar(reward)),
|
|
237
|
+
terminated=term,
|
|
238
|
+
termination_reason="success" if (term and success) else None,
|
|
239
|
+
truncated=bool(_scalar(truncated)),
|
|
240
|
+
info={"success": success},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def close(self) -> None:
|
|
244
|
+
"""Release the gym env and Isaac Sim app (and the GPU memory they hold).
|
|
245
|
+
|
|
246
|
+
Idempotent: safe to call before launch or twice. ``eval()`` closes what
|
|
247
|
+
it resolves — an embodiment looked up by registry name is closed when
|
|
248
|
+
the run finishes, but an embodiment object you construct is yours to
|
|
249
|
+
close. Use it as a context manager (or call ``close()`` in a
|
|
250
|
+
``finally``) to guarantee the simulator is torn down and GPU memory is
|
|
251
|
+
freed when a run ends.
|
|
252
|
+
"""
|
|
253
|
+
global _ACTIVE_APP
|
|
254
|
+
if self._env is not None:
|
|
255
|
+
self._env.close()
|
|
256
|
+
self._env = None
|
|
257
|
+
if self._app is not None:
|
|
258
|
+
self._app.close()
|
|
259
|
+
if _ACTIVE_APP is self._app:
|
|
260
|
+
_ACTIVE_APP = None
|
|
261
|
+
self._app = None
|
|
262
|
+
self._torch = None
|
|
263
|
+
|
|
264
|
+
def __enter__(self) -> IsaacSimEmbodiment:
|
|
265
|
+
return self
|
|
266
|
+
|
|
267
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
268
|
+
self.close()
|
|
269
|
+
|
|
270
|
+
# ------------------------------------------------------------------ #
|
|
271
|
+
# Translation: Isaac Lab dict/tensors <-> Inspect Robots types
|
|
272
|
+
# ------------------------------------------------------------------ #
|
|
273
|
+
def _to_observation(self, raw: Any, instruction: str | None) -> Observation:
|
|
274
|
+
group = raw[self.obs_group] if isinstance(raw, Mapping) and self.obs_group in raw else raw
|
|
275
|
+
images: dict[str, np.ndarray] = {}
|
|
276
|
+
state: dict[str, np.ndarray] = {}
|
|
277
|
+
|
|
278
|
+
for cam in self.info.observation_space.cameras:
|
|
279
|
+
key = self.image_keys.get(cam.name, cam.name)
|
|
280
|
+
if isinstance(group, Mapping) and key in group:
|
|
281
|
+
images[cam.name] = _to_image(group[key])
|
|
282
|
+
|
|
283
|
+
if self.info.observation_space.state is not None:
|
|
284
|
+
for field in self.info.observation_space.state.fields:
|
|
285
|
+
key = self.state_keys.get(field.key, field.key)
|
|
286
|
+
if isinstance(group, Mapping) and key in group:
|
|
287
|
+
state[field.key] = _to_float_array(group[key])
|
|
288
|
+
|
|
289
|
+
return Observation(images=images, state=state, instruction=instruction)
|
|
290
|
+
|
|
291
|
+
def _read_success(self, info: Any, terminated: Any) -> bool:
|
|
292
|
+
if isinstance(info, Mapping) and self.success_info_key in info:
|
|
293
|
+
return bool(_scalar(info[self.success_info_key]))
|
|
294
|
+
# Fall back to "terminated implies success" when the task exposes no oracle.
|
|
295
|
+
return bool(_scalar(terminated))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# --------------------------------------------------------------------------- #
|
|
299
|
+
# Tensor/array helpers (kept torch-free at import time; operate duck-typed)
|
|
300
|
+
# --------------------------------------------------------------------------- #
|
|
301
|
+
def _np(value: Any) -> np.ndarray:
|
|
302
|
+
"""Best-effort conversion of a (possibly GPU torch) value to a NumPy array."""
|
|
303
|
+
if hasattr(value, "detach"): # torch.Tensor
|
|
304
|
+
value = value.detach().cpu().numpy()
|
|
305
|
+
return np.asarray(value)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _scalar(value: Any) -> float:
|
|
309
|
+
arr = _np(value).reshape(-1)
|
|
310
|
+
return float(arr[0]) if arr.size else 0.0
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _to_float_array(value: Any) -> np.ndarray:
|
|
314
|
+
arr = _np(value).astype(np.float64)
|
|
315
|
+
# Drop the leading num_envs axis (we run num_envs=1).
|
|
316
|
+
return arr[0] if arr.ndim >= 1 and arr.shape[0] == 1 else arr
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _to_image(value: Any) -> np.ndarray:
|
|
320
|
+
arr = _np(value)
|
|
321
|
+
if arr.ndim >= 1 and arr.shape[0] == 1: # drop num_envs axis
|
|
322
|
+
arr = arr[0]
|
|
323
|
+
if arr.ndim == 3 and arr.shape[0] in (1, 3, 4) and arr.shape[-1] not in (1, 3, 4):
|
|
324
|
+
arr = np.transpose(arr, (1, 2, 0)) # CHW -> HWC
|
|
325
|
+
if np.issubdtype(arr.dtype, np.floating):
|
|
326
|
+
arr = np.clip(arr * 255.0 if arr.max() <= 1.0 else arr, 0, 255)
|
|
327
|
+
return arr.astype(np.uint8)
|
|
File without changes
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Tests for the Isaac Lab embodiment adapter.
|
|
2
|
+
|
|
3
|
+
These run on any machine — Isaac Sim is NOT required. They verify the parts that
|
|
4
|
+
matter before a simulator ever boots: the declared spaces/semantics, protocol
|
|
5
|
+
conformance, registry resolution, Inspect Robots compatibility checking, and that
|
|
6
|
+
calling reset() without Isaac fails with a clear, actionable error.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from inspect_robots import (
|
|
17
|
+
ActionSemantics,
|
|
18
|
+
Box,
|
|
19
|
+
Embodiment,
|
|
20
|
+
ObservationSpace,
|
|
21
|
+
PolicyConfig,
|
|
22
|
+
PolicyInfo,
|
|
23
|
+
Scene,
|
|
24
|
+
)
|
|
25
|
+
from inspect_robots.compat import check_compatibility
|
|
26
|
+
from inspect_robots.errors import CompatibilityError
|
|
27
|
+
from inspect_robots.types import ActionChunk, Observation
|
|
28
|
+
|
|
29
|
+
from inspect_robots_isaacsim import IsaacSimEmbodiment, isaacsim_embodiment
|
|
30
|
+
|
|
31
|
+
_FRANKA_SEM = ActionSemantics(
|
|
32
|
+
control_mode="joint_pos", rotation_repr="none", gripper="binary", frame="base"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _FrankaPolicy:
|
|
37
|
+
"""A minimal policy whose spaces match the default Franka profile (dim 8)."""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self.info = PolicyInfo(
|
|
41
|
+
name="franka-stub",
|
|
42
|
+
action_space=Box(shape=(8,), semantics=_FRANKA_SEM),
|
|
43
|
+
observation_space=ObservationSpace(state_keys=frozenset({"joint_pos"})),
|
|
44
|
+
)
|
|
45
|
+
self.config = PolicyConfig(action_horizon=1)
|
|
46
|
+
|
|
47
|
+
def reset(self, scene: Scene) -> None: ...
|
|
48
|
+
|
|
49
|
+
def act(self, observation: Observation) -> ActionChunk: # pragma: no cover - unused
|
|
50
|
+
from inspect_robots import Action
|
|
51
|
+
|
|
52
|
+
return ActionChunk(actions=[Action(data=np.zeros(8))])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_info_describes_franka_joint_pos() -> None:
|
|
56
|
+
emb = IsaacSimEmbodiment()
|
|
57
|
+
assert emb.info.name == "isaacsim"
|
|
58
|
+
assert emb.info.is_simulated is True
|
|
59
|
+
# 7 arm joints + 1 binary gripper command.
|
|
60
|
+
assert emb.info.action_space.dim == 8
|
|
61
|
+
sem = emb.info.action_space.semantics
|
|
62
|
+
assert sem is not None
|
|
63
|
+
assert sem.control_mode == "joint_pos"
|
|
64
|
+
assert sem.gripper == "binary"
|
|
65
|
+
assert "privileged_success" in emb.info.capabilities
|
|
66
|
+
assert "self_paced" not in emb.info.capabilities # sim is framework-paced
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_action_dim_tracks_arm_joints() -> None:
|
|
70
|
+
assert IsaacSimEmbodiment(num_arm_joints=6).info.action_space.dim == 7
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_rejects_bad_joint_count() -> None:
|
|
74
|
+
with pytest.raises(ValueError):
|
|
75
|
+
IsaacSimEmbodiment(num_arm_joints=0)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_observation_space_has_state_and_cameras() -> None:
|
|
79
|
+
emb = IsaacSimEmbodiment()
|
|
80
|
+
obs_space = emb.info.observation_space
|
|
81
|
+
assert "base_rgb" in obs_space.camera_names
|
|
82
|
+
assert {"joint_pos", "joint_vel", "eef_pos", "eef_quat", "gripper"} <= obs_space.state_keys
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_satisfies_embodiment_protocol() -> None:
|
|
86
|
+
assert isinstance(IsaacSimEmbodiment(), Embodiment)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_factory_and_kwargs() -> None:
|
|
90
|
+
emb = isaacsim_embodiment(task_id="Isaac-Open-Drawer-Franka-v0", control_hz=20.0)
|
|
91
|
+
assert isinstance(emb, IsaacSimEmbodiment)
|
|
92
|
+
assert emb.task_id == "Isaac-Open-Drawer-Franka-v0"
|
|
93
|
+
assert emb.info.control_hz == 20.0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_compatible_with_matching_franka_policy() -> None:
|
|
97
|
+
report = check_compatibility(_FrankaPolicy(), IsaacSimEmbodiment())
|
|
98
|
+
assert report.ok, report.errors
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_incompatible_with_2d_policy_fails_fast() -> None:
|
|
102
|
+
class _2DPolicy(_FrankaPolicy):
|
|
103
|
+
def __init__(self) -> None:
|
|
104
|
+
self.info = PolicyInfo(
|
|
105
|
+
name="cube2d",
|
|
106
|
+
action_space=Box(
|
|
107
|
+
shape=(2,),
|
|
108
|
+
semantics=ActionSemantics(control_mode="eef_delta_pos", frame="world"),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
self.config = PolicyConfig()
|
|
112
|
+
|
|
113
|
+
report = check_compatibility(_2DPolicy(), IsaacSimEmbodiment())
|
|
114
|
+
assert not report.ok
|
|
115
|
+
codes = {i.code for i in report.errors}
|
|
116
|
+
assert "action_dim" in codes
|
|
117
|
+
with pytest.raises(CompatibilityError):
|
|
118
|
+
report.raise_for_errors()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_reset_without_isaac_raises_clear_error() -> None:
|
|
122
|
+
emb = IsaacSimEmbodiment()
|
|
123
|
+
with pytest.raises(RuntimeError, match="Isaac Sim / Isaac Lab is not importable"):
|
|
124
|
+
emb.reset(Scene(id="s0", instruction="lift the cube"))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_close_is_safe_before_launch_and_idempotent() -> None:
|
|
128
|
+
emb = IsaacSimEmbodiment()
|
|
129
|
+
emb.close() # no env/app yet -> no-op, must not raise
|
|
130
|
+
emb.close() # second call must also be safe
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_context_manager_calls_close() -> None:
|
|
134
|
+
closed = {"n": 0}
|
|
135
|
+
|
|
136
|
+
class _Emb(IsaacSimEmbodiment):
|
|
137
|
+
def close(self) -> None:
|
|
138
|
+
closed["n"] += 1
|
|
139
|
+
|
|
140
|
+
with _Emb() as emb:
|
|
141
|
+
assert isinstance(emb, IsaacSimEmbodiment)
|
|
142
|
+
assert closed["n"] == 1
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# --------------------------------------------------------------------------- #
|
|
146
|
+
# Memory-safety: drive reset()/step() many times against a fake Isaac env and
|
|
147
|
+
# assert the adapter accumulates no state and leaks no RAM. Runs without Isaac.
|
|
148
|
+
# --------------------------------------------------------------------------- #
|
|
149
|
+
class _FakeIsaacEnv:
|
|
150
|
+
"""A stand-in for the Isaac Lab gym env: dict obs batched over num_envs=1."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, action_dim: int) -> None:
|
|
153
|
+
self.action_dim = action_dim
|
|
154
|
+
self.reset_calls = 0
|
|
155
|
+
self.step_calls = 0
|
|
156
|
+
|
|
157
|
+
def _obs(self) -> dict[str, dict[str, np.ndarray]]:
|
|
158
|
+
# Fresh arrays each call, exactly like Isaac handing back buffer reads.
|
|
159
|
+
return {
|
|
160
|
+
"policy": {
|
|
161
|
+
"base_rgb": np.zeros((1, 224, 224, 3), dtype=np.uint8),
|
|
162
|
+
"joint_pos": np.zeros((1, 7), dtype=np.float32),
|
|
163
|
+
"joint_vel": np.zeros((1, 7), dtype=np.float32),
|
|
164
|
+
"eef_pos": np.zeros((1, 3), dtype=np.float32),
|
|
165
|
+
"eef_quat": np.zeros((1, 4), dtype=np.float32),
|
|
166
|
+
"gripper": np.zeros((1, 1), dtype=np.float32),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def reset(self, seed: int | None = None) -> tuple[Any, dict[str, Any]]:
|
|
171
|
+
self.reset_calls += 1
|
|
172
|
+
return self._obs(), {}
|
|
173
|
+
|
|
174
|
+
def step(self, action: Any) -> tuple[Any, Any, Any, Any, dict[str, Any]]:
|
|
175
|
+
self.step_calls += 1
|
|
176
|
+
return (
|
|
177
|
+
self._obs(),
|
|
178
|
+
np.array([0.0]),
|
|
179
|
+
np.array([False]),
|
|
180
|
+
np.array([False]),
|
|
181
|
+
{"success": False},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def close(self) -> None: ...
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class _FakeTorch:
|
|
188
|
+
@staticmethod
|
|
189
|
+
def as_tensor(value: Any, device: str | None = None) -> np.ndarray:
|
|
190
|
+
return np.asarray(value)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _inject_fake(emb: IsaacSimEmbodiment) -> _FakeIsaacEnv:
|
|
194
|
+
fake = _FakeIsaacEnv(emb.info.action_space.dim)
|
|
195
|
+
emb._env = fake
|
|
196
|
+
emb._torch = _FakeTorch()
|
|
197
|
+
return fake
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_step_translation_against_fake_env() -> None:
|
|
201
|
+
from inspect_robots import Action
|
|
202
|
+
|
|
203
|
+
emb = IsaacSimEmbodiment()
|
|
204
|
+
_inject_fake(emb)
|
|
205
|
+
emb.reset(Scene(id="s", instruction="lift"))
|
|
206
|
+
result = emb.step(Action(data=np.zeros(8)))
|
|
207
|
+
assert set(result.observation.images) == {"base_rgb"}
|
|
208
|
+
assert result.observation.images["base_rgb"].dtype == np.uint8
|
|
209
|
+
assert {"joint_pos", "eef_pos", "gripper"} <= set(result.observation.state)
|
|
210
|
+
assert result.terminated is False
|
|
211
|
+
assert result.info["success"] is False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_no_ram_leak_over_many_steps() -> None:
|
|
215
|
+
"""RAM must stay flat over thousands of steps (caller drops each result)."""
|
|
216
|
+
import gc
|
|
217
|
+
import tracemalloc
|
|
218
|
+
|
|
219
|
+
from inspect_robots import Action
|
|
220
|
+
|
|
221
|
+
emb = IsaacSimEmbodiment()
|
|
222
|
+
fake = _inject_fake(emb)
|
|
223
|
+
act = Action(data=np.zeros(8, dtype=np.float32))
|
|
224
|
+
|
|
225
|
+
# Warm up so one-time allocations (interned strings, caches) settle.
|
|
226
|
+
emb.reset(Scene(id="s", instruction="lift"))
|
|
227
|
+
for _ in range(200):
|
|
228
|
+
emb.step(act)
|
|
229
|
+
|
|
230
|
+
gc.collect()
|
|
231
|
+
tracemalloc.start()
|
|
232
|
+
before = tracemalloc.take_snapshot()
|
|
233
|
+
for _ in range(3000):
|
|
234
|
+
emb.step(act) # result intentionally dropped each iteration
|
|
235
|
+
gc.collect()
|
|
236
|
+
after = tracemalloc.take_snapshot()
|
|
237
|
+
tracemalloc.stop()
|
|
238
|
+
|
|
239
|
+
grew = sum(s.size_diff for s in after.compare_to(before, "filename") if s.size_diff > 0)
|
|
240
|
+
# 3000 steps each allocating a 150KB image would be ~450MB if retained; a
|
|
241
|
+
# leak-free loop holds nothing, so allow only a small slack for allocator noise.
|
|
242
|
+
assert grew < 5_000_000, f"RAM grew {grew} bytes over 3000 steps; suspect a leak"
|
|
243
|
+
# The adapter itself must hold no per-step accumulation.
|
|
244
|
+
assert fake.step_calls == 3200
|
|
245
|
+
for value in vars(emb).values():
|
|
246
|
+
assert not isinstance(value, (list, dict)) or len(value) <= 1
|