Skip to content

Commit 93bef42

Browse files
committed
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.
1 parent 3eeab25 commit 93bef42

2 files changed

Lines changed: 166 additions & 3 deletions

File tree

strands_robots_sim/isaac/tests/test_loaders.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,162 @@ def test_load_urdf_does_not_require_pxr(self, tmp_path: Path) -> None:
524524
# ``loaders`` itself in this test file).
525525
robot = loaders.load_urdf(str(urdf_path))
526526
assert robot.num_joints == 7
527+
528+
529+
# ---------------------------------------------------------------------------
530+
# Real-asset parity: robosuite robots that strands-robots' LIBERO adapter
531+
# consumes via MJCF must round-trip through ``load_mjcf`` cleanly.
532+
#
533+
# Closes cagataycali's review on PR #51 asking for "tests for verifying the
534+
# robots we have in strands-robots to smoothly maps into isaac".
535+
#
536+
# The strands-robots LIBERO adapter uses robosuite's bundled MJCF assets
537+
# (``robosuite/models/assets/robots/<name>/robot.xml``) for the seven
538+
# robot embodiments LIBERO ships against: baxter / iiwa / jaco / kinova3
539+
# / panda / sawyer / ur5e. Loading each via ``load_mjcf`` proves the
540+
# loader handles the wire format the rest of the strands ecosystem
541+
# already produces -- not just the synthetic fixtures above.
542+
#
543+
# The ``robosuite`` package is an optional / heavy dep (pulls mujoco +
544+
# numpy etc.); these tests skip when it's not on PYTHONPATH, mirroring
545+
# the ``pxr`` import-guard pattern on ``TestLoadUsd``.
546+
# ---------------------------------------------------------------------------
547+
548+
549+
_ROBOSUITE_ROBOT_EMBODIMENTS = {
550+
"panda": {"min_joints": 7, "max_joints": 7}, # Franka Panda 7-DOF arm
551+
"iiwa": {"min_joints": 7, "max_joints": 7}, # KUKA LBR iiwa 7-DOF
552+
"kinova3": {"min_joints": 7, "max_joints": 7}, # Kinova Gen3 7-DOF
553+
"jaco": {"min_joints": 7, "max_joints": 7}, # Kinova Jaco 7-DOF
554+
"sawyer": {"min_joints": 7, "max_joints": 7}, # Rethink Sawyer 7-DOF
555+
"ur5e": {"min_joints": 6, "max_joints": 6}, # Universal Robots UR5e 6-DOF
556+
"baxter": {"min_joints": 14, "max_joints": 14}, # Rethink Baxter dual 7-DOF arms
557+
}
558+
559+
560+
def _robosuite_robot_xml_path(robot_name: str) -> Path | None:
561+
"""Return the path to the robosuite-bundled MJCF for ``robot_name``.
562+
563+
Returns ``None`` if ``robosuite`` isn't installed, so each test can
564+
skip individually without a session-level fixture.
565+
"""
566+
try:
567+
import robosuite
568+
except ImportError:
569+
return None
570+
rs_root = Path(robosuite.__file__).parent
571+
candidate = rs_root / "models" / "assets" / "robots" / robot_name / "robot.xml"
572+
return candidate if candidate.is_file() else None
573+
574+
575+
_HAS_ROBOSUITE = _robosuite_robot_xml_path("panda") is not None
576+
577+
578+
@pytest.mark.skipif(
579+
not _HAS_ROBOSUITE,
580+
reason="robosuite not installed; real-asset parity tests are gated on the optional dep",
581+
)
582+
class TestRobosuiteMjcfParity:
583+
"""Pin: every robot embodiment LIBERO consumes via robosuite's bundled
584+
MJCFs must round-trip through ``load_mjcf`` and produce a sensible
585+
ProceduralRobot with the documented joint count.
586+
587+
Locking the joint counts catches two failure modes at once:
588+
589+
1. **Loader regression** (e.g. revolute / prismatic / fixed joint
590+
handling drifts under future refactors) — would surface as a
591+
``num_joints`` mismatch on a known robot.
592+
2. **strands-robots upstream change** (e.g. robosuite ships a
593+
different MJCF schema or renames the asset) — would surface as
594+
a missing-file skip in CI rather than a silent zero-joints
595+
phantom robot (the #33-class failure).
596+
"""
597+
598+
@pytest.mark.parametrize("robot_name", sorted(_ROBOSUITE_ROBOT_EMBODIMENTS))
599+
def test_robosuite_robot_loads_cleanly(self, robot_name: str) -> None:
600+
"""Each robosuite-bundled MJCF must load without raising."""
601+
xml_path = _robosuite_robot_xml_path(robot_name)
602+
if xml_path is None:
603+
pytest.skip(f"robosuite asset for {robot_name!r} not present (custom robosuite install?)")
604+
605+
robot = loaders.load_mjcf(str(xml_path))
606+
607+
assert robot.name, f"loaded {robot_name!r} robot has empty name (MJCF model attribute missing?)"
608+
assert len(robot.bodies) > 0, f"loaded {robot_name!r} robot has zero bodies"
609+
# Every body referenced by a joint must be a real body in the dataclass.
610+
body_count = len(robot.bodies)
611+
for j in robot.joints:
612+
assert 0 <= j.parent_body < body_count, (
613+
f"{robot_name}: joint {j.name!r} parent_body={j.parent_body} out of range "
614+
f"(robot has {body_count} bodies)"
615+
)
616+
assert 0 <= j.child_body < body_count, (
617+
f"{robot_name}: joint {j.name!r} child_body={j.child_body} out of range "
618+
f"(robot has {body_count} bodies)"
619+
)
620+
621+
@pytest.mark.parametrize("robot_name", sorted(_ROBOSUITE_ROBOT_EMBODIMENTS))
622+
def test_robosuite_robot_joint_count_matches_spec(self, robot_name: str) -> None:
623+
"""Loaded joint count must match the documented DOF for the embodiment.
624+
625+
Locking these prevents silent regressions where a loader change
626+
drops a joint type or duplicates one. Spec values are based on
627+
each robot's published kinematic spec (from robosuite's docs).
628+
"""
629+
xml_path = _robosuite_robot_xml_path(robot_name)
630+
if xml_path is None:
631+
pytest.skip(f"robosuite asset for {robot_name!r} not present")
632+
633+
spec = _ROBOSUITE_ROBOT_EMBODIMENTS[robot_name]
634+
robot = loaders.load_mjcf(str(xml_path))
635+
636+
assert spec["min_joints"] <= robot.num_joints <= spec["max_joints"], (
637+
f"{robot_name}: loaded {robot.num_joints} joints, expected "
638+
f"{spec['min_joints']}-{spec['max_joints']}. Either the loader regressed "
639+
f"or robosuite's MJCF for this robot changed; verify the spec table above "
640+
f"against the upstream asset."
641+
)
642+
643+
@pytest.mark.parametrize("robot_name", sorted(_ROBOSUITE_ROBOT_EMBODIMENTS))
644+
def test_robosuite_robot_joints_are_revolute(self, robot_name: str) -> None:
645+
"""Every robosuite arm uses revolute joints; the loader must reflect that.
646+
647+
All seven robots are revolute-only manipulators (no prismatic
648+
actuation in the standard arm bodies). If the loader misclassifies
649+
a hinge as prismatic or fixed, this test catches it.
650+
"""
651+
xml_path = _robosuite_robot_xml_path(robot_name)
652+
if xml_path is None:
653+
pytest.skip(f"robosuite asset for {robot_name!r} not present")
654+
655+
robot = loaders.load_mjcf(str(xml_path))
656+
joint_types = {j.joint_type for j in robot.joints if j.joint_type != "fixed"}
657+
assert joint_types == {"revolute"}, (
658+
f"{robot_name}: expected all actuated joints to be revolute; got {sorted(joint_types)}. "
659+
f"The loader's hinge -> revolute mapping may be misclassifying joints."
660+
)
661+
662+
def test_all_embodiments_at_least_load(self) -> None:
663+
"""Sanity check: every robosuite robot in the spec table loads.
664+
665+
If this fails on a particular robot, the parametrized tests above
666+
will give a more specific diagnostic. This test exists to make
667+
the failure visible at a glance even when the parametrized cases
668+
scroll off-screen.
669+
"""
670+
failures = []
671+
for robot_name in _ROBOSUITE_ROBOT_EMBODIMENTS:
672+
xml_path = _robosuite_robot_xml_path(robot_name)
673+
if xml_path is None:
674+
continue
675+
try:
676+
loaders.load_mjcf(str(xml_path))
677+
except Exception as exc: # noqa: BLE001 - aggregate failures
678+
failures.append(f" {robot_name}: {type(exc).__name__}: {exc}")
679+
680+
assert not failures, (
681+
"robosuite-bundled MJCFs failed to load:\n"
682+
+ "\n".join(failures)
683+
+ "\nstrands-robots' LIBERO adapter consumes these directly; a regression here "
684+
"would silently break the matrix's mujoco baseline for any of the affected robots."
685+
)

strands_robots_sim/isaac/tests/test_procedural_g1_dof.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
``test_phase1_doc_banner.py`` (lands in PR-5 alongside the docs file
1212
itself).
1313
14-
Note: this pin does NOT cover the kinematic-tree topology validity (each
15-
non-root link has exactly one inbound joint); that invariant is pinned
16-
separately in ``test_procedural_kinematic_guard.py``.
14+
The kinematic-tree topology invariant (each non-root link has exactly
15+
one inbound joint; G1 splits its 2-DOF compound joints through six
16+
intermediate massless link bodies) is pinned in this same PR's companion
17+
file ``test_procedural_kinematic_guard.py``. Split by concern: this
18+
file pins doc/comment honesty against a stale literal value, that file
19+
pins the invariant the literal value is asserting against. Both run on
20+
every CI invocation; neither is deferred.
1721
"""
1822

1923
from __future__ import annotations

0 commit comments

Comments
 (0)