Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b135f84
SAP-style IPC coupling: predicted-pose coupling + velocity output
ACMLCZH Mar 5, 2026
ad8cbca
Update two way
ACMLCZH Mar 5, 2026
b3cf8b8
Update unittest
ACMLCZH Mar 5, 2026
4936854
Update collider logic
ACMLCZH Mar 5, 2026
d95ba5c
Update set qpos
ACMLCZH Mar 6, 2026
67ce1bb
Merge upstream/main into feature/ipc-sap-style-coupling
ACMLCZH Mar 6, 2026
ed4dc07
Update review
ACMLCZH Mar 6, 2026
f4fa441
Update test_ipc.py
ACMLCZH Mar 6, 2026
d4757f2
Update coupler.py
ACMLCZH Mar 6, 2026
0b8fa5a
Update refactor
ACMLCZH Mar 7, 2026
58328f8
update review
ACMLCZH Mar 7, 2026
29b64d4
Update review
ACMLCZH Mar 7, 2026
78f9d37
Merge branch 'main' into feature/ipc-sap-style-coupling
ACMLCZH Mar 7, 2026
0508682
Update cloth
ACMLCZH Mar 7, 2026
4c1110a
Address PR review: restore set_qpos/set_dofs_position, shared utils, …
ACMLCZH Mar 8, 2026
5eb3b5b
Revert conftest.py changes
ACMLCZH Mar 8, 2026
2608dce
Move constraint_strength to per-entity coup_stiffness on RigidMaterial
ACMLCZH Mar 8, 2026
7a75fb1
Add set_qpos/set_dofs_position tests, fix CUDA device and overlap shr…
ACMLCZH Mar 8, 2026
0cea049
Use named friction constants and add force estimate FIXME in biaxial …
ACMLCZH Mar 8, 2026
5afdc45
Add traceability comments for test tolerance and parameter changes
ACMLCZH Mar 8, 2026
9afb8d0
Minor cleanup.
duburcqa Mar 9, 2026
a669b08
Add IPC restitution with one-shot impulse to fix per-frame compounding
ACMLCZH Mar 9, 2026
1a718d5
Per-frame restitution + per-link ABD dirty tracking
ACMLCZH Mar 10, 2026
d592bed
Update ipc_abd_momentum.py
ACMLCZH Mar 10, 2026
aa0ff0a
Update ipc_abd_momentum.py
ACMLCZH Mar 10, 2026
747c881
Update grasp abd
ACMLCZH Mar 11, 2026
9fbff06
primary fix
alanray-tech Mar 11, 2026
980430a
Merge branch 'feature/ipc-sap-style-coupling' of https://github.com/A…
alanray-tech Mar 11, 2026
a1749f9
fix format
alanray-tech Mar 11, 2026
06f2c66
Update init - build
ACMLCZH Mar 12, 2026
f6166f6
Merge remote-tracking branch 'upstream/main' into feature/ipc-sap-sty…
ACMLCZH Mar 12, 2026
deacd6d
Refactor: move heterogeneous variant tracking from Entity to Link
ACMLCZH Mar 12, 2026
e4b9340
Merge branch 'refactor/move-heterogeneous-variants-to-link' into feat…
ACMLCZH Mar 12, 2026
508afaf
Merge upstream/main: resolve collider and IPC coupler options conflicts
ACMLCZH Mar 13, 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
2 changes: 1 addition & 1 deletion examples/IPC_Solver/ipc_objects_falling.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def main():
)

# Ground plane
scene.add_entity(gs.morphs.Plane())
scene.add_entity(gs.morphs.Plane(), material=gs.materials.Rigid(coup_type="ipc_only"))
Comment thread
ACMLCZH marked this conversation as resolved.
Outdated

# Cloth using Cloth material
# Note: Using coarse grid mesh to avoid IPC thickness violations
Expand Down
2 changes: 1 addition & 1 deletion examples/IPC_Solver/ipc_robot_cloth_teleop.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def main():
)

# Add flat floor
scene.add_entity(gs.morphs.Plane())
scene.add_entity(gs.morphs.Plane(), material=gs.materials.Rigid(coup_type="ipc_only"))

# Add Franka robot
franka_material_kwargs = dict(
Expand Down
2 changes: 1 addition & 1 deletion examples/IPC_Solver/ipc_robot_grasp_cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def main():
show_viewer=args.vis,
)

scene.add_entity(gs.morphs.Plane())
scene.add_entity(gs.morphs.Plane(), material=gs.materials.Rigid(coup_type="ipc_only"))

franka_material_kwargs = dict(
coup_friction=0.8,
Expand Down
189 changes: 103 additions & 86 deletions genesis/engine/couplers/ipc_coupler/coupler.py

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions genesis/engine/couplers/ipc_coupler/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,13 @@ class ArticulatedEntityData:

articulation_slots: list[GeometrySlot]

ref_dof_prev: np.ndarray
qpos_stored: np.ndarray
qpos_current: np.ndarray
qpos_new: np.ndarray
delta_theta_tilde: np.ndarray
delta_theta_ipc: np.ndarray

# Previous timestep link transforms for ref_dof_prev computation {(joint, env_idx): transform_matrix_4x4}
# Previous timestep link transforms for IPC ABD ref_dof_prev sync {(joint, env_idx): transform_matrix_4x4}
Comment thread
ACMLCZH marked this conversation as resolved.
Outdated
prev_links_transform: list[list[np.ndarray | None]]


Expand Down
8 changes: 3 additions & 5 deletions genesis/engine/materials/rigid.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,12 @@ class Rigid(Material):
Compensation factor for gravity. 1.0 cancels gravity. Default is 0.
coup_type : str or None, optional
Coupling mode for this entity. Only used by the IPC coupler. Requires ``needs_coup=True``.
If None, auto-selected based on entity type: ``'external_articulation'`` for fixed-base
articulated robots, ``'two_way_soft_constraint'`` for floating-base robots, and
``'ipc_only'`` for non-articulated objects. Valid values:
Must be explicitly set when ``needs_coup=True``. Valid values:
- 'two_way_soft_constraint': Two-way soft coupling.
- 'external_articulation': Joint-level coupling for articulated bodies. Joint positions will be coupled at
the DOF level.
- 'ipc_only': IPC controls entity, transforms copied to Genesis (one-way). Only supported by rigid
non-articulated objects.
- 'ipc_only': IPC controls entity, transforms copied to Genesis (one-way). Not supported for
entities with constrained joints (revolute, prismatic, etc.).
Comment thread
ACMLCZH marked this conversation as resolved.
Outdated
Default is None.
coup_links : tuple of str or None, optional
Tuple of link names to include in coupling. When set, only the named links participate
Expand Down
221 changes: 214 additions & 7 deletions genesis/engine/solvers/rigid/abd/forward_dynamics.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,126 @@


@qd.kernel
def update_qacc_from_qvel_delta(
def kernel_restore_integrate(
Comment thread
ACMLCZH marked this conversation as resolved.
dofs_state: array_class.DofsState,
links_info: array_class.LinksInfo,
joints_info: array_class.JointsInfo,
rigid_global_info: array_class.RigidGlobalInfo,
static_rigid_sim_config: qd.template(),
is_backward: qd.template(),
restore_qpos: qd.template(),
):
BW = qd.static(is_backward)

n_dofs = dofs_state.ctrl_mode.shape[0]
_B = dofs_state.ctrl_mode.shape[1]

# Phase 1 (when restore_qpos): vel = (qpos - qpos_prev) / dt, then qpos = qpos_prev
if qd.static(restore_qpos):
qd.loop_config(serialize=static_rigid_sim_config.para_level < gs.PARA_LEVEL.ALL)
for i_0, i_b in (
(qd.ndrange(1, _B))
if qd.static(static_rigid_sim_config.use_hibernation)
else (qd.ndrange(links_info.root_idx.shape[0], _B))
):
for i_1 in (
(
range(rigid_global_info.n_awake_links[i_b])
if qd.static(static_rigid_sim_config.use_hibernation)
else qd.static(range(1))
)
if qd.static(not BW)
else (
qd.static(range(static_rigid_sim_config.max_n_awake_links))
if qd.static(static_rigid_sim_config.use_hibernation)
else qd.static(range(1))
)
):
if func_check_index_range(
i_1, 0, rigid_global_info.n_awake_links[i_b], static_rigid_sim_config.use_hibernation
):
i_l = (
rigid_global_info.awake_links[i_1, i_b]
if qd.static(static_rigid_sim_config.use_hibernation)
else i_0
)
I_l = [i_l, i_b] if qd.static(static_rigid_sim_config.batch_links_info) else i_l
if links_info.n_dofs[I_l] > 0:
EPS = rigid_global_info.EPS[None]

dof_start = links_info.dof_start[I_l]
q_start = links_info.q_start[I_l]
q_end = links_info.q_end[I_l]

i_j = links_info.joint_start[I_l]
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]

# Back-compute vel from qpos delta, then restore qpos
if joint_type == gs.JOINT_TYPE.FREE:
for j in qd.static(range(3)):
dofs_state.vel[dof_start + j, i_b] = (
rigid_global_info.qpos[q_start + j, i_b]
- rigid_global_info.qpos_prev[q_start + j, i_b]
) / dt
if joint_type == gs.JOINT_TYPE.SPHERICAL or joint_type == gs.JOINT_TYPE.FREE:
rot_offset = 3 if joint_type == gs.JOINT_TYPE.FREE else 0
quat_new = qd.Vector(
[
rigid_global_info.qpos[q_start + rot_offset + 0, i_b],
rigid_global_info.qpos[q_start + rot_offset + 1, i_b],
rigid_global_info.qpos[q_start + rot_offset + 2, i_b],
rigid_global_info.qpos[q_start + rot_offset + 3, i_b],
]
)
quat_prev = qd.Vector(
[
rigid_global_info.qpos_prev[q_start + rot_offset + 0, i_b],
rigid_global_info.qpos_prev[q_start + rot_offset + 1, i_b],
rigid_global_info.qpos_prev[q_start + rot_offset + 2, i_b],
rigid_global_info.qpos_prev[q_start + rot_offset + 3, i_b],
]
)
qrot = gu.qd_transform_quat_by_quat(quat_new, gu.qd_inv_quat(quat_prev))
ang = gu.qd_quat_to_rotvec(qrot, EPS)
for j in qd.static(range(3)):
dofs_state.vel[dof_start + rot_offset + j, i_b] = ang[j] / dt
else:
for j_ in (
(range(q_end - q_start))
if qd.static(not BW)
else (qd.static(range(static_rigid_sim_config.max_n_qs_per_link)))
):
j = q_start + j_
if j < q_end:
dofs_state.vel[dof_start + j_, i_b] = (
rigid_global_info.qpos[j, i_b] - rigid_global_info.qpos_prev[j, i_b]
) / dt

# Restore qpos
for j_ in (
(range(q_end - q_start))
if qd.static(not BW)
else (qd.static(range(static_rigid_sim_config.max_n_qs_per_link)))
):
j = q_start + j_
if j < q_end:
rigid_global_info.qpos[j, i_b] = rigid_global_info.qpos_prev[j, i_b]

# Phase 2: acc = (vel - vel_prev) / dt; vel = vel_prev
n_dofs = dofs_state.ctrl_mode.shape[0]
qd.loop_config(serialize=static_rigid_sim_config.para_level < gs.PARA_LEVEL.ALL)
for i_0, i_b in qd.ndrange(1, _B) if qd.static(static_rigid_sim_config.use_hibernation) else qd.ndrange(n_dofs, _B):
for i_1 in (
(
# Dynamic inner loop for forward pass
range(rigid_global_info.n_awake_dofs[i_b])
if qd.static(static_rigid_sim_config.use_hibernation)
else qd.static(range(1))
)
if qd.static(not BW)
else (
qd.static(range(static_rigid_sim_config.max_n_awake_dofs)) # Static inner loop for backward pass
qd.static(range(static_rigid_sim_config.max_n_awake_dofs))
Comment thread
ACMLCZH marked this conversation as resolved.
if qd.static(static_rigid_sim_config.use_hibernation)
else qd.static(range(1))
)
Expand All @@ -67,29 +164,32 @@ def update_qacc_from_qvel_delta(


@qd.kernel
def update_qvel(
def kernel_predict_integrate(
Comment thread
ACMLCZH marked this conversation as resolved.
dofs_state: array_class.DofsState,
links_info: array_class.LinksInfo,
joints_info: array_class.JointsInfo,
rigid_global_info: array_class.RigidGlobalInfo,
static_rigid_sim_config: qd.template(),
is_backward: qd.template(),
update_qpos: qd.template(),
):
BW = qd.static(is_backward)

_B = dofs_state.vel.shape[1]
n_dofs = dofs_state.vel.shape[0]

# Phase 1: vel_prev = vel; vel = vel + acc * dt
qd.loop_config(serialize=static_rigid_sim_config.para_level < gs.PARA_LEVEL.ALL)
for i_0, i_b in qd.ndrange(1, _B) if qd.static(static_rigid_sim_config.use_hibernation) else qd.ndrange(n_dofs, _B):
for i_1 in (
(
# Dynamic inner loop for forward pass
range(rigid_global_info.n_awake_dofs[i_b])
if qd.static(static_rigid_sim_config.use_hibernation)
else qd.static(range(1))
)
if qd.static(not BW)
else (
qd.static(range(static_rigid_sim_config.max_n_awake_dofs)) # Static inner loop for backward pass
qd.static(range(static_rigid_sim_config.max_n_awake_dofs))
if qd.static(static_rigid_sim_config.use_hibernation)
Comment thread
ACMLCZH marked this conversation as resolved.
else qd.static(range(1))
)
Expand All @@ -105,6 +205,113 @@ def update_qvel(
dofs_state.vel[i_d, i_b] + dofs_state.acc[i_d, i_b] * rigid_global_info.substep_dt[None]
)

# Phase 2: qpos_prev = qpos; qpos = qpos + vel * dt (with quaternion handling)
if qd.static(update_qpos):
qd.loop_config(serialize=static_rigid_sim_config.para_level < gs.PARA_LEVEL.ALL)
for i_0, i_b in (
(qd.ndrange(1, _B))
if qd.static(static_rigid_sim_config.use_hibernation)
else (qd.ndrange(links_info.root_idx.shape[0], _B))
):
for i_1 in (
(
range(rigid_global_info.n_awake_links[i_b])
if qd.static(static_rigid_sim_config.use_hibernation)
else qd.static(range(1))
)
if qd.static(not BW)
else (
qd.static(range(static_rigid_sim_config.max_n_awake_links))
if qd.static(static_rigid_sim_config.use_hibernation)
else qd.static(range(1))
)
):
if func_check_index_range(
i_1, 0, rigid_global_info.n_awake_links[i_b], static_rigid_sim_config.use_hibernation
):
i_l = (
rigid_global_info.awake_links[i_1, i_b]
if qd.static(static_rigid_sim_config.use_hibernation)
else i_0
)
I_l = [i_l, i_b] if qd.static(static_rigid_sim_config.batch_links_info) else i_l
if links_info.n_dofs[I_l] > 0:
EPS = rigid_global_info.EPS[None]

dof_start = links_info.dof_start[I_l]
q_start = links_info.q_start[I_l]
q_end = links_info.q_end[I_l]

i_j = links_info.joint_start[I_l]
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]

# Save qpos to qpos_prev
for j_ in (
(range(q_end - q_start))
if qd.static(not BW)
else (qd.static(range(static_rigid_sim_config.max_n_qs_per_link)))
):
j = q_start + j_
if j < q_end:
rigid_global_info.qpos_prev[j, i_b] = rigid_global_info.qpos[j, i_b]

# Compute predicted qpos (mirrors func_integrate Phase 2)
if joint_type == gs.JOINT_TYPE.FREE:
pos = qd.Vector(
[
rigid_global_info.qpos[q_start, i_b],
rigid_global_info.qpos[q_start + 1, i_b],
rigid_global_info.qpos[q_start + 2, i_b],
]
)
vel = qd.Vector(
[
dofs_state.vel[dof_start, i_b],
dofs_state.vel[dof_start + 1, i_b],
dofs_state.vel[dof_start + 2, i_b],
]
)
pos = pos + vel * rigid_global_info.substep_dt[None]
for j in qd.static(range(3)):
rigid_global_info.qpos[q_start + j, i_b] = pos[j]
if joint_type == gs.JOINT_TYPE.SPHERICAL or joint_type == gs.JOINT_TYPE.FREE:
rot_offset = 3 if joint_type == gs.JOINT_TYPE.FREE else 0
rot0 = qd.Vector(
[
rigid_global_info.qpos[q_start + rot_offset + 0, i_b],
rigid_global_info.qpos[q_start + rot_offset + 1, i_b],
rigid_global_info.qpos[q_start + rot_offset + 2, i_b],
rigid_global_info.qpos[q_start + rot_offset + 3, i_b],
]
)
ang = (
qd.Vector(
[
dofs_state.vel[dof_start + rot_offset + 0, i_b],
dofs_state.vel[dof_start + rot_offset + 1, i_b],
dofs_state.vel[dof_start + rot_offset + 2, i_b],
]
)
* rigid_global_info.substep_dt[None]
)
qrot = gu.qd_rotvec_to_quat(ang, EPS)
rot = gu.qd_transform_quat_by_quat(qrot, rot0)
for j in qd.static(range(4)):
rigid_global_info.qpos[q_start + j + rot_offset, i_b] = rot[j]
else:
for j_ in (
(range(q_end - q_start))
if qd.static(not BW)
else (qd.static(range(static_rigid_sim_config.max_n_qs_per_link)))
):
j = q_start + j_
if j < q_end:
rigid_global_info.qpos[j, i_b] = (
rigid_global_info.qpos[j, i_b]
+ dofs_state.vel[dof_start + j_, i_b] * rigid_global_info.substep_dt[None]
)


@qd.kernel(fastcache=gs.use_fastcache)
def kernel_compute_mass_matrix(
Expand Down
Loading
Loading