Skip to content

Opt-in deterministic contacts#2409

Merged
eric-heiden merged 30 commits into
newton-physics:mainfrom
nvtw:dev/tw3/deterministic_contacts
Apr 14, 2026
Merged

Opt-in deterministic contacts#2409
eric-heiden merged 30 commits into
newton-physics:mainfrom
nvtw:dev/tw3/deterministic_contacts

Conversation

@nvtw

@nvtw nvtw commented Apr 10, 2026

Copy link
Copy Markdown
Member

Description

Adds an option to run contact detection deterministically to get bit-identical contacts when the contact detection is run multiple times on the same input.

Two mechanisms are introduced:

  • Fingerprint tiebreaking — each contact carries a geometry-derived fingerprint (triangle/edge index) that is encoded into the atomic_max contact-reduction value, so the reduction winner is independent of GPU thread scheduling.
  • Radix sort — after the narrow phase, all contact arrays are reordered by a 64-bit key encoding (shape_a, shape_b, sub_key) via a radix sort + gather pass, producing a canonical contact order.

Key changes:

  • ContactData gains a sort_sub_key field propagated through all collision paths (convex, SDF, mesh-plane, mesh-triangle).
  • contact_reduction_global.py introduces dual packing modes (fast / deterministic) for the hashtable atomic_max values, switchable via enable_deterministic_contact_packing().
  • New ContactSorter class (contact_sort.py) performs allocation-free, CUDA-graph-capturable radix sort + gather over the contact buffer.
  • CollisionPipeline and NarrowPhase accept a deterministic=True flag that wires everything together.

Hydroelastic contacts are currently not yet deterministic. That will come in a future MR.

Checklist

  • New or existing tests cover these changes
  • The documentation is up to date with these changes
  • CHANGELOG.md has been updated (if user-facing change)

Test plan

uv run --extra dev -m newton.tests -k TestFloatFlipPrecision
uv run --extra dev -m newton.tests -k test_contact_reduction_global

New feature / API change

import newton

builder = newton.ModelBuilder()
# ... build scene ...
model = builder.finalize()

pipeline = newton.CollisionPipeline(model, deterministic=True)
contacts = pipeline.contacts()

state = model.state()
pipeline.collide(state, contacts)
# contacts are now in a canonical, reproducible order

Summary by CodeRabbit

  • New Features

    • Added a global "deterministic" mode to produce reproducible contact ordering across runs (fingerprint tie‑breaking + post‑narrow‑phase radix sort and gather).
    • Exposed stable per-contact sort keys and a contact sorter to reorder contact arrays deterministically.
  • Tests

    • Added comprehensive unit and end‑to‑end tests validating deterministic packing, sort key ordering, and repeatable reduction results.
  • Documentation

    • Added “Deterministic Contact Ordering” guide describing usage, overhead, and current hydroelastic limitation.

@nvtw nvtw self-assigned this Apr 10, 2026
@nvtw nvtw marked this pull request as draft April 10, 2026 15:12
@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds deterministic contact ordering: per-contact sort sub-keys and 64-bit sort keys, fingerprint-based deterministic contact packing/reduction, a radix-sort-based ContactSorter, and integration across narrow-phase, reduction, and collision pipeline to produce reproducible contact outputs.

Changes

Cohort / File(s) Summary
Contact Data & Keying
newton/_src/geometry/contact_data.py
Added ContactData.sort_sub_key and make_contact_sort_key() to compute 64-bit composite sort keys.
Collision Core & Convex
newton/_src/geometry/collision_core.py, newton/_src/geometry/collision_convex.py
Threaded sort_sub_key into contact template and assigned it in single- and multi-contact solver paths.
Global Contact Reduction
newton/_src/geometry/contact_reduction_global.py
Added deterministic packing mode, fingerprint storage, fingerprint-threading through export/reduction APIs, and process-global switch to enable deterministic packing.
Hydroelastic Reduction
newton/_src/geometry/contact_reduction_hydroelastic.py
Adjusted export and value-packing calls to pass explicit fingerprint (0) per revised APIs.
Manifold Extraction
newton/_src/geometry/multicontact.py
Assigned sort_sub_key for manifold points using template sub-key plus local index.
Narrow Phase & Writer
newton/_src/geometry/narrow_phase.py
Added deterministic flag, contact sort-key array wiring, per-path sort_sub_key assignments (primitive, mesh, plane), post-launch sort integration, and ContactWriterData.contact_sort_key.
SDF Contacts
newton/_src/geometry/sdf_contact.py
Populated sort_sub_key for edge-based contacts and passed derived sub-keys into export/reduce calls.
Contact Sorter
newton/_src/geometry/contact_sort.py
New ContactSorter with radix sort preparation and gather kernels to reorder contact arrays (int, float, vec3, vec2i); preallocated scratch and permutation buffers for allocation-free sorts.
Collision Pipeline Integration
newton/_src/sim/collide.py
Added deterministic option, per-contact out_sort_key wiring, sorter allocation/use, and validation requiring narrow-phase determinism when enabled.
Tests
newton/tests/test_contact_reduction_global.py
Extensive tests added for float-flip, sort-key composition, deterministic packing/unpacking, preprune behavior, and end-to-end deterministic reduction consistency.
Docs & Changelog
docs/concepts/collisions.rst, CHANGELOG.md
Documentation and changelog entry describing deterministic contact ordering, fingerprint tie-breaking, radix-sort reordering, and overhead/constraints (hydroelastic excluded).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CollisionPipeline
    participant NarrowPhase
    participant GlobalContactReducer
    participant ContactSorter

    Client->>CollisionPipeline: collide(..., deterministic=True)
    CollisionPipeline->>NarrowPhase: launch(writer_data with sort_key buffer)
    NarrowPhase->>NarrowPhase: compute contacts & assign sort_sub_key per contact
    NarrowPhase->>NarrowPhase: compute sort_key via make_contact_sort_key(shape_a, shape_b, sort_sub_key)
    NarrowPhase->>NarrowPhase: write sort_key to writer_data.contact_sort_key
    NarrowPhase-->>CollisionPipeline: return contact_count
    CollisionPipeline->>GlobalContactReducer: reduce contacts with fingerprint-based tie-breaking
    GlobalContactReducer-->>CollisionPipeline: reduced contacts
    CollisionPipeline->>ContactSorter: sort_full(sort_keys, contact arrays)
    ContactSorter->>ContactSorter: radix_sort_pairs(sort_keys, permutation)
    ContactSorter->>ContactSorter: gather all contact arrays using permutation
    ContactSorter-->>CollisionPipeline: reordered contact buffers
    CollisionPipeline-->>Client: deterministic contact output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • mzamoramora-nvidia
  • lenroe-nv
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Opt-in deterministic contacts' accurately and concisely describes the main feature introduced in the changeset: the addition of an optional deterministic mode for contact detection. The title is clear, specific, and reflects the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nvtw nvtw changed the title Draft Opt-in deterministic contacts Draft: Opt-in deterministic contacts Apr 10, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@newton/_src/geometry/contact_data.py`:
- Around line 73-80: The packing currently shifts raw shape_a and shape_b which
can overflow into adjacent bits; mask shape_a and shape_b to 20 bits before
shifting so the 64-bit key layout is preserved even when values exceed the
intended range. Concretely, apply (shape_a & 0xFFFFF) and (shape_b & 0xFFFFF)
(or wp.int64(...) & wp.int64(0xFFFFF)) when constructing the returned value
alongside the existing sort_sub_key masking and keep the warning logs as-is;
update the expressions used in the return (the shifted parts that reference
shape_a and shape_b) to use these masked values.

In `@newton/_src/geometry/contact_reduction_global.py`:
- Around line 245-254: The reducer is truncating one bit of the sort_sub_key
(FINGERPRINT) causing aliasing during atomic_max; change the end-to-end
fingerprint budget to 23 bits by updating FINGERPRINT_BITS from 22 to 23 and
update FINGERPRINT_MASK to (1 << 23) - 1 (and any duplicate definitions around
lines 313-329) so make_contact_sort_key and the reduction use the same 23-bit
fingerprint; ensure any related constants (e.g., where FINGERPRINT_BITS is
referenced in make_contact_sort_key or the reducer) are adjusted consistently so
no mask/truncation drops that extra bit.

In `@newton/_src/geometry/narrow_phase.py`:
- Around line 1613-1615: The deterministic path currently allocates
self._sort_key_array and ContactSorter using max_candidate_pairs but
write_contact_simple emits one sort key per actual emitted contact
(contact_pair.shape[0]), so the buffer can be overflowed; change the allocation
to use the contact-capacity (the maximum number of emitted contacts) instead of
max_candidate_pairs—i.e., size self._sort_key_array and initialize ContactSorter
with the maximum contacts capacity (the same capacity used for emitted contacts
in write_contact_simple) so deterministic sorting cannot write past the buffer.
- Around line 35-43: The import of ContactSorter from ..geometry.contact_sort is
invalid and breaks module loading; locate the ContactSorter class/definition in
the repository (search for "class ContactSorter" or its factory function) and
update the import in narrow_phase.py to the correct module path that actually
defines ContactSorter, or remove/replace the import with the correct alternative
API (e.g., from ..geometry.contact_sorter or the module where ContactSorter is
implemented); ensure the symbol name (ContactSorter) matches the exported name
and run tests to verify the module imports cleanly.

In `@newton/_src/sim/collide.py`:
- Around line 14-16: The import of ContactSorter from ..geometry.contact_sort is
failing because contact_sort does not exist; update the import in collide.py to
reference the correct module that defines ContactSorter (for example, import
ContactSorter from the actual module where it's implemented), or remove/replace
the ContactSorter import and usages in functions like
any_collisions/collect_contacts that reference ContactSorter so the module can
import successfully; ensure you update the import line importing ContactSorter
and adjust downstream references to the ContactSorter symbol accordingly.
- Around line 715-725: The non-deterministic branch currently sets
self._sort_key_array = None which breaks collide() and write_contact() that
always expect an array (copied into ContactWriterData.out_sort_key); replace the
None sentinel with an empty Warp array allocated on the same device and with the
same dtype/shape semantics used in the deterministic branch (i.e. an int64
zero-length array on device) so code that copies or indexes the array can
operate uniformly; leave _contact_sorter as None when deterministic is False.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: d8502213-3a5a-4cab-9ed8-5dd204e2b9cf

📥 Commits

Reviewing files that changed from the base of the PR and between 8baee87 and 50858c7.

📒 Files selected for processing (11)
  • newton/_src/geometry/collision_convex.py
  • newton/_src/geometry/collision_core.py
  • newton/_src/geometry/contact_data.py
  • newton/_src/geometry/contact_reduction.py
  • newton/_src/geometry/contact_reduction_global.py
  • newton/_src/geometry/contact_reduction_hydroelastic.py
  • newton/_src/geometry/multicontact.py
  • newton/_src/geometry/narrow_phase.py
  • newton/_src/geometry/sdf_contact.py
  • newton/_src/sim/collide.py
  • newton/tests/test_contact_reduction_global.py

Comment thread newton/_src/geometry/contact_data.py Outdated
Comment thread newton/_src/geometry/contact_reduction_global.py Outdated
Comment thread newton/_src/geometry/narrow_phase.py
Comment thread newton/_src/geometry/narrow_phase.py Outdated
Comment thread newton/_src/sim/collide.py

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
newton/_src/geometry/contact_sort.py (1)

64-92: Use concrete Warp array annotations instead of bare wp.array.

These signatures currently drop the element dtype, which goes against the repo’s Warp typing convention and makes the API harder to validate at a glance. Please annotate them as wp.array[wp.int64], wp.array[wp.int32], wp.array[wp.vec3], wp.array[float], etc., and use wp.array[Any] only where the dtype is intentionally generic.

Representative cleanup
+from typing import Any
+
     def sort_simple(
         self,
-        sort_keys: wp.array,
-        contact_count: wp.array,
+        sort_keys: wp.array[wp.int64],
+        contact_count: wp.array[int],
         *,
-        contact_pair: wp.array,
-        contact_position: wp.array,
-        contact_normal: wp.array,
-        contact_penetration: wp.array,
-        contact_tangent: wp.array | None = None,
+        contact_pair: wp.array[wp.vec2i],
+        contact_position: wp.array[wp.vec3],
+        contact_normal: wp.array[wp.vec3],
+        contact_penetration: wp.array[float],
+        contact_tangent: wp.array[wp.vec3] | None = None,
         device: Devicelike = None,
     ) -> None:

As per coding guidelines "Annotate Warp arrays with bracket syntax (wp.array[wp.vec3], wp.array2d[float], wp.array[Any]), not the parenthesized form. Use wp.array[X] for 1-D arrays, not wp.array1d[X]."

Also applies to: 126-142, 192-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton/_src/geometry/contact_sort.py` around lines 64 - 92, The method
signatures use bare wp.array types; update them to concrete Warp array
annotations (e.g., use wp.array[wp.int64], wp.array[wp.int32],
wp.array[wp.vec3], wp.array[float], etc.) so the element dtype is explicit.
Specifically modify sort_simple's parameters: annotate sort_keys as
wp.array[wp.int64], contact_count as wp.array[wp.int32], contact_pair as
wp.array[wp.vec2i] (or wp.array[wp.int32] if pairs are flattened indices),
contact_position and contact_normal as wp.array[wp.vec3], contact_penetration as
wp.array[float], and contact_tangent (when present) as wp.array[wp.vec3] | None;
then apply the same bracketed wp.array[...] annotation style to the other
function signatures mentioned (around the regions noted: 126-142 and 192-210) so
all 1-D Warp arrays carry explicit element dtypes rather than bare wp.array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@newton/_src/geometry/contact_sort.py`:
- Around line 60-62: The class docstring incorrectly references a nonexistent
:meth:`sort` method; update the docstring for ContactSorter to reference the
actual public methods (:meth:`sort_simple` and :meth:`sort_full`) using the
correct Sphinx :meth: role and fully qualified names if needed so readers are
pointed to the real API (e.g., replace :meth:`sort` with
:meth:`ContactSorter.sort_simple` and :meth:`ContactSorter.sort_full` or
equivalent short forms used in this project).
- Around line 1-2: The SPDX header at the top of the file currently uses the
wrong creation year; update the line "# SPDX-FileCopyrightText: Copyright (c)
2025 The Newton Developers" to use the actual first-created year (change 2025 to
2026) so the SPDX metadata is correct; leave the license identifier line "#
SPDX-License-Identifier: Apache-2.0" unchanged.

---

Nitpick comments:
In `@newton/_src/geometry/contact_sort.py`:
- Around line 64-92: The method signatures use bare wp.array types; update them
to concrete Warp array annotations (e.g., use wp.array[wp.int64],
wp.array[wp.int32], wp.array[wp.vec3], wp.array[float], etc.) so the element
dtype is explicit. Specifically modify sort_simple's parameters: annotate
sort_keys as wp.array[wp.int64], contact_count as wp.array[wp.int32],
contact_pair as wp.array[wp.vec2i] (or wp.array[wp.int32] if pairs are flattened
indices), contact_position and contact_normal as wp.array[wp.vec3],
contact_penetration as wp.array[float], and contact_tangent (when present) as
wp.array[wp.vec3] | None; then apply the same bracketed wp.array[...] annotation
style to the other function signatures mentioned (around the regions noted:
126-142 and 192-210) so all 1-D Warp arrays carry explicit element dtypes rather
than bare wp.array.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: de346e66-cd07-422c-8b33-e0e0ea54ff19

📥 Commits

Reviewing files that changed from the base of the PR and between 50858c7 and f9ef6c4.

📒 Files selected for processing (4)
  • docs/api/newton.rst
  • newton/_src/geometry/contact_data.py
  • newton/_src/geometry/contact_reduction_global.py
  • newton/_src/geometry/contact_sort.py
✅ Files skipped from review due to trivial changes (1)
  • docs/api/newton.rst
🚧 Files skipped from review as they are similar to previous changes (2)
  • newton/_src/geometry/contact_data.py
  • newton/_src/geometry/contact_reduction_global.py

Comment thread newton/_src/geometry/contact_sort.py Outdated
Comment thread newton/_src/geometry/contact_sort.py
@codecov

codecov Bot commented Apr 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.81818% with 26 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
newton/_src/geometry/contact_sort.py 77.52% 20 Missing ⚠️
newton/_src/sim/collide.py 82.35% 3 Missing ⚠️
newton/_src/geometry/narrow_phase.py 91.30% 2 Missing ⚠️
newton/_src/geometry/contact_reduction_global.py 92.30% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
newton/_src/geometry/contact_sort.py (1)

106-116: Use typed Warp array annotations on the sorter API.

These signatures use bare wp.array, which hides the expected dtypes for a class that permutes several heterogeneous buffers. Please switch them to wp.array[...] (wp.array[wp.int64], wp.array[wp.vec3], etc.) so mismatches are easier to catch.

As per coding guidelines "Annotate Warp arrays with bracket syntax (wp.array[wp.vec3], wp.array2d[float], wp.array[Any]), not the parenthesized form. Use wp.array[X] for 1-D arrays, not wp.array1d[X]."

Also applies to: 143-159, 207-236

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton/_src/geometry/contact_sort.py` around lines 106 - 116, The sort_simple
signature (and the other sorter APIs in this file) use untyped wp.array
annotations; update them to use Warp typed generics (e.g., wp.array[wp.int64]
for indices/counters, wp.array[wp.vec3] for 3D vectors, wp.array[float] for
scalars, or wp.array[Any] where heterogeneous) so mismatches are caught: replace
the bare wp.array on parameters like sort_keys, contact_count, contact_pair,
contact_position, contact_normal, contact_penetration, contact_tangent and the
device param annotation if needed with the appropriate wp.array[...] types
consistent with how those buffers are used in sort_simple and the other sort_*
functions in this module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@newton/_src/geometry/contact_sort.py`:
- Around line 106-116: The sort_simple signature (and the other sorter APIs in
this file) use untyped wp.array annotations; update them to use Warp typed
generics (e.g., wp.array[wp.int64] for indices/counters, wp.array[wp.vec3] for
3D vectors, wp.array[float] for scalars, or wp.array[Any] where heterogeneous)
so mismatches are caught: replace the bare wp.array on parameters like
sort_keys, contact_count, contact_pair, contact_position, contact_normal,
contact_penetration, contact_tangent and the device param annotation if needed
with the appropriate wp.array[...] types consistent with how those buffers are
used in sort_simple and the other sort_* functions in this module.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ed10bd4e-0682-4840-9a8c-8b06bdd44f83

📥 Commits

Reviewing files that changed from the base of the PR and between f9ef6c4 and c38b83e.

📒 Files selected for processing (1)
  • newton/_src/geometry/contact_sort.py

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@newton/_src/geometry/contact_reduction_global.py`:
- Around line 414-424: The current enable_deterministic_contact_packing()
mutates module-level symbols (make_contact_value, make_preprune_probe,
unpack_contact_id) to deterministic implementations (_make_contact_value_det,
_make_preprune_probe_det, _unpack_contact_id_det), which makes the choice
process-global; instead, stop swapping globals and thread the packer/unpacker
implementations into reducer/kernel construction so each reducer can choose fast
vs deterministic independently—modify the reducer/kernel builder to accept a
packer strategy (e.g., pass
make_contact_value/make_preprune_probe/unpack_contact_id as parameters or
encapsulate them in a Packer strategy object) and update all places that build
kernels (including the other swap site that currently mirrors
enable_deterministic_contact_packing behavior) to bind the appropriate
implementation at build time rather than calling
enable_deterministic_contact_packing().
- Around line 265-268: The packer currently only reserves 19 bits for contact
ids, which overflows for defaults like max_triangle_pairs=1_000_000; update
CONTACT_ID_BITS to wp.uint64(20) and recompute CONTACT_ID_MASK =
wp.constant(wp.uint64((1 << 20) - 1)) (and update the paired FINGERPRINT_MASK if
you keep FINGERPRINT_BITS unchanged) in both places where these constants are
defined (symbols: FINGERPRINT_BITS, CONTACT_ID_BITS, CONTACT_ID_MASK,
FINGERPRINT_MASK) so packed ids no longer alias and unpack_contact_id() returns
correct winners.

In `@newton/_src/sim/collide.py`:
- Around line 715-725: The deterministic sort state (_sort_key_array and
_contact_sorter) is sized only from the pipeline constructor rigid_contact_max
and can be too small for a caller-supplied Contacts buffer
(writer_data.contact_max / contacts.rigid_contact_max), causing write_contact to
index out of bounds; update the constructor or the deterministic branch in the
Collide class to compare and resize _sort_key_array (and recreate or resize
_contact_sorter) to match the caller's contact capacity
(writer_data.contact_max) just like NarrowPhase.launch does, or alternatively
validate/reject oversized Contacts up front so write_contact and sort_full
cannot access a smaller buffer than provided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 99a0b90b-3e96-468e-a0e9-0c219fd985fd

📥 Commits

Reviewing files that changed from the base of the PR and between 587be69 and ed6ee76.

📒 Files selected for processing (4)
  • newton/_src/geometry/contact_data.py
  • newton/_src/geometry/contact_reduction_global.py
  • newton/_src/geometry/narrow_phase.py
  • newton/_src/sim/collide.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/_src/geometry/contact_data.py

Comment thread newton/_src/geometry/contact_reduction_global.py Outdated
Comment thread newton/_src/geometry/contact_reduction_global.py Outdated
Comment thread newton/_src/sim/collide.py
@nvtw nvtw marked this pull request as ready for review April 13, 2026 08:45
@nvtw nvtw requested a review from adenzler-nvidia April 13, 2026 08:45
@nvtw nvtw changed the title Draft: Opt-in deterministic contacts Opt-in deterministic contacts Apr 13, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@newton/_src/geometry/narrow_phase.py`:
- Around line 2202-2206: The code only grows the deterministic sort state but
must rebuild it when capacity changes in either direction; in the block handling
self.deterministic compare self._sort_key_array.shape[0] (or the current sorter
capacity via self._contact_sorter._capacity if available) against contact_max
and if they differ recreate self._sort_key_array to size contact_max and
reinstantiate self._contact_sorter = ContactSorter(contact_max, device=dev) so
ContactSorter.sort_simple() won't copy/gather past the shorter buffers; ensure
the new array uses the same device and dtype as before.

In `@newton/_src/geometry/sdf_contact.py`:
- Line 1262: The deterministic packing for contact_data.sort_sub_key in
make_contact_sort_key() currently shifts edge_idx and ORs mode into a 23-bit
field which can overflow for large meshes; update the packing to either allocate
more bits (e.g., use a 32-bit sort_sub_key and adjust masks in
make_contact_sort_key()) or add an explicit guard: check edge_idx against the
maximum representable value ((1 << (23 - bits_used_for_mode)) - 1) before
packing and, if it exceeds the limit, set a reserved overflow value or fall back
to a non-deterministic/secondary sort path and/or log/assert to avoid silent
aliasing. Ensure this check/update is applied to both places where sort_sub_key
is computed (the assignment using (edge_idx << 1) | mode and the block around
lines 1552–1559) so edges cannot wrap into the mode bits.

In `@newton/_src/sim/collide.py`:
- Around line 934-940: The current check in CollisionPipeline (using
self.deterministic, contacts.rigid_contact_max and self._sort_key_array) only
rejects when the Contacts buffer is larger than the sort buffer, but reusing a
smaller Contacts after a larger one can cause out‑of‑bounds reads in
ContactSorter.sort_full() because it assumes a fixed capacity; change the logic
to enforce an exact capacity match (contacts.rigid_contact_max ==
self._sort_key_array.shape[0]) or, when they differ in either direction,
recreate/reset the sorter state (the array backing self._sort_key_array and any
ContactSorter instance) so the sort buffer always matches
Contacts.rigid_contact_max before assigning writer_data.out_sort_key.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ddef5058-5579-46cd-a661-4f7160e507b1

📥 Commits

Reviewing files that changed from the base of the PR and between 587be69 and d2f7a09.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • docs/concepts/collisions.rst
  • newton/_src/geometry/contact_data.py
  • newton/_src/geometry/contact_reduction_global.py
  • newton/_src/geometry/contact_sort.py
  • newton/_src/geometry/narrow_phase.py
  • newton/_src/geometry/sdf_contact.py
  • newton/_src/sim/collide.py
✅ Files skipped from review due to trivial changes (1)
  • docs/concepts/collisions.rst
🚧 Files skipped from review as they are similar to previous changes (2)
  • newton/_src/geometry/contact_data.py
  • newton/_src/geometry/contact_reduction_global.py

Comment thread newton/_src/geometry/narrow_phase.py Outdated
Comment thread newton/_src/geometry/sdf_contact.py Outdated
Comment thread newton/_src/sim/collide.py Outdated
@nvtw nvtw requested a review from eric-heiden April 13, 2026 08:58

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
newton/_src/sim/collide.py (1)

562-599: ⚠️ Potential issue | 🟠 Major

Validate determinism in the expert-components path.

When a caller supplies a prebuilt NarrowPhase, deterministic=True here only enables the post-sorter later. If that NarrowPhase was built with non-deterministic reduction, the pipeline still won't be fully deterministic even though the public flag says it is. Please reject mismatched configurations or require the provided narrow_phase to already be deterministic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton/_src/sim/collide.py` around lines 562 - 599, When using expert
components (broad_phase_instance or narrow_phase provided) ensure the public
deterministic flag and the supplied NarrowPhase agree: if deterministic=True
then validate the provided narrow_phase is already deterministic (e.g., check an
identifying attribute like narrow_phase.is_deterministic or
narrow_phase.deterministic_reduction) and raise a ValueError if it is not;
likewise, if deterministic=False but the provided NarrowPhase enforces
deterministic reduction, either accept it or explicitly reject based on project
policy. Add this validation in the expert-components branch before assigning
self.narrow_phase (after the max_candidate_pairs check) so mismatched
configurations are rejected rather than silently creating a non-deterministic
pipeline.
♻️ Duplicate comments (1)
newton/_src/geometry/narrow_phase.py (1)

2202-2206: ⚠️ Potential issue | 🔴 Critical

Rebuild deterministic sort state whenever contact_max changes.

Line 2203 still only grows the sorter. If a later launch uses a smaller contact buffer, ContactSorter keeps the older capacity and its gather pass can still walk past the shorter arrays. Recreate the sort buffers whenever contact_max differs, not only when it increases.

🛠️ Proposed fix
-        if self.deterministic and self._sort_key_array.shape[0] < contact_max:
+        if self.deterministic and self._sort_key_array.shape[0] != contact_max:
             dev = self._sort_key_array.device
             self._sort_key_array = wp.zeros(contact_max, dtype=wp.int64, device=dev)
             self._contact_sorter = ContactSorter(contact_max, device=dev)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton/_src/geometry/narrow_phase.py` around lines 2202 - 2206, The current
logic only grows the deterministic sort state when self._sort_key_array.shape[0]
< contact_max; change it to rebuild the sort-key buffer and ContactSorter
whenever the capacity differs so both growth and shrink are handled: compare
self._sort_key_array.shape[0] != contact_max (or use a separate capacity field)
and when different recreate self._sort_key_array with wp.zeros(contact_max,
dtype=wp.int64, device=dev) and reinstantiate self._contact_sorter =
ContactSorter(contact_max, device=dev), preserving the device via
self._sort_key_array.device and guarding with self.deterministic.
🧹 Nitpick comments (1)
CHANGELOG.md (1)

41-41: Keep the changelog entry at the API level.

radix sort and fingerprint tiebreaking are implementation details. This line should just describe the new opt-in deterministic contact ordering behavior, and optionally note the current hydroelastic limitation.

As per coding guidelines, CHANGELOG.md entries should "avoid internal implementation details."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` at line 41, Update the changelog entry to avoid implementation
details and describe the API change at a high level: say that a new opt-in
deterministic contact ordering flag (e.g., the deterministic flag on
CollisionPipeline and NarrowPhase) was added to produce
GPU-thread-scheduling-independent contact ordering, and optionally note that
hydroelastic contacts are still non-deterministic; remove references to "radix
sort" and "fingerprint tiebreaking" which are implementation details.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@newton/_src/geometry/contact_reduction_global.py`:
- Around line 698-705: The current check off-by-one rejects capacity == (1 <<
CONTACT_ID_BITS); change the limit to allow the full zero-based range by
computing max_det_contacts = 1 << int(CONTACT_ID_BITS) (not minus one) and keep
the guard as if deterministic and capacity > max_det_contacts: raise ValueError;
update the error text to reflect "supports at most {max_det_contacts} buffered
contacts (0..{max_det_contacts-1})" and reference CONTACT_ID_BITS,
max_det_contacts, contact_id, capacity, deterministic when making the change.

In `@newton/_src/geometry/narrow_phase.py`:
- Line 985: The packed fingerprint currently overflows because
make_contact_sort_key only preserves 23 bits of sort_sub_key yet code shifts
tri_idx/vertex_idx into that space (e.g., the expression (tri_idx << 1) | 1 and
similar uses of vertex_idx), allowing silent aliasing for large meshes; update
make_contact_sort_key and all call sites (references to tri_idx and vertex_idx
at the lines noted) to either increase the reserved bit width for sort_sub_key
to accommodate the maximum allowed triangle/vertex indices or add explicit range
checks that raise/return an error when tri_idx or vertex_idx exceeds the
supported max (e.g., > (1<<22)-1 for a single-bit flag use), and ensure any
alternate packing mirrors that decision so no bits are silently lost.

---

Outside diff comments:
In `@newton/_src/sim/collide.py`:
- Around line 562-599: When using expert components (broad_phase_instance or
narrow_phase provided) ensure the public deterministic flag and the supplied
NarrowPhase agree: if deterministic=True then validate the provided narrow_phase
is already deterministic (e.g., check an identifying attribute like
narrow_phase.is_deterministic or narrow_phase.deterministic_reduction) and raise
a ValueError if it is not; likewise, if deterministic=False but the provided
NarrowPhase enforces deterministic reduction, either accept it or explicitly
reject based on project policy. Add this validation in the expert-components
branch before assigning self.narrow_phase (after the max_candidate_pairs check)
so mismatched configurations are rejected rather than silently creating a
non-deterministic pipeline.

---

Duplicate comments:
In `@newton/_src/geometry/narrow_phase.py`:
- Around line 2202-2206: The current logic only grows the deterministic sort
state when self._sort_key_array.shape[0] < contact_max; change it to rebuild the
sort-key buffer and ContactSorter whenever the capacity differs so both growth
and shrink are handled: compare self._sort_key_array.shape[0] != contact_max (or
use a separate capacity field) and when different recreate self._sort_key_array
with wp.zeros(contact_max, dtype=wp.int64, device=dev) and reinstantiate
self._contact_sorter = ContactSorter(contact_max, device=dev), preserving the
device via self._sort_key_array.device and guarding with self.deterministic.

---

Nitpick comments:
In `@CHANGELOG.md`:
- Line 41: Update the changelog entry to avoid implementation details and
describe the API change at a high level: say that a new opt-in deterministic
contact ordering flag (e.g., the deterministic flag on CollisionPipeline and
NarrowPhase) was added to produce GPU-thread-scheduling-independent contact
ordering, and optionally note that hydroelastic contacts are still
non-deterministic; remove references to "radix sort" and "fingerprint
tiebreaking" which are implementation details.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ddef5058-5579-46cd-a661-4f7160e507b1

📥 Commits

Reviewing files that changed from the base of the PR and between 587be69 and d2f7a09.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • docs/concepts/collisions.rst
  • newton/_src/geometry/contact_data.py
  • newton/_src/geometry/contact_reduction_global.py
  • newton/_src/geometry/contact_sort.py
  • newton/_src/geometry/narrow_phase.py
  • newton/_src/geometry/sdf_contact.py
  • newton/_src/sim/collide.py
✅ Files skipped from review due to trivial changes (1)
  • docs/concepts/collisions.rst
🚧 Files skipped from review as they are similar to previous changes (3)
  • newton/_src/geometry/contact_data.py
  • newton/_src/geometry/sdf_contact.py
  • newton/_src/geometry/contact_sort.py

Comment thread newton/_src/geometry/contact_reduction_global.py Outdated
Comment thread newton/_src/geometry/narrow_phase.py
Comment thread newton/_src/geometry/contact_reduction_global.py Outdated
Comment thread newton/_src/geometry/contact_data.py
Comment thread newton/_src/geometry/narrow_phase.py Outdated
Comment thread newton/_src/geometry/contact_reduction_global.py Outdated
@eric-heiden eric-heiden enabled auto-merge April 14, 2026 19:28
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.

3 participants