Add documentation for ContactSensor#608
Conversation
Signed-off-by: camevor <camevor@nvidia.com>
📝 WalkthroughWalkthroughRefactors 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
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
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 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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Signed-off-by: camevor <camevor@nvidia.com>
There was a problem hiding this comment.
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: Ensureflipis cast to bool before use inwp.whereIn
select_aggregate_net_force,sp_flipis awp.bool(result ofnormalized_pair[0] != pair[0]), whereasflipis currently assigned fromep[1], awp.int32. Comparing these mismatched types inwp.wheremay 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
flipis awp.bool, matchingsp_flip.
• Optionally, add a small Warp kernel unit test that exercises bothflip == Falseandflip == Truepaths to guard against future API changes in CI.
316-357: Maintain consistent columns for non-pruned force matrixIn the inner loop of
newton/_src/utils/contact_sensor.py, whenshape_contact_pairsisNone(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
ContactSensorinner function, around the blockelif not (sp_flips := get_colliding_sps(sensing_obj, counterpart)): continueApply 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 + continueThis 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.
📒 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.
There was a problem hiding this comment.
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 missingpair,normal, andforceattributes onContactsThe kernel launch in
newton/_src/utils/contact_sensor.py(lines 360–378) referencescontact.pair,contact.normal, andcontact.force, but theContactsclass only defines internal arrays named:
rigid_contact_shape0/rigid_contact_shape1rigid_contact_normalrigid_contact_forceAs a result, the code will raise an
AttributeErrorat runtime. You have two options to resolve this:• Add shim properties to
Contactsinnewton/_src/sim/contacts.pyso 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.pyto 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.,
pairshould 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.array2dwith widthmax_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_mappingsreturns and the intended “indices into counterparts” usage. Consider adding “indices intoself.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; usewp.arrayinstead ofwp.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 withwp.array(orAny) and expressing dtype/shape in the docstring.- self.net_force: wp.array2d(dtype=wp.vec3) + self.net_force: wp.arrayAnd 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_indicesis empty andmax(map(len, counterpart_indices))raisesValueError. 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 callwp.array2d; annotate withwp.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.
📒 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_noncollidingare stated clearly.
232-245:include_totalhandling is intuitive; ensures a “total” column exists when counterparts are specified.Prepending
MatchAnyis 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.flipbookkeeping is consistent with sign handling in the kernel.- Using
max(len(...))to fixn_readingsworks well with pruned layouts.
72-74: Attribute types inContactsconfirmedThe
Contactsclass innewton/_src/sim/contacts.pydefinesrigid_contact_normalaswp.zeros(..., dtype=wp.vec3)andrigid_contact_forceaswp.zeros(..., dtype=wp.vec3)(notwp.float32) (). This matches the kernel signature incontact_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_forceaswp.float32; it should bewp.vec3to align with the upstreamContactsprovider. Please update the snippet (and any documentation) accordingly:- contact_force: wp.array(dtype=wp.float32), + contact_force: wp.array(dtype=wp.vec3),
Signed-off-by: camevor <camevor@nvidia.com> Co-authored-by: Eric Heiden <eheiden@nvidia.com>
Signed-off-by: camevor <camevor@nvidia.com> Co-authored-by: Eric Heiden <eheiden@nvidia.com>
Description
This PR adds documentation and some more type hints for the contact sensor.
Before your PR is "Ready for review"
pre-commit run -aSummary by CodeRabbit
New Features
Refactor
Documentation