Multi-Backend Architecture#
Isaac Lab 3.0 introduced a multi-backend architecture that enables running simulations with different physics engines (PhysX and Newton) while maintaining a unified API. This page explains how the backend system works and how to extend it.
Overview#
Instead of hard-coding a single physics engine, Isaac Lab uses a factory pattern to dispatch object creation to backend-specific implementations at runtime. When you write:
from isaaclab.assets import Articulation
robot = Articulation(cfg)
The Articulation class is a factory that automatically creates a
Articulation or
Articulation depending on which physics backend
is active. Your code never needs to import backend-specific modules directly.
This pattern applies to all simulation components:
Component |
Core API ( |
PhysX ( |
Newton ( |
|---|---|---|---|
Physics Manager |
|
|
|
Articulation |
|
||
Rigid Object |
|
||
Contact Sensor |
|
|
|
Renderer |
|
|
|
Scene Data Provider |
|
|
|
Cloner |
|
|
|
The Factory Pattern#
All factories inherit from FactoryBase, which uses a
convention-over-configuration approach to locate backend implementations:
The active physics backend is determined by inspecting
SimulationContext.physics_manager.The factory’s module path is used to derive the backend module path by replacing
isaaclabwithisaaclab_{backend}. For example,isaaclab.assets.articulationmaps toisaaclab_physx.assets.articulationorisaaclab_newton.assets.articulation.The backend module is lazily imported and the implementation class is cached in a registry.
User code: Articulation(cfg)
│
▼
FactoryBase.__new__()
│
├─ _get_backend() → "physx" or "newton"
│ (reads SimulationContext.physics_manager)
│
├─ _get_module_name() → "isaaclab_physx.assets.articulation"
│ (convention: isaaclab.X.Y → isaaclab_{backend}.X.Y)
│
├─ importlib.import_module()
│ (lazy load — only on first use)
│
└─ Return backend-specific instance
Custom backend resolution: Some factories override the default resolution. For example, the
Renderer factory selects backends based on the renderer config type
rather than the physics manager, because renderers and physics backends are independent:
class Renderer(FactoryBase, BaseRenderer):
_backend_class_names = {
"physx": "IsaacRtxRenderer",
"newton": "NewtonWarpRenderer",
"ov": "OVRTXRenderer",
}
Similarly, visualizers select backends based on the visualizer_type field in their config,
allowing any visualizer to work with any physics backend.
Backend Selection#
The physics backend is selected via the physics field in
SimulationCfg:
from isaaclab.sim import SimulationCfg
from isaaclab_physx.physics import PhysxCfg
from isaaclab_newton.physics import NewtonCfg, MJWarpSolverCfg
# Use PhysX (default)
sim_cfg = SimulationCfg(physics=PhysxCfg())
# Use Newton with MuJoCo-Warp solver
sim_cfg = SimulationCfg(physics=NewtonCfg(
solver_cfg=MJWarpSolverCfg(),
num_substeps=4,
))
Once the SimulationContext is initialized, all subsequent factory
instantiations automatically use the selected backend.
Multi-Backend Environments with Presets#
Environments can support multiple backends simultaneously using the preset system. Each backend gets its own configuration variant:
from isaaclab.utils import configclass
from isaaclab_tasks.utils import PresetCfg
from isaaclab_physx.physics import PhysxCfg
from isaaclab_newton.physics import NewtonCfg, MJWarpSolverCfg
@configclass
class CartpolePhysicsCfg(PresetCfg):
default: PhysxCfg = PhysxCfg()
physx: PhysxCfg = PhysxCfg()
newton: NewtonCfg = NewtonCfg(
solver_cfg=MJWarpSolverCfg(njmax=5, nconmax=3)
)
@configclass
class CartpoleEnvCfg(ManagerBasedRLEnvCfg):
sim: SimulationCfg = SimulationCfg(physics=CartpolePhysicsCfg())
Users then select a backend at the command line:
# Default (PhysX)
python train.py --task Isaac-Cartpole-v0
# Newton
python train.py --task Isaac-Cartpole-v0 presets=newton
The Physics Manager#
Each backend implements PhysicsManager, the abstract base class
that drives the simulation loop:
class PhysicsManager(ABC):
@classmethod
@abstractmethod
def initialize(cls, sim_context: SimulationContext) -> None: ...
@classmethod
@abstractmethod
def reset(cls, soft: bool = False) -> None: ...
@classmethod
@abstractmethod
def forward(cls) -> None: ...
@classmethod
@abstractmethod
def step(cls) -> None: ...
@classmethod
def close(cls) -> None: ... # concrete; dispatches STOP event
The physics manager also provides a callback system via
PhysicsEvent for cross-backend event handling:
from isaaclab.physics import PhysicsManager, PhysicsEvent
handle = PhysicsManager.register_callback(
callback=my_setup_fn,
event=PhysicsEvent.PHYSICS_READY,
order=0,
name="my_callback",
)
Available events: MODEL_INIT (during scene building), PHYSICS_READY (after physics
initialization), and STOP (on simulation shutdown).
Asset and Sensor Interfaces#
Assets and sensors follow the same pattern. Each has:
A base class in
isaaclabdefining the interface (e.g.,BaseArticulation,BaseContactSensor)A factory class that inherits from both
FactoryBaseand the base classBackend implementations in
isaaclab_physxandisaaclab_newton
The base classes define the public API contract — properties, methods, and data accessors
that all backends must provide. Both backends use wp.array (Warp arrays) as their
primary data type for asset and sensor data.
Data classes follow the same pattern with their own factories (e.g.,
ArticulationData(FactoryBase, BaseArticulationData)).
Adding a New Physics Backend#
To add a new physics backend (e.g., mybackend), create a new extension package following
the established conventions:
1. Package structure:
source/isaaclab_mybackend/
└── isaaclab_mybackend/
├── __init__.py
├── physics/
│ ├── __init__.py # lazy_export()
│ ├── __init__.pyi # public exports
│ ├── mybackend_manager.py
│ └── mybackend_manager_cfg.py
├── assets/
│ ├── articulation/
│ │ ├── __init__.py
│ │ ├── __init__.pyi
│ │ ├── articulation.py
│ │ └── articulation_data.py
│ ├── rigid_object/
│ │ └── ...
│ └── rigid_object_collection/
│ └── ...
├── sensors/
│ ├── contact_sensor/
│ └── ...
├── renderers/
│ └── ...
├── cloner/
│ └── ...
└── scene_data_providers/
└── ...
2. Implement the physics manager:
# isaaclab_mybackend/physics/mybackend_manager.py
from isaaclab.physics import PhysicsManager
class MyBackendManager(PhysicsManager):
@classmethod
def initialize(cls, sim_context):
super().initialize(sim_context)
# Initialize your physics engine
@classmethod
def step(cls):
# Advance simulation by one timestep
@classmethod
def forward(cls):
# Update kinematics without stepping
@classmethod
def reset(cls, soft=False):
if not soft:
cls.dispatch_event(PhysicsEvent.PHYSICS_READY)
# Reset simulation state
@classmethod
def close(cls):
super().close()
# Clean up resources
3. Create the physics config:
# isaaclab_mybackend/physics/mybackend_manager_cfg.py
from isaaclab.physics import PhysicsCfg
from isaaclab.utils import configclass
@configclass
class MyBackendCfg(PhysicsCfg):
class_type = "{DIR}.mybackend_manager:MyBackendManager"
# Backend-specific settings here
4. Implement assets and sensors:
Each asset/sensor must extend the corresponding base class from isaaclab. The class name
must match the factory’s expected name (by convention, the same name as the factory class).
Use lazy_export() in __init__.py files — no manual registration needed.
# isaaclab_mybackend/assets/articulation/articulation.py
from isaaclab.assets.articulation import BaseArticulation
class Articulation(BaseArticulation):
def __init__(self, cfg):
super().__init__(cfg)
# Set up backend-specific simulation structures
5. Module discovery is automatic. The FactoryBase convention maps
isaaclab.assets.articulation to isaaclab_mybackend.assets.articulation based on the
active physics manager name. As long as you follow the package structure above, your backend
classes will be discovered automatically.
Key Design Principles#
Lazy loading: Backend modules are imported only when first instantiated, keeping startup fast and avoiding hard dependencies on unused backends.
Convention over configuration: Module paths follow a strict pattern (
isaaclab.X.Y→isaaclab_{backend}.X.Y), so no manual registration is needed.Independent selection: Physics backend, renderer, and visualizer are selected independently — you can use any combination.
Warp-native data types: Both backends return
wp.arrayfor asset and sensor data. Usewp.to_torch()when interoperating with PyTorch-based code.Zero runtime overhead: Backend selection happens at instantiation time. There are no if-statements or dispatch logic on the hot path.
See Also#
Migrating to Isaac Lab 3.0 — migration guide from Isaac Lab 2.x to the multi-backend architecture
Hydra Configuration System — preset system for multi-backend environment configurations
Newton Physics Integration — Newton physics integration guide
Renderers — renderer backend architecture