Integrate mjwarp primitive collision funcs#693
Conversation
📝 WalkthroughWalkthroughAdds 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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.
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 unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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_countshould 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.wherefor 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.pyre-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.geometryto 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 innewton._src.geometryor 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.
📒 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__.pynewton/geometry.pynewton/_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 removingor ldist > 0and rely on signed corner distance to the plane.
592-1045: Edge case: hz scope and initialization.Ensure
hzis 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 invalidcltypeis 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_boxfor 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.geometryandnewton.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 thatnewton/geometry.pyre-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 innewton/_src/geometry/__init__.py. Tests can safely import these viafrom newton import geometry.
There was a problem hiding this comment.
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 fornewton.geometryimport.Recent refactor exposed common geometry symbols at the top-level
newton/__init__.py, whilenewton.geometrywas kept for broad-phase classes. This PR reintroduces primitives undernewton.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.float32and 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 geometryApply mechanically across the file:
- Replace
dtype=floatwithdtype=F.- Prefer
wp.zeros(..., dtype=F)over list-initialized float arrays.
323-329: Preferwp.zerosfor initialization (clarity + perf).Initialize device arrays with
wp.zerosinstead 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_spherereturns 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.
📒 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__.pynewton/geometry.pynewton/_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.
There was a problem hiding this comment.
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_normalas 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 @ cornervia column access to avoid reusing the same localcornervariable for both frames.- Consider early exit when
ncontact == 0and 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_dirnormalization; 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 respectWARP_DEVICEin 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.
📒 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.whereboolean 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.
adenzler-nvidia
left a comment
There was a problem hiding this comment.
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 :)
|
The primitive collision functions are identical to those in MJWarp. I only removed the |
…funcs # Conflicts: # newton/_src/geometry/__init__.py
…d code accordingly
There was a problem hiding this comment.
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) * brAlso applies to: 913-919
📜 Review details
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📒 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__.pynewton/_src/geometry/__init__.pynewton/__init__.pynewton/geometry.pynewton/_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__.pynewton/__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
There was a problem hiding this comment.
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, posnewton/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
📒 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__.pynewton/_src/core/__init__.pynewton/_src/geometry/collision_primitive.pynewton/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__.pynewton/_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
.typesand 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.geometrywith clear, descriptive names following thecollide_*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.int32for 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.
There was a problem hiding this comment.
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 * limitAlso 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
📒 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.
nvlukasz
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 signCurrent 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 divisionUse 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” contractDistances/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-frontSeveral 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 matrixSignature returns
wp.vec3but 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 normalDistance 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 denominatorPrefer 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–sphereDocument 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 unhandledEdge 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 countersWarp 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 variablel(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 docsRe-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
[*] * Nduplicates 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.5is 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
📒 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__.pynewton/geometry.pynewton/tests/test_collision_primitives.pynewton/_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 goodImports mirror the collision primitives intended for public geometry API.
54-66: API surface: confirm exposure plan vs previous refactorEarlier 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 consistentlyNames 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.
There was a problem hiding this comment.
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, normnewton/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
📒 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.pynewton/_src/geometry/__init__.pynewton/_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.
) # 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
Description
Add MJWarp primitive collision functions and comprehensive tests
Newton Migration Guide
Please ensure the migration guide for warp.sim users is up-to-date with the changes made in this PR.
docs/migration.rstis up-to dateBefore your PR is "Ready for review"
newton/tests/test_examples.py)pre-commit run -aSummary by CodeRabbit
New Features
Tests
Chores