From bf8058847c9aab1d9421cf9662cd6ab017c86823 Mon Sep 17 00:00:00 2001 From: Yin Song Date: Sat, 4 Jul 2026 14:14:47 +0000 Subject: [PATCH] chore(isaac): switch to create_simulation("isaac") + drop direct-constructor workarounds (closes #139) robots#131 shipped the strands_robots.backends entry-point walker in the strands-robots 0.4.1 release (2026-07-01), so create_simulation("isaac", ...) now resolves to this repo's IsaacSimulation exactly like create_simulation("mujoco"). This is the downstream switch-over. - pyproject.toml: bump the dependency floor to strands-robots>=0.4.1 (the first release whose factory walks the entry-point group) and correct the three comments that incorrectly claimed 0.4.0 walked it, plus the stale hatch skip-install rationale. - examples/libero/run_isaac.py + run_isaac_agent.py: construct via create_simulation("isaac", headless=True, num_envs=1, render_mode="rtx_realtime") instead of IsaacSimulation(IsaacConfig(...)). - examples/isaac_gs/app.py + render_demo.py: same switch, behavior identical (render_mode="rtx_realtime" preserved). - docs (index, quickstart, overview, api-reference, architecture, README, installation, domain-randomization): remove the now-obsolete "direct constructor" / "not wired up yet" / ">=0.3.8,<0.4" notes and show create_simulation("isaac", ...) as the primary path. The direct constructor is kept only where intentional (config-object-in-hand, render-mode field demo, multiprocessing example). - test_create_simulation_isaac.py: flip the discovery test from "asserts the ValueError today, auto-flips later" to asserting create_simulation("isaac") resolves to IsaacSimulation against the >=0.4.1 floor (skips cleanly when strands-robots is absent or below the floor). Verification: create_simulation("isaac", headless=True, render_mode="rtx_realtime") resolves to IsaacSimulation; hatch run lint + hatch run test pass (264 passed, 40 skipped); the discovery test passes positively against strands-robots 0.4.1 in a full install; mkdocs build --strict is clean. --- README.md | 37 +++--- docs/api-reference.md | 11 +- docs/architecture.md | 31 +++-- docs/getting-started/installation.md | 4 +- docs/getting-started/quickstart.md | 32 ++--- docs/index.md | 43 +++---- docs/simulation/domain-randomization.md | 4 +- docs/simulation/overview.md | 27 ++-- examples/isaac_gs/app.py | 6 +- examples/isaac_gs/render_demo.py | 6 +- examples/libero/run_isaac.py | 31 +++-- examples/libero/run_isaac_agent.py | 18 ++- pyproject.toml | 27 ++-- .../tests/test_create_simulation_isaac.py | 121 ++++++++++-------- 14 files changed, 218 insertions(+), 180 deletions(-) diff --git a/README.md b/README.md index 9683d1e..dcfb3f8 100644 --- a/README.md +++ b/README.md @@ -27,21 +27,22 @@ MuJoCo backend implements, so a Strands Agent that drives a MuJoCo world today switches to Isaac Sim by swapping the backend it constructs. ```python -from strands_robots_sim.isaac import IsaacSimulation, IsaacConfig +from strands_robots.simulation import create_simulation -sim = IsaacSimulation(IsaacConfig(render_mode="rtx_realtime", headless=True)) +sim = create_simulation("isaac", render_mode="rtx_realtime", headless=True) sim.create_world() sim.add_robot("so100") # procedural; no asset files needed sim.step(100) frame = sim.render(camera_name="default") ``` -> **Note:** `IsaacSimulation` is also registered as a `strands_robots.backends` -> entry point, but no released `strands-robots` (`>=0.3.8,<0.4`) walks that -> group from `create_simulation` yet, so `create_simulation("isaac")` raises -> `ValueError: Unknown simulation backend: 'isaac'`. Construct `IsaacSimulation` -> directly until the upstream entry-point walker ships -> ([`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)). +> **Note:** `create_simulation("isaac", ...)` resolves to this repo's +> `IsaacSimulation` via the `strands_robots.backends` entry point — +> `strands-robots>=0.4.1` walks that group from its `create_simulation` +> factory ([`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)), +> the same UX as `create_simulation("mujoco")`. The kwargs flow into +> `IsaacConfig`. You can still construct `IsaacSimulation(IsaacConfig(...))` +> directly when you want the config object in hand. > **📚 Documentation:** > @@ -75,7 +76,7 @@ agent contract is identical. ```mermaid graph LR A[Strands Agent] --> B[Simulation
AgentTool] - B --> C[create_simulation 'isaac'
once upstream walks entry points] + B --> C[create_simulation 'isaac'
walks entry points] C --> D[Entry-point lookup
strands_robots.backends] D --> E[IsaacSimulation
this repo] E --> F[Isaac Sim Kit
SimulationApp] @@ -91,10 +92,12 @@ graph LR ``` `strands-robots-sim` registers `IsaacSimulation` as a -`strands_robots.backends` entry point. The intent is that -`create_simulation("isaac")` walks the entry-point group, imports the target -string, and instantiates it — but no released `strands-robots` (`>=0.3.8,<0.4`) -ships that walker yet, so today you construct `IsaacSimulation` directly (see +`strands_robots.backends` entry point. `create_simulation("isaac")` walks the +entry-point group, imports the target string, and instantiates it — +`strands-robots>=0.4.1` ships that walker +([`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)), +so `create_simulation("isaac", ...)` resolves to this repo's `IsaacSimulation` +with nothing more than `pip install strands-robots-sim` (see [Quick start](#quick-start)). The full plugin contract is documented in [Architecture](https://strands-labs.github.io/robots-sim/architecture/). @@ -126,9 +129,9 @@ Full install matrix in ### Single-env RTX render ```python -from strands_robots_sim.isaac import IsaacSimulation, IsaacConfig +from strands_robots.simulation import create_simulation -sim = IsaacSimulation(IsaacConfig(render_mode="rtx_pathtracing", headless=True)) +sim = create_simulation("isaac", render_mode="rtx_pathtracing", headless=True) sim.create_world() sim.add_robot("so100") # procedural builder sim.add_object(name="cube", shape="cuboid", @@ -142,8 +145,8 @@ sim.destroy() ### IsaacLab-style fleet (preview) ```python -sim = IsaacSimulation(IsaacConfig(num_envs=1024, headless=True, - render_mode="headless")) +sim = create_simulation("isaac", num_envs=1024, headless=True, + render_mode="headless") sim.create_world() sim.add_robot(name="panda", usd_path="/path/to/franka.usda") # ... RL training loop ... diff --git a/docs/api-reference.md b/docs/api-reference.md index 360f33a..924551d 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -270,8 +270,10 @@ versions = update one constant (`ISAAC_SIM_DOCKER_IMAGE` / isaac = "strands_robots_sim.isaac.simulation:IsaacSimulation" ``` -Once an upstream `strands-robots` release walks the -`strands_robots.backends` group, `create_simulation("isaac", ...)` will do: +`strands-robots>=0.4.1` walks the `strands_robots.backends` group from its +`create_simulation` factory (shipped via +[`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)), +so `create_simulation("isaac", ...)` does: ```python import importlib.metadata @@ -281,8 +283,9 @@ cls = ep.load() return cls(**kwargs) ``` -Until then (the pinned floor is `strands-robots>=0.3.8,<0.4`, which has no -such walker), construct the backend directly: +This resolves to this repo's `IsaacSimulation` with nothing more than +`pip install strands-robots-sim`. If you want the `IsaacConfig` object in +hand, you can still construct the backend directly: ```python from strands_robots_sim.isaac import IsaacSimulation, IsaacConfig diff --git a/docs/architecture.md b/docs/architecture.md index df7a21f..f1c2e6e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -103,22 +103,21 @@ isaac = "strands_robots_sim.isaac.simulation:IsaacSimulation" ``` When `strands-robots-sim` is installed, `importlib.metadata.entry_points` -sees the `isaac` name. The intended flow is that `create_simulation("isaac", ...)` -upstream walks the `strands_robots.backends` group, finds the `isaac` entry, -imports the target string, and instantiates it. - -!!! warning "Discovery is not live in the pinned upstream yet" - - No released `strands-robots` (this package pins `>=0.3.8,<0.4`) walks the - `strands_robots.backends` group from its `create_simulation` factory, so - `create_simulation("isaac")` currently raises - `ValueError: Unknown simulation backend: 'isaac'`. Until the upstream - walker ships - ([`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)), - construct the backend directly with - `IsaacSimulation(IsaacConfig(...))`. The entry-point declaration here is - forward-compatible plumbing: once upstream walks the group, no change to - this package is required. +sees the `isaac` name. `create_simulation("isaac", ...)` upstream walks the +`strands_robots.backends` group, finds the `isaac` entry, imports the target +string, and instantiates it. + +!!! note "Discovery is live in `strands-robots>=0.4.1`" + + `strands-robots>=0.4.1` walks the `strands_robots.backends` group from its + `create_simulation` factory (shipped via + [`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)), + so `create_simulation("isaac", ...)` resolves to this repo's + `IsaacSimulation` with nothing more than `pip install strands-robots-sim`. + The entry-point declaration here is all the plumbing required — no + upstream code change is needed to add a backend. You can still construct + the backend directly with `IsaacSimulation(IsaacConfig(...))` when you want + the config object in hand. This is the same pattern other packages use to extend `strands-robots` (future cuRobo / MoveIt2 backends, custom user backends). The upstream diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index fb016ea..93cdb62 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -105,9 +105,9 @@ not just that the package imports. Run this with Isaac Sim's bundled Python (`python.sh` / `setup_python_env.sh`-activated venv): ```python -from strands_robots_sim.isaac import IsaacSimulation, IsaacConfig +from strands_robots.simulation import create_simulation -sim = IsaacSimulation(IsaacConfig(render_mode="rtx_realtime", headless=True)) +sim = create_simulation("isaac", render_mode="rtx_realtime", headless=True) sim.create_world() # boots SimulationApp; resolves all extensions sim.add_robot("so100") sim.add_object(name="cube", shape="cuboid", position=[0.4, 0.0, 0.05]) diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index c90435d..0bd774a 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -10,12 +10,13 @@ Bring up an Isaac Sim world, drop a robot in, render an RTX frame. ## Hello, RTX ```python -from strands_robots_sim.isaac import IsaacSimulation, IsaacConfig +from strands_robots.simulation import create_simulation -sim = IsaacSimulation(IsaacConfig( +sim = create_simulation( + "isaac", render_mode="rtx_realtime", # or "rtx_pathtracing" for path-traced headless=True, -)) +) sim.create_world() sim.add_robot("so100") # procedural builder, no asset files sim.add_object(name="cube", shape="cuboid", @@ -28,22 +29,21 @@ frame = sim.render(camera_name="front") # {"rgb": (H, W, 3) uint8, "depth sim.destroy() ``` -!!! note "`create_simulation('isaac')` is not wired up yet" +!!! note "`create_simulation('isaac')` resolves via the entry-point group" `IsaacSimulation` is registered as a `strands_robots.backends` entry - point, but the released `strands-robots` floor (`>=0.3.8,<0.4`) does not - walk that group from `create_simulation`, so - `create_simulation("isaac")` raises - `ValueError: Unknown simulation backend: 'isaac'`. Construct - `IsaacSimulation(IsaacConfig(...))` directly until the upstream walker - ships ([`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)); - the kwargs are identical, forwarded into `IsaacConfig`. + point. `strands-robots>=0.4.1` walks that group from `create_simulation` + ([`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)), + so `create_simulation("isaac", ...)` resolves to this repo's + `IsaacSimulation` — the same UX as `create_simulation("mujoco")`. The + kwargs are forwarded into `IsaacConfig`. If you want the config object in + hand you can still construct `IsaacSimulation(IsaacConfig(...))` directly. What happened: -1. `IsaacSimulation(IsaacConfig(...))` constructs the backend directly — the - supported path until `create_simulation("isaac")` resolves through the - `strands_robots.backends` entry point. +1. `create_simulation("isaac", ...)` resolves the backend through the + `strands_robots.backends` entry point and forwards the kwargs into + `IsaacConfig` — the recommended path. 2. `create_world()` spins up a `SimulationApp`, opens a USD stage, and adds a ground plane. 3. `add_robot("so100")` runs the procedural SO-100 builder — no asset @@ -144,9 +144,9 @@ that closes over the `sim` instance — the same pattern ```python from strands import Agent, tool -from strands_robots_sim.isaac import IsaacSimulation, IsaacConfig +from strands_robots.simulation import create_simulation -sim = IsaacSimulation(IsaacConfig(render_mode="rtx_realtime", headless=True)) +sim = create_simulation("isaac", render_mode="rtx_realtime", headless=True) sim.create_world() sim.add_robot("so100") diff --git a/docs/index.md b/docs/index.md index ea66438..aae7497 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,9 +22,9 @@ backend it constructs: === "Isaac Sim (this repo, RTX, USD-native)" ```python - from strands_robots_sim.isaac import IsaacSimulation, IsaacConfig + from strands_robots.simulation import create_simulation - sim = IsaacSimulation(IsaacConfig(render_mode="rtx_pathtracing", headless=True)) + sim = create_simulation("isaac", render_mode="rtx_pathtracing", headless=True) sim.create_world() sim.add_robot("so100") # procedural; no asset files needed sim.step(100) @@ -36,18 +36,12 @@ are all identical across backends — once the `IsaacSimulation` instance exists, every downstream call is `SimEngine`-shaped regardless of how it was constructed. -!!! note "Why the direct constructor instead of `create_simulation('isaac')`?" - - `strands-robots-sim` registers `IsaacSimulation` as a - `strands_robots.backends` entry point (see [How it works](#how-it-works)), - but the released `strands-robots` floor this package pins - (`>=0.3.8,<0.4`) does **not** yet walk that entry-point group from - `create_simulation` — so `create_simulation("isaac")` raises - `ValueError: Unknown simulation backend: 'isaac'`. Until an upstream - release ships the entry-point walker (tracked in - [`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)), - construct `IsaacSimulation` directly as shown above. The kwargs are the - same either way: they flow into `IsaacConfig`. +`create_simulation("isaac", ...)` resolves to this repo's `IsaacSimulation` +via the `strands_robots.backends` entry point (see [How it works](#how-it-works)), +exactly like `create_simulation("mujoco")` resolves to the built-in MuJoCo +backend. The kwargs flow straight into `IsaacConfig`. You can still construct +`IsaacSimulation(IsaacConfig(...))` directly if you want the config object in +hand, but the factory is the recommended path. ## When you want this repo @@ -67,14 +61,13 @@ contract is the same. ## How it works `strands-robots-sim` registers `IsaacSimulation` as a -`strands_robots.backends` entry point. The intent is that -`create_simulation("isaac")` resolves to it without `strands-robots` ever -needing a hard dependency on Isaac Sim: +`strands_robots.backends` entry point. `create_simulation("isaac")` resolves +to it without `strands-robots` ever needing a hard dependency on Isaac Sim: ```mermaid graph LR A[Strands Agent] --> B[Simulation
AgentTool] - B --> C[create_simulation 'isaac'
once upstream walks entry points] + B --> C[create_simulation 'isaac'
walks entry points] C --> D[Entry-point lookup
strands_robots.backends] D --> E[IsaacSimulation
this repo] E --> F[Isaac Sim Kit
SimulationApp] @@ -89,17 +82,15 @@ graph LR class E,F,G plugin ``` -!!! warning "Entry-point discovery is not live yet" +!!! note "Entry-point discovery" The entry point above is declared and discoverable (`importlib.metadata.entry_points(group="strands_robots.backends")` - lists `isaac`), but no released `strands-robots` walks that group from - its `create_simulation` factory yet — the pinned floor - (`strands-robots>=0.3.8,<0.4`) only knows the built-in MuJoCo aliases. - So today you construct `IsaacSimulation` directly (see - [Quickstart](#quickstart)); the entry-point path lights up once the - upstream walker ships - ([`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)). + lists `isaac`). `strands-robots>=0.4.1` walks that group from its + `create_simulation` factory (shipped via + [`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)), + so `create_simulation("isaac")` resolves to this repo's `IsaacSimulation` + with nothing more than `pip install strands-robots-sim`. The same plugin shape is what makes the `mujoco` backend in `strands-robots` and `isaac` here interchangeable: both are `SimEngine` subclasses; the diff --git a/docs/simulation/domain-randomization.md b/docs/simulation/domain-randomization.md index 4e2ccbd..7d1e339 100644 --- a/docs/simulation/domain-randomization.md +++ b/docs/simulation/domain-randomization.md @@ -61,9 +61,9 @@ descriptions of what to perturb each frame, expressed as keyword args to `replicate(...)`: ```python -from strands_robots_sim.isaac import IsaacSimulation, IsaacConfig +from strands_robots.simulation import create_simulation -sim = IsaacSimulation(IsaacConfig(render_mode="rtx_pathtracing", headless=True)) +sim = create_simulation("isaac", render_mode="rtx_pathtracing", headless=True) sim.create_world() sim.add_robot(name="panda", usd_path="/path/to/franka.usda") sim.add_object(name="cube", shape="cuboid", diff --git a/docs/simulation/overview.md b/docs/simulation/overview.md index 9ba5a91..0b8af9c 100644 --- a/docs/simulation/overview.md +++ b/docs/simulation/overview.md @@ -34,9 +34,10 @@ sensible defaults: | `extra` | `{}` | Escape-hatch for experimental options. | ```python -from strands_robots_sim.isaac import IsaacConfig, IsaacSimulation +from strands_robots.simulation import create_simulation -cfg = IsaacConfig( +sim = create_simulation( + "isaac", num_envs=1, headless=True, render_mode="rtx_pathtracing", @@ -44,16 +45,24 @@ cfg = IsaacConfig( camera_width=1280, camera_height=720, ) -sim = IsaacSimulation(cfg) ``` -`IsaacSimulation(IsaacConfig(headless=True, render_mode="rtx_realtime"))` is -the supported way to construct the backend today. Once an upstream -`strands-robots` release walks the `strands_robots.backends` entry-point -group, `create_simulation("isaac", headless=True, render_mode="rtx_realtime")` -will be shorthand for the same thing — kwargs are forwarded into -`IsaacConfig` either way (tracked in +`create_simulation("isaac", headless=True, render_mode="rtx_realtime")` is the +recommended way to construct the backend: `strands-robots>=0.4.1` walks the +`strands_robots.backends` entry-point group, so `create_simulation("isaac")` +resolves to this repo's `IsaacSimulation` — the same UX as +`create_simulation("mujoco")` (shipped via [`strands-labs/robots#131`](https://github.com/strands-labs/robots/issues/131)). +The kwargs are forwarded into `IsaacConfig`. If you want the config object in +hand — e.g. to introspect or `dataclasses.replace(...)` it before construction +— build it explicitly and pass it to the class: + +```python +from strands_robots_sim.isaac import IsaacConfig, IsaacSimulation + +cfg = IsaacConfig(num_envs=1, headless=True, render_mode="rtx_pathtracing") +sim = IsaacSimulation(cfg) +``` ### Environment-variable overrides diff --git a/examples/isaac_gs/app.py b/examples/isaac_gs/app.py index 2318a86..9d17e0c 100644 --- a/examples/isaac_gs/app.py +++ b/examples/isaac_gs/app.py @@ -226,7 +226,9 @@ def set_camera(self, view: str) -> None: def boot(self) -> None: """Create SimulationApp + build the scene. **Main thread only.**""" - from strands_robots_sim.isaac import IsaacConfig, IsaacSimulation + from strands_robots.simulation import create_simulation + + from strands_robots_sim.isaac import IsaacSimulation available, reason = IsaacSimulation.is_available() if not available: @@ -236,7 +238,7 @@ def boot(self) -> None: from examples.isaac_gs.scene import add_preset_cameras, build_default_scene logger.info("Booting IsaacSimulation on the main thread (~200 s)...") - sim = IsaacSimulation(IsaacConfig(headless=True, num_envs=1, render_mode="rtx_realtime")) + sim = create_simulation("isaac", headless=True, num_envs=1, render_mode="rtx_realtime") # Robot-aware: build_default_scene creates the "front" camera, so align # it with the chosen presets' "front" pose; add_preset_cameras adds the # rest. Defaults (no robot_usd / presets) = the bundled Franka. diff --git a/examples/isaac_gs/render_demo.py b/examples/isaac_gs/render_demo.py index c795bd5..30bda98 100644 --- a/examples/isaac_gs/render_demo.py +++ b/examples/isaac_gs/render_demo.py @@ -224,7 +224,9 @@ def main() -> None: logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") args = _build_parser().parse_args() - from strands_robots_sim.isaac import IsaacConfig, IsaacSimulation + from strands_robots.simulation import create_simulation + + from strands_robots_sim.isaac import IsaacSimulation # Fail-fast on non-Isaac hosts (cheap probe, no omni import). available, reason = IsaacSimulation.is_available() @@ -238,7 +240,7 @@ def main() -> None: out_dir = _date_out(args.out) # rtx_realtime so render() takes the RTX frame path (not headless blanks). - sim = IsaacSimulation(IsaacConfig(headless=True, num_envs=1, render_mode="rtx_realtime")) + sim = create_simulation("isaac", headless=True, num_envs=1, render_mode="rtx_realtime") try: build = build_default_scene( sim, diff --git a/examples/libero/run_isaac.py b/examples/libero/run_isaac.py index c98d797..4d3e6e1 100644 --- a/examples/libero/run_isaac.py +++ b/examples/libero/run_isaac.py @@ -143,7 +143,9 @@ import os import time -from strands_robots_sim.isaac import IsaacConfig, IsaacSimulation +from strands_robots.simulation import create_simulation + +from strands_robots_sim.isaac import IsaacSimulation def _date_dir(date_root: str = "rollouts") -> str: @@ -614,18 +616,23 @@ def main() -> None: else: policy_kwargs = {"policy_provider": "mock"} - # Construct the Isaac sim. headless=True avoids opening a Kit - # viewport (the GR00T eval doesn't need an interactive GUI); it does - # NOT control the render pipeline. render_mode="rtx_realtime" makes - # render() take the RTX frame path instead of short-circuiting to - # zero-filled (blank) frames in the default render_mode="headless", - # which is what produced all-black rollout MP4s. (Allowed modes: - # "headless", "rtx_realtime", "rtx_pathtracing" -- see config.py. + # Construct the Isaac sim via the strands-robots factory. Now that + # strands-robots>=0.4.1 walks the `strands_robots.backends` + # entry-point group, `create_simulation("isaac", ...)` resolves to + # this package's IsaacSimulation — same UX as create_simulation("mujoco"). + # headless=True avoids opening a Kit viewport (the GR00T eval doesn't + # need an interactive GUI); it does NOT control the render pipeline. + # render_mode="rtx_realtime" makes render() take the RTX frame path + # instead of short-circuiting to zero-filled (blank) frames in the + # default render_mode="headless", which is what produced all-black + # rollout MP4s. (Allowed modes: "headless", "rtx_realtime", + # "rtx_pathtracing" -- see config.py. # STRANDS_ISAAC_RTX_PATHTRACING=1 upgrades to photoreal pathtracing.) - # The IsaacConfig dataclass is a pure-Python construct (no omni.* - # imports), so this constructor is cheap and runs on a non-Isaac - # host — the actual SimulationApp boot happens inside create_world(). - sim = IsaacSimulation(IsaacConfig(headless=True, num_envs=1, render_mode="rtx_realtime")) + # The kwargs are forwarded to the IsaacConfig dataclass (a pure-Python + # construct with no omni.* imports), so this call is cheap and runs on + # a non-Isaac host — the actual SimulationApp boot happens inside + # create_world(). + sim = create_simulation("isaac", headless=True, num_envs=1, render_mode="rtx_realtime") try: result = sim.create_world() if result.get("status") != "success": diff --git a/examples/libero/run_isaac_agent.py b/examples/libero/run_isaac_agent.py index 41b396b..491f9cd 100644 --- a/examples/libero/run_isaac_agent.py +++ b/examples/libero/run_isaac_agent.py @@ -169,9 +169,10 @@ from strands import Agent, tool from strands_robots.benchmarks.libero import load_libero_suite +from strands_robots.simulation import create_simulation from strands_robots.tools import gr00t_inference -from strands_robots_sim.isaac import IsaacConfig, IsaacSimulation +from strands_robots_sim.isaac import IsaacSimulation # Module-level handle for the @tool-wrapped function below. # The wrapper has to access ``_sim`` from outer scope because @@ -671,12 +672,15 @@ def main() -> None: server_handle = _bring_up_gr00t_server(args, suite) - # render_mode="rtx_realtime" makes render() take the RTX frame path - # instead of returning zero-filled (blank) frames in the default - # render_mode="headless" -- the latter produced all-black rollout - # MP4s. headless=True only suppresses the Kit viewport, not the - # render pipeline. (STRANDS_ISAAC_RTX_PATHTRACING=1 -> photoreal.) - _sim = IsaacSimulation(IsaacConfig(headless=True, num_envs=1, render_mode="rtx_realtime")) + # Construct via the strands-robots factory: strands-robots>=0.4.1 + # walks the `strands_robots.backends` entry-point group, so + # create_simulation("isaac", ...) resolves to this package's + # IsaacSimulation. render_mode="rtx_realtime" makes render() take the + # RTX frame path instead of returning zero-filled (blank) frames in + # the default render_mode="headless" -- the latter produced all-black + # rollout MP4s. headless=True only suppresses the Kit viewport, not + # the render pipeline. (STRANDS_ISAAC_RTX_PATHTRACING=1 -> photoreal.) + _sim = create_simulation("isaac", headless=True, num_envs=1, render_mode="rtx_realtime") try: result = _sim.create_world() if result.get("status") != "success": diff --git a/pyproject.toml b/pyproject.toml index c467a6e..cafa874 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,15 +31,17 @@ classifiers = [ # strands-robots-sim is a *plugin* of strands-robots: the SimEngine ABC, # factory, model registry, LIBERO/MuJoCo backend, GR00T policy client, and # LeRobot recording adapter all live upstream. The only runtime dep this -# package ships with is strands-robots itself (>=0.4.0 — the version that -# walks `[project.entry-points."strands_robots.backends"]` to discover -# pluggable backends like the Isaac one registered below). +# package ships with is strands-robots itself (>=0.4.1 — the first release +# whose factory walks `[project.entry-points."strands_robots.backends"]` to +# discover pluggable backends like the Isaac one registered below; 0.4.0 +# registered the built-in backends only and did NOT walk the group, so +# `create_simulation("isaac")` raised on that release — see robots#131). # # GPU-only backend dependencies (Isaac Sim) live behind the `[isaac]` # extra; pip-install with no extras keeps the runtime tiny and lets # users layer the GPU bits on per-host. dependencies = [ - "strands-robots>=0.4.0", + "strands-robots>=0.4.1", ] [project.optional-dependencies] @@ -81,7 +83,7 @@ dependencies = [ # Isaac Lab is required only for the fleet-RL path, which installs from # source. See #107. # -# Why Isaac Sim **6.0** specifically (not 5.0/5.1)? `strands-robots>=0.4.0` +# Why Isaac Sim **6.0** specifically (not 5.0/5.1)? `strands-robots>=0.4.1` # requires Python >=3.12, and Isaac Sim 6.0 is the only release whose # embedded interpreter targets 3.12 (the 5.x line is 3.11) (#98). Keep # that major.minor in lockstep with ISAAC_SIM_DOCKER_IMAGE in @@ -113,7 +115,7 @@ dev = [ # Backend entry points -- the heart of this PR. # -# strands-robots>=0.4.0 walks `entry_points(group="strands_robots.backends")` +# strands-robots>=0.4.1 walks `entry_points(group="strands_robots.backends")` # from its factory and registers every (name -> "module:Class") pair it # finds. The three entries below mean: # @@ -142,13 +144,12 @@ packages = ["strands_robots_sim"] [tool.hatch.envs.default] # `skip-install = true` keeps hatch from re-installing the project (and -# its `strands-robots>=0.4.0` dep) into a fresh venv on every `hatch run` -# invocation -- the >=0.4.0 pin can't resolve from PyPI until upstream U2 -# (entry-point walker) ships and the strands-labs/robots repo cuts a -# matching release. Until then, contributors should `pip install -e .` -# from a system Python that already has `strands-robots` installed from -# a local checkout. The deps below are purely the lint + test toolchain -# we want hatch to manage in its venv. +# its `strands-robots>=0.4.1` dep) into a fresh venv on every `hatch run` +# invocation. The `>=0.4.1` floor resolves from PyPI now that upstream's +# entry-point walker (robots#131) has shipped in the strands-robots 0.4.1 +# release. Contributors can `pip install -e .` from a system Python; the +# deps below are purely the lint + test toolchain we want hatch to manage +# in its venv. skip-install = true dependencies = [ "black", diff --git a/strands_robots_sim/isaac/tests/test_create_simulation_isaac.py b/strands_robots_sim/isaac/tests/test_create_simulation_isaac.py index 8a38697..6f482d8 100644 --- a/strands_robots_sim/isaac/tests/test_create_simulation_isaac.py +++ b/strands_robots_sim/isaac/tests/test_create_simulation_isaac.py @@ -1,33 +1,29 @@ -"""Regression tests for the documented Isaac quickstart path (#97). +"""Regression tests for the documented Isaac quickstart path (#97, #139). `docs/index.md`, `README.md`, `docs/getting-started/quickstart.md`, and -`docs/simulation/overview.md` all previously opened with:: +`docs/simulation/overview.md` all open with the factory path:: - import strands_robots_sim from strands_robots.simulation import create_simulation sim = create_simulation("isaac", render_mode="rtx_realtime", headless=True) -That snippet is **broken** against every released ``strands-robots`` this -package can install: the pinned floor (``strands-robots>=0.3.8,<0.4``) does -not walk the ``strands_robots.backends`` entry-point group from its -``create_simulation`` factory, so the call raises -``ValueError: Unknown simulation backend: 'isaac'`` (issue #97). The docs -now use the supported direct constructor:: +That path now works: ``strands-robots>=0.4.1`` (the pinned floor, shipped via +robots#131) walks the ``strands_robots.backends`` entry-point group from its +``create_simulation`` factory, so ``create_simulation("isaac", ...)`` resolves +to this package's ``IsaacSimulation`` — the same UX as +``create_simulation("mujoco")``. The direct constructor stays supported for +callers that want the ``IsaacConfig`` object in hand:: from strands_robots_sim.isaac import IsaacSimulation, IsaacConfig sim = IsaacSimulation(IsaacConfig(render_mode="rtx_realtime", headless=True)) These tests pin both halves of that contract so neither can silently drift: -1. ``TestDocumentedDirectConstructor`` — the path the docs actually show - must keep working on a CPU-only box (no ``omni.*`` import at construct - time), accepting both the ``IsaacConfig`` and the kwargs forms. -2. ``TestCreateSimulationIsaacDiscovery`` — encodes the *current* upstream - reality. While the pinned ``strands-robots`` lacks the entry-point - walker, ``create_simulation("isaac")`` must raise the documented - ``ValueError``; the moment a future upstream gains the walker, the same - call must resolve to ``IsaacSimulation`` (and this test flips to assert - that). Either way the docs and code stay in lockstep. +1. ``TestDocumentedDirectConstructor`` — the direct-constructor path must keep + working on a CPU-only box (no ``omni.*`` import at construct time), + accepting both the ``IsaacConfig`` and the kwargs forms. +2. ``TestCreateSimulationIsaacDiscovery`` — the documented factory path: + ``create_simulation("isaac", ...)`` must resolve to ``IsaacSimulation`` + against the pinned ``strands-robots>=0.4.1`` floor. Run with:: @@ -62,7 +58,7 @@ def test_construct_from_isaac_config(self): def test_construct_from_kwargs(self): """The kwargs form ``IsaacSimulation(render_mode=..., headless=...)`` constructs. - These are the same kwargs the docs note will one day flow through + These are the same kwargs that flow through ``create_simulation("isaac", ...)`` into ``IsaacConfig``. """ from strands_robots_sim.isaac import IsaacSimulation @@ -115,55 +111,76 @@ def _isaac_via_factory(): return None, exc -class TestCreateSimulationIsaacDiscovery: - """Pin the ``create_simulation('isaac')`` contract against the pinned upstream (#97). +def _factory_walks_entry_points() -> bool: + """True if the installed ``strands-robots`` walks ``strands_robots.backends``. + + The entry-point walker landed in ``strands-robots>=0.4.1`` (robots#131); + the pinned floor requires it. But the lint/test hatch env is + ``skip-install=true`` and CI/dev boxes may still have an older + ``strands-robots`` in the ambient Python, so this probe lets the positive + assertion skip cleanly rather than fail on a stale, below-floor install. + """ + try: + from strands_robots.simulation import list_backends + except Exception: # pragma: no cover - strands-robots absent + return False + try: + # The walker surfaces plugin backends (e.g. "isaac") in list_backends(); + # a pre-0.4.1 factory only lists the built-in mujoco aliases. + return "isaac" in set(list_backends()) + except Exception: # pragma: no cover - defensive + return False + - The pinned ``strands-robots>=0.3.8,<0.4`` has no entry-point walker, so - the factory cannot resolve ``"isaac"``. This test asserts the documented - failure mode today and auto-flips to assert success once an upstream - release gains the walker (so the docs' "one day this collapses to - ``create_simulation('isaac')``" promise is itself guarded). +class TestCreateSimulationIsaacDiscovery: + """Pin the documented ``create_simulation('isaac')`` contract (#97, #139). + + ``strands-robots>=0.4.1`` (the pinned floor, robots#131) walks the + ``strands_robots.backends`` entry-point group, so ``create_simulation`` + resolves ``"isaac"`` to this package's ``IsaacSimulation``. This test + asserts that documented behavior. It skips cleanly when ``strands-robots`` + is absent or below the floor (e.g. the ``skip-install=true`` hatch env or + a stale ambient install) rather than failing spuriously. """ - def test_factory_either_resolves_isaac_or_raises_the_documented_error(self): + def test_create_simulation_isaac_resolves_to_isaac_simulation(self): # ``strands-robots`` provides ``create_simulation`` but is not # installed in the lint/test hatch env (skip-install=true). Skip # cleanly there; runs anywhere the runtime dep is present. pytest.importorskip("strands_robots.simulation") - import strands_robots_sim # noqa: F401 - parity with the (former) doc snippet + if not _factory_walks_entry_points(): + pytest.skip( + "Installed strands-robots is below the >=0.4.1 floor that walks " + "strands_robots.backends (robots#131). Upgrade to validate the " + "factory path: pip install 'strands-robots>=0.4.1'." + ) + + import strands_robots_sim # noqa: F401 - parity with the doc snippet sim, err = _isaac_via_factory() - if err is None: - # Upstream gained the entry-point walker: the docs' forward-looking - # promise is now real. Guard that "isaac" resolves to *our* class. - from strands_robots_sim.isaac import IsaacSimulation + assert err is None, ( + f"create_simulation('isaac') raised {type(err).__name__ if err else None}: {err!r}; " + "expected it to resolve to IsaacSimulation against strands-robots>=0.4.1." + ) - assert isinstance(sim, IsaacSimulation), ( - f"create_simulation('isaac') resolved to {type(sim)!r}; expected IsaacSimulation. " - "An upstream strands-robots now walks strands_robots.backends but routed " - "'isaac' to the wrong class." - ) - return + from strands_robots_sim.isaac import IsaacSimulation - # No walker yet: the call must fail with the exact documented error - # so the quickstart docs (which use the direct constructor) stay honest. - assert isinstance(err, ValueError), ( - f"create_simulation('isaac') raised {type(err).__name__}: {err!r}; expected ValueError. " - "If upstream now resolves 'isaac', this test auto-detects it via the err-is-None branch." - ) - assert "isaac" in str(err).lower(), ( - f"create_simulation('isaac') raised an unexpected ValueError: {err!r}. " - "Expected the 'Unknown simulation backend: isaac' message." + assert isinstance(sim, IsaacSimulation), ( + f"create_simulation('isaac') resolved to {type(sim)!r}; expected IsaacSimulation. " + "strands-robots walked strands_robots.backends but routed 'isaac' to the wrong class." ) + # The documented kwargs must reach IsaacConfig. + assert sim._config.render_mode == "rtx_realtime" + assert sim._config.headless is True - def test_entry_point_is_registered_even_though_factory_cannot_use_it_yet(self): - """The ``isaac`` entry point is declared/discoverable regardless of the walker. + def test_entry_point_is_registered(self): + """The ``isaac`` entry point is declared/discoverable. - This is the forward-compatible plumbing: once upstream walks the - group, no change to this package is required (see docs/architecture.md). - Skips cleanly if the package isn't pip-installed in this env. + This is the plumbing the upstream walker consumes (see + docs/architecture.md). Skips cleanly if the package isn't pip-installed + in this env. """ import importlib.metadata