Skip to content

Commit 724c720

Browse files
committed
Skip pruning for degenerated contact buckets.
1 parent bed71df commit 724c720

3 files changed

Lines changed: 77 additions & 26 deletions

File tree

genesis/engine/solvers/rigid/collider/collider.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ def __init__(self, rigid_solver: "RigidSolver"):
8989
self._diff_pos_tolerance = 1e-2
9090
self._diff_normal_tolerance = 1e-2
9191
self._prune_deep_penetration_ratio = 3.0
92+
# Multiplier on the link-pair effective inertia radius squared (invweight_trans / invweight_rot) that defines
93+
# the support-polygon area threshold below which the bucket's hull pruning is skipped (all hull-dropped contacts
94+
# restored). Catches buckets too thin to absorb the first-order-Taylor bias in perturbed contact normals.
95+
self._prune_degenerate_area_ratio = 0.2
9296

9397
self._init_static_config()
9498
self._use_split_narrowphase = (
@@ -240,6 +244,7 @@ def _init_collision_fields(self) -> None:
240244
diff_normal_tolerance=self._diff_normal_tolerance,
241245
contact_pruning_tolerance=self._solver._options.contact_pruning_tolerance or 0.0,
242246
prune_deep_penetration_ratio=self._prune_deep_penetration_ratio,
247+
prune_degenerate_area_ratio=self._prune_degenerate_area_ratio,
243248
)
244249
self._init_collision_pair_idx(self._collision_pair_idx)
245250
self._init_valid_pairs()
@@ -860,6 +865,7 @@ def detection(self) -> None:
860865
)
861866

862867
func_clamp_prune_and_sort_contacts(
868+
self._solver.links_info,
863869
self._collider_state,
864870
self._collider_info,
865871
self._solver._rigid_global_info,

genesis/engine/solvers/rigid/collider/contact.py

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ def func_rotate_frame(
538538

539539
@qd.kernel(fastcache=True)
540540
def func_clamp_prune_and_sort_contacts(
541+
links_info: array_class.LinksInfo,
541542
collider_state: array_class.ColliderState,
542543
collider_info: array_class.ColliderInfo,
543544
rigid_global_info: array_class.RigidGlobalInfo,
@@ -582,6 +583,7 @@ def func_clamp_prune_and_sort_contacts(
582583
max_contact_pairs = collider_info.max_contact_pairs[None]
583584
tol = collider_info.contact_pruning_tolerance[None]
584585
prune_deep_penetration_ratio = collider_info.prune_deep_penetration_ratio[None]
586+
prune_degenerate_area_ratio = collider_info.prune_degenerate_area_ratio[None]
585587
LP_KEY_STRIDE = gs.qd_float(1.0e7)
586588
EPS = rigid_global_info.EPS[None]
587589

@@ -739,23 +741,23 @@ def func_clamp_prune_and_sort_contacts(
739741
vy = mnz * ux - mnx * uz
740742
vz = mnx * uy - mny * ux
741743

742-
# Project bucket contacts to (u, v). sort_key holds u, contact_proj_v holds v. Both
743-
# are indexed by bucket-logical position so the (u, v) sort below can read them without
744-
# another indirection.
744+
# Project bucket contacts to (u, v). sort_key holds u, contact_proj_v holds v. Both are
745+
# indexed by bucket-logical position so the (u, v) sort below can read them without another
746+
# indirection.
745747
for i in range(b_start, b_end):
746748
phys_i = collider_state.contact_sort_idx[i, i_b]
747749
p_i = collider_state.contact_data.pos[phys_i, i_b]
748750
collider_state.contact_sort_key[i, i_b] = p_i[0] * ux + p_i[1] * uy + p_i[2] * uz
749751
collider_state.contact_proj_v[i, i_b] = p_i[0] * vx + p_i[1] * vy + p_i[2] * vz
750752

751-
# Sort bucket positions lexicographically by (u, v), with a tolerance on u so that
752-
# contacts whose u values differ only by float noise (or by sub-millimeter physics noise
753-
# from MPR perturbations) sort by v. Without the tolerance, the wrong point pops from a
754-
# 3-collinear triplet when the corner and the mid-edge have u values that differ by a
755-
# few microns and the mid-edge happens to sort first.
753+
# Sort bucket positions lexicographically by (u, v), with a tolerance on u so that contacts
754+
# whose u values differ only by float noise (or by sub-millimeter physics noise from MPR
755+
# perturbations) sort by v. Without the tolerance, the wrong point pops from a 3-collinear
756+
# triplet when the corner and the mid-edge have u values that differ by a few microns and
757+
# the mid-edge happens to sort first.
756758
#
757-
# The permutation lives in contact_keep[b_start..b_end). contact_keep is rewritten with
758-
# the final keep flags below before this bucket exits, so reusing it as scratch is safe.
759+
# The permutation lives in contact_keep[b_start..b_end). contact_keep is rewritten with the
760+
# final keep flags below before this bucket exits, so reusing it as scratch is safe.
759761
sort_u_tol = gs.qd_float(1e-3) * qd.sqrt(max_in_plane_r2)
760762
for i in range(b_start, b_end):
761763
collider_state.contact_keep[i, i_b] = i
@@ -774,10 +776,9 @@ def func_clamp_prune_and_sort_contacts(
774776
j -= 1
775777
collider_state.contact_keep[j + 1, i_b] = ci
776778

777-
# Collinearity threshold for hull pops, scaled to the bucket extent. A pure
778-
# "cross <= 0" check fails on numerically-near-collinear edge points (cross is a tiny
779-
# positive epsilon from float roundoff), so genuine midpoints would survive as spurious
780-
# hull vertices.
779+
# Collinearity threshold for hull pops, scaled to the bucket extent. A pure "cross <= 0"
780+
# check fails on numerically-near-collinear edge points (cross is a tiny positive epsilon
781+
# from float roundoff), so genuine midpoints would survive as spurious hull vertices.
781782
hull_collinear_tol = tol * max_in_plane_r2
782783

783784
# Andrew's monotone chain. The (u, v) permutation lives in contact_keep; the hull stack
@@ -828,13 +829,12 @@ def func_clamp_prune_and_sort_contacts(
828829
k -= 1
829830
else:
830831
break
831-
# The closing iteration of the upper hull visits the leftmost point, which already
832-
# sits at stack[b_start] from the lower hull. Skipping that push, plus the
833-
# k < b_size guard, bounds k to b_size and keeps the write index within
834-
# max_contact_pairs even for buckets where the lower-hull pass already kept all
835-
# b_size points (downward-convex layouts: every lex-sorted triple makes a left turn
836-
# so nothing gets popped, then the upper-hull pass tries to push a duplicate of an
837-
# already-kept lower-hull vertex).
832+
# The closing iteration of the upper hull visits the leftmost point, which already sits
833+
# at stack[b_start] from the lower hull. Skipping that push, plus the k < b_size guard,
834+
# bounds k to b_size and keeps the write index within max_contact_pairs even for buckets
835+
# where the lower-hull pass already kept all b_size points (downward-convex layouts:
836+
# every lex-sorted triple makes a left turn so nothing gets popped, then the upper-hull
837+
# pass tries to push a duplicate of an already-kept lower-hull vertex).
838838
if ci != collider_state.contact_hull_stack[b_start, i_b] and k < b_size:
839839
collider_state.contact_hull_stack[b_start + k, i_b] = ci
840840
k += 1
@@ -851,12 +851,12 @@ def func_clamp_prune_and_sort_contacts(
851851
# average. The support-polygon argument says interior contacts are wrench-redundant only
852852
# when ALL contacts share the same normal and penetration; a contact with substantially
853853
# higher penetration than the hull's average represents a distinct physical support (the
854-
# body of a fork resting beyond its tines, the deep middle of a long body) and dropping
855-
# it lets the body sink into the surface. The 3x factor is well above the typical ~1.x
854+
# body of a fork resting beyond its tines, the deep middle of a long body) and dropping it
855+
# lets the body sink into the surface. The 3x factor is well above the typical ~1.x
856856
# penetration spread on transient/rocking faces (so non-uniform-penetration buckets like
857-
# irregular mesh contacts keep only the hull) but well below the deep interior
858-
# penetrations seen when a non-flat body rests inside its convex envelope (so genuine
859-
# deep supports are restored).
857+
# irregular mesh contacts keep only the hull) but well below the deep interior penetrations
858+
# seen when a non-flat body rests inside its convex envelope (so genuine deep supports are
859+
# restored).
860860
hull_pen_max = gs.qd_float(0.0)
861861
for hk in range(k):
862862
survivor = collider_state.contact_hull_stack[b_start + hk, i_b]
@@ -871,6 +871,49 @@ def func_clamp_prune_and_sort_contacts(
871871
if collider_state.contact_data.penetration[phys_i, i_b] > deep_keep_threshold:
872872
collider_state.contact_keep[i, i_b] = 1
873873

874+
# Support-polygon degeneracy gate. When the hull polygon's surface area is too small to
875+
# absorb the first-order-Taylor bias in perturbed contact normals (each contact's bias
876+
# creates a horizontal force ~ N * mc_perturbation^2; if the polygon is thin the moment arms
877+
# align rather than cancel and the LCP drifts), restore every contact the hull dropped, i.e.
878+
# skip pruning for this bucket. Closed-form shoelace area on the hull stack
879+
# contact_hull_stack[b_start..b_start+k); The threshold is the link-pair effective inertia
880+
# radius squared (invweight_trans / invweight_rot): mass cancels out, so the ratio is a
881+
# shape-and-density-distribution quantity.
882+
hull_area = gs.qd_float(0.0)
883+
if k >= 3:
884+
for i in range(k):
885+
a_pos = collider_state.contact_hull_stack[b_start + i, i_b]
886+
j = i + 1
887+
if j == k:
888+
j = 0
889+
b_pos = collider_state.contact_hull_stack[b_start + j, i_b]
890+
au = collider_state.contact_sort_key[a_pos, i_b]
891+
av = collider_state.contact_proj_v[a_pos, i_b]
892+
bu = collider_state.contact_sort_key[b_pos, i_b]
893+
bv = collider_state.contact_proj_v[b_pos, i_b]
894+
hull_area = hull_area + au * bv - bu * av
895+
hull_area = 0.5 * qd.abs(hull_area)
896+
I_la0 = (la0, i_b) if qd.static(static_rigid_sim_config.batch_links_info) else la0
897+
I_lb0 = (lb0, i_b) if qd.static(static_rigid_sim_config.batch_links_info) else lb0
898+
invw_ratio = gs.qd_float(0.0)
899+
if links_info.is_fixed[I_la0]:
900+
invw_ratio = links_info.invweight[I_lb0][0] / qd.max(
901+
links_info.invweight[I_lb0][1], EPS
902+
)
903+
elif links_info.is_fixed[I_lb0]:
904+
invw_ratio = links_info.invweight[I_la0][0] / qd.max(
905+
links_info.invweight[I_la0][1], EPS
906+
)
907+
else:
908+
invw_ratio = min(
909+
links_info.invweight[I_la0][0] / qd.max(links_info.invweight[I_la0][1], EPS),
910+
links_info.invweight[I_lb0][0] / qd.max(links_info.invweight[I_lb0][1], EPS),
911+
)
912+
if hull_area < prune_degenerate_area_ratio * invw_ratio:
913+
for i in range(b_start, b_end):
914+
if collider_state.contact_keep[i, i_b] == 0:
915+
collider_state.contact_keep[i, i_b] = 1
916+
874917
b_start = b_end
875918

876919
# Phase 3: compact contact_sort_idx by squeezing out dropped slots.

genesis/utils/array_class.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,7 @@ class ColliderInfo:
792792
# link-pair contact pruning
793793
contact_pruning_tolerance: qd.Tensor
794794
prune_deep_penetration_ratio: qd.Tensor
795+
prune_degenerate_area_ratio: qd.Tensor
795796

796797

797798
def get_collider_info(solver, n_vert_neighbors, n_valid_pairs, collider_static_config, **kwargs):
@@ -824,6 +825,7 @@ def get_collider_info(solver, n_vert_neighbors, n_valid_pairs, collider_static_c
824825
diff_normal_tolerance=V_SCALAR_FROM(dtype=gs.qd_float, value=kwargs["diff_normal_tolerance"]),
825826
contact_pruning_tolerance=V_SCALAR_FROM(dtype=gs.qd_float, value=kwargs["contact_pruning_tolerance"]),
826827
prune_deep_penetration_ratio=V_SCALAR_FROM(dtype=gs.qd_float, value=kwargs["prune_deep_penetration_ratio"]),
828+
prune_degenerate_area_ratio=V_SCALAR_FROM(dtype=gs.qd_float, value=kwargs["prune_degenerate_area_ratio"]),
827829
)
828830

829831

0 commit comments

Comments
 (0)