Skip to content

Conversation

@nvtw
Copy link
Member

@nvtw nvtw commented Jan 8, 2026

Description

Add a warning that informs the user when the contact margin is smaller than the thickness. Also switches max(contact_margin_shape_a, contact_margin_shape_b) to their sum such that it is compatible with the thickness.

Newton Migration Guide

Please ensure the migration guide for warp.sim users is up-to-date with the changes made in this PR.

  • The migration guide in docs/migration.rst is up-to date

Before your PR is "Ready for review"

  • Necessary tests have been added and new examples are tested (see newton/tests/test_examples.py)
  • Documentation is up-to-date
  • Code passes formatting and linting checks with pre-commit run -a

Summary by CodeRabbit

  • Bug Fixes

    • Per-shape contact margins are now combined by summing instead of taking the maximum, affecting collision/contact detection thresholds and write-out behavior.
  • Validation

    • Finalize now errors on joints not attached to an articulation to prevent invalid setups; builder validation added.
  • Documentation

    • Clarified guidance for shape thickness vs. contact margin and broad-phase implications.
  • Tests

    • Added and adjusted tests for thickness warnings, margin behavior, tolerance handling, and orphan-joint validation.

✏️ Tip: You can customize this high-level summary in your review settings.

…r than the thickness. Also switches max(contact_margin_shape_a, contact_margin_shape_b) to their sum such that it is compatible with the thickness.
@nvtw nvtw self-assigned this Jan 8, 2026
@nvtw nvtw marked this pull request as draft January 8, 2026 14:57
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

Replaced per-shape margin combination from max(...) to summing per-shape margins across broad-phase, narrow-phase, SDF/hydroelastic, and contact-write kernels; added ShapeConfig doc clarifications, a finalize-time warning for shapes with thickness > contact_margin, and tests for the warning and orphan-joint validation.

Changes

Cohort / File(s) Summary
Collision Geometry Kernels
newton/_src/geometry/kernels.py, newton/_src/geometry/narrow_phase.py, newton/_src/geometry/sdf_contact.py, newton/_src/geometry/sdf_hydroelastic.py
Replaced max(margin_a, margin_b) with margin_a + margin_b in broad-phase, narrow-phase, SDF contact, and hydroelastic kernels; updates effective margin used for AABB expansion, contact detection thresholds, isovalue/voxel checks, and contact generation.
Collision Contact Handling
newton/_src/sim/collide_unified.py, newton/_src/solvers/mujoco/kernels.py
Use summed per-shape margins when computing contact margin/gap and MuJoCo contact parameters (previously used max).
Configuration & Validation
newton/_src/sim/builder.py
Clarified ShapeConfig.thickness and contact_margin docstrings; added finalize-time UserWarning listing up to five shapes where thickness > contact_margin; added finalize-time validation that raises for joints not belonging to an articulation.
Tests
newton/tests/test_model.py, newton/tests/test_mujoco_solver.py, newton/tests/test_collision_pipeline.py
Added tests verifying thickness-vs-contact_margin warning behavior (emit/no-emit/aggregated); moved finalize() into error context for orphan-joint test; adjusted collision tests for reduced rigid contact margin and tolerance parameter additions.

Sequence Diagram(s)

(omitted — changes are margin-value adjustments across many kernels and validation, not a new multi-component control-flow feature)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • adenzler-nvidia
  • eric-heiden
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.38% 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 'Contact margin improvements' directly describes the main change: converting margin combination from max() to sum() and adding validation warnings.

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

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d68b522 and c66d1e3.

📒 Files selected for processing (1)
  • newton/tests/test_collision_pipeline.py
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: nvtw
Repo: newton-physics/newton PR: 981
File: newton/_src/geometry/collision_convex.py:180-289
Timestamp: 2025-10-24T07:56:14.792Z
Learning: In newton/_src/geometry/collision_convex.py, the single-contact solver (create_solve_convex_single_contact) intentionally returns count=1 even when shapes are separated (signed_distance > contact_threshold). The calling code is responsible for deciding whether to accept the contact based on the signed distance value. This design pushes filtering responsibility to the caller.
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
Repo: newton-physics/newton PR: 519
File: newton/geometry.py:16-22
Timestamp: 2025-08-12T17:51:37.474Z
Learning: In the Newton public API refactor, common geometry symbols like ParticleFlags and ShapeFlags are now exposed at the top level in newton/__init__.py rather than through newton.geometry. The newton/geometry.py module is intentionally focused only on broad-phase collision detection classes (BroadPhaseAllPairs, BroadPhaseExplicit, BroadPhaseSAP).

Applied to files:

  • newton/tests/test_collision_pipeline.py
📚 Learning: 2025-10-10T10:47:41.082Z
Learnt from: nvtw
Repo: newton-physics/newton PR: 899
File: newton/tests/test_rigid_contact.py:214-268
Timestamp: 2025-10-10T10:47:41.082Z
Learning: In `newton/tests/test_rigid_contact.py`, using `add_shape_plane(width=0, length=0)` for an infinite plane works correctly with the collision system and does not cause degenerate AABB issues in the test scenario `test_shape_collisions_gjk_mpr_multicontact`.

Applied to files:

  • newton/tests/test_collision_pipeline.py
📚 Learning: 2025-09-26T05:58:21.013Z
Learnt from: WenchaoHuang
Repo: newton-physics/newton PR: 835
File: newton/_src/solvers/style3d/collision/collision.py:49-53
Timestamp: 2025-09-26T05:58:21.013Z
Learning: In newton/_src/solvers/style3d/collision/collision.py, the broad_phase buffers (broad_phase_ee, broad_phase_ef, broad_phase_vf) created with wp.array() don't need zero-initialization because they follow a write-first-read-later pattern: BVH query kernels in frame_begin() populate these buffers completely (write-only operations), and collision kernels only read from them afterward in accumulate_contact_force(), ensuring no undefined reads occur.

Applied to files:

  • newton/tests/test_collision_pipeline.py
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
Repo: newton-physics/newton PR: 587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/tests/test_collision_pipeline.py
📚 Learning: 2025-12-03T09:33:27.083Z
Learnt from: camevor
Repo: newton-physics/newton PR: 1019
File: newton/_src/solvers/kamino/examples/sim/example_sim_basics_cartpole.py:388-392
Timestamp: 2025-12-03T09:33:27.083Z
Learning: In the Kamino ModelBuilder class located at newton/_src/solvers/kamino/core/builder.py, the attributes `gravity`, `bodies`, `joints`, `collision_geoms`, and `physical_geoms` are implemented as property decorated methods, so they should be accessed as attributes (e.g., `builder.gravity`) rather than called as methods (e.g., `builder.gravity()`).

Applied to files:

  • newton/tests/test_collision_pipeline.py
📚 Learning: 2025-12-12T17:41:15.097Z
Learnt from: nvtw
Repo: newton-physics/newton PR: 1221
File: newton/_src/sim/builder.py:5556-5587
Timestamp: 2025-12-12T17:41:15.097Z
Learning: newton._src.geometry.sdf_utils.compute_sdf accepts max_resolution=None (e.g., when sdf_target_voxel_size is provided), so passing None from ModelBuilder.finalize is valid and should not be flagged.

Applied to files:

  • newton/tests/test_collision_pipeline.py
📚 Learning: 2026-01-12T10:11:39.312Z
Learnt from: nvtw
Repo: newton-physics/newton PR: 1314
File: newton/_src/sim/builder.py:6061-6091
Timestamp: 2026-01-12T10:11:39.312Z
Learning: Repo: newton-physics/newton PR: 1314
File: newton/_src/sim/builder.py (ModelBuilder.finalize)
Learning: The project prefers to keep finalize() validation lightweight; do not add union-find/connected-components validation for loop joints. Current behavior (allow non-articulated joints when the child is in any articulation) is acceptable; avoid overengineering here.

Applied to files:

  • newton/tests/test_collision_pipeline.py
📚 Learning: 2025-09-26T05:58:21.013Z
Learnt from: WenchaoHuang
Repo: newton-physics/newton PR: 835
File: newton/_src/solvers/style3d/collision/collision.py:49-53
Timestamp: 2025-09-26T05:58:21.013Z
Learning: In newton/_src/solvers/style3d/collision/collision.py, the broad_phase buffers (broad_phase_ee, broad_phase_ef, broad_phase_vf) created with wp.array() don't need zero-initialization because the BVH query kernels that use them always write the count to query_results[0, tid] = 0 first before populating any data, ensuring no undefined reads occur.

Applied to files:

  • newton/tests/test_collision_pipeline.py
🧬 Code graph analysis (1)
newton/tests/test_collision_pipeline.py (2)
newton/_src/sim/collide.py (1)
  • device (334-341)
newton/_src/sim/collide_unified.py (1)
  • BroadPhaseMode (69-79)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run GPU Benchmarks / Run GPU Benchmarks on AWS EC2
  • GitHub Check: Run GPU Tests / Run GPU Unit Tests on AWS EC2
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (4)
newton/tests/test_collision_pipeline.py (4)

79-81: LGTM!

The comment clearly explains the rationale for halving the margin value. Since margins are now summed (0.005 + 0.005 = 0.01), this maintains equivalent behavior to the previous max(0.01, 0.01) = 0.01 approach.


403-425: LGTM!

Adding a configurable tolerance parameter is a clean approach to handle varying precision requirements across different collision modes. The default value of 3e-3 preserves backward compatibility with existing tests.


438-459: Asymmetric tolerances between symmetric test cases warrant investigation.

test_mesh_mesh_sdf_vs_bvh uses tolerance=0.2 while test_mesh_mesh_bvh_vs_sdf uses tolerance=0.5. Since these test cases only differ by which mesh has the SDF (shape order), the 2.5x tolerance difference suggests the collision behavior may be order-dependent in a way that could indicate an underlying issue.

Given that issue #1319 was created to track more rigorous margin testing, consider documenting why these specific tolerance values were chosen and whether this asymmetry is expected behavior or a known limitation.


489-490: LGTM!

Disabling output checking is a reasonable approach to avoid flaky tests caused by non-deterministic Warp module loading messages.


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 requested a review from gyeomannvidia January 8, 2026 14:57
@codecov
Copy link

codecov bot commented Jan 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @newton/_src/sim/builder.py:
- Around line 5818-5839: The warning loop currently flags any shape where
shape_thickness[i] > shape_contact_margin[i]; change it to only consider shapes
that participate in collision tests by gating the check on the per-shape
collision flag (i.e., the flag/field that indicates the shape actually collides
with shapes or particles), so iterate i in range(shape_count) and skip shapes
where the collision flag is false before evaluating thickness vs contact_margin;
keep the existing message and formatting (shape_key, example_shapes,
warnings.warn) but only include collidable shapes in shapes_with_bad_margin so
visual-only sites/meshes do not trigger the warning.
🧹 Nitpick comments (2)
newton/_src/sim/builder.py (1)

167-174: Clarify thickness/contact_margin semantics vs enforcement

The new docstrings capture the “thickened surface + summed thickness” idea, but two minor inconsistencies stand out:

  • Line 171–174 says contact_margin must be >= thickness, while finalize() only emits a warning when that isn’t true. Consider softening this to “should be” or explicitly mentioning that violating it results in a runtime warning rather than an error.
  • Line 168–170 describes thickness purely as a collision offset. It’s also used in mass/inertia and SDF/hydro setup paths, so a short note that it influences both collision and mass properties would make the behavior less surprising to users tuning physical thickness vs. margin.

These are documentation-level only, but tightening them would reduce confusion around how to choose thickness and contact_margin.

newton/_src/solvers/mujoco/kernels.py (1)

140-143: Margin/gap mixing now uses sum instead of max; verify MuJoCo interop behavior

contact_params now computes margin and gap as the sum of both geoms’ values. This matches Newton’s new “sum-of-margins” semantics but will increase the effective MuJoCo includemargin vs the previous max-based rule. Please recheck MuJoCo-bridge tests / benchmarks where geom margins or gaps were tuned assuming max-combination, to ensure contact onsets and constraint regularization still behave as expected.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bbdcbf and 911507b.

📒 Files selected for processing (8)
  • newton/_src/geometry/kernels.py
  • newton/_src/geometry/narrow_phase.py
  • newton/_src/geometry/sdf_contact.py
  • newton/_src/geometry/sdf_hydroelastic.py
  • newton/_src/sim/builder.py
  • newton/_src/sim/collide_unified.py
  • newton/_src/solvers/mujoco/kernels.py
  • newton/tests/test_model.py
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: shi-eric
Repo: newton-physics/newton PR: 521
File: newton/examples/example_cloth_hanging.py:36-36
Timestamp: 2025-08-12T05:17:34.423Z
Learning: The Newton migration guide (docs/migration.rst) is specifically for documenting how to migrate existing warp.sim functionality to Newton equivalents. New Newton-only features that didn't exist in warp.sim do not need migration documentation.
Learnt from: nvtw
Repo: newton-physics/newton PR: 899
File: newton/tests/test_rigid_contact.py:214-268
Timestamp: 2025-10-10T10:47:41.082Z
Learning: In `newton/tests/test_rigid_contact.py`, using `add_shape_plane(width=0, length=0)` for an infinite plane works correctly with the collision system and does not cause degenerate AABB issues in the test scenario `test_shape_collisions_gjk_mpr_multicontact`.
📚 Learning: 2025-10-24T07:56:14.792Z
Learnt from: nvtw
Repo: newton-physics/newton PR: 981
File: newton/_src/geometry/collision_convex.py:180-289
Timestamp: 2025-10-24T07:56:14.792Z
Learning: In newton/_src/geometry/collision_convex.py, the single-contact solver (create_solve_convex_single_contact) intentionally returns count=1 even when shapes are separated (signed_distance > contact_threshold). The calling code is responsible for deciding whether to accept the contact based on the signed distance value. This design pushes filtering responsibility to the caller.

Applied to files:

  • newton/_src/geometry/sdf_contact.py
  • newton/_src/sim/collide_unified.py
  • newton/_src/geometry/sdf_hydroelastic.py
  • newton/_src/geometry/kernels.py
  • newton/_src/geometry/narrow_phase.py
  • newton/_src/sim/builder.py
  • newton/_src/solvers/mujoco/kernels.py
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
Repo: newton-physics/newton PR: 519
File: newton/geometry.py:16-22
Timestamp: 2025-08-12T17:51:37.474Z
Learning: In the Newton public API refactor, common geometry symbols like ParticleFlags and ShapeFlags are now exposed at the top level in newton/__init__.py rather than through newton.geometry. The newton/geometry.py module is intentionally focused only on broad-phase collision detection classes (BroadPhaseAllPairs, BroadPhaseExplicit, BroadPhaseSAP).

Applied to files:

  • newton/_src/geometry/sdf_contact.py
  • newton/_src/sim/collide_unified.py
  • newton/_src/geometry/sdf_hydroelastic.py
  • newton/_src/geometry/kernels.py
  • newton/_src/geometry/narrow_phase.py
  • newton/_src/sim/builder.py
📚 Learning: 2025-12-12T08:45:51.908Z
Learnt from: nvtw
Repo: newton-physics/newton PR: 1221
File: newton/examples/example_sdf.py:277-287
Timestamp: 2025-12-12T08:45:51.908Z
Learning: In Newton examples (e.g., example_sdf.py), computing contacts once per frame and reusing them across multiple substeps is an intentional design choice, not a bug. The contacts are computed before the substep loop and intentionally kept constant throughout all substeps within that frame.

Applied to files:

  • newton/_src/geometry/sdf_contact.py
  • newton/_src/sim/collide_unified.py
📚 Learning: 2025-12-12T17:45:26.847Z
Learnt from: vastsoun
Repo: newton-physics/newton PR: 1019
File: newton/_src/solvers/kamino/geometry/primitives.py:0-0
Timestamp: 2025-12-12T17:45:26.847Z
Learning: In newton/_src/solvers/kamino/geometry/primitives.py, the narrow-phase kernel `_primitive_narrowphase` does not need to handle symmetric shape-pair orderings (e.g., both SPHERE-BOX and BOX-SPHERE) because `kamino.core.builder.ModelBuilder` and `kamino.geometry.primitive.broadphase` guarantee that explicit candidate geometry pairs are always defined with GIDs in ascending order.

Applied to files:

  • newton/_src/geometry/sdf_contact.py
  • newton/_src/geometry/kernels.py
  • newton/_src/geometry/narrow_phase.py
📚 Learning: 2025-10-10T10:47:41.082Z
Learnt from: nvtw
Repo: newton-physics/newton PR: 899
File: newton/tests/test_rigid_contact.py:214-268
Timestamp: 2025-10-10T10:47:41.082Z
Learning: In `newton/tests/test_rigid_contact.py`, using `add_shape_plane(width=0, length=0)` for an infinite plane works correctly with the collision system and does not cause degenerate AABB issues in the test scenario `test_shape_collisions_gjk_mpr_multicontact`.

Applied to files:

  • newton/_src/sim/collide_unified.py
  • newton/tests/test_model.py
  • newton/_src/geometry/narrow_phase.py
📚 Learning: 2025-09-26T05:58:21.013Z
Learnt from: WenchaoHuang
Repo: newton-physics/newton PR: 835
File: newton/_src/solvers/style3d/collision/collision.py:49-53
Timestamp: 2025-09-26T05:58:21.013Z
Learning: In newton/_src/solvers/style3d/collision/collision.py, the broad_phase buffers (broad_phase_ee, broad_phase_ef, broad_phase_vf) created with wp.array() don't need zero-initialization because they follow a write-first-read-later pattern: BVH query kernels in frame_begin() populate these buffers completely (write-only operations), and collision kernels only read from them afterward in accumulate_contact_force(), ensuring no undefined reads occur.

Applied to files:

  • newton/_src/sim/collide_unified.py
  • newton/_src/geometry/kernels.py
  • newton/_src/geometry/narrow_phase.py
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
Repo: newton-physics/newton PR: 587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/collide_unified.py
  • newton/_src/geometry/kernels.py
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Run GPU Tests / Run GPU Unit Tests on AWS EC2
  • GitHub Check: Run GPU Benchmarks / Run GPU Benchmarks on AWS EC2
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (11)
newton/_src/geometry/sdf_hydroelastic.py (3)

1197-1200: LGTM! Margin calculation updated to sum for consistency.

The change from using individual margins to summing both shape margins aligns with the PR objective to make margin calculation compatible with thickness. The comment clearly documents the rationale.

Note: This effectively increases the collision detection threshold compared to the previous max-based approach, which could impact performance and simulation behavior but is intentional per the PR design.


1389-1392: LGTM! Consistent margin calculation for unreduced contacts.

The margin summation is correctly applied in the decode path for unreduced contacts, maintaining consistency with the count_faces_kernel changes and ensuring the contact solver receives the combined margin value.


1671-1674: LGTM! Consistent margin calculation for reduced contacts.

The margin summation is correctly applied in the binning path for reduced contacts, ensuring all contact generation paths (count_faces, decode, and binning) use consistent margin calculation logic.

newton/tests/test_model.py (4)

18-18: LGTM! Required import for warning tests.

The warnings import is correctly added to support the new test methods that verify warning behavior.


867-885: LGTM! Well-structured test for thickness warning.

The test correctly verifies that a UserWarning is raised when thickness > contact_margin, and validates that the warning message contains the expected diagnostic information (shape key, condition, and consequence).


887-904: LGTM! Proper negative test case.

The test correctly verifies that no warning is raised when thickness <= contact_margin, using appropriate warning capture and filtering to avoid false positives.


906-932: LGTM! Comprehensive test for multiple shape violations.

The test effectively validates that the warning correctly reports multiple shapes with thickness > contact_margin while excluding compliant shapes, demonstrating good coverage of the reporting logic.

newton/_src/sim/collide_unified.py (1)

120-124: Summed per-shape margin in write_contact is consistent with thickness semantics

Switching contact_margin to shape_contact_margin[shape_a] + shape_contact_margin[shape_b] aligns the writer’s distance filter (d > contact_margin) with the rest of the pipeline, which already sums per-shape thickness (thickness_a + thickness_b) and now uses summed margins in the narrow-phase kernels. This keeps the pair-wise acceptance condition symmetric and avoids discrepancies between where contacts are generated and where they are written out.

newton/_src/geometry/sdf_contact.py (1)

761-765: SDF mesh–mesh margin changes are internally consistent with thickness handling

Both the direct and reduced mesh–SDF kernels now form a pair-wise margin as shape_contact_margin[a] + shape_contact_margin[b], then use contact_threshold = margin + triangle_mesh_thickness consistently in:

  • Midphase culling (bounding-sphere distance tests in scaled/unscaled space), and
  • Final contact acceptance (dist < contact_threshold) and contact_data.margin.

Given SDF-side thickness is already baked into the volume, this keeps the effective separation budget as “sum of per-shape margins plus explicit triangle-mesh thickness” without double-counting and matches the rest of the PR’s margin semantics.

Also applies to: 967-969

newton/_src/geometry/kernels.py (1)

1110-1111: Broadphase and rigid narrow-phase now share a consistent sum-of-margins budget

Both broadphase_collision_pairs and handle_contact_pairs now derive a pair-wise contact margin as shape_contact_margin[shape_a] + shape_contact_margin[shape_b]. That value:

  • Inflates the broadphase bounding-sphere checks, and
  • Is used in narrow-phase both for mesh/plane SDF queries (max_dist) and for the final acceptance test d < rigid_contact_margin, where d is distance minus (radius_eff_a + radius_eff_b + thickness_a + thickness_b).

This keeps the separation allowance consistent across stages and symmetric in the two shapes, which is exactly what you want given thickness is already summed. The main effect is to admit slightly more candidate pairs than the previous max-based rule, but in a predictable way.

Also applies to: 2006-2008

newton/_src/geometry/narrow_phase.py (1)

345-349: Narrow-phase consistently upgrades to pair-wise “sum-of-margins” across all paths

In all the updated kernels here:

  • GJK/MPR (narrow_phase_kernel_gjk_mpr),
  • Mesh–triangle midphase (narrow_phase_find_mesh_triangle_overlaps_kernel + narrow_phase_process_mesh_triangle_contacts_kernel), and
  • Mesh–plane (both direct and reduced variants),

the effective margin is now always shape_contact_margin[a] + shape_contact_margin[b], and that pair-wise value is combined with the summed thicknesses when comparing distances. Per-shape AABBs are still expanded by each shape’s own margin, so broadphase, midphase, and final distance gating all agree on the contact “reach.”

Given the single-contact solvers intentionally push acceptance filtering to the caller, using a single, summed pair-wise margin here is the right abstraction layer and keeps behavior consistent with the rest of the PR.

Also applies to: 431-435, 513-517, 611-615, 743-747

@gyeomannvidia
Copy link
Member

gyeomannvidia commented Jan 8, 2026

Do we have any tests that generate a contact when the distance between two surfaces is slightly smaller than marginA+marginB and fail to generate a contact when the distance is slightly bigger than marginA + marginB? If not, then it would be a great idea to add a ticket to add these tests.

gyeomannvidia
gyeomannvidia previously approved these changes Jan 8, 2026
Copy link
Member

@gyeomannvidia gyeomannvidia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking as approved.
Just a minor point about shapes that don't participate in collision and a subsequent ticket for more rigorous testing of margin behaviour.

@nvtw nvtw marked this pull request as ready for review January 9, 2026 07:52
nvtw added 2 commits January 9, 2026 09:00
# Conflicts:
#	newton/_src/sim/builder.py
#	newton/tests/test_model.py
@nvtw
Copy link
Member Author

nvtw commented Jan 9, 2026

Marking as approved. Just a minor point about shapes that don't participate in collision and a subsequent ticket for more rigorous testing of margin behaviour.

Here is a ticket #1319

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
newton/_src/sim/builder.py (1)

167-174: Clarify that contact_margin >= thickness is a sufficient/conservative condition (docs currently read like “required” in all cases).
Right now the docstrings imply a hard requirement; in practice it’s the safe per-shape rule that guarantees broad-phase overlap regardless of the other shape (and likely matches the intent of this PR), but it’s not strictly necessary for every possible pair.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 911507b and a8eb561.

📒 Files selected for processing (2)
  • newton/_src/sim/builder.py
  • newton/_src/solvers/mujoco/kernels.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/_src/solvers/mujoco/kernels.py
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: nvtw
Repo: newton-physics/newton PR: 899
File: newton/tests/test_rigid_contact.py:214-268
Timestamp: 2025-10-10T10:47:41.082Z
Learning: In `newton/tests/test_rigid_contact.py`, using `add_shape_plane(width=0, length=0)` for an infinite plane works correctly with the collision system and does not cause degenerate AABB issues in the test scenario `test_shape_collisions_gjk_mpr_multicontact`.
Learnt from: nvtw
Repo: newton-physics/newton PR: 981
File: newton/_src/geometry/collision_convex.py:180-289
Timestamp: 2025-10-24T07:56:14.792Z
Learning: In newton/_src/geometry/collision_convex.py, the single-contact solver (create_solve_convex_single_contact) intentionally returns count=1 even when shapes are separated (signed_distance > contact_threshold). The calling code is responsible for deciding whether to accept the contact based on the signed distance value. This design pushes filtering responsibility to the caller.
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
Repo: newton-physics/newton PR: 519
File: newton/geometry.py:16-22
Timestamp: 2025-08-12T17:51:37.474Z
Learning: In the Newton public API refactor, common geometry symbols like ParticleFlags and ShapeFlags are now exposed at the top level in newton/__init__.py rather than through newton.geometry. The newton/geometry.py module is intentionally focused only on broad-phase collision detection classes (BroadPhaseAllPairs, BroadPhaseExplicit, BroadPhaseSAP).

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
Repo: newton-physics/newton PR: 587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-09-26T05:58:21.013Z
Learnt from: WenchaoHuang
Repo: newton-physics/newton PR: 835
File: newton/_src/solvers/style3d/collision/collision.py:49-53
Timestamp: 2025-09-26T05:58:21.013Z
Learning: In newton/_src/solvers/style3d/collision/collision.py, the broad_phase buffers (broad_phase_ee, broad_phase_ef, broad_phase_vf) created with wp.array() don't need zero-initialization because they follow a write-first-read-later pattern: BVH query kernels in frame_begin() populate these buffers completely (write-only operations), and collision kernels only read from them afterward in accumulate_contact_force(), ensuring no undefined reads occur.

Applied to files:

  • newton/_src/sim/builder.py
🧬 Code graph analysis (1)
newton/_src/sim/builder.py (1)
newton/_src/geometry/flags.py (1)
  • ShapeFlags (30-48)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run GPU Benchmarks / Run GPU Benchmarks on AWS EC2
  • GitHub Check: Run GPU Tests / Run GPU Unit Tests on AWS EC2
🔇 Additional comments (1)
newton/_src/sim/builder.py (1)

5813-5851: Improve message formatting for orphan-joint errors and thickness warnings.

The proposed formatting changes are sound:

  • Add indices alongside keys in orphan joint error (line 5817): f"{i}:{self.joint_key[i]}" prevents confusion when keys are auto-generated or collide.
  • Use ", ".join(example_shapes) instead of list repr for thickness warning (line 5848) for readability.

Regarding threshold tests: The builder comments describe "sum of margins >= sum of thicknesses" (line 5828), but check_aabb_overlap in broad_phase_common.py currently uses max(box1_cutoff, box2_cutoff), not sum. Confirm whether the threshold test request targets the current max-margin logic or anticipates an upcoming behavioral change to sum-margin logic, then add corresponding boundary tests (distance just below vs. just above the threshold).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @newton/_src/sim/builder.py:
- Around line 6061-6071: The orphan-joint check should iterate over the expected
joint range (use self.joint_count) instead of enumerating
self.joint_articulation to avoid silent under-reporting when joint_articulation
is shorter; compute orphan indices as [i for i in range(self.joint_count) if i
>= len(self.joint_articulation) or self.joint_articulation[i] < 0], then when
raising the ValueError include both the joint indices and their keys (from
self.joint_key) for the first N (e.g., 5) to make the error actionable and still
include the total orphan count and a reminder to call add_articulation() for
affected joints.
🧹 Nitpick comments (2)
newton/_src/sim/builder.py (2)

167-175: Docstrings: consider explicitly stating pairwise margin behavior is additive (margin_a + margin_b).

Given the PR-wide semantics change, spelling this out here would reduce ambiguity between (a) per-shape AABB expansion and (b) pairwise contact thresholds.

Proposed doc tweak
         contact_margin: float | None = None
         """The contact margin for collision detection. If None, uses builder.rigid_contact_margin as default.
         AABBs are expanded by this value for broad phase detection. Must be >= thickness to ensure
-        collisions are not missed when thickened surfaces approach each other."""
+        collisions are not missed when thickened surfaces approach each other.
+
+        Note:
+            Pairwise contact detection uses the sum of both shapes' margins
+            (contact_margin_a + contact_margin_b)."""

6072-6099: Thickness-vs-margin warning: improve message readability (avoid list repr) and align comment wording with “missed broad-phase collisions”.

The behavior is good, but the warning currently prints Affected shapes: [...] using a Python list repr; joining is easier to read/copy-paste. Also the comment says “unstable contact behavior” while the rest of the block (and warning) is about broad-phase misses.

Proposed refinement
-        # warn if any shape has thickness > contact_margin (causes unstable contact behavior)
+        # warn if any shape has thickness > contact_margin (can cause missed broad-phase collisions)
         # Thickness is an outward offset from each shape's surface. AABBs are expanded by contact_margin.
         # For proper broad phase detection, each shape must have contact_margin >= thickness.
@@
         if shapes_with_bad_margin:
-            example_shapes = shapes_with_bad_margin[:5]
+            example_shapes = ", ".join(shapes_with_bad_margin[:5])
             warnings.warn(
                 f"Found {len(shapes_with_bad_margin)} shape(s) with thickness > contact_margin. "
                 f"This can cause missed collisions in broad phase since AABBs are only expanded by contact_margin. "
                 f"Set contact_margin >= thickness for each shape. "
-                f"Affected shapes: {example_shapes}" + ("..." if len(shapes_with_bad_margin) > 5 else ""),
+                f"Affected shapes: {example_shapes}" + ("..." if len(shapes_with_bad_margin) > 5 else ""),
                 stacklevel=2,
             )
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 212cf1c and 8a0e9c3.

📒 Files selected for processing (1)
  • newton/_src/sim/builder.py
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: nvtw
Repo: newton-physics/newton PR: 981
File: newton/_src/geometry/collision_convex.py:180-289
Timestamp: 2025-10-24T07:56:14.792Z
Learning: In newton/_src/geometry/collision_convex.py, the single-contact solver (create_solve_convex_single_contact) intentionally returns count=1 even when shapes are separated (signed_distance > contact_threshold). The calling code is responsible for deciding whether to accept the contact based on the signed distance value. This design pushes filtering responsibility to the caller.
Learnt from: nvtw
Repo: newton-physics/newton PR: 899
File: newton/tests/test_rigid_contact.py:214-268
Timestamp: 2025-10-10T10:47:41.082Z
Learning: In `newton/tests/test_rigid_contact.py`, using `add_shape_plane(width=0, length=0)` for an infinite plane works correctly with the collision system and does not cause degenerate AABB issues in the test scenario `test_shape_collisions_gjk_mpr_multicontact`.
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
Repo: newton-physics/newton PR: 519
File: newton/geometry.py:16-22
Timestamp: 2025-08-12T17:51:37.474Z
Learning: In the Newton public API refactor, common geometry symbols like ParticleFlags and ShapeFlags are now exposed at the top level in newton/__init__.py rather than through newton.geometry. The newton/geometry.py module is intentionally focused only on broad-phase collision detection classes (BroadPhaseAllPairs, BroadPhaseExplicit, BroadPhaseSAP).

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-10-24T07:56:14.792Z
Learnt from: nvtw
Repo: newton-physics/newton PR: 981
File: newton/_src/geometry/collision_convex.py:180-289
Timestamp: 2025-10-24T07:56:14.792Z
Learning: In newton/_src/geometry/collision_convex.py, the single-contact solver (create_solve_convex_single_contact) intentionally returns count=1 even when shapes are separated (signed_distance > contact_threshold). The calling code is responsible for deciding whether to accept the contact based on the signed distance value. This design pushes filtering responsibility to the caller.

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
Repo: newton-physics/newton PR: 587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-09-26T05:58:21.013Z
Learnt from: WenchaoHuang
Repo: newton-physics/newton PR: 835
File: newton/_src/solvers/style3d/collision/collision.py:49-53
Timestamp: 2025-09-26T05:58:21.013Z
Learning: In newton/_src/solvers/style3d/collision/collision.py, the broad_phase buffers (broad_phase_ee, broad_phase_ef, broad_phase_vf) created with wp.array() don't need zero-initialization because they follow a write-first-read-later pattern: BVH query kernels in frame_begin() populate these buffers completely (write-only operations), and collision kernels only read from them afterward in accumulate_contact_force(), ensuring no undefined reads occur.

Applied to files:

  • newton/_src/sim/builder.py
🧬 Code graph analysis (1)
newton/_src/sim/builder.py (1)
newton/_src/geometry/flags.py (1)
  • ShapeFlags (30-48)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run GPU Tests / Run GPU Unit Tests on AWS EC2
  • GitHub Check: Run GPU Benchmarks / Run GPU Benchmarks on AWS EC2

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
newton/tests/test_mujoco_solver.py (1)

3071-3086: Make the exception assertion a bit more robust.

The new test flow (raising from builder.finalize()) matches the updated validation semantics. To reduce brittleness, consider lowercasing the exception string before assertIn.

Proposed tweak
         with self.assertRaises(ValueError) as ctx:
             builder.finalize()
-        self.assertIn("not belonging to any articulation", str(ctx.exception))
+        self.assertIn("not belonging to any articulation", str(ctx.exception).lower())
🧹 Nitpick comments (1)
newton/_src/sim/builder.py (1)

6092-6119: Finalize-time warning is a good guardrail; consider making the warning message easier to read.
The condition (only shapes that collide; warn when thickness > contact_margin) matches the broad-phase risk described. The current formatting prints a Python list repr.

Suggested tweak: join the sample shapes into a string
         if shapes_with_bad_margin:
-            example_shapes = shapes_with_bad_margin[:5]
+            example_shapes = ", ".join(shapes_with_bad_margin[:5])
             warnings.warn(
                 f"Found {len(shapes_with_bad_margin)} shape(s) with thickness > contact_margin. "
                 f"This can cause missed collisions in broad phase since AABBs are only expanded by contact_margin. "
                 f"Set contact_margin >= thickness for each shape. "
-                f"Affected shapes: {example_shapes}" + ("..." if len(shapes_with_bad_margin) > 5 else ""),
+                f"Affected shapes: {example_shapes}" + ("..." if len(shapes_with_bad_margin) > 5 else ""),
                 stacklevel=2,
             )
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a0e9c3 and d68b522.

📒 Files selected for processing (6)
  • newton/_src/geometry/narrow_phase.py
  • newton/_src/geometry/sdf_contact.py
  • newton/_src/geometry/sdf_hydroelastic.py
  • newton/_src/sim/builder.py
  • newton/_src/sim/collide_unified.py
  • newton/tests/test_mujoco_solver.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/_src/geometry/sdf_contact.py
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: nvtw
Repo: newton-physics/newton PR: 981
File: newton/_src/geometry/collision_convex.py:180-289
Timestamp: 2025-10-24T07:56:14.792Z
Learning: In newton/_src/geometry/collision_convex.py, the single-contact solver (create_solve_convex_single_contact) intentionally returns count=1 even when shapes are separated (signed_distance > contact_threshold). The calling code is responsible for deciding whether to accept the contact based on the signed distance value. This design pushes filtering responsibility to the caller.
Learnt from: nvtw
Repo: newton-physics/newton PR: 899
File: newton/tests/test_rigid_contact.py:214-268
Timestamp: 2025-10-10T10:47:41.082Z
Learning: In `newton/tests/test_rigid_contact.py`, using `add_shape_plane(width=0, length=0)` for an infinite plane works correctly with the collision system and does not cause degenerate AABB issues in the test scenario `test_shape_collisions_gjk_mpr_multicontact`.
📚 Learning: 2025-12-12T17:45:26.847Z
Learnt from: vastsoun
Repo: newton-physics/newton PR: 1019
File: newton/_src/solvers/kamino/geometry/primitives.py:0-0
Timestamp: 2025-12-12T17:45:26.847Z
Learning: In newton/_src/solvers/kamino/geometry/primitives.py, the narrow-phase kernel `_primitive_narrowphase` does not need to handle symmetric shape-pair orderings (e.g., both SPHERE-BOX and BOX-SPHERE) because `kamino.core.builder.ModelBuilder` and `kamino.geometry.primitive.broadphase` guarantee that explicit candidate geometry pairs are always defined with GIDs in ascending order.

Applied to files:

  • newton/_src/geometry/narrow_phase.py
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
Repo: newton-physics/newton PR: 519
File: newton/geometry.py:16-22
Timestamp: 2025-08-12T17:51:37.474Z
Learning: In the Newton public API refactor, common geometry symbols like ParticleFlags and ShapeFlags are now exposed at the top level in newton/__init__.py rather than through newton.geometry. The newton/geometry.py module is intentionally focused only on broad-phase collision detection classes (BroadPhaseAllPairs, BroadPhaseExplicit, BroadPhaseSAP).

Applied to files:

  • newton/_src/geometry/narrow_phase.py
  • newton/_src/sim/collide_unified.py
  • newton/_src/geometry/sdf_hydroelastic.py
  • newton/_src/sim/builder.py
📚 Learning: 2025-10-24T07:56:14.792Z
Learnt from: nvtw
Repo: newton-physics/newton PR: 981
File: newton/_src/geometry/collision_convex.py:180-289
Timestamp: 2025-10-24T07:56:14.792Z
Learning: In newton/_src/geometry/collision_convex.py, the single-contact solver (create_solve_convex_single_contact) intentionally returns count=1 even when shapes are separated (signed_distance > contact_threshold). The calling code is responsible for deciding whether to accept the contact based on the signed distance value. This design pushes filtering responsibility to the caller.

Applied to files:

  • newton/_src/geometry/narrow_phase.py
  • newton/_src/sim/collide_unified.py
  • newton/_src/geometry/sdf_hydroelastic.py
  • newton/_src/sim/builder.py
📚 Learning: 2025-10-10T10:47:41.082Z
Learnt from: nvtw
Repo: newton-physics/newton PR: 899
File: newton/tests/test_rigid_contact.py:214-268
Timestamp: 2025-10-10T10:47:41.082Z
Learning: In `newton/tests/test_rigid_contact.py`, using `add_shape_plane(width=0, length=0)` for an infinite plane works correctly with the collision system and does not cause degenerate AABB issues in the test scenario `test_shape_collisions_gjk_mpr_multicontact`.

Applied to files:

  • newton/_src/geometry/narrow_phase.py
  • newton/_src/sim/collide_unified.py
📚 Learning: 2025-09-26T05:58:21.013Z
Learnt from: WenchaoHuang
Repo: newton-physics/newton PR: 835
File: newton/_src/solvers/style3d/collision/collision.py:49-53
Timestamp: 2025-09-26T05:58:21.013Z
Learning: In newton/_src/solvers/style3d/collision/collision.py, the broad_phase buffers (broad_phase_ee, broad_phase_ef, broad_phase_vf) created with wp.array() don't need zero-initialization because they follow a write-first-read-later pattern: BVH query kernels in frame_begin() populate these buffers completely (write-only operations), and collision kernels only read from them afterward in accumulate_contact_force(), ensuring no undefined reads occur.

Applied to files:

  • newton/_src/geometry/narrow_phase.py
  • newton/_src/sim/collide_unified.py
📚 Learning: 2025-12-12T08:45:51.908Z
Learnt from: nvtw
Repo: newton-physics/newton PR: 1221
File: newton/examples/example_sdf.py:277-287
Timestamp: 2025-12-12T08:45:51.908Z
Learning: In Newton examples (e.g., example_sdf.py), computing contacts once per frame and reusing them across multiple substeps is an intentional design choice, not a bug. The contacts are computed before the substep loop and intentionally kept constant throughout all substeps within that frame.

Applied to files:

  • newton/_src/sim/collide_unified.py
📚 Learning: 2025-08-20T18:02:36.099Z
Learnt from: eric-heiden
Repo: newton-physics/newton PR: 587
File: newton/_src/sim/builder.py:656-661
Timestamp: 2025-08-20T18:02:36.099Z
Learning: In Newton physics, collisions between shapes with body index -1 (world-attached/global shapes) are automatically skipped by the collision system, so no manual collision filter pairs need to be added between global shapes from different builders.

Applied to files:

  • newton/_src/sim/builder.py
📚 Learning: 2025-09-22T21:03:39.624Z
Learnt from: dylanturpin
Repo: newton-physics/newton PR: 806
File: newton/_src/sim/ik/ik_lbfgs_optimizer.py:739-752
Timestamp: 2025-09-22T21:03:39.624Z
Learning: The L-BFGS optimizer in newton/_src/sim/ik/ik_lbfgs_optimizer.py currently intentionally only supports additive updates (assuming n_coords == n_dofs). Velocity space integration for joints with mismatched coordinate/DOF dimensions (like free/ball joints) is planned for future work and should not be flagged as an issue in current reviews.

Applied to files:

  • newton/_src/sim/builder.py
🧬 Code graph analysis (2)
newton/tests/test_mujoco_solver.py (1)
newton/_src/sim/builder.py (1)
  • finalize (6032-6706)
newton/_src/sim/builder.py (1)
newton/_src/geometry/flags.py (1)
  • ShapeFlags (30-48)
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Run GPU Benchmarks / Run GPU Benchmarks on AWS EC2
  • GitHub Check: Run GPU Tests / Run GPU Unit Tests on AWS EC2
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (8)
newton/_src/sim/collide_unified.py (1)

117-120: LGTM! Margin summation aligns with thickness handling.

The change from max(margin_a, margin_b) to margin_a + margin_b correctly mirrors how thickness is combined on lines 96-98. This ensures the contact margin threshold is consistent with the total separation calculation.

Note: This is a breaking change that increases the effective contact margin (sum ≥ max for non-negative values), which may generate contacts at greater separation distances than before. Please verify that the migration guide update mentioned in the PR objectives documents this behavioral change for users upgrading existing simulations.

newton/_src/sim/builder.py (1)

167-175: Docstrings align with the new “sum” semantics (thickness + per-shape AABB expansion).
Nice clarification that thickness contributes as thickness_a + thickness_b, and that broad-phase expansion is per-shape via contact_margin.

newton/_src/geometry/sdf_hydroelastic.py (1)

1197-1201: Good consistency switch to sum-of-margins; consider clamping to non-negative and add/keep threshold tests.

The summed margin is applied consistently across the SDF-hydro pipeline. As a small defensive improvement, consider margin = wp.max(margin_a + margin_b, 0.0) at each site to avoid surprising behavior if margins can ever be negative.

Also applies to: 1389-1393, 1669-1673

newton/_src/geometry/narrow_phase.py (5)

296-299: LGTM! Margin summing aligns with thickness handling.

The change from max(margin_a, margin_b) to margin_a + margin_b is consistent with how thickness values are already combined (summed) in write_contact_simple at lines 82-84. This ensures the margin calculation is compatible with the thickness model.

Note: This is a behavioral change—contacts will now be detected at greater separation distances when both shapes have non-zero margins. The PR mentions the migration guide has been updated accordingly.


382-385: LGTM!

Consistent margin summing applied to the mesh-convex midphase overlap detection.


464-467: LGTM!

Consistent margin summing applied to mesh-triangle contact processing.


558-561: LGTM!

Consistent margin summing applied to the non-reduction mesh-plane kernel. The contact threshold at line 580 correctly combines margin + total_thickness where both values are now sums of per-shape contributions.


685-688: LGTM!

Consistent margin summing applied to the contact-reduction variant of the mesh-plane kernel, matching the non-reduction variant.

@nvtw nvtw added this pull request to the merge queue Jan 12, 2026
Merged via the queue into newton-physics:main with commit a9aff99 Jan 12, 2026
22 checks passed
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.

2 participants