@@ -538,6 +538,7 @@ def func_rotate_frame(
538538
539539@qd .kernel (fastcache = True )
540540def 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.
0 commit comments