Skip to content

feat(newton): stub NewtonSimulation(SimEngine) skeleton — PR 1/7#104

Closed
cagataycali wants to merge 37 commits intostrands-labs:mainfrom
cagataycali:feat/newton-backend
Closed

feat(newton): stub NewtonSimulation(SimEngine) skeleton — PR 1/7#104
cagataycali wants to merge 37 commits intostrands-labs:mainfrom
cagataycali:feat/newton-backend

Conversation

@cagataycali
Copy link
Copy Markdown
Member

Summary

Add the Newton GPU-native simulation backend skeleton. This is PR 1 of 7 in the Newton backend migration.

Dependency: This PR depends on PR #84 (simulation foundation / SimEngine ABC). It will conflict until #84 merges. Once #84 lands, this PR should rebase cleanly.

What's included

File LOC Purpose
simulation/newton/__init__.py 52 Lazy exports — importing does NOT trigger Warp load
simulation/newton/config.py 95 NewtonConfig dataclass with fail-fast validation
simulation/newton/solvers.py 48 SOLVER_MAP (7 solvers), RIGID_BODY_SOLVERS, render/broadphase constants
simulation/newton/simulation.py 630 NewtonSimulation(SimEngine) — all 12+4 ABC methods + Newton extensions
simulation/factory.py +6 Register "newton" backend + "warp", "nt" aliases
tests/test_newton_backend.py 320 47 tests: config, factory, lazy import, lifecycle, errors

Total: ~1,150 LOC added, 1 line modified.

Design decisions

  1. Lazy imports: NewtonSimulation class is lazy-loaded via __getattr__. Importing strands_robots.simulation.newton only loads config + constants (no Warp/Newton).

  2. Fail-fast config: NewtonConfig.__post_init__ validates solver, render_backend, broad_phase, physics_dt, num_envs, substeps. Bad values fail immediately — before any GPU resource is touched.

  3. All methods stubbed: Every SimEngine abstract method has a documented stub with proper error handling. Methods raise RuntimeError("World not created") when called without create_world().

  4. Factory registration: _BUILTIN_BACKENDS["newton"] and aliases "warp", "nt" so create_simulation("newton") and create_simulation("warp") work.

  5. Newton extensions: replicate(), run_diffsim(), solve_ik(), add_sensor(), read_sensor(), enable_dual_solver() — all documented and stubbed.

Tests

47 new tests, 0 regressions on feat/simulation-foundation:

279 passed, 6 skipped in 1.19s

Test categories:

  • Config validation (10): valid/invalid solver, render_backend, broad_phase, dt, envs, substeps
  • Solver constants (5): map size, rigid/soft categorisation, backends/broadphase
  • Factory registration (6): builtin backends, aliases, list_backends()
  • Lazy import (3): config, solvers, init all import without Warp
  • Simulation (23): construction, kwargs, get_state, error handling (missing world, duplicates, invalid params), context manager, repr

Next PRs

  1. World lifecycle + so100 procedural builder (~600 LOC)
  2. Observation/action API (~300 LOC)
  3. Rendering (~400 LOC)
  4. replicate() + 4096-env benchmark (~250 LOC)
  5. solve_ik + diffsim (~350 LOC)
  6. Docs + examples

🤖 AI agent response. Strands Agents. Feedback welcome!

cagataycali and others added 30 commits April 17, 2026 18:52
Changed logger.debug to logger.warning when auto-download module is
unavailable, so users see actionable guidance to install robot_descriptions.
…C21)

Added logging import and changed bare 'except Exception: pass' to log
the error at debug level. This helps diagnose cleanup bugs while still
preventing GC exceptions from propagating.
Changed load_scene, run_policy, randomize, get_contacts from returning
error dicts to raising NotImplementedError. This makes unimplemented
features explicit during development. Updated tests accordingly.
Added logger.debug for _HAS_ASSET_MANAGER so users can diagnose
why model resolution may fail.
…C28)

Replaced inline lazy import of strands_robots.registry inside
resolve_urdf() with top-level try/except and _HAS_REGISTRY guard.
Cleaner and follows project conventions.
… C30)

- register_backend now raises ValueError if name/alias already exists
  unless force=True is passed (C22, C23)
- Protects built-in aliases (mj, mjc, mjx) from accidental override
- Fixed test to use lambda: FakeBackend (correct loader signature) and
  verify create_simulation returns correct instance (C30)
- Added tests for duplicate rejection and alias conflict detection
Renamed primary ABC class from SimulationBackend to SimEngine for
consistency with Sim* prefix convention (SimWorld, SimRobot, SimObject).
SimulationBackend kept as backward-compatible alias.
Updated factory, __init__, and all tests.
- Removed mujoco/ subtree from Architecture docstring since those
  modules don't exist in this PR (they come in subsequent PRs)
- Cleared _LAZY_IMPORTS of mujoco-specific entries that would cause
  ModuleNotFoundError
- Commented out Simulation/MuJoCoSimulation/MJCFBuilder from __all__
- Simplified Usage docstring to show only what's available now
…s (C27)

Changed resolve_model() to check local/custom paths first, then fall
back to Menagerie asset manager. This ensures users who place custom
URDFs in ./urdfs/ or STRANDS_URDF_DIR get their version, not the
default Menagerie one.
… C8, C10)

- assets/__init__.py reduced from 282 to 42 lines (thin exports only)
- All implementation moved to assets/manager.py
- Fixed docstring: clarified assets are NOT bundled, meshes are
  downloaded on-demand from robot_descriptions/GitHub (C4)
- Added comment explaining lazy import of download_assets (C8)
- download.py now re-exports from tools/download_assets.py (C10)

This addresses the core structural concern about __init__.py being
too heavy (AGENTS.md Convention strands-labs#3).
…S_URDF_DIR (C5)

STRANDS_ASSETS_DIR is now the single canonical env var for custom
asset paths. STRANDS_URDF_DIR still works but emits a
DeprecationWarning guiding users to switch.
- models.py: SimWorld, SimRobot, SimObject, SimCamera, TrajectoryStep,
  SimStatus dataclasses for typed simulation state management (C24)
- tools/download_assets.py: robot asset download via robot_descriptions
  with git clone fallback. Note: download logic is the single source
  of truth, consumed by assets/manager.py (addresses C11 concern)
Reduced search paths from 5 to 2 (plus env var overrides):
- ~/.strands_robots/assets/ (user cache)
- CWD/assets/ (project-local)
Added documentation explaining the resolution order and pointing
to resolve_model() as the preferred API.
…er docstring (C17, C20)

- Added Method categories section to SimEngine docstring explaining
  the rationale for required (@AbstractMethod) vs optional methods
- Improved render() docstring to specify return type: dict with
  'image' (numpy RGB uint8) and optional 'depth' (float32)
…E (C6, C7)

Documented STRANDS_ASSETS_DIR, GROOT_API_TOKEN env vars in a table.
Added cache directory section explaining ~/.strands_robots/assets/
purpose, how to clear it, and how to override location.
…acade methods (C16, C18)

Added docstrings explaining these are convenience/facade methods that
delegate to Robot abstraction. SimEngine intentionally provides a
unified interface for agent tools (simulation tool actions) without
requiring users to understand the Robot/Policy/Sim separation.
…levels

Mypy fixes (25 errors → 0):
- Add from __future__ import annotations to all simulation modules
- Fix implicit Optional params (float = None → float | None = None)
- Add missing return type annotations (__post_init__ → None, etc.)
- Add **kwargs: Any type annotations to abstract methods
- Fix Any return types with explicit str() casts

Review feedback:
- Remove SimulationBackend backward-compat alias from base.py — not
  needed since this PR introduces SimEngine (self-review thread strands-labs#35)
- Remove SimulationBackend from __init__.py exports and __all__
- Remove stale comments from __init__.py (thread strands-labs#36)
- Fix __del__ log level: logger.debug → logger.warning (thread strands-labs#26)
- Fix asset manager log level: logger.debug → logger.info (thread strands-labs#31)
- Update test: remove test_backward_compat_alias (alias removed)

Tests: 24/24 pass, ruff clean, mypy clean
…ts.py

- assets/manager.py: 4 no-any-return errors — add str() casts for dict
  subscript returns used in Path operations (dict[str,Any]['key'] → Any)
- tools/download_assets.py: annotate _needs_download/_get_source params
  as dict[str,Any], add type annotations to local variables, fix PEP 484
  implicit Optional (robots: str = None → str | None = None), filter None
  from all_sim dict comprehension
…riptions to stubs

1. download_assets.py:41 — type: ignore[misc] → type: ignore[no-redef]
   (mypy error code was no-redef, not misc)
2. pyproject.toml — add robot_descriptions.* to ignore_missing_imports
   (optional dep without type stubs, same as lerobot/torch/etc)

Tests: 290 passed, mypy clean, ruff clean.
…fix mypy

Registry:
- Expand robots.json from 38 to 68 robots (all MuJoCo-validated)
- Add new categories: aerial, expressive, mobile_manip
- New robots include Stretch, Spot, Sawyer, ANYmal, Crazyflie, etc.
- Sort entries alphabetically by category then name

User registration API (register_robot/unregister_robot):
- New strands_robots/registry/user_registry.py
- Persists to ~/.strands_robots/user_robots.json
- Merged at load time via loader._merge_user_robots()
- Supports aliases, hardware config, robot_descriptions_module
- Validates model_xml exists, normalizes names
- Exported from strands_robots.registry.__init__

Shared path utilities (strands_robots/utils.py):
- get_base_dir(), get_assets_dir(), resolve_asset_path()
- Single source of truth for STRANDS_ASSETS_DIR resolution
- Used by user_registry, model_registry, asset manager

mypy: 37 → 0 errors:
- simulation/base.py: explicit Optional types, return annotations
- simulation/factory.py: rename cls → backend_cls (no-redef)
- simulation/models.py: __post_init__ return type
- simulation/model_registry.py: register_urdf return type, str() wraps
- tools/download_assets.py: Optional params, None guards, type annotations
- assets/manager.py: typed candidates list, Path() wraps

Tests:
- New tests/test_user_registry.py (364 lines, 20 test cases)
- 324 tests pass, ruff clean, mypy 0 errors
The code in assets/manager.py and tools/download_assets.py references
robot_descriptions for automatic asset downloads, and the docstrings
reference 'pip install strands-robots[sim]', but the [sim] extra was
never declared in pyproject.toml.

Add:
- sim extra: robot_descriptions>=1.11.0,<2.0.0
- Include [sim] in [all] extra
…ndomize base

Expand the base SimEngine.add_object signature with orientation and
mesh_path parameters needed by concrete backends (MuJoCo).
Simplify randomize() docstring — backends define their own params.
1. manager.py: Fix docstring — we don't bundle assets, use robot_descriptions
2. manager.py: Remove _BUNDLED_DIR and redundant STRANDS_ASSETS_DIR handling
3. model_registry.py: Remove 'legacy' prefix, remove redundant 'as' aliases
4. download_assets.py: Simplify strands import (strands is a required dep)
5. download_assets.py: Remove CLI main() — tool is the entry point
6. download_assets.py: Document :3 mesh check heuristic
7. robots.json: Remove empty aliases arrays (omit key instead)
8. user_registry.py: Use public invalidate_cache() instead of private _cache/_mtimes
9. models.py: Replace MuJoCo-specific field comments with generic engine comments

Tests: 84 passed, 0 failures (0.89s)
- Add invalidate_cache to __all__ in registry/__init__.py
- Merge unsorted imports in simulation/model_registry.py
- Remove unused argparse import in tools/download_assets.py
- Fix import block formatting in tools/download_assets.py
main() was removed from tools/download_assets.py in 2cfcbfc but the
import in assets/download.py was not cleaned up, breaking mypy:

  strands_robots/assets/download.py:7: error: Module
  "strands_robots.tools.download_assets" has no attribute "main"

Removes: main import, __all__ entry, __main__ block.
Tests: 238 passed, 6 skipped (2.29s)
Per @awsarron review (PR strands-labs#84 threads on download.py and download_assets.py):
tools should be lightweight wrappers that expose strands-robots library
functions to agents, not contain the actual implementation.

Before:
  tools/download_assets.py (493 lines) — all download logic + @tool
  assets/download.py (14 lines) — just re-exports from tools

After:
  assets/download.py (421 lines) — all download logic (library code)
  tools/download_assets.py (78 lines) — thin @tool wrapper

Changes:
- assets/download.py: moved download_robots(), all backends
  (_download_via_robot_descriptions, _download_via_git,
  _download_from_github), and helpers from tools/download_assets.py
- tools/download_assets.py: slim @tool wrapper that parses action/args
  and delegates to assets.download.download_robots()
- assets/manager.py: fix _auto_download_robot() lazy import from
  tools.download_assets → .download (same package, relative import)
- tools/__init__.py: add download_assets to lazy imports

No circular imports: assets/download.py imports from assets/manager.py
at module level; manager.py lazy-imports from assets/download.py inside
_auto_download_robot() only.

Tests: 238 passed, 6 skipped, 0 failures.
strands-agent and others added 7 commits April 17, 2026 18:52
Per @awsarron review thread — removed tests that test Python itself
(dataclass defaults, dict membership, enum values) rather than behavior.

Removed:
- test_sim_robot_defaults — tests dataclass default values
- test_sim_robot_custom_position — tests kwarg passthrough
- test_sim_object_defaults — tests dataclass defaults
- test_sim_camera_defaults — tests dataclass defaults
- test_sim_world_defaults — tests dataclass defaults
- test_sim_status_enum — tests enum values (Python itself)
- test_trajectory_step_default_instruction — tests default empty string
- test_list_backends_returns_list — tests isinstance(list)
- test_resolve_known_model — tests None-or-string (weak)
- test_resolve_unknown_returns_none — tests None return

Kept all behavioral tests: factory round-trip, context manager cleanup,
registration validation, URDF registration+resolution, model table output.

Consolidated remaining dataclass tests into TestSimModelsUsage with
behavioral names (tracks_robots, preserves_originals, records_episode_data).

Tests: 228 passed, 6 skipped, 0 failures (was 238).
…s, legacy docs, mesh check

Addresses review comments from @awsarron on PR strands-labs#84:

- Fix ruff I001 import sort in test_simulation_foundation.py (CI fix)
- manager.py: remove redundant STRANDS_ASSETS_DIR handling — get_assets_dir()
  is now the single source of truth (lines 46-52 were redundant)
- models.py: remove MuJoCo-specific references from base dataclasses — now
  says 'engine-specific handle (set by simulation backend)' instead of
  referencing mj.MjModel/mj.MjData
- model_registry.py: document 'legacy_urdf' meaning (backward-compat URDF
  paths from before MJCF asset system), remove redundant 'as' aliases in
  import block
- download.py: remove ':3' mesh check limit — now validates all mesh files
  instead of sampling first 3
- download_assets.py: remove unused main(), add README docstring about
  auto-download sources (robot_descriptions, git, HuggingFace)
_resolve_robot_descriptions_module returns dict .get() result which
mypy sees as Any. Add type annotation and str() cast to satisfy
warn_return_any. Fixes CI failure on ca4ca6c.
Move _xml, _robot_base_xml, and _tmpdir from SimWorld into a generic
_backend_state dict. Each backend stores its format-specific data there
instead of polluting the base class with implementation details.

Addresses @awsarron review: 'how can we avoid having implementation
details (Mujoco) in base classes like this?'

The MuJoCo backend (PR strands-labs#85) will store these in
world._backend_state['xml'], etc. during rebase.
…on in _shallow_clone

Address @mrgh-test review comments on PR strands-labs#84:

1. manager.py: Add _safe_join() validation when resolving asset paths.
   Registry-sourced asset_dir_name and xml_file values are now validated
   against path-traversal attacks (../) before filesystem access.
   Extracted _resolve_candidates() helper to centralise the safe-join +
   search loop that was repeated 3 times in resolve_model_path().
   Also applied _safe_join in resolve_model_dir().

2. download.py: Move URL validation into _shallow_clone() itself.
   Added _ALLOWED_CLONE_URL_RE that only accepts HTTPS github.com URLs.
   This prevents ssh://, git://, file:// and other schemes regardless
   of the caller, removing the burden from individual call sites.
   Updated exception handlers to catch ValueError from the new validation.

Tests: 228 passed, 6 skipped, 0 failures.
Lint: ruff + mypy clean.
Address @mrgh-test callout on PR strands-labs#84 thread 3 (user_registry.py:95):
register_robot() must not be exposed as an agent @tool without
STRANDS_TRUST_REMOTE_CODE gating + path validation, since malicious
MJCF could execute code via MuJoCo plugins.

Tests: 228 passed, 6 skipped, 0 failures.
…factory, and 47 tests

Add Newton GPU-native simulation backend skeleton (PR 1/7):

- NewtonConfig dataclass with fail-fast validation (solver, device,
  physics_dt, num_envs, substeps, render_backend, broad_phase)
- SOLVER_MAP with all 7 Newton solvers + RIGID_BODY_SOLVERS categorization
- NewtonSimulation(SimEngine) with all 12 required + 4 optional ABC methods
- Newton-specific extensions: replicate(), run_diffsim(), solve_ik(),
  add_sensor(), read_sensor(), enable_dual_solver(), add_cloth/cable/particles
- Factory registration: _BUILTIN_BACKENDS['newton'], aliases 'warp' and 'nt'
- Lazy imports: importing config/solvers does NOT trigger Warp/Newton load
- Context manager protocol (__enter__/__exit__)
- 47 tests covering config validation, factory registration, lazy imports,
  SimEngine conformance, error handling (duplicate names, missing world,
  invalid params)

All methods are present with documented stubs — implementations arrive in
PRs 2-7 (world lifecycle, obs/action, rendering, replicate, diffsim, docs).

Tests: 279 passed, 6 skipped (0 regressions). Ruff clean.
@cagataycali
Copy link
Copy Markdown
Member Author

Closing as duplicate of #99 — three concurrent Newton-stub PRs were opened simultaneously (#99, #102, #104). Consolidating on #99 which has the most complete scope (28 files) and has been iterated on. If reviewers prefer pieces from this PR, we will cherry-pick before merging #99.

Canonical PR: #99
Tracking issue: #96

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