fix(simulation): create_world's ground_plane is checked, not read by truthiness - #3553
Conversation
…truthiness The MuJoCo and Newton backends validated every knob beside ground_plane (timestep, gravity, terrain, difficulty) and read this one by truthiness, so "false"/"no"/"off"/"0" laid the floor the word declines and None/0/"" omitted it without being a declared spelling - every row status=success. Bind the shared boolean_flag_error domain through the existing SimEngine._validate_posture_flags envelope, ahead of the world-exists report that reads the flag and ahead of the build. Isaac's create_world reads the same flag and is left for the open Isaac backend rewrite to absorb (merge-order item).
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
Binds create_world's ground_plane on the MuJoCo and Newton backends to the shared boolean_flag_error domain through the existing SimEngine._validate_posture_flags envelope, so a non-boolean is refused by name with a structured error instead of laying (or omitting) a floor by truthiness. The production change is two if err := ...: return err guards, placed after the terrain/difficulty domain checks (which the flag does not gate) and ahead of everything that reads the flag - MuJoCo's world-exists report and both builds - with the ordering pinned in both directions by the new test module. Verified against the head: _validate_posture_flags and boolean_flag_error(value, param, context) exist at the merge base with matching signatures; every internal caller of create_world passes defaults or literal booleans, and the tool spec already declares the parameter boolean, so no working caller is newly refused; the refused call builds nothing on either backend (pinned). The three commits main gained since the merge base touch neither backend module nor SimEngine, so the composition is clean. The Isaac backend's identical truthiness read is explicitly deferred behind the open rewrite (#3343), which is the right merge-order call.
What's good
- Follows the AGENTS.md posture-flag convention (checked, not read by truthiness; refusal ahead of the side effect it selects; error dict, never raise) and matches the shape of the sibling fixes that just landed (#3546/#3548/#3549).
- Guard ordering is justified against the existing difficulty-domain suite and pinned in both directions rather than asserted in prose.
- Newton class grades the real
create_worldon a__new__engine, the same shape astests/simulation/newton/test_create_world_terrain_reject.py, so it runs (and failed pre-fix) without warp installed. - Changelog fragment carries the PR number; added hunks are plain ASCII; the
:func:cross-reference targets resolve and stay on one line.
What
create_world(ground_plane=...)on the MuJoCo and Newton backends is held toboolean_flag_error- the domain the rollout facades in the same class already apply throughSimEngine._validate_posture_flags- so a non-boolean is refused by name instead of selecting a floor by truthiness.Why
The flag selects a posture: lay a floor at
z=0or leave the world open. Both backends validate every knob beside it (timestepandgravitythrough the shared numeric bindings,terrainanddifficultythrough their own domains) and read this one by truthiness - MuJoCo compiles the planeif world.ground_plane, Newton callsbuilder.add_ground_plane()under the same test. Measured on07ba807through the MuJoCo facade, readinggeom_typeoff the compiled model:create_world(ground_plane=)True/False"false""no""off""0"1nanNone00.0""[]{}Nothing raised and nothing logged on either half. What a caller sees is a scene: a floating-base robot spawned into a world it asked to be open lands on a floor, and a policy evaluated on a world it asked to have a floor falls through the one it did not get - reported by whichever predicate reads the height first, not by the argument that caused it.
Where the check sits
After the
terrain/difficultydomain guards and ahead of everything that reads the flag: MuJoCo's world-exists report (which describes the world it cannot rebuild in terms ofground_plane) and the build itself. Not ahead ofterrain/difficulty: the flag gates neither of those reads, andtests/simulation/test_create_world_difficulty_domain.pydrives both backends'create_worldon a stub carrying only the state that runs before its guard - a first draft placed the check first and that suite reported 12 parity failures, which is the suite doing its job. The settled ordering is pinned in both directions (an unusable terrain is still named first; the flag is named ahead of the world-exists report on MuJoCo and ahead of the timestep it would build with on Newton).Four corners
create_world/ posture suites (tests/simulation -k "create_world or ground_plane or terrain or difficulty or posture", 760 tests)mainin this envmain, byte-identical node ids (alllerobot/serialabsent), 0 attributableThe Newton class grades the real
create_worldon a__new__engine with_rebuildstubbed, so it runs (and fails pre-fix) withoutnewton/warpinstalled - the shapetests/simulation/newton/test_create_world_terrain_reject.pyalready uses.Gate
ruff check+ruff format --checkclean on touched files;mypyno issues on the two backend modules and the test; touched hunks are plain ASCII.Scope
Production:
+2lines per backend (oneif err := ...: return err) plus a comment and a docstring sentence each. Tests: one module. Docs: one sentence indocs/simulation/world-building.mdbeside the paragraph that already callsground_planethe master floor switch.SimEngine.create_world's base docstring andoverview.mdare deliberately untouched - the base contract should state the domain once every backend enforces it, andtest_overview_documents_the_rollout_posture_flags.pypins exactly one posture-flag paragraph on that page.Not touching the Isaac backend, which reads the same flag by truthiness: #3343 (open) rewrites
strands_robots/simulation/isaac/simulation.py, so that is a merge-order item for after it lands, alongside the fourIsaacConfigflags #3548 records for the same reason. #3343 also edits both backend modules touched here, but none of its hunks fall inside eithercreate_world(read from its diff againstmain: the nearest MuJoCo hunk starts at line 1635, the nearest Newton hunk ends at line 320 andcreate_worldbegins at 339), so the two compose textually clean.Opened as a draft so the changelog fragment - which needs this number - ships before any approval can be dismissed by it.