Skip to content

feat(isaac): [isaac] extras + entry-point + PEP 562 lazy import (1/5 of #31 split)#44

Merged
cagataycali merged 2 commits into
strands-labs:mainfrom
yinsong1986:pr1/isaac-foundation
May 26, 2026
Merged

feat(isaac): [isaac] extras + entry-point + PEP 562 lazy import (1/5 of #31 split)#44
cagataycali merged 2 commits into
strands-labs:mainfrom
yinsong1986:pr1/isaac-foundation

Conversation

@yinsong1986
Copy link
Copy Markdown
Contributor

Summary

Part 1 / 5 of the split of #31 — tracked by #42.

Adds the package skeleton for the Isaac Sim backend with zero omni overhead on import strands_robots_sim. Lands first; PR-2 (config) / PR-3 (procedural) / PR-4 (simulation) all import from strands_robots_sim.isaac, so this stub + extras + entry-points has to exist before they can be code-reviewed against current main.

What's in this slice

File Status Purpose
pyproject.toml modified [isaac] + [all] extras; strands_robots.backends entry-points; pytest>=7.0 in dev + hatch envs; gpu pytest marker
strands_robots_sim/isaac/__init__.py new (43 LOC) PEP 562 lazy stub — __getattr__ defers imports of .simulation and .config until first attribute access
strands_robots_sim/isaac/tests/__init__.py new (1 LOC) empty package marker for future test collection

Why each bit

[isaac] extra — declares the Isaac-companion deps that ARE pip-installable (usd-core>=24.5,<26.0, warp-lang>=1.13.0). Inline comment in pyproject.toml makes clear that Isaac Sim itself is not PyPI-installable; users install it via Omniverse Launcher, Isaac Lab, or nvcr.io/nvidia/isaac-sim:4.5.0.

Entry-points — both isaac and isaac_sim aliases pointing at strands_robots_sim.isaac.simulation:IsaacSimulation. The target class itself lands in PR-4; declaring it now means PR-4's test_entry_points_declared_in_pyproject (which reads pyproject.toml directly) can pass against either an installed package or the source tree.

Lazy __init__.pyimport strands_robots_sim.isaac does NOT pull omni / Isaac Sim / usd-core. That overhead only fires when a caller actually accesses IsaacSimulation or IsaacConfig — preserving the "lightweight import" contract of the rest of the package.

pytest>=7.0 + markers config — added now so subsequent slices can run pytest under hatch run test without each PR having to re-add the dependency. The gpu marker pre-declares the gate that PR-4's test_gpu_integ.py will use (STRANDS_GPU_TEST=1).

CI signal

  • hatch run test (the existing import-smoke python -c "import strands_robots_sim; print(...)") — passes; doesn't touch isaac, so no missing-submodule errors.
  • hatch run lint (black --check / isort --check-only / flake8 over strands_robots_sim/ + examples/) — clean.
  • python -c "import strands_robots_sim.isaac; print(strands_robots_sim.isaac.__all__)"['IsaacSimulation', 'IsaacConfig']. The lazy stub doesn't import any omni modules at this point.

The hatch test command stays as the import-smoke placeholder in this slice and will flip to pytest strands_robots_sim/isaac/tests/ in PR-3, when the first real test files land.

Dependency chain

PR-1 (this) → unblocks PR-2 / PR-3 / PR-4 / PR-5 (parallel-mergeable after this lands).

Diff: 3 files, +68 / -0 LOC.

Original authorship

Original work by @cagataycali in #31 (413ff15..befb1e3). This slice cherry-picks just the foundation files; the R1-R4 review-thread anchors on the original commits stay intact for whichever child PR consumes the relevant code (per #42's review-thread bookkeeping note).

strands-labs#31 split)

Part 1 / 5 of the split of strands-labs#31 — tracked by strands-labs#42.

Adds the package skeleton for the Isaac Sim backend with zero `omni`
overhead on `import strands_robots_sim`:

- `pyproject.toml`:
  - new `[isaac]` extra (lightweight Isaac-companion deps that ARE
    pip-installable: `usd-core>=24.5,<26.0`, `warp-lang>=1.13.0`).
    Comment explains that Isaac Sim itself is NOT installable from
    PyPI — must come via Omniverse Launcher / Isaac Lab / nvcr.io.
  - new `[all]` aggregate extra (currently delegates to `[isaac]`;
    will gain `[newton]` etc. as later backends land).
  - new `[project.entry-points."strands_robots.backends"]` declaring
    both `isaac` and `isaac_sim` aliases pointing at
    `strands_robots_sim.isaac.simulation:IsaacSimulation` (the class
    itself lands in PR-4 / `feat(isaac): IsaacSimulation backend`).
  - new `pytest>=7.0` in `dev` extras and `[tool.hatch.envs.default]`
    deps so future PRs in this split chain can run pytest under
    `hatch run test`.
  - new `[tool.pytest.ini_options]` with a `gpu` marker so PR-4's
    GPU-only integ tests can gate on `STRANDS_GPU_TEST=1`.

- `strands_robots_sim/isaac/__init__.py`:
  PEP 562 lazy stub — declares `__all__ = ["IsaacSimulation",
  "IsaacConfig"]` and routes attribute access through `__getattr__`
  to deferred imports of `.simulation` and `.config`. The bare
  `import strands_robots_sim.isaac` does NOT import any `omni`
  modules; that overhead only fires when a caller actually accesses
  `IsaacSimulation` or `IsaacConfig`.

- `strands_robots_sim/isaac/tests/__init__.py`: empty package marker
  so future test files in this directory are collectable. Test files
  themselves land in subsequent slices alongside the code they cover.

CI signal: lint clean (black / isort / flake8); the existing
`hatch run test` import-smoke passes (it doesn't touch `isaac`, so
no missing-submodule errors). Pytest doesn't run yet from `hatch
run test` — the test command stays as the import-smoke placeholder
in this slice and flips to `pytest strands_robots_sim/isaac/tests/`
in PR-3 once the first real test files land.

Why this lands first: PR-2 / PR-3 / PR-4 all import from
`strands_robots_sim.isaac`, so the package stub + extras + entry
points have to exist before any of those files can be code-reviewed
against current `main`.

Original work by @cagataycali in strands-labs#31 (`413ff15..befb1e3`); this
slice cherry-picks just the foundation files. Review-thread anchors
on the original commits stay intact for whichever child PR consumes
the relevant code.
Copy link
Copy Markdown
Member

@cagataycali cagataycali left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local review on Thor — lazy contract works, but ships zero tests

Verified locally:

  • pip install -e . clean
  • import strands_robots_sim.isaac adds zero omni.* modules to sys.modules
  • strands_robots_sim.isaac.__all__ == ['IsaacSimulation', 'IsaacConfig']
  • pytest strands_robots_sim/isaac/tests/0 tests collected

Why this matters

This PR is the structural parent of PR45/46/47/48 — they all branched from this commit (3a0a854). So merging this lands the foundation. But it ships with zero test coverage; the lazy-import contract claim is verified only by manual probing, not by CI.

Meanwhile, the orphan PR43 (feat/isaac-extras-and-lazy-import) ships 10 cleanly-passing tests that verify exactly what PR-1 delivers:

  • 6 tests in TestEntryPointDeclaration — pyproject parsing + importlib.metadata discovery
  • 4 tests in TestLazyImportSurface — including the omni.*-not-loaded assertion that the PR description promises

Those tests don't depend on PR-2/3/4/5 — they only inspect the surface PR-1 actually creates. Safe to land here.

Recommendation

Before merging, port these from PR43:

  1. strands_robots_sim/isaac/tests/test_entrypoint.py (140 LOC, 10 tests)
  2. pytest>=7.0 in the [isaac] extra (currently in [default] env only)
  3. Switch hatch test script from python -c "import strands_robots_sim; ..." to pytest strands_robots_sim/isaac/tests/ -v

Then close PR43.

What works ✅

  • Lazy __init__.py (43 LOC) — clean PEP 562 stub
  • pyproject.toml extras + entry-points — both isaac and isaac_sim aliases declared correctly
  • Lazy contract holds at runtime
  • Lint clean

Waiting on the test port before approving.

…abs#43 (PR strands-labs#44 R1)

Closes @cagataycali's R1 review on strands-labs#44 -- the lazy-import contract
ships with concrete CI signal now, not just a manual probe in the PR
description.

Three coordinated changes per the review:

1. **Port `tests/test_entrypoint.py` (140 LOC, 10 tests)** from the
   closed orphan PR strands-labs#43 (`feat/isaac-extras-and-lazy-import`). These
   tests verify *only* PR-1's surface and have **zero dependency on
   `simulation.py`** (which lands in PR-4 of the strands-labs#31 split):

   - `TestEntryPointDeclaration` (6 tests):
     - `test_pyproject_exists`
     - `test_isaac_entry_point_declared_in_pyproject`
     - `test_isaac_sim_alias_entry_point_declared_in_pyproject`
     - `test_isaac_extra_declared_in_pyproject`
     - `test_isaac_extra_includes_pytest`
     - `test_entry_points_visible_via_importlib_metadata_when_installed`
       (skips with actionable hint when not pip-installed)

   - `TestLazyImportSurface` (4 tests):
     - `test_import_isaac_does_not_load_omni` -- pins the `omni`-not-
       loaded contract via `sys.modules` set diff before/after
       import.
     - `test_isaac_subpackage_exposes_lazy_attrs_in___all__`
     - `test_unknown_attr_raises_attributeerror` -- pins the PEP 562
       `__getattr__` raises `AttributeError` (not `ImportError`) on
       unknown attrs.
     - `test_dunder_getattr_is_present`

2. **Add `pytest>=7.0` to the `[isaac]` extra** in `pyproject.toml`.
   Means `pip install '.[isaac]'` covers the test deps without a
   separate dev extra on CI hosts. Also pinned by
   `test_isaac_extra_includes_pytest`.

3. **Flip `hatch run test`** from the import-smoke placeholder
   (`python -c "import strands_robots_sim; ..."`) to
   `pytest strands_robots_sim/isaac/tests/ -v`. The 10 tests collect
   and pass standalone; PR-4's simulation-module tests join the
   collection automatically when that PR rebases.

Verification:

- `pytest strands_robots_sim/isaac/tests/ -v` -> **10 passed in 0.14s**
- `black --check` / `isort --check-only` / `flake8` (max-line=120) on
  `strands_robots_sim/` + `examples/` -> clean
- AST parse on touched files -> ok
- The 10 tests have no import path through `simulation.py`; they
  validate only PR-1's actual surface (pyproject parsing, entry-
  point declaration, PEP 562 `__getattr__` contract).

The original `test_entrypoint.py` on `cagataycali/feat/isaac-sim`
imports from `strands_robots_sim.isaac.simulation`, which is why
PR-1 of the split couldn't ship that file unmodified. The version
ported here was rewritten on the strands-labs#43 branch specifically for the
PR-1 surface; cleanly self-contained against this slice.
@yinsong1986
Copy link
Copy Markdown
Contributor Author

yinsong1986 commented May 26, 2026

Thanks for the review. R1 fix pushed as c7e2e5e. All three asks resolved:

# Ask Resolution
1 Port test_entrypoint.py from PR #43 (10 tests, no simulation.py dep) Cherry-picked from the closed feat/isaac-extras-and-lazy-import branch. 6 TestEntryPointDeclaration tests + 4 TestLazyImportSurface tests; all 10 pass standalone (pytest strands_robots_sim/isaac/tests/ -v10 passed in 0.14s).
2 Add pytest>=7.0 to [isaac] extra Added; pinned by TestEntryPointDeclaration::test_isaac_extra_includes_pytest.
3 Flip hatch run test from import-smoke to pytest … Done; [tool.hatch.envs.default.scripts] test = "pytest strands_robots_sim/isaac/tests/ -v". PR-4's simulation-module tests join the collection automatically once that PR rebases.

The ported tests are designed for PR-1's surface specifically — they read pyproject.toml directly (not via metadata when not installed), feature-detect importlib.metadata discovery (skip with actionable hint when not pip-installed), and pin the PEP 562 __getattr__ contract (AttributeError not ImportError on unknown attrs, lazy access doesn't pull omni.* modules). Zero imports through strands_robots_sim.isaac.simulation.

Downstream impact on the rest of the chain

[Update 2026-05-26]: the rebase below was executed shortly after this comment was posted (per follow-up direction). The plan in this section is no longer pending — see the per-PR status comments on #45 / #46 / #47 / #48 for the actual SHA shifts and conflict-resolution details.

#47 specifically: the test_entrypoint.py conflict was resolved by keeping the new R1 surface-only version + migrating two unique-to-PR-4 tests (test_isaac_is_simengine_subclass, test_isaac_implements_all_abstract_methods) into test_unit.py::TestIsaacSimulationAvailability. The migrated test_isaac_implements_all_abstract_methods now loudly pins the pre-existing abstract-method gap on IsaacSimulation.robot_joint_names (24 failing tests) — flagged on #47 as a separate review concern, not introduced by the rebase.

pr1/isaac-foundation advanced from 3a0a854c7e2e5e. The downstream branches (pr2/isaac-configpr5/isaac-docs) all branched off the old SHA. Once you approve and merge this, I'll:

  1. Rebase feat(isaac): IsaacConfig dataclass with validation (2/5 of #31 split) #45 / feat(isaac): procedural USD builders (SO-100, Panda, G1) (3/5 of #31 split) #46 / feat(isaac): IsaacSimulation backend (lifecycle + observation + render) (4/5 of #31 split) #47 / docs(isaac): backend reference + Phase 1 status banner (5/5 of #31 split) #48 onto the new pr1 head (then onto main after feat(isaac): [isaac] extras + entry-point + PEP 562 lazy import (1/5 of #31 split) #44 lands).
  2. feat(isaac): IsaacSimulation backend (lifecycle + observation + render) (4/5 of #31 split) #47 needs special care — it currently carries cagataycali's original test_entrypoint.py (which imports from strands_robots_sim.isaac.simulation import IsaacSimulation, SimEngine). Those simulation-dependent tests (test_isaac_is_simengine_subclass, test_isaac_has_is_available_classmethod, test_is_available_returns_false_without_gpu) overlap with feat(isaac): IsaacSimulation backend (lifecycle + observation + render) (4/5 of #31 split) #47's test_unit.py::TestIsaacSimulationAvailability and TestIsaacSimulationContract. On rebase I'll resolve the conflict by:

Let me know if you'd prefer a different rebase strategy (e.g., merge commit on #45-#48 instead of rebase to preserve their commit SHAs for any inline-thread anchors).

Comment thread pyproject.toml
@cagataycali cagataycali merged commit 2741e42 into strands-labs:main May 26, 2026
2 checks passed
@github-project-automation github-project-automation Bot moved this from In review to Done in Strands Labs - Robots May 26, 2026
cagataycali pushed a commit that referenced this pull request May 26, 2026
…#45)

* feat(isaac): [isaac] extras + entry-point + PEP 562 lazy import (1/5 of #31 split)

Part 1 / 5 of the split of #31 — tracked by #42.

Adds the package skeleton for the Isaac Sim backend with zero `omni`
overhead on `import strands_robots_sim`:

- `pyproject.toml`:
  - new `[isaac]` extra (lightweight Isaac-companion deps that ARE
    pip-installable: `usd-core>=24.5,<26.0`, `warp-lang>=1.13.0`).
    Comment explains that Isaac Sim itself is NOT installable from
    PyPI — must come via Omniverse Launcher / Isaac Lab / nvcr.io.
  - new `[all]` aggregate extra (currently delegates to `[isaac]`;
    will gain `[newton]` etc. as later backends land).
  - new `[project.entry-points."strands_robots.backends"]` declaring
    both `isaac` and `isaac_sim` aliases pointing at
    `strands_robots_sim.isaac.simulation:IsaacSimulation` (the class
    itself lands in PR-4 / `feat(isaac): IsaacSimulation backend`).
  - new `pytest>=7.0` in `dev` extras and `[tool.hatch.envs.default]`
    deps so future PRs in this split chain can run pytest under
    `hatch run test`.
  - new `[tool.pytest.ini_options]` with a `gpu` marker so PR-4's
    GPU-only integ tests can gate on `STRANDS_GPU_TEST=1`.

- `strands_robots_sim/isaac/__init__.py`:
  PEP 562 lazy stub — declares `__all__ = ["IsaacSimulation",
  "IsaacConfig"]` and routes attribute access through `__getattr__`
  to deferred imports of `.simulation` and `.config`. The bare
  `import strands_robots_sim.isaac` does NOT import any `omni`
  modules; that overhead only fires when a caller actually accesses
  `IsaacSimulation` or `IsaacConfig`.

- `strands_robots_sim/isaac/tests/__init__.py`: empty package marker
  so future test files in this directory are collectable. Test files
  themselves land in subsequent slices alongside the code they cover.

CI signal: lint clean (black / isort / flake8); the existing
`hatch run test` import-smoke passes (it doesn't touch `isaac`, so
no missing-submodule errors). Pytest doesn't run yet from `hatch
run test` — the test command stays as the import-smoke placeholder
in this slice and flips to `pytest strands_robots_sim/isaac/tests/`
in PR-3 once the first real test files land.

Why this lands first: PR-2 / PR-3 / PR-4 all import from
`strands_robots_sim.isaac`, so the package stub + extras + entry
points have to exist before any of those files can be code-reviewed
against current `main`.

Original work by @cagataycali in #31 (`413ff15..befb1e3`); this
slice cherry-picks just the foundation files. Review-thread anchors
on the original commits stay intact for whichever child PR consumes
the relevant code.

* review(isaac): port 10 entry-point + lazy-import tests from #43 (PR #44 R1)

Closes @cagataycali's R1 review on #44 -- the lazy-import contract
ships with concrete CI signal now, not just a manual probe in the PR
description.

Three coordinated changes per the review:

1. **Port `tests/test_entrypoint.py` (140 LOC, 10 tests)** from the
   closed orphan PR #43 (`feat/isaac-extras-and-lazy-import`). These
   tests verify *only* PR-1's surface and have **zero dependency on
   `simulation.py`** (which lands in PR-4 of the #31 split):

   - `TestEntryPointDeclaration` (6 tests):
     - `test_pyproject_exists`
     - `test_isaac_entry_point_declared_in_pyproject`
     - `test_isaac_sim_alias_entry_point_declared_in_pyproject`
     - `test_isaac_extra_declared_in_pyproject`
     - `test_isaac_extra_includes_pytest`
     - `test_entry_points_visible_via_importlib_metadata_when_installed`
       (skips with actionable hint when not pip-installed)

   - `TestLazyImportSurface` (4 tests):
     - `test_import_isaac_does_not_load_omni` -- pins the `omni`-not-
       loaded contract via `sys.modules` set diff before/after
       import.
     - `test_isaac_subpackage_exposes_lazy_attrs_in___all__`
     - `test_unknown_attr_raises_attributeerror` -- pins the PEP 562
       `__getattr__` raises `AttributeError` (not `ImportError`) on
       unknown attrs.
     - `test_dunder_getattr_is_present`

2. **Add `pytest>=7.0` to the `[isaac]` extra** in `pyproject.toml`.
   Means `pip install '.[isaac]'` covers the test deps without a
   separate dev extra on CI hosts. Also pinned by
   `test_isaac_extra_includes_pytest`.

3. **Flip `hatch run test`** from the import-smoke placeholder
   (`python -c "import strands_robots_sim; ..."`) to
   `pytest strands_robots_sim/isaac/tests/ -v`. The 10 tests collect
   and pass standalone; PR-4's simulation-module tests join the
   collection automatically when that PR rebases.

Verification:

- `pytest strands_robots_sim/isaac/tests/ -v` -> **10 passed in 0.14s**
- `black --check` / `isort --check-only` / `flake8` (max-line=120) on
  `strands_robots_sim/` + `examples/` -> clean
- AST parse on touched files -> ok
- The 10 tests have no import path through `simulation.py`; they
  validate only PR-1's actual surface (pyproject parsing, entry-
  point declaration, PEP 562 `__getattr__` contract).

The original `test_entrypoint.py` on `cagataycali/feat/isaac-sim`
imports from `strands_robots_sim.isaac.simulation`, which is why
PR-1 of the split couldn't ship that file unmodified. The version
ported here was rewritten on the #43 branch specifically for the
PR-1 surface; cleanly self-contained against this slice.

* feat(isaac): IsaacConfig dataclass with validation (2/5 of #31 split)

Part 2 / 5 of the split of #31 — tracked by #42. Branched off PR-1
(`pr1/isaac-foundation` -> #44); rebases cleanly to `main` once PR-1
merges.

Adds the configuration dataclass that `IsaacSimulation` (PR-4) consumes:

- `strands_robots_sim/isaac/config.py` (116 LOC):
  `IsaacConfig` dataclass with:
  - `num_envs`, `device` (default `"cuda:0"`), `headless` (default
    `True` -- safe for cloud/CI runners), `render_mode`, `physics_dt`,
    `gravity`, `ground_plane`, `camera_width`, `camera_height`,
    `rtx_pathtracing`, `nucleus_url`.
  - environment-variable overrides via `STRANDS_ISAAC_*` (`HEADLESS`,
    `RTX_PATHTRACING`, `NUCLEUS_URL`).
  - dataclass-level validation in `__post_init__` so e.g.
    `IsaacConfig(num_envs=0)` raises `ValueError` at construction.
  - `from_kwargs` classmethod that PR-4's `IsaacSimulation.__init__`
    uses to validate caller-supplied kwargs and surface unknown keys
    as `TypeError` (the R1 "reject unknown kwargs" pin).

No `omni` / Isaac Sim imports. Pure-Python dataclass; can be imported
on any host without a GPU. Tests for this module ship in PR-4
alongside `test_unit.py::TestIsaacConfig` (cleaner than carving a
test slice for ~120 LOC of dataclass).

CI signal: lint clean (black / isort / flake8); the lazy
`__init__.py` from PR-1 now resolves `IsaacConfig` to a real class
when accessed -- the `from strands_robots_sim.isaac import
IsaacConfig` import path becomes live in this slice.

Original work by @cagataycali in #31 (`413ff15`); this slice
cherry-picks just `config.py`. R1's `from_kwargs` validation pin
(commit `32ef307`) is already baked into this file.

* fix(isaac): address review comment on PR #45

cagataycali's comment at strands_robots_sim/isaac/config.py:116 flagged
that from_kwargs was promised by the PR description but missing from
the slice. Added the classmethod so PR-4 (#47) can DRY its
unknown-kwarg validation against IsaacConfig.from_kwargs rather than
inlining dataclasses.fields() reflection at simulation.py:174-180.
yinsong1986 added a commit that referenced this pull request May 27, 2026
…split) (#46)

* feat(isaac): [isaac] extras + entry-point + PEP 562 lazy import (1/5 of #31 split)

Part 1 / 5 of the split of #31 — tracked by #42.

Adds the package skeleton for the Isaac Sim backend with zero `omni`
overhead on `import strands_robots_sim`:

- `pyproject.toml`:
  - new `[isaac]` extra (lightweight Isaac-companion deps that ARE
    pip-installable: `usd-core>=24.5,<26.0`, `warp-lang>=1.13.0`).
    Comment explains that Isaac Sim itself is NOT installable from
    PyPI — must come via Omniverse Launcher / Isaac Lab / nvcr.io.
  - new `[all]` aggregate extra (currently delegates to `[isaac]`;
    will gain `[newton]` etc. as later backends land).
  - new `[project.entry-points."strands_robots.backends"]` declaring
    both `isaac` and `isaac_sim` aliases pointing at
    `strands_robots_sim.isaac.simulation:IsaacSimulation` (the class
    itself lands in PR-4 / `feat(isaac): IsaacSimulation backend`).
  - new `pytest>=7.0` in `dev` extras and `[tool.hatch.envs.default]`
    deps so future PRs in this split chain can run pytest under
    `hatch run test`.
  - new `[tool.pytest.ini_options]` with a `gpu` marker so PR-4's
    GPU-only integ tests can gate on `STRANDS_GPU_TEST=1`.

- `strands_robots_sim/isaac/__init__.py`:
  PEP 562 lazy stub — declares `__all__ = ["IsaacSimulation",
  "IsaacConfig"]` and routes attribute access through `__getattr__`
  to deferred imports of `.simulation` and `.config`. The bare
  `import strands_robots_sim.isaac` does NOT import any `omni`
  modules; that overhead only fires when a caller actually accesses
  `IsaacSimulation` or `IsaacConfig`.

- `strands_robots_sim/isaac/tests/__init__.py`: empty package marker
  so future test files in this directory are collectable. Test files
  themselves land in subsequent slices alongside the code they cover.

CI signal: lint clean (black / isort / flake8); the existing
`hatch run test` import-smoke passes (it doesn't touch `isaac`, so
no missing-submodule errors). Pytest doesn't run yet from `hatch
run test` — the test command stays as the import-smoke placeholder
in this slice and flips to `pytest strands_robots_sim/isaac/tests/`
in PR-3 once the first real test files land.

Why this lands first: PR-2 / PR-3 / PR-4 all import from
`strands_robots_sim.isaac`, so the package stub + extras + entry
points have to exist before any of those files can be code-reviewed
against current `main`.

Original work by @cagataycali in #31 (`413ff15..befb1e3`); this
slice cherry-picks just the foundation files. Review-thread anchors
on the original commits stay intact for whichever child PR consumes
the relevant code.

* review(isaac): port 10 entry-point + lazy-import tests from #43 (PR #44 R1)

Closes @cagataycali's R1 review on #44 -- the lazy-import contract
ships with concrete CI signal now, not just a manual probe in the PR
description.

Three coordinated changes per the review:

1. **Port `tests/test_entrypoint.py` (140 LOC, 10 tests)** from the
   closed orphan PR #43 (`feat/isaac-extras-and-lazy-import`). These
   tests verify *only* PR-1's surface and have **zero dependency on
   `simulation.py`** (which lands in PR-4 of the #31 split):

   - `TestEntryPointDeclaration` (6 tests):
     - `test_pyproject_exists`
     - `test_isaac_entry_point_declared_in_pyproject`
     - `test_isaac_sim_alias_entry_point_declared_in_pyproject`
     - `test_isaac_extra_declared_in_pyproject`
     - `test_isaac_extra_includes_pytest`
     - `test_entry_points_visible_via_importlib_metadata_when_installed`
       (skips with actionable hint when not pip-installed)

   - `TestLazyImportSurface` (4 tests):
     - `test_import_isaac_does_not_load_omni` -- pins the `omni`-not-
       loaded contract via `sys.modules` set diff before/after
       import.
     - `test_isaac_subpackage_exposes_lazy_attrs_in___all__`
     - `test_unknown_attr_raises_attributeerror` -- pins the PEP 562
       `__getattr__` raises `AttributeError` (not `ImportError`) on
       unknown attrs.
     - `test_dunder_getattr_is_present`

2. **Add `pytest>=7.0` to the `[isaac]` extra** in `pyproject.toml`.
   Means `pip install '.[isaac]'` covers the test deps without a
   separate dev extra on CI hosts. Also pinned by
   `test_isaac_extra_includes_pytest`.

3. **Flip `hatch run test`** from the import-smoke placeholder
   (`python -c "import strands_robots_sim; ..."`) to
   `pytest strands_robots_sim/isaac/tests/ -v`. The 10 tests collect
   and pass standalone; PR-4's simulation-module tests join the
   collection automatically when that PR rebases.

Verification:

- `pytest strands_robots_sim/isaac/tests/ -v` -> **10 passed in 0.14s**
- `black --check` / `isort --check-only` / `flake8` (max-line=120) on
  `strands_robots_sim/` + `examples/` -> clean
- AST parse on touched files -> ok
- The 10 tests have no import path through `simulation.py`; they
  validate only PR-1's actual surface (pyproject parsing, entry-
  point declaration, PEP 562 `__getattr__` contract).

The original `test_entrypoint.py` on `cagataycali/feat/isaac-sim`
imports from `strands_robots_sim.isaac.simulation`, which is why
PR-1 of the split couldn't ship that file unmodified. The version
ported here was rewritten on the #43 branch specifically for the
PR-1 surface; cleanly self-contained against this slice.

* feat(isaac): procedural USD builders (SO-100, Panda, G1) (3/5 of #31 split)

Part 3 / 5 of the split of #31 — tracked by #42. Branched off PR-1
(`pr1/isaac-foundation` -> #44); rebases cleanly onto `main` once
PR-1 merges.

Adds the procedural USD robot builders that `IsaacSimulation`
(PR-4) consumes via `add_robot(data_config=<name>, procedural=True)`
without requiring binary asset downloads from Nucleus:

- `strands_robots_sim/isaac/procedural.py` (258 LOC):
  - `ProceduralRobot` named-tuple shape: `(prim_root, num_joints,
    joint_names, body_names, parent_body_indices)`.
  - `get_procedural_robot(name: str) -> ProceduralRobot` registry.
  - Three builders behind the registry:
    - `_build_so100`: 6-DOF SO-100 arm (HuggingFace's open-source
      teaching arm).
    - `_build_franka_panda`: 9-DOF Panda + 2-DOF gripper.
    - `_build_unitree_g1`: 21-DOF G1 humanoid (1 torso + 6 left leg
      + 6 right leg + 4 left arm + 4 right arm).
  - All builders are pure-Python; no `omni` / Isaac Sim imports.

- `strands_robots_sim/isaac/tests/test_procedural_g1_dof.py` (~70
  LOC, 3 tests):
  Carved out of cagataycali's original
  `test_phase1_doc_honesty.py` -- the `TestG1DOFCount` class only.
  The companion `TestIsaacDocsPhase1Banner` class (which reads
  `docs/backends/isaac.md`) lands in PR-5 alongside the docs file
  itself. Pin tests:
  - `test_g1_actual_joint_count_is_21`: truth-source pin against
    `procedural.py`'s actual joint set.
  - `test_g1_module_docstring_advertises_21_not_29`: pin against
    R2's stale 29-DOF claim in the module docstring (commit
    `85e180f`).
  - `test_g1_builder_docstring_advertises_21_not_29`: same pin on
    the `_build_unitree_g1` function docstring.

CI signal: lint clean (black / isort / flake8); the 3 G1-DOF pin
tests pass standalone (`pytest strands_robots_sim/isaac/tests/
test_procedural_g1_dof.py -v` -> 3 passed).

Note: this slice does NOT pin the kinematic-tree topology defect
on the G1 joint graph (duplicate `(parent_body, child_body)`
edges -- e.g. `l_hip_roll` and `l_hip_pitch` both map bodies 3 ->
4). That defect is documented in `procedural.py` line 165 as
deferred to Phase 2; it requires intermediate massless link
bodies that this Phase 1 stub does not instantiate. The dormant-
on-Phase-1 framing is the same one R2 captured.

Why this lands in parallel with PR-2 (config): both are pure
modules with no cross-dependencies; either can land first. PR-4
(simulation) needs both.

Original work by @cagataycali in #31 (`413ff15..85e180f`); this
slice cherry-picks `procedural.py` plus the carved-out G1 test
file. R2's DOF-count fix (commit `85e180f`) is already baked into
this file's docstrings.

* fix(isaac): add Phase-1 fail-fast guard for duplicate kinematic edges

cagataycali's comment at strands_robots_sim/isaac/procedural.py:213
flagged that the documented duplicate-edge defect on G1 is silently
returned by _build_unitree_g1 and would only surface as a cryptic
USD/MuJoCo articulation error two layers down on Phase-2 wireup.

Adds _validate_kinematic_tree(), wired into the end of all three
procedural builders, opt-in via STRANDS_ISAAC_VALIDATE_KINEMATICS=1
(Phase-2 dev path). Default is no-op so Phase-1 callers (which never
instantiate the articulation) are unaffected; with the env var set,
G1 raises ValueError naming the duplicate edges + offending joints,
while SO-100 and Panda still build cleanly.

Pinned by tests/test_procedural_kinematic_guard.py: default-off
behaviour, accepted env values (1/true/yes, case-insensitive),
G1 raises with diagnostic context, SO-100 and Panda pass guard
when enabled.
yinsong1986 added a commit that referenced this pull request May 28, 2026
…r) (4/5 of #31 split) (#47)

* feat(isaac): IsaacSimulation backend (lifecycle + observation + render) (4/5 of #31 split)

Part 4 / 5 of the split of #31 — tracked by #42. The bulk slice;
~2,000 LOC of simulation backend + tests carrying all R1/R2/R4
review fixes. Branched off PR-3 (`pr3/isaac-procedural` -> #46);
needs PR-1 (#44) + PR-2 (#45) + PR-3 (#46) all in main before
the rebase produces a clean diff and CI goes green.

Files in this slice (excluding the foundation + procedural files
that come in via PR-3's branch base):

- `strands_robots_sim/isaac/simulation.py` (1,097 LOC):
  `IsaacSimulation(SimEngine)` with `__init__`, `is_available`,
  `create_world`, `destroy`, `reset`, `step`, `get_state`,
  `add_robot`, `add_object`, `get_observation`, `send_action`,
  `render`, `add_camera`, `replicate`, `_load_usd_robot`,
  `_load_urdf_robot`, `cleanup`, `__enter__/__exit__/__repr__`.
  Plus module-level `_get_or_create_simulation_app` (SimulationApp
  singleton) and `_RobotState` / `_CameraState` named-tuples.
  Lazy `omni` imports throughout; thread-safe via `RLock`.
  Carries:
  - R1's `is_available()` probing `omni.isaac.kit` specifically
    (commit `65ca6fe`) -- not the bare `omni` namespace, which is
    a PEP 420 shared package.
  - R1's no-`__del__` finalizer (commit `29ccfe3`); cleanup must
    be explicit via `cleanup()` or context manager.
  - R1's narrowed `except` clauses (commit `5309623`) at the four
    sites that previously swallowed bare `Exception`.
  - R4's distinguishable WARNING / DEBUG diagnostic logs across
    `get_observation`'s four silent-empty modes (commit `befb1e3`):
    pre-init probe DEBUG; ambiguous + unknown-name WARNING with
    typo echo + sorted known-robots; Phase 1 articulation-None
    DEBUG.

- `strands_robots_sim/isaac/config.py` (116 LOC):
  Duplicated from PR-2 (#45) so this branch is locally testable.
  After PR-2 lands, this file becomes a no-change file in the
  rebase and the dup goes away.

- `strands_robots_sim/isaac/tests/test_unit.py` (464 LOC, 6 test
  classes): `TestIsaacConfig` (R1/R2 carry-over), `TestIsaacSim
  ulationAvailability`, `TestIsaacSimulationContract` (R1's
  unknown-kwarg pin lives here), `TestProceduralRobots`,
  `TestNoEmojisInOutput`, `TestExceptionClauseHygiene` (AST walk
  pinning R1's narrowed exception clauses).

- `strands_robots_sim/isaac/tests/test_entrypoint.py` (139 LOC):
  R6 entry-point registration tests + `IsaacSimulation` is a
  `SimEngine` subclass + `is_available()` returns
  `(bool, str|None)` tuple.

- `strands_robots_sim/isaac/tests/test_get_observation_diagnostic_logs.py`
  (209 LOC, R4's pin file): 9 tests across 5 classes pinning the
  WARNING / DEBUG level discipline + content of the diagnostic
  log lines + the docstring contract enumerating the four silent-
  empty modes.

- `strands_robots_sim/isaac/tests/test_gpu_integ.py` (117 LOC):
  GPU-only integ tests gated on `STRANDS_GPU_TEST=1` + the `gpu`
  pytest marker (declared in pyproject.toml from PR-1).

CI signal: red in isolation -- `simulation.py` imports from
`isaac.config` and `isaac.procedural`. Once PR-1/PR-2/PR-3 land in
main and this branch rebases, expect 33-50 tests passing on CPU
(the 25 tests that currently fail with `TypeError: Can't
instantiate abstract class` are a separate pre-existing defect on

Original work by @cagataycali in #31 (`413ff15..befb1e3`); this
slice cherry-picks `simulation.py` + 4 test files. R1/R2/R4 review
fixes are baked in.

* fix(isaac): mirror real SimEngine abstract surface in fallback ABC

cagataycali's review comment on simulation.py:50 (#47) flagged that the
fallback ABC declared zero @AbstractMethod decorators, so
IsaacSimulation() instantiated cleanly in tests when strands-robots was
absent — masking the abstract-method gap that surfaces the moment the
real ABC is in scope.

Mirror the real strands_robots.simulation.base.SimEngine surface (14
abstract methods) in the fallback. Now `pytest` produces the same
failure mode (25 failed / 33 passed) whether strands-robots is installed
or not, including the dedicated canary
test_isaac_implements_all_abstract_methods. The 4 missing concrete
implementations on IsaacSimulation (list_robots, remove_robot,
remove_object, robot_joint_names) remain a separately tracked gap per
the PR body — this commit just makes the gap visible in CI.

* fix(isaac): implement missing SimEngine abstract methods

cagataycali's review at strands_robots_sim/isaac/simulation.py:774 flagged
that IsaacSimulation didn't implement four abstract methods inherited
from strands_robots.simulation.base.SimEngine — list_robots,
robot_joint_names, remove_robot, remove_object — making the class
non-instantiable with TypeError.

Added concrete implementations using the existing _robots dict and
_prim_registry list, all guarded by self._lock for thread safety.
Removal methods only update the in-Python registry; USD prim deletion
is deferred to world teardown (Phase 1 scope, matches add_robot's
procedural / load-stub behaviour).

Unblocks 24 previously-failing tests (now 73 passed, 5 skipped).
Refs #49.

* fix(isaac): use scalar gravity magnitude for Isaac Sim 5.1 set_gravity

cagataycali's comment at strands_robots_sim/isaac/simulation.py:346
flagged that Isaac Sim 5.1 changed PhysicsContext.set_gravity to take
a scalar magnitude rather than a vector. Passing a list/tuple now
raises TypeError. Extract the Z-component from the configured gravity
vector (convention: gravity points along -Z) and pass the scalar
magnitude to set_gravity. Preserves the public list[float] API of
create_world(gravity=...).

* fix(isaac): type SimulationApp launch config kwargs

cagataycali's review at strands_robots_sim/isaac/simulation.py:108 flagged
that the **kwargs forwarded to omni.isaac.kit.SimulationApp were
untyped. Add a SimulationAppLaunchConfig TypedDict (total=False)
documenting the well-known Kit launch keys (renderer, width, height,
physics_gpu, active_gpu, multi_gpu, sync_loads, hide_ui, anti_aliasing)
and accept it as an optional launch_config parameter on
_get_or_create_simulation_app. The **kwargs path is preserved as the
escape hatch for Kit options not in the TypedDict; the explicit
headless argument always wins over either source so caller intent is
unambiguous.

Tool exposure (the second half of the comment) is intentionally
out-of-scope: _get_or_create_simulation_app is a process-singleton
lifecycle helper and the agent-facing entry point is
IsaacSimulation.create_world(), which already routes through this
function.

* refactor(isaac): centralise install hints in _install module

cagataycali's comment at strands_robots_sim/isaac/simulation.py:272
flagged that the Isaac Sim install instructions (docker tag, Omniverse
Launcher hint, Isaac Lab bootstrap) were duplicated across docstrings
and error messages -- they would drift whenever the upstream image is
bumped (security update or otherwise).

Extract them into strands_robots_sim/isaac/_install.py as a single
source of truth:

- ISAAC_SIM_DOCKER_IMAGE / ISAAC_SIM_MIN_VERSION / ISAAC_LAB_BOOTSTRAP /
  PIP_EXTRA constants
- not_importable_reason() composes the multi-line bullet block used by
  IsaacSimulation.is_available()
- not_available_import_error() composes the inline ImportError raised
  by _get_or_create_simulation_app

simulation.py now imports those helpers; the package docstring in
isaac/__init__.py points readers at _install for the canonical values.

A regression pin in TestInstallConstants asserts simulation.py contains
no hardcoded `nvcr.io/nvidia/isaac-sim:` literal so re-hardcoding gets
caught at CI time.

PR: #47, comment: #47 (comment)

* feat(isaac): include world-info json in create_world() success result

cagataycali's review on PR #47 at strands_robots_sim/isaac/simulation.py:512
asked for the freshly-created sim env to also be surfaced as a 'json'
content block alongside the existing 'text' summary, so an agent that
just spun up a world can introspect device / dt / scene config without
re-querying via get_state().

Following the existing convention in this file (get_state() at L624 and
replicate() at L1238 already attach a 'json' payload to the same content
block as the text), I add a 'world_info' dict next to the text in the
create_world() success branch. Fields cover the snapshot the agent is
most likely to need at startup: physics_dt, rendering_dt, gravity,
ground_plane, stage_path, stage_units_in_meters, device, headless,
render_mode, num_envs, num_envs_active, replicated, sim_time, step_count.

The error branches stay text-only (an agent inspecting them just needs
the message). Tests + lint unchanged.

* feat(isaac): include destroy-info json in destroy() success result

cagataycali's review on PR #47 at strands_robots_sim/isaac/simulation.py:534
("Similar to get_state implementation below ^^") asked for the destroy()
success branch to also surface a structured 'json' content block alongside
the existing 'text' summary, matching the convention already established by
get_state() (and recently extended to create_world() in 30a9487).

Capture pre-teardown counts BEFORE the registry/state clears, then attach
them as a 'destroy_info' dict on the same content block as the text:
num_robots_released, num_cameras_released, num_prims_released,
num_envs_released, sim_time_at_destroy, step_count_at_destroy, stage_path,
and simulation_app_alive=True (the singleton survives destroy()). This is
the snapshot an agent inspecting destroy()'s return value is most likely
to need -- get_state() is no longer reachable after destroy() returns.

The error branches stay text-only. Tests (78 passed, 5 skipped) and lint
unchanged; the existing test_destroy_without_world / test_destroy_message
_no_emoji assertions still hold.

* lint(isaac): annotate intentional E402 on simulation.py late import

Surfaces during PR #47 rebase onto current main: the late
`from strands_robots_sim.isaac.config import IsaacConfig` import
inside simulation.py is intentional — it must follow the SimEngine
ABC fallback class definition above it (~line 95-130) which provides
the import-time abstract surface mirror. Adding `# noqa: E402  #
<reason>` keeps lint green without weakening the flake8 rule for
real violations elsewhere.

* test(isaac): mark test_add_procedural_robot xfail for Phase 1

cagataycali's L40S GPU validation comment on PR #47 confirmed that
test_add_procedural_robot fails with len(obs)==0 because the procedural
builder registers metadata but does not yet create USD prims /
Articulation handle on the stage -- the documented Phase 1 limitation
in simulation.py:1001.

Mark the test xfail (strict=False) with a reason linking the expected
code path so the gating signal stays meaningful: the other three GPU
tests (create_world+step, render, replicate) still gate on PASS, while
this one is allowed to fail until Phase 2's procedural USD prim
builder lands. strict=False so the suite stays green if a future
Phase 2 commit makes it pass before this xfail marker is removed.

---------

Co-authored-by: yinsong1986 <yinsong1986@users.noreply.github.com>
yinsong1986 added a commit that referenced this pull request May 28, 2026
…#51)

* feat(isaac): URDF / MJCF / USD loaders → ProceduralRobot (closes #50)

Follow-up to the R7 Phase 1 procedural-builder slice (PR #46). Instead of
only hardcoding _build_so100 / _build_panda / _build_unitree_g1 in
isaac/procedural.py, this adds isaac/loaders.py — a generic loader module
that produces ProceduralRobot instances from the three description-file
formats Isaac Sim natively understands:

  * load_urdf(path)  — stdlib xml.etree.ElementTree, no external dep.
                        Mirrors the parser semantics on the Newton side
                        (strands_robots_sim/newton/simulation.py on PR #30)
                        so both backends share behaviour.
  * load_mjcf(path)  — stdlib XML walk over <worldbody> / nested <body>
                        / <joint>. Handles LIBERO-style scenes (the
                        matrix's main consumer ships MJCF).
  * load_usd(path)   — pxr.Usd / pxr.UsdPhysics walk for
                        PhysicsRevoluteJoint / PhysicsPrismaticJoint +
                        UsdPhysicsRigidBodyAPI / MassAPI. Lazy-imported,
                        gated behind [isaac] extra (PR #44 pattern).

Failure semantics (closes the #33 class of "phantom robot" bugs — silent
joint_count=0 on parse failure):

  * Missing path                → FileNotFoundError
  * Malformed XML               → ValueError carrying the file path
  * Wrong root tag              → ValueError naming the offender
  * Zero links / zero bodies    → ValueError ("phantom robot guard")
  * Unknown joint type          → ValueError naming the type
  * Joint referencing unknown
    parent / child link or USD
    body0/body1 dangling target → ValueError naming the missing target

The three procedural _build_* functions are intentionally retained as
the zero-dep, testable fallback used when no description file is
configured (matches the issue's recommendation so the existing pin
tests in test_procedural_g1_dof.py can run on a clean environment with
no Pixar USD / mujoco / urdfpy installed).

Test surface (24 new tests under strands_robots_sim/isaac/tests/test_loaders.py):

  * URDF round-trip + Panda parity (DOF count, joint names, joint
    types, body parent/child indices match _build_panda exactly)
  * URDF round-trip + so100 parity (mixed revolute + prismatic case —
    the gripper joint type must be preserved through the loader)
  * URDF axis + limit extraction
  * URDF failure modes (missing file, malformed XML, wrong root,
    zero links, unknown joint type, dangling parent/child link refs)
  * MJCF round-trip + nested-body chain test
  * MJCF joint type mapping (hinge → revolute, slide → prismatic)
  * MJCF parent/child indices (synthetic "world" prepended for
    top-level <body>s under <worldbody>)
  * MJCF failure modes (missing file, wrong root, no <worldbody>,
    empty <worldbody>, unknown joint type, malformed XML)
  * USD round-trip with two RigidBody prims + a RevoluteJoint
    (creates a tmp USDA stage in the test, no committed assets)
  * USD MassAPI mass extraction
  * USD failure modes (missing file, missing pxr → ImportError with
    [isaac] install hint, zero rigid bodies, joint with non-rigid
    body0/body1 target)

Cross-format invariant test confirming the loaders module imports on a
stdlib-only environment (no pxr / mujoco / urdfpy required for URDF +
MJCF; pxr only needed when load_usd is actually called).

Acceptance criteria from #50:

  [x] loaders.load_urdf(path) round-trips a Panda-style URDF
  [x] loaders.load_mjcf(path) round-trips a LIBERO-style MJCF scene
  [x] loaders.load_usd(path) round-trips a USD asset (gated behind
      [isaac] extra; tests skip cleanly when pxr is absent)
  [x] Parse failure raises explicit ValueError with file path +
      offending element — no silent joint_count=0 (closes #33-class)
  [x] DOF count, joint names, joint types, body parents match the
      procedural builders for at least one robot per format (parity
      tests for Panda and so100)
  [x] No binary assets committed — loaders take paths and accept
      user-provided files; tests synthesise fixtures into pytest
      tmp_path and tear them down between runs

Test results: 51 passed, 2 skipped. The 2 skipped are the
"exercise the no-pxr code path" guard tests which only run on
environments without pxr (mutually exclusive with the round-trip USD
tests, which require pxr).

Out of scope (explicitly per the issue, will get follow-ups if needed):

  * Bundling URDF / MJCF / USD assets in this repo
  * Newton-side parser hardening (#33 already tracks it; this PR
    mirrors the eventual fix pattern via explicit-error semantics)
  * The Phase-2 articulation defect in _build_unitree_g1 (duplicate
    parent/child edges) — needs intermediate massless link bodies
    regardless of source format, part of the Phase 2 instantiation
    work tracked under #14

Stacks on top of pr3/isaac-procedural (PR #46) since it consumes the
ProceduralRobot dataclass introduced there.

* fix(isaac): make kinematic-tree guard fail-first, fix G1 topology

cagataycali's comment at strands_robots_sim/isaac/tests/test_procedural_kinematic_guard.py:13
on PR #51 pushed back on the opt-in env-var guard from def3c21:
shipping a robot we know cannot instantiate has no good use case
in this package, and the default should fail first rather than
silently producing a broken robot.

This commit lands the fix-first answer rather than a flag-flip:

* _validate_kinematic_tree now runs unconditionally on every
  procedural builder + every URDF/MJCF/USD loader. The
  STRANDS_ISAAC_VALIDATE_KINEMATICS env-var escape hatch is
  removed; there is no use case for opting out of a check that
  only fires on robots that cannot instantiate.

* _build_unitree_g1 inserts six massless intermediate '*_link'
  bodies (l_hip_link, r_hip_link, l_ankle_link, r_ankle_link,
  l_elbow_link, r_elbow_link) so each 2-DOF compound joint
  (hips, ankles, shoulder-yaw/elbow on each arm) splits across
  two unique (parent, child) edges. Actuated joint count stays
  at 21; only the topology gains the six fixed link bodies, so
  the existing 21-DOF doc-pin in test_procedural_g1_dof.py keeps
  passing.

* test_procedural_kinematic_guard.py is rewritten to pin the
  fail-first contract:
  - all three shipped robots build cleanly under the default
    guard (test_g1_builds_cleanly_by_default,
    test_so100_and_panda_build_cleanly)
  - G1 topology has zero duplicate edges
    (test_g1_topology_has_no_duplicate_edges)
  - an injected duplicate edge raises with diagnostic context
    (test_guard_raises_on_injected_duplicate_edge)
  - procedural.py source contains no env-var gate or os.environ
    read for the guard (test_guard_has_no_env_var_escape_hatch)
    -- a future refactor that re-introduces the escape hatch
    will fail this pin.

hatch run test: 44 passed, 2 skipped.
hatch run lint: clean.

* review(isaac): address PR #51 inline comments — real-asset parity tests + docstring cleanup

Closes the 3 inline review threads cagataycali opened on PR #51:

1. `test_procedural_kinematic_guard.py:13` — "Default behaviour can
   be fail first here. Is there a good use of having broken robot in
   sim?"

   Already implemented in `7f306e9` (the original PR commit). The
   `_validate_kinematic_tree` guard runs unconditionally with no env-
   var escape hatch; ``test_guard_has_no_env_var_escape_hatch`` pins
   that contract via AST scan of `procedural.py`. No code change for
   this thread; reply on the PR confirms the design matches the ask.

2. `test_procedural_g1_dof.py:17` — "Why deferred tho? Lets bash the
   cases related with procedural in this PR"

   The kinematic-topology pin lives in this same PR's companion file
   `test_procedural_kinematic_guard.py`; the docstring on
   `test_procedural_g1_dof.py` was misleading reading as if it were
   "deferred". Replaced the "Note: this pin does NOT cover" framing
   with explicit cross-reference + "Both run on every CI invocation;
   neither is deferred" (the actual state).

3. `test_loaders.py:1` — "Can we add series of tests for verifying
   the robots we have in strands-robots to smoothly maps into isaac?"

   Added `TestRobosuiteMjcfParity` class — 22 parametrized + 1
   aggregate test against the seven robosuite-bundled MJCFs that
   strands-robots' LIBERO adapter consumes (panda / iiwa / kinova3 /
   jaco / sawyer / ur5e / baxter). Each robot is asserted to:

   - Load via `loaders.load_mjcf(...)` without raising
     (`test_robosuite_robot_loads_cleanly`)
   - Match its documented joint count from the spec table
     (`test_robosuite_robot_joint_count_matches_spec`) — Panda 7-DOF,
     UR5e 6-DOF, Baxter dual 7-DOF (14 total), etc.
   - Have all actuated joints classified as revolute (no hinge ->
     prismatic / fixed mis-mapping by the loader)
     (`test_robosuite_robot_joints_are_revolute`)
   - Body indices on every joint are within range (no phantom
     references to non-existent bodies — closes a #33-class failure
     mode preemptively)

   `test_all_embodiments_at_least_load` aggregates failures across
   all robots so a regression in one is visible at a glance even if
   the parametrized output scrolls.

   The robosuite dependency is optional; `_HAS_ROBOSUITE` gating
   skips the whole class when it isn't installed (mirrors the `pxr`
   gate on `TestLoadUsd`).

Verification:

* `pytest strands_robots_sim/isaac/tests/test_loaders.py -v` ->
  **49 passed, 1 skipped** (skip is `pxr`-gated USD path when run
  without the `[isaac]` extra).
* `pytest strands_robots_sim/isaac/tests/ --ignore=test_gpu_integ.py`
  -> 67 passed, 1 skipped (no regressions across the chain).
* `black --check` / `isort --check-only` / `flake8 --max-line-length=120`
  on `strands_robots_sim/` + `examples/` -> clean.

Diff: 2 files, +166 / -3 LOC.

The 7 robosuite robots locked here are the same 7 LIBERO ships
against; a regression on any of them silently breaks the matrix's
mujoco baseline (PR #26's task surface). Catching it in the
loader's unit test suite is cheaper than catching it 3 PRs deep
in a Phase-2 articulation-instantiation failure.
yinsong1986 added a commit that referenced this pull request May 28, 2026
…lit) (#48)

* docs(isaac): backend reference + Phase 1 status banner (5/5 of #31 split)

Part 5 / 5 of the split of #31 — tracked by #42. Branched off PR-1
(`pr1/isaac-foundation` -> #44); rebases cleanly onto `main` once
PR-1 merges. Parallel-mergeable with PR-4 (#47, simulation) -- this
slice has no code dependency on simulation.py.

Adds the user-facing reference doc for the Isaac backend plus the
companion test file that pins R2's "Phase 1 status" disclosure
banner:

- `docs/backends/isaac.md` (208 LOC):
  Install + Quick Start + API reference + environment variables
  + R2's Phase 1 status banner before the Installation section.
  The banner discloses that the Phase 1 skeleton silently no-ops
  the data plane (`add_robot` procedural branch, `_load_usd_robot`,
  `_load_urdf_robot`, `add_object`, `add_camera`, `replicate` all
  return `status: "success"` without instantiating the underlying
  USD prim or articulation handle, with the observable downstream
  effect that `get_observation()` returns `{}` and `render()`
  returns blank frames). R2 reviewer asked for this explicitly so
  a user following the Quick Start sees the disclaimer before the
  silent path.

- `strands_robots_sim/isaac/tests/test_phase1_doc_banner.py`
  (~75 LOC, 3 tests):
  Carved out of cagataycali's original
  `test_phase1_doc_honesty.py` -- the `TestIsaacDocsPhase1Banner`
  class only. The companion `TestG1DOFCount` class lives in PR-3
  (#46) alongside `procedural.py`. Pin tests:
  - `test_isaac_docs_file_exists`: truth-source pin for the doc
    file's location.
  - `test_phase1_banner_present_before_installation`: pins that
    "Phase 1 status" appears AND precedes the `## Installation`
    heading.
  - `test_phase1_banner_names_the_silent_methods`: pins that the
    banner enumerates `add_robot`, `replicate`, `get_observation`
    by name (so a future maintainer who reads only the banner
    knows which API surfaces are affected).

CI signal: lint clean (black / isort / flake8); the 3 doc-banner
pin tests pass standalone (`pytest test_phase1_doc_banner.py -v` ->
3 passed).

Why split `test_phase1_doc_honesty.py` into G1-DOF (PR-3) + doc-
banner (this PR): keeps each parallel-mergeable slice CI-green
standalone. Shipping the original combined file in either PR would
couple them (PR-3 would CI-red until PR-5 lands, or vice versa).
The combined coverage equals the original file.

Original work by @cagataycali in #31 (`413ff15..85e180f`); this
slice cherry-picks `docs/backends/isaac.md` plus the carved-out
banner test file. R2's banner addition (commit `85e180f`) is
already baked into this doc.

* docs(isaac): align with PR #47 + PR #51 merged to main

Updates docs/backends/isaac.md to reflect the actual state of
strands_robots_sim.isaac after PR #44 / #45 / #46 / #47 / #51 all
landed. The Phase 1 status banner had become stale (claimed all data-
plane methods were silent no-op when in fact the procedural builders
work, the loaders module ships, and several SimEngine methods now
have concrete implementations).

Three substantive changes:

1. Phase 1 status banner — split into 'Working today' (config,
   is_available, lifecycle, procedural builders, loaders module) vs
   'Still no-op' (add_object, add_camera, replicate, the per-class
   _load_usd_robot / _load_urdf_robot stubs, articulation-touching
   paths under get_observation / send_action / render). The loaders
   module is the working URDF / MJCF / USD ingestion path today; the
   IsaacSimulation private no-op stubs remain Phase 2 work.

2. New 'Loading External Description Files (URDF / MJCF / USD)'
   section between Procedural Robots and Comparison with Newton.
   Documents the loaders module (PR #51): three load_*() functions
   producing ProceduralRobot dataclass instances, shared failure
   semantics (FileNotFoundError / ValueError, never silent phantom
   robot), and the parity-test pin against the seven robosuite-
   bundled MJCFs the strands-robots LIBERO adapter consumes.

3. Procedural Robots — added the G1 intermediate-link-bodies
   explanation and the fail-first kinematic-tree guard contract
   (PR #51's  commit). No env-var escape hatch.

Plus housekeeping in the Architecture file-list and Testing section:

- Architecture: added _install.py (PR #47's commit  —
  centralised install hints), loaders.py (PR #51), and the actual
  test files now in main (test_get_observation_diagnostic_logs.py,
  test_procedural_g1_dof.py, test_procedural_kinematic_guard.py,
  test_loaders.py) instead of the placeholder list.

- Testing: pytest invocations updated to cover the five no-GPU test
  files now shipped, plus a one-liner for running them all at once
  (the --ignore pattern that the hatch script uses).

The doc was already accurate about the *direction* of Phase 1 vs
Phase 2; the update just brings the surface lists in line with what
actually shipped.

Verification:
- pytest strands_robots_sim/isaac/tests/test_phase1_doc_banner.py
  → 3 passed (the banner-content pin still matches; the rewrite
    keeps 'Phase 1 status' + the same enumerated method names).
- diff stat: docs/backends/isaac.md, +51 / -7 LOC.

This change lands inside PR #48 (5/5 of #31 split) since it's the
canonical home for docs/backends/isaac.md updates. Top-level README
and examples/MIGRATION.md drift (Stage-3-no-longer-future framing)
is a separate doc-alignment concern and lands in a follow-up PR
rather than widening this slice.
yinsong1986 added a commit that referenced this pull request May 28, 2026
Top-level docs claimed Isaac would 'land in v0.3.0+' as future work,
but PR #44 / #45 / #46 / #47 / #51 (R6 + R7 Phase 1) all merged to
main on 2026-05-26. Updates the doc-side framing to reflect what's
actually shipped vs what's still pending.

README.md:

1. Status banner: 'Backend code lands in v0.3.0+ (Isaac)' →
   explicit Phase 1 / Phase 2 split. Names what works today (entry-
   point registration, IsaacConfig, lifecycle scaffolding,
   procedural builders, URDF / MJCF / USD loaders) vs what's still
   no-op (add_object, add_camera, replicate, per-IsaacSimulation
   _load_*_robot stubs).

2. Quick-start preamble: 'become copy-paste-runnable when R6/#13
   and R7/#14 ship' → 'Isaac Sim Phase 1 has shipped — the snippet
   below is copy-paste-runnable today on a host with Isaac Sim
   2024.x+ installed'. Newton snippets remain gated on R11/#18.

3. Isaac Sim quick-start snippet: fixed the kwarg name
   (rtx_mode='path_traced' → render_mode='rtx_pathtracing') so
   it matches the IsaacConfig field actually defined in
   strands_robots_sim/isaac/config.py. The previous spelling
   would TypeError at construction. Added a second snippet showing
   the loaders module (load_urdf / load_mjcf / load_usd) since
   that's the working URDF / MJCF / USD ingestion path today.

4. Status & roadmap section:
   - Stage 2 R5: [ ] → [x] (#26 merged).
   - Stage 3 R6: [ ] → [x] with link to #44.
   - Stage 3 R7: split into 'Phase 1 [x]' (linking #45 / #46 /
     #47 / #51) and 'Phase 2 [ ]' so the next-up scope is visible
     without dropping the historical #14 reference.

examples/MIGRATION.md:

1. [isaac] extras line: 'Heavy GPU-only backends ship in later 0.x
   releases' / commented-out pip install → uncommented; comment
   notes 'Phase 1 shipped; data-plane in Phase 2'. Newton stays
   commented (Stage 4 pending).

2. 'After — same task on Isaac Sim' heading: 'Stage 3, future' →
   'Stage 3, Phase 1 shipped'. Snippet kwarg fixed
   (rtx_mode → render_mode; same bug as the README). Added a
   2-line comment noting evaluate_benchmark on a real LIBERO
   scene needs Phase 2 data-plane wiring (matches the disclosure
   banner on docs/backends/isaac.md, so the three doc surfaces
   stay in sync).

examples/README.md is left as-is: its TBD rows reference the
example files (libero/run_isaac.py etc.), which haven't shipped
yet — those issue links (R8/#15, R23/#27) are still accurate
forward-pointers.

Verification:
- diff stat: 2 files, +28 / -14 LOC. No code touched.
- README + MIGRATION render cleanly under standard markdown
  parsers (verified by spot-checking the table / heading nesting).
- The doc-banner test (test_phase1_doc_banner.py) is on the
  parallel pr5/isaac-docs branch (PR #48); the docstring banner
  on docs/backends/isaac.md was already updated there to the same
  framing — this PR brings README + MIGRATION in line.

Cross-references: PR #36 (the previous examples/README.md drift
fix) lands the same kind of post-merge alignment for #26's R5
work; this is its R7 Phase 1 sibling.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants