Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ authors = [
keywords = [
"robots", "agents", "strands", "sdk",
"mujoco", "simulation", "lerobot", "reinforcement-learning",
"gr00t", "isaac-sim", "vla",
"gr00t", "isaac-sim", "vla", "newton", "warp",
"robot-learning", "imitation-learning",
"teleoperation", "manipulation", "strands-agents",
]
Expand Down Expand Up @@ -51,10 +51,15 @@ lerobot = [
sim = [
"robot_descriptions>=1.11.0,<2.0.0",
]
newton = [
"warp-lang>=1.1.0,<2.0.0",
"newton-sim>=0.4.0,<1.0.0",
]
all = [
"strands-robots[groot-service]",
"strands-robots[lerobot]",
"strands-robots[sim]",
# "strands-robots[newton]", # excluded: newton-sim not yet on PyPI
]
dev = [
"pytest>=6.0,<9.0.0",
Expand Down Expand Up @@ -128,7 +133,7 @@ ignore_missing_imports = false

# Third-party libs without type stubs
[[tool.mypy.overrides]]
module = ["lerobot.*", "gr00t.*", "draccus.*", "msgpack.*", "zmq.*", "huggingface_hub.*", "serial.*", "psutil.*", "torch.*", "torchvision.*", "transformers.*", "einops.*", "robot_descriptions.*"]
module = ["lerobot.*", "gr00t.*", "draccus.*", "msgpack.*", "zmq.*", "huggingface_hub.*", "serial.*", "psutil.*", "torch.*", "torchvision.*", "transformers.*", "einops.*", "robot_descriptions.*", "warp.*", "newton.*"]
ignore_missing_imports = true

# @tool decorator injects runtime signatures mypy cannot check
Expand Down
27 changes: 18 additions & 9 deletions strands_robots/simulation/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@
# Explicit backend
sim = create_simulation("mujoco", timestep=0.001)

# Future backends
sim = create_simulation("isaac", gpu_id=0)
sim = create_simulation("newton")
# GPU-native backend
sim = create_simulation("newton", num_envs=4096)

# Custom backend (runtime-registered)
from strands_robots.simulation.factory import register_backend
Expand Down Expand Up @@ -43,15 +42,20 @@
"strands_robots.simulation.mujoco.simulation",
"Simulation",
),
"newton": (
"strands_robots.simulation.newton.simulation",
"NewtonSimulation",
),
# Future:
# "isaac": ("strands_robots.simulation.isaac.simulation", "IsaacSimulation"),
# "newton": ("strands_robots.simulation.newton.simulation", "NewtonSimulation"),
}

_BUILTIN_ALIASES: dict[str, str] = {
"mj": "mujoco",
"mjc": "mujoco",
"mjx": "mujoco",
"warp": "newton",
"wp": "newton",
# "isaac_sim": "isaac",
# "isaacsim": "isaac",
# "nvidia": "isaac",
Expand Down Expand Up @@ -139,7 +143,7 @@ def list_backends() -> list[str]:
Example::

>>> list_backends()
['mj', 'mjc', 'mjx', 'mujoco']
['mj', 'mjc', 'mjx', 'mujoco', 'newton', 'warp', 'wp']
"""
names: set[str] = set()
names.update(_BUILTIN_BACKENDS.keys())
Expand Down Expand Up @@ -201,17 +205,19 @@ def create_simulation(

Args:
backend: Backend name or alias. Defaults to ``"mujoco"``.
Built-in: ``"mujoco"`` (aliases: ``"mj"``, ``"mjc"``, ``"mjx"``).
Built-in: ``"mujoco"`` (aliases: ``"mj"``, ``"mjc"``, ``"mjx"``),
``"newton"`` (aliases: ``"warp"``, ``"wp"``).
**kwargs: Backend-specific keyword arguments passed to the
constructor (e.g., ``tool_name``, ``timestep``).
constructor (e.g., ``tool_name``, ``timestep``,
``num_envs``, ``solver``).

Returns:
A ``SimEngine`` instance ready for ``create_world()``.

Raises:
ValueError: If the backend name is not recognized.
ImportError: If the backend's dependencies are missing
(e.g., ``pip install mujoco``).
(e.g., ``pip install mujoco`` or ``pip install 'strands-robots[newton]'``).

Examples::

Expand All @@ -220,8 +226,11 @@ def create_simulation(
sim.create_world()
sim.add_robot("so100")

# Newton GPU backend
sim = create_simulation("newton", num_envs=4096)

# With alias
sim = create_simulation("mj")
sim = create_simulation("warp")

# Pass kwargs to backend constructor
sim = create_simulation("mujoco", tool_name="my_sim")
Expand Down
47 changes: 47 additions & 0 deletions strands_robots/simulation/newton/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Newton simulation backend — GPU-native physics via NVIDIA Warp + Newton.

Lazy-loading module: importing this package does NOT trigger ``import warp``
or ``import newton``. Heavy dependencies are loaded only when
``NewtonSimulation`` is instantiated or its methods are called.

Usage::

from strands_robots.simulation.newton import NewtonSimulation, NewtonConfig

config = NewtonConfig(num_envs=4096, solver="mujoco", device="cuda:0")
sim = NewtonSimulation(config=config)

Or via the factory::

from strands_robots.simulation import create_simulation
sim = create_simulation("newton", num_envs=4096)
"""

from __future__ import annotations

from typing import Any

# Light import — dataclass, no heavy deps
from strands_robots.simulation.newton.config import NewtonConfig

# Lazy-loaded heavy imports
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
"NewtonSimulation": (
"strands_robots.simulation.newton.simulation",
"NewtonSimulation",
),
}

__all__ = ["NewtonConfig", "NewtonSimulation"]


def __getattr__(name: str) -> Any:
if name in _LAZY_IMPORTS:
import importlib

module_path, attr_name = _LAZY_IMPORTS[name]
module = importlib.import_module(module_path)
value = getattr(module, attr_name)
globals()[name] = value
return value
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
105 changes: 105 additions & 0 deletions strands_robots/simulation/newton/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Newton backend configuration.

Dataclass-only module — no heavy dependencies (no warp, no newton).
Safe to import at module level without triggering GPU initialization.
"""

from __future__ import annotations

from dataclasses import dataclass

# Solver name → Newton solver class name.
# Kept here (not solvers.py) so config validation works without importing warp.
SOLVER_MAP: dict[str, str] = {
"mujoco": "SolverMuJoCo",
"featherstone": "SolverFeatherstone",
"semi_implicit": "SolverSemiImplicit",
"xpbd": "SolverXPBD",
"vbd": "SolverVBD",
"style3d": "SolverStyle3D",
"implicit_mpm": "SolverImplicitMPM",
}

RENDER_BACKENDS: frozenset[str] = frozenset({"opengl", "rerun", "viser", "null", "none"})

BROAD_PHASE_OPTIONS: frozenset[str] = frozenset({"sap", "bvh", "none"})


@dataclass
class NewtonConfig:
"""Configuration for the Newton GPU simulation backend.

Parameters
----------
num_envs : int
Number of parallel environments. GPU backends support 4096+
on a single device.
device : str
Warp device string (``"cuda:0"``, ``"cpu"``).
solver : str
Solver backend name. Must be a key in ``SOLVER_MAP``.
``"mujoco"`` is the default — fastest for rigid-body articulated
systems. ``"xpbd"`` / ``"semi_implicit"`` are alternatives.
``"vbd"`` / ``"style3d"`` are cloth-only. ``"implicit_mpm"`` is
for granular/fluid simulation.
physics_dt : float
Physics timestep in seconds.
substeps : int
Number of substeps per ``step()`` call.
render_backend : str
Rendering backend (``"opengl"``, ``"rerun"``, ``"viser"``,
``"null"``/``"none"``).
enable_cuda_graph : bool
Capture the simulation loop in a CUDA graph for minimal
Python overhead. Requires static graph (no dynamic shapes).
enable_differentiable : bool
Enable gradient tracking for differentiable simulation
via ``wp.Tape``.
broad_phase : str
Broad-phase collision algorithm (``"sap"``, ``"bvh"``, ``"none"``).
soft_contact_margin : float
Soft-contact margin distance.
soft_contact_ke : float
Contact stiffness.
soft_contact_kd : float
Contact damping.
soft_contact_mu : float
Contact friction coefficient.
soft_contact_restitution : float
Contact restitution coefficient.

Raises
------
ValueError
If ``solver``, ``render_backend``, or ``broad_phase`` is invalid,
or if ``physics_dt <= 0`` or ``num_envs < 1``.
"""

num_envs: int = 1
device: str = "cuda:0"
solver: str = "mujoco"
physics_dt: float = 0.005
substeps: int = 1
render_backend: str = "none"
enable_cuda_graph: bool = False
enable_differentiable: bool = False
broad_phase: str = "sap"
soft_contact_margin: float = 0.5
soft_contact_ke: float = 10000.0
soft_contact_kd: float = 10.0
soft_contact_mu: float = 0.5
soft_contact_restitution: float = 0.0

def __post_init__(self) -> None:
if self.solver not in SOLVER_MAP:
raise ValueError(f"Unknown solver {self.solver!r}. Valid options: {sorted(SOLVER_MAP.keys())}")
if self.render_backend not in RENDER_BACKENDS:
raise ValueError(
f"Unknown render_backend {self.render_backend!r}. Valid options: {sorted(RENDER_BACKENDS)}"
)
if self.broad_phase not in BROAD_PHASE_OPTIONS:
raise ValueError(f"Unknown broad_phase {self.broad_phase!r}. Valid options: {sorted(BROAD_PHASE_OPTIONS)}")
if self.physics_dt <= 0:
raise ValueError(f"physics_dt must be positive, got {self.physics_dt}")
if self.num_envs < 1:
raise ValueError(f"num_envs must be >= 1, got {self.num_envs}")
Loading
Loading