Skip to content

Fix MJCF collision mask filtering - #3104

Closed
m-abr wants to merge 1 commit into
newton-physics:mainfrom
m-abr:m-abr/fix-mjcf-contype-conaffinity
Closed

Fix MJCF collision mask filtering#3104
m-abr wants to merge 1 commit into
newton-physics:mainfrom
m-abr:m-abr/fix-mjcf-contype-conaffinity

Conversation

@m-abr

@m-abr m-abr commented Jun 8, 2026

Copy link
Copy Markdown

Description

Fixes #3014.

This PR implements MuJoCo-style contype / conaffinity collision filtering in Newton contact generation.

The change adds collision_type and collision_affinity masks to shape configuration/model data, maps MJCF contype and conaffinity directly to those masks, and resolves existing collision_group filtering into the same runtime mask representation during model finalization.

The broad phase and explicit contact-pair generation now use the same mask rule:

(type_a & affinity_b) != 0 or (type_b & affinity_a) != 0

This fixes the reported case where two spheres with contype=1, conaffinity=0 should each collide with a floor using contype=0, conaffinity=1, but should not collide with each other.

A full pair matrix would be very expressive, but it would scale poorly in memory and is not ideal for broad-phase kernels. This PR keeps the existing collision mechanisms rather than replacing them:

  • collision_group remains the simple group-level API. It is useful for common cases and existing models, and is resolved into masks at finalization, so it does not add per-step cost.
  • collision_type / collision_affinity provide the more expressive category-level filtering needed for MJCF-style masks.
  • shape_collision_filter_pairs remains the sparse exact-pair exclusion layer for same-body, parent-child, USD filteredPairs, disabled collision shapes, articulation self-collision filtering, and other one-off pair exclusions.

The runtime path therefore stays uniform and fast: broad phase and solver code use resolved type/affinity masks, while pairwise filters handle sparse exceptions.

Checklist

  • New or existing tests cover these changes
  • The documentation is up to date with these changes
  • CHANGELOG.md has been updated (if user-facing change)

Test plan

WARP_CACHE_PATH=/tmp/warp-cache python -m unittest newton.tests.test_broad_phase.TestCollisionTypeAffinityBroadPhase
WARP_CACHE_PATH=/tmp/warp-cache python -m unittest newton.tests.test_import_mjcf.TestContypeConaffinityZero.test_contype_conaffinity_masks_filter_contacts
python -m py_compile newton/_src/sim/builder.py newton/_src/sim/collide.py newton/_src/sim/model.py newton/_src/geometry/broad_phase_common.py newton/_src/geometry/broad_phase_nxn.py newton/_src/geometry/broad_phase_sap.py newton/_src/solvers/mujoco/solver_mujoco.py newton/_src/utils/import_mjcf.py newton/tests/test_broad_phase.py newton/tests/test_import_mjcf.py

Bug fix

Steps to reproduce:

  1. Import an MJCF model with:
    • floor: contype="0" conaffinity="1"
    • two spheres: contype="1" conaffinity="0"
  2. Finalize the Newton model and inspect generated shape contact pairs.
  3. Without this fix, the sphere-sphere pair is generated even though MuJoCo filtering excludes it.

Minimal reproduction:

<mujoco model="minexample">
    <option timestep="0.002" gravity="0 0 -9.81"/>

    <default>
        <geom contype="0" conaffinity="0" condim="3"/>
    </default>

    <worldbody>
        <geom name="floor" type="plane" size="5 5 0.01"
              contype="0" conaffinity="1"/>

        <body name="sphere_a" pos="0 -0.06 0.6">
            <freejoint/>
            <geom name="sphere_a" type="sphere" size="0.1"
                  contype="1" conaffinity="0" mass="1"/>
        </body>

        <body name="sphere_b" pos="0 0.06 0.6">
            <freejoint/>
            <geom name="sphere_b" type="sphere" size="0.1"
                  contype="1" conaffinity="0" mass="1"/>
        </body>
    </worldbody>
</mujoco>

New feature / API change

This PR adds mask-based collision filtering through ModelBuilder.ShapeConfig:

floor_cfg = newton.ModelBuilder.ShapeConfig(
    collision_type=0,
    collision_affinity=1,
)
sphere_cfg = newton.ModelBuilder.ShapeConfig(
    collision_type=1,
    collision_affinity=0,
)

collision_group remains available for group-level filtering. During finalization, Newton resolves either group-derived or explicit type/affinity masks into Model.shape_collision_type and Model.shape_collision_affinity, which are used by the runtime collision pipeline.

Summary by CodeRabbit

  • New Features
    • Added per-shape collision_type / collision_affinity masks for model building and runtime collision filtering, used by both NXN and SAP broad-phase/contact pair generation (with automatic derivation from collision_group when masks are not authored).
  • Bug Fixes
    • Fixed MJCF contype / conaffinity mask collision filtering and improved contact-generation stability across multiple solver and example scenarios.
  • Documentation
    • Documented collision type/affinity semantics and added a matching test example.
  • Tests
    • Added broad-phase, MJCF import, solver export, and model-builder regression tests for mask-based filtering.

@linux-foundation-easycla

linux-foundation-easycla Bot commented Jun 8, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: m-abr / name: m-abr (9ae35b1)

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds MuJoCo-style per-shape uint32 collision type/affinity masks throughout the physics engine. Masks are authored on shapes, compiled from collision groups during model finalization, and applied consistently in broad-phase filtering (NxN and SAP), builder contact-pair generation, MJCF import/export, and MuJoCo solver export; includes regression tests and documentation updates.

Changes

Collision type/affinity filtering implementation

Layer / File(s) Summary
Broad-phase mask predicates and compilation
newton/_src/geometry/broad_phase_common.py
Introduces test_type_affinity_pair() and test_world_and_type_affinity_pair() Warp predicates for MuJoCo-style bitmask pair acceptance, and compile_group_masks_for_broad_phase() host helper to derive per-shape uint32 collision type/affinity masks from collision groups.
NxN broad-phase kernel updates
newton/_src/geometry/broad_phase_nxn.py
NxN kernel signature replaced to accept per-shape collision_type/collision_affinity arrays instead of collision_group; pair filtering now uses mask-based testing. BroadPhaseAllPairs.launch() makes shape_collision_group optional and adds shape_collision_type/shape_collision_affinity parameters; derives masks from groups on host when needed.
SAP broad-phase kernel updates
newton/_src/geometry/broad_phase_sap.py
SAP kernel and launch API updated identically to NxN: kernel signature replaces collision_group with collision_type/collision_affinity arrays, pair filtering uses mask-based testing, and launch supports both group-to-mask compilation and direct mask provision with validation/derivation.
Edge redundancy SAP integration
newton/_src/geometry/edge_redundancy.py
Updates edge redundancy detection to construct per-edge collision_type/collision_affinity uint32 arrays (both set to 1) and pass them to SAP instead of collision_group.
Model and ShapeConfig collision mask fields
newton/_src/sim/model.py, newton/_src/sim/builder.py (ShapeConfig)
Adds shape_collision_type and shape_collision_affinity runtime uint32 array attributes to Model; adds optional collision_type/collision_affinity int fields to ShapeConfig with uint32 range validation, missing-pair warnings, and site-flag reset handling.
Builder collision mask compilation and propagation
newton/_src/sim/builder.py (core logic)
Adds _compile_shape_collision_masks() to convert authored masks or derive from collision_group sign semantics (with 32-group limit); propagates masks through add_builder(), add_shape(), convex decomposition, and remeshing; assigns compiled uint32 arrays to Model at finalize.
Shape contact pair filtering with masks
newton/_src/sim/builder.py (contact generation)
Adds _test_type_affinity_pair() and _test_world_and_type_affinity_pair() helpers; updates find_shape_contact_pairs() to precompile and use collision_type/affinity masks instead of collision_group for candidate pair filtering.
Runtime collision pipeline integration
newton/_src/sim/collide.py
Updates CollisionPipeline.collide() to pass shape_collision_type and shape_collision_affinity as keyword arguments to both NxN and SAP broad-phase launches, while setting shape_collision_group to None.
MJCF import of contype/conaffinity
newton/_src/utils/import_mjcf.py
parse_shapes() now maps MJCF contype/conaffinity into ShapeConfig.collision_type/collision_affinity fields so authored masks persist through builder finalization and solver export.
MuJoCo solver collision export
newton/_src/solvers/mujoco/solver_mujoco.py
Removes graph-coloring approach (_color_collision_shapes) and color-mask inference; builds mujoco_auto_contact_shapes from compatible colliding pairs using type/affinity bitmask compatibility checks; exports per-geom contype/conaffinity directly from shape masks via _mujoco_mask_value() helper.
Broad-phase and MJCF integration tests
newton/tests/test_broad_phase.py, newton/tests/test_import_mjcf.py, newton/tests/test_model.py
Adds TestCollisionTypeAffinityBroadPhase validating NxN/SAP mask-filtering with floor-vs-spheres, zero-type inertness, and disjoint-bits scenarios; adds MJCF contype/conaffinity regression tests validating filter behavior and export correctness; adds ShapeConfig flag/group interaction tests.
Documentation and changelog
docs/concepts/collisions.rst, CHANGELOG.md
Documents collision type/affinity mask semantics, ShapeConfig fields, mask derivation from groups, and finalization behavior including unset-mask warnings; includes test example (collision-type-affinity) demonstrating asymmetric floor-sphere vs sphere-sphere collisions; records bug fix in CHANGELOG.

Sequence Diagram(s)

sequenceDiagram
  participant Builder as ModelBuilder
  participant Compiler as _compile_shape_collision_masks()
  participant Model as Model (runtime)
  participant CollPipeline as CollisionPipeline
  participant BroadPhase as BroadPhaseAllPairs / SAP
  participant KernelTest as test_world_and_type_affinity_pair

  Builder->>Compiler: finalize: convert collision_group or explicit masks
  Compiler->>Model: set shape_collision_type & shape_collision_affinity arrays
  Model->>CollPipeline: model contains mask arrays
  CollPipeline->>BroadPhase: launch with shape_collision_type/affinity
  BroadPhase->>KernelTest: per-pair test(world_a, world_b, type_a, affinity_a, type_b, affinity_b)
  KernelTest->>BroadPhase: bool accept/reject pair
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • newton-physics/newton#2701: Hydroelastic broad-phase overhaul and SDF cache recooking behavior relates to multiple stability fixes in this PR's CHANGELOG update.
  • newton-physics/newton#984: Both PRs modify broad-phase kernel signatures and pair-filtering logic; this PR switches collision-group logic to collision type/affinity masks while #984 refactors kernels to be world-id aware.

Suggested reviewers

  • adenzler-nvidia
  • nvtw
  • mmacklin
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix MJCF collision mask filtering' directly describes the main change: implementing contype/conaffinity collision filtering in Newton, which is the primary objective of this PR.
Linked Issues check ✅ Passed The PR implements the complete solution for issue #3014: contype/conaffinity collision mask filtering is now properly applied via the (type_a & affinity_b) or (type_b & affinity_a) rule across broad phase and contact generation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing collision mask filtering: broad-phase filtering functions, model builder mask fields, solver export logic, MJCF import, documentation, and comprehensive testing. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 82.69% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@m-abr
m-abr had a problem deploying to external-pr-approval June 8, 2026 16:03 — with GitHub Actions Error
@m-abr
m-abr had a problem deploying to external-pr-approval June 8, 2026 16:03 — with GitHub Actions Error

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (3)
newton/_src/sim/builder.py (1)

467-474: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't wipe authored masks when updating non-site flags.

This branch now resets collision_type and collision_affinity to defaults every time flags is assigned without the SITE bit. That breaks round-tripping for configs with authored masks, and the remeshing path later in this file hits exactly that pattern, so decomposed convex parts fall back to default collision filtering.

Suggested fix
         else:
-            # SITE flag is being cleared - restore non-site defaults
-            defaults = self.__class__()
+            # Only restore site-specific defaults when transitioning from site -> non-site.
+            was_site = self.is_site
             self.is_site = False
-            self.density = defaults.density
-            self.collision_group = defaults.collision_group
-            self.collision_type = defaults.collision_type
-            self.collision_affinity = defaults.collision_affinity
+            if was_site:
+                defaults = self.__class__()
+                self.density = defaults.density
+                self.collision_group = defaults.collision_group
+                self.collision_type = defaults.collision_type
+                self.collision_affinity = defaults.collision_affinity
             self.has_shape_collision = bool(value & ShapeFlags.COLLIDE_SHAPES)
             self.has_particle_collision = bool(value & ShapeFlags.COLLIDE_PARTICLES)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/_src/sim/builder.py` around lines 467 - 474, When clearing the SITE
flag in the flags-assignment branch, do not unconditionally reset authored
collision masks; instead only restore non-site defaults that are not
mask-related. Remove or stop setting self.collision_type and
self.collision_affinity in that else-branch where defaults = self.__class__()
and self.is_site is set to False, keeping restoration of density and
collision_group as needed so that authored masks on
collision_type/collision_affinity are preserved and remeshing/decomposition will
round-trip correctly.
newton/_src/geometry/broad_phase_nxn.py (1)

316-360: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Incomplete docstring for new parameters.

The launch() method signature added shape_collision_type and shape_collision_affinity optional parameters (lines 331-332), but the docstring's Args: section doesn't document them. Additionally, the shape_collision_group description (line 345) should note it's now optional and explain the either-or relationship with the new parameters (either provide shape_collision_group, or provide both shape_collision_type and shape_collision_affinity).

📝 Add missing parameter documentation

Update the Args section to document all three collision-filtering parameters and their relationship:

         Args:
             shape_lower: Array of lower bounds for each shape's AABB
             shape_upper: Array of upper bounds for each shape's AABB
             shape_gap: Optional array of per-shape effective gaps. If None or empty array,
                 assumes AABBs are pre-expanded (gaps = 0). If provided, gaps are added during overlap checks.
-            shape_collision_group: Array of collision group IDs for each shape.
+            shape_collision_group: Optional array of collision group IDs for each shape.
+                If None, ``shape_collision_type`` and ``shape_collision_affinity`` must be provided.
+                When provided, used to derive collision masks if ``shape_collision_type``
+                and ``shape_collision_affinity`` are not provided.
             shape_world: Array of world indices for each shape. Index -1 indicates global entities
                 that collide with all worlds. Indices 0, 1, 2, ... indicate world-specific entities.
             shape_count: Number of active bounding boxes to check
             candidate_pair: Output array to store overlapping shape pairs
             candidate_pair_count: Output array to store number of overlapping pairs found
             device: Device to launch on. If None, uses the device of the input arrays.
+            filter_pairs: Optional array of shape-pair exclusions (sorted).
+            num_filter_pairs: Number of pairs in filter_pairs.
             skip_count_zero: If True, skip the internal ``candidate_pair_count.zero_()``.
                 The caller guarantees ``candidate_pair_count[0] == 0`` on entry (e.g. when
                 the counter was zeroed by a preceding fused kernel).  Defaults to False so
                 the launch remains self-contained.
+            shape_collision_type: Optional per-shape collision type bitmask (uint32).
+                If None, derived from ``shape_collision_group``.
+            shape_collision_affinity: Optional per-shape collision affinity bitmask (uint32).
+                If None, derived from ``shape_collision_group``.

As per coding guidelines, follow Google-style docstrings with all parameters documented in the Args section.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/_src/geometry/broad_phase_nxn.py` around lines 316 - 360, The
docstring for launch() is missing entries for the new parameters
shape_collision_type and shape_collision_affinity and must be updated; add
Google-style Args descriptions for both parameters and update the existing
shape_collision_group description to mark it optional and explain the either-or
relationship (caller must provide shape_collision_group OR provide both
shape_collision_type and shape_collision_affinity), plus state how they affect
filtering (e.g., type and affinity are bitmask-based filters applied per-shape),
and mention defaults/behavior when any of these are None. Locate these symbols
in the launch() signature (shape_collision_group, shape_collision_type,
shape_collision_affinity) and edit the Args section accordingly.

Source: Coding guidelines

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

521-565: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Incomplete docstring for new parameters.

The launch() method added shape_collision_type and shape_collision_affinity optional parameters (lines 536-537), but they're not documented in the Args: section. Additionally, shape_collision_group (line 550) should note it's now optional and explain the either-or contract with the new type/affinity parameters.

📝 Add missing parameter documentation

Update the Args section:

         Args:
             shape_lower: Array of lower bounds for each shape's AABB
             shape_upper: Array of upper bounds for each shape's AABB
             shape_gap: Optional array of per-shape effective gaps. If None or empty array,
                 assumes AABBs are pre-expanded (gaps = 0). If provided, gaps are added during overlap checks.
-            shape_collision_group: Array of collision group IDs for each shape.
+            shape_collision_group: Optional array of collision group IDs for each shape.
+                If None, ``shape_collision_type`` and ``shape_collision_affinity`` must be provided.
+                When provided, used to derive collision masks if the type/affinity parameters are not provided.
             shape_world: Array of world indices for each shape. Index -1 indicates global entities
                 that collide with all worlds. Indices 0, 1, 2, ... indicate world-specific entities.
             shape_count: Number of active bounding boxes to check (not used in world-based approach)
             candidate_pair: Output array to store overlapping shape pairs
             candidate_pair_count: Output array to store number of overlapping pairs found
             device: Device to launch on. If None, uses the device of the input arrays.
+            filter_pairs: Optional array of shape-pair exclusions (sorted).
+            num_filter_pairs: Number of pairs in filter_pairs.
             skip_count_zero: If True, skip the internal ``candidate_pair_count.zero_()``.
                 The caller guarantees ``candidate_pair_count[0] == 0`` on entry (e.g. when
                 the counter was zeroed by a preceding fused kernel).  Defaults to False so
                 the launch remains self-contained.
+            shape_collision_type: Optional per-shape collision type bitmask (uint32).
+                If None, derived from ``shape_collision_group``.
+            shape_collision_affinity: Optional per-shape collision affinity bitmask (uint32).
+                If None, derived from ``shape_collision_group``.

As per coding guidelines, follow Google-style docstrings with all parameters documented in Args.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/_src/geometry/broad_phase_sap.py` around lines 521 - 565, The
docstring for launch() is missing entries for the new optional parameters
shape_collision_type and shape_collision_affinity and should also update the
shape_collision_group doc to reflect that it's now optional and mutually
alternative to the type/affinity pair; edit the Args block inside the launch
method docstring in broad_phase_sap.py to add Google-style descriptions for
shape_collision_type (per-shape collision type mask), shape_collision_affinity
(per-shape affinity mask), and clarify shape_collision_group is optional and
that either shape_collision_group is used or the pair of
shape_collision_type/shape_collision_affinity are used (explain the either-or
contract and default/None behavior).

Source: Coding guidelines

🧹 Nitpick comments (1)
newton/_src/geometry/edge_redundancy.py (1)

532-533: 💤 Low value

Inconsistent dtype specification for warp arrays.

Line 532 uses dtype=wp.uint32 while line 533 uses dtype=np.uint32 when constructing warp arrays. For consistency, both should use dtype=wp.uint32.

🔧 Align dtype specification
         shape_world_wp = wp.array(shape_world_np, dtype=wp.int32)
         shape_collision_type_wp = wp.array(shape_collision_type_np, dtype=wp.uint32)
-        shape_collision_affinity_wp = wp.array(shape_collision_affinity_np, dtype=np.uint32)
+        shape_collision_affinity_wp = wp.array(shape_collision_affinity_np, dtype=wp.uint32)
         shape_flags_wp = wp.array(shape_flags_np, dtype=wp.int32)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/_src/geometry/edge_redundancy.py` around lines 532 - 533, The two warp
arrays created for shape collision metadata use inconsistent dtype specifiers:
change the dtype on shape_collision_affinity_wp to use wp.uint32 to match
shape_collision_type_wp (i.e., ensure both wp.array calls use dtype=wp.uint32);
update the wp.array invocation that constructs shape_collision_affinity_wp so it
references wp.uint32 rather than np.uint32, leaving variable names
shape_collision_type_wp and shape_collision_affinity_wp unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@newton/_src/geometry/broad_phase_common.py`:
- Around line 183-193: Update the docstring for the function
test_type_affinity_pair to a full Google-style docstring: expand the one-line
summary to describe MuJoCo-style collision filtering (two 32-bit bitmasks per
object: "collision_type" is the object's type bits and "collision_affinity" is
which types it can collide with), add an Args: section listing collision_type_a:
wp.uint32, collision_affinity_a: wp.uint32, collision_type_b: wp.uint32,
collision_affinity_b: wp.uint32 with brief descriptions, and add a Returns:
section describing that the function returns a bool which is True when
(collision_type_a & collision_affinity_b) != 0 or (collision_type_b &
collision_affinity_a) != 0 (i.e., either object A accepts B's type or B accepts
A's type), keeping types consistent with annotations and using plain language to
explain the bitwise logic; place this updated docstring immediately above the
def test_type_affinity_pair declaration.
- Around line 332-380: Update the docstring of
compile_group_masks_for_broad_phase to a Google-style docstring: add an Args:
section describing collision_group (wp.array | np.ndarray | list[int]) and
device (Any | None) and their semantics, a Returns: section describing the tuple
of wp.array collision_type and wp.array collision_affinity (both uint32 masks on
the specified device), and a Raises: section documenting the ValueError when
more than 32 non-zero groups are provided; keep the short summary line and
ensure types and behavior (zero group skipped, positive/negative group handling)
are noted concisely.
- Around line 196-209: Add a Google-style docstring to
test_world_and_type_affinity_pair that includes an Args section listing each
parameter with type and brief description (world_a: int — world index for entity
A, use -1 to ignore/world wildcard; world_b: int — world index for entity B, use
-1 to ignore; collision_type_a: wp.uint32 — collision type mask for entity A;
collision_affinity_a: wp.uint32 — collision affinity mask for entity A;
collision_type_b: wp.uint32 — collision type mask for entity B;
collision_affinity_b: wp.uint32 — collision affinity mask for entity B) and a
Returns section describing the boolean result (bool — True if the two entities
should collide based on world indices and type/affinity masks). Keep the
existing one-line summary and add the Args: and Returns: blocks in Google
docstring format above the function body; no behavior changes to
test_type_affinity_pair or logic are needed.

In `@newton/_src/sim/builder.py`:
- Around line 1464-1470: The code currently counts all values in
self.shape_collision_group when computing unique_groups, which erroneously
includes shapes that already have explicit collision_type or collision_affinity
and therefore shouldn't consume mask bits; update the computation to exclude
collision_group entries for shapes with authored collision_type or
collision_affinity. Concretely, replace the list comprehension that builds
unique_groups with one that filters by index (e.g.
enumerate(self.shape_collision_group)) and only includes group values when the
corresponding shape does NOT have an explicit collision_type or
collision_affinity (check the parallel arrays/attributes used to determine
authored types), then perform the sorted(set(...)) and the same >32 check on
that filtered set. Ensure you reference and use the existing attributes
(self.shape_collision_group and whatever arrays/flags indicate authored
collision_type/collision_affinity) so mixed models don't count explicit-mask
shapes toward the 32-bit limit.

In `@newton/_src/solvers/mujoco/solver_mujoco.py`:
- Around line 4353-4360: The current graph_edges and recoloring logic converts
mask relations into a color-based compatibility, which is incorrect; instead
export the original masks into geom_params by assigning shape_collision_type and
shape_collision_affinity directly to geom_params["contype"] and
geom_params["conaffinity"] (use the existing variables shape_collision_type /
shape_collision_affinity), and change uses_mujoco_contacts to be computed from
the exact pairwise predicate ((type_a & affinity_b) != 0 or (type_b &
affinity_a) != 0) plus an OR branch that keeps explicit pair entries from
model.shape_collision_filter_pairs; leave the explicit-<pair> override handling
intact so shapes referenced by explicit pairs are included even if the automatic
mask predicate is false; update the three other locations you noted (around
lines ~5171-5175 and ~5204-5217) to the same pattern.

---

Outside diff comments:
In `@newton/_src/geometry/broad_phase_nxn.py`:
- Around line 316-360: The docstring for launch() is missing entries for the new
parameters shape_collision_type and shape_collision_affinity and must be
updated; add Google-style Args descriptions for both parameters and update the
existing shape_collision_group description to mark it optional and explain the
either-or relationship (caller must provide shape_collision_group OR provide
both shape_collision_type and shape_collision_affinity), plus state how they
affect filtering (e.g., type and affinity are bitmask-based filters applied
per-shape), and mention defaults/behavior when any of these are None. Locate
these symbols in the launch() signature (shape_collision_group,
shape_collision_type, shape_collision_affinity) and edit the Args section
accordingly.

In `@newton/_src/geometry/broad_phase_sap.py`:
- Around line 521-565: The docstring for launch() is missing entries for the new
optional parameters shape_collision_type and shape_collision_affinity and should
also update the shape_collision_group doc to reflect that it's now optional and
mutually alternative to the type/affinity pair; edit the Args block inside the
launch method docstring in broad_phase_sap.py to add Google-style descriptions
for shape_collision_type (per-shape collision type mask),
shape_collision_affinity (per-shape affinity mask), and clarify
shape_collision_group is optional and that either shape_collision_group is used
or the pair of shape_collision_type/shape_collision_affinity are used (explain
the either-or contract and default/None behavior).

In `@newton/_src/sim/builder.py`:
- Around line 467-474: When clearing the SITE flag in the flags-assignment
branch, do not unconditionally reset authored collision masks; instead only
restore non-site defaults that are not mask-related. Remove or stop setting
self.collision_type and self.collision_affinity in that else-branch where
defaults = self.__class__() and self.is_site is set to False, keeping
restoration of density and collision_group as needed so that authored masks on
collision_type/collision_affinity are preserved and remeshing/decomposition will
round-trip correctly.

---

Nitpick comments:
In `@newton/_src/geometry/edge_redundancy.py`:
- Around line 532-533: The two warp arrays created for shape collision metadata
use inconsistent dtype specifiers: change the dtype on
shape_collision_affinity_wp to use wp.uint32 to match shape_collision_type_wp
(i.e., ensure both wp.array calls use dtype=wp.uint32); update the wp.array
invocation that constructs shape_collision_affinity_wp so it references
wp.uint32 rather than np.uint32, leaving variable names shape_collision_type_wp
and shape_collision_affinity_wp unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 91c5e2f8-18d9-4a4e-b5b3-1ca717f9702b

📥 Commits

Reviewing files that changed from the base of the PR and between b11f714 and 9ae35b1.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • docs/concepts/collisions.rst
  • newton/_src/geometry/broad_phase_common.py
  • newton/_src/geometry/broad_phase_nxn.py
  • newton/_src/geometry/broad_phase_sap.py
  • newton/_src/geometry/edge_redundancy.py
  • newton/_src/sim/builder.py
  • newton/_src/sim/collide.py
  • newton/_src/sim/model.py
  • newton/_src/solvers/mujoco/solver_mujoco.py
  • newton/_src/utils/import_mjcf.py
  • newton/tests/test_broad_phase.py
  • newton/tests/test_import_mjcf.py

Comment thread newton/_src/geometry/broad_phase_common.py
Comment thread newton/_src/geometry/broad_phase_common.py
Comment thread newton/_src/geometry/broad_phase_common.py
Comment thread newton/_src/sim/builder.py Outdated
Comment thread newton/_src/solvers/mujoco/solver_mujoco.py Outdated
@m-abr
m-abr force-pushed the m-abr/fix-mjcf-contype-conaffinity branch from 9ae35b1 to 5d3f72a Compare June 8, 2026 16:26
@m-abr
m-abr had a problem deploying to external-pr-approval June 8, 2026 16:27 — with GitHub Actions Error
@m-abr
m-abr had a problem deploying to external-pr-approval June 8, 2026 16:27 — with GitHub Actions Error

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
newton/_src/solvers/mujoco/solver_mujoco.py (1)

4348-4360: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Export the exact type/affinity masks instead of recoloring them.

This still changes the collision relation before it reaches MuJoCo: Lines 4348-4360 build a compatibility graph from the masks, but Lines 5204-5217 collapse that graph into “same color never collides, different colors always collide.” That is not equivalent to MuJoCo’s exact rule (type_a & affinity_b) != 0 or (type_b & affinity_a) != 0, so the exported model can admit spurious contacts or miss valid ones. The same approximation leaks into Lines 5171-5175, where any non-zero mask marks a mesh as using MuJoCo automatic contacts even if it has no compatible partner, which can incorrectly reject planar meshes.

Please write shape_collision_type / shape_collision_affinity directly into geom_params["contype"] / geom_params["conaffinity"] instead of deriving colors, and compute uses_mujoco_contacts from the exact automatic-contact predicate plus the existing explicit-<pair> override. If you need to preserve the full 32-bit mask through MuJoCo’s signed storage, cast with np.int32(...).item() rather than recoloring. Based on learnings, shapes referenced by explicit MuJoCo <pair> entries must remain included even when the automatic-contact branch switches to the exact mask predicate.

Also applies to: 5171-5175, 5204-5217

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/_src/solvers/mujoco/solver_mujoco.py` around lines 4348 - 4360, The
current code recolors collision masks and derives a coarse color-based collision
rule which changes MuJoCo semantics; instead, write the exact masks from
shape_collision_type and shape_collision_affinity into geom_params["contype"]
and geom_params["conaffinity"] (use np.int32(mask).item() to preserve full
32-bit signed storage), compute uses_mujoco_contacts using the exact predicate
((type_a & affinity_b) != 0 or (type_b & affinity_a) != 0) combined with the
explicit override from model.shape_collision_filter_pairs, and ensure any shape
referenced by explicit <pair> entries (look up via shape_a/shape_b and
selected_shapes) remains included even if the automatic-contact predicate would
exclude it. This change touches the logic that builds ctype/caffinity and the
branches that set uses_mujoco_contacts and the geom_params entries.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@newton/_src/sim/builder.py`:
- Around line 265-269: Update the docstring for the collision_group attribute to
clarify that setting collision_group=0 only disables collisions that would be
derived from the group and does not override explicitly authored masks; mention
that during finalization the function _compile_shape_collision_masks() resolves
collision_group into collision_type and collision_affinity but will ignore
collision_group when explicit masks are present, so mixed configurations retain
explicit-mask behavior.

---

Duplicate comments:
In `@newton/_src/solvers/mujoco/solver_mujoco.py`:
- Around line 4348-4360: The current code recolors collision masks and derives a
coarse color-based collision rule which changes MuJoCo semantics; instead, write
the exact masks from shape_collision_type and shape_collision_affinity into
geom_params["contype"] and geom_params["conaffinity"] (use np.int32(mask).item()
to preserve full 32-bit signed storage), compute uses_mujoco_contacts using the
exact predicate ((type_a & affinity_b) != 0 or (type_b & affinity_a) != 0)
combined with the explicit override from model.shape_collision_filter_pairs, and
ensure any shape referenced by explicit <pair> entries (look up via
shape_a/shape_b and selected_shapes) remains included even if the
automatic-contact predicate would exclude it. This change touches the logic that
builds ctype/caffinity and the branches that set uses_mujoco_contacts and the
geom_params entries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 05366e01-c417-42ae-866e-4d6cdf90b8ab

📥 Commits

Reviewing files that changed from the base of the PR and between 9ae35b1 and 5d3f72a.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • docs/concepts/collisions.rst
  • newton/_src/geometry/broad_phase_common.py
  • newton/_src/geometry/broad_phase_nxn.py
  • newton/_src/geometry/broad_phase_sap.py
  • newton/_src/geometry/edge_redundancy.py
  • newton/_src/sim/builder.py
  • newton/_src/sim/collide.py
  • newton/_src/sim/model.py
  • newton/_src/solvers/mujoco/solver_mujoco.py
  • newton/_src/utils/import_mjcf.py
  • newton/tests/test_broad_phase.py
  • newton/tests/test_import_mjcf.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • newton/_src/utils/import_mjcf.py
  • newton/_src/sim/model.py
  • docs/concepts/collisions.rst
  • newton/tests/test_broad_phase.py
  • newton/_src/geometry/edge_redundancy.py
  • newton/_src/geometry/broad_phase_common.py
  • newton/tests/test_import_mjcf.py
  • newton/_src/geometry/broad_phase_sap.py
  • newton/_src/geometry/broad_phase_nxn.py
  • newton/_src/sim/collide.py

Comment thread newton/_src/sim/builder.py Outdated
@jcarius-nv

Copy link
Copy Markdown
Member

Heads up for after the 1.3 changelog split: this PR currently adds or updates a CHANGELOG.md entry. Once #3145 is merged, please move that entry into the new top-level [Unreleased] section before this PR merges, so it does not land under the released 1.3.0 section.

@m-abr
m-abr force-pushed the m-abr/fix-mjcf-contype-conaffinity branch from 5d3f72a to 1c4d0b3 Compare June 14, 2026 18:40
@m-abr
m-abr had a problem deploying to external-pr-approval June 14, 2026 18:40 — with GitHub Actions Error
@m-abr
m-abr had a problem deploying to external-pr-approval June 14, 2026 18:40 — with GitHub Actions Error
@m-abr
m-abr force-pushed the m-abr/fix-mjcf-contype-conaffinity branch from 1c4d0b3 to 98dcb88 Compare June 14, 2026 18:44
@m-abr
m-abr had a problem deploying to external-pr-approval June 14, 2026 18:44 — with GitHub Actions Error
@m-abr
m-abr had a problem deploying to external-pr-approval June 14, 2026 18:44 — with GitHub Actions Error

@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

🧹 Nitpick comments (1)
newton/_src/solvers/mujoco/solver_mujoco.py (1)

4925-4937: ⚡ Quick win

Skip the quadratic auto-contact scan when MuJoCo contacts are off.

mujoco_auto_contact_shapes is only consumed by the planar-mesh rejection at Lines 5131-5134. When use_mujoco_contacts=False or disable_contacts=True, this O(n^2) loop does no useful work and can noticeably slow solver construction for large shape counts.

♻️ Suggested change
         mujoco_auto_contact_shapes: set[int] = set()
-        for i, shape_a in enumerate(colliding_shapes):
-            shape_a = int(shape_a)
-            for shape_b in colliding_shapes[i + 1 :]:
-                shape_b = int(shape_b)
-                if (min(shape_a, shape_b), max(shape_a, shape_b)) in shape_collision_filter_pairs:
-                    continue
-                if not _shape_worlds_compatible(shape_a, shape_b):
-                    continue
-                if _shape_masks_compatible(shape_a, shape_b):
-                    mujoco_auto_contact_shapes.add(shape_a)
-                    mujoco_auto_contact_shapes.add(shape_b)
+        if self._use_mujoco_contacts and not disable_contacts:
+            for i, shape_a in enumerate(colliding_shapes):
+                shape_a = int(shape_a)
+                for shape_b in colliding_shapes[i + 1 :]:
+                    shape_b = int(shape_b)
+                    if (min(shape_a, shape_b), max(shape_a, shape_b)) in shape_collision_filter_pairs:
+                        continue
+                    if not _shape_worlds_compatible(shape_a, shape_b):
+                        continue
+                    if _shape_masks_compatible(shape_a, shape_b):
+                        mujoco_auto_contact_shapes.add(shape_a)
+                        mujoco_auto_contact_shapes.add(shape_b)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/_src/solvers/mujoco/solver_mujoco.py` around lines 4925 - 4937, The
quadratic loop that populates mujoco_auto_contact_shapes starting at the
enumeration of colliding_shapes performs unnecessary work when MuJoCo contacts
are disabled. Add a guard condition before this loop that checks whether
contacts are actually enabled (checking use_mujoco_contacts and disable_contacts
flags), and skip the entire loop when contacts are off, since
mujoco_auto_contact_shapes is only consumed by the planar-mesh rejection logic
that won't be reached when contacts are disabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@newton/_src/geometry/broad_phase_sap.py`:
- Around line 591-598: Change the condition in the block at shape_collision_type
and shape_collision_affinity validation from using OR to AND, so that
shape_collision_group is only used to compile masks when both
shape_collision_type and shape_collision_affinity are omitted together.
Additionally, add validation to reject cases where exactly one of the two masks
is provided (partial masks), raising a ValueError if either one is None while
the other is not. This ensures the mask arrays must be provided together and
unambiguously, with shape_collision_group only as a fallback when both are
omitted.

---

Nitpick comments:
In `@newton/_src/solvers/mujoco/solver_mujoco.py`:
- Around line 4925-4937: The quadratic loop that populates
mujoco_auto_contact_shapes starting at the enumeration of colliding_shapes
performs unnecessary work when MuJoCo contacts are disabled. Add a guard
condition before this loop that checks whether contacts are actually enabled
(checking use_mujoco_contacts and disable_contacts flags), and skip the entire
loop when contacts are off, since mujoco_auto_contact_shapes is only consumed by
the planar-mesh rejection logic that won't be reached when contacts are
disabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 3115d25e-9588-4648-b798-e17e6c911cca

📥 Commits

Reviewing files that changed from the base of the PR and between 5d3f72a and 1c4d0b3.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • docs/concepts/collisions.rst
  • newton/_src/geometry/broad_phase_common.py
  • newton/_src/geometry/broad_phase_nxn.py
  • newton/_src/geometry/broad_phase_sap.py
  • newton/_src/geometry/edge_redundancy.py
  • newton/_src/sim/builder.py
  • newton/_src/sim/collide.py
  • newton/_src/sim/model.py
  • newton/_src/solvers/mujoco/solver_mujoco.py
  • newton/_src/utils/import_mjcf.py
  • newton/tests/test_broad_phase.py
  • newton/tests/test_import_mjcf.py
  • newton/tests/test_model.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (9)
  • newton/_src/utils/import_mjcf.py
  • newton/_src/sim/collide.py
  • newton/_src/sim/model.py
  • newton/_src/geometry/edge_redundancy.py
  • newton/_src/geometry/broad_phase_nxn.py
  • newton/tests/test_broad_phase.py
  • docs/concepts/collisions.rst
  • newton/_src/geometry/broad_phase_common.py
  • newton/_src/sim/builder.py

Comment thread newton/_src/geometry/broad_phase_sap.py Outdated
@m-abr
m-abr force-pushed the m-abr/fix-mjcf-contype-conaffinity branch from 98dcb88 to 5d18cf2 Compare June 14, 2026 18:51
@m-abr
m-abr had a problem deploying to external-pr-approval June 14, 2026 18:51 — with GitHub Actions Error
@m-abr
m-abr had a problem deploying to external-pr-approval June 14, 2026 18:51 — with GitHub Actions Error

@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.

🧹 Nitpick comments (1)
newton/_src/geometry/broad_phase_nxn.py (1)

229-242: 💤 Low value

Class docstring references outdated filtering mechanism.

The docstring mentions "collision groups" and test_group_pair(), but the implementation now uses type/affinity masks and test_world_and_type_affinity_pair. Consider updating to reflect the current mask-based filtering semantics.

📝 Suggested docstring update
     """A broad phase collision detection class that performs N x N collision checks between all geometry pairs.

     This class performs collision detection between all possible pairs of geometries by checking for
     axis-aligned bounding box (AABB) overlaps. It uses a lower triangular matrix approach to avoid
     checking each pair twice.

-    The collision checks take into account per-geometry cutoff distances and collision groups. Two geometries
+    The collision checks take into account per-geometry cutoff distances and collision masks. Two geometries
     will only be considered as a candidate pair if:
     1. Their AABBs overlap when expanded by their cutoff distances
-    2. Their collision groups allow interaction (determined by test_group_pair())
+    2. Their collision type/affinity masks allow interaction

     The class outputs an array of candidate collision pairs that need more detailed narrow phase collision
     checking.
     """
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@newton/_src/geometry/broad_phase_nxn.py` around lines 229 - 242, The
docstring for the BroadPhaseNxN class contains outdated references to its
filtering mechanism. Update the docstring to replace mentions of "collision
groups" and the `test_group_pair()` method with the current implementation
details that use type/affinity masks and the `test_world_and_type_affinity_pair`
method. Ensure the documentation accurately reflects how geometry pairs are
filtered based on mask-based criteria rather than collision group interactions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@newton/_src/geometry/broad_phase_nxn.py`:
- Around line 229-242: The docstring for the BroadPhaseNxN class contains
outdated references to its filtering mechanism. Update the docstring to replace
mentions of "collision groups" and the `test_group_pair()` method with the
current implementation details that use type/affinity masks and the
`test_world_and_type_affinity_pair` method. Ensure the documentation accurately
reflects how geometry pairs are filtered based on mask-based criteria rather
than collision group interactions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 341a6cdc-6de8-4f98-9689-49453e4e9a60

📥 Commits

Reviewing files that changed from the base of the PR and between 1c4d0b3 and 98dcb88.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • docs/concepts/collisions.rst
  • newton/_src/geometry/broad_phase_common.py
  • newton/_src/geometry/broad_phase_nxn.py
  • newton/_src/geometry/broad_phase_sap.py
  • newton/_src/geometry/edge_redundancy.py
  • newton/_src/sim/builder.py
  • newton/_src/sim/collide.py
  • newton/_src/sim/model.py
  • newton/_src/solvers/mujoco/solver_mujoco.py
  • newton/_src/utils/import_mjcf.py
  • newton/tests/test_broad_phase.py
  • newton/tests/test_import_mjcf.py
  • newton/tests/test_model.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • newton/_src/geometry/edge_redundancy.py
  • docs/concepts/collisions.rst
  • newton/_src/utils/import_mjcf.py
  • newton/_src/sim/collide.py
  • newton/tests/test_model.py
  • newton/_src/geometry/broad_phase_common.py
  • newton/tests/test_broad_phase.py
  • newton/_src/geometry/broad_phase_sap.py
  • newton/tests/test_import_mjcf.py
  • newton/_src/sim/builder.py
  • newton/_src/solvers/mujoco/solver_mujoco.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/concepts/collisions.rst`:
- Around line 1081-1086: The documentation for `collision_group` currently
states that "0 disables collisions" without clarifying that this is only true
when the `collision_type` and `collision_affinity` masks are unset. Since these
masks can override group-derived filtering, the statement is misleading. Modify
the description of the `collision_group` parameter to scope the "0 disables
collisions" statement to only apply when both masks are unset (for example, by
changing it to "0 disables collisions when masks are unset" or similar
clarifying language that prevents users from misinterpreting this as a hard
guarantee when explicit masks are configured).
- Around line 431-445: The collision-type-affinity example in the testcode block
only builds the model but does not verify that the asymmetric filtering behavior
works correctly. After the `builder.finalize()` call, add a `collide()` call to
retrieve collision pairs and include assertions to verify that floor↔ball
collision pairs are present while ball↔ball collision pairs are absent. This
ensures the mask logic validation is part of the example and will catch any
regressions in the filtering behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: fb025a06-8666-47e8-88dc-f775546731ac

📥 Commits

Reviewing files that changed from the base of the PR and between 98dcb88 and 5d18cf2.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • docs/concepts/collisions.rst
  • newton/_src/geometry/broad_phase_common.py
  • newton/_src/geometry/broad_phase_nxn.py
  • newton/_src/geometry/broad_phase_sap.py
  • newton/_src/geometry/edge_redundancy.py
  • newton/_src/sim/builder.py
  • newton/_src/sim/collide.py
  • newton/_src/sim/model.py
  • newton/_src/solvers/mujoco/solver_mujoco.py
  • newton/_src/utils/import_mjcf.py
  • newton/tests/test_broad_phase.py
  • newton/tests/test_import_mjcf.py
  • newton/tests/test_model.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (12)
  • newton/_src/utils/import_mjcf.py
  • newton/_src/sim/collide.py
  • newton/_src/geometry/broad_phase_common.py
  • newton/tests/test_model.py
  • newton/_src/geometry/broad_phase_nxn.py
  • newton/tests/test_broad_phase.py
  • newton/_src/geometry/edge_redundancy.py
  • newton/tests/test_import_mjcf.py
  • newton/_src/sim/model.py
  • newton/_src/geometry/broad_phase_sap.py
  • newton/_src/sim/builder.py
  • newton/_src/solvers/mujoco/solver_mujoco.py

Comment on lines +431 to +445
.. testcode:: collision-type-affinity

builder = newton.ModelBuilder()
floor_cfg = builder.ShapeConfig(collision_type=0, collision_affinity=1)
ball_cfg = builder.ShapeConfig(collision_type=1, collision_affinity=0)

floor = builder.add_shape_box(body=-1, hx=5.0, hy=5.0, hz=0.01, cfg=floor_cfg)
body_a = builder.add_body()
body_b = builder.add_body()
sphere_a = builder.add_shape_sphere(body_a, radius=0.1, cfg=ball_cfg)
sphere_b = builder.add_shape_sphere(body_b, radius=0.1, cfg=ball_cfg)

# The spheres collide with the floor, but not with each other.
model = builder.finalize()

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the example assert the asymmetric filtering behavior.

Right now the doctest only builds the scene; it never checks that floor↔ball pairs are present and ball↔ball pairs are absent. Add a tiny collide + assertion block so this example fails if the mask logic regresses.

✅ Possible check
     sphere_a = builder.add_shape_sphere(body_a, radius=0.1, cfg=ball_cfg)
     sphere_b = builder.add_shape_sphere(body_b, radius=0.1, cfg=ball_cfg)
 
-    # The spheres collide with the floor, but not with each other.
     model = builder.finalize()
+    state = model.state()
+    contacts = model.contacts()
+    model.collide(state, contacts)
+
+    pairs = {
+        tuple(sorted((int(a), int(b))))
+        for a, b in zip(
+            contacts.rigid_contact_shape0.numpy()[:contacts.rigid_contact_count.numpy()[0]],
+            contacts.rigid_contact_shape1.numpy()[:contacts.rigid_contact_count.numpy()[0]],
+        )
+    }
+    assert (floor, sphere_a) in pairs
+    assert (floor, sphere_b) in pairs
+    assert (sphere_a, sphere_b) not in pairs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/concepts/collisions.rst` around lines 431 - 445, The
collision-type-affinity example in the testcode block only builds the model but
does not verify that the asymmetric filtering behavior works correctly. After
the `builder.finalize()` call, add a `collide()` call to retrieve collision
pairs and include assertions to verify that floor↔ball collision pairs are
present while ball↔ball collision pairs are absent. This ensures the mask logic
validation is part of the example and will catch any regressions in the
filtering behavior.

Comment thread docs/concepts/collisions.rst
@m-abr
m-abr force-pushed the m-abr/fix-mjcf-contype-conaffinity branch from 5d18cf2 to 2c7115e Compare June 14, 2026 19:29
@m-abr
m-abr had a problem deploying to external-pr-approval June 14, 2026 19:29 — with GitHub Actions Error
@m-abr
m-abr had a problem deploying to external-pr-approval June 14, 2026 19:29 — with GitHub Actions Failure
@m-abr

m-abr commented Jun 14, 2026

Copy link
Copy Markdown
Author

Thank @jcarius-nv. I think all the suggestions are now addressed

@adenzler-nvidia

Copy link
Copy Markdown
Member

hi @m-abr - sorry for the late response. How critical is this feature for you? we are still undecided on our side if we want to adopt a full mujoco-style collision filtering in the newton core API and what the implications are.

@m-abr

m-abr commented Jul 5, 2026

Copy link
Copy Markdown
Author

Hi @adenzler-nvidia, it's not super critical for me, and I am also not fully happy with my PR as-is, because I would prefer Newton to have one coherent collision filtering method rather than a mix of methods.

Also, I think Newton's collision groups plus excluded pairs are expressive enough for MJCF collisions. Unless I'm missing something, contype/conaffinity can be converted by assigning unique negative groups and excluding every pair that the Mujoco mask rule would reject. This would not be as efficient as the approach in this PR, but it would be cleaner, and MJCF collisions would at least be respected if you prefer to keep the current mechanism.

Regarding efficiency, I think masks might sometimes be faster since they can avoid some excluded-pair lookups, but they are also limited by the fixed bit width, which is usually not a problem anyway. My impression is that the two approaches are probably in a similar range in terms of performance, and I am also not sure what the best long-term direction is.

@han-xudong

Copy link
Copy Markdown
Contributor

Hi @adenzler-nvidia and @m-abr, I wanted to share some context from recent MJCF integration work where this limitation has a practical impact.
Some MJCF models require selective self-collision within the same articulation: certain internal geometry pairs must collide, while others must remain filtered. Treating all nonzero contype/conaffinity values alike can introduce unintended internal contacts, while disabling self-collision entirely also removes required contacts. This affects both simulation correctness and collision-processing cost.
Given the preference for keeping one coherent collision-filtering API in Newton, the conversion approach discussed above seems particularly suitable. The importer could evaluate geom-pair compatibility using the MuJoCo mask rule and compile rejected pairs into Newton collision groups and shape_collision_filter_pairs, potentially using groups to reduce the number of explicit exclusions.
A regression test comparing eligible geom pairs before and after MJCF import would also help verify semantic preservation. I appreciate the work already put into this PR and the careful consideration of the long-term API design.

@eric-heiden

Copy link
Copy Markdown
Member

Hi @han-xudong and @m-abr, I'm prototyping a solution to come up with a conversion from contype/conaffinity to Newton's collision groups and pairwise filters here: #3714.
Can you give this a try? There are limitations going from collision groups/filters to MuJoCo's 32-bit contype/conaffinity values but at least using SolverMuJoCo(..., use_mujoco_contacts=False) should work with Newton's CollisionPipeline to simulate these contact exclusions from MJCF.
I tried this on the Menagerie assets which seems to work so far. I would be interested to see if your use cases are also covered by this. Ideally we can maintain just one convention here, which is the collision group/filter pairs approach from UsdPhysics that we implement in Newton.

@han-xudong

Copy link
Copy Markdown
Contributor

Hi @eric-heiden, thank you for putting this together. I tested #3714 with our MJCF use case. The imported collision relationships match the original contype/conaffinity configuration, and SolverMuJoCo(..., use_mujoco_contacts=False) runs correctly with the expected contact exclusions. This approach covers our use case well.

@eric-heiden

Copy link
Copy Markdown
Member

Closing in favor of #3714.

@eric-heiden eric-heiden closed this Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] contype/conaffinity collision filtering not working

5 participants