Skip to content

Commit 978d0f5

Browse files
committed
Deduplicate roundoff-equivalent contacts
Select one canonical winner when separate reduction slots contain contact geometry that differs only by floating-point rounding. Keep the comparison bounded to the seven winners in each entry.
1 parent ffadc1e commit 978d0f5

2 files changed

Lines changed: 92 additions & 5 deletions

File tree

newton/_src/geometry/contact_reduction_global.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,40 @@ def is_contact_already_exported(
120120
return False
121121

122122

123+
@wp.func
124+
def _floats_are_near_ulps(a: float, b: float) -> bool:
125+
"""Return whether two floats are close in ULPs and absolute magnitude."""
126+
if wp.abs(a - b) > 1.0e-8:
127+
return False
128+
a_bits = float_flip(a)
129+
b_bits = float_flip(b)
130+
if a_bits > b_bits:
131+
return a_bits - b_bits <= wp.uint32(16)
132+
return b_bits - a_bits <= wp.uint32(16)
133+
134+
135+
@wp.func
136+
def _contacts_are_numerically_equivalent(
137+
contact_a: int,
138+
contact_b: int,
139+
position_depth: wp.array[wp.vec4],
140+
normal: wp.array[wp.vec2],
141+
) -> bool:
142+
"""Return whether two buffered contacts differ only by float rounding."""
143+
pd_a = position_depth[contact_a]
144+
pd_b = position_depth[contact_b]
145+
n_a = normal[contact_a]
146+
n_b = normal[contact_b]
147+
return (
148+
_floats_are_near_ulps(pd_a[0], pd_b[0])
149+
and _floats_are_near_ulps(pd_a[1], pd_b[1])
150+
and _floats_are_near_ulps(pd_a[2], pd_b[2])
151+
and _floats_are_near_ulps(pd_a[3], pd_b[3])
152+
and _floats_are_near_ulps(n_a[0], n_b[0])
153+
and _floats_are_near_ulps(n_a[1], n_b[1])
154+
)
155+
156+
123157
@wp.func
124158
def compute_effective_radius(shape_type: int, shape_scale: wp.vec4) -> float:
125159
"""Compute effective radius for a shape based on its type.
@@ -1619,7 +1653,7 @@ def export_reduced_contacts_kernel(
16191653
exported_ids = exported_ids_vec()
16201654
num_exported = int(0)
16211655

1622-
# Read all value slots for this entry (slot-major layout)
1656+
# Read all value slots for this entry (slot-major layout).
16231657
for slot in range(wp.static(VALUES_PER_KEY)):
16241658
value = ht_values[slot * ht_capacity + entry_idx]
16251659

@@ -1638,6 +1672,28 @@ def export_reduced_contacts_kernel(
16381672
exported_ids[num_exported] = contact_id
16391673
num_exported = num_exported + 1
16401674

1675+
# Suppress roundoff-equivalent winners using the lower topology
1676+
# fingerprint. This examines at most 21 pairs per seven-slot entry.
1677+
duplicate_mask = int(0)
1678+
for local_idx in range(num_exported):
1679+
contact_id = exported_ids[local_idx]
1680+
fingerprint = contact_fingerprints[contact_id]
1681+
for other_idx in range(local_idx):
1682+
other_id = exported_ids[other_idx]
1683+
if _contacts_are_numerically_equivalent(contact_id, other_id, position_depth, normal):
1684+
other_fingerprint = contact_fingerprints[other_id]
1685+
if fingerprint < other_fingerprint:
1686+
duplicate_mask = duplicate_mask | (1 << other_idx)
1687+
else:
1688+
duplicate_mask = duplicate_mask | (1 << local_idx)
1689+
1690+
for local_idx in range(num_exported):
1691+
if duplicate_mask & (1 << local_idx) != 0:
1692+
continue
1693+
1694+
contact_id = exported_ids[local_idx]
1695+
fingerprint = contact_fingerprints[contact_id]
1696+
16411697
# Cross-entry dedup: same contact can win slots in different entries
16421698
# (e.g., normal-bin AND voxel entry). Atomic flag per contact_id.
16431699
old_flag = wp.atomic_add(exported_flags, contact_id, 1)
@@ -1677,7 +1733,7 @@ def export_reduced_contacts_kernel(
16771733
contact_data.shape_a = shape_a
16781734
contact_data.shape_b = shape_b
16791735
contact_data.gap_sum = gap_sum
1680-
contact_data.sort_sub_key = contact_fingerprints[contact_id]
1736+
contact_data.sort_sub_key = fingerprint
16811737

16821738
# Call the writer function
16831739
writer_func(contact_data, writer_data, -1)

newton/tests/test_contact_reduction_global.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
GlobalContactReducer,
1616
GlobalContactReducerData,
1717
_make_contact_value_det,
18+
_make_contact_value_fast,
1819
_make_preprune_probe_det,
1920
_unpack_contact_id_det,
2021
create_export_reduced_contacts_kernel,
@@ -23,8 +24,10 @@
2324
export_and_reduce_contact,
2425
export_and_reduce_contact_centered,
2526
export_and_reduce_contact_centered_two_spatial_depths,
27+
export_contact_to_buffer,
2628
make_contact_key,
2729
)
30+
from newton._src.geometry.hashtable import hashtable_find_or_insert
2831
from newton._src.geometry.narrow_phase import ContactWriterData
2932
from newton.tests.unittest_utils import add_function_test, get_test_devices
3033

@@ -539,6 +542,34 @@ def store_contacts_kernel(
539542
device=device,
540543
)
541544

545+
@wp.kernel
546+
def store_roundoff_duplicate_winners_kernel(reducer_data: GlobalContactReducerData):
547+
contact_a = export_contact_to_buffer(
548+
shape_a=10,
549+
shape_b=110,
550+
position=wp.vec3(0.01, 0.02, 0.03),
551+
normal=wp.vec3(0.0, 1.0, 0.0),
552+
depth=-0.01,
553+
fingerprint=10,
554+
reducer_data=reducer_data,
555+
)
556+
contact_b = export_contact_to_buffer(
557+
shape_a=10,
558+
shape_b=110,
559+
position=wp.vec3(0.010000000707805157, 0.02, 0.03),
560+
normal=wp.vec3(0.0, 1.0, 0.0),
561+
depth=-0.01,
562+
fingerprint=20,
563+
reducer_data=reducer_data,
564+
)
565+
entry_idx = hashtable_find_or_insert(
566+
make_contact_key(10, 110, 0), reducer_data.ht_keys, reducer_data.ht_active_slots
567+
)
568+
reducer_data.ht_values[entry_idx] = _make_contact_value_fast(1.0, 10, contact_a)
569+
reducer_data.ht_values[reducer_data.ht_capacity + entry_idx] = _make_contact_value_fast(1.0, 20, contact_b)
570+
571+
wp.launch(store_roundoff_duplicate_winners_kernel, dim=1, inputs=[reducer_data], device=device)
572+
542573
# Prepare output buffers
543574
max_output = 100
544575
contact_pair_out = wp.zeros(max_output, dtype=wp.vec2i, device=device)
@@ -594,10 +625,10 @@ def store_contacts_kernel(
594625
device=device,
595626
)
596627

597-
# Verify output - should have exported all unique winners
628+
# Verify output: five distinct pairs plus one representative from the
629+
# numerically equivalent winner pair.
598630
num_exported = int(contact_count_out.numpy()[0])
599-
600-
test.assertGreater(num_exported, 0)
631+
test.assertEqual(num_exported, 6)
601632

602633

603634
def test_key_uniqueness(test, device):

0 commit comments

Comments
 (0)