Skip to content

[FEATURE] IPC Coupler Basic Logic#2931

Open
ACMLCZH wants to merge 35 commits into
Genesis-Embodied-AI:mainfrom
ACMLCZH:pr/ipc-basic-logic
Open

[FEATURE] IPC Coupler Basic Logic#2931
ACMLCZH wants to merge 35 commits into
Genesis-Embodied-AI:mainfrom
ACMLCZH:pr/ipc-basic-logic

Conversation

@ACMLCZH

@ACMLCZH ACMLCZH commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add IPC (Incremental Potential Contact) as a coupler option in Genesis, sitting alongside the existing SAP/Legacy couplers. Wraps libuipc's C++ ABD + FEM solver and orchestrates a per-step Rigid↔FEM↔IPC handshake so that Genesis's rigid-body dynamics and FEM dynamics share a single contact-resolution pass.

Coupling modes (per gs.materials.Rigid(coup_type=...))

coup_type Who owns the body's dynamics Who applies contact forces Used for
two_way_soft_constraint Genesis rigid solver (full integration) IPC ABD body tracks Genesis pose via SoftTransformConstraint; back-coupling force returned to Genesis Any rigid bodies that controlled in RigidSolver and coupled with FEM (not recommended to be used in articulations because it ignores constraints)
external_articulation Genesis rigid solver (joint-level) IPC's ExternalArticulationConstraint corrects each joint's delta_theta after contact Articulated robots ( fixed-base only )
ipc_only libuipc ABD (free body) libuipc directly Free-floating rigid objects that don't need PD/joint control — base joint gets FREE→FIXED conversion at build

FEM entities (tet meshes via gs.materials.FEM.*) and Cloth entities (thin-shell via gs.materials.FEM.Cloth) are always owned by libuipc and contact-coupled with all ABD bodies above.

Per-step flow

For each scene.step() substep, the order is:

substep_pre_coupling:
    rigid_solver.substep_pre_coupling(f)
        kernel_compute_qacc(...)                                  # Genesis-side forward dynamics
        if IPC:
            kernel_predict_integrate(update_qpos=True)            # vel_prev=vel; vel+=acc*dt; qpos_prev=qpos; qpos+=vel*dt
            kernel_forward_kinematics_links_geoms(...)            # FK on predicted qpos
        elif SAP:
            kernel_predict_integrate(update_qpos=False)           # only vel prediction
        else:
            constraint_force()                                    # legacy / no-coupler path

coupler.couple(f):                                                # IPC step (this PR's core)
    1. _store_gs_rigid_states()                                   # snapshot Genesis mass_mat, links_state
    2. cache_pre_prediction_transforms()                          # write any teleport (set_qpos) into IPC ABD bodies
    3. _pre_advance_write_ipc_attributes()
         two_way:  aim_transform[link]   = Genesis predicted T    # what STC tracks
         ext_art:  ref_dof_prev[entity]  = Genesis delta_theta    # what ExternalArticulationConstraint constrains
         ipc_only: aim_transform[link]   = Genesis predicted T (animator path)
    4. _ipc_world.advance()                                       # libuipc Newton solve (contact + dynamics)
    5. _ipc_world.retrieve()
    6. _retrieve_ipc_fem_states()                                 # copy IPC vertex positions  -> FEM solver
    7. _retrieve_ipc_rigid_states()                               # copy IPC ABD transforms    -> abd_data.ipc_transforms
    8. _post_advance_write_qpos()
         Step 1a: ipc_only  base links   -> links_state.pos/quat  (direct write)
         Step 1b: non-fixed base links   -> qpos[0:7]              (FREE-base translation+rotation)
         Step 2a: two_way   child links  -> qpos[q_idx]            (back-compute joint angle from IPC transform)
         Step 2b: ext_art   entities     -> qpos[joints]           (apply IPC's delta_theta)

substep_post_coupling:
    rigid_solver.substep_post_coupling(f)
        if IPC:
            kernel_restore_integrate(restore_qpos=True)           # vel = (qpos_ipc - qpos_prev)/dt; qpos = qpos_prev; acc back-fill
        elif SAP:
            kernel_restore_integrate(restore_qpos=False)          # only acc back-fill
        kernel_step_2(...)                                         # final FK + collision update

Key invariant: for two_way_soft_constraint and external_articulation, Genesis remains the authority for qpos; IPC only adjusts qpos in response to contact. For ipc_only, libuipc is fully authoritative for the body's pose — Genesis just reads the result.

Velocity back-compute (BDF1 assumption)

After IPC writes corrected qpos, kernel_restore_integrate derives velocity as vel = (qpos − qpos_prev) / dt — BDF1. The libuipc integrator is pinned to bdf1 in build_ipc_scene_config so this stays consistent. Switching to BDF2 would require also resetting q_{n−1} / v_{n−1} on teleport, which libuipc doesn't currently expose. Pin + FIXME tracked in utils.py.

Self-collision filtering

IPC's contact tabular respects the same self-collision policy as the rigid solver:

  • enable_self_collision — disable all same-entity pairs
  • enable_adjacent_collision — disable parent↔child link pairs in the same articulation
  • enable_neutral_collision — disable pairs whose merged meshes already overlap at qpos₀ (auto-detect via 99.9%-shrunk trimesh intersection)

coup_links on the material limits which links participate in IPC at all; coup_collision_links further limits which links get contact-active (vs no-collision).

ABD body construction

  • Mesh ABD bodies: built via apply_to(mesh, kappa=100 MPa, mass_density=material.rho) — libuipc derives mass/COM/inertia from mesh volumetric integration. Caveat: Genesis-side link.inertial_mass may follow the URDF's explicit <mass>, which can differ from material.rho × mesh_volume. The two solvers can silently disagree for URDFs that author non-default <inertial> blocks. Tracked as a FIXME in coupler.py; principled fix (overload-2 with explicit 12×12 ABD matrix) is verified mechanically correct against libuipc tests but requires test re-calibration before shipping.
  • Proxy ABD bodies (no collision mesh): built via create_proxy(mass, mass_center, inertia, volume) using Genesis URDF inertials directly — no mass disagreement in this path.

Fixed-link merging

find_target_link_for_fixed_merge walks up the kinematic tree across FIXED joints, merging child geometry into the first non-fixed ancestor's ABD body. E.g., Franka's hand (FIXED joint to link7) merges into link7's ABD body. Reduces ABD body count and avoids the merged children's collision-detection ambiguity.

Scope

Area Files Notes
IPC coupler genesis/engine/couplers/ipc_coupler/{coupler,data,utils}.py core implementation
Coupler options genesis/options/solvers.py (IPCCouplerOptions) Newton/line-search/contact knobs + Genesis-side toggles
Materials genesis/engine/materials/rigid.py coup_type, coup_links, coup_collision_links, soft_constraint_strength, contact_resistance
Rigid kernels rigid_solver.py, abd/forward_dynamics.py kernel_predict_integrate, kernel_restore_integrate
Collider filter solvers/rigid/collider/collider.py self-collision filter + IPC-vs-rigid delegation
Tests tests/test_ipc.py 54 tests covering all three coup_types, contact pairs, set_qpos sync, ext_art merge, cloth coupling

Excludes (separate PRs): FEM Paper/Rope materials, plastic discrete-shell bending, per-entity contact_d_hat override, COM/inertia pass-through to libuipc (the principled fix for the mass-disagreement caveat above).

Test plan

  • pytest tests/test_ipc.py — 54/54 pass on RTX 5090 Linux
  • pytest tests/test_fem.py — 9 pass + 1 pre-existing upstream xfail unrelated to IPC
  • pytest tests/test_rigid_physics.py — passes with the IPC delegation/filter changes (re-run with the post-review cleanups recommended)
  • Examples under examples/IPC_Solver/ (4 scripts) ported to the new API

ACMLCZH and others added 15 commits June 9, 2026 19:55
…erials/rigid, array_class, kinematic_solver, mesh, test_ipc done; rigid_solver/rigid_entity/forward_dynamics/options/solvers pending)
…rigid_solver, kernels added to forward_dynamics, utils.py from Genesis-Embodied-AI#2502
…egation rename

- Pin self.options.{joint_strength_ratio, enable_fem_fem_friction,
  before_ipc_world_init, verbose_ipc_log} to safe defaults — these are
  post-Genesis-Embodied-AI#2502 IPCCouplerOptions fields not in Genesis-Embodied-AI#2502.
- Replace entity.delegation (post-Genesis-Embodied-AI#2502 RigidEntity attribute) with
  entity.material.coup_type == 'ipc_only' (Genesis-Embodied-AI#2502-equivalent test).
- Rename find_abd_merge_target -> find_target_link_for_fixed_merge to
  match Genesis-Embodied-AI#2502's utils.py.

10 IPC tests passing (single_joint two_way_soft_constraint, needs_coup,
link_filter); 8 failing (ext_art, momentum, ground_clearance) — those
are likely RealMain post-Genesis-Embodied-AI#2502 logic still leaking, to investigate.
…I#2502's 'fixed-base ext_art transforms is None' test invariant

The Genesis-Embodied-AI#2502 design had ipc_transforms=None for fixed-base ext_art because
set_qpos for ext_art wasn't supported. RealMain extends set_qpos to all
ABD coup types, which requires ipc_transforms to be populated uniformly.
Drop the now-meaningless 'if fixed: assert is None' test branch.

Define JOINT_STRENGTH_RATIO constant (was self.options.joint_strength_ratio
in RealMain; Genesis-Embodied-AI#2502's IPCCouplerOptions doesn't have this field).
Without this, Genesis rigid solver integrates ipc_only entities under
gravity (they have 6 DOFs from the implicit FREE joint), causing them
to fall through IPC ground geometry. RealMain's _create_joints override
zeros out DOFs for ipc_only's FREE joint so the constraint solver
contributes nothing and IPC fully controls the dynamics.

Fixes test_rigid_ground_sliding[0/2]. test_ipc_rigid_ground_clearance[0/2]
still fails hairline numerical (4.78e-5 vs tol 5e-5).
- test_set_object_qpos[ipc_only]: use set_pos/set_quat (no DOFs after FIXED)
- test_find_target_links[two_way_soft_constraint]: pass coup_links=('link7',)
- test_robot_grasp_abd: use get_pos() not get_dofs_position() (0 DOFs)
- coupler._mark_ipc_abd_updated: accept int/slice for links_idx (set_pos
  zerocopy path passes a single int)
RealMain defines apply_restitution_velocity but never calls it from
rigid_solver.substep_post_coupling. Genesis-Embodied-AI#2502 wired it up to apply
restitution velocity corrections after step_2, but those corrections
caused the rigid body to bounce back harder than physically correct
(-1.04 m/s vs -0.71 m/s in 4 m/s incident collision test).

Removing the call brings cube-bounce velocity to within 3% of RealMain's
output. The method is kept in place (matches RealMain's coupler content)
but unused — future cleanup PR can decide whether to remove it.

Effect on test_momentum_conservation: still fails by the same hairline
margin (test sets restitution=0.0 which bypasses the accumulator anyway).
The remaining ~0.001 momentum drift comes from a different source.
Drops apply_restitution_velocity + _accumulate_restitution_base_link
method definitions + RESTITUTION_CONTACT_THRESHOLD + accumulator state
+ all callsites. The accumulator was wired by Genesis-Embodied-AI#2502 but RealMain
disconnected it (defines but never calls), and the previous commit
removed the rigid_solver.substep_post_coupling call site.

Net effect: restitution=1.0 default is now a no-op for IPC; users
relying on it for two-way coupling should rely on IPC's native contact
resolution (which conserves momentum cleanly in test_momentum_conservation
even with restitution=0.0).
…-frame log

- test_cloth_uniform_biaxial_stretching: pre-compute phase-0/1/2 DOF
  targets per box (free-joint 6-DOF axis-angle, identity rotation),
  removing the per-phase `get_dofs_position()` reads.
- coupler.couple: demote per-frame `[IPC] frame ...` log to debug.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ACMLCZH ACMLCZH changed the title [FEATURE] IPC Coupler — basic-logic slice (#2502-equivalent) [FEATURE] IPC Coupler Basic Logic Jun 12, 2026
ACMLCZH and others added 14 commits June 12, 2026 10:58
restitution: float = 1.0 contradicted the docstring's "Defaults to 0.0"
and every test that explicitly sets it. The 1.0 value silently turned
contacts perfectly elastic in any user code that constructed
IPCCouplerOptions() without naming restitution. Match the docstring.

ipc_momentum.py, ipc_objects_falling.py, ipc_robot_cloth_teleop.py and
ipc_robot_grasp_cube.py still referenced removed IPCCouplerOptions
fields (constraint_strength_translation/rotation, two_way_coupling).
Pydantic strict-mode now rejects them. Move the soft-constraint
strengths onto each entity's `gs.materials.Rigid(soft_constraint_strength=...)`
and drop the obsolete `two_way_coupling=True` (coup_type is per-entity).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Correctness:
- mark_abd_updated: route envs_idx through Scene._sanitize_envs_idx so torch
  bool masks / scalar ints / iterables all normalize to a torch int tensor.
  Short-circuit n_envs==0. Previously a bool mask flagged wrong envs as
  dirty (set(int(i) for i in bool_tensor) gives 0/1 not indices), and a
  scalar envs_idx=0 raised TypeError in the zero-copy fast path.
- _post_advance_write_qpos Step 2b: cast ipc_qpos to qpos_tc.dtype as well
  as device. .to(device) alone left dtype unspecified on torch>=2.x.
- cache_pre_prediction_transforms: drop the leftover DEBUG warning that
  fired every set_qpos/set_dofs_position (spammed teleop control loops).

Cleanup / altitude:
- Remove dead IPCBeforeWorldInitCallback machinery (~30 lines + 8 unused
  imports). The callback variable was assigned None then tested for None.
- Add COUPLING_TYPE.auto_infer(entity) / .resolve(entity) classmethods;
  use them from IPCCoupler._setup_coupling_config and RigidCollider's
  self-collision filter. Previously the auto-infer rule lived in two
  source-of-truth copies that had to stay in lockstep.
- Pin libuipc integrator to BDF1 in build_ipc_scene_config. The coupler's
  velocity back-compute and teleport-sync logic assume BDF1; this guard
  prevents a future libuipc default flip from silently breaking them.
- Step 2a back-compute: rename child_quat_pre/pos_pre to child_cur_quat0
  /child_cur_pos0 (and qloc/rotvec/axis/angle_ipc to child_cur_q/_rot/
  /child_axis/_qaxis). The "_pre" suffix read as "previous timestep"
  but actually meant "child orientation/position with the joint at
  qpos==qpos0" — i.e. parent_current composed with the URDF/MJCF static
  offset. New names make the cancellation against FK's `qpos - qpos0`
  formula explicit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three small cleanups surfaced while answering code-review follow-ups:

* Remove _sync_rigid_fk() and its sole caller. The call was introduced in
  RealMain commit 9fbff06 "primary fix" alongside a debug surface
  exporter (_export_genesis_surface) that needed an FK refresh before
  reading link/geom transforms. Our basic-logic slice stripped the
  exporter but left the FK call orphaned. rigid_solver.substep_post_coupling
  runs kernel_step_2 -> func_update_cartesian_space (= FK) immediately
  after, so this call was double work. 54 test_ipc.py tests still pass.

* Replace the module-level _link_is_fixed_for_ipc(link) helper with an
  is_fixed: bool field on ABDLinkData, computed inline where the libuipc
  is_fixed attr is written and stored alongside the link's other ABD
  data. Drops a free function and three repeated dict-lookup-by-key
  patterns at the call sites.

* Rename local variables _n_enabled / _n_disabled (contact pair counters
  in _setup_contact_pairs) to n_enabled / n_disabled. Leading-underscore
  on a local variable conventionally signals "intentionally unused",
  but these are read on the next line; the underscore was just noise.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous FIXME described the COM pass-through blocker as a libuipc-side
"frame alignment SIGABRT". Deeper investigation found the actual issue:

  - Genesis honors URDF `<mass>` (URDF wins over rho * mesh volume in
    rigid_link.py:717-769 when authored), so `link.inertial_mass` follows
    the URDF, not the material default rho.
  - The coupler passes `material.rho` (default RHO_ROBOT=1500) to libuipc's
    `apply_to(mesh, kappa, mass_density=rho)`, which integrates volume*rho
    and stores that as the body's mass.
  - When URDF mass != rho * volume, Genesis and libuipc simulate different
    physical bodies for the same link. Concrete: two_cube_revolute.urdf
    authors mass=1.0, default RHO_ROBOT=1500 for 0.1m³ → Genesis stores
    1.0kg, libuipc stores 1.5kg. 50% mass disagreement, silent.

The principled fix (build the 12x12 ABD mass matrix via
`uipc.geometry.affine_body.from_rigid_body(mass, com, inertia)` and use
`apply_to(sc, kappa, mass=abd_M12, volume)` overload-2) is verified correct
against libuipc tests 75_abd_mass_properties and 84_proxy_abd_gravity.

The blocker is asset-level: shipping the fix makes libuipc honor the URDF,
which changes simulated dynamics for any URDF authored with
mass != default rho * volume. test_single_joint[revolute-two_way_soft_constraint-False]
regresses on this. Resolving requires test re-calibration or an opt-in
material flag; tracked for a focused follow-up PR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The moving link's <inertial><origin> in both URDFs was shifted from `0 0 0`
to `0.1 0 0` via the b5cb90e CHECKPOINT (originally introduced by RealMain
commit 9fbff06 "primary fix" to avoid base/moving cube overlap at qpos=0
under that branch's IPC setup).

These are shared test assets — tests/test_kinematic.py, tests/test_ipc.py,
and tests/test_rigid_physics.py all consume them — so the PR shouldn't carry
asset-level modifications. Verified all 54 test_ipc.py tests + test_kinematic
test_setters pass with the upstream URDFs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ACMLCZH and others added 3 commits June 16, 2026 16:55
This PR's bfc9041 ("CHECKPOINT: Genesis-Embodied-AI#2502 patches") rewired the SAP-coupler branch
in rigid_solver.substep_post_coupling to call kernel_restore_integrate(restore_qpos=False)
instead of update_qacc_from_qvel_delta. The replacement is byte-identical math
(both compute `acc = (vel - vel_prev) / dt; vel = vel_prev` with the same
hibernation / parallelization structure — phase 1 of kernel_restore_integrate
is skipped when restore_qpos=False, leaving only phase 2 which mirrors the
original kernel exactly).

`update_qacc_from_qvel_delta` was left orphaned by that rewire — defined but
no longer called anywhere. Removing it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Same pattern as the prior commit's update_qacc_from_qvel_delta deletion.

Upstream's substep_pre_coupling SAP-coupler branch called update_qvel
(`vel_prev = vel; vel = vel + acc * dt` — predict next velocity). This
PR's bfc9041 ("CHECKPOINT: Genesis-Embodied-AI#2502 patches") rewired that branch to call
kernel_predict_integrate(update_qpos=False) instead. Phase 1 of
kernel_predict_integrate is byte-identical math to update_qvel; Phase 2
(qpos integration) is skipped when update_qpos=False. Result: SAP behavior
preserved, update_qvel orphaned. Removing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7 test functions (test_link_filter_strict, test_robot_grasp_fem,
test_robot_grasp_abd, test_collision_delegation_ipc_vs_rigid, test_cloth_corner_drag,
test_set_robot_qpos, test_coup_collision_links) had a blank line right after
the closing docstring """, before the first code statement. Inconsistent with
the rest of the test suite (2/121 across test_rigid_physics + test_fem +
test_kinematic — essentially noise level) and with the standard Python style.
Tightening to match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ACMLCZH ACMLCZH marked this pull request as ready for review June 17, 2026 00:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: acdb7b2d05

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1072 to +1075
if qs_idx is not None:
if isinstance(qs_idx, slice):
qs_idx = range(*qs_idx.indices(len(self._q_to_abd_link)))
for qi in qs_idx:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle scalar qpos indices when marking IPC state dirty

When using IPCCoupler, a direct RigidSolver.set_qpos(..., qs_idx=3) call reaches this branch with qs_idx still as an int; iterating it raises TypeError: 'int' object is not iterable before the ABD state can be marked dirty. The link index branch below already normalizes scalar integers, so scalar qpos/dof indices need the same treatment to keep single-DOF updates working under IPC.

Useful? React with 👍 / 👎.

Comment thread genesis/options/solvers.py Outdated
self._show_ipc_gui = bool(_show_ipc_gui)
enable_rigid_ground_contact: bool = True
enable_rigid_rigid_contact: bool = True
restitution: float = 0.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Implement the advertised restitution option

This new option is documented and tests explicitly set it to 0.0, but a repo-wide search shows no read of options.restitution/self.options.restitution, so any nonzero value is silently ignored. Users enabling IPC restitution for bounces or impact experiments will still get the inelastic IPC result, contrary to the API contract added here.

Useful? React with 👍 / 👎.

ACMLCZH and others added 3 commits June 16, 2026 17:11
The same `centroid + (1.0 - 1e-3) * (verts - centroid)` pattern was duplicated
in two places — `genesis/engine/solvers/rigid/collider/collider.py` (rigid
self-collision filter) and `genesis/engine/couplers/ipc_coupler/coupler.py`
(IPC merged-mesh cache used for neutral overlap detection). The coupler's
comment even said "to match rigid collider's neutral overlap check," tracking
the duplication explicitly.

Extract a small helper next to `are_meshes_overlapping` (the related neutral-
overlap check). Uses `axis=-2, keepdims=True` so the same helper works for both
the collider's batched `(N_envs, N_verts, 3)` input and the coupler's flat
`(N_verts, 3)` input.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- coupler.py mark_abd_updated: normalize scalar qs_idx / dofs_idx the same
  way scalar links_idx is normalized; without this, set_qpos(qs_idx=3)
  reaches the IPC dirty-marking branch as a plain int and raises
  TypeError: 'int' object is not iterable.
- solvers.py IPCCouplerOptions: drop the documented-but-unimplemented
  `restitution` field (no reader exists in the tree; option silently
  accepted any value). Remove the orphaned _post_advance_write_qpos
  docstring paragraph and the two test sites that set it to 0.0.
- test_ipc.py test_cloth_uniform_biaxial_stretching: stop pointing at a
  hardcoded /tmp/grid_c4_parity.obj (which CI cannot find) and pull the
  same mesh from the HF assets dataset as the other cloth tests.
- tests/utils.py: bump HUGGINGFACE_ASSETS_REVISION to 6fb2318, the
  commit that adds IPC/grid21x21_c4.obj. The bump also pulls in a
  microwave URDF/handle update; test_urdf_parsing was re-run and still
  passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
if gs.logger.level <= logging.DEBUG:
uipc.Logger.set_level(uipc.Logger.Level.Info)
uipc.Timer.enable_all()
else:
uipc.Logger.set_level(uipc.Logger.Level.Error)
# TODO: revert to Error after profiling
uipc.Logger.set_level(uipc.Logger.Level.Info)
uipc.Timer.disable_all()

# Create workspace directory for IPC output, named after scene UID.
workspace = os.path.join(tempfile.gettempdir(), f"genesis_ipc_{self.sim.scene.uid.full()}")
os.makedirs(workspace, exist_ok=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: exist_ok=False will crash on re-run or after unclean shutdown

The workspace path is deterministic (genesis_ipc_{scene_uid}). If a previous run crashed without cleaning the temp directory, or if the user re-creates a scene with the same UID, this line raises FileExistsError and the coupler cannot initialize.

Questions:

  1. Was exist_ok=False intentional here (e.g. to detect stale state)? If so, should there be a try/except with a clear error message guiding the user to clean up?
  2. Would shutil.rmtree + os.makedirs be preferable to exist_ok=True, to ensure stale IPC output files from a previous session don't corrupt the new one?
  3. There is also no cleanup at shutdown -- the workspace directory accumulates on disk across runs. Would it make sense to register an atexit handler or add a close() method that calls shutil.rmtree(self._ipc_workspace)?

# Apply geom transform to vertices (geom-local → link-local)
geom_verts = gu.transform_by_trans_quat(geom.init_verts, geom.init_pos, geom.init_quat)
# If from a merged child, transform child-local → parent-local
if frame_pos is not None and np.any(frame_pos) or np.any(frame_quat[1:]):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Operator precedence -- and binds tighter than or

This expression parses as:

(frame_pos is not None and np.any(frame_pos)) or np.any(frame_quat[1:])

rather than the likely intended:

frame_pos is not None and (np.any(frame_pos) or np.any(frame_quat[1:]))

Additionally, frame_pos is initialized on line 452 as a numpy array -- it is never None here, so the is not None check is dead code.

The current expression happens to produce correct results because when frame_pos is all-zero, np.any(frame_pos) is False, so the or falls through to np.any(frame_quat[1:]), which matches the correct logic. But this is a fragile coincidence.

Questions:

  1. Can you confirm the intended semantics? I believe this should simply be:
    if np.any(frame_pos) or np.any(frame_quat[1:]):
  2. Is there any code path where frame_pos could actually be None at this point? If not, the is not None guard should be removed to avoid misleading future readers.

I_j = [i_j, i_b] if qd.static(static_rigid_sim_config.batch_joints_info) else i_j
joint_type = joints_info.type[I_j]

dt = rigid_global_info.substep_dt[None]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Division by dt without zero-guard + interaction with func_implicit_damping

Two related concerns about kernel_restore_integrate:

1. No zero-guard on dt division
Lines 99, 107, 129, 139-140, and 179 all divide by substep_dt without any floor/clamp. A zero or near-zero dt would produce inf/NaN that silently propagates through acc, vel, and into kernel_step_2. The existing func_implicit_damping in this same file uses EPS-clamped denominators as a defensive pattern. Should the same guard be applied here, e.g. dt = qd.max(rigid_global_info.substep_dt[None], EPS)?

2. func_implicit_damping can overwrite IPC-derived acceleration
After kernel_restore_integrate, dofs_state.acc holds the IPC-derived value (vel_ipc - vel_prev) / dt. However, kernel_step_2 subsequently calls func_implicit_damping, which -- for entities with non-zero dofs_info.damping -- solves acc = M_damped^{-1} * force, overwriting the IPC correction. This means if a user's IPC entity has joint damping enabled, IPC's contact resolution is silently discarded.

I understand this is low-risk in practice (IPC scenes typically use free-floating bodies without joint damping), but there is no validation or documentation of this constraint.

Questions:

  1. Is substep_dt == 0 considered unreachable in all configurations? If so, is it worth adding a build-time assertion?
  2. Would it be appropriate to add an assertion or warning (either in the coupler's _setup_coupling_config or at the kernel_restore_integrate call site) that damped DOFs are not used with IPC coupling?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants