Skip to content

Integrate mjwarp primitive collision funcs#693

Merged
nvtw merged 19 commits into
newton-physics:mainfrom
nvtw:dev/tw/integrate_mjwarp_primitive_collision_funcs
Sep 9, 2025
Merged

Integrate mjwarp primitive collision funcs#693
nvtw merged 19 commits into
newton-physics:mainfrom
nvtw:dev/tw/integrate_mjwarp_primitive_collision_funcs

Conversation

@nvtw

@nvtw nvtw commented Sep 1, 2025

Copy link
Copy Markdown
Member

Description

Add MJWarp primitive collision functions and comprehensive tests

  • MJWarp Integration: Exposes primitive collision functions from MJWarp (sphere_sphere, plane_box, box_box, etc.) as public Newton API
  • Tests: Implemented comprehensive test suite for all 12 core collision primitives with physics-correct validation
  • API: Made collision functions and matrix types (mat23f, mat43f, etc.) available via newton.geometry

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

  • New Features

    • Added comprehensive collision detection utilities across common 3D shapes (planes, spheres, capsules, cylinders, boxes, ellipsoids), including multi-contact results where applicable.
    • Expanded the public geometry API to expose these collision utilities for direct use.
  • Tests

    • Introduced an extensive test suite covering diverse collision scenarios (separation, contact, penetration) for all supported shape pairs, validating distances, normals, and contact counts.
  • Chores

    • Improved module exports to make new geometry capabilities readily accessible without breaking existing usage.

@nvtw nvtw self-assigned this Sep 1, 2025
@coderabbitai

coderabbitai Bot commented Sep 1, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a new collision_primitive module implementing multiple shape-pair collision routines, re-exports these functions via newton._src.geometry and newton.geometry, and introduces comprehensive Warp-based unit tests covering all new collision functions.

Changes

Cohort / File(s) Summary of edits
Collision primitives implementation
newton/_src/geometry/collision_primitive.py
New module defining helpers and 12 collision routines (plane/sphere/capsule/cylinder/box/ellipsoid combinations), including multi-contact variants; introduces numerous Warp functions and geometric utilities.
Geometry re-exports (internal and public)
newton/_src/geometry/__init__.py, newton/geometry.py
Re-export new collision functions from collision_primitive; update all to expose them in both internal and public geometry modules.
Tests for collision primitives
newton/tests/test_collision_primitives.py
New test suite with Warp kernels and unittest cases validating distances, contact points, normals/frames, and contact counts across all new collision functions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Public API refactor #519 — Prior work modifying the _src.geometry re-export surface, related to this PR’s added re-exports.

Suggested reviewers

  • mmacklin
  • eric-heiden

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.47% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the integration of MJWarp’s primitive collision functions into the Newton API, clearly reflecting the primary purpose of the changeset.
Description Check ✅ Passed The description accurately outlines the addition of MJWarp primitive collision functions, the accompanying comprehensive test suite, and the API exposure changes, matching the scope of the code modifications.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
		  - name: "Undocumented Breaking Changes"
			  mode: "warning"
			  instructions: |
				  Flag potential breaking changes that are not documented:
				  1. Identify changes to public APIs/exports, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints (including removed/renamed items and changes to types, required params, return values, defaults, or behavior).
				  2. Ignore purely internal/private changes (e.g., code not exported from package entry points or marked internal).
				  3. Verify documentation exists: a "Breaking Change" section in the PR description and updates to CHANGELOG.md.

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@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: 5

🧹 Nitpick comments (20)
newton/_src/geometry/collision_primitive.py (9)

488-492: Use wp.int32 for counters in device code.

contact_count should be a Warp int to avoid host-side ints leaking into kernels.

-    contact_count = 0
+    contact_count = wp.int32(0)

732-737: Boolean mask for wp.where.

Pass booleans, not ints, as the predicate to wp.where for clarity and to avoid implicit casts.

-        r = rotmore @ wp.where(box_idx, rot12, rot21)
-        p = rotmore @ wp.where(box_idx, pos12, pos21)
-        ss = wp.abs(rotmore @ wp.where(box_idx, box2_size, box1_size))
-        s = wp.where(box_idx, box1_size, box2_size)
+        r = rotmore @ wp.where(box_idx != 0, rot12, rot21)
+        p = rotmore @ wp.where(box_idx != 0, pos12, pos21)
+        ss = wp.abs(rotmore @ wp.where(box_idx != 0, box2_size, box1_size))
+        s = wp.where(box_idx != 0, box1_size, box2_size)
@@
-        rw = wp.where(box_idx, box2_rot, box1_rot) @ wp.transpose(rotmore)
-        pw = wp.where(box_idx, box2_pos, box1_pos)
-        normal = wp.where(box_idx, -1.0, 1.0) * wp.transpose(rw)[2]
+        rw = wp.where(box_idx != 0, box2_rot, box1_rot) @ wp.transpose(rotmore)
+        pw = wp.where(box_idx != 0, box2_pos, box1_pos)
+        normal = wp.where(box_idx != 0, -1.0, 1.0) * wp.transpose(rw)[2]

Also applies to: 833-836


33-36: Make safe_div robust to near-zero denominators.

Guard against tiny |y|, not just y==0, to avoid huge spikes.

-@wp.func
-def safe_div(x: Any, y: Any) -> Any:
-    return x / wp.where(y != 0.0, y, MINVAL)
+@wp.func
+def safe_div(x: Any, y: Any) -> Any:
+    den = wp.where(wp.abs(y) >= MINVAL, y, wp.where(y >= 0.0, MINVAL, -MINVAL))
+    return x / den

242-295: Normal orientation and input normalization conventions need to be stated and tested.

Please document whether normals point from shape A→B and whether inputs (plane normals, axes) must be unit-length. Add tests for near-parallel axes (capsule–capsule, sphere–cylinder, edge–edge in box–box).

Would you like me to add unit tests for parallel/degenerate cases and update docs accordingly?

Also applies to: 388-460, 592-1045, 1105-1443


16-23: ONE epsilon policy.

You mix 1e-15/1e-12/1e-6. Consider a single module-level EPS with comments per usage to avoid brittle thresholds.

Also applies to: 30-31


561-590: Minor: document _compute_rotmore.

A brief comment on the face index mapping helps future maintainers.


113-121: Add docstring for plane_sphere.

Public API without docs.


193-240: Capsule–capsule: consider explicit parallel-axis handling.

You have a TODO; near-parallel segments can cause unstable normals/points. Recommend special-case with projected point clamp.


30-31: MINVAL magnitude.

For float32 kernels, 1e-15 may underflow practical use; 1e-8–1e-6 tends to be safer. Up to you given dataset scales.

newton/_src/geometry/__init__.py (2)

18-35: Public export surface OK, but confirm what should be public.

If MINVAL/helpers were intended to be public per the PR description, they’re not exported here. Decide intentionally: export them or mark them private in docs.

I can wire up the exports or open a follow-up to keep helpers internal.

Also applies to: 57-77


18-35: Single source of truth for symbols.

Since newton/geometry.py re-exports the same names, prefer importing from this package (._src.geometry) there, not directly from the submodule, to avoid drift.

newton/geometry.py (1)

16-34: API layering regression vs prior refactor—confirm intent.

Earlier refactor limited newton.geometry to broad-phase only (learned context). This PR re-exposes many primitives/types here. If that’s intentional, proceed and update migration notes; if not, keep these only in newton._src.geometry or top-level.

Recommend re-exporting from ._src.geometry (not ._src.geometry.collision_primitive) to keep one authoritative export point.

-from ._src.geometry.collision_primitive import (
+from ._src.geometry import (
     box_box,
     capsule_box,
     capsule_capsule,
     mat23f,
     mat43f,
     mat83f,
     plane_box,
     plane_capsule,
     plane_cylinder,
     plane_ellipsoid,
     plane_sphere,
     sphere_box,
     sphere_capsule,
     sphere_cylinder,
     sphere_sphere,
     vec8f,
 )
newton/tests/test_collision_primitives.py (8)

29-31: Prefer explicit wp.float32 for Warp array dtypes (applies across file).

Using Python float relies on implicit mapping; being explicit avoids device/precision surprises.

Example change:

-    sphere_radii: wp.array(dtype=float),
-    distances: wp.array(dtype=float),
+    sphere_radii: wp.array(dtype=wp.float32),
+    distances: wp.array(dtype=wp.float32),

310-313: Avoid clearing the Warp kernel cache before every test; move to setUpClass and drop the main clear.

Clearing per-test slows the suite and defeats kernel caching benefits.

Apply:

-class TestCollisionPrimitives(unittest.TestCase):
-    def setUp(self):
-        wp.clear_kernel_cache()
+class TestCollisionPrimitives(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        wp.clear_kernel_cache()

And remove the duplicate clear at module run (see separate diff).


863-865: Remove duplicate cache clear at module run.

Redundant with setUpClass; saves a sync and avoids extra cache churn.

-if __name__ == "__main__":
-    wp.clear_kernel_cache()
-    unittest.main(verbosity=2, failfast=True)
+if __name__ == "__main__":
+    unittest.main(verbosity=2, failfast=True)

367-379: Strengthen sphere-sphere verification: assert expected normal and contact position.

Validates the math contract, not just magnitudes.

-        distances_np = distances.numpy()
-        normals_np = contact_normals.numpy()
+        distances_np = distances.numpy()
+        positions_np = contact_positions.numpy()
+        normals_np = contact_normals.numpy()

         # Verify expected distances
         for i, expected_dist in enumerate([tc[4] for tc in test_cases]):
             self.assertAlmostEqual(distances_np[i], expected_dist, places=6, msg=f"Distance failed for test case {i}")

-        # Check normal vectors are unit length (except for zero distance case)
-        for i in range(len(test_cases)):
-            if i != 2:  # Skip exact touching case
-                normal_length = np.linalg.norm(normals_np[i])
-                self.assertAlmostEqual(normal_length, 1.0, places=6, msg=f"Normal not unit length for test case {i}")
+        # Check normal orientation and contact position for non-degenerate cases (0 and 1)
+        for i in (0, 1):
+            p1 = np.array(test_cases[i][0], dtype=np.float32)
+            p2 = np.array(test_cases[i][2], dtype=np.float32)
+            r1 = float(test_cases[i][1])
+            dir_vec = p2 - p1
+            expected_n = dir_vec / np.linalg.norm(dir_vec)
+            np.testing.assert_allclose(normals_np[i], expected_n, rtol=1e-5, atol=1e-6, err_msg=f"Normal mismatch case {i}")
+            expected_pos = p1 + expected_n * (r1 + 0.5 * distances_np[i])
+            np.testing.assert_allclose(positions_np[i], expected_pos, rtol=1e-5, atol=1e-6, err_msg=f"Contact position mismatch case {i}")

690-698: Use np.isfinite for robust contact counting.

Safer than comparing to float('inf') and also guards against NaNs.

-        for i in range(len(test_cases)):
-            valid_contacts = sum(1 for d in distances_np[i] if d != float("inf"))
+        for i in range(len(test_cases)):
+            valid_contacts = int(np.isfinite(distances_np[i]).sum())

739-747: Same robustness for plane-cylinder contact counting.

-        valid_contacts_0 = sum(1 for d in distances_np[0] if d != float("inf"))
-        valid_contacts_1 = sum(1 for d in distances_np[1] if d != float("inf"))
+        valid_contacts_0 = int(np.isfinite(distances_np[0]).sum())
+        valid_contacts_1 = int(np.isfinite(distances_np[1]).sum())

802-808: Use np.isfinite for box-box valid contact counting.

-        for i in range(len(test_cases)):
-            valid_contacts = sum(1 for j in range(8) if distances_np[i][j] != float("inf"))
+        for i in range(len(test_cases)):
+            valid_contacts = int(np.isfinite(distances_np[i]).sum())

330-336: Optional: exercise both CPU and CUDA (if available) to catch backend-specific issues.

Parameterizing device (e.g., run each kernel on ["cpu"] + ["cuda:0"] if wp.is_cuda_available()) improves coverage with minimal overhead.

I can provide a small helper to launch each kernel across available devices if you want it in this PR.

Also applies to: 360-366, 399-415, 447-465, 493-507, 536-552, 582-597, 624-640, 672-687, 719-735, 782-798, 833-850

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 89366ca and 06db8c4.

📒 Files selected for processing (4)
  • newton/_src/geometry/__init__.py (2 hunks)
  • newton/_src/geometry/collision_primitive.py (1 hunks)
  • newton/geometry.py (1 hunks)
  • newton/tests/test_collision_primitives.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: nvlukasz
PR: newton-physics/newton#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).
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
PR: newton-physics/newton#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/__init__.py
  • newton/geometry.py
  • newton/_src/geometry/collision_primitive.py
🧬 Code graph analysis (3)
newton/_src/geometry/__init__.py (1)
newton/_src/geometry/collision_primitive.py (16)
  • box_box (593-1044)
  • capsule_box (1105-1442)
  • capsule_capsule (194-239)
  • mat23f (100-101)
  • mat43f (104-105)
  • mat83f (108-109)
  • plane_box (330-385)
  • plane_capsule (243-294)
  • plane_cylinder (462-557)
  • plane_ellipsoid (298-326)
  • plane_sphere (114-120)
  • sphere_box (1048-1101)
  • sphere_capsule (157-190)
  • sphere_cylinder (389-458)
  • sphere_sphere (124-153)
  • vec8f (96-97)
newton/geometry.py (1)
newton/_src/geometry/collision_primitive.py (16)
  • box_box (593-1044)
  • capsule_box (1105-1442)
  • capsule_capsule (194-239)
  • mat23f (100-101)
  • mat43f (104-105)
  • mat83f (108-109)
  • plane_box (330-385)
  • plane_capsule (243-294)
  • plane_cylinder (462-557)
  • plane_ellipsoid (298-326)
  • plane_sphere (114-120)
  • sphere_box (1048-1101)
  • sphere_capsule (157-190)
  • sphere_cylinder (389-458)
  • sphere_sphere (124-153)
  • vec8f (96-97)
newton/tests/test_collision_primitives.py (1)
newton/_src/geometry/collision_primitive.py (15)
  • plane_sphere (114-120)
  • sphere_sphere (124-153)
  • sphere_capsule (157-190)
  • capsule_capsule (194-239)
  • plane_ellipsoid (298-326)
  • sphere_cylinder (389-458)
  • sphere_box (1048-1101)
  • mat23f (100-101)
  • plane_capsule (243-294)
  • mat43f (104-105)
  • plane_box (330-385)
  • vec8f (96-97)
  • mat83f (108-109)
  • box_box (593-1044)
  • capsule_box (1105-1442)
⏰ 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 Benchmarks (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
🔇 Additional comments (14)
newton/_src/geometry/collision_primitive.py (10)

329-386: Plane-box corner filtering logic double-check.

if center_dist + ldist > 0 or ldist > 0: makes the second condition redundant or overly strict in some configurations. Consider removing or ldist > 0 and rely on signed corner distance to the plane.


592-1045: Edge case: hz scope and initialization.

Ensure hz is always defined before used outside the branch; it’s set in both branches today—keep that invariant if refactoring.


1303-1311: Fast-return sentinel OK.

Returning (wp.vec2(wp.inf), mat23f(), mat23f()) on invalid cltype is a good sentinel pattern.


123-154: Sphere–sphere core looks solid.

Graceful zero-distance handling and midpoint contact are correct.


461-557: Plane–cylinder strategy reads well.

Good coverage of cap/side/triangle sampling; minor counter typing aside.


1048-1102: Sphere–box implementation LGTM.

Clamp/inside detection and midpoint contact are consistent with the module’s convention.


1105-1443: Capsule–box algorithm parity with MJC noted.

Good to reuse sphere_box for contacts once closest segment is found.


269-295: Plane–capsule contact frame is sensible.

Fallback basis selection avoids degeneracy when axis≈normal.


388-460: Sphere–cylinder path split is clear.

Side/cap/corner separation is clean; see earlier notes on normals and epsilons.


47-60: Closest-point helpers are fine.

Stable and succinct.

Also applies to: 62-94

newton/geometry.py (1)

38-61: all expansion is fine; ensure docs/migration.rst updated.

Please add a note that these symbols are available under both newton._src.geometry and newton.geometry, or clarify the recommended import path.

newton/tests/test_collision_primitives.py (3)

1-15: Comprehensive, well-structured test coverage for all 12 primitives.

Good use of Warp kernels, clear assertions, and reusability across cases. This substantially increases confidence in the new public API.


853-861: Clarify capsule_box semantics for separated cases.

Does capsule_box guarantee dist1 is always a finite witness distance even when separated (with dist2=inf), or are both entries inf when no contact? Current assertion requires at least one finite in both cases, which will fail if the API uses (inf, inf) for “no contact.”

If the intended behavior is “finite witness, second inf,” keep the test. If not, update as:

-        for i in range(len(test_cases)):
-            dist1, dist2 = distances_np[i][0], distances_np[i][1]
-            # At least one distance should be valid (not inf)
-            self.assertTrue(
-                dist1 != float("inf") or dist2 != float("inf"),
-                msg=f"Should have at least one valid contact for test case {i}",
-            )
+        for i in range(len(test_cases)):
+            finite_count = int(np.isfinite(distances_np[i]).sum())
+            if i == 0:  # intersecting
+                self.assertGreaterEqual(finite_count, 1, "Intersecting capsule-box should yield >=1 finite distance")
+            else:       # separated
+                self.assertIn(finite_count, (0, 1), "Separated capsule-box should yield 0 or 1 finite distances per API")

21-21: Public API re-exports complete. Confirmed that newton/geometry.py re-exports all collision-primitive functions (box_box, capsule_box, capsule_capsule, plane_box, plane_capsule, plane_cylinder, plane_ellipsoid, plane_sphere, sphere_box, sphere_capsule, sphere_cylinder, sphere_sphere) and matrix/vector types (mat23f, mat43f, mat83f, vec8f) in its __all__, matching the symbols defined in newton/_src/geometry/__init__.py. Tests can safely import these via from newton import geometry.

Comment thread newton/_src/geometry/collision_primitive.py
Comment thread newton/_src/geometry/collision_primitive.py Outdated
Comment thread newton/_src/geometry/collision_primitive.py Outdated
Comment thread newton/_src/geometry/collision_primitive.py Outdated
Comment thread newton/_src/geometry/collision_primitive.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: 6

🧹 Nitpick comments (13)
newton/geometry.py (1)

17-34: API surface change: newton.geometry now re-exports many primitives—confirm alignment with prior API policy.

Past refactor (PR #519) focused geometry.py on broad-phase only. Expanding it now is fine if intentional, but please confirm with maintainers and update docs/migration (noted as unchecked in PR). Consider also re-exporting at top-level newton/init.py if that’s our canonical surface.

Would you like a short docs snippet for docs/migration.rst explaining the new geometry.* entry points?

newton/_src/geometry/collision_primitive.py (6)

46-52: Prefer safe_div over manual epsilon in closest_segment_point.

Consistency and fewer magic numbers.

-    t = wp.dot(pt - a, ab) / (wp.dot(ab, ab) + 1e-6)
+    t = safe_div(wp.dot(pt - a, ab), wp.dot(ab, ab))

112-121: Add/clarify docstring for plane_sphere (currently TODO).

Public API; document inputs/outputs to match others.

-    # TODO(team): docstring
+    """Core contact geometry calculation for plane-sphere collision.
+
+    Returns:
+      dist: signed distance from plane to sphere surface (negative on penetration)
+      pos: contact position on sphere
+    """

315-321: Docstring return description doesn’t match outputs.

Function returns (float, vec3, vec3), not matrices.

-    Returns:
-      Tuple containing:
-        contact_dist: Vector of contact distances
-        contact_pos: Matrix of contact positions (one per row)
-        contact_normals: Matrix of contact normal vectors (one per row)
+    Returns:
+      dist: signed distance
+      pos: contact position
+      normal: contact normal

471-487: Docstring: plane_cylinder returns a single normal vector, not a matrix.

Align wording with actual signature (wp.vec4, mat43f, wp.vec3).

-        contact_normals: Matrix of contact normal vectors (one per row)
+        contact_normal: contact normal vector

224-239: Unhandled parallel-axes TODO in capsule_capsule.

Edge case will degrade accuracy; add a branch or at least a test that exercises the parallel case so we don’t regress silently.

I can implement the parallel-axis handling using projected segment parameters if helpful.


360-385: Use wp.int32 for counters for consistency.

ncontact is declared as int(0); prefer wp.int32 like elsewhere.

-    ncontact = int(0)
+    ncontact = wp.int32(0)
newton/tests/test_collision_primitives.py (6)

21-21: Confirm API surface for newton.geometry import.

Recent refactor exposed common geometry symbols at the top-level newton/__init__.py, while newton.geometry was kept for broad-phase classes. This PR reintroduces primitives under newton.geometry. Please confirm this intended public API change and update the migration guide accordingly to prevent user confusion. I can help draft the migration note.


18-22: Make float dtype explicit to avoid backend-dependent drift.

Use a module-level alias to wp.float32 and reference it for all scalar arrays. This improves determinism across CPU/GPU and readability.

 import numpy as np
 import warp as wp
 
+F = wp.float32
 from newton import geometry

Apply mechanically across the file:

  • Replace dtype=float with dtype=F.
  • Prefer wp.zeros(..., dtype=F) over list-initialized float arrays.

323-329: Prefer wp.zeros for initialization (clarity + perf).

Initialize device arrays with wp.zeros instead of Python lists; it’s clearer and avoids an extra host->device copy.

-        distances = wp.array([0.0] * len(test_cases), dtype=float)
-        contact_positions = wp.array([wp.vec3(0.0, 0.0, 0.0)] * len(test_cases), dtype=wp.vec3)
+        distances = wp.zeros(len(test_cases), dtype=F)
+        contact_positions = wp.zeros(len(test_cases), dtype=wp.vec3)

375-378: Don’t skip the unit-normal check for the touching case.

geometry.sphere_sphere returns a unit normal even at exact contact; this assertion can cover all cases.

-        for i in range(len(test_cases)):
-            if i != 2:  # Skip exact touching case
-                normal_length = np.linalg.norm(normals_np[i])
-                self.assertAlmostEqual(normal_length, 1.0, places=6, msg=f"Normal not unit length for test case {i}")
+        for i in range(len(test_cases)):
+            normal_length = np.linalg.norm(normals_np[i])
+            self.assertAlmostEqual(normal_length, 1.0, places=6, msg=f"Normal not unit length for test case {i}")

560-571: Factor a shared identity matrix constant.

identity = wp.mat33(...) is repeated. Define once at module level (e.g., IDENTITY33) and reuse to reduce noise.

Example:

# near imports
IDENTITY33 = wp.mat33(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)

760-809: Optional: add a rotated box–box case.

Consider adding a rotated–rotated overlap and separation case to exercise the edge–edge path and contact multiplicity, not just axis-aligned boxes.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 89366ca and 06db8c4.

📒 Files selected for processing (4)
  • newton/_src/geometry/__init__.py (2 hunks)
  • newton/_src/geometry/collision_primitive.py (1 hunks)
  • newton/geometry.py (1 hunks)
  • newton/tests/test_collision_primitives.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: nvlukasz
PR: newton-physics/newton#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).
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
PR: newton-physics/newton#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/__init__.py
  • newton/geometry.py
  • newton/_src/geometry/collision_primitive.py
🧬 Code graph analysis (3)
newton/_src/geometry/__init__.py (1)
newton/_src/geometry/collision_primitive.py (16)
  • box_box (593-1044)
  • capsule_box (1105-1442)
  • capsule_capsule (194-239)
  • mat23f (100-101)
  • mat43f (104-105)
  • mat83f (108-109)
  • plane_box (330-385)
  • plane_capsule (243-294)
  • plane_cylinder (462-557)
  • plane_ellipsoid (298-326)
  • plane_sphere (114-120)
  • sphere_box (1048-1101)
  • sphere_capsule (157-190)
  • sphere_cylinder (389-458)
  • sphere_sphere (124-153)
  • vec8f (96-97)
newton/geometry.py (1)
newton/_src/geometry/collision_primitive.py (16)
  • box_box (593-1044)
  • capsule_box (1105-1442)
  • capsule_capsule (194-239)
  • mat23f (100-101)
  • mat43f (104-105)
  • mat83f (108-109)
  • plane_box (330-385)
  • plane_capsule (243-294)
  • plane_cylinder (462-557)
  • plane_ellipsoid (298-326)
  • plane_sphere (114-120)
  • sphere_box (1048-1101)
  • sphere_capsule (157-190)
  • sphere_cylinder (389-458)
  • sphere_sphere (124-153)
  • vec8f (96-97)
newton/tests/test_collision_primitives.py (1)
newton/_src/geometry/collision_primitive.py (13)
  • plane_sphere (114-120)
  • sphere_sphere (124-153)
  • sphere_capsule (157-190)
  • capsule_capsule (194-239)
  • plane_ellipsoid (298-326)
  • sphere_cylinder (389-458)
  • sphere_box (1048-1101)
  • mat23f (100-101)
  • mat43f (104-105)
  • vec8f (96-97)
  • mat83f (108-109)
  • box_box (593-1044)
  • capsule_box (1105-1442)
⏰ 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 (Pull Request)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
🔇 Additional comments (3)
newton/_src/geometry/__init__.py (2)

18-35: Re-exports look consistent with internal API surface.

Import list mirrors the functions/types defined in collision_primitive and aligns with the stated PR goals.


47-77: all correctly exposes new primitives and types.

No ordering or duplication issues spotted. Good to go.

newton/geometry.py (1)

38-61: Double-check downstream star-import behavior.

Adding many names to all changes from newton.geometry import * in user code. Low risk, but worth a quick scan in examples/tutorials.

Comment thread newton/_src/geometry/collision_primitive.py
Comment thread newton/_src/geometry/collision_primitive.py Outdated
Comment thread newton/_src/geometry/collision_primitive.py
Comment thread newton/tests/test_collision_primitives.py
Comment thread newton/tests/test_collision_primitives.py Outdated
Comment thread newton/tests/test_collision_primitives.py Outdated

@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: 4

♻️ Duplicate comments (1)
newton/_src/geometry/collision_primitive.py (1)

541-549: Guard zero-length cross in plane_cylinder (prevents NaNs).

When axis ∥ vec, wp.cross(vec, axis) is ~0; normalizing produces NaNs and corrupts contacts. Add a stable fallback.

-    vec1 = wp.cross(vec, axis)
-    vec1 = wp.normalize(vec1) * (cylinder_radius * wp.sqrt(3.0) * 0.5)
+    vec1 = wp.cross(vec, axis)
+    len_v1 = wp.length(vec1)
+    if len_v1 < MINVAL:
+        basis = wp.vec3(1.0, 0.0, 0.0)
+        if wp.abs(wp.dot(basis, axis)) > 0.9:
+            basis = wp.vec3(0.0, 1.0, 0.0)
+        vec1 = wp.cross(basis, axis)
+        len_v1 = wp.length(vec1)
+    vec1 = vec1 * safe_div(cylinder_radius * wp.sqrt(3.0) * 0.5, len_v1)
🧹 Nitpick comments (11)
newton/_src/geometry/collision_primitive.py (5)

47-51: Avoid magic constant; use safe_div for segment parameter.

Hard-coding 1e-6 mixes tolerances. Reuse MINVAL via safe_div for consistency.

 ab = b - a
- t = wp.dot(pt - a, ab) / (wp.dot(ab, ab) + 1e-6)
+ denom = wp.dot(ab, ab)
+ t = safe_div(wp.dot(pt - a, ab), denom)
 return a + wp.clamp(t, 0.0, 1.0) * ab

114-121: Add docstring and document plane normal convention.

Public API needs explicit normal orientation and unit-length requirement.

 @wp.func
 def plane_sphere(
     plane_normal: wp.vec3, plane_pos: wp.vec3, sphere_pos: wp.vec3, sphere_radius: float
 ) -> tuple[float, wp.vec3]:
-    # TODO(team): docstring
+    """Core contact geometry for plane–sphere.
+
+    Args:
+      plane_normal: Unit plane normal pointing from plane toward sphere side considered “outside”.
+      plane_pos: Any point on the plane.
+      sphere_pos: Sphere center.
+      sphere_radius: Sphere radius.
+
+    Returns:
+      (dist, pos):
+        dist: signed distance from sphere surface to plane (negative if penetrating).
+        pos: “midpoint contact” position along plane_normal between closest surface points.
+    """
     dist = wp.dot(sphere_pos - plane_pos, plane_normal) - sphere_radius
     pos = sphere_pos - plane_normal * (sphere_radius + 0.5 * dist)
     return dist, pos

321-326: Clarify normal orientation and unit-length expectations in plane_ellipsoid.

Return value uses plane_normal as the normal; document that it must be unit length or normalize before returning to ensure consistent outputs.

Would you like me to add an in-function normalization (tiny cost) for safety?


329-386: Plane-box: performance/readability nits.

  • Precompute box_rot @ corner via column access to avoid reusing the same local corner variable for both frames.
  • Consider early exit when ncontact == 0 and center_dist > max projected corner distance.

Low-priority; function is already correct.


1047-1101: sphere_box: inside-case math OK; optional: ensure unit normals.

Normals are unit length in both branches given clamped_dir normalization; consider asserting or normalizing once more for safety when center is inside.

newton/tests/test_collision_primitives.py (6)

21-22: Public API surface: confirm newton.geometry exposure is intentional.

Earlier refactors moved common geometry to top-level newton; this PR re-exposes primitives under newton.geometry. Please confirm this namespace decision and update docs/migration.rst accordingly.

I can draft the migration note (warp.sim → newton.geometry) with examples if you like.


340-342: Numerical tolerance: use places=5 or 1e-5 for robustness.

Warp kernels can differ subtly across backends. Loosen by one decimal to reduce flaky tests.

-            self.assertAlmostEqual(distances_np[i], expected_dist, places=6, msg=f"Distance failed for test case {i}")
+            self.assertAlmostEqual(distances_np[i], expected_dist, places=5, msg=f"Distance failed for test case {i}")

374-379: Assert unit normals in all non-degenerate cases.

Also validate for overlapping case when distance ≠ 0; skip only when centers coincide.

-        for i in range(len(test_cases)):
-            if i != 2:  # Skip exact touching case
+        for i in range(len(test_cases)):
+            if not np.isclose(distances_np[i], 0.0):
                 normal_length = np.linalg.norm(normals_np[i])
                 self.assertAlmostEqual(normal_length, 1.0, places=6, msg=f"Normal not unit length for test case {i}")

515-560: Sphere–cylinder tests: great coverage; consider explicit side/cap/corner assertions.

To catch regressions in branch selection, assert which branch was taken via expected normal direction (axis-aligned vs radial) per case.


703-752: Plane–cylinder: contact count assertions are weak.

Function always populates 4 distances (finite), so “more contacts than far” may not detect issues. Consider asserting min distance sign and ordering (caps vs triangle points) instead.


866-870: Set deterministic device for CI.

Kernel caches and device choice can affect numeric drift. Pin with wp.set_device("cpu") or respect WARP_DEVICE in setUp.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 06db8c4 and b4b44a5.

📒 Files selected for processing (2)
  • newton/_src/geometry/collision_primitive.py (1 hunks)
  • newton/tests/test_collision_primitives.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: nvlukasz
PR: newton-physics/newton#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).
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:75-75
Timestamp: 2025-08-12T18:04:06.577Z
Learning: The Newton physics framework requires nightly Warp builds, which means compatibility concerns with older stable Warp versions (like missing functions such as wp.spatial_adjoint) are not relevant for this project.
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
PR: newton-physics/newton#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/collision_primitive.py
🧬 Code graph analysis (1)
newton/tests/test_collision_primitives.py (1)
newton/_src/geometry/collision_primitive.py (15)
  • plane_sphere (114-120)
  • sphere_sphere (124-153)
  • sphere_capsule (157-190)
  • capsule_capsule (194-239)
  • plane_ellipsoid (298-326)
  • sphere_cylinder (389-458)
  • sphere_box (1048-1101)
  • mat23f (100-101)
  • plane_capsule (243-294)
  • mat43f (104-105)
  • plane_box (330-385)
  • vec8f (96-97)
  • mat83f (108-109)
  • box_box (593-1044)
  • capsule_box (1105-1442)
⏰ 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 Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (2)
newton/_src/geometry/collision_primitive.py (2)

592-1045: box_box: algorithm looks solid; confirm device-compatibility on Warp nightly.

Assuming the wp.where boolean fixes above, the SAT + contact generation flow mirrors the MJCF path well. Nice work on keeping epsilons centralized via MINVAL.

If you want, I can script-compile these kernels against Warp nightly to sanity-check JIT types across CUDA/CPU.


1104-1443: capsule_box: good port; keep wp.min/wp.abs/safe_div usage consistent.

The device intrinsics are correctly used; thanks for avoiding Python builtins. No functional issues spotted.

Comment thread newton/_src/geometry/collision_primitive.py
Comment thread newton/_src/geometry/collision_primitive.py
Comment thread newton/_src/geometry/collision_primitive.py
Comment thread newton/_src/geometry/collision_primitive.py
Comment thread newton/geometry.py Outdated

@adenzler-nvidia adenzler-nvidia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it looks good, most things are copy-pasted from MjWarp anyway? So no point in doing an actual code review.

I guess then it's mostly about API surface and whether things are in the right place. Deferring to @nvlukasz for this :)

@nvtw nvtw requested a review from nvlukasz September 1, 2025 13:33
@nvtw

nvtw commented Sep 1, 2025

Copy link
Copy Markdown
Member Author

The primitive collision functions are identical to those in MJWarp. I only removed the wp.set_module_options({"enable_backward": False}) which we have everywhere in MJWarp. The collision tests are new but we know already from MJWarp that these collision functions work.
I also implemented some smaller improvements suggested by CodeRabbit but this Rabbit never stops coming up with change requests, so I won't do all of them.

…funcs

# Conflicts:
#	newton/_src/geometry/__init__.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: 1

♻️ Duplicate comments (5)
newton/_src/geometry/collision_primitive.py (5)

190-226: Docstrings: keep return shapes and normal orientation consistent.

A few functions still describe “matrices of normals” when a single normal is returned (e.g., plane_cylinder, plane_box). Align wording with signatures and the “normal from first shape toward second” convention.

I can push a pass updating these docstrings if you confirm the normal convention.

Also applies to: 228-281, 283-313, 315-371, 374-445, 447-544, 578-1031, 1033-1089


528-535: Guard normalize(cross) to avoid NaNs in plane_cylinder.

When vec ∥ axis, cross=0; normalizing yields NaNs and corrupts contacts.

-    vec1 = wp.cross(vec, axis)
-    vec1 = wp.normalize(vec1) * (cylinder_radius * wp.sqrt(3.0) * 0.5)
+    vec1 = wp.cross(vec, axis)
+    len_v1 = wp.length(vec1)
+    if len_v1 < MINVAL:
+        basis = wp.vec3(1.0, 0.0, 0.0)
+        if wp.abs(wp.dot(basis, axis)) > 0.9:
+            basis = wp.vec3(0.0, 1.0, 0.0)
+        vec1 = wp.cross(basis, axis)
+        len_v1 = wp.length(vec1)
+    vec1 = vec1 * safe_div(cylinder_radius * wp.sqrt(3.0) * 0.5, len_v1)

718-722: wp.where predicates must be boolean, not int.

Cast bitwise results and counters to booleans to avoid Warp JIT typing issues.

-        r = rotmore @ wp.where(box_idx, rot12, rot21)
-        p = rotmore @ wp.where(box_idx, pos12, pos21)
-        ss = wp.abs(rotmore @ wp.where(box_idx, box2_size, box1_size))
-        s = wp.where(box_idx, box1_size, box2_size)
+        pred_box2 = (box_idx != 0)
+        r = rotmore @ wp.where(pred_box2, rot12, rot21)
+        p = rotmore @ wp.where(pred_box2, pos12, pos21)
+        ss = wp.abs(rotmore @ wp.where(pred_box2, box2_size, box1_size))
+        s = wp.where(pred_box2, box1_size, box2_size)
@@
-            lp += rt[i] * s[i] * wp.where(clcorner & 1 << i, 1.0, -1.0)
+            lp += rt[i] * s[i] * wp.where((clcorner & (1 << i)) != 0, 1.0, -1.0)
@@
-                    cn1 = rt[i] * s[i] * wp.where(clcorner & (1 << i), -2.0, 2.0)
+                    cn1 = rt[i] * s[i] * wp.where((clcorner & (1 << i)) != 0, -2.0, 2.0)
                 else:
-                    cn2 = rt[i] * s[i] * wp.where(clcorner & (1 << i), -2.0, 2.0)
+                    cn2 = rt[i] * s[i] * wp.where((clcorner & (1 << i)) != 0, -2.0, 2.0)
@@
-                llx = wp.where(i // 2, lx, -lx)
-                lly = wp.where(i % 2, ly, -ly)
+                llx = wp.where((i // 2) != 0, lx, -lx)
+                lly = wp.where((i % 2) != 0, ly, -ly)
@@
-        rw = wp.where(box_idx, box2_rot, box1_rot) @ wp.transpose(rotmore)
-        pw = wp.where(box_idx, box2_pos, box1_pos)
-        normal = wp.where(box_idx, -1.0, 1.0) * wp.transpose(rw)[2]
+        rw = wp.where(pred_box2, box2_rot, box1_rot) @ wp.transpose(rotmore)
+        pw = wp.where(pred_box2, box2_pos, box1_pos)
+        normal = wp.where(pred_box2, -1.0, 1.0) * wp.transpose(rw)[2]
@@
-        rotmore = _compute_rotmore(wp.where(cle1 & (1 << pax2), pax2, pax2 + 3))
+        rotmore = _compute_rotmore(wp.where((cle1 & (1 << pax2)) != 0, pax2, pax2 + 3))
@@
-            + rt[ax1] * box2_size[ax1] * wp.where(cle2 & (1 << ax1), 1.0, -1.0)
-            + rt[ax2] * box2_size[ax2] * wp.where(cle2 & (1 << ax2), 1.0, -1.0)
+            + rt[ax1] * box2_size[ax1] * wp.where((cle2 & (1 << ax1)) != 0, 1.0, -1.0)
+            + rt[ax2] * box2_size[ax2] * wp.where((cle2 & (1 << ax2)) != 0, 1.0, -1.0)
@@
-            + rt[ax1] * box2_size[ax1] * wp.where(cle2 & (1 << ax1), -1.0, 1.0)
-            + rt[ax2] * box2_size[ax2] * wp.where(cle2 & (1 << ax2), 1.0, -1.0)
+            + rt[ax1] * box2_size[ax1] * wp.where((cle2 & (1 << ax1)) != 0, -1.0, 1.0)
+            + rt[ax2] * box2_size[ax2] * wp.where((cle2 & (1 << ax2)) != 0, 1.0, -1.0)
@@
-            la = pts_lp[q] + wp.where(i < 2, 0.0, wp.where(i == 2, pts_cn1[q], pts_cn2[q]))
-            lb = wp.where(i == 0 or i == 3, pts_cn1[q], pts_cn2[q])
-            lc = pts_lp[1 - q] + wp.where(i < 2, 0.0, wp.where(i == 2, pts_cn1[1 - q], pts_cn2[1 - q]))
-            ld = wp.where(i == 0 or i == 3, pts_cn1[1 - q], pts_cn2[1 - q])
+            la = pts_lp[q] + wp.where((i < 2), 0.0, wp.where(i == 2, pts_cn1[q], pts_cn2[q]))
+            lb = wp.where((i == 0) or (i == 3), pts_cn1[q], pts_cn2[q])
+            lc = pts_lp[1 - q] + wp.where((i < 2), 0.0, wp.where(i == 2, pts_cn1[1 - q], pts_cn2[1 - q]))
+            ld = wp.where((i == 0) or (i == 3), pts_cn1[1 - q], pts_cn2[1 - q])
@@
-            llx = wp.where(i // 2, lx, -lx)
-            lly = wp.where(i % 2, ly, -ly)
+            llx = wp.where((i // 2) != 0, lx, -lx)
+            lly = wp.where((i % 2) != 0, ly, -ly)

Also update predicates similarly in capsule-box (see below).

Also applies to: 734-737, 746-749, 786-788, 819-822, 841-841, 855-867, 871-871, 889-910, 943-945


1188-1196: wp.where int predicates in capsule-box corner vector.

Make them boolean.

-                wp.vec3(
-                    wp.where(i & 1, 1.0, -1.0),
-                    wp.where(i & 2, 1.0, -1.0),
-                    wp.where(i & 4, 1.0, -1.0),
-                ),
+                wp.vec3(
+                    wp.where((i & 1) != 0, 1.0, -1.0),
+                    wp.where((i & 2) != 0, 1.0, -1.0),
+                    wp.where((i & 4) != 0, 1.0, -1.0),
+                ),

35-46: Numerical guardrails: safe_div/normalize need epsilon-based checks.

Avoid exact-zero comparisons to prevent spikes/NaNs on device.

Apply:

 @wp.func
 def safe_div(x: Any, y: Any) -> Any:
-    return x / wp.where(y != 0.0, y, MINVAL)
+    abs_y = wp.abs(y)
+    y_safe = wp.where(abs_y >= MINVAL, y, wp.where(y >= 0.0, MINVAL, -MINVAL))
+    return x / y_safe

 @wp.func
 def normalize_with_norm(x: Any):
     norm = wp.length(x)
-    if norm == 0.0:
+    if norm < MINVAL:
         return x, 0.0
     return x / norm, norm
🧹 Nitpick comments (4)
newton/_src/core/types.py (1)

133-135: Broaden integer handling in Axis.from_any.

Rely on isinstance against Python/NumPy integer scalars to avoid brittle exact-type set checks.

Apply:

-        if type(value) in {int, wp.int32, wp.int64, np.int32, np.int64}:
-            return cls(value)
+        if isinstance(value, (int, np.integer)):
+            return cls(int(value))
newton/_src/geometry/collision_primitive.py (3)

100-106: Add docstring for collide_plane_sphere.

Briefly document returns and normal orientation.

-# TODO(team): docstring
+"""Signed distance from sphere surface to plane and a contact point on/near the sphere toward the plane.
+
+Returns:
+  (dist, pos) where:
+    dist: signed surface distance (negative if penetrating),
+    pos: midpoint between closest surface points (on sphere toward plane).
+"""

131-139: Zero-distance tolerance in sphere-sphere.

Use epsilon to stabilize normal selection when centers nearly coincide.

-    if dist == 0.0:
+    if dist < MINVAL:
         n = wp.vec3(1.0, 0.0, 0.0)

766-771: Rename ambiguous variable l (Ruff E741).

Use clearer name to satisfy lint and readability.

-                    for j in range(-1, 2, 2):
-                        l = ss[q] * wp.float32(j)
-                        c1 = (l - lav[q]) * br
+                    for j in range(-1, 2, 2):
+                        limit = ss[q] * wp.float32(j)
+                        c1 = (limit - lav[q]) * br
-                        if n == max_con_pair:
+                        if n == max_con_pair:
                             break
-                        l = s[q] * wp.float32(j)
-                        c1 = (l - la) * br
+                        limit = s[q] * wp.float32(j)
+                        c1 = (limit - la) * br

Also applies to: 913-919

📜 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 283b021 and e7b07b8.

📒 Files selected for processing (7)
  • newton/__init__.py (1 hunks)
  • newton/_src/core/__init__.py (2 hunks)
  • newton/_src/core/types.py (2 hunks)
  • newton/_src/geometry/__init__.py (2 hunks)
  • newton/_src/geometry/collision_primitive.py (1 hunks)
  • newton/geometry.py (1 hunks)
  • newton/tests/test_collision_primitives.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/tests/test_collision_primitives.py
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:75-75
Timestamp: 2025-08-12T18:04:06.577Z
Learning: The Newton physics framework requires nightly Warp builds, which means compatibility concerns with older stable Warp versions (like missing functions such as wp.spatial_adjoint) are not relevant for this project.
Learnt from: nvlukasz
PR: newton-physics/newton#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).
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
PR: newton-physics/newton#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/core/__init__.py
  • newton/_src/geometry/__init__.py
  • newton/__init__.py
  • newton/geometry.py
  • newton/_src/geometry/collision_primitive.py
📚 Learning: 2025-08-12T17:58:00.815Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:18-20
Timestamp: 2025-08-12T17:58:00.815Z
Learning: In newton/_src/core/__init__.py, the function transform_twist is properly re-exported from the spatial module via "from .spatial import (..., transform_twist, ...)" and included in __all__, making it available for import as "from ...core import transform_twist" from other modules.

Applied to files:

  • newton/_src/core/__init__.py
  • newton/__init__.py
📚 Learning: 2025-08-12T18:04:06.287Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/utils/import_usd.py:364-365
Timestamp: 2025-08-12T18:04:06.287Z
Learning: The quat_between_axes function in Newton accepts AxisType parameters, which includes both Axis enum values and strings. The function internally handles string-to-Axis conversion using Axis.from_any(), so passing strings directly to quat_between_axes is valid and correct.

Applied to files:

  • newton/_src/core/__init__.py
📚 Learning: 2025-08-12T18:04:06.287Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/utils/import_usd.py:364-365
Timestamp: 2025-08-12T18:04:06.287Z
Learning: The quat_between_axes function in Newton accepts AxisType parameters, which includes strings like "X", "Y", "Z" as well as Axis enum values and integers. The function internally handles conversion using Axis.from_any(), so passing strings directly to quat_between_axes is valid and correct behavior.

Applied to files:

  • newton/_src/core/__init__.py
📚 Learning: 2025-08-14T17:38:36.106Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/__init__.py:25-29
Timestamp: 2025-08-14T17:38:36.106Z
Learning: The Newton project prefers incremental __all__ building using __all__ += [...] pattern to group exports with their related imports, rather than a single consolidated __all__ at the end of the file.

Applied to files:

  • newton/__init__.py
📚 Learning: 2025-08-12T18:04:06.577Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:75-75
Timestamp: 2025-08-12T18:04:06.577Z
Learning: The Newton physics framework requires nightly Warp builds, which means compatibility concerns with older stable Warp versions (like missing functions such as wp.spatial_adjoint) are not relevant for this project.

Applied to files:

  • newton/geometry.py
🧬 Code graph analysis (5)
newton/_src/core/__init__.py (1)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • mat83f (73-74)
  • vec8f (61-62)
newton/_src/geometry/__init__.py (2)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • mat83f (73-74)
  • vec8f (61-62)
newton/_src/geometry/collision_primitive.py (12)
  • collide_box_box (579-1030)
  • collide_capsule_box (1091-1428)
  • collide_capsule_capsule (180-225)
  • collide_plane_box (316-371)
  • collide_plane_capsule (229-280)
  • collide_plane_cylinder (448-543)
  • collide_plane_ellipsoid (284-312)
  • collide_plane_sphere (100-106)
  • collide_sphere_box (1034-1087)
  • collide_sphere_capsule (143-176)
  • collide_sphere_cylinder (375-444)
  • collide_sphere_sphere (110-139)
newton/__init__.py (1)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • mat83f (73-74)
  • vec8f (61-62)
newton/geometry.py (2)
newton/_src/geometry/collision_primitive.py (12)
  • collide_box_box (579-1030)
  • collide_capsule_box (1091-1428)
  • collide_capsule_capsule (180-225)
  • collide_plane_box (316-371)
  • collide_plane_capsule (229-280)
  • collide_plane_cylinder (448-543)
  • collide_plane_ellipsoid (284-312)
  • collide_plane_sphere (100-106)
  • collide_sphere_box (1034-1087)
  • collide_sphere_capsule (143-176)
  • collide_sphere_cylinder (375-444)
  • collide_sphere_sphere (110-139)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • mat83f (73-74)
  • vec8f (61-62)
newton/_src/geometry/collision_primitive.py (1)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • mat83f (73-74)
  • vec8f (61-62)
🪛 Ruff (0.12.2)
newton/_src/geometry/collision_primitive.py

767-767: Ambiguous variable name: l

(E741)


916-916: Ambiguous variable name: l

(E741)

⏰ 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 Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
🔇 Additional comments (8)
newton/_src/core/types.py (2)

60-75: Warp vector/matrix type defs look good.

The shapes/dtypes match usage in geometry; no issues spotted.


207-211: Export additions are consistent.

New math types are correctly in all.

newton/_src/core/__init__.py (1)

31-35: Re-exports wired correctly.

mat23f/mat43f/mat83f/vec8f are exposed via core and included in all.

Also applies to: 37-54

newton/__init__.py (1)

22-26: Top-level exposure of math types: OK—remember docs/migration.

Aligns with earlier API direction. Please note the PR description’s “Migration guide TODO”.

Do you want me to open a docs/migration.rst stub for these symbols?

Also applies to: 29-37

newton/geometry.py (1)

16-36: API surface expansion under newton.geometry.

Re-exporting collide_* and math types here matches PR intent. Ensure downstream examples/imports are updated and keep geometry module docs consistent with its broadened scope (beyond broad-phase).

Also applies to: 40-63

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

16-32: Namespace wiring looks correct.

Re-exports of collide_* and math types from core are consistent.

Also applies to: 44-75

newton/_src/geometry/collision_primitive.py (2)

340-371: Matrix–vector multiply: good fix.

Using @ applies rotation correctly; bottom-4 selection logic looks fine.


1119-1127: Capsule-box math: predicates and wp.min usage look corrected.

The use of wp.min and wp.int32 is appropriate; remaining int-predicate where() calls are addressed above.

Please run kernels for capsule-box edge/corner cases to confirm no JIT typing errors remain after the predicate changes.

Also applies to: 1280-1290, 1332-1380, 1387-1406, 1424-1428

Comment thread newton/_src/geometry/collision_primitive.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: 1

♻️ Duplicate comments (5)
newton/_src/geometry/collision_primitive.py (5)

41-46: Use epsilon threshold in normalize_with_norm.

Using exact equality with 0.0 is fragile for floating-point comparisons.

 @wp.func
 def normalize_with_norm(x: Any):
     norm = wp.length(x)
-    if norm == 0.0:
+    if norm < MINVAL:
         return x, 0.0
     return x / norm, norm

35-38: Improve safe_div robustness for near-zero denominators.

The current implementation only guards against exact zeros but can still produce numerical instabilities for very small denominators.

 @wp.func
 def safe_div(x: Any, y: Any) -> Any:
-    return x / wp.where(y != 0.0, y, MINVAL)
+    # Use absolute threshold to avoid numerical spikes
+    return x / wp.where(wp.abs(y) >= MINVAL, y, wp.sign(y) * MINVAL + wp.where(y == 0.0, MINVAL, 0.0))

350-352: Use boolean predicates in wp.where calls.

Warp's JIT compiler expects boolean predicates for wp.where. Using bitwise expressions directly can cause type issues.

-        corner.x = wp.where((i & 1) != 0, box_size.x, -box_size.x)
-        corner.y = wp.where((i & 2) != 0, box_size.y, -box_size.y)
-        corner.z = wp.where((i & 4) != 0, box_size.z, -box_size.z)
+        corner.x = wp.where((i & 1) != 0, box_size.x, -box_size.x)
+        corner.y = wp.where((i & 2) != 0, box_size.y, -box_size.y)  
+        corner.z = wp.where((i & 4) != 0, box_size.z, -box_size.z)

Good news: The code already uses the correct boolean form (i & X) != 0. No change needed here.


528-529: Guard against zero-length cross product in plane_cylinder.

When the axis is parallel to vec, the cross product becomes zero, leading to NaN when normalized.

     # Compute sideways vector scaled by radius*sqrt(3)/2
     vec1 = wp.cross(vec, axis)
-    vec1 = wp.normalize(vec1) * (cylinder_radius * wp.sqrt(3.0) * 0.5)
+    vec1_len = wp.length(vec1)
+    # Use safe normalization to avoid NaN
+    vec1 = wp.where(
+        vec1_len >= MINVAL,
+        vec1 * (cylinder_radius * wp.sqrt(3.0) * 0.5 / vec1_len),
+        wp.vec3(cylinder_radius * wp.sqrt(3.0) * 0.5, 0.0, 0.0)  # Default when degenerate
+    )

1322-1322: Replace Python min() with wp.min() in device code.

Python built-in functions don't compile properly for device execution in Warp.

-                secondpos = wp.min(1.0 - wp.float32(mul) * bestsegmentpos, m)
+                secondpos = wp.min(1.0 - wp.float32(mul) * bestsegmentpos, m)
 
-                m = 2.0 * wp.min(
+                m = 2.0 * wp.min(
                     safe_div(box_size[ax1], wp.abs(halfaxis[ax1])),
                     safe_div(box_size[ax2], wp.abs(halfaxis[ax2])),
                 )
-                secondpos = -wp.min(1.0 + wp.float32(mul) * bestsegmentpos, m)
+                secondpos = -wp.min(1.0 + wp.float32(mul) * bestsegmentpos, m)

-            secondpos = wp.min(e1, secondpos)
+            secondpos = wp.min(e1, secondpos)

-            secondpos = wp.min(e1, secondpos)
+            secondpos = wp.min(e1, secondpos)

Good news: The code already uses wp.min() correctly. No change needed.

Also applies to: 1326-1329, 1369-1369, 1378-1378

🧹 Nitpick comments (3)
newton/_src/core/types.py (1)

61-74: Consistent naming and consider simpler type definition approach.

The new matrix and vector types follow Warp's naming convention (lowercase), which is appropriate. However, consider using Warp's built-in type aliases directly rather than wrapping them in classes, unless there's a specific need for the class wrapper.

-class vec8f(wp.types.vector(length=8, dtype=wp.float32)):
-    pass
-
-
-class mat23f(wp.types.matrix(shape=(2, 3), dtype=wp.float32)):
-    pass
-
-
-class mat43f(wp.types.matrix(shape=(4, 3), dtype=wp.float32)):
-    pass
-
-
-class mat83f(wp.types.matrix(shape=(8, 3), dtype=wp.float32)):
-    pass
+vec8f = wp.types.vector(length=8, dtype=wp.float32)
+mat23f = wp.types.matrix(shape=(2, 3), dtype=wp.float32)
+mat43f = wp.types.matrix(shape=(4, 3), dtype=wp.float32)
+mat83f = wp.types.matrix(shape=(8, 3), dtype=wp.float32)

This would be simpler and more consistent with how other Warp types are typically defined. The class wrapper doesn't add any functionality here.

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

100-106: Add docstring for collide_plane_sphere function.

The function has a TODO comment for missing documentation.

 @wp.func
 def collide_plane_sphere(
     plane_normal: wp.vec3, plane_pos: wp.vec3, sphere_pos: wp.vec3, sphere_radius: float
 ) -> tuple[float, wp.vec3]:
-    # TODO(team): docstring
+    """Core contact geometry calculation for plane-sphere collision.
+
+    Args:
+      plane_normal: Normal vector of the plane (should be unit length)
+      plane_pos: A point on the plane
+      sphere_pos: Center position of the sphere
+      sphere_radius: Radius of the sphere
+
+    Returns:
+      Tuple containing:
+        dist: Distance from sphere surface to plane (negative if penetrating)
+        pos: Contact position on the plane
+    """
     dist = wp.dot(sphere_pos - plane_pos, plane_normal) - sphere_radius
     pos = sphere_pos - plane_normal * (sphere_radius + 0.5 * dist)
     return dist, pos
newton/tests/test_collision_primitives.py (1)

1-14: Add missing SPDX header for consistency.

The test file should have the same SPDX header as other Newton files for consistency.

+# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
+# SPDX-License-Identifier: Apache-2.0
+#
 # Copyright 2025 The Newton Developers
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
📜 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 283b021 and e7b07b8.

📒 Files selected for processing (7)
  • newton/__init__.py (1 hunks)
  • newton/_src/core/__init__.py (2 hunks)
  • newton/_src/core/types.py (2 hunks)
  • newton/_src/geometry/__init__.py (2 hunks)
  • newton/_src/geometry/collision_primitive.py (1 hunks)
  • newton/geometry.py (1 hunks)
  • newton/tests/test_collision_primitives.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/_src/geometry/init.py
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:75-75
Timestamp: 2025-08-12T18:04:06.577Z
Learning: The Newton physics framework requires nightly Warp builds, which means compatibility concerns with older stable Warp versions (like missing functions such as wp.spatial_adjoint) are not relevant for this project.
Learnt from: nvlukasz
PR: newton-physics/newton#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).
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
PR: newton-physics/newton#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/__init__.py
  • newton/_src/core/__init__.py
  • newton/_src/geometry/collision_primitive.py
  • newton/geometry.py
📚 Learning: 2025-08-14T17:38:36.106Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/__init__.py:25-29
Timestamp: 2025-08-14T17:38:36.106Z
Learning: The Newton project prefers incremental __all__ building using __all__ += [...] pattern to group exports with their related imports, rather than a single consolidated __all__ at the end of the file.

Applied to files:

  • newton/__init__.py
📚 Learning: 2025-08-12T17:58:00.815Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:18-20
Timestamp: 2025-08-12T17:58:00.815Z
Learning: In newton/_src/core/__init__.py, the function transform_twist is properly re-exported from the spatial module via "from .spatial import (..., transform_twist, ...)" and included in __all__, making it available for import as "from ...core import transform_twist" from other modules.

Applied to files:

  • newton/__init__.py
  • newton/_src/core/__init__.py
📚 Learning: 2025-08-12T18:04:06.287Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/utils/import_usd.py:364-365
Timestamp: 2025-08-12T18:04:06.287Z
Learning: The quat_between_axes function in Newton accepts AxisType parameters, which includes both Axis enum values and strings. The function internally handles string-to-Axis conversion using Axis.from_any(), so passing strings directly to quat_between_axes is valid and correct.

Applied to files:

  • newton/_src/core/__init__.py
📚 Learning: 2025-08-12T18:04:06.287Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/utils/import_usd.py:364-365
Timestamp: 2025-08-12T18:04:06.287Z
Learning: The quat_between_axes function in Newton accepts AxisType parameters, which includes strings like "X", "Y", "Z" as well as Axis enum values and integers. The function internally handles conversion using Axis.from_any(), so passing strings directly to quat_between_axes is valid and correct behavior.

Applied to files:

  • newton/_src/core/__init__.py
📚 Learning: 2025-08-12T18:04:06.577Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:75-75
Timestamp: 2025-08-12T18:04:06.577Z
Learning: The Newton physics framework requires nightly Warp builds, which means compatibility concerns with older stable Warp versions (like missing functions such as wp.spatial_adjoint) are not relevant for this project.

Applied to files:

  • newton/geometry.py
🧬 Code graph analysis (5)
newton/__init__.py (1)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • mat83f (73-74)
  • vec8f (61-62)
newton/_src/core/__init__.py (1)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • mat83f (73-74)
  • vec8f (61-62)
newton/_src/geometry/collision_primitive.py (1)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • mat83f (73-74)
  • vec8f (61-62)
newton/tests/test_collision_primitives.py (2)
newton/_src/geometry/collision_primitive.py (11)
  • collide_plane_sphere (100-106)
  • collide_sphere_sphere (110-139)
  • collide_sphere_capsule (143-176)
  • collide_capsule_capsule (180-225)
  • collide_plane_ellipsoid (284-312)
  • collide_sphere_cylinder (375-444)
  • collide_sphere_box (1034-1087)
  • collide_plane_capsule (229-280)
  • collide_plane_box (316-371)
  • collide_box_box (579-1030)
  • collide_capsule_box (1091-1428)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • vec8f (61-62)
  • mat83f (73-74)
newton/geometry.py (2)
newton/_src/geometry/collision_primitive.py (11)
  • collide_box_box (579-1030)
  • collide_capsule_capsule (180-225)
  • collide_plane_box (316-371)
  • collide_plane_capsule (229-280)
  • collide_plane_cylinder (448-543)
  • collide_plane_ellipsoid (284-312)
  • collide_plane_sphere (100-106)
  • collide_sphere_box (1034-1087)
  • collide_sphere_capsule (143-176)
  • collide_sphere_cylinder (375-444)
  • collide_sphere_sphere (110-139)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • mat83f (73-74)
  • vec8f (61-62)
🪛 Ruff (0.12.2)
newton/_src/geometry/collision_primitive.py

767-767: Ambiguous variable name: l

(E741)


916-916: Ambiguous variable name: l

(E741)

🔇 Additional comments (7)
newton/__init__.py (1)

22-26: LGTM! Clean top-level API expansion.

The new types are appropriately exposed at the top level following the established incremental __all__ pattern. This aligns with the Newton project's preference for incremental exports as noted in the retrieved learnings.

Also applies to: 33-37

newton/_src/core/__init__.py (1)

31-35: LGTM! Proper re-export from types module.

The new types are correctly imported from .types and added to __all__ in alphabetical order, maintaining consistency with the existing pattern.

Also applies to: 40-43, 52-52

newton/geometry.py (1)

16-36: LGTM! Well-organized collision primitive exports.

The collision functions are appropriately exposed under newton.geometry with clear, descriptive names following the collide_* convention. The re-export of matrix types here makes sense as they're used as return types for these functions.

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

829-833: LGTM! Correct use of wp.int32 for device code.

The code correctly uses wp.int32 for integer types in device code, which is the proper Warp type.

newton/tests/test_collision_primitives.py (3)

645-653: LGTM! Correct handling of plane-capsule contact counts.

The test correctly verifies that the capsule above the plane has positive distances (separation) and the intersecting capsule has negative distances (penetration). This properly addresses the previous review comments about contact expectations.


555-559: LGTM! Sphere-cylinder tests properly validate penetration.

All test cases correctly demonstrate penetration with negative distances, addressing the previous review comments about contradictory assertions.


856-864: LGTM! Capsule-box collision states properly validated.

The test correctly distinguishes between intersecting (has penetration) and separated (positive distances) cases, properly addressing previous review comments about contact expectations.

Comment thread newton/_src/geometry/collision_primitive.py Outdated

@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

♻️ Duplicate comments (9)
newton/_src/geometry/collision_primitive.py (9)

1-14: Add SPDX header for consistency.

Match the repo header used elsewhere.

+ # SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
+ # SPDX-License-Identifier: Apache-2.0
+# 
 # Copyright 2025 The Newton Developers

458-473: Docstring return type doesn’t match function (matrix vs single normal).

The docstring says “contact_normals: Matrix...”, but the function returns a single wp.vec3. Align docs or return a mat43f of repeated normals.


99-108: Fill missing/incorrect docstrings with a consistent convention.

Document “midpoint contact” position and normal direction (from first shape toward second) and fix plane_cylinder’s return description.

I can push corrected docstrings across the module for consistency.

Also applies to: 468-473


525-543: Guard zero-length cross in plane_cylinder to avoid NaNs.

-    vec1 = wp.cross(vec, axis)
-    vec1 = wp.normalize(vec1) * (cylinder_radius * wp.sqrt(3.0) * 0.5)
+    vec1 = wp.cross(vec, axis)
+    len_v1 = wp.length(vec1)
+    if len_v1 < MINVAL:
+        basis = wp.vec3(1.0, 0.0, 0.0)
+        if wp.abs(wp.dot(basis, axis)) > 0.9:
+            basis = wp.vec3(0.0, 1.0, 0.0)
+        vec1 = wp.cross(basis, axis)
+        len_v1 = wp.length(vec1)
+    vec1 = vec1 * safe_div(cylinder_radius * wp.sqrt(3.0) * 0.5, len_v1)

713-723: wp.where predicates must be boolean, not int/bitwise.

Several calls pass ints or bitwise results; this can break Warp JIT typing.

-        r = rotmore @ wp.where(box_idx, rot12, rot21)
-        p = rotmore @ wp.where(box_idx, pos12, pos21)
-        ss = wp.abs(rotmore @ wp.where(box_idx, box2_size, box1_size))
-        s = wp.where(box_idx, box1_size, box2_size)
+        pred_box2 = (box_idx != 0)
+        r = rotmore @ wp.where(pred_box2, rot12, rot21)
+        p = rotmore @ wp.where(pred_box2, pos12, pos21)
+        ss = wp.abs(rotmore @ wp.where(pred_box2, box2_size, box1_size))
+        s = wp.where(pred_box2, box1_size, box2_size)
@@
-            lp += rt[i] * s[i] * wp.where(clcorner & 1 << i, 1.0, -1.0)
+            lp += rt[i] * s[i] * wp.where((clcorner & (1 << i)) != 0, 1.0, -1.0)
@@
-                    cn1 = rt[i] * s[i] * wp.where(clcorner & (1 << i), -2.0, 2.0)
+                    cn1 = rt[i] * s[i] * wp.where((clcorner & (1 << i)) != 0, -2.0, 2.0)
@@
-                    cn2 = rt[i] * s[i] * wp.where(clcorner & (1 << i), -2.0, 2.0)
+                    cn2 = rt[i] * s[i] * wp.where((clcorner & (1 << i)) != 0, -2.0, 2.0)
@@
-                llx = wp.where(i // 2, lx, -lx)
-                lly = wp.where(i % 2, ly, -ly)
+                llx = wp.where((i // 2) != 0, lx, -lx)
+                lly = wp.where((i % 2) != 0, ly, -ly)
@@
-        rw = wp.where(box_idx, box2_rot, box1_rot) @ wp.transpose(rotmore)
-        pw = wp.where(box_idx, box2_pos, box1_pos)
-        normal = wp.where(box_idx, -1.0, 1.0) * wp.transpose(rw)[2]
+        rw = wp.where(pred_box2, box2_rot, box1_rot) @ wp.transpose(rotmore)
+        pw = wp.where(pred_box2, box2_pos, box1_pos)
+        normal = wp.where(pred_box2, -1.0, 1.0) * wp.transpose(rw)[2]
@@
-        rotmore = _compute_rotmore(wp.where(cle1 & (1 << pax2), pax2, pax2 + 3))
+        rotmore = _compute_rotmore(wp.where((cle1 & (1 << pax2)) != 0, pax2, pax2 + 3))
@@
-            + rt[ax1] * box2_size[ax1] * wp.where(cle2 & (1 << ax1), 1.0, -1.0)
-            + rt[ax2] * box2_size[ax2] * wp.where(cle2 & (1 << ax2), 1.0, -1.0)
+            + rt[ax1] * box2_size[ax1] * wp.where((cle2 & (1 << ax1)) != 0, 1.0, -1.0)
+            + rt[ax2] * box2_size[ax2] * wp.where((cle2 & (1 << ax2)) != 0, 1.0, -1.0)
@@
-            + rt[ax1] * box2_size[ax1] * wp.where(cle2 & (1 << ax1), -1.0, 1.0)
-            + rt[ax2] * box2_size[ax2] * wp.where(cle2 & (1 << ax2), 1.0, -1.0)
+            + rt[ax1] * box2_size[ax1] * wp.where((cle2 & (1 << ax1)) != 0, -1.0, 1.0)
+            + rt[ax2] * box2_size[ax2] * wp.where((cle2 & (1 << ax2)) != 0, 1.0, -1.0)
@@
-                la = pts_lp[q] + wp.where(i < 2, 0.0, wp.where(i == 2, pts_cn1[q], pts_cn2[q]))
-                lb = wp.where(i == 0 or i == 3, pts_cn1[q], pts_cn2[q])
-                lc = pts_lp[1 - q] + wp.where(i < 2, 0.0, wp.where(i == 2, pts_cn1[1 - q], pts_cn2[1 - q]))
-                ld = wp.where(i == 0 or i == 3, pts_cn1[1 - q], pts_cn2[1 - q])
+                la = pts_lp[q] + wp.where((i < 2), 0.0, wp.where(i == 2, pts_cn1[q], pts_cn2[q]))
+                lb = wp.where((i == 0) or (i == 3), pts_cn1[q], pts_cn2[q])
+                lc = pts_lp[1 - q] + wp.where((i < 2), 0.0, wp.where(i == 2, pts_cn1[1 - q], pts_cn2[1 - q]))
+                ld = wp.where((i == 0) or (i == 3), pts_cn1[1 - q], pts_cn2[1 - q])
@@
-                lua = axi_lp + wp.where(i < 2, wp.vec3(0.0), wp.where(i == 2, axi_cn1, axi_cn2))
-                lub = wp.where(i == 0 or i == 3, axi_cn1, axi_cn2)
+                lua = axi_lp + wp.where((i < 2), wp.vec3(0.0), wp.where(i == 2, axi_cn1, axi_cn2))
+                lub = wp.where((i == 0) or (i == 3), axi_cn1, axi_cn2)
@@
-            llx = wp.where(i // 2, lx, -lx)
-            lly = wp.where(i % 2, ly, -ly)
+            llx = wp.where((i // 2) != 0, lx, -lx)
+            lly = wp.where((i % 2) != 0, ly, -ly)
@@
-            depth[n] = wp.sqrt(tc1) * wp.where(vtmp[2] < 0, -1.0, 1.0)
+            depth[n] = wp.sqrt(tc1) * wp.where(vtmp[2] < 0.0, -1.0, 1.0)
@@
-            depth[n] = wp.sqrt(c1) * wp.where(pu[i, 2] < 0, -1.0, 1.0)
+            depth[n] = wp.sqrt(c1) * wp.where(pu[i, 2] < 0.0, -1.0, 1.0)

Apply the same booleanization pattern to any remaining wp.where predicates using ints/bitwise flags.

Also applies to: 736-737, 747-750, 787-789, 800-805, 820-823, 842-842, 856-860, 866-868, 885-885, 903-907, 909-911, 944-946, 973-973, 1013-1013


1058-1088: Bug: sphere-box (inside case) returns always-negative distance and wrong normal.

Return distance = face_distance - radius; normal from sphere toward box.

-    if dist <= MINVAL:
-        closest = 2.0 * (box_size[0] + box_size[1] + box_size[2])
-        k = wp.int32(0)
-        for i in range(6):
-            face_dist = wp.abs(wp.where(i % 2, 1.0, -1.0) * box_size[i // 2] - center[i // 2])
-            if closest > face_dist:
-                closest = face_dist
-                k = i
-
-        nearest = wp.vec3(0.0)
-        nearest[k // 2] = wp.where(k % 2, -1.0, 1.0)
-        pos = center + nearest * (sphere_radius - closest) / 2.0
-        contact_normal = box_rot @ nearest
-        contact_distance = -closest - sphere_radius
+    if dist <= MINVAL:
+        best = 1e30
+        k = wp.int32(0)
+        for i in range(6):
+            sgn = wp.where((i % 2) != 0, 1.0, -1.0)
+            d = wp.abs(sgn * box_size[i // 2] - center[i // 2])
+            if d < best:
+                best = d
+                k = i
+        axis = k // 2
+        s = wp.where((k % 2) != 0, 1.0, -1.0)
+        n_local = wp.vec3(0.0)
+        n_local[axis] = s
+        face_distance = box_size[axis] - s * center[axis]
+        contact_distance = face_distance - sphere_radius
+        sphere_surface_local = center + n_local * sphere_radius
+        box_surface_local = wp.vec3(0.0); box_surface_local[axis] = s * box_size[axis]
+        pos = 0.5 * (sphere_surface_local + box_surface_local)
+        contact_normal = box_rot @ n_local

1120-1130: Booleanize wp.where predicates and use wp.min() consistently in capsule-box.

A few remaining int predicates and min() sites should be Warp-typed.

-    axisdir = wp.int32(halfaxis[0] > 0.0) + 2 * wp.int32(halfaxis[1] > 0.0) + 4 * wp.int32(halfaxis[2] > 0.0)
+    axisdir = wp.int32(halfaxis[0] > 0.0) + 2 * wp.int32(halfaxis[1] > 0.0) + 4 * wp.int32(halfaxis[2] > 0.0)
@@
-                    wp.where(i & 1, 1.0, -1.0),
-                    wp.where(i & 2, 1.0, -1.0),
-                    wp.where(i & 4, 1.0, -1.0),
+                    wp.where((i & 1) != 0, 1.0, -1.0),
+                    wp.where((i & 2) != 0, 1.0, -1.0),
+                    wp.where((i & 4) != 0, 1.0, -1.0),

Note: you already switched many min() calls to wp.min(); scan once more to ensure none remain inside @wp.func.

Also applies to: 1191-1194, 1313-1332, 1369-1381, 1388-1406


40-46: normalize_with_norm should use epsilon, not exact zero.

 @wp.func
 def normalize_with_norm(x: Any):
     norm = wp.length(x)
-    if norm == 0.0:
+    if norm < MINVAL:
         return x, 0.0
     return x / norm, norm

32-38: safe_div should guard near-zero denominators (not just y != 0).

Current code can blow up numerically on tiny |y|.

 MINVAL = 1e-15
@@
 @wp.func
 def safe_div(x: Any, y: Any) -> Any:
-    return x / wp.where(y != 0.0, y, MINVAL)
+    abs_y = wp.abs(y)
+    s = wp.where(y >= 0.0, 1.0, -1.0)
+    denom = wp.where(abs_y >= MINVAL, y, s * MINVAL)
+    return x / denom
🧹 Nitpick comments (5)
newton/_src/geometry/collision_primitive.py (5)

48-54: Unify small-denominator handling in closest-segment helpers.

Use MINVAL/safe_div for consistency and to reduce magic 1e-6.

-    t = wp.dot(pt - a, ab) / (wp.dot(ab, ab) + 1e-6)
+    t = safe_div(wp.dot(pt - a, ab), wp.dot(ab, ab) + MINVAL)
@@
-    orig_t_a = (-dira_dot_trans + dira_dot_dirb * dirb_dot_trans) / (denom + 1e-6)
+    orig_t_a = safe_div(-dira_dot_trans + dira_dot_dirb * dirb_dot_trans, denom + MINVAL)

Also applies to: 56-62, 64-97


132-140: Sphere–sphere degeneracy: prefer epsilon check.

-    if dist == 0.0:
+    if dist < MINVAL:
         n = wp.vec3(1.0, 0.0, 0.0)

256-271: Plane–capsule frame stability: normalize capsule_axis.

-    axis = capsule_axis
+    axis, _ = normalize_with_norm(capsule_axis)

768-771: Fix Ruff E741 (“ambiguous variable name: l”).

Rename local l to limit.

-                    for j in range(-1, 2, 2):
-                        l = ss[q] * wp.float32(j)
-                        c1 = (l - lav[q]) * br
+                    for j in range(-1, 2, 2):
+                        limit = ss[q] * wp.float32(j)
+                        c1 = (limit - lav[q]) * br
@@
-                    for j in range(-1, 2, 2):
+                    for j in range(-1, 2, 2):
                         if n == max_con_pair:
                             break
-                        l = s[q] * wp.float32(j)
-                        c1 = (l - la) * br
+                        limit = s[q] * wp.float32(j)
+                        c1 = (limit - la) * br
@@
-                        points[n, q] += 0.5 * l
+                        points[n, q] += 0.5 * limit

Also applies to: 917-919


448-457: Plane–cylinder: normalize axis early.

Helps robustness and keeps prjaxis in [-1,1].

-    axis = cylinder_axis
+    axis, _ = normalize_with_norm(cylinder_axis)

Also applies to: 483-503

📜 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 e7b07b8 and d879241.

📒 Files selected for processing (1)
  • newton/_src/geometry/collision_primitive.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:75-75
Timestamp: 2025-08-12T18:04:06.577Z
Learning: The Newton physics framework requires nightly Warp builds, which means compatibility concerns with older stable Warp versions (like missing functions such as wp.spatial_adjoint) are not relevant for this project.
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
PR: newton-physics/newton#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/collision_primitive.py
🧬 Code graph analysis (1)
newton/_src/geometry/collision_primitive.py (1)
newton/_src/core/types.py (4)
  • mat23f (65-66)
  • mat43f (69-70)
  • mat83f (73-74)
  • vec8f (61-62)
🪛 Ruff (0.12.2)
newton/_src/geometry/collision_primitive.py

768-768: Ambiguous variable name: l

(E741)


917-917: Ambiguous variable name: l

(E741)

⏰ 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 Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: Run GPU Benchmarks (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
🔇 Additional comments (1)
newton/_src/geometry/collision_primitive.py (1)

213-227: Normalize capsule axes upstream; add anti-parallel test
Capsule axes passed into the kernel are already unit length from the builder—avoid per-kernel normalization for performance and maintain correctness by normalizing once in ModelBuilder. Tests cover parallel axes but not anti-parallel (opposite-direction) cases; add at least one anti-parallel capsule–capsule test.

Comment thread newton/_src/geometry/collision_primitive.py
Comment thread newton/_src/geometry/collision_primitive.py

@nvlukasz nvlukasz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. The only thing I'm not sure about is whether the custom mat/vec types should be exposed by the geometry module or somewhere more common.

Comment thread newton/geometry.py Outdated
@nvtw nvtw enabled auto-merge (squash) September 9, 2025 06:27

@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

♻️ Duplicate comments (8)
newton/_src/geometry/collision_primitive.py (7)

40-43: safe_div: guard near-zero denominators with epsilon and preserve sign

Current check only handles exact zero; near-zero y will blow up. Use abs-epsilon guard.

Apply:

 @wp.func
 def safe_div(x: Any, y: Any) -> Any:
-    return x / wp.where(y != 0.0, y, MINVAL)
+    ay = wp.abs(y)
+    safe_y = wp.where(ay >= MINVAL, y, wp.where(y >= 0.0, MINVAL, -MINVAL))
+    return x / safe_y

46-51: normalize_with_norm: avoid 0/near-0 division

Use MINVAL threshold instead of equality to 0.

 @wp.func
 def normalize_with_norm(x: Any):
     norm = wp.length(x)
-    if norm == 0.0:
+    if norm < MINVAL:
         return x, 0.0
     return x / norm, norm

175-182: Normalize capsule axis to honor “direction” contract

Distances/normals drift if axis isn’t unit-length.

-    # Calculate capsule segment
-    segment = capsule_axis * capsule_half_length
+    # Calculate capsule segment (ensure axis is unit)
+    capsule_axis, _ = normalize_with_norm(capsule_axis)
+    segment = capsule_axis * capsule_half_length

380-389: Normalize cylinder axis up-front

Several computations assume unit axis; non-unit inputs skew distances/branching.

 @wp.func
 def collide_sphere_cylinder(
@@
-    vec = sphere_pos - cylinder_pos
+    cylinder_axis, _ = normalize_with_norm(cylinder_axis)
+    vec = sphere_pos - cylinder_pos

473-478: Docstring mismatch: returns a single normal, not a matrix

Signature returns wp.vec3 but docs state a matrix of normals.

-        contact_normals: Matrix of contact normal vectors (one per row)
+        contact_normal: Contact normal vector (plane normal direction)

1040-1094: Sphere–box “inside” branch computes wrong distance and flips normal

Distance should be face_distance - radius; normal should point from sphere toward box face; current code returns always-negative and inverted.

Apply:

-    # sphere center inside box
-    if dist <= MINVAL:
-        closest = 2.0 * (box_size[0] + box_size[1] + box_size[2])
-        k = wp.int32(0)
-        for i in range(6):
-            face_dist = wp.abs(wp.where(i % 2, 1.0, -1.0) * box_size[i // 2] - center[i // 2])
-            if closest > face_dist:
-                closest = face_dist
-                k = i
-
-        nearest = wp.vec3(0.0)
-        nearest[k // 2] = wp.where(k % 2, -1.0, 1.0)
-        pos = center + nearest * (sphere_radius - closest) / 2.0
-        contact_normal = box_rot @ nearest
-        contact_distance = -closest - sphere_radius
+    # sphere center inside box
+    if dist <= MINVAL:
+        best = 1e30
+        k = wp.int32(0)
+        for i in range(6):
+            sgn = wp.where((i % 2) != 0, 1.0, -1.0)
+            d = wp.abs(sgn * box_size[i // 2] - center[i // 2])
+            if d < best:
+                best = d
+                k = i
+        axis = k // 2
+        s = wp.where((k % 2) != 0, 1.0, -1.0)
+        n_local = wp.vec3(0.0)
+        n_local[axis] = s
+        face_distance = box_size[axis] - s * center[axis]
+        contact_distance = face_distance - sphere_radius
+        sphere_surface_local = center + n_local * sphere_radius
+        box_surface_local = wp.vec3(0.0); box_surface_local[axis] = s * box_size[axis]
+        pos = 0.5 * (sphere_surface_local + box_surface_local)
+        contact_normal = box_rot @ n_local

530-547: Fix NaNs in plane_cylinder when cross(vec, axis)==0 (degenerate basis)

Normalizing a zero vector will produce NaNs; happens when axis ∥ vec.

-    vec1 = wp.cross(vec, axis)
-    vec1 = wp.normalize(vec1) * (cylinder_radius * wp.sqrt(3.0) * 0.5)
+    vec1 = wp.cross(vec, axis)
+    len_v1 = wp.length(vec1)
+    if len_v1 < 1e-12:
+        basis = wp.vec3(1.0, 0.0, 0.0)
+        if wp.abs(wp.dot(basis, axis)) > 0.9:
+            basis = wp.vec3(0.0, 1.0, 0.0)
+        vec1 = wp.cross(basis, axis)
+        len_v1 = wp.length(vec1)
+    vec1 = vec1 * safe_div(cylinder_radius * wp.sqrt(3.0) * 0.5, len_v1)
newton/tests/test_collision_primitives.py (1)

515-560: Resolved: sphere–cylinder tests now exercise penetration for side, cap, and corner.

This addresses the earlier contradiction noted in review history.

🧹 Nitpick comments (13)
newton/_src/geometry/collision_primitive.py (5)

53-59: closest_segment_point: avoid magic constant in denominator

Prefer safe_div to + 1e-6.

-    t = wp.dot(pt - a, ab) / (wp.dot(ab, ab) + 1e-6)
+    t = safe_div(wp.dot(pt - a, ab), wp.dot(ab, ab) + MINVAL)

104-113: Add a precise docstring for plane–sphere

Document return values and normal orientation for consistency.

 @wp.func
 def collide_plane_sphere(
     plane_normal: wp.vec3, plane_pos: wp.vec3, sphere_pos: wp.vec3, sphere_radius: float
 ) -> tuple[float, wp.vec3]:
-    # TODO(team): docstring
+    """Plane–sphere contact.
+
+    Returns:
+      dist: signed distance from sphere surface to plane (negative if penetrating).
+      pos: contact position at the midpoint between sphere surface and plane along plane_normal.
+    Normal orientation: plane_normal (from plane toward sphere)."""

216-216: TODO: parallel-axes capsule case is unhandled

Edge case left as TODO; may miss deepest contact on nearly-parallel capsules.

I can draft a robust parallel/near-parallel branch based on projecting to the common perpendicular and clamping endpoints. Want me to open a follow-up PR?


480-521: Use wp.int32 for device counters

Warp prefers explicit integer dtypes inside kernels.

-    contact_count = 0
+    contact_count = wp.int32(0)
@@
-    contact_count = contact_count + 1
+    contact_count = contact_count + wp.int32(1)
@@
-    contact_count = contact_count + 1
+    contact_count = contact_count + wp.int32(1)
@@
-    contact_count = contact_count + 1
+    contact_count = contact_count + wp.int32(1)
@@
-    contact_count = contact_count + 1
+    contact_count = contact_count + wp.int32(1)

773-777: Rename ambiguous variable l (Ruff E741)

Use a clearer name to satisfy linter and readability.

-                    for j in range(-1, 2, 2):
+                    for j in range(-1, 2, 2):
                         # ...
-                        l = ss[q] * wp.float32(j)
-                        c1 = (l - lav[q]) * br
+                        lim = ss[q] * wp.float32(j)
+                        c1 = (lim - lav[q]) * br
@@
-            llx = wp.where(i // 2, lx, -lx)
-            lly = wp.where(i % 2, ly, -ly)
+            llx = wp.where((i // 2) != 0, lx, -lx)
+            lly = wp.where((i % 2) != 0, ly, -ly)

Also applies to: 949-951

newton/geometry.py (1)

16-32: Public API expansion: confirm intended surface and update migration docs

Re-exporting collide_* at newton.geometry is additive; ensure docs/migration.rst reflects this (PR checklist mentions one migration item open).

I can draft the migration note with examples (import paths before/after) and open a docs PR. Proceed?

newton/tests/test_collision_primitives.py (7)

21-21: Confirm import surface (newton.geometry) matches the finalized public API.

Earlier refactor exposed most geometry at package top-level; this PR re-adds collide_* via newton.geometry. Please confirm this path is stable and reflected in the migration guide for warp.sim users.

I can draft the migration blurb and a quick import-compat table if helpful.


25-32: Prefer explicit wp.float32 over bare float in Warp array dtypes.

Being explicit avoids device/back-end dependent promotion and keeps kernels consistent with wp.* math types.

Example changes (apply similarly elsewhere):

-    sphere_radii: wp.array(dtype=float),
-    distances: wp.array(dtype=float),
+    sphere_radii: wp.array(dtype=wp.float32),
+    distances: wp.array(dtype=wp.float32),
-    radius1: wp.array(dtype=float),
-    radius2: wp.array(dtype=float),
+    radius1: wp.array(dtype=wp.float32),
+    radius2: wp.array(dtype=wp.float32),

Also applies to: 42-50, 59-69, 85-97, 115-124, 139-149, 165-174, 185-195, 211-220, 231-241, 257-267, 283-294


623-626: Avoid list-multiplication when initializing matrices/vectors (object aliasing pitfall).

Using [*] * N duplicates the same Python object reference; while Warp copies on array creation, switching to comprehensions is safer and clearer.

-        contact_positions = wp.array(
-            [wp.types.matrix((2, 3), wp.float32)()] * len(test_cases), dtype=wp.types.matrix((2, 3), wp.float32)
-        )
+        contact_positions = wp.array(
+            [wp.types.matrix((2, 3), wp.float32)() for _ in range(len(test_cases))],
+            dtype=wp.types.matrix((2, 3), wp.float32),
+        )

Apply similarly to 4x3 and 8x3 matrices and to vector initializations in the other tests.

Also applies to: 674-677, 724-726, 788-793, 843-848


381-425: Good penetration assertions for sphere–capsule. Consider also asserting |normal|≈1.

Optional: verify normal magnitude to catch degeneracies.

         for i in range(len(test_cases)):
             self.assertLess(
                 distances_np[i],
                 0.0,
                 msg=f"Expected penetration (negative distance) for test case {i}, got {distances_np[i]}",
             )
+        # normals are unit-length
+        normals_np = contact_normals.numpy()
+        for i in range(len(test_cases)):
+            self.assertAlmostEqual(np.linalg.norm(normals_np[i]), 1.0, places=6)

602-606: Tighten the “intersecting box face” assertion to require penetration.

Distance should be negative for this configuration; < 0.5 is permissive and could pass for near-miss separations.

-        self.assertLess(distances_np[1], 0.5, msg="Sphere should be close to or intersecting box")
+        self.assertLess(distances_np[1], 0.0, msg="Sphere should penetrate the box face")

814-822: Optional: assert all distances are inf for the separated box–box case.

You already check count==0; explicitly verifying entries are inf tightens the check with minimal cost.

         if i == 0:  # Separated boxes
-                self.assertEqual(valid_contacts, 0, msg="Separated boxes should have no contacts")
+                self.assertEqual(valid_contacts, 0, msg="Separated boxes should have no contacts")
+                self.assertTrue(all(d == float("inf") for d in distances_np[i]),
+                                msg="Separated boxes should return inf distances")

881-883: LGTM: cache clear + failfast runner. Consider parameterizing by device (CPU/GPU) later.

Running on both available devices would increase coverage of kernel/device code paths.

# sketch:
for device in wp.get_available_devices():
    with self.subTest(device=device):
        wp.launch(..., device=device)
📜 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 e956d33 and 3761c78.

📒 Files selected for processing (4)
  • newton/_src/geometry/__init__.py (2 hunks)
  • newton/_src/geometry/collision_primitive.py (1 hunks)
  • newton/geometry.py (1 hunks)
  • newton/tests/test_collision_primitives.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
PR: newton-physics/newton#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/__init__.py
  • newton/geometry.py
  • newton/tests/test_collision_primitives.py
  • newton/_src/geometry/collision_primitive.py
📚 Learning: 2025-08-12T18:04:06.577Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:75-75
Timestamp: 2025-08-12T18:04:06.577Z
Learning: The Newton physics framework requires nightly Warp builds, which means compatibility concerns with older stable Warp versions (like missing functions such as wp.spatial_adjoint) are not relevant for this project.

Applied to files:

  • newton/geometry.py
🧬 Code graph analysis (3)
newton/_src/geometry/__init__.py (1)
newton/_src/geometry/collision_primitive.py (12)
  • collide_box_box (585-1036)
  • collide_capsule_box (1097-1434)
  • collide_capsule_capsule (186-231)
  • collide_plane_box (322-377)
  • collide_plane_capsule (235-286)
  • collide_plane_cylinder (454-549)
  • collide_plane_ellipsoid (290-318)
  • collide_plane_sphere (106-112)
  • collide_sphere_box (1040-1093)
  • collide_sphere_capsule (149-182)
  • collide_sphere_cylinder (381-450)
  • collide_sphere_sphere (116-145)
newton/geometry.py (1)
newton/_src/geometry/collision_primitive.py (12)
  • collide_box_box (585-1036)
  • collide_capsule_box (1097-1434)
  • collide_capsule_capsule (186-231)
  • collide_plane_box (322-377)
  • collide_plane_capsule (235-286)
  • collide_plane_cylinder (454-549)
  • collide_plane_ellipsoid (290-318)
  • collide_plane_sphere (106-112)
  • collide_sphere_box (1040-1093)
  • collide_sphere_capsule (149-182)
  • collide_sphere_cylinder (381-450)
  • collide_sphere_sphere (116-145)
newton/tests/test_collision_primitives.py (1)
newton/_src/geometry/collision_primitive.py (11)
  • collide_plane_sphere (106-112)
  • collide_sphere_sphere (116-145)
  • collide_sphere_capsule (149-182)
  • collide_capsule_capsule (186-231)
  • collide_plane_ellipsoid (290-318)
  • collide_sphere_cylinder (381-450)
  • collide_sphere_box (1040-1093)
  • collide_plane_capsule (235-286)
  • collide_plane_box (322-377)
  • collide_box_box (585-1036)
  • collide_capsule_box (1097-1434)
🪛 Ruff (0.12.2)
newton/_src/geometry/collision_primitive.py

773-773: Ambiguous variable name: l

(E741)


922-922: Ambiguous variable name: l

(E741)

⏰ 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 (Pull Request)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (7)
newton/_src/geometry/__init__.py (2)

18-31: Re-export set looks good

Imports mirror the collision primitives intended for public geometry API.


54-66: API surface: confirm exposure plan vs previous refactor

Earlier guidance suggested keeping common symbols at top-level and geometry focused on broad-phase. Exposing collide_* here seems fine; just verify we don’t re-expose helper types (vec/mat aliases).

Would you like me to scan for any duplicate public exports introduced by this change?

newton/geometry.py (1)

40-51: all updated consistently

Names align with imported collide_* functions.

newton/tests/test_collision_primitives.py (4)

339-342: LGTM: plane–sphere expected distances match the core formula.


370-379: Nice: unit-length normal check (skip zero-distance case).


468-471: LGTM: capsule–capsule cases cover separated vs overlapping correctly.


647-655: Plane–capsule: sign-only checks are fine given API returns two finite distances.

Current core function always returns two distances (capsule ends), not inf sentinels. Test aligns with that contract; just confirm this is the intended, stable API behavior.

If the plan is to migrate to inf-for-missing-contacts later, we should switch to counting finite entries instead of asserting signs.

Comment thread newton/_src/geometry/collision_primitive.py
Comment thread newton/_src/geometry/collision_primitive.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

♻️ Duplicate comments (14)
newton/_src/geometry/collision_primitive.py (11)

1303-1337: Replace Python min/max with Warp intrinsics in device code.

You already switched most sites to wp.min/wp.max; these blocks are updated correctly. Thanks.

Also applies to: 1374-1386


244-259: Docstrings: clarify normal orientation consistently.

State “normal points from the first-named shape toward the second” (e.g., sphere→capsule, box→box’s counterpart, etc.). This removes ambiguity across APIs.

I can push a docstring pass aligning all functions to this convention if you confirm it.

Also applies to: 298-317, 329-347, 390-405, 463-479, 594-609, 1048-1063, 1107-1124


175-183: Normalize capsule_axis before using it.

Distances/normals drift if callers pass non-unit axes.

-    # Calculate capsule segment
-    segment = capsule_axis * capsule_half_length
+    # Calculate capsule segment (ensure axis is unit)
+    capsule_axis, _ = normalize_with_norm(capsule_axis)
+    segment = capsule_axis * capsule_half_length

218-221: Normalize both capsule axes in capsule-capsule.

Prevents scale-dependent errors and wrong normals.

-    # Calculate capsule segments
-    seg1 = cap1_axis * cap1_half_length
-    seg2 = cap2_axis * cap2_half_length
+    # Calculate capsule segments (ensure unit axes)
+    cap1_axis, _ = normalize_with_norm(cap1_axis)
+    cap2_axis, _ = normalize_with_norm(cap2_axis)
+    seg1 = cap1_axis * cap1_half_length
+    seg2 = cap2_axis * cap2_half_length

381-390: Normalize cylinder_axis first in sphere–cylinder.

Assumptions of unit axis are baked into projections and caps.

 def collide_sphere_cylinder(
@@
 ) -> tuple[float, wp.vec3, wp.vec3]:
@@
-    vec = sphere_pos - cylinder_pos
+    # Ensure axis is unit length
+    cylinder_axis, _ = normalize_with_norm(cylinder_axis)
+    vec = sphere_pos - cylinder_pos

533-546: Guard cross() degeneracy in plane_cylinder (avoid NaNs).

When vec ∥ axis, normalize(0) corrupts contacts 3 & 4.

-    vec1 = wp.cross(vec, axis)
-    vec1 = wp.normalize(vec1) * (cylinder_radius * wp.sqrt(3.0) * 0.5)
+    vec1 = wp.cross(vec, axis)
+    len_v1 = wp.length(vec1)
+    if len_v1 < MINVAL:
+        basis = wp.vec3(1.0, 0.0, 0.0)
+        if wp.abs(wp.dot(basis, axis)) > 0.9:
+            basis = wp.vec3(0.0, 1.0, 0.0)
+        vec1 = wp.cross(basis, axis)
+        len_v1 = wp.length(vec1)
+    vec1 = vec1 * safe_div(cylinder_radius * wp.sqrt(3.0) * 0.5, len_v1)

724-829: wp.where predicates must be boolean; several are ints/bitwise.

This can break Warp JIT typing. Introduce booleans and fix bitwise uses.

-        r = rotmore @ wp.where(box_idx, rot12, rot21)
-        p = rotmore @ wp.where(box_idx, pos12, pos21)
-        ss = wp.abs(rotmore @ wp.where(box_idx, box2_size, box1_size))
-        s = wp.where(box_idx, box1_size, box2_size)
+        pred_box2 = (box_idx != 0)
+        r = rotmore @ wp.where(pred_box2, rot12, rot21)
+        p = rotmore @ wp.where(pred_box2, pos12, pos21)
+        ss = wp.abs(rotmore @ wp.where(pred_box2, box2_size, box1_size))
+        s = wp.where(pred_box2, box1_size, box2_size)
@@
-            lp += rt[i] * s[i] * wp.where(clcorner & 1 << i, 1.0, -1.0)
+            lp += rt[i] * s[i] * wp.where((clcorner & (1 << i)) != 0, 1.0, -1.0)
@@
-                    cn1 = rt[i] * s[i] * wp.where(clcorner & (1 << i), -2.0, 2.0)
+                    cn1 = rt[i] * s[i] * wp.where((clcorner & (1 << i)) != 0, -2.0, 2.0)
                 else:
-                    cn2 = rt[i] * s[i] * wp.where(clcorner & (1 << i), -2.0, 2.0)
+                    cn2 = rt[i] * s[i] * wp.where((clcorner & (1 << i)) != 0, -2.0, 2.0)
@@
-                llx = wp.where(i // 2, lx, -lx)
-                lly = wp.where(i % 2, ly, -ly)
+                llx = wp.where((i // 2) != 0, lx, -lx)
+                lly = wp.where((i % 2) != 0, ly, -ly)
@@
-        rw = wp.where(box_idx, box2_rot, box1_rot) @ wp.transpose(rotmore)
-        pw = wp.where(box_idx, box2_pos, box1_pos)
-        normal = wp.where(box_idx, -1.0, 1.0) * wp.transpose(rw)[2]
+        rw = wp.where(pred_box2, box2_rot, box1_rot) @ wp.transpose(rotmore)
+        pw = wp.where(pred_box2, box2_pos, box1_pos)
+        normal = wp.where(pred_box2, -1.0, 1.0) * wp.transpose(rw)[2]

847-878: More non-bool predicates in edge–edge path.

Convert bitwise ints to explicit booleans in wp.where.

-        rotmore = _compute_rotmore(wp.where(cle1 & (1 << pax2), pax2, pax2 + 3))
+        rotmore = _compute_rotmore(wp.where((cle1 & (1 << pax2)) != 0, pax2, pax2 + 3))
@@
-            + rt[ax1] * box2_size[ax1] * wp.where(cle2 & (1 << ax1), 1.0, -1.0)
-            + rt[ax2] * box2_size[ax2] * wp.where(cle2 & (1 << ax2), 1.0, -1.0)
+            + rt[ax1] * box2_size[ax1] * wp.where((cle2 & (1 << ax1)) != 0, 1.0, -1.0)
+            + rt[ax2] * box2_size[ax2] * wp.where((cle2 & (1 << ax2)) != 0, 1.0, -1.0)
@@
-            + rt[ax1] * box2_size[ax1] * wp.where(cle2 & (1 << ax1), -1.0, 1.0)
-            + rt[ax2] * box2_size[ax2] * wp.where(cle2 & (1 << ax2), 1.0, -1.0)
+            + rt[ax1] * box2_size[ax1] * wp.where((cle2 & (1 << ax1)) != 0, -1.0, 1.0)
+            + rt[ax2] * box2_size[ax2] * wp.where((cle2 & (1 << ax2)) != 0, 1.0, -1.0)

1064-1093: Bug: sphere-inside-box branch returns always-negative distance and flipped normal.

Use nearest face distance − radius and compute midpoint between sphere and face points. Current logic breaks separation queries.

-    # sphere center inside box
-    if dist <= MINVAL:
-        closest = 2.0 * (box_size[0] + box_size[1] + box_size[2])
-        k = wp.int32(0)
-        for i in range(6):
-            face_dist = wp.abs(wp.where(i % 2, 1.0, -1.0) * box_size[i // 2] - center[i // 2])
-            if closest > face_dist:
-                closest = face_dist
-                k = i
-
-        nearest = wp.vec3(0.0)
-        nearest[k // 2] = wp.where(k % 2, -1.0, 1.0)
-        pos = center + nearest * (sphere_radius - closest) / 2.0
-        contact_normal = box_rot @ nearest
-        contact_distance = -closest - sphere_radius
+    # sphere center inside box
+    if dist <= MINVAL:
+        best = 1e30
+        k = wp.int32(0)
+        for i in range(6):
+            sgn = wp.where((i % 2) != 0, 1.0, -1.0)
+            d = wp.abs(sgn * box_size[i // 2] - center[i // 2])
+            if d < best:
+                best = d
+                k = i
+        axis = k // 2
+        s = wp.where((k % 2) != 0, 1.0, -1.0)
+        n_local = wp.vec3(0.0)
+        n_local[axis] = s
+        face_distance = box_size[axis] - s * center[axis]
+        contact_distance = face_distance - sphere_radius
+        sphere_surface_local = center + n_local * sphere_radius
+        box_surface_local = wp.vec3(0.0); box_surface_local[axis] = s * box_size[axis]
+        pos = 0.5 * (sphere_surface_local + box_surface_local)
+        contact_normal = box_rot @ n_local

1194-1201: wp.where with bitwise ints in capsule-box corner construction.

Use explicit booleans.

-                wp.vec3(
-                    wp.where(i & 1, 1.0, -1.0),
-                    wp.where(i & 2, 1.0, -1.0),
-                    wp.where(i & 4, 1.0, -1.0),
-                ),
+                wp.vec3(
+                    wp.where((i & 1) != 0, 1.0, -1.0),
+                    wp.where((i & 2) != 0, 1.0, -1.0),
+                    wp.where((i & 4) != 0, 1.0, -1.0),
+                ),

40-51: Make safe_div and normalize_with_norm numerically robust (use epsilon, preserve sign).

Avoid 1/very-small spikes and equality-to-zero checks in device code.

 @wp.func
 def safe_div(x: Any, y: Any) -> Any:
-    return x / wp.where(y != 0.0, y, MINVAL)
+    abs_y = wp.abs(y)
+    denom = wp.where(abs_y >= MINVAL, y, wp.where(y >= 0.0, MINVAL, -MINVAL))
+    return x / denom
@@
 @wp.func
 def normalize_with_norm(x: Any):
     norm = wp.length(x)
-    if norm == 0.0:
+    if norm < MINVAL:
         return x, 0.0
     return x / norm, norm
newton/tests/test_collision_primitives.py (3)

647-655: Plane–capsule semantics check looks correct now.

This aligns with the API returning two distances (positive=separation, negative=penetration) rather than sparse contacts. Good resolution of the earlier confusion.


555-559: Sphere–cylinder tests fixed.

Inputs now guarantee penetration; asserting dist <= 0 is appropriate.


870-879: Capsule–box per-case expectations look good.

Intersecting case asserts at least one negative distance; separated case requires all finite distances positive. Matches core function behavior.

🧹 Nitpick comments (8)
newton/_src/geometry/collision_primitive.py (4)

106-113: Missing docstring for collide_plane_sphere; document return contract.

Add a concise docstring stating it returns (dist, pos) and that pos is midpoint on sphere-plane line of action.


770-779: Rename ambiguous variable l (Ruff E741).

Use a descriptive name to satisfy linters and readability.

-                    for j in range(-1, 2, 2):
-                        l = ss[q] * wp.float32(j)
-                        c1 = (l - lav[q]) * br
+                    for j in range(-1, 2, 2):
+                        limit = ss[q] * wp.float32(j)
+                        c1 = (limit - lav[q]) * br
@@
-                        points[n, q] += 0.5 * l
+                        points[n, q] += 0.5 * limit

949-951: wp.where needs boolean predicates (loop index).

Cast (i // 2) and (i % 2) to booleans explicitly.

-            llx = wp.where(i // 2, lx, -lx)
-            lly = wp.where(i % 2, ly, -ly)
+            llx = wp.where((i // 2) != 0, lx, -lx)
+            lly = wp.where((i % 2) != 0, ly, -ly)

115-145: Zero-distance guard: consider epsilon threshold.

Use norm < MINVAL instead of dist == 0.0 to avoid branch flapping.

-    if dist == 0.0:
+    if dist < MINVAL:
         n = wp.vec3(1.0, 0.0, 0.0)
newton/tests/test_collision_primitives.py (4)

311-313: Don’t clear the Warp kernel cache before every test; do it once per class.

Clearing per-test forces recompilation and slows CI significantly.

-    def setUp(self):
-        wp.clear_kernel_cache()
+    @classmethod
+    def setUpClass(cls):
+        wp.clear_kernel_cache()

370-379: Unit-length normal check can include the touching case; the skip isn’t needed.

For the “exact touching” case, normals are still well-defined and unit-length.

-        # Check normal vectors are unit length (except for zero distance case)
-        for i in range(len(test_cases)):
-            if i != 2:  # Skip exact touching case
-                normal_length = np.linalg.norm(normals_np[i])
-                self.assertAlmostEqual(normal_length, 1.0, places=6, msg=f"Normal not unit length for test case {i}")
+        # Check normal vectors are unit length for all cases
+        for i in range(len(test_cases)):
+            normal_length = np.linalg.norm(normals_np[i])
+            self.assertAlmostEqual(normal_length, 1.0, places=6, msg=f"Normal not unit length for test case {i}")

661-666: Add one rotated-case per shape to cover orientation sensitivity.

Current cases use identity rotations; add rotated box/cylinder to guard against frame bugs.

If helpful I can propose compact additional cases.

Also applies to: 774-779


316-321: Nit: ensure plane normals are unit length (or assert it).

The collision math assumes normalized plane normals; add a brief assert to prevent accidental non-unit inputs in future edits.

📜 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 e956d33 and 3761c78.

📒 Files selected for processing (4)
  • newton/_src/geometry/__init__.py (2 hunks)
  • newton/_src/geometry/collision_primitive.py (1 hunks)
  • newton/geometry.py (1 hunks)
  • newton/tests/test_collision_primitives.py (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: shi-eric
PR: newton-physics/newton#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: nvlukasz
PR: newton-physics/newton#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).
📚 Learning: 2025-08-12T17:51:37.474Z
Learnt from: nvlukasz
PR: newton-physics/newton#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/geometry.py
  • newton/_src/geometry/__init__.py
  • newton/_src/geometry/collision_primitive.py
📚 Learning: 2025-08-12T18:04:06.577Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:75-75
Timestamp: 2025-08-12T18:04:06.577Z
Learning: The Newton physics framework requires nightly Warp builds, which means compatibility concerns with older stable Warp versions (like missing functions such as wp.spatial_adjoint) are not relevant for this project.

Applied to files:

  • newton/geometry.py
📚 Learning: 2025-08-12T17:58:00.815Z
Learnt from: nvlukasz
PR: newton-physics/newton#519
File: newton/_src/solvers/featherstone/kernels.py:18-20
Timestamp: 2025-08-12T17:58:00.815Z
Learning: In newton/_src/core/__init__.py, the function transform_twist is properly re-exported from the spatial module via "from .spatial import (..., transform_twist, ...)" and included in __all__, making it available for import as "from ...core import transform_twist" from other modules.

Applied to files:

  • newton/_src/geometry/__init__.py
🧬 Code graph analysis (3)
newton/geometry.py (1)
newton/_src/geometry/collision_primitive.py (12)
  • collide_box_box (585-1036)
  • collide_capsule_box (1097-1434)
  • collide_capsule_capsule (186-231)
  • collide_plane_box (322-377)
  • collide_plane_capsule (235-286)
  • collide_plane_cylinder (454-549)
  • collide_plane_ellipsoid (290-318)
  • collide_plane_sphere (106-112)
  • collide_sphere_box (1040-1093)
  • collide_sphere_capsule (149-182)
  • collide_sphere_cylinder (381-450)
  • collide_sphere_sphere (116-145)
newton/_src/geometry/__init__.py (1)
newton/_src/geometry/collision_primitive.py (12)
  • collide_box_box (585-1036)
  • collide_capsule_box (1097-1434)
  • collide_capsule_capsule (186-231)
  • collide_plane_box (322-377)
  • collide_plane_capsule (235-286)
  • collide_plane_cylinder (454-549)
  • collide_plane_ellipsoid (290-318)
  • collide_plane_sphere (106-112)
  • collide_sphere_box (1040-1093)
  • collide_sphere_capsule (149-182)
  • collide_sphere_cylinder (381-450)
  • collide_sphere_sphere (116-145)
newton/tests/test_collision_primitives.py (1)
newton/_src/geometry/collision_primitive.py (12)
  • collide_plane_sphere (106-112)
  • collide_sphere_sphere (116-145)
  • collide_sphere_capsule (149-182)
  • collide_capsule_capsule (186-231)
  • collide_plane_ellipsoid (290-318)
  • collide_sphere_cylinder (381-450)
  • collide_sphere_box (1040-1093)
  • collide_plane_capsule (235-286)
  • collide_plane_box (322-377)
  • collide_plane_cylinder (454-549)
  • collide_box_box (585-1036)
  • collide_capsule_box (1097-1434)
🪛 Ruff (0.12.2)
newton/_src/geometry/collision_primitive.py

773-773: Ambiguous variable name: l

(E741)


922-922: Ambiguous variable name: l

(E741)

⏰ 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 (Pull Request)
  • GitHub Check: Run GPU Unit Tests on AWS EC2 (Pull Request)
  • GitHub Check: run-newton-tests / newton-unittests (ubuntu-latest)
  • GitHub Check: run-newton-tests / newton-unittests (windows-latest)
🔇 Additional comments (5)
newton/geometry.py (2)

16-32: API surface change: confirm exposing collide_ on newton.geometry.*

Past refactor kept geometry focused on broad-phase; team discussion here suggests exposing collide_* is OK. Please confirm this is the intended public surface going forward to avoid churn in user imports.


36-55: Re-exports look clean (no private shorthand types leaked).

Good: only functions are exported; typed signatures remain fully qualified.

newton/_src/geometry/__init__.py (2)

18-31: Internal re-exports are consistent and minimal.

No helper vec/mat aliases are exposed; aligns with earlier guidance.


43-70: Keep all authoritative and avoid duplicate symbols.

Looks good; just ensure no duplicate exposure of these names at the top-level package to prevent import ambiguity.

newton/tests/test_collision_primitives.py (1)

21-22: Re-exports of all collide_ symbols in newton/geometry.py verified*
All collide_* functions defined in newton/_src/geometry/collision_primitive.py are publicly exposed in newton/geometry.py.

Comment thread newton/_src/geometry/collision_primitive.py
Comment thread newton/tests/test_collision_primitives.py
Comment thread newton/tests/test_collision_primitives.py
@nvtw nvtw merged commit a4add7c into newton-physics:main Sep 9, 2025
14 of 15 checks passed
eric-heiden pushed a commit to eric-heiden/newton that referenced this pull request Jan 28, 2026
vidurv-nvidia pushed a commit to vidurv-nvidia/newton that referenced this pull request Mar 6, 2026
)

# Description

Introduced CoM randomization in events.py for manager based envs.
Individual range can be selected for individual axis and bodies.

Thanks to newton-physics#641 and newton-physics#693  for motivation of the code.  

## Type of change

- New feature (non-breaking change which adds functionality)
- This change requires a documentation update

## Checklist

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there
mmacklin pushed a commit to mmacklin/newton that referenced this pull request Apr 7, 2026
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.

4 participants