Skip to content

Add documentation for ContactSensor#608

Merged
eric-heiden merged 8 commits into
newton-physics:mainfrom
camevor:contact-sensor-docs
Aug 25, 2025
Merged

Add documentation for ContactSensor#608
eric-heiden merged 8 commits into
newton-physics:mainfrom
camevor:contact-sensor-docs

Conversation

@camevor

@camevor camevor commented Aug 21, 2025

Copy link
Copy Markdown
Member

Description

This PR adds documentation and some more type hints for the contact sensor.

Before your PR is "Ready for review"

  • All commits are signed-off to indicate that your contribution adheres to the Developer Certificate of Origin requirements
  • Documentation is up-to-date
  • Code passes formatting and linting checks with pre-commit run -a

Summary by CodeRabbit

  • New Features

    • Added options to prepend a total-reading column and to prune non-colliding counterparts, producing sparse per-sensor matrices with indices to recover active counterparts.
  • Refactor

    • ContactSensor initialization/API changed to explicitly separate sensing objects and counterparts (bodies vs shapes) and exposes sensor state (shape, reading indices, sensing_objs, counterparts, net_force); validation and matrix indexing updated.
  • Documentation

    • Expanded docs covering matrix semantics, wildcard matching, include_total, and prune_noncolliding.

Signed-off-by: camevor <camevor@nvidia.com>
@coderabbitai

coderabbitai Bot commented Aug 21, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Refactors ContactSensor API and internals: separates sensing objects and counterparts into explicit bodies vs shapes, enforces argument exclusivity, exposes mapping state (shape, reading_indices, sensing_objs, counterparts, net_force), and updates evaluation and get_total_force to use underscore-prefixed internals and wp.vec3 net-force arrays.

Changes

Cohort / File(s) Summary
ContactSensor implementation
newton/_src/utils/contact_sensor.py
Constructor split into sensing_obj_bodies/sensing_obj_shapes and counterpart_bodies/counterpart_shapes with validation; added/clarified include_total and prune_noncolliding semantics; introduced public state attributes shape, reading_indices, sensing_objs, counterparts, net_force; renamed internal mappings to _sp_sorted, _sp_reading, _n_shape_pairs, _sp_ep_offset, _sp_ep_count; updated _assemble_sensor_mappings, _eval_net_force / eval flows; changed get_total_force return dtype to wp.array2d(dtype=wp.vec3) and updated docstrings and MatchKind/MatchAny docs.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant CS as ContactSensor
  participant M as Model
  participant C as Contacts

  U->>CS: __init__(model, sensing_obj_bodies|shapes, counterpart_bodies|shapes, match_fn, include_total, prune_noncolliding)
  CS->>M: enumerate bodies/shapes & contact candidates
  note right of CS #DDEBF7: build internal mappings (_sp_sorted, _sp_reading, _n_shape_pairs, _sp_ep_*)
  CS->>CS: _assemble_sensor_mappings()
  note right of CS #DDEBF7: derive public state: shape, reading_indices, sensing_objs, counterparts

  U->>CS: eval(contacts)
  CS->>C: validate contacts populated
  CS->>CS: _eval_net_force()
  note right of CS #F7F0DB: compute 1D _net_force → reshape to 2D net_force (wp.vec3)

  alt include_total && counterparts exist
    CS->>CS: prepend total column to net_force
  end

  opt prune_noncolliding
    CS->>CS: produce pruned rows & expose reading_indices
  end

  U->>CS: get_total_force()
  CS->>U: return wp.array2d(net_force)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • nvlukasz

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

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

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codecov

codecov Bot commented Aug 21, 2025

Copy link
Copy Markdown

Codecov Report

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

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
newton/_src/utils/contact_sensor.py (2)

97-100: Ensure flip is cast to bool before use in wp.where

In select_aggregate_net_force, sp_flip is a wp.bool (result of normalized_pair[0] != pair[0]), whereas flip is currently assigned from ep[1], a wp.int32. Comparing these mismatched types in wp.where may compile unpredictably or break on different Warp versions.

Please apply the following change around lines 97–100 in newton/_src/utils/contact_sensor.py:

-           ep = sp_ep[offset + i]
-           force_acc, flip = ep[0], ep[1]
-           wp.atomic_add(net_force, force_acc, wp.where(sp_flip != flip, -force, force))
+           ep = sp_ep[offset + i]
+           force_acc = ep[0]
+           flip = ep[1] != 0           # cast to bool
+           wp.atomic_add(net_force, force_acc, wp.where(sp_flip != flip, -force, force))

• This ensures flip is a wp.bool, matching sp_flip.
• Optionally, add a small Warp kernel unit test that exercises both flip == False and flip == True paths to guard against future API changes in CI.


316-357: Maintain consistent columns for non-pruned force matrix

In the inner loop of newton/_src/utils/contact_sensor.py, when shape_contact_pairs is None (i.e. prune_noncolliding=False), we must still allocate a zero-column for any counterpart that yields no shape pairs. Otherwise the returned shape (n_sensing_objs, n_counterparts) (per the docstring) can shrink if some counterparts never collide.

• Location: inside the ContactSensor inner function, around the block

elif not (sp_flips := get_colliding_sps(sensing_obj, counterpart)):
    continue

Apply this patch:

-            elif not (sp_flips := get_colliding_sps(sensing_obj, counterpart)):
-                continue
+            elif not (sp_flips := get_colliding_sps(sensing_obj, counterpart)):
+                # for prune_noncolliding=False, preserve a zero-column per counterpart
+                if shape_contact_pairs is None:
+                    sens_counterparts.append(counterpart_idx)
+                    reading_idx += 1
+                continue

This ensures every counterpart still occupies a column (with zero readings) when pruning is disabled, matching the documented shape of the force matrix.

🧹 Nitpick comments (7)
newton/_src/utils/contact_sensor.py (7)

120-133: Nice: populate_contacts docstring adds missing context.

The function is self-explanatory now. Consider adding a one-liner in ContactSensor.eval referencing this helper for discoverability, but optional.


138-164: ContactSensor docstring is solid; minor nuance on matrix shape when pruning is disabled.

Great high-level overview. One nuance: when prune_noncolliding=False, the code currently skips counterparts that yield no shape pairs (e.g., identical shape vs shape), potentially making the number of columns smaller than “n_counterparts”. Either (a) include a zero column for such counterparts (my preferred), or (b) amend the docstring to say columns equal the number of counterparts that yield at least one shape pair. See code-level suggestion below on how to keep zero columns.

Would you like me to propagate the chosen convention to the docs and add a small example?


208-210: Unify vec3 dtype across allocation, attribute type, and return type.

You allocate with dtype=wp.vec3 but annotate/return as wp.vec3f. While Warp aliases may hide this at runtime, keeping a single dtype avoids confusion.

Suggested diff to standardize on vec3f (change both type hints and allocation):

-        self.net_force: wp.array2d(dtype=wp.vec3)
+        self.net_force: wp.array2d(dtype=wp.vec3f)
@@
-        self._net_force = wp.zeros(self.shape[0] * self.shape[1], dtype=wp.vec3, device=self.device)
+        self._net_force = wp.zeros(self.shape[0] * self.shape[1], dtype=wp.vec3f, device=self.device)
@@
-    def get_total_force(self) -> wp.array2d(dtype=wp.vec3f):
+    def get_total_force(self) -> wp.array2d(dtype=wp.vec3f):

Alternatively, standardize on wp.vec3 everywhere—just be consistent.

If you’re unsure about aliasing guarantees in your installed Warp version, I can check the exact behavior and recommend one canonical dtype.

Also applies to: 261-263, 276-283


109-109: Nit: avoid bool(i) inside kernel loop.

Using bool(i) is clever but cryptic. Make the condition explicit for readability.

-            wp.atomic_add(net_force, force_acc, wp.where(bool(i), -force, force))
+            wp.atomic_add(net_force, force_acc, wp.where(i == 1, -force, force))

265-274: eval docstring is accurate; consider validating contacts freshness (optional).

As a nicety, you could assert contacts.n_contacts is set or recommend calling populate_contacts before eval when verbose, but not mandatory.


277-283: Method name get_total_force is misleading; it returns the full matrix.

Two options:

  • Rename to get_force_matrix() or get_net_force().
  • Or adjust docstring to clarify it returns the net force matrix and, when include_total=True, the first column contains totals.

Minimal doc tweak:

-        Get the total net force measured by the contact sensor.
+        Get the net force matrix measured by the contact sensor.
+        When ``include_total`` is True, column 0 contains the per-sensor total.

I can update references across the repo if you choose to rename the method.


401-414: _lol_to_arrays returns None on empty input; ensure callers never pass Nones to kernels.

Currently, _assemble_sensor_mappings builds non-empty sp_reading as long as at least one reading exists (MatchAny ensures this). That said, adding a defensive fallback avoids hard-to-debug Warp errors.

Optional safeguard:

-        self.sp_reading, self.sp_ep_offset, self.sp_ep_count = _lol_to_arrays(sp_reading, wp.vec2i, device=self.device)
+        self.sp_reading, self.sp_ep_offset, self.sp_ep_count = _lol_to_arrays(sp_reading, wp.vec2i, device=self.device)
+        if self.sp_reading is None:
+            # Ensure valid empty arrays to satisfy kernel signatures
+            self.sp_reading = wp.empty(0, dtype=wp.vec2i, device=self.device)
+            self.sp_ep_offset = wp.empty(0, dtype=wp.int32, device=self.device)
+            self.sp_ep_count = wp.empty(0, dtype=wp.int32, device=self.device)

If you prefer assertions instead of fallbacks, I can add asserts documenting invariants established by _assemble_sensor_mappings.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 254f24c and faba190.

📒 Files selected for processing (1)
  • newton/_src/utils/contact_sensor.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
newton/_src/utils/contact_sensor.py (1)
newton/_src/solvers/mujoco/solver_mujoco.py (1)
  • update_contacts (1612-1654)
⏰ 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 (5)
newton/_src/utils/contact_sensor.py (5)

31-36: Docstring for MatchKind is clear and precise.

The updated description concisely explains the enum semantics. No action needed.


112-114: Good: MatchAny sentinel clarified.

Keeping a simple sentinel class with a brief docstring is fine and matches how you use it as a marker (not instances).


177-194: Init-time API validation reads well.

The exclusivity rules and argument descriptions are clear and aligned with the new bodies/shapes split.


308-315: get_colliding_sps correctness looks good; complexity OK for expected sizes.

The intersection-based pruning is clear and efficient for a precomputed set. No changes needed.


358-376: Kernel launch arguments consistent with solver_mujoco.update_contacts.

The shapes and dtypes of pair, normal, and force match the kernel expectations based on the converter kernel. Good.

Comment thread newton/_src/utils/contact_sensor.py
@linux-foundation-easycla

linux-foundation-easycla Bot commented Aug 25, 2025

Copy link
Copy Markdown

CLA Signed

The committers listed above are authorized under a signed CLA.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
newton/_src/utils/contact_sensor.py (1)

360-378: Fix missing pair, normal, and force attributes on Contacts

The kernel launch in newton/_src/utils/contact_sensor.py (lines 360–378) references contact.pair, contact.normal, and contact.force, but the Contacts class only defines internal arrays named:

  • rigid_contact_shape0 / rigid_contact_shape1
  • rigid_contact_normal
  • rigid_contact_force

As a result, the code will raise an AttributeError at runtime. You have two options to resolve this:

• Add shim properties to Contacts in newton/_src/sim/contacts.py so the existing launch call remains unchanged:

class Contacts:
    … 
    @property
    def pair(self):
        # return an (N,2) array of shape indices for each contact
        return wp.stack([self.rigid_contact_shape0, self.rigid_contact_shape1], dim=1)

    @property
    def normal(self):
        return self.rigid_contact_normal

    @property
    def force(self):
        return self.rigid_contact_force

• Or update the kernel launch inputs in contact_sensor.py to use the underlying arrays directly:

     inputs=[
         contact.rigid_contact_count,
         …,
-        contact.pair,
-        contact.normal,
-        contact.force,
+        contact.rigid_contact_shape0,
+        contact.rigid_contact_shape1,
+        contact.rigid_contact_normal,
+        contact.rigid_contact_force,
     ],

Make sure the chosen approach matches the kernel’s expected shapes (e.g., pair should be an int32 array of shape (N,2)).

🧹 Nitpick comments (5)
newton/_src/utils/contact_sensor.py (5)

138-156: Clarify “sparse” wording to avoid confusion with storage format.

The force matrix is conceptually sparse under pruning, but the storage is still a dense wp.array2d with width max_active_counterparts. Consider tightening the wording to reflect conceptual sparsity to avoid users expecting a sparse container.

Proposed doc tweak:

-    If ``prune_noncolliding`` is True, the force matrix will be sparse, containing only readings for shape pairs that
-    can collide. In this case, force matrix will have as many columns as the maximum number of active counterparts
-    for any sensing object, and the ``reading_indices`` attribute can be used to recover the active counterparts
-    for each sensing object.
+    If ``prune_noncolliding`` is True, the force matrix is conceptually sparse: it contains readings only for shape
+    pairs that can collide. The stored array remains dense with width equal to the maximum number of active
+    counterparts across sensing objects; use ``reading_indices`` to recover which counterparts are active per row.

202-203: Good fix: reading_indices now correctly typed as list[list[int]].

This matches what _assemble_sensor_mappings returns and the intended “indices into counterparts” usage. Consider adding “indices into self.counterparts” for extra clarity.

-        """List of active counterpart indices per sensing object."""
+        """Indices into ``self.counterparts`` for the active counterparts of each sensing object."""

210-213: Avoid callable-style types in annotations; use wp.array instead of wp.array2d(...).

wp.array2d(dtype=wp.vec3) is an invocation, not a type. It can confuse type checkers and isn’t valid as a return annotation. Prefer annotating with wp.array (or Any) and expressing dtype/shape in the docstring.

-        self.net_force: wp.array2d(dtype=wp.vec3)
+        self.net_force: wp.array

And for get_total_force (see comment on Lines 279–285).


246-255: Edge case: empty sensing selection leads to a runtime error in _assemble_sensor_mappings.

If the patterns select zero sensing objects, counterpart_indices is empty and max(map(len, counterpart_indices)) raises ValueError. Consider validating early and raising a clear error when no sensing objects match.

Example guard:

         sp_sorted, sp_reading, self.shape, self.reading_indices, self.sensing_objs, self.counterparts = (
             self._assemble_sensor_mappings(
                 ...
             )
         )
+        if self.shape[0] == 0:
+            raise ValueError("No sensing objects matched the provided patterns.")

279-285: Return annotation should not call wp.array2d; annotate with wp.array.

Similar to the instance attribute, using a call expression in a return type is not valid. Keep shape/dtype details in the docstring.

-    def get_total_force(self) -> wp.array2d(dtype=wp.vec3):
+    def get_total_force(self) -> wp.array:
         """Get the total net force measured by the contact sensor.
 
         Returns:
-            The net force array, shaped according to the sensor configuration.
+            A 2-D warp array (dtype=wp.vec3) shaped according to the sensor configuration.
         """
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between faba190 and f409731.

📒 Files selected for processing (1)
  • newton/_src/utils/contact_sensor.py (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
newton/_src/utils/contact_sensor.py (2)
newton/_src/sim/collide.py (1)
  • device (329-336)
newton/_src/sim/contacts.py (2)
  • device (89-93)
  • Contacts (23-93)
⏰ 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 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 (6)
newton/_src/utils/contact_sensor.py (6)

31-36: Nice, concise enum documentation.

The added docstring clarifies semantics of MATCH_ANY, SHAPE, and BODY and aligns with how kinds are used in mappings.


112-114: Helpful docstring for MatchAny.

The intent (“Wildcard counterpart; matches any object.”) is crisp and reflected in the (-1, shape) mono-pair handling.


182-197: Init doc reads well and matches the validation rules.

The exclusivity requirements and the effects of include_total/prune_noncolliding are stated clearly.


232-245: include_total handling is intuitive; ensures a “total” column exists when counterparts are specified.

Prepending MatchAny is a nice way to guarantee column 0 is total without complicating the mapping logic.


318-358: Mapping assembly logic is solid; complexity is acceptable for typical sizes.

  • Handling of mono-pairs via (-1, shape) is clean and matches kernel reads.
  • flip bookkeeping is consistent with sign handling in the kernel.
  • Using max(len(...)) to fix n_readings works well with pruned layouts.

72-74: Attribute types in Contacts confirmed

The Contacts class in newton/_src/sim/contacts.py defines rigid_contact_normal as wp.zeros(..., dtype=wp.vec3) and rigid_contact_force as wp.zeros(..., dtype=wp.vec3) (not wp.float32) (). This matches the kernel signature in contact_sensor.py, which expects:

  • contact_normal: wp.array(dtype=wp.vec3)
  • contact_force: wp.array(dtype=wp.vec3)

The review’s snippet mistakenly listed contact_force as wp.float32; it should be wp.vec3 to align with the upstream Contacts provider. Please update the snippet (and any documentation) accordingly:

-   contact_force: wp.array(dtype=wp.float32),
+   contact_force: wp.array(dtype=wp.vec3),

Comment thread newton/_src/utils/contact_sensor.py

@eric-heiden eric-heiden left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, LGTM!

@eric-heiden eric-heiden enabled auto-merge (squash) August 25, 2025 21:50
@eric-heiden eric-heiden merged commit 1e253cb into newton-physics:main Aug 25, 2025
12 checks passed
@camevor camevor deleted the contact-sensor-docs branch October 17, 2025 13:50
eric-heiden added a commit to eric-heiden/newton that referenced this pull request Jan 28, 2026
Signed-off-by: camevor <camevor@nvidia.com>
Co-authored-by: Eric Heiden <eheiden@nvidia.com>
mmacklin pushed a commit to mmacklin/newton that referenced this pull request Apr 7, 2026
Signed-off-by: camevor <camevor@nvidia.com>
Co-authored-by: Eric Heiden <eheiden@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants