From 899f9127a6df3253b090101ef917ec3764e95ae6 Mon Sep 17 00:00:00 2001 From: kioxli <162790769+kioxli@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:30:17 -0400 Subject: [PATCH 1/7] fix(fluid): clear DFSPH warm start after explicit state writes Co-Authored-By: Claude Opus 5.5 --- embodichain/lab/sim/physics/newton.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/embodichain/lab/sim/physics/newton.py b/embodichain/lab/sim/physics/newton.py index 28e17ad5e..01dcb0e3a 100644 --- a/embodichain/lab/sim/physics/newton.py +++ b/embodichain/lab/sim/physics/newton.py @@ -174,6 +174,9 @@ def invalidate_fluid_state(self, env_ids: Sequence[int] | None = None) -> None: boundary = solver.sdf_boundary if boundary is not None and boundary._body_q_prev is not None: boundary._body_q_prev.assign(backend.state_0.body_q) + reset_warm_start = getattr(solver, "reset_dfsph_warm_start", None) + if reset_warm_start is not None: + reset_warm_start() def fluid_diagnostics(self, fluid: FluidObject) -> dict[str, object]: """Read detached native SPH diagnostics without publishing or stepping.""" From 6533337513e4e7300e85caf5a4e4ecf5a4670232 Mon Sep 17 00:00:00 2001 From: kioxli <162790769+kioxli@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:41:42 -0400 Subject: [PATCH 2/7] feat(fluid): prototype DexUni + DFSPH pouring path Allow fluid objects when DexUni carries an sph_cfg, route fluid state invalidation through the backend's SPH solver, and add a pouring demo that swaps the SPH+MuJoCo composition for DexUni followed by DFSPH. Co-Authored-By: Claude Opus 5.5 --- embodichain/lab/sim/physics/newton.py | 2 +- embodichain/lab/sim/sim_manager.py | 23 +- .../tutorials/sim/franka_dexuni_pour_demo.py | 331 ++++++++++++++++++ 3 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 scripts/tutorials/sim/franka_dexuni_pour_demo.py diff --git a/embodichain/lab/sim/physics/newton.py b/embodichain/lab/sim/physics/newton.py index 01dcb0e3a..533c056f9 100644 --- a/embodichain/lab/sim/physics/newton.py +++ b/embodichain/lab/sim/physics/newton.py @@ -156,7 +156,7 @@ def invalidate_fluid_state(self, env_ids: Sequence[int] | None = None) -> None: from dexsim.engine.newton_physics.backend_registry import get_newton_backend backend = get_newton_backend(self._manager._world) - if backend is None or backend.solver_type != "sph": + if backend is None or getattr(backend, "sph_solver", None) is None: raise RuntimeError("Prepared SPH backend required.") backend.invalidate_cuda_graph("fluid state write") solver = getattr(backend, "sph_solver", backend.solver) diff --git a/embodichain/lab/sim/sim_manager.py b/embodichain/lab/sim/sim_manager.py index 5fa1b3fed..a4e57446a 100644 --- a/embodichain/lab/sim/sim_manager.py +++ b/embodichain/lab/sim/sim_manager.py @@ -941,6 +941,23 @@ def _active_newton_solver_type(self) -> str | None: """Return the active backend's resolved solver type, when available.""" return self.physics.solver_type if self.is_newton_backend else None + @property + def _newton_solves_fluids(self) -> bool: + """Whether the Newton solver advances fluid particles with DFSPH. + + Standalone SPH does; the DexUni prototype does when it carries an + ``sph_cfg`` that steps particles after the rigid/articulated solve. + """ + solver_type = self._active_newton_solver_type + if solver_type == "sph": + return True + if solver_type != "dexuni": + return False + solver_cfg = self.sim_config.physics_cfg.solver_cfg + if isinstance(solver_cfg, Mapping): + return solver_cfg.get("sph_cfg") is not None + return getattr(solver_cfg, "sph_cfg", None) is not None + @property def newton_manager(self): """Compatibility accessor for the removed NewtonManager API. @@ -2757,10 +2774,12 @@ def add_fluid_object(self, cfg: FluidObjectCfg) -> FluidObject: "Batch fluids require a DexSim SPH build with world isolation and selected-world reset." ) if ( - self._active_newton_solver_type != "sph" + not self._newton_solves_fluids or self.sim_config.physics_cfg.requires_grad ): - raise NotImplementedError("Fluid objects require non-differentiable SPH.") + raise NotImplementedError( + "Fluid objects require non-differentiable SPH or DexUni with sph_cfg." + ) if self.spawn_result is not None: raise NotImplementedError("Declare fluids before prepare().") if not isinstance(cfg, FluidObjectCfg): diff --git a/scripts/tutorials/sim/franka_dexuni_pour_demo.py b/scripts/tutorials/sim/franka_dexuni_pour_demo.py new file mode 100644 index 000000000..e81acbbc0 --- /dev/null +++ b/scripts/tutorials/sim/franka_dexuni_pour_demo.py @@ -0,0 +1,331 @@ +# ---------------------------------------------------------------------------- +# 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. +# ---------------------------------------------------------------------------- +"""Prototype: live Franka grasp/pour/place solved by DexUni, then DFSPH. + +Same scene, planning and qualification as ``franka_dfsph_pour_demo.py``. The +physics solver is DexUni (MuJoCo articulation + VBD rigid cup) on a +particle-free view; one DFSPH step then advances the water against the live +body boundaries. Coupling is one-way. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import time + +import imageio.v2 as imageio +import numpy as np +import torch + +from embodichain.lab.scripts.run_fluid_task_program import ( + _configure, + _cup_qualification, +) +from embodichain.lab.sim import SimulationManager +from embodichain.lab.sim.atomic_actions import ( + AxisAlignAffordance, + ControlPartCommandProfile, + GraspGoal, + HeldObjectPoseGoal, + MoveHeldObjectOptions, + MotionPolicy, + ObjectSemantics, + PickUpOptions, + PlaceGoal, + PlaceOptions, + PourGoal, + PourOptions, + create_simulation_atomic_action_engine, +) +from embodichain.lab.sim.motion.motion_generator import MotionGenerator, MotionGenCfg +from embodichain.lab.sim.motion.planners import ToppraPlannerCfg +from embodichain.lab.sim.sensors import CameraCfg + +__all__ = ["main"] + +ROOT = Path(__file__).resolve().parents[3] +TASK = ( + ROOT + / "embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/task.franka.fluid.yaml" +) + + +def _use_dexuni(cfg, args) -> None: + """Replace the SPH+MuJoCo composition with DexUni followed by DFSPH.""" + solver = cfg.sim_cfg.physics_cfg.solver_cfg + sph = { + k: v + for k, v in solver.items() + if k not in ("rigid_solver_cfg", "rigid_solver_substeps") + } + cfg.sim_cfg.physics_cfg.solver_cfg = { + "solver_type": "dexuni", + "joint_mode": "dynamic", + "contact_mode": "auto", + "coupling": args.coupling, + "vbd_options": {"iterations": args.vbd_iterations}, + "sph_cfg": sph, + } + + +def main() -> None: + """Plan fresh joint commands and solve the robot, cup and fluid online.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--output", type=Path, default=ROOT / "outputs/fluid-dexuni-online" + ) + parser.add_argument("--spacing", type=float, default=0.0015) + parser.add_argument("--seconds", type=float) + parser.add_argument("--num-envs", type=int, default=1) + parser.add_argument( + "--renderer", choices=["fast-rt", "no-render"], default="fast-rt" + ) + parser.add_argument("--headless", action="store_true") + parser.add_argument("--vbd-iterations", type=int, default=12) + parser.add_argument("--coupling", choices=["one_way", "two_way"], default="one_way") + args = parser.parse_args() + if args.seconds is not None and ( + not np.isfinite(args.seconds) or args.seconds <= 0 + ): + parser.error("seconds must be finite and positive") + output = args.output.resolve() + output.mkdir(parents=True, exist_ok=True) + _, cfg = _configure( + TASK, + num_envs=args.num_envs, + renderer=args.renderer, + output=output, + particle_spacing=args.spacing, + ) + _use_dexuni(cfg, args) + cfg.sim_cfg.num_envs = cfg.num_envs + sim = SimulationManager(cfg.sim_cfg) + writers = [] + robot = cup = camera = engine = None + try: + robot = sim.add_robot(cfg.robot) + for object_cfg in [*cfg.background, *cfg.rigid_object]: + sim.add_rigid_object(object_cfg) + for fluid_cfg in cfg.fluid_object: + sim.add_fluid_object(fluid_cfg) + for light_cfg in cfg.light.direct: + sim.add_light(light_cfg) + sim.prepare() + robot.build_pk_serial_chain() + sim.reset_objects_state() + cup = sim.get_rigid_object("cup") + # Live gravity/contact settling before observing the planning scene. + sim.update(step=24, render_final_step=False) + initial_pose = cup.get_local_pose(to_matrix=True).clone() + fill_pose = cup.get_local_pose().clone() + fill_pose[:, :3] += sim.arena_offsets + sim.get_fluid_object("water").reset_to_pose(fill_pose) + object_to_eef = torch.tensor( + [[0, 0, -1, 0.065], [0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1]], + dtype=torch.float32, + device=sim.device, + ) + semantics = ObjectSemantics( + entity_id="cup", + label="cup", + geometry={}, + affordance=AxisAlignAffordance( + mesh_vertices=cup.get_vertices(env_ids=[0], scale=True)[0], + mesh_triangles=cup.get_triangles(env_ids=[0])[0], + internal_axis=torch.tensor([1.0, 0, 0], device=sim.device), + ), + ) + motion = MotionGenerator( + MotionGenCfg( + planner_cfg=ToppraPlannerCfg( + robot_uid=robot.uid, sim_instance_id=sim.instance_id + ) + ) + ) + engine = create_simulation_atomic_action_engine( + motion, + (cup,), + control_profiles={ + "eef": ControlPartCommandProfile.joint_positions( + open=torch.tensor([0.04, 0.04], device=sim.device), + grasp=torch.zeros(2, device=sim.device), + ) + }, + ) + bindings = {"primary": {"motion": "arm", "grasp": "eef"}} + policy = MotionPolicy( + strategy="ik_interp", sample_count=360, dynamic_collision_mode="off" + ) + receiver_pose = sim.get_rigid_object("receiver").get_local_pose(to_matrix=True) + receiver_to_cup = torch.eye(4, device=sim.device) + receiver_to_cup[:3, 3] = torch.tensor([0.005, 0.09, 0.17], device=sim.device) + pour_pose = receiver_pose @ receiver_to_cup + invocations = ( + engine.make_invocation( + "pick_up", + GraspGoal(semantics), + control_parts=bindings, + motion_policy=policy, + skill_options=PickUpOptions( + fixed_object_to_eef=object_to_eef, + approach_direction=torch.tensor([-1.0, 0, 0], device=sim.device), + pre_grasp_distance=0.08, + lift_height=0.12, + hand_interp_steps=20, + grasp_settle_steps=20, + ), + ), + engine.make_invocation( + "move_held_object", + HeldObjectPoseGoal(pour_pose), + control_parts=bindings, + motion_policy=policy, + skill_options=MoveHeldObjectOptions(), + ), + engine.make_invocation( + "pour", + PourGoal(), + control_parts=bindings, + motion_policy=policy, + skill_options=PourOptions(rotate_angle=np.deg2rad(80)), + ), + engine.make_invocation( + "place", + PlaceGoal(initial_pose @ object_to_eef), + control_parts=bindings, + motion_policy=policy, + skill_options=PlaceOptions( + hand_interp_steps=20, release_settle_steps=20 + ), + ), + ) + control_dt = cfg.sim_steps_per_control * sim.sim_config.physics_dt + plan = engine.compile( + invocations, engine.initial_context(control_dt=control_dt) + ) + if not bool(plan.plan_success.all()): + raise RuntimeError("Atomic Action planning failed.") + if args.renderer != "no-render": + params = cfg.events.record_camera.params + camera = sim.add_sensor( + CameraCfg( + uid="fluid_sim_record", + width=params["resolution"][0], + height=params["resolution"][1], + intrinsics=params["intrinsics"], + visualization_role="record", + extrinsics=CameraCfg.ExtrinsicsCfg( + eye=params["eye"], target=params["target"], up=params["up"] + ), + ) + ) + writers = [ + imageio.get_writer( + output / f"fluid_sim_env{i}.mp4", + fps=1 / control_dt, + codec="libx264", + quality=8, + macro_block_size=1, + ) + for i in range(sim.num_envs) + ] + if not args.headless: + sim.open_window() + positions = plan.trajectory.positions + poses = [cup.get_local_pose().cpu().numpy().copy()] + samples = [] + started = time.perf_counter() + count = positions.shape[1] + round(2 / control_dt) + if args.seconds is not None: + count = min(count, int(np.ceil(args.seconds / control_dt))) + for i in range(count): + # Set targets only. Every measured joint/cup pose comes from physics. + robot.set_qpos(positions[:, min(i, positions.shape[1] - 1)], target=True) + sim.update(step=cfg.sim_steps_per_control, render_final_step=False) + poses.append(cup.get_local_pose().cpu().numpy().copy()) + if camera is not None: + with sim.render_frame(): + sim.sync_render_state() + camera.update() + rgb = camera.get_data()["color"][..., :3].cpu().numpy() + for env_id, writer in enumerate(writers): + writer.append_data(rgb[env_id]) + if i % round(1 / control_dt) == 0 or i == count - 1: + fluid = sim.get_fluid_object("water") + state = fluid.data.particle_state_w + if not bool(torch.isfinite(state).all()): + raise RuntimeError("Non-finite online fluid state.") + local = state[..., :3] - sim.arena_offsets[:, None, :] + receiver_pose = sim.get_rigid_object("receiver").get_local_pose( + to_matrix=True + ) + receiver = torch.bmm( + local - receiver_pose[:, None, :3, 3], receiver_pose[:, :3, :3] + ) + received = ( + (receiver[..., 2] > 0.0045) + & (receiver[..., 2] < 0.0675) + & ( + torch.linalg.vector_norm( + receiver - torch.tensor([0, 0, 0.088], device=sim.device), + dim=-1, + ) + < 0.093 + ) + ) + sample = { + "time_s": (i + 1) * control_dt, + "cup_pose": poses[-1].tolist(), + "received_ml": (received.sum(dim=1) * args.spacing**3 * 1e6) + .cpu() + .tolist(), + } + samples.append(sample) + print(json.dumps(sample), flush=True) + report = { + "coupling": "online one-way rigid-to-fluid", + "controller": "fresh Atomic Action joint targets", + "cup": _cup_qualification(poses), + "num_envs": sim.num_envs, + "particle_count_per_env": sim.get_fluid_object("water").num_particles, + "control_dt": control_dt, + "simulated_seconds": count * control_dt, + "wall_seconds": time.perf_counter() - started, + "samples": samples, + } + report["accepted"] = bool( + all(report["cup"]["accepted"]) + and all(v >= 50 for v in samples[-1]["received_ml"]) + ) + (output / "validation.json").write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(report["cup"]), flush=True) + if args.seconds is None and not report["accepted"]: + raise RuntimeError( + "Online Sim pouring failed physical qualification; inspect validation.json." + ) + finally: + for writer in writers: + writer.close() + robot = cup = camera = engine = None + sim.destroy(exit_process=False) + del sim + SimulationManager.flush_cleanup_queue() + + +if __name__ == "__main__": + main() From 59b866eca752bf48dff9b4f9b7da8029bcffbe43 Mon Sep 17 00:00:00 2001 From: kioxli <162790769+kioxli@users.noreply.github.com> Date: Fri, 16 Oct 2026 03:10:27 -0400 Subject: [PATCH 3/7] feat(fluid): solve the DexUni pouring prototype with MuJoCo and DFSPH only Every joint, the cup's free joint included, is MuJoCo-owned, so DexUni runs its pure-MuJoCo backend and VBD is skipped. DFSPH steps at 480 Hz on a second CUDA stream while MuJoCo takes 15 substeps per DFSPH step (7,200 Hz). A stiffer cup contact offsets the coarser MuJoCo step's frictional creep, so the pour matches the 10,080 Hz MuJoCo result 1.4x faster. Co-Authored-By: Claude Opus 5.5 --- .../tutorials/sim/franka_dexuni_pour_demo.py | 67 ++++++++++++++++--- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/scripts/tutorials/sim/franka_dexuni_pour_demo.py b/scripts/tutorials/sim/franka_dexuni_pour_demo.py index e81acbbc0..2380e1fdf 100644 --- a/scripts/tutorials/sim/franka_dexuni_pour_demo.py +++ b/scripts/tutorials/sim/franka_dexuni_pour_demo.py @@ -16,9 +16,17 @@ """Prototype: live Franka grasp/pour/place solved by DexUni, then DFSPH. Same scene, planning and qualification as ``franka_dfsph_pour_demo.py``. The -physics solver is DexUni (MuJoCo articulation + VBD rigid cup) on a -particle-free view; one DFSPH step then advances the water against the live -body boundaries. Coupling is one-way. +physics solver is DexUni on a particle-free view: every joint, the cup's free +joint included, is MuJoCo-owned, so DexUni runs its pure-MuJoCo backend and +VBD is skipped. One DFSPH step advances the water against the live body +boundaries, overlapping the MuJoCo substeps on a second CUDA stream. Coupling +is one-way. + +Defaults: DFSPH at 480 Hz (two steps per 1/240 s physics step) with 15 MuJoCo +substeps each (7,200 Hz). A stiffer cup contact (``ke`` x4, ``kd`` x2) offsets +the extra frictional creep of the coarser MuJoCo step, so the pour matches +the PR's 10,080 Hz MuJoCo result. Requires a DexSim build with the DexUni +DFSPH prototype options (``DexUniSolverCfg.sph_cfg``). """ from __future__ import annotations @@ -65,22 +73,51 @@ ) +# The PR's MuJoCo rigid solver settings DexUni accepts. +_DEXUNI_MUJOCO_OPTIONS = ( + "solver", + "integrator", + "iterations", + "ls_iterations", + "cone", + "impratio", +) +# Cup contact stiffness/damping (env.fluid.yaml: 4e5 / 1265): timeconst +# 0.79 ms at damping ratio 1, against frictional creep at 7,200 Hz. +_CUP_CONTACT_KE = 1.6e6 +_CUP_CONTACT_KD = 2530.0 + + def _use_dexuni(cfg, args) -> None: """Replace the SPH+MuJoCo composition with DexUni followed by DFSPH.""" - solver = cfg.sim_cfg.physics_cfg.solver_cfg + physics = cfg.sim_cfg.physics_cfg + solver = physics.solver_cfg + rigid = solver.get("rigid_solver_cfg") or {} sph = { k: v for k, v in solver.items() if k not in ("rigid_solver_cfg", "rigid_solver_substeps") } - cfg.sim_cfg.physics_cfg.solver_cfg = { + mujoco_options = {k: rigid[k] for k in _DEXUNI_MUJOCO_OPTIONS if k in rigid} + # One contact per geom pair and 10 GJK/EPA iterations leave the pour + # unchanged; multi-point CCD over the cup's convex pieces is not needed. + mujoco_options["enable_multiccd"] = False + mujoco_options["ccd_iterations"] = 10 + physics.num_substeps = args.fluid_substeps + physics.solver_cfg = { "solver_type": "dexuni", "joint_mode": "dynamic", "contact_mode": "auto", - "coupling": args.coupling, - "vbd_options": {"iterations": args.vbd_iterations}, + "mujoco_options": mujoco_options, + "sph_rigid_substeps": args.rigid_substeps, + # Body accelerations are unused, so skip MuJoCo's post-constraint RNE. + "sph_rne_mode": "disabled", + "sph_concurrent": True, "sph_cfg": sph, } + cup = next(o for o in cfg.rigid_object if o.uid == "cup") + cup.attrs.material_props.ke = _CUP_CONTACT_KE + cup.attrs.material_props.kd = _CUP_CONTACT_KD def main() -> None: @@ -96,9 +133,21 @@ def main() -> None: "--renderer", choices=["fast-rt", "no-render"], default="fast-rt" ) parser.add_argument("--headless", action="store_true") - parser.add_argument("--vbd-iterations", type=int, default=12) - parser.add_argument("--coupling", choices=["one_way", "two_way"], default="one_way") + parser.add_argument( + "--fluid-substeps", + type=int, + default=2, + help="DFSPH steps per 1/240 s physics step (the PR uses 6)", + ) + parser.add_argument( + "--rigid-substeps", + type=int, + default=15, + help="MuJoCo substeps per DFSPH step (the PR uses 7 at 6 DFSPH steps)", + ) args = parser.parse_args() + if args.fluid_substeps < 1 or args.rigid_substeps < 1: + parser.error("substep counts must be positive") if args.seconds is not None and ( not np.isfinite(args.seconds) or args.seconds <= 0 ): From 8e88cd07aebde600c3970b582da7e288bf8ba8ec Mon Sep 17 00:00:00 2001 From: yuecideng Date: Wed, 28 Oct 2026 13:02:37 +0800 Subject: [PATCH 4/7] perf(task): shorten fluid pouring trajectory --- .../trajectory_open_loop_fluid_pour.yaml | 30 +++++++++++++++++++ .../manipulation/tableware/pour_cup/README.md | 5 ++++ .../tableware/pour_cup/task.franka.fluid.yaml | 2 +- tests/test_task_program_package_data.py | 11 +++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 extensions/embodichain_tasks/configs/components/execution_policies/trajectory_open_loop_fluid_pour.yaml diff --git a/extensions/embodichain_tasks/configs/components/execution_policies/trajectory_open_loop_fluid_pour.yaml b/extensions/embodichain_tasks/configs/components/execution_policies/trajectory_open_loop_fluid_pour.yaml new file mode 100644 index 000000000..f3b54e5ea --- /dev/null +++ b/extensions/embodichain_tasks/configs/components/execution_policies/trajectory_open_loop_fluid_pour.yaml @@ -0,0 +1,30 @@ +policy_id: trajectory_open_loop_fluid_pour_v1 +preset_id: trajectory + +requires: + embodiment_contract: single_arm_parallel_gripper + +motion: + strategy: ik_interp + # This is a minimum; interpolation adds samples to respect robot limits. + sample_count: 160 + dynamic_collision_mode: "off" + +tracking: + kind: timed + settle_duration: 0.4 + +recovery: + max_replans: 0 + max_action_retries: 0 + goal_rotation_threshold: 3.141592653589793 + +workflow_recovery: + max_recovery_attempts: 0 + +runner: + minimum_cycle_time: 0.0 + hold_on_completion: true + hold_during_effect_verification: false + +effect_assurance: projected diff --git a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md index d8e043335..7db632a99 100644 --- a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md +++ b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md @@ -22,6 +22,11 @@ python -m embodichain.lab.scripts.run_fluid_task_program \ --num-envs 1 --renderer fast-rt --output outputs/fluid-task-program-online ``` +任务的执行节奏由 +[`trajectory_open_loop_fluid_pour.yaml`](../../../../components/execution_policies/trajectory_open_loop_fluid_pour.yaml) +配置。轨迹采样数是下限;规划器会根据关节速度和加速度约束增加采样点,并保留 +抓取及放回时的稳定等待。 + 也可使用普通任务入口: ```bash diff --git a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/task.franka.fluid.yaml b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/task.franka.fluid.yaml index 2c159d783..83b655949 100644 --- a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/task.franka.fluid.yaml +++ b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/task.franka.fluid.yaml @@ -6,7 +6,7 @@ environment: task_program: program: task_program/program.fluid.yaml integration: task_program/integration.fluid.yaml - execution_policy: ../../../../components/execution_policies/trajectory_open_loop_gentle.yaml + execution_policy: ../../../../components/execution_policies/trajectory_open_loop_fluid_pour.yaml embodiment: component: ../../../../components/embodiments/franka_panda_firm_grip_newton.yaml overrides: diff --git a/tests/test_task_program_package_data.py b/tests/test_task_program_package_data.py index 0ba7f5206..78b24cd9f 100644 --- a/tests/test_task_program_package_data.py +++ b/tests/test_task_program_package_data.py @@ -117,6 +117,11 @@ "task_program_pour_water", "cobotmagic", ), + Path("tasks/manipulation/tableware/pour_cup/task.franka.fluid.yaml"): ( + "pick_aim_pour_return_cup", + "task_program_pour_cup_fluid", + "franka_panda_firm_grip_newton", + ), } _DUAL_ARM_VARIANTS = ( "dual_ur5_dh_pgi_140_80", @@ -185,6 +190,12 @@ { Path("tasks/manipulation/tableware/pour_cup/task.franka.fluid.yaml"), Path("tasks/manipulation/tableware/pour_cup/env.fluid.yaml"), + Path("tasks/manipulation/tableware/pour_cup/task_program/program.fluid.yaml"), + Path( + "tasks/manipulation/tableware/pour_cup/task_program/integration.fluid.yaml" + ), + Path("components/embodiments/franka_panda_firm_grip_newton.yaml"), + Path("components/execution_policies/trajectory_open_loop_fluid_pour.yaml"), *_DUAL_ARM_RESOURCES, *_PROGRAMS, *_DEPLOYMENTS, From 4a7be2702776bfeba88fa272e2e5b20ddfbe7273 Mon Sep 17 00:00:00 2001 From: yuecideng Date: Wed, 28 Oct 2026 13:43:03 +0800 Subject: [PATCH 5/7] refactor(fluid): keep DexUni pouring demo --- .../manipulation/tableware/pour_cup/README.md | 3 + .../tutorials/sim/franka_dexuni_pour_demo.py | 15 +- .../tutorials/sim/franka_dfsph_pour_demo.py | 310 ------------------ 3 files changed, 9 insertions(+), 319 deletions(-) delete mode 100644 scripts/tutorials/sim/franka_dfsph_pour_demo.py diff --git a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md index 7db632a99..9a0d9150a 100644 --- a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md +++ b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md @@ -42,6 +42,9 @@ embodichain run-task \ python scripts/tutorials/sim/franka_dexuni_pour_demo.py --headless --renderer fast-rt ``` +增加 `--num-envs 4` 可在同一个 World 中并行运行四个环境,分别输出 +`fluid_sim_env0.mp4` 至 `fluid_sim_env3.mp4`。 + 默认粒子间距为 1.5 mm,初始填充 76,607 粒。Gym 的 `--particle-spacing 0.003` 或 Sim 的 `--spacing 0.003` 可用于较快的物理检查,但会改变流体分辨率。 `--seconds 2` 只运行起始片段,不代表完整任务验收。NoRender 仅用于物理检查。 diff --git a/scripts/tutorials/sim/franka_dexuni_pour_demo.py b/scripts/tutorials/sim/franka_dexuni_pour_demo.py index 8a1bc06b7..f3665eb35 100644 --- a/scripts/tutorials/sim/franka_dexuni_pour_demo.py +++ b/scripts/tutorials/sim/franka_dexuni_pour_demo.py @@ -13,13 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # ---------------------------------------------------------------------------- -"""Prototype: live Franka grasp/pour/place solved by DexUni, then DFSPH. +"""Drive a live Franka grasp/pour/place with DexUni and one-way DFSPH. -Same scene, planning and qualification as ``franka_dfsph_pour_demo.py``. The -physics solver is DexUni on a particle-free view: every joint, the cup's free -joint included, is MuJoCo-owned, so DexUni runs its pure-MuJoCo backend and -VBD is skipped. One DFSPH step advances the water against the live body -boundaries after the MuJoCo substeps. Coupling is one-way. +DexUni advances the MuJoCo-owned robot and cup and then solves the water +against their live boundaries. Coupling is one-way. Defaults: DFSPH at 480 Hz (two steps per 1/240 s physics step) with 15 MuJoCo substeps each (7,200 Hz). The task config supplies the cup contact settings. @@ -96,13 +93,13 @@ def main() -> None: "--fluid-substeps", type=int, default=2, - help="DFSPH steps per 1/240 s physics step (the PR uses 6)", + help="DFSPH steps per 1/240 s physics step", ) parser.add_argument( "--rigid-substeps", type=int, default=15, - help="MuJoCo substeps per DFSPH step (the PR uses 7 at 6 DFSPH steps)", + help="MuJoCo substeps per DFSPH step", ) args = parser.parse_args() if args.fluid_substeps < 1 or args.rigid_substeps < 1: @@ -178,7 +175,7 @@ def main() -> None: ) bindings = {"primary": {"motion": "arm", "grasp": "eef"}} policy = MotionPolicy( - strategy="ik_interp", sample_count=360, dynamic_collision_mode="off" + strategy="ik_interp", sample_count=160, dynamic_collision_mode="off" ) receiver_pose = sim.get_rigid_object("receiver").get_local_pose(to_matrix=True) receiver_to_cup = torch.eye(4, device=sim.device) diff --git a/scripts/tutorials/sim/franka_dfsph_pour_demo.py b/scripts/tutorials/sim/franka_dfsph_pour_demo.py deleted file mode 100644 index 3921cea18..000000000 --- a/scripts/tutorials/sim/franka_dfsph_pour_demo.py +++ /dev/null @@ -1,310 +0,0 @@ -# ---------------------------------------------------------------------------- -# 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. -# ---------------------------------------------------------------------------- -"""Drive a live Franka grasp/pour/place with DexUni and one-way DFSPH. - -Atomic Actions plan controller targets from the current scene. MuJoCo solves -joint drives, gravity and grasp contact; DFSPH reads the live cup boundary. -The Gym counterpart uses a configured Task Program for the same physical scene. -""" - -from __future__ import annotations - -import argparse -import json -from pathlib import Path -import time - -import imageio.v2 as imageio -import numpy as np -import torch - -from embodichain.lab.scripts.run_fluid_task_program import ( - _configure, - _cup_qualification, -) -from embodichain.lab.sim import SimulationManager -from embodichain.lab.sim.atomic_actions import ( - AxisAlignAffordance, - ControlPartCommandProfile, - GraspGoal, - HeldObjectPoseGoal, - MoveHeldObjectOptions, - MotionPolicy, - ObjectSemantics, - PickUpOptions, - PlaceGoal, - PlaceOptions, - PourGoal, - PourOptions, - create_simulation_atomic_action_engine, -) -from embodichain.lab.sim.motion.motion_generator import MotionGenerator, MotionGenCfg -from embodichain.lab.sim.motion.planners import ToppraPlannerCfg -from embodichain.lab.sim.sensors import CameraCfg - -__all__ = ["main"] - -ROOT = Path(__file__).resolve().parents[3] -TASK = ( - ROOT - / "extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/task.franka.fluid.yaml" -) - - -def main() -> None: - """Plan fresh joint commands and solve the robot, cup and fluid online.""" - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--output", type=Path, default=ROOT / "outputs/fluid-sim-online" - ) - parser.add_argument("--spacing", type=float, default=0.0015) - parser.add_argument("--seconds", type=float) - parser.add_argument("--num-envs", type=int, default=1) - parser.add_argument( - "--renderer", choices=["fast-rt", "no-render"], default="fast-rt" - ) - parser.add_argument("--headless", action="store_true") - args = parser.parse_args() - if args.seconds is not None and ( - not np.isfinite(args.seconds) or args.seconds <= 0 - ): - parser.error("seconds must be finite and positive") - output = args.output.resolve() - output.mkdir(parents=True, exist_ok=True) - _, cfg = _configure( - TASK, - num_envs=args.num_envs, - renderer=args.renderer, - output=output, - particle_spacing=args.spacing, - ) - cfg.sim_cfg.num_envs = cfg.num_envs - sim = SimulationManager(cfg.sim_cfg) - writers = [] - robot = cup = camera = engine = None - try: - robot = sim.add_robot(cfg.robot) - for object_cfg in [*cfg.background, *cfg.rigid_object]: - sim.add_rigid_object(object_cfg) - for fluid_cfg in cfg.fluid_object: - sim.add_fluid_object(fluid_cfg) - if sim.has_native_renderer: - for light_cfg in cfg.light.direct: - sim.add_light(light_cfg) - sim.prepare() - robot.build_pk_serial_chain() - sim.reset_objects_state() - cup = sim.get_rigid_object("cup") - # Live gravity/contact settling before observing the planning scene. - sim.update(step=24, render_final_step=False) - initial_pose = cup.get_local_pose(to_matrix=True).clone() - fill_pose = cup.get_local_pose().clone() - fill_pose[:, :3] += sim.arena_offsets - sim.get_fluid_object("water").reset_to_pose(fill_pose) - object_to_eef = torch.tensor( - [[0, 0, -1, 0.065], [0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1]], - dtype=torch.float32, - device=sim.device, - ) - semantics = ObjectSemantics( - entity_id="cup", - label="cup", - geometry={}, - affordance=AxisAlignAffordance( - mesh_vertices=cup.get_vertices(env_ids=[0], scale=True)[0], - mesh_triangles=cup.get_triangles(env_ids=[0])[0], - internal_axis=torch.tensor([1.0, 0, 0], device=sim.device), - ), - ) - motion = MotionGenerator( - MotionGenCfg( - planner_cfg=ToppraPlannerCfg( - robot_uid=robot.uid, sim_instance_id=sim.instance_id - ) - ) - ) - engine = create_simulation_atomic_action_engine( - motion, - (cup,), - control_profiles={ - "eef": ControlPartCommandProfile.joint_positions( - open=torch.tensor([0.04, 0.04], device=sim.device), - grasp=torch.zeros(2, device=sim.device), - ) - }, - ) - bindings = {"primary": {"motion": "arm", "grasp": "eef"}} - policy = MotionPolicy( - strategy="ik_interp", sample_count=360, dynamic_collision_mode="off" - ) - receiver_pose = sim.get_rigid_object("receiver").get_local_pose(to_matrix=True) - receiver_to_cup = torch.eye(4, device=sim.device) - receiver_to_cup[:3, 3] = torch.tensor([0.005, 0.09, 0.17], device=sim.device) - pour_pose = receiver_pose @ receiver_to_cup - invocations = ( - engine.make_invocation( - "pick_up", - GraspGoal(semantics), - control_parts=bindings, - motion_policy=policy, - skill_options=PickUpOptions( - fixed_object_to_eef=object_to_eef, - approach_direction=torch.tensor([-1.0, 0, 0], device=sim.device), - pre_grasp_distance=0.08, - lift_height=0.12, - hand_interp_steps=20, - grasp_settle_steps=20, - ), - ), - engine.make_invocation( - "move_held_object", - HeldObjectPoseGoal(pour_pose), - control_parts=bindings, - motion_policy=policy, - skill_options=MoveHeldObjectOptions(), - ), - engine.make_invocation( - "pour", - PourGoal(), - control_parts=bindings, - motion_policy=policy, - skill_options=PourOptions(rotate_angle=np.deg2rad(80)), - ), - engine.make_invocation( - "place", - PlaceGoal(initial_pose @ object_to_eef), - control_parts=bindings, - motion_policy=policy, - skill_options=PlaceOptions( - hand_interp_steps=20, release_settle_steps=20 - ), - ), - ) - control_dt = cfg.sim_steps_per_control * sim.sim_config.physics_dt - plan = engine.compile( - invocations, engine.initial_context(control_dt=control_dt) - ) - if not bool(plan.plan_success.all()): - raise RuntimeError("Atomic Action planning failed.") - if args.renderer != "no-render": - params = cfg.events.record_camera.params - camera = sim.add_sensor( - CameraCfg( - uid="fluid_sim_record", - width=params["resolution"][0], - height=params["resolution"][1], - intrinsics=params["intrinsics"], - visualization_role="record", - extrinsics=CameraCfg.ExtrinsicsCfg( - eye=params["eye"], target=params["target"], up=params["up"] - ), - ) - ) - writers = [ - imageio.get_writer( - output / f"fluid_sim_env{i}.mp4", - fps=1 / control_dt, - codec="libx264", - quality=8, - macro_block_size=1, - ) - for i in range(sim.num_envs) - ] - if not args.headless: - sim.open_window() - positions = plan.trajectory.positions - poses = [cup.get_local_pose().cpu().numpy().copy()] - samples = [] - started = time.perf_counter() - count = positions.shape[1] + round(2 / control_dt) - if args.seconds is not None: - count = min(count, int(np.ceil(args.seconds / control_dt))) - for i in range(count): - # Set targets only. Every measured joint/cup pose comes from physics. - robot.set_qpos(positions[:, min(i, positions.shape[1] - 1)], target=True) - sim.update(step=cfg.sim_steps_per_control, render_final_step=False) - poses.append(cup.get_local_pose().cpu().numpy().copy()) - if camera is not None: - with sim.render_frame(): - sim.sync_render_state() - camera.update() - rgb = camera.get_data()["color"][..., :3].cpu().numpy() - for env_id, writer in enumerate(writers): - writer.append_data(rgb[env_id]) - if i % round(1 / control_dt) == 0 or i == count - 1: - fluid = sim.get_fluid_object("water") - state = fluid.data.particle_state_w - if not bool(torch.isfinite(state).all()): - raise RuntimeError("Non-finite online fluid state.") - local = state[..., :3] - sim.arena_offsets[:, None, :] - receiver_pose = sim.get_rigid_object("receiver").get_local_pose( - to_matrix=True - ) - receiver = torch.bmm( - local - receiver_pose[:, None, :3, 3], receiver_pose[:, :3, :3] - ) - received = ( - (receiver[..., 2] > 0.0045) - & (receiver[..., 2] < 0.0675) - & ( - torch.linalg.vector_norm( - receiver - torch.tensor([0, 0, 0.088], device=sim.device), - dim=-1, - ) - < 0.093 - ) - ) - sample = { - "time_s": (i + 1) * control_dt, - "cup_pose": poses[-1].tolist(), - "received_ml": (received.sum(dim=1) * args.spacing**3 * 1e6) - .cpu() - .tolist(), - } - samples.append(sample) - print(json.dumps(sample), flush=True) - report = { - "coupling": "online one-way rigid-to-fluid", - "controller": "fresh Atomic Action joint targets", - "cup": _cup_qualification(poses), - "num_envs": sim.num_envs, - "particle_count_per_env": sim.get_fluid_object("water").num_particles, - "control_dt": control_dt, - "simulated_seconds": count * control_dt, - "wall_seconds": time.perf_counter() - started, - "samples": samples, - } - report["accepted"] = bool( - all(report["cup"]["accepted"]) - and all(v >= 50 for v in samples[-1]["received_ml"]) - ) - (output / "validation.json").write_text(json.dumps(report, indent=2) + "\n") - print(json.dumps(report["cup"]), flush=True) - if args.seconds is None and not report["accepted"]: - raise RuntimeError( - "Online Sim pouring failed physical qualification; inspect validation.json." - ) - finally: - for writer in writers: - writer.close() - robot = cup = camera = engine = None - sim.destroy(exit_process=False) - del sim - SimulationManager.flush_cleanup_queue() - - -if __name__ == "__main__": - main() From bb6bab7812507c45909e96542d397ff0e5b3680f Mon Sep 17 00:00:00 2001 From: yuecideng Date: Wed, 28 Oct 2026 14:09:35 +0800 Subject: [PATCH 6/7] fix(fluid): keep neighboring pours out of camera view --- .../topics/simulation-system/fluids.md | 8 ++++ .../manipulation/tableware/pour_cup/README.md | 2 + .../tableware/pour_cup/env.fluid.yaml | 4 +- tests/gym/envs/test_fluid_integration.py | 41 +++++++++++++++++++ tests/sim/objects/test_fluid_visibility.py | 17 ++++++++ 5 files changed, 71 insertions(+), 1 deletion(-) diff --git a/agent_context/topics/simulation-system/fluids.md b/agent_context/topics/simulation-system/fluids.md index 4249ab375..a30f53620 100644 --- a/agent_context/topics/simulation-system/fluids.md +++ b/agent_context/topics/simulation-system/fluids.md @@ -70,6 +70,14 @@ 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). +Each fluid render particle belongs to its Arena and is published in world +coordinates. Native cameras view the shared render scene: Arena ownership and +physics-world isolation do not filter other Arenas from a camera. Camera framing +and spacing can keep a known task workspace out of view, but do not provide +general camera-level isolation. In the qualified DexSim build above, the +reconstructed fluid surface is a separate native render instance and does not +inherit the particle node's visibility toggle. + `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 diff --git a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md index 9a0d9150a..f392b4c3f 100644 --- a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md +++ b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md @@ -44,6 +44,8 @@ python scripts/tutorials/sim/franka_dexuni_pour_demo.py --headless --renderer fa 增加 `--num-envs 4` 可在同一个 World 中并行运行四个环境,分别输出 `fluid_sim_env0.mp4` 至 `fluid_sim_env3.mp4`。 +场景间距按录制相机的视野设置,以避免相邻环境的倒水工作区出现在预览中。 +各环境仍共享渲染场景;其他相机视角不自动隔离环境。 默认粒子间距为 1.5 mm,初始填充 76,607 粒。Gym 的 `--particle-spacing 0.003` 或 Sim 的 `--spacing 0.003` 可用于较快的物理检查,但会改变流体分辨率。 diff --git a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/env.fluid.yaml b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/env.fluid.yaml index 6fd0a6d13..99d2aba56 100644 --- a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/env.fluid.yaml +++ b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/env.fluid.yaml @@ -3,7 +3,9 @@ physics: newton render_cfg: renderer: fast-rt num_envs: 2 -arena_space: 2.5 +# Native cameras see the shared World. Keep neighboring pouring workspaces +# outside the calibrated recording camera's field of view. +arena_space: 7.0 max_episode_steps: 2000 max_episodes: 1 physics_config: diff --git a/tests/gym/envs/test_fluid_integration.py b/tests/gym/envs/test_fluid_integration.py index c74fe9367..10b25dfb8 100644 --- a/tests/gym/envs/test_fluid_integration.py +++ b/tests/gym/envs/test_fluid_integration.py @@ -18,6 +18,7 @@ from __future__ import annotations +from itertools import product from pathlib import Path from types import SimpleNamespace import numpy as np @@ -100,6 +101,46 @@ def test_fluid_deployment_resolves_sources_and_observation_entities(): assert cfg.task_program is not None +def test_record_camera_excludes_neighboring_pour_workspaces(): + """The recording view excludes each neighbor's expected pour region.""" + cfg = config_to_cfg(load_config(TASK), source_path=TASK) + params = cfg.events.record_camera.params + eye = np.asarray(params["eye"], dtype=float) + forward = np.asarray(params["target"], dtype=float) - eye + forward /= np.linalg.norm(forward) + right = np.cross(forward, np.asarray(params["up"], dtype=float)) + right /= np.linalg.norm(right) + up = np.cross(right, forward) + width, height = params["resolution"] + fx, fy, cx, cy = params["intrinsics"] + + # Envelope around the cup and receiver, including space above the pour. + workspace = np.asarray(list(product((-0.90, -0.60), (-0.28, 0.08), (0.49, 0.80)))) + spacing = cfg.sim_cfg.arena_space + offsets = np.asarray(list(product((0.0, spacing), repeat=2))) + for camera_offset in offsets: + for other_offset in offsets: + if np.array_equal(camera_offset, other_offset): + continue + relative = workspace.copy() + relative[:, :2] += other_offset - camera_offset + relative -= eye + depth = relative @ forward + horizontal = relative @ right + vertical = relative @ up + outside = ( + depth <= 0, + horizontal < -(cx / fx) * depth, + horizontal > ((width - cx) / fx) * depth, + vertical < -((height - cy) / fy) * depth, + vertical > (cy / fy) * depth, + ) + assert any(np.all(plane) for plane in outside), ( + camera_offset, + other_offset, + ) + + def test_fluid_component_rejects_duplicate_inline_ownership(): config = load_config(TASK) config["fluid_object"] = [] diff --git a/tests/sim/objects/test_fluid_visibility.py b/tests/sim/objects/test_fluid_visibility.py index acfc10ab9..0f1954283 100644 --- a/tests/sim/objects/test_fluid_visibility.py +++ b/tests/sim/objects/test_fluid_visibility.py @@ -147,6 +147,23 @@ def _run_visibility_worker(output: Path, clipping: bool) -> None: ) ) sim.prepare() + # Each render particle must belong to its own Arena and inherit that + # Arena's transform before its world-space particle data is published. + for env_id, particle in enumerate(fluid._entities): + arena = sim.get_env(env_id) + render_particle = particle._render_resource + assert ( + render_particle.get_arena().get_native_handle() + == arena.get_native_handle() + ) + np.testing.assert_allclose( + render_particle.get_world_pose(), + arena.get_root_node().get_world_pose() + @ render_particle.get_local_pose(), + atol=1e-6, + rtol=0, + ) + arena = particle = render_particle = None sim.update(step=2) def capture(): From 2b5a4ee7da4d57f55f69a55be5bdd4d2cd1ad9a3 Mon Sep 17 00:00:00 2001 From: yuecideng Date: Wed, 28 Oct 2026 14:15:11 +0800 Subject: [PATCH 7/7] revert: restore fluid task environment spacing --- .../topics/simulation-system/fluids.md | 8 ---- .../manipulation/tableware/pour_cup/README.md | 2 - .../tableware/pour_cup/env.fluid.yaml | 4 +- tests/gym/envs/test_fluid_integration.py | 41 ------------------- tests/sim/objects/test_fluid_visibility.py | 17 -------- 5 files changed, 1 insertion(+), 71 deletions(-) diff --git a/agent_context/topics/simulation-system/fluids.md b/agent_context/topics/simulation-system/fluids.md index a30f53620..4249ab375 100644 --- a/agent_context/topics/simulation-system/fluids.md +++ b/agent_context/topics/simulation-system/fluids.md @@ -70,14 +70,6 @@ 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). -Each fluid render particle belongs to its Arena and is published in world -coordinates. Native cameras view the shared render scene: Arena ownership and -physics-world isolation do not filter other Arenas from a camera. Camera framing -and spacing can keep a known task workspace out of view, but do not provide -general camera-level isolation. In the qualified DexSim build above, the -reconstructed fluid surface is a separate native render instance and does not -inherit the particle node's visibility toggle. - `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 diff --git a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md index f392b4c3f..9a0d9150a 100644 --- a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md +++ b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/README.md @@ -44,8 +44,6 @@ python scripts/tutorials/sim/franka_dexuni_pour_demo.py --headless --renderer fa 增加 `--num-envs 4` 可在同一个 World 中并行运行四个环境,分别输出 `fluid_sim_env0.mp4` 至 `fluid_sim_env3.mp4`。 -场景间距按录制相机的视野设置,以避免相邻环境的倒水工作区出现在预览中。 -各环境仍共享渲染场景;其他相机视角不自动隔离环境。 默认粒子间距为 1.5 mm,初始填充 76,607 粒。Gym 的 `--particle-spacing 0.003` 或 Sim 的 `--spacing 0.003` 可用于较快的物理检查,但会改变流体分辨率。 diff --git a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/env.fluid.yaml b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/env.fluid.yaml index 99d2aba56..6fd0a6d13 100644 --- a/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/env.fluid.yaml +++ b/extensions/embodichain_tasks/configs/tasks/manipulation/tableware/pour_cup/env.fluid.yaml @@ -3,9 +3,7 @@ physics: newton render_cfg: renderer: fast-rt num_envs: 2 -# Native cameras see the shared World. Keep neighboring pouring workspaces -# outside the calibrated recording camera's field of view. -arena_space: 7.0 +arena_space: 2.5 max_episode_steps: 2000 max_episodes: 1 physics_config: diff --git a/tests/gym/envs/test_fluid_integration.py b/tests/gym/envs/test_fluid_integration.py index 10b25dfb8..c74fe9367 100644 --- a/tests/gym/envs/test_fluid_integration.py +++ b/tests/gym/envs/test_fluid_integration.py @@ -18,7 +18,6 @@ from __future__ import annotations -from itertools import product from pathlib import Path from types import SimpleNamespace import numpy as np @@ -101,46 +100,6 @@ def test_fluid_deployment_resolves_sources_and_observation_entities(): assert cfg.task_program is not None -def test_record_camera_excludes_neighboring_pour_workspaces(): - """The recording view excludes each neighbor's expected pour region.""" - cfg = config_to_cfg(load_config(TASK), source_path=TASK) - params = cfg.events.record_camera.params - eye = np.asarray(params["eye"], dtype=float) - forward = np.asarray(params["target"], dtype=float) - eye - forward /= np.linalg.norm(forward) - right = np.cross(forward, np.asarray(params["up"], dtype=float)) - right /= np.linalg.norm(right) - up = np.cross(right, forward) - width, height = params["resolution"] - fx, fy, cx, cy = params["intrinsics"] - - # Envelope around the cup and receiver, including space above the pour. - workspace = np.asarray(list(product((-0.90, -0.60), (-0.28, 0.08), (0.49, 0.80)))) - spacing = cfg.sim_cfg.arena_space - offsets = np.asarray(list(product((0.0, spacing), repeat=2))) - for camera_offset in offsets: - for other_offset in offsets: - if np.array_equal(camera_offset, other_offset): - continue - relative = workspace.copy() - relative[:, :2] += other_offset - camera_offset - relative -= eye - depth = relative @ forward - horizontal = relative @ right - vertical = relative @ up - outside = ( - depth <= 0, - horizontal < -(cx / fx) * depth, - horizontal > ((width - cx) / fx) * depth, - vertical < -((height - cy) / fy) * depth, - vertical > (cy / fy) * depth, - ) - assert any(np.all(plane) for plane in outside), ( - camera_offset, - other_offset, - ) - - def test_fluid_component_rejects_duplicate_inline_ownership(): config = load_config(TASK) config["fluid_object"] = [] diff --git a/tests/sim/objects/test_fluid_visibility.py b/tests/sim/objects/test_fluid_visibility.py index 0f1954283..acfc10ab9 100644 --- a/tests/sim/objects/test_fluid_visibility.py +++ b/tests/sim/objects/test_fluid_visibility.py @@ -147,23 +147,6 @@ def _run_visibility_worker(output: Path, clipping: bool) -> None: ) ) sim.prepare() - # Each render particle must belong to its own Arena and inherit that - # Arena's transform before its world-space particle data is published. - for env_id, particle in enumerate(fluid._entities): - arena = sim.get_env(env_id) - render_particle = particle._render_resource - assert ( - render_particle.get_arena().get_native_handle() - == arena.get_native_handle() - ) - np.testing.assert_allclose( - render_particle.get_world_pose(), - arena.get_root_node().get_world_pose() - @ render_particle.get_local_pose(), - atol=1e-6, - rtol=0, - ) - arena = particle = render_particle = None sim.update(step=2) def capture():