Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
41806cd
feat(demo): add Franka volume-map DFSPH pouring demo
kioxli Oct 13, 2026
40f73d8
feat(sim): integrate fixed-count fluids through public scene APIs
yuecideng Oct 13, 2026
c0be5af
feat(gym): replay qualified Task Program trajectories with live fluids
yuecideng Oct 13, 2026
952575b
docs(fluid): record Gym qualification and finalize API entries
yuecideng Oct 13, 2026
d5b93bc
feat(gym): record fluid replay camera views per environment
yuecideng Oct 14, 2026
fa3db6a
feat(sim): generate fluid fills from local volume profiles
yuecideng Oct 14, 2026
fc3f1ba
fix(fluid): render visible water in recorded Gym views
yuecideng Oct 14, 2026
d720fe6
fix(sim): enforce fluid gate for Hybrid process renderers
yuecideng Oct 14, 2026
f1e9e90
Merge main into fluid integration and adapt the pouring demo
yuecideng Oct 14, 2026
d6c7b39
feat(data): host pouring task assets on Hugging Face
yuecideng Oct 14, 2026
26507e1
chore(pr): remove development notes and obsolete asset documentation
yuecideng Oct 14, 2026
0dd4d16
fix(fluid): preserve cleanup and selected replay state
yuecideng Oct 14, 2026
7c17d77
feat(fluid): solve pouring demos and Task Program online
yuecideng Oct 14, 2026
eb6b9aa
refactor(fluid): separate rc1 foundation from pouring tasks
yuecideng Oct 22, 2026
9655ab0
chore(fluid): sync foundation with main
yuecideng Oct 22, 2026
697afb4
fix(fluid): address material, solver and lifecycle review findings
yuecideng Oct 22, 2026
848ca61
refactor(fluid): separate construction from source resolution
yuecideng Oct 22, 2026
c24a6fd
perf(fluid): vectorize render clipping across particle ranges
yuecideng Oct 22, 2026
b440c56
Merge DexSim adaptation and enable multi-world fluid tutorial
yuecideng Oct 27, 2026
cddb815
feat(tutorial): render fluid mesh surface by default
yuecideng Oct 27, 2026
0835fc9
refactor(fluid): route DFSPH through DexUni
yuecideng Oct 27, 2026
c224a59
perf(tutorial): use static fluid container
yuecideng Oct 27, 2026
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
5 changes: 3 additions & 2 deletions agent_context/MAP.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ topics:
NRDCfg, DLSS, NRD, frame_time_delta_ms, explicit physics stepping, arena, DexSim, PhysicsBackendCfg,
DefaultPhysicsCfg, NewtonPhysicsCfg, VisualObject, VisualObjectCfg, VisualMaterial, VisualMaterialCfg, VisualMaterialInst, preview-material, 材质预览, UsdSourceCfg, source_prim_path, add_visual_object, 纯渲染对象, AutoSolverCfg, DexUniSolverCfg, DexUni, runtime control, deformable, particle set,
ArticulationJointKinematics, get_parent_joint_chain, asset_physics_mode, joint_drive_props, enable_gravity,
fixed-cadence playback, quaternion, xyzw]
fixed-cadence playback, FluidObjectCfg, FluidSpawnCfg, FluidObject, SPHSolverCfg, revolved_volume, fill_level, 粒子采样, fluid, 流体, quaternion, xyzw]
paths: [topics/simulation-system/simulation-system.md]
source_of_truth:
- embodichain/lab/sim/motion/__init__.py
Expand All @@ -22,6 +22,7 @@ topics:
- embodichain/cli/_visualization.py
- embodichain/lab/sim/__init__.py
- embodichain/lab/sim/cfg/
- embodichain/compute/geometry/particles.py
- embodichain/lab/sim/physics/
- embodichain/lab/sim/spawn/
- embodichain/lab/sim/material.py
Expand All @@ -37,7 +38,7 @@ topics:
- embodichain/lab/sim/objects/gizmo.py
- embodichain/lab/gym/envs/base_env.py
watch_paths: [embodichain/lab/sim/sim_manager.py, embodichain/lab/sim/cfg/, embodichain/lab/sim/physics/,
embodichain/lab/sim/spawn/, embodichain/lab/sim/material.py, embodichain/lab/sim/_material_presets.py, embodichain/lab/scripts/preview_material.py, tests/lab/scripts/test_preview_material.py, embodichain/lab/sim/objects/, tests/sim/]
embodichain/lab/sim/spawn/, embodichain/lab/sim/material.py, embodichain/lab/sim/_material_presets.py, embodichain/lab/scripts/preview_material.py, tests/lab/scripts/test_preview_material.py, embodichain/lab/sim/objects/, embodichain/compute/geometry/particles.py, tests/compute/test_particles.py, tests/sim/]
related_topics: [env-framework, robot-system, sensor-system, sim-visualization, ik-solvers, motion-planning,
atomic-actions, rl-learning, configclass-pattern, robot-workspace]
status: active
Expand Down
88 changes: 88 additions & 0 deletions agent_context/topics/simulation-system/fluids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Initial-fill fluids

`cfg/fluid.py` owns source, object and render configuration. Fluids use DexUni,
which dispatches DFSPH when the scene contains fluid particles. Optional
numerical settings belong to DexSim `SPHSolverCfg`, nested under
`NewtonPhysicsCfg.solver_cfg.sph_cfg`; omitting them uses native DFSPH defaults.
`SimulationManager.add_fluid_object()` declares through SpawnScene and binds
`objects/fluid.py` in place at `prepare()`.

The fluid declaration owns density and spacing. Default native SPH values for
`rest_density` and `particle_diameter` are placeholders, equally for mappings
and native config objects; non-default solver values must match the fluid.
DexSim derives the effective solver values from the fluid descriptors.

## Runtime requirements

Fluid simulation requires Newton CUDA, a DexSim build exposing
`DexUniSolverCfg.sph_cfg`, no gradients and fixed particle topology.
Multiple environments require a DexSim SPH solver that explicitly advertises
both world isolation and selected-world reset; arena spacing alone does not
establish isolation. The development build
`0.5.1+0b8422cc6f6717058bd15371e61bd35cdbbe3c0c` supports both. The published
`0.5.1rc1` artifact does not provide the DexUni/DFSPH composition and cannot
run this fluid path.

The current native DexUni/DFSPH implementation is a prototype: DFSPH follows
the rigid step with one-way rigid-to-fluid interaction. When rigid bodies are
present, it requires the pure MuJoCo rigid path; static collision shapes do not
require a rigid-body solver. It cannot mix fluids with cloth or soft-body particles.
Online robot pouring, Gym declarations and replay belong to the dependent task
layer; two-way fluid feedback is not implied by DexUni's rigid/soft coupling.

## State and sampling

Fluid state uses world-frame `(instances, particles, 3|6)` Torch snapshots.
Writes use Scene particle batches, invalidate captured graphs and reset
supported solver history. The legacy single-world solver rebaselines optional
SDF boundary pose history. Reset restores the immutable initial state or
transforms a saved local template without stepping the world. Explicit state
writes do not refresh density diagnostics until the next SPH step.

A spawn reference resolves a declared rigid body's initial pose once; it is
not a persistent attachment. Sources include explicit points/NPZ, primitives,
capped Z-axis revolved profiles and closed fill-volume meshes. Profile points
are ordered `(local Z, radius)` pairs. `fill_level` clips the full source Z
extent before sampling; clearance does not inset that free surface. Mesh
sources describe closed cavities, rather than container walls. Point imports
reject volume-only profile/fill-level controls.

Sampling math belongs to `compute/geometry/particles.py`, independent of lab.
The manager samples once at declaration and retains the local template;
binding and reset do not reopen source files. Particle count is fixed after
preparation, and all fluids share density and spacing. Adding, replacing or
removing rigid assets after preparing a particle scene requires a new scene.

`FluidObject(cfg, device)` creates an unregistered facade without sampling or
state allocation. The manager resolves the source recipe and reference pose,
then privately initializes the facade's local particle template, initial
rotation and write-invalidation hook. Spawn receives the same sampled points
and resolved pose. Derived caches never become additional config fields;
binding and reset reuse them while the input config remains a source recipe.

## Rendering and validation

Native fluids require FastRT (`fast-rt`) or OfflineRT (`rt`). Hybrid's Vulkan
primary surface lacks the OptiX liquid geometry, so the manager rejects Hybrid
before sampling or declaration, including an already initialized Hybrid
process. Use `no-render` for physics only; fluid mode `none` requires it.
Display clipping changes only a render copy. Viser consumes detached particle
point clouds. NoRender needs no optical material. Advanced optical parameters
follow the renderer requirements of the [material system](materials.md).

`scripts/tutorials/sim/create_fluid.py` demonstrates procedural box sampling,
five primitive container walls, prepare, step, state reads and reset. Pass
`--num-envs` to use multiple independent worlds on a capable SPH build. The
fixed container uses static SDF collision shapes, so DFSPH can collide with it
without running a rigid-body solver. It needs no robot, task configuration or downloaded assets:

```bash
python scripts/tutorials/sim/create_fluid.py --headless --renderer no-render
```

Focused checks are `tests/compute/test_particles.py`,
`tests/sim/test_fluid_cfg.py`, `tests/sim/objects/test_fluid_object.py` and
`tests/visualization/test_scene_exporter.py`. Serial CUDA lifecycle coverage is
`tests/sim/objects/test_fluid_runtime.py`; `test_fluid_visibility.py` checks
camera pixels after stepping and direct state writes. Tutorial checks remain
syntax/CLI smoke checks rather than tutorial import tests.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ supports full-surface rigid-soft feedback in current DexSim builds. Native
validation owns compatible contact modes and option combinations. Prescribed kinematic trajectories
cannot respond to contact reactions. Forward solver expert options through
`NewtonPhysicsCfg.solver_cfg`; DexSim owns their validation and dispatch.
The current [DFSPH composition](fluids.md) remains one-way even when DexUni's
rigid/soft coupling options are set to two-way.
Articulation gravity compensation and passive damping are separate creation
settings; see [articulation adapters](articulation-adapters.md).
`async_sync`, `rigid_transform_sync_mode`, `capture_acquire_timeout` and
Expand Down
8 changes: 8 additions & 0 deletions agent_context/topics/simulation-system/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ The Newton backend step hook suppresses automatic DexSim publication during
manager-owned updates to avoid publishing twice. The manager honors explicit
`sync_to_renderer=True` even without consumers; `None`/`False` publish only for
consumers. Raw DexSim `World.update()` retains its configured automatic policy.
When fluids are present, publication orders Newton and Torch work before the
native CUDA staging copy and waits for that copy before a camera reads the frame.
After an explicit fluid-state write, it first drains prior staged updates so a
busy native double buffer cannot silently discard the newer state.
Render-thread recording only consumes already-published state and must never
call the blocking physics-to-render bridge. Marker publication while paused uses
`capture_visualization(force=True)` without advancing physics.
Expand Down Expand Up @@ -147,6 +151,10 @@ Configuration tests do not prove GPU/NGX/NRD runtime support, which initializes
on a rendered frame. Read exact renderer and algorithm defaults in the config
source.

Optical settings use `VisualMaterialCfg.parameters` for direct and shape-owned
materials. Resource definitions and their reset baselines are owned by the
[material system](materials.md).

## Startup summaries

`sim/_startup_summary.py` collects read-only rows. Manager engine output follows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ tests need non-symmetric rotations to expose component-order mistakes.
| Preparation, topology changes, reset, cleanup, controls | [Lifecycle](lifecycle.md); manager and `spawn/scene.py` |
| Backend/device selection, source overlays, collision/mass policy | [Physics configuration](physics-configuration.md); `cfg/`, `spawn/descriptors.py` |
| Joint state order, drive lowering, mimic coupling, native adaptation | [Articulation and batch adapters](articulation-adapters.md); `objects/backends/`, `objects/articulation.py` |
| Initial-fill fluid configuration, state and rendering | [Fluids](fluids.md); `cfg/fluid.py`, `objects/fluid.py` |
| Volume/surface state, topology and kinematic nodes | [Deformables](deformables.md); `cfg/deformable.py`, `objects/deformable/` |
| Camera synchronization, native window/offscreen denoising, startup readiness | [Rendering and diagnostics](rendering.md); manager, `cfg/simulation.py`, `_startup_summary.py` |
| Robot definition, control parts, robot presets | [Robot system](../robot-system/robot-system.md) |
Expand Down
41 changes: 41 additions & 0 deletions docs/source/api_reference/public_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ decoding and compiling the constrained Task Program schema surface.
compile_mllm_task_program
decode_mllm_task_program

embodichain.compute.geometry.particles
--------------------------------------

Deterministic lattice sampling for primitive, revolved and closed mesh fill volumes.

.. autosummary::

embodichain.compute.geometry.particles.sample_volume_particles

.. autofunction:: embodichain.compute.geometry.particles.sample_volume_particles
embodichain.data
----------------

Expand Down Expand Up @@ -683,6 +693,37 @@ embodichain.lab.gym.envs.embodied_env
EmbodiedEnvCfg
EmbodiedEnv

embodichain.lab.sim.cfg.fluid
-----------------------------

Fixed-count fluid sources, physical properties and native particle or surface display.

.. autosummary::

embodichain.lab.sim.cfg.fluid.FluidSpawnCfg
embodichain.lab.sim.cfg.fluid.FluidRenderCfg
embodichain.lab.sim.cfg.fluid.FluidObjectCfg

.. autoclass:: embodichain.lab.sim.cfg.fluid.FluidSpawnCfg

.. autoclass:: embodichain.lab.sim.cfg.fluid.FluidRenderCfg

.. autoclass:: embodichain.lab.sim.cfg.fluid.FluidObjectCfg

embodichain.lab.sim.objects.fluid
---------------------------------

Scene-owned fluid state snapshots, explicit writes and initial-state restoration.

.. autosummary::

embodichain.lab.sim.objects.fluid.FluidObject
embodichain.lab.sim.objects.fluid.FluidObjectData

.. autoclass:: embodichain.lab.sim.objects.fluid.FluidObject

.. autoclass:: embodichain.lab.sim.objects.fluid.FluidObjectData

embodichain.lab.sim.scene.expansion.layout
------------------------------------------

Expand Down
157 changes: 157 additions & 0 deletions embodichain/compute/geometry/particles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------

"""Deterministic volume sampling, independent of simulation objects."""

from __future__ import annotations

import math
from typing import Sequence
import numpy as np

__all__ = ["sample_volume_particles"]


def sample_volume_particles(
kind: str,
spacing: float,
*,
size: Sequence[float] = (0.05, 0.05, 0.05),
radius: float = 0.025,
height: float = 0.05,
clearance: float = 0.0,
profile: Sequence[Sequence[float]] | None = None,
fill_level: float = 1.0,
mesh_path: str | None = None,
max_particles: int = 1_000_000,
) -> np.ndarray:
"""Sample a local primitive, revolved profile or closed mesh on a cubic lattice.

Args:
kind: ``box``, ``sphere``, ``cylinder`` (Z axis),
``revolved_volume`` (Z-axis profile), or ``mesh_volume``.
spacing: Positive lattice spacing, metres.
size: Full box dimensions, metres.
radius: Sphere or cylinder radius, metres.
height: Full cylinder height, metres.
clearance: Inward distance from the volume boundary, metres.
profile: For ``revolved_volume``, at least two ``(z, radius)`` pairs
with strictly increasing Z and nonnegative radii, in local metres.
Linear segments define the side wall; both ends are capped.
fill_level: Fraction of the source's full Z extent to fill, in ``(0, 1]``.
The free-surface cutoff is independent of solid-wall clearance.
mesh_path: Closed fill-volume mesh for ``mesh_volume``.
max_particles: Maximum candidate-grid size before allocation.

Returns:
Contiguous float32 local positions of shape ``(N, 3)``.
"""
if not math.isfinite(spacing) or spacing <= 0:
raise ValueError("spacing must be finite and positive.")
if not math.isfinite(clearance) or clearance < 0:
raise ValueError("clearance must be finite and nonnegative.")
if not math.isfinite(fill_level) or not 0 < fill_level <= 1:
raise ValueError("fill_level must be finite and in (0, 1].")
if type(max_particles) is not int or max_particles <= 0:
raise ValueError("max_particles must be a positive integer.")
mesh = None
section = None
if kind == "revolved_volume":
section = np.asarray(profile, dtype=float)
if (
section.ndim != 2
or section.shape[1] != 2
or len(section) < 2
or not np.isfinite(section).all()
or np.any(np.diff(section[:, 0]) <= 0)
or np.any(section[:, 1] < 0)
or not np.any(section[:, 1] > 0)
):
raise ValueError(
"profile requires finite (z, radius) pairs with increasing Z and nonnegative radii."
)
elif profile is not None:
raise ValueError("profile is only valid for revolved_volume.")
if kind == "mesh_volume":
import trimesh

if mesh_path is None:
raise ValueError("mesh_volume requires mesh_path.")
mesh = trimesh.load(mesh_path, force="mesh")
if not mesh.is_watertight or not mesh.is_volume:
raise ValueError("Fluid fill mesh must be a closed, oriented volume.")
lower, upper = mesh.bounds
else:
if mesh_path is not None:
raise ValueError("mesh_path is only valid for mesh_volume.")
if kind == "box":
extent = np.asarray(size, dtype=float)
elif kind == "sphere":
extent = np.array([2 * radius] * 3)
elif kind == "cylinder":
extent = np.array([2 * radius, 2 * radius, height])
elif section is not None:
max_radius = section[:, 1].max()
extent = np.array([2 * max_radius, 2 * max_radius, np.ptp(section[:, 0])])
else:
raise ValueError(f"Unsupported fluid volume {kind!r}.")
if extent.shape != (3,) or not np.isfinite(extent).all() or (extent <= 0).any():
raise ValueError("Volume dimensions must be finite and positive.")
lower, upper = -extent / 2, extent / 2
if section is not None:
lower[2], upper[2] = section[0, 0], section[-1, 0]
surface_z = lower[2] + fill_level * (upper[2] - lower[2])
lower, upper = lower + clearance, upper - clearance
upper[2] = min(upper[2], surface_z)
counts = [max(0, math.ceil(float(v) / spacing)) for v in upper - lower]
if not all(counts):
raise ValueError("clearance leaves an empty fill volume.")
if math.prod(counts) > max_particles:
raise ValueError("Fluid candidate grid exceeds max_particles.")
axes = [lo + (np.arange(n) + 0.5) * spacing for lo, n in zip(lower, counts)]
points = np.stack(np.meshgrid(*axes, indexing="ij"), axis=-1).reshape(-1, 3)
keep = np.all(points < upper, axis=1)
if kind == "sphere":
keep &= np.linalg.norm(points, axis=1) < radius - clearance
elif kind == "cylinder":
keep &= np.linalg.norm(points[:, :2], axis=1) < radius - clearance
elif section is not None:
radial = np.linalg.norm(points[:, :2], axis=1)
keep &= radial < np.interp(points[:, 2], section[:, 0], section[:, 1])
if clearance:
# Rotational symmetry reduces side-wall distance to the meridian
# plane. Include all segments, including steep or concave profiles.
meridian = np.column_stack((points[:, 2], radial))
distance2 = np.full(len(points), np.inf)
for start, end in zip(section[:-1], section[1:]):
segment = end - start
t = np.clip(
(meridian - start) @ segment / np.dot(segment, segment), 0, 1
)
delta = meridian - (start + t[:, None] * segment)
distance2 = np.minimum(distance2, np.einsum("ij,ij->i", delta, delta))
keep &= distance2 >= clearance**2
elif mesh is not None:
import trimesh

keep &= mesh.contains(points)
if clearance:
_, distance, _ = trimesh.proximity.closest_point(mesh, points)
keep &= distance >= clearance
points = points[keep]
if not len(points):
raise ValueError("Fluid volume contains no particles at this spacing.")
return np.ascontiguousarray(points, dtype=np.float32)
1 change: 1 addition & 0 deletions embodichain/lab/sim/_startup_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ def scene_rows(sim: SimulationManager) -> list[Row]:
),
("Scene / env", "Object groups", str(len(sim._rigid_object_groups))),
("Scene / env", "Deformables", str(len(sim._deformable_objects))),
("Scene / env", "Fluids", str(len(getattr(sim, "_fluid_objects", {})))),
("Scene / env", "Sensors", str(len(sim._sensors))),
(
"Scene",
Expand Down
4 changes: 4 additions & 0 deletions embodichain/lab/sim/cfg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
link_attrs_from_dict,
)
from .asset import AssetPhysicsMode, ObjectBaseCfg, _resolve_asset_physics_mode
from .fluid import FluidObjectCfg, FluidSpawnCfg, FluidRenderCfg
from .source import UsdSourceCfg
from .deformable import (
SurfaceElementPropertiesCfg,
Expand Down Expand Up @@ -88,6 +89,9 @@
from .robot import RobotCfg, RobotPresetCfg # noqa: E402

__all__ = [
"FluidObjectCfg",
"FluidSpawnCfg",
"FluidRenderCfg",
"DEFAULT_RENDERER",
"AssetPhysicsMode",
"DenoisingCfg",
Expand Down
Loading