How to extend/Augment Newton Sim Physics and integrate with the Issac Sim.
Warning
This code was developed and tested with an experimental build of Isaac Sim. (commit )
A Newton "solver" is anything that subclasses newton.solvers.SolverBase and implements one method — step(state_in, state_out, control, contacts, dt). Newton's stepping loop calls it once per substep; everything else (collision detection, integration, contact resolution) is whatever you choose to do inside step().
The demo solver in this repo — XSolver — illustrates the smallest non-trivial pattern: pre-process the input state, delegate to an existing solver, restore the caller's state. It doubles the linear-X component of every body's external wrench, hands the modified state off to newton.solvers.SolverSemiImplicit, then puts the caller's body_f array pointer back so the operation is idempotent.
SolverBase.step is the only method you must implement. Its signature is fixed:
def step(
self,
state_in: newton.State, # body_q, body_qd, body_f, joint_q, joint_qd, particle_*
state_out: newton.State, # write the new state here
control: newton.Control | None, # joint_f, joint_target_pos, activations…
contacts: newton.Contacts | None, # output of model.collide()
dt: float,
) -> None: ...The optional notify_model_changed(flags: int) hook lets the solver react to inertia/property edits at runtime — its default in SolverBase is a no-op, which is fine for most solvers.
The newton.State arrays a solver typically touches:
| Field | Type | Layout |
|---|---|---|
body_q |
wp.array[wp.transform] |
(tx, ty, tz, qx, qy, qz, qw) per body |
body_qd |
wp.array[wp.spatial_vector] |
(wx, wy, wz, vx, vy, vz) — angular first, linear second |
body_f |
wp.array[wp.spatial_vector] |
(fx, fy, fz, tx, ty, tz) — linear force first, torque second |
joint_q, joint_qd |
wp.array[float] |
generalised joint coords / velocities |
Note that body_qd and body_f use different component orders. XSolver doubles index 0 of body_f because that's fx.
class XSolver(newton.solvers.SolverBase):
def __init__(self, model, delegate=None):
super().__init__(model)
self.delegate = delegate or newton.solvers.SolverSemiImplicit(model)
# 1. Allocate one persistent scratch buffer; reuse it every step.
self._body_f_mod = wp.zeros(
model.body_count, dtype=wp.spatial_vector, device=model.device,
)
def step(self, state_in, state_out, control, contacts, dt):
# 2. Copy the caller's wrench into the scratch buffer.
wp.copy(self._body_f_mod, state_in.body_f)
# 3. Mutate the scratch buffer with a Warp kernel.
wp.launch(_double_linear_x_kernel,
dim=self._body_f_mod.shape[0],
inputs=[self._body_f_mod],
device=self._body_f_mod.device)
# 4. Temporarily swap the pointer, delegate, then restore.
original_f = state_in.body_f
state_in.body_f = self._body_f_mod
try:
self.delegate.step(state_in, state_out, control, contacts, dt)
finally:
state_in.body_f = original_f # leave caller's state untouchedThe Warp kernel itself is just the spatial-vector layout written out long-form:
@wp.kernel
def _double_linear_x_kernel(body_f: wp.array(dtype=wp.spatial_vector)):
tid = wp.tid()
f = body_f[tid]
body_f[tid] = wp.spatial_vector(2.0 * f[0], f[1], f[2], f[3], f[4], f[5])Three properties of this pattern worth borrowing:
- Persistent scratch buffer. Allocating inside
step()would churn GPU memory at the physics rate. Allocate once in__init__, reuse forever. - Pointer swap, not mutation. We never write back into
state_in.body_fitself; we pointstate_in.body_fat our scratch buffer for the delegate call, then put it back. The caller's array is bit-identical before and afterstep(). Callingstep()twice with the same input gives the same output — important if some other code path inadvertently re-runs the solver. - Delegation is optional but powerful. XSolver wraps
SolverSemiImplicit, but you could pre/post-process around any solver — Featherstone, MuJoCo, XPBD — to add force fields, force shaping, custom damping, etc., without re-implementing integration.
A solver only runs if the stage knows to construct it. Isaac Sim's NewtonStage builds the solver via a _get_solver(model, solver_cfg) classmethod; subclass that to recognise your config:
solver_config.pydefinesXSolverConfigwith a singlesolver_type: Literal["xsolver"]discriminator.xewton_stage.py:57-62overrides_get_solver: if the config isXSolverConfig, build aSolverSemiImplicitdelegate and wrap it inXSolver; otherwise fall through to the base implementation.xewton_config.pymakesXSolverConfigthe default solver in the Xewton stage's config.
That's the entire integration on the Newton side — one config dataclass, one classmethod override.
- Newton's FAQ on Multi-physics and Custom solvers
- Newton's Docs on Solvers and BaseSolver class
The extension lives in minsim.physics.xewton/. It is a self-contained Omniverse Kit extension — no build step is required, just point Isaac Sim's extension manager at the folder.
- Clone the repo somewhere stable (the path will be referenced by Isaac Sim):
git clone <repo-url> ~/code/newton_extension
- Launch Isaac Sim (6.0 early-access or later, see warning above).
- Register the extension search path. In Isaac Sim, open
Window → Extensions, click the ⚙ (gear) icon, and add the absolute path to the parent folder containing the extension (i.e. the repo root, notminsim.physics.xewton/itself) to "Extension Search Paths". For the clone above:/home/<you>/code/newton_extension - Enable the extension. Switch to the "Third Party" tab, search for
minsim.physics.xewton(title: Xewton: Amin's Custom Newton Physics), toggle it on, and tick "Autoload" if you want it active on every Isaac Sim launch. - Verify. Open Window → Script Editor and run:
You should see
import minsim.physics.xewton as xewton for name, active in xewton.get_available_physics_engines(verbose=True): print(name, active)
PhysX,Newton, andXewtonlisted, withXewtonactive (the extension auto-switches on startup — seeauto_switch_on_startup = trueinextension.toml).
If you prefer a non-interactive launch, pass these to the Isaac Sim binary directly:
--ext-folder /home/<you>/code/newton_extension
--enable minsim.physics.xewton
scripts/test_xewton_cube.py is a self-contained behavioural test of the doubled-force solver:
- Builds a 1 kg cube on a prismatic X-joint anchored at the origin, gravity off, no collisions in the way.
- Each physics step, injects a constant 5 N in +X via
state_0.body_f. - Auto-plays for ~2 s, then prints the cube's final X position.
Stock Newton would predict x(2s) = ½·5·2² = 10.00 m. The XSolver doubles +X forces, so the cube actually accelerates at 10 m/s² and lands at ~20.01 m — visibly different and easy to confirm. The script prints a ratio line; ≈ 2.0× means the custom solver is active.
To run it: enable the extension as above, then in Window → Script Editor open scripts/test_xewton_cube.py and click Run Script. The script subscribes a physics-step callback, calls omni.timeline.play() itself, stops the timeline after TARGET_T = 2.0 s of simulated time, and prints results. Re-running it in the same session unsubscribes the previous callback first, so the test is repeatable without restarting Isaac Sim.
The pattern is "subclass the upstream Newton extension classes, override the smallest possible surface, register under a new engine name." Six classes are involved, all in minsim.physics.xewton/minsim/physics/xewton/impl/:
| Class | File | Parent | Purpose |
|---|---|---|---|
XSolver |
xsolver.py |
newton.solvers.SolverBase |
The custom solver itself; doubles +X linear forces, delegates to SolverSemiImplicit. |
XSolverConfig |
solver_config.py |
isaacsim.physics.newton.NewtonSolverConfig |
Discriminator dataclass — solver_type = "xsolver". |
XewtonConfig |
xewton_config.py |
isaacsim.physics.newton.NewtonConfig |
Top-level config; defaults solver_cfg to XSolverConfig. |
XewtonStage |
xewton_stage.py |
isaacsim.physics.newton.impl.newton_stage.NewtonStage |
Overrides _get_solver to build XSolver when the config calls for it; also wraps model.collide() for older Newton API compatibility. |
XewtonSimulationRegistry |
xewton_register.py |
isaacsim.physics.newton.impl.register_simulation.NewtonSimulationRegistry |
Registers under the unified omni.physics.core interface as engine name "Xewton", alongside "Newton" and "PhysX". |
XewtonSimExtension |
extension.py |
omni.ext.IExt |
Kit lifecycle entry point — on startup builds XewtonConfig, instantiates XewtonStage, registers it, and optionally switches the active engine. |
The flow at startup is: XewtonSimExtension.on_startup → XewtonConfig() → XewtonStage(cfg=...) → XewtonSimulationRegistry().register_xewton(...) → when the user hits Play, XewtonStage.initialize_newton() calls XewtonStage._get_solver(...) which returns XSolver(model, delegate=SolverSemiImplicit(model)) — and from then on each physics tick calls XSolver.step(...).
The Kit-side surface is just the config/extension.toml: name, deps (isaacsim.physics.newton, omni.physics, usdrt.scenegraph, omni.warp.core, omni.isaac.ml_archive), and a single [[python.module]] block with path = "." so Kit finds minsim/physics/xewton/__init__.py at the repo-relative root rather than in the default config/ directory.
- Needless to say, Isaac 6.0 is early access hence unstable, incomplete, and can have bugs.
- Only a limited set of Newton solvers are integrated. (namely hydro-elastic contact is not integrated)
- Not all of the Newton integration code is focused in the three Kit extensions related to Newton. The approach taken in this repo is limited to depending on those extensions. However, some user-facing features or warnings might need changes to other experimental kit extensions from Isaac Sim for deeper support. (see Simulation Manager and general schema handling)
- Example:
SimulationManagerdoes not recognise third-party engines. When Xewton is active you'll see[Warning] Unknown engine 'xewton', defaulting to PhysXin the console. The cause issimulation_manager.py:405-414, whose_create_physics_scenehardcodesif engine == "physx" / elif engine == "newton"with no extension hook; anything else falls into theelse:branch and gets wrapped in aPhysxScene. The actual physics still runs through the unifiedomni.physics.coreinterface (XSolver's 2× result in the demo proves this), so scripts that talk directly toxewton.acquire_stage()/xewton.acquire_physics_interface()are unaffected. Higher-level wrappers that consume theSimulationManager's scene object (e.g. fabric-backed transforms,isaacsim.core.api.World) may follow PhysX-flavoured code paths — fixing that needs an upstream change to add an engine-registration hook.
- Example:
- Omniverse Kit Developer tools and experience is non-standard and it is hard to work with. All things considered the documentation and examples repo is quite nice and illuminating and help with the starting point, however, having a build and test environment only for the extensions is not easy. The easiest way for me to test my code was doing an end to end test using an Isaac Sim instance and loading the extension to see if the extension can be loaded properly or not. Hopefully NVidia will improve on this in the future.
- AI Coding Agents have a very difficult time to figure out Omniverse and Isaac Sim. Newton on the other hand, is very well structured and AI friendly. Omniverse's history and defacto stack choices not only slows the Human developers but also confuses AI agents a lot.
- Isaac Sim is just slow to start up and the user experience is a bit clunky.
- It seems that Isaac Lab has a different way of integrating Newton into the application. It looks like Isaac Lab is not following Omniverse's best practices and the team is hacking around it and taking a more opinionated (Pythonic?) approach.