Optimize SensorContact and simplify API#2135
Conversation
📝 WalkthroughWalkthroughUpdates SensorContact: replaces Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant SensorContact as SensorContact
participant Contacts as Contacts
participant DeviceKernel as DeviceKernel
User->>SensorContact: construct(measure_total, sensing selectors, counterpart selectors)
User->>Contacts: provide contacts (device, pairs, forces)
User->>SensorContact: update(Contacts)
SensorContact->>Contacts: validate device matches
SensorContact->>DeviceKernel: launch accumulate_contact_forces_kernel(contacts, mappings)
DeviceKernel-->>SensorContact: write into force_matrix and total_force
SensorContact-->>User: expose total_force / force_matrix (or None based on measure_total)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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 |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
newton/_src/sensors/sensor_contact.py (1)
155-166: Consider simplifying the sorting logic.The recursive call to sort is correct but could be simplified:
def _ensure_sorted_unique(indices: list[int], param_name: str) -> list[int]: indices = sorted(indices) # always sort for i in range(1, len(indices)): if indices[i] == indices[i - 1]: raise ValueError(f"{param_name} contains duplicate index {indices[i]}") return indicesThis avoids the recursive call while maintaining the same behavior. However, the current implementation is functionally correct.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@newton/_src/sensors/sensor_contact.py` around lines 155 - 166, The _ensure_sorted_unique function currently uses a recursive sort when it encounters an out-of-order pair; replace that with a simpler always-sort approach: sort the indices up front (use sorted(indices)), then iterate once to detect duplicates and raise ValueError with the same message using param_name and the duplicate value, and finally return the sorted list; update the body of _ensure_sorted_unique to remove recursion and always return the sorted, duplicate-checked indices.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Line 30: Update the Changelog entry for deprecating SensorContact.shape and
SensorContact.reading_indices to include explicit migration guidance: state
which new field(s) replace each deprecated property (referencing
SensorContact.shape and SensorContact.reading_indices), provide a short example
or pseudo-code showing how to map old values to the new fields, and call out any
serialization or API compatibility steps users must take (e.g., updating stored
records or client code). Ensure the text is concise and actionable so users can
follow a clear migration path.
---
Nitpick comments:
In `@newton/_src/sensors/sensor_contact.py`:
- Around line 155-166: The _ensure_sorted_unique function currently uses a
recursive sort when it encounters an out-of-order pair; replace that with a
simpler always-sort approach: sort the indices up front (use sorted(indices)),
then iterate once to detect duplicates and raise ValueError with the same
message using param_name and the duplicate value, and finally return the sorted
list; update the body of _ensure_sorted_unique to remove recursion and always
return the sorted, duplicate-checked indices.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 2270ca68-c49f-43fa-abd4-5c41de4cc69f
📒 Files selected for processing (7)
CHANGELOG.mdasv/benchmarks/simulation/bench_sensor_contact.pynewton/_src/sensors/sensor_contact.pynewton/_src/utils/benchmark.pynewton/_src/utils/selection.pynewton/examples/sensors/example_sensor_contact.pynewton/tests/test_sensor_contact.py
There was a problem hiding this comment.
Pull request overview
This PR refactors SensorContact to provide clearer force outputs (separating aggregate totals from per-counterpart breakdowns), updates downstream tests/examples to the new API, and adds benchmarking support to evaluate construction/update cost.
Changes:
- Redesign
SensorContactoutputs: introducetotal_forceand optionalforce_matrix, plus new indexing metadata (sensing_obj_idx,counterpart_indices, type fields); deprecate legacy properties/params. - Update unit tests and the contact-sensor example to use the new API and ordering semantics.
- Tighten
match_labels()input validation, add a warmup call to the local benchmark runner, and add an ASV benchmark forSensorContact.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
newton/_src/sensors/sensor_contact.py |
Replaces legacy “net_force matrix” design with total_force + optional force_matrix, adds multi-world column mapping, and introduces deprecation shims. |
newton/tests/test_sensor_contact.py |
Reworks tests to use real ModelBuilder models and validate the new SensorContact outputs and ordering behavior. |
newton/examples/sensors/example_sensor_contact.py |
Updates the example to use total_force and per-counterpart force_matrix, and clarifies ordering assumptions. |
newton/_src/utils/selection.py |
Adds stricter runtime validation to match_labels() for clearer error behavior. |
newton/_src/utils/benchmark.py |
Adds an unmeasured warmup call before timing loops in run_benchmark(). |
asv/benchmarks/simulation/bench_sensor_contact.py |
Adds ASV benchmarks for SensorContact init and update (including CUDA graph capture path). |
CHANGELOG.md |
Documents the SensorContact API change and deprecations. |
Comments suppressed due to low confidence (1)
newton/_src/utils/benchmark.py:210
- The warmup call runs
method(*params)immediately before the timed loop. Ifmethodenqueues asynchronous Warp/CUDA work and only synchronizes during/after the timed loop, the first measured iteration may end up paying for outstanding warmup work. To keep timings isolated, consider synchronizing after the warmup (and/or before starting the timer) when Warp is initialized and the active device is CUDA.
if attr.startswith("time_"):
# Warmup run (not measured).
method(*params)
# Run timing benchmarks multiple times and measure elapsed time.
for _ in range(number):
start = time.perf_counter()
method(*params)
t = time.perf_counter() - start
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
b605c02 to
04c7fc2
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@newton/_src/sensors/sensor_contact.py`:
- Around line 495-496: The metadata sets counterpart_type to "body"/"shape"
unconditionally based on counterpart_is_body even when counterpart_indices is
empty or force_matrix is None, making it inconsistent; update the assignment for
counterpart_type (where sensing_obj_type and counterpart_type are set) to
produce None when there are zero-column configs—i.e., only set "shape" or "body"
if counterpart_indices contains at least one non-empty entry (or force_matrix is
not None/has columns); otherwise set counterpart_type = None so it stays in sync
with counterpart_indices and force_matrix.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 814d7309-7fc3-4fc8-b59b-8b9abd05cfe0
📒 Files selected for processing (7)
CHANGELOG.mdasv/benchmarks/simulation/bench_sensor_contact.pynewton/_src/sensors/sensor_contact.pynewton/_src/utils/benchmark.pynewton/_src/utils/selection.pynewton/examples/sensors/example_sensor_contact.pynewton/tests/test_sensor_contact.py
🚧 Files skipped from review as they are similar to previous changes (3)
- newton/_src/utils/benchmark.py
- newton/examples/sensors/example_sensor_contact.py
- newton/_src/utils/selection.py
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@newton/_src/sensors/sensor_contact.py`:
- Around line 445-446: The current allocation of counterpart columns uses
max_readings computed before filtering to worlds that actually have sensing
rows, causing unused columns (see _assign_counterpart_columns,
counterpart_world_start, world_count, max_readings, counterpart_indices). Update
the logic so max_readings is derived only from counterparts whose worlds
intersect the sensing rows' worlds: first compute the set/list of worlds that
have sensing rows from counterpart_indices (or reject counterparts mapped to
worlds with no sensing rows), then call or modify _assign_counterpart_columns
(or its caller) to calculate max_readings and build
counterparts_by_world/_net_force based on that filtered set so columns are only
allocated for worlds that expose sensing rows.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 04e23f81-aa07-4db2-8674-88e7e4c95318
📒 Files selected for processing (3)
CHANGELOG.mdnewton/_src/sensors/sensor_contact.pynewton/tests/test_sensor_contact.py
🚧 Files skipped from review as they are similar to previous changes (1)
- CHANGELOG.md
There was a problem hiding this comment.
🧹 Nitpick comments (1)
asv/benchmarks/simulation/bench_sensor_contact.py (1)
57-58: Use immutable tuples instead of mutable lists for ASV benchmark parameters to prevent accidental mutation and comply with RUF012.Lines 57–58, 80–81, and 102–103 use mutable lists at class scope, which violates the RUF012 linting rule and risks accidental mutation across benchmark iterations. Refactor to use immutable tuples instead.
Proposed refactor
class InitSensorContact: @@ - params = (["ant", "humanoid"], [64, 8192]) - param_names = ["robot", "world_count"] + params = (("ant", "humanoid"), (64, 8192)) + param_names = ("robot", "world_count") @@ class InitSensorContactWithMatching: @@ - params = (["ant", "humanoid"], [8192]) - param_names = ["robot", "world_count"] + params = (("ant", "humanoid"), (8192,)) + param_names = ("robot", "world_count") @@ class UpdateSensorContact: @@ - params = (["ant", "humanoid"], [64, 8192]) - param_names = ["robot", "world_count"] + params = (("ant", "humanoid"), (64, 8192)) + param_names = ("robot", "world_count")No downstream code mutates these class-level attributes, making the refactoring safe.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@asv/benchmarks/simulation/bench_sensor_contact.py` around lines 57 - 58, Class-level benchmark parameter attributes use mutable lists (params and param_names) which violates RUF012 and risks accidental mutation; change them to immutable tuples by replacing ["ant", "humanoid"] and [64, 8192] with ("ant", "humanoid") and (64, 8192), and replace ["robot", "world_count"] with ("robot", "world_count"); also audit and convert any other class-scope parameter lists in this file (the other occurrences of params/param_names at the same class scope) to tuples to ensure immutability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@asv/benchmarks/simulation/bench_sensor_contact.py`:
- Around line 57-58: Class-level benchmark parameter attributes use mutable
lists (params and param_names) which violates RUF012 and risks accidental
mutation; change them to immutable tuples by replacing ["ant", "humanoid"] and
[64, 8192] with ("ant", "humanoid") and (64, 8192), and replace ["robot",
"world_count"] with ("robot", "world_count"); also audit and convert any other
class-scope parameter lists in this file (the other occurrences of
params/param_names at the same class scope) to tuples to ensure immutability.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: b0a4dbf2-0fa5-46af-b970-0b5bd9ab8048
📒 Files selected for processing (1)
asv/benchmarks/simulation/bench_sensor_contact.py
Description
Redesign SensorContact for clarity and performance. Replace the monolithic net_force array (where column 0 was optionally a total) with two explicit outputs: total_force (1D, per sensing object) and force_matrix (2D, per
counterpart), each nullable. Add flat metadata attributes (sensing_obj_idx, counterpart_indices, sensing_obj_type, counterpart_type) replacing the nested per-world tuple lists.
Internally, replace sorted pair-table binary search with row/col index maps, yielding 50-90x faster init and 1.4-2x faster update at 16k worlds.
All old attributes are preserved as deprecated shims with DeprecationWarning.
Test plan
Checklist
CHANGELOG.mdhas been updated (if user-facing change)New feature / API change
Benchmarks
SensorContact.__init__These init benchmarks use lists of body indices instead of relying on label matching.
SensorContact.updatePer-call cost averaged over 1000 CUDA-graph iterations.
Summary by CodeRabbit
New Features
Deprecated
Benchmarks
Tests
Bug Fixes / UX