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
55 changes: 55 additions & 0 deletions docs/source/api_reference/model_catalogs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,58 @@ Other dynamics families
:maxdepth: 2

rwm

WorldParticle inference
-----------------------

WorldParticle owns frozen networks, strict official checkpoint loading and
sample-ordered features, static boundaries and CSR topology. Inputs and outputs
are world-space particle positions and velocities; external force conditioning
is total acceleration, including gravity. Model-frame conversion uses float32,
while neural inference retains the official BF16 arithmetic. Loading and use
must occur on the caller-owned execution stream.

.. currentmodule:: embodichain_models.dynamics.worldparticle

.. autosummary::

WorldParticleModel
load_model

.. autoclass:: WorldParticleModel
:members: __call__

.. autofunction:: load_model

The implementation module exposes the same frozen model contract.

.. currentmodule:: embodichain_models.dynamics.worldparticle.model

.. autosummary::

WorldParticleModel
load_model

.. autoclass:: WorldParticleModel
:no-index:

.. autofunction:: load_model
:no-index:

The NPZ reader applies task stride before the configured frame slice and then
selects initial position and velocity. Optional reference frames are returned
for evaluation and are never used as future solver inputs.

.. currentmodule:: embodichain_models.dynamics.worldparticle.data

.. autosummary::

load_sample

.. autofunction:: load_sample

The model package imports neither simulation nor examples. Its simulator
Provider belongs to ``embodichain.lab.sim.physics.worldparticle`` and uses
DexSim's native particle inference adapter. Model weights and datasets remain
outside the wheel. The retained ToMe CC-BY-NC-4.0 license requires separate
commercial redistribution review.
21 changes: 21 additions & 0 deletions docs/source/api_reference/public_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,27 @@ embodichain.lab.sim.physics.newton

NewtonPhysicsBackend

embodichain.lab.sim.physics.worldparticle
---------------------------------------

The per-model Provider validates authored particle order, checkpoint timestep,
gravity and supported scene capabilities before allocating frozen weights.
It supports single-world CUDA particle inference and reset, without native
rigid coupling, gradients or CUDA Graph capture. Model-specific boundaries
replace native collision geometry; SimulationManager callers explicitly set
``enable_default_ground=False`` and pass a ``ProviderNewtonSolverCfg`` object.
Built-in and default solver selection remain unchanged.

.. currentmodule:: embodichain.lab.sim.physics.worldparticle

.. autosummary::

WorldParticleProvider

.. autoclass:: WorldParticleProvider
:members: register_custom_attributes, create


embodichain.lab.sim.motion.planners.base_planner
------------------------------------------------

Expand Down
47 changes: 27 additions & 20 deletions embodichain/lab/sim/cfg/physics.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,15 +373,8 @@ def to_dexsim_cfg(
) -> NewtonCfg:
"""Convert this config to ``dexsim.engine.newton_physics.NewtonCfg``."""
from dexsim.engine.newton_physics import (
AutoSolverCfg,
DexUniSolverCfg,
FeatherstoneSolverCfg,
MJWarpSolverCfg,
NewtonCfg,
NewtonCollisionPipelineCfg as DexsimNewtonCollisionPipelineCfg,
SemiImplicitSolverCfg,
VBDSolverCfg,
XPBDSolverCfg,
)

torch_device = (
Expand All @@ -393,20 +386,34 @@ def to_dexsim_cfg(
else str(torch_device)
)

solver_cfg_map: dict[str, type] = {
"auto": AutoSolverCfg,
"dexuni": DexUniSolverCfg,
"mujoco_warp": MJWarpSolverCfg,
"xpbd": XPBDSolverCfg,
"semi_implicit": SemiImplicitSolverCfg,
"featherstone": FeatherstoneSolverCfg,
"vbd": VBDSolverCfg,
}
solver_cfg = _newton_solver_cfg_to_dexsim(
solver_cfg=self.solver_cfg,
solver_cfg_map=solver_cfg_map,
)
if self.solver_cfg is not None and not isinstance(self.solver_cfg, Mapping):
# Explicit SDK configs (including external Providers) already own
# solver selection. Do not resolve unrelated built-in solver APIs.
solver_cfg = _newton_solver_cfg_to_dexsim(self.solver_cfg, {})
else:
from dexsim.engine.newton_physics import (
AutoSolverCfg,
DexUniSolverCfg,
FeatherstoneSolverCfg,
MJWarpSolverCfg,
SemiImplicitSolverCfg,
VBDSolverCfg,
XPBDSolverCfg,
)

solver_cfg_map: dict[str, type] = {
"auto": AutoSolverCfg,
"dexuni": DexUniSolverCfg,
"mujoco_warp": MJWarpSolverCfg,
"xpbd": XPBDSolverCfg,
"semi_implicit": SemiImplicitSolverCfg,
"featherstone": FeatherstoneSolverCfg,
"vbd": VBDSolverCfg,
}
solver_cfg = _newton_solver_cfg_to_dexsim(
solver_cfg=self.solver_cfg,
solver_cfg_map=solver_cfg_map,
)
if self.requires_grad and (
solver_cfg is None or solver_cfg.solver_type != "semi_implicit"
):
Expand Down
122 changes: 122 additions & 0 deletions embodichain/lab/sim/physics/worldparticle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------
"""WorldParticle native inference adapter for DexSim's provider lifecycle."""

from __future__ import annotations
from typing import Any

__all__ = ["WorldParticleProvider"]
import math
from dataclasses import replace

from dexsim.engine.newton_physics.solver_extensions import (
NewtonSolverCapabilities,
NewtonSolverExtension,
)


class WorldParticleProvider:
"""Select one of the four local official WorldParticle networks with explicit static scene inputs.

Options: ``checkpoint`` and ``config`` paths, an ``inputs`` NPZ with raw
sample-ordered raw features/boundaries/CSR topology, and optional
``model_to_world`` rotation. Cloth uses its authored surface; duck
deformation uses an unconnected-particle carrier, not an SPH model.
One CUDA particle world and static network boundaries are supported. This
provider supplies inference and native reset, not functional gradients,
native contact/force reports, dynamic rigid bodies, or CUDA Graph capture.
Boundary points are model inputs, not automatically generated collisions.
"""

capabilities = NewtonSolverCapabilities(
devices=("cuda",),
reset=True,
cuda_graph=False,
reduced_coordinates=False,
external_wrenches=False,
supported_control_fields=(),
runtime_controls=False,
supported_particle_kinds=("fluid",),
)

def register_custom_attributes(self, builder: Any, options: dict[str, Any]) -> None:
"""Declare the authored topology kind before particle materialization."""
from embodichain_models.dynamics.worldparticle._runtime import load_config

cfg = load_config(options.get("config"))
kind = "cloth" if cfg["data"] == "cloth_on_ball" else "fluid"
self.capabilities = replace(
type(self).capabilities, supported_particle_kinds=(kind,)
)

def create(self, context: Any, options: dict[str, Any]) -> NewtonSolverExtension:
"""Validate the scene before allocating network weights on the GPU."""
import numpy as np
import torch
from embodichain_models.dynamics.worldparticle import _runtime
from embodichain_models.dynamics.worldparticle.model import WorldParticleModel

load_assets = _runtime.load_assets

model = context.model
if not context.device.is_cuda:
raise ValueError("WorldParticle requires CUDA")
if model.requires_grad:
raise ValueError(
"WorldParticle currently supports inference only (requires_grad=False)"
)
if (
model.body_count
or model.joint_count
or model.shape_count
or model.world_count > 1
):
raise ValueError(
"WorldParticle supports one particle world without rigid bodies, joints or native collision shapes"
)
if not model.particle_count:
raise ValueError("WorldParticle requires a nonempty fluid particle set")
cfg, arrays, rotation = load_assets(options)
if len(arrays[0]) != model.particle_count:
raise ValueError(
"Input features must match the flattened Newton particle order/count"
)
if not math.isclose(context.dt, cfg["timestep"], rel_tol=1e-7, abs_tol=1e-10):
raise ValueError("Newton substep dt must match the checkpoint timestep")
expected_gravity = rotation @ np.asarray(cfg["gravity"], dtype=np.float32)
gravity = model.gravity.numpy().reshape(-1, 3)
if not np.allclose(gravity, expected_gravity, atol=1e-6):
raise ValueError(
"Newton gravity must equal model_to_world @ checkpoint gravity"
)
inverse_mass = model.particle_inv_mass.numpy()
if not np.isfinite(inverse_mass).all() or np.any(inverse_mass <= 0):
raise ValueError("Initially fixed/massless particles are not supported")
from newton import ParticleFlags

if np.any((model.particle_flags.numpy() & int(ParticleFlags.ACTIVE)) == 0):
raise ValueError("Initially inactive particles are not supported")
from dexsim.engine.newton_physics.particle_inference import (
ParticleInferenceSolver,
)

def create_predictor(device):
inputs = tuple(torch.as_tensor(value, device=device) for value in arrays)
network = _runtime.load_network(cfg, options["checkpoint"], device)
return WorldParticleModel(network, inputs, rotation, cfg)

solver = ParticleInferenceSolver(model, create_predictor, context.dt)
return NewtonSolverExtension(solver, self.capabilities, close=solver.close)
44 changes: 34 additions & 10 deletions embodichain/lab/sim/sim_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,11 @@ def __init__(
startup_summary: Literal["compact", "full", "off"] = "compact",
dexsim_startup_info: bool = False,
scene_node_capacity: int | None = None,
enable_default_ground: bool = True,
) -> None:
self.startup_summary = startup_summary
self.dexsim_startup_info = dexsim_startup_info
self.enable_default_ground = enable_default_ground
self.scene_node_capacity = scene_node_capacity
self.width = width
self.height = height
Expand Down Expand Up @@ -323,6 +325,13 @@ def __init__(
height: int = 1080
"""The height of the simulation window."""

enable_default_ground: bool = True
"""Declare the built-in collision/render plane.

Disable explicitly for solvers whose boundaries are authored model inputs.
Other scene objects and custom boundary visuals remain caller-owned.
"""

headless: bool = False
"""Whether to run without an automatically opened native window.

Expand Down Expand Up @@ -753,7 +762,8 @@ def _initialize_simulation(
if self.has_native_renderer:
self._init_sim_resources()
self.set_default_background()
self._declare_spawn_default_plane()
if sim_config.enable_default_ground:
self._declare_spawn_default_plane()
if self.has_native_renderer:
self.set_default_global_lighting()

Expand Down Expand Up @@ -981,7 +991,7 @@ def asset_uids(self) -> List[str]:
Returns:
List[str]: list of all assets uid.
"""
uid_list = ["default_plane"]
uid_list = ["default_plane"] if self.sim_config.enable_default_ground else []
uid_list.extend(list(self._lights.keys()))
uid_list.extend(list(self._sensors.keys()))
uid_list.extend(list(self._robots.keys()))
Expand Down Expand Up @@ -1735,7 +1745,7 @@ def prepare(self) -> None:
self._env = result.get_arena("default")
self._arenas = [result.get_arena(name) for name in scene.arena_names]
self.__dict__.pop("arena_offsets", None)
if self._default_plane is None:
if self.sim_config.enable_default_ground and self._default_plane is None:
self._bind_default_plane(scene.handles("default_plane")[0])

# Runtime readiness belongs to the SimulationManager. Keep this and
Expand Down Expand Up @@ -2855,14 +2865,28 @@ def add_deformable_object(self, cfg: DeformableObjectCfg) -> DeformableObject:
"Newton deformable particle sets currently require a CUDA device."
)
solver_type = self._active_newton_solver_type
supported_solvers = {"auto", "dexuni", "xpbd", "semi_implicit", "vbd"}
if solver_type not in supported_solvers:
raise NotImplementedError(
f"Newton solver {solver_type!r} does not support deformable "
"particle sets; select one of 'auto', 'dexuni', 'xpbd', "
"'semi_implicit', or 'vbd'."
)
# External Providers declare supported particle kinds at Spawn build;
# DexSim validates those capabilities before materializing this object.
supported_solvers = {
"auto",
"dexuni",
"xpbd",
"semi_implicit",
"vbd",
"provider",
}
physics_cfg = self.sim_config.physics_cfg
if solver_type not in supported_solvers:
from dexsim.engine.newton_physics import RegisteredNewtonSolverCfg

# Registered names retain their entry-point identity rather than
# the direct factory's "provider" type. Spawn validates their
# actual particle capabilities during finalization.
if not isinstance(physics_cfg.solver_cfg, RegisteredNewtonSolverCfg):
raise NotImplementedError(
f"Newton solver {solver_type!r} does not support deformable "
"particle sets; select a particle solver or an external Provider."
)
if isinstance(physics_cfg, NewtonPhysicsCfg) and physics_cfg.requires_grad:
raise NotImplementedError(
"Newton deformable state mutation is unavailable when "
Expand Down
Loading
Loading