Skip to content

Fix masked coupled solver resets - #3649

Merged
jcarius-nv merged 14 commits into
newton-physics:mainfrom
maxkra15:max/fix-coupled-reset-mask-main
Aug 3, 2026
Merged

Fix masked coupled solver resets#3649
jcarius-nv merged 14 commits into
newton-physics:mainfrom
maxkra15:max/fix-coupled-reset-mask-main

Conversation

@maxkra15

@maxkra15 maxkra15 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Description

Builds on the canonical reset-mask contract introduced by #3726, now merged into main: a boolean mask with shape (world_count + 1,), with local-world slots followed by the global-world -1 slot. Established public solver reset entry points still accept the deprecated (world_count,) form, warn once, and normalize it immediately; internal reset paths and child-solver dispatch use only the canonical form.

SolverCoupled.reset() previously forwarded a mask to child solvers while its surrounding distribution, synchronization, reconciliation, and history clearing could still touch complete entry states. Some child solvers also cleared private history globally, so resetting one world could perturb an unselected world.

This PR:

  • applies the canonical mask to coupled gather, scatter, synchronization, reconciliation, and child-solver dispatch;
  • preserves unselected proxy, ADMM, implicit-MPM, MuJoCo/MJWarp, Kamino, VBD, and contact-matching state and history;
  • clears selected proxy feedback and Aitken history with one consolidated masked kernel;
  • evaluates canonical mutable masks on the device so CUDA reset paths remain graph-replay compatible;
  • keeps all-false masks as no-ops for per-world state and persistent coupling/contact history;
  • validates mask dimensionality, dtype, device, and canonical length without reading device values.

Generic reconciliation covers registered STATE attributes with explicit coupled-entry ownership. Custom frequencies require coupler-specific ownership logic. Native MuJoCo CPU synchronization remains host-based, and Kamino receives a local-world slice only at its private backend adapter.

Checklist

  • New or existing tests cover these changes
  • The documentation is up to date with these changes
  • CHANGELOG.md has been updated

Validation

  • uvx pre-commit run -a — passed
  • CI-equivalent Sphinx HTML build with warnings as errors — passed (223 sources)
  • Reset-focused affected suite with unexpected DeprecationWarnings treated as errors — 96 passed
  • Custom/coupled/ADMM/contact modules — 183 passed
  • MuJoCo/VBD/implicit-MPM modules — 239 passed, 1 skipped
  • Kamino DVI/MuJoCo solver modules — 295 passed, 7 skipped
  • Exercised on CPU, RTX 5090 (cuda:0), and RTX 4090 (cuda:1)

Bug reproduction

  1. Advance multiple worlds through a coupled solver so solver-owned and coupling history are populated.
  2. Reset one local or global world with a canonical (world_count + 1,) mask.
  3. Without this change, coupled synchronization or solver-owned history clearing can modify an unselected world; an all-false mask can also discard history.

Summary by CodeRabbit

  • New Features
    • Added world-selective (“masked”) reset support across solvers, coupling state, contact history, and warmstarts.
    • Added support for selecting global entities in reset masks.
    • Added per-world contact-history reset functionality.
  • Bug Fixes
    • Masked resets now preserve unselected worlds and their coupling/contact history.
    • Added validation for invalid reset masks while retaining support for legacy mask formats.
  • Documentation
    • Clarified reset-mask formats and proxy-coupling contact behavior.
  • Tests
    • Expanded coverage across solvers, contact matching, validation, and CUDA replay.

@coderabbitai

coderabbitai Bot commented Jul 24, 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

Masked reset support centralizes world-mask validation and applies selective clearing across coupled state, contact matching, ADMM history, implicit MPM history, MuJoCo synchronization, proxy state, and solver-specific buffers. Tests cover selected, unselected, global, empty, invalid, and replayed masks.

Changes

Masked world reset

Layer / File(s) Summary
Reset mask contract and solver entry points
newton/_src/core/reset.py, newton/_src/solvers/solver.py, newton/_src/solvers/vbd/*, newton/_src/solvers/kamino/*, newton/_src/sim/collide.py, newton/_src/solvers/implicit_mpm/*, newton/_src/solvers/mujoco/*
Shared validation enforces boolean, dimensional, device, and size requirements while solver-specific paths normalize canonical and deprecated masks.
Coupled state distribution and reconciliation
newton/_src/solvers/coupled/interface.py, newton/_src/solvers/coupled/solver_coupled.py
Selected rows are distributed to entries, synchronized through the reset hook, selectively cleared, and reconciled to parent state.
Contact, ADMM, and proxy history reset
newton/_src/geometry/contact_match.py, newton/_src/solvers/coupled/*
Contact matching, broken-contact reporting, ADMM dual rows, proxy forces, and collision pipeline history respect selected worlds.
Regression coverage and documentation
newton/tests/*, docs/concepts/*, CHANGELOG.md
Tests cover selective reset behavior across solvers and history types, while documentation describes global mask entries and masked collision behavior.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: chschuma-disney, nvtw

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the pull request's primary change to masked coupled solver reset behavior.
✨ 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.

@maxkra15
maxkra15 had a problem deploying to external-pr-approval July 24, 2026 23:51 — with GitHub Actions Error
@maxkra15
maxkra15 had a problem deploying to external-pr-approval July 24, 2026 23: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.

Actionable comments posted: 1

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

3765-3780: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add CPU-backend coverage for masked MuJoCo buffer resets.

newton/tests/test_mujoco_reset.py::TestMuJoCoReset exercises SolverMuJoCo’s default mjwarp path, but reset()’s CPU-only branch takes a separate control-flow path and checks local_world_mask[0]. Add a use_mujoco_cpu=True multi-world reset test that covers global-only masks, local-world-only masks, and the early-return behavior.

🤖 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 3765 - 3780, Add
CPU-backend coverage in TestMuJoCoReset for SolverMuJoCo.reset() with
use_mujoco_cpu=True and multiple worlds. Exercise global-only and
local-world-only masks, verifying the corresponding MuJoCo buffers reset, and
verify early return when local_world_mask[0] is false; preserve existing default
mjwarp tests.
newton/_src/geometry/contact_match.py (1)

762-779: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

ContactMatcher.reset() skips the shared mask-validation contract.

Unlike SolverBase._validate_reset_world_mask (dtype, ndim, device, and W/W+1 length checks), this reset() calls world_mask.numpy() and OR's it into the host mask with no dtype/device/shape validation. A caller passing a wrong-dtype, wrong-device, or wrong-length mask will hit a raw numpy/Warp error instead of the clear, consistent error messages the rest of this PR establishes for the same contract (e.g. SolverBase, SolverVBD, SolverKamino).

♻️ Suggested validation for consistency with the shared contract
     def reset(self, world_mask: wp.array[wp.bool] | None = None) -> None:
         """Clear all or reset-selected cross-frame contact history.
         ...
         """
         if world_mask is None:
             self._prev_count.zero_()
             self._reset_world_mask_host.fill(False)
             self._reset_world_mask.zero_()
             return

+        if not isinstance(world_mask, wp.array) or world_mask.dtype != wp.bool or world_mask.ndim != 1:
+            raise TypeError("'world_mask' must be a one-dimensional Warp bool array or None.")
+        if world_mask.shape[0] not in (self._world_count, self._world_count + 1):
+            raise ValueError(
+                f"'world_mask' length {world_mask.shape[0]} must equal world_count "
+                f"({self._world_count}) or world_count + 1 ({self._world_count + 1})."
+            )
         mask = world_mask.numpy()
         if not bool(mask.any()):
             return
         self._reset_world_mask_host[: mask.shape[0]] |= mask
         self._reset_world_mask.assign(self._reset_world_mask_host)

Since newton/_src/sim/collide.py (where CollisionPipeline.reset_contact_matching() presumably calls into this) is not part of this review, please confirm whether validation already happens at that call site before deciding whether to duplicate it here.

🤖 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/contact_match.py` around lines 762 - 779, Update
ContactMatcher.reset to enforce the same world-mask validation contract as
SolverBase._validate_reset_world_mask before calling world_mask.numpy() or
modifying reset masks, including dtype, ndim, device, and W/W+1 length checks.
First verify whether CollisionPipeline.reset_contact_matching already performs
this validation; if it does not, add validation in ContactMatcher.reset using
the shared validator or equivalent established checks and preserve the existing
full-reset behavior.
newton/_src/solvers/coupled/interface.py (1)

248-261: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an Args: section to the new hook's docstring.

coupling_sync_reset_state has four parameters but no Args: block, unlike sibling hooks in this file (e.g. coupling_eval_effective_mass, coupling_eval_effective_mass_block) which document each parameter. Since this is a new public extension point solver authors will override, documenting state_in, state_out, world_mask, and flags improves discoverability.

As per coding guidelines: "Use Google-style docstrings, keep types in annotations, format Args: entries as name: description..."

📝 Proposed docstring addition
     def coupling_sync_reset_state(
         self,
         state_in: State,
         state_out: State,
         world_mask: wp.array[wp.bool],
         flags: StateFlags | int | None,
     ) -> None:
         """Synchronize solver-owned state after a masked coupled reset.

         Public state arrays are synchronized by the coupled solver. Override
         this hook only for persistent custom state arrays.
+
+        Args:
+            state_in: Entry-local reset input state.
+            state_out: Entry-local reset output state.
+            world_mask: Validated reset mask forwarded unchanged from the coupled reset call.
+            flags: Reset flags bitmask forwarded unchanged from the coupled reset call.
         """
         del state_in, state_out, world_mask, flags
🤖 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/coupled/interface.py` around lines 248 - 261, Add a
Google-style Args section to the coupling_sync_reset_state docstring,
documenting state_in, state_out, world_mask, and flags with concise
descriptions; keep their type information in the existing annotations and leave
the hook behavior unchanged.

Source: Coding guidelines

newton/_src/sim/collide.py (1)

1227-1246: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider sharing world_mask validation with SolverBase._validate_reset_world_mask.

This method re-implements the same type/dtype/ndim/device/shape (world_count or world_count + 1) checks as SolverBase._validate_reset_world_mask in newton/_src/solvers/solver.py. Extracting a shared module-level validator (e.g. in a common utils module) both call would prevent the two copies from silently drifting if the mask contract changes.

🤖 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/collide.py` around lines 1227 - 1246, Extract the duplicated
world_mask validation from reset_contact_matching and
SolverBase._validate_reset_world_mask into a shared module-level validator, then
call it from both methods. Preserve all existing type, dtype, dimensionality,
device, and world_count/world_count + 1 shape checks and their current
validation behavior.
newton/_src/solvers/coupled/solver_coupled.py (1)

2108-2153: 🚀 Performance & Scalability | 🔵 Trivial

Avoid host-side .numpy() roundtrips in _reset_rows.

_reset_rows copies model entry mapping arrays to host, applies the mask on host, then launches entry-local Warp arrays. With world_mask, masked reset() is intended as the selective RL reset path; on this path it syncs per entry/domain pair, which can scale poorly in repeated per-step resets. Keep the row-selection/result on-device or amortize the selection across entries instead of redoing it inside _reset_rows.

🤖 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/coupled/solver_coupled.py` around lines 2108 - 2153, The
_reset_rows method currently materializes world mappings with host-side .numpy()
calls and repeats masked selection for every entry/domain pair. Replace this
path with device-resident row selection, or cache/amortize the shared selection
so world_mask resets avoid per-entry/domain synchronization while preserving the
returned per-entry domain mappings.
newton/_src/solvers/coupled/solver_coupled_proxy.py (1)

1078-1105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document why masked reset only clears coupling_forces.

The masked branch zeroes only mapping.coupling_forces, while the full reset also clears coupling_forces_previous, aitken_residual_previous, aitken_stats, aitken_relaxation, aitken_has_previous, and proxy_qd_before. This is safe today because _stash_proxy_feedback/wp.copy(proxy.proxy_qd_before, ...) unconditionally overwrite those buffers before they're read each step, and _reset_aitken_iteration_state() resets the Aitken scalars every _step_coupled call regardless of reset(). However, this asymmetry is non-obvious and risks a future regression (e.g., if the overwrite-before-use invariant is ever broken elsewhere). A short comment explaining why these buffers are intentionally excluded from the masked path would help future maintainers avoid re-introducing the removed per-buffer masked kernels unnecessarily, or worse, missing a case where the invariant doesn't hold.

📝 Suggested comment
         else:
+            # coupling_forces_previous / aitken_* / proxy_qd_before are intentionally left
+            # untouched here: they are unconditionally overwritten (stash/copy) before being
+            # read on the next step, and Aitken scalars are reset every _step_coupled call
+            # regardless of reset(), so masking them would be redundant.
             for mapping_group, entity_world in (
🤖 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/coupled/solver_coupled_proxy.py` around lines 1078 -
1105, Add a concise comment in the masked reset branch near the
`_zero_global_proxy_values_masked_kernel` launch explaining that only
`coupling_forces` is intentionally cleared because
`_stash_proxy_feedback`/`wp.copy` overwrite `proxy_qd_before` and related
feedback/Aitken buffers before use, while `_reset_aitken_iteration_state()`
resets Aitken state each step. Clarify that the full reset still clears those
buffers, preserving the intentional asymmetry.
🤖 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/tests/test_implicit_mpm_multiworld_sparse.py`:
- Around line 523-530: Extend the reset assertions after solver.reset in the
sparse-grid test to also validate solver._grid_accumulated_status. Assert its
value remains the expected Volume.REBUILD_VOXEL_CAPACITY_EXCEEDED status,
alongside the existing _grid_status assertion, covering both persistent status
buffers.

---

Nitpick comments:
In `@newton/_src/geometry/contact_match.py`:
- Around line 762-779: Update ContactMatcher.reset to enforce the same
world-mask validation contract as SolverBase._validate_reset_world_mask before
calling world_mask.numpy() or modifying reset masks, including dtype, ndim,
device, and W/W+1 length checks. First verify whether
CollisionPipeline.reset_contact_matching already performs this validation; if it
does not, add validation in ContactMatcher.reset using the shared validator or
equivalent established checks and preserve the existing full-reset behavior.

In `@newton/_src/sim/collide.py`:
- Around line 1227-1246: Extract the duplicated world_mask validation from
reset_contact_matching and SolverBase._validate_reset_world_mask into a shared
module-level validator, then call it from both methods. Preserve all existing
type, dtype, dimensionality, device, and world_count/world_count + 1 shape
checks and their current validation behavior.

In `@newton/_src/solvers/coupled/interface.py`:
- Around line 248-261: Add a Google-style Args section to the
coupling_sync_reset_state docstring, documenting state_in, state_out,
world_mask, and flags with concise descriptions; keep their type information in
the existing annotations and leave the hook behavior unchanged.

In `@newton/_src/solvers/coupled/solver_coupled_proxy.py`:
- Around line 1078-1105: Add a concise comment in the masked reset branch near
the `_zero_global_proxy_values_masked_kernel` launch explaining that only
`coupling_forces` is intentionally cleared because
`_stash_proxy_feedback`/`wp.copy` overwrite `proxy_qd_before` and related
feedback/Aitken buffers before use, while `_reset_aitken_iteration_state()`
resets Aitken state each step. Clarify that the full reset still clears those
buffers, preserving the intentional asymmetry.

In `@newton/_src/solvers/coupled/solver_coupled.py`:
- Around line 2108-2153: The _reset_rows method currently materializes world
mappings with host-side .numpy() calls and repeats masked selection for every
entry/domain pair. Replace this path with device-resident row selection, or
cache/amortize the shared selection so world_mask resets avoid per-entry/domain
synchronization while preserving the returned per-entry domain mappings.

In `@newton/_src/solvers/mujoco/solver_mujoco.py`:
- Around line 3765-3780: Add CPU-backend coverage in TestMuJoCoReset for
SolverMuJoCo.reset() with use_mujoco_cpu=True and multiple worlds. Exercise
global-only and local-world-only masks, verifying the corresponding MuJoCo
buffers reset, and verify early return when local_world_mask[0] is false;
preserve existing default mjwarp tests.
🪄 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 Plus

Run ID: df6dbd9b-5351-4edd-ba62-1ffe0eb5279f

📥 Commits

Reviewing files that changed from the base of the PR and between d7581b7 and 6704635.

📒 Files selected for processing (23)
  • CHANGELOG.md
  • newton/_src/geometry/contact_match.py
  • newton/_src/sim/collide.py
  • newton/_src/solvers/coupled/admm_utils.py
  • newton/_src/solvers/coupled/interface.py
  • newton/_src/solvers/coupled/solver_coupled.py
  • newton/_src/solvers/coupled/solver_coupled_admm.py
  • newton/_src/solvers/coupled/solver_coupled_proxy.py
  • newton/_src/solvers/implicit_mpm/implicit_mpm_solver_kernels.py
  • newton/_src/solvers/implicit_mpm/solver_implicit_mpm.py
  • newton/_src/solvers/kamino/_src/solvers/dvi/solver.py
  • newton/_src/solvers/kamino/_src/solvers/padmm/solver.py
  • newton/_src/solvers/kamino/solver_kamino.py
  • newton/_src/solvers/mujoco/kernels.py
  • newton/_src/solvers/mujoco/solver_mujoco.py
  • newton/_src/solvers/solver.py
  • newton/_src/solvers/vbd/solver_vbd.py
  • newton/tests/test_admm_coupled_solver.py
  • newton/tests/test_contact_matching.py
  • newton/tests/test_coupled_solver.py
  • newton/tests/test_implicit_mpm_multiworld_sparse.py
  • newton/tests/test_mujoco_reset.py
  • newton/tests/test_solver_vbd.py

Comment thread newton/tests/test_implicit_mpm_multiworld_sparse.py Outdated
@maxkra15
maxkra15 marked this pull request as ready for review July 25, 2026 08:47
@maxkra15
maxkra15 requested a review from a team as a code owner July 25, 2026 08:47
@coderabbitai coderabbitai Bot mentioned this pull request Jul 27, 2026
3 tasks

@gdaviet gdaviet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Graph-capturability and attribute-metadata findings.

Comment thread newton/_src/solvers/solver.py Outdated
Comment thread newton/_src/solvers/coupled/solver_coupled.py Outdated
Comment thread newton/_src/solvers/coupled/solver_coupled.py Outdated
Comment thread newton/_src/solvers/coupled/solver_coupled.py Outdated
Comment thread newton/_src/geometry/contact_match.py Outdated
@maxkra15
maxkra15 force-pushed the max/fix-coupled-reset-mask-main branch from 6704635 to 5dac41a Compare July 27, 2026 21:11
@maxkra15
maxkra15 temporarily deployed to external-pr-approval July 27, 2026 21:11 — with GitHub Actions Inactive
@maxkra15
maxkra15 temporarily deployed to external-pr-approval July 27, 2026 21:11 — with GitHub Actions Inactive

Copy link
Copy Markdown
Contributor Author

@gdaviet One follow-up from testing this that I’d appreciate your take on. I kept this PR scoped to masked reset instead of adding broad configuration rejections, but two adjacent cases remain:

  • Proxy collide_interval > 1 is driven by a Python counter. Masked reset now preserves that cadence and cached contact history, but a captured full step freezes the collide/no-collide branch recorded at capture time, and a teleported selected world may reuse cached proxy contacts until the next scheduled refresh. Would you prefer a device-side cadence/conditional graph path, or should > 1 simply be documented as outside full-step graph capture?
  • Kamino’s private pre/post reset callbacks currently receive the full converted state and can run arbitrary Python, so they cannot strictly guarantee masked or replayable behavior. Would passing the mask through a callback-aware API be the right direction, or should callback configurations remain explicitly outside the guarantee?

I left both existing behaviors intact here rather than rejecting otherwise valid configurations.

@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 (2)
newton/_src/solvers/coupled/solver_coupled.py (1)

490-509: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unresolvable frequencies silently drop out of reset.

When _custom_frequency_row_worlds returns None (custom frequency with no attribute referencing WORLD), the frequency is omitted from _reset_row_world, so _launch_reset_view_kernel / _launch_reset_owned_scatter become no-ops for every state attribute at that frequency — including the full-reset path (world_mask=None_full_reset_world_mask). A registered STATE attribute at such a frequency will never be synchronized or cleared, with no diagnostic. Consider a debug-level log so this is discoverable.

🤖 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/coupled/solver_coupled.py` around lines 490 - 509, The
reset-row-world construction in the loop handling required frequencies silently
skips custom frequencies when _custom_frequency_row_worlds returns None. Add a
debug-level diagnostic at that branch identifying the unresolved item_frequency
and that reset handling is being skipped, while preserving the existing continue
behavior and all other reset paths.
newton/_src/geometry/contact_match.py (1)

594-612: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the sort-key shape decode into make_contact_sort_key.

The broken-contact kernel in newton/_src/geometry/contact_match.py:607-608 duplicates the layout (shape_a << 43 | shape_b << 23 | sub_key, with 20-bit masks) from newton/_src/geometry/contact_data.py. Add an inverse wp.func, e.g. _decode_sort_key_shapes, to both the packer and this kernel so the layout stays single-sourced.

🤖 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/contact_match.py` around lines 594 - 612, Extract the
duplicated shape-bit decoding from the broken-contact kernel into a reusable
inverse wp.func alongside make_contact_sort_key in contact_data.py, using the
same 43/23 shifts and 20-bit masks. Update the broken-contact kernel to call
this decoder for shape0 and shape1, preserving the existing reset_world_selected
behavior.
🤖 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/contact_match.py`:
- Around line 764-770: Update the ContactMatcher.reset docstring to add a
Google-style Args: entry for world_mask, documenting the accepted W or W + 1
shape contract and that applying a mask accumulates selection rather than
clearing _prev_count; leave the reset behavior unchanged.

---

Nitpick comments:
In `@newton/_src/geometry/contact_match.py`:
- Around line 594-612: Extract the duplicated shape-bit decoding from the
broken-contact kernel into a reusable inverse wp.func alongside
make_contact_sort_key in contact_data.py, using the same 43/23 shifts and 20-bit
masks. Update the broken-contact kernel to call this decoder for shape0 and
shape1, preserving the existing reset_world_selected behavior.

In `@newton/_src/solvers/coupled/solver_coupled.py`:
- Around line 490-509: The reset-row-world construction in the loop handling
required frequencies silently skips custom frequencies when
_custom_frequency_row_worlds returns None. Add a debug-level diagnostic at that
branch identifying the unresolved item_frequency and that reset handling is
being skipped, while preserving the existing continue behavior and all other
reset paths.
🪄 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 Plus

Run ID: 9c7621a9-9078-4e06-bf13-1e2ee8b96160

📥 Commits

Reviewing files that changed from the base of the PR and between 6704635 and 5dac41a.

📒 Files selected for processing (25)
  • CHANGELOG.md
  • docs/concepts/coupling.rst
  • newton/_src/core/reset.py
  • newton/_src/geometry/contact_match.py
  • newton/_src/sim/collide.py
  • newton/_src/solvers/coupled/admm_utils.py
  • newton/_src/solvers/coupled/interface.py
  • newton/_src/solvers/coupled/solver_coupled.py
  • newton/_src/solvers/coupled/solver_coupled_admm.py
  • newton/_src/solvers/coupled/solver_coupled_proxy.py
  • newton/_src/solvers/implicit_mpm/implicit_mpm_solver_kernels.py
  • newton/_src/solvers/implicit_mpm/solver_implicit_mpm.py
  • newton/_src/solvers/kamino/_src/solvers/dvi/solver.py
  • newton/_src/solvers/kamino/_src/solvers/padmm/solver.py
  • newton/_src/solvers/kamino/solver_kamino.py
  • newton/_src/solvers/mujoco/kernels.py
  • newton/_src/solvers/mujoco/solver_mujoco.py
  • newton/_src/solvers/solver.py
  • newton/_src/solvers/vbd/solver_vbd.py
  • newton/tests/test_admm_coupled_solver.py
  • newton/tests/test_contact_matching.py
  • newton/tests/test_coupled_solver.py
  • newton/tests/test_implicit_mpm_multiworld_sparse.py
  • newton/tests/test_mujoco_reset.py
  • newton/tests/test_solver_vbd.py
🚧 Files skipped from review as they are similar to previous changes (12)
  • CHANGELOG.md
  • newton/_src/solvers/coupled/interface.py
  • newton/_src/solvers/mujoco/kernels.py
  • newton/_src/sim/collide.py
  • newton/_src/solvers/kamino/_src/solvers/padmm/solver.py
  • newton/_src/solvers/kamino/_src/solvers/dvi/solver.py
  • newton/_src/solvers/kamino/solver_kamino.py
  • newton/_src/solvers/coupled/solver_coupled_admm.py
  • newton/tests/test_implicit_mpm_multiworld_sparse.py
  • newton/tests/test_admm_coupled_solver.py
  • newton/_src/solvers/mujoco/solver_mujoco.py
  • newton/_src/solvers/coupled/solver_coupled_proxy.py

Comment thread newton/_src/geometry/contact_match.py
@gdaviet

gdaviet commented Jul 28, 2026

Copy link
Copy Markdown
Member

@gdaviet One follow-up from testing this that I’d appreciate your take on. I kept this PR scoped to masked reset instead of adding broad configuration rejections, but two adjacent cases remain:

* Proxy `collide_interval > 1` is driven by a Python counter. Masked reset now preserves that cadence and cached contact history, but a captured full step freezes the collide/no-collide branch recorded at capture time, and a teleported selected world may reuse cached proxy contacts until the next scheduled refresh. Would you prefer a device-side cadence/conditional graph path, or should `> 1` simply be documented as outside full-step graph capture?

* Kamino’s private pre/post reset callbacks currently receive the full converted state and can run arbitrary Python, so they cannot strictly guarantee masked or replayable behavior. Would passing the mask through a callback-aware API be the right direction, or should callback configurations remain explicitly outside the guarantee?

I left both existing behaviors intact here rather than rejecting otherwise valid configurations.

I think 1 is fine, at least for the scope of this PR. Typically if collide_interval > 1 we should expect that the graph granularity will be a multiple of collide_interval steps.
For the Kamino callbacks I would tend to agree that passing the world_mask to the callbacks would be helpful, but I don;t know much about them. This can likely be done as a follow-up, @newton-physics/disney-research for viz

@chschuma-disney chschuma-disney left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for looking into this. The changes on Kamino's side look good to me.

Good catch pointing out the issues with the callbacks. I'll make sure that we track the issue and fix it in a follow-up PR.

I'm not too familiar with the contact match code, so it might make sense to ping someone to review those changes.

@jcarius-nv jcarius-nv added the IsaacLab Issues that are related to the Newton integration into IsaacLab. label Jul 28, 2026
@jcarius-nv jcarius-nv added this to the 1.5 Release milestone Jul 28, 2026
@jcarius-nv jcarius-nv mentioned this pull request Jul 28, 2026
3 tasks
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

@jcarius-nv jcarius-nv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Standards and spec findings from an exact-head review of 5dac41a7. I did not duplicate the existing open ContactMatcher.reset() docstring thread.

Comment thread newton/_src/core/reset.py Outdated
Comment thread newton/_src/sim/collide.py Outdated
Comment thread newton/_src/solvers/coupled/solver_coupled.py

Copy link
Copy Markdown
Member

Fit question for @nvtw: this PR adds CollisionPipeline.reset_contact_matching() as stable public API, while its primary consumer here is the experimental coupled-solver path. Since you have been working on the collision pipeline, could you assess whether there is an intended durable standalone user workflow for this method? If not, I think the reset repair still fits Newton, but the hook should remain internal or be explicitly experimental to avoid an unnecessary long-term public contract.

Use a final mask entry for global world -1 across the public solver
reset APIs and coupled proxy state.

Keep local-only masks compatible during deprecation and document how
world-start offsets represent split global entity ranges.
@maxkra15
maxkra15 had a problem deploying to external-pr-approval July 31, 2026 01:00 — with GitHub Actions Error
@maxkra15

Copy link
Copy Markdown
Contributor Author

Thanks, @maxkra15 can you please rebase this PR on top of the changes in #3726? There are a couple of changes here related to the coupled-reset handling we should have but it would be better to enforce one convention for the world masks across newton, i.e. to have the array with dimension world_count + 1 rather than both world_count and world_count + 1.

had my agent rebase, can you check this matches your idea @eric-heiden ?

@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

Caution

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

⚠️ Outside diff range comments (1)
newton/_src/solvers/implicit_mpm/solver_implicit_mpm.py (1)

1566-1572: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Docstring overstates status clearing for masked resets.

Rebuild status is now cleared only when at least one mask entry is selected (see the masked kernel at lines 141-151); an all-false mask leaves both status buffers untouched, which the new regression asserts. Please align the wording.

📝 Proposed doc tweak
-        every warm-start field. Sparse-grid rebuild status is always cleared at
-        a valid reset boundary, and the previous-collider-pose cache is
+        every warm-start field. Sparse-grid rebuild status is cleared at a
+        valid reset boundary when at least one world is selected, and the
+        previous-collider-pose cache is
         refreshed from ``state``.
🤖 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/implicit_mpm/solver_implicit_mpm.py` around lines 1566 -
1572, Update the reset-operation docstring near the masked reset behavior to
state that sparse-grid rebuild status is cleared only when the mask selects at
least one entry; explicitly note that an all-false mask leaves both status
buffers unchanged. Keep the existing descriptions of full resets and
previous-collider-pose cache refresh intact.
🧹 Nitpick comments (3)
newton/_src/solvers/vbd/rigid_vbd_kernels.py (1)

99-110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate with the shared canonical mask-selection helper.

This reimplements the "reset mask contract" selection logic already centralized in newton/_src/core/reset.py::reset_world_selected, but with slightly different semantics: it treats any negative world as the global slot, whereas the shared helper strictly requires world == -1. implicit_mpm_solver_kernels.py already calls the shared wp.func directly from another solver module, so the same pattern works here. Delegating avoids two independently-maintained copies of this contract drifting apart.

♻️ Proposed consolidation
+from ...core.reset import reset_world_selected
+
+
 `@wp.func`
 def _reset_world_selected(
     world: int,
     world_mask: wp.array[wp.bool],
     reset_all: bool,
     world_count: int,
 ):
     """Query a public reset mask whose final entry selects global entities."""
     if reset_all:
         return True
-    if world < 0:
-        world = world_count
-    return world_mask[world]
+    return reset_world_selected(world, world_mask, world_count)
🤖 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/vbd/rigid_vbd_kernels.py` around lines 99 - 110, Replace
the local selection logic in _reset_world_selected with a call to the canonical
reset_world_selected helper from newton._src.core.reset, preserving the existing
arguments and return behavior while relying on its strict world == -1 contract;
remove the duplicated reset_all, negative-world, and mask-index handling.
newton/_src/solvers/coupled/solver_coupled_admm.py (1)

1820-1857: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a single group-enumeration helper.

The full ADMM group set is now spelled out in _reset_coupling_state, _reset_admm_history, and the proximal-marking paths. A new group type must be added to every list or masked reset silently stops clearing it. A shared _all_admm_groups() (or per-kind accessors returning the endpoint/world pairs) would keep these in lockstep.

🤖 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/coupled/solver_coupled_admm.py` around lines 1820 - 1857,
Add a shared ADMM group-enumeration helper, such as _all_admm_groups(), that
returns every group with its endpoint IDs and world views, then reuse it in
_reset_admm_history, _reset_coupling_state, and proximal-marking paths. Remove
the duplicated group lists and preserve each caller’s existing filtering or
masking behavior so newly added group types are handled consistently.
newton/tests/test_coupled_solver.py (1)

870-885: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the legacy-mask and empty-model assertions into their own tests.

These lines run after the for mask_values, selected_rows loop and silently reuse model, parent, coupled, and entry from the last iteration (the all-false subTest), then rebind model for an unrelated fixture. It works, but the coupling to loop-variable leakage makes failures hard to attribute; two small test methods (..._expands_legacy_mask, ..._rejects_oversized_mask_on_empty_model) would be self-contained.

🤖 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/tests/test_coupled_solver.py` around lines 870 - 885, The legacy-mask
deprecation assertions and empty-model oversized-mask assertion currently depend
on variables left by the preceding mask loop. Move the legacy-mask scenario into
a self-contained test named for expanding the legacy mask, and move the
empty-model ValueError scenario into a separate test named for rejecting an
oversized mask; create each test’s own model, parent, coupled solver, and entry
fixtures while preserving the existing assertions.
🤖 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/coupling.rst`:
- Around line 211-212: Revise the reset-contract wording in the paragraph
beginning “Masked resets preserve” to explicitly include global entities:
describe matching history selected by the mask, including the optional W + 1
global-entity entry, rather than only selected-world history.

In `@newton/_src/solvers/coupled/solver_coupled.py`:
- Around line 146-157: Update _zero_reset_view_rows_kernel to zero values using
a dtype-compatible default scalar constructor instead of values.dtype(0.0),
ensuring reset and forced input attributes such as state.body_f,
state.particle_f, body_parent_f, and scalar transform-backed fields work across
their supported array dtypes; alternatively, restrict the kernel to dtypes that
safely support the current constructor.

---

Outside diff comments:
In `@newton/_src/solvers/implicit_mpm/solver_implicit_mpm.py`:
- Around line 1566-1572: Update the reset-operation docstring near the masked
reset behavior to state that sparse-grid rebuild status is cleared only when the
mask selects at least one entry; explicitly note that an all-false mask leaves
both status buffers unchanged. Keep the existing descriptions of full resets and
previous-collider-pose cache refresh intact.

---

Nitpick comments:
In `@newton/_src/solvers/coupled/solver_coupled_admm.py`:
- Around line 1820-1857: Add a shared ADMM group-enumeration helper, such as
_all_admm_groups(), that returns every group with its endpoint IDs and world
views, then reuse it in _reset_admm_history, _reset_coupling_state, and
proximal-marking paths. Remove the duplicated group lists and preserve each
caller’s existing filtering or masking behavior so newly added group types are
handled consistently.

In `@newton/_src/solvers/vbd/rigid_vbd_kernels.py`:
- Around line 99-110: Replace the local selection logic in _reset_world_selected
with a call to the canonical reset_world_selected helper from
newton._src.core.reset, preserving the existing arguments and return behavior
while relying on its strict world == -1 contract; remove the duplicated
reset_all, negative-world, and mask-index handling.

In `@newton/tests/test_coupled_solver.py`:
- Around line 870-885: The legacy-mask deprecation assertions and empty-model
oversized-mask assertion currently depend on variables left by the preceding
mask loop. Move the legacy-mask scenario into a self-contained test named for
expanding the legacy mask, and move the empty-model ValueError scenario into a
separate test named for rejecting an oversized mask; create each test’s own
model, parent, coupled solver, and entry fixtures while preserving the existing
assertions.
🪄 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 Plus

Run ID: 6e48bbdd-f5e9-4731-a778-2149503a3aa8

📥 Commits

Reviewing files that changed from the base of the PR and between 8e17fb7 and f8dcb17.

📒 Files selected for processing (30)
  • CHANGELOG.md
  • docs/concepts/coupling.rst
  • docs/concepts/worlds.rst
  • newton/_src/core/reset.py
  • newton/_src/geometry/contact_match.py
  • newton/_src/sim/collide.py
  • newton/_src/solvers/coupled/admm_utils.py
  • newton/_src/solvers/coupled/interface.py
  • newton/_src/solvers/coupled/solver_coupled.py
  • newton/_src/solvers/coupled/solver_coupled_admm.py
  • newton/_src/solvers/coupled/solver_coupled_proxy.py
  • newton/_src/solvers/implicit_mpm/implicit_mpm_solver_kernels.py
  • newton/_src/solvers/implicit_mpm/solver_implicit_mpm.py
  • newton/_src/solvers/kamino/_src/solvers/dvi/solver.py
  • newton/_src/solvers/kamino/_src/solvers/padmm/solver.py
  • newton/_src/solvers/kamino/solver_kamino.py
  • newton/_src/solvers/kamino/tests/test_solvers_dvi.py
  • newton/_src/solvers/mujoco/kernels.py
  • newton/_src/solvers/mujoco/solver_mujoco.py
  • newton/_src/solvers/solver.py
  • newton/_src/solvers/vbd/rigid_vbd_kernels.py
  • newton/_src/solvers/vbd/solver_vbd.py
  • newton/tests/test_admm_coupled_solver.py
  • newton/tests/test_contact_matching.py
  • newton/tests/test_coupled_solver.py
  • newton/tests/test_custom_solver.py
  • newton/tests/test_implicit_mpm_multiworld_sparse.py
  • newton/tests/test_mujoco_reset.py
  • newton/tests/test_solver_kamino_dvi.py
  • newton/tests/test_solver_vbd.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Comment thread docs/concepts/coupling.rst Outdated
Comment thread newton/_src/solvers/coupled/solver_coupled.py Outdated
@eric-heiden

Copy link
Copy Markdown
Member

🔵 Nice work making the reset mask handle global entities in world -1 explicitly, and thanks for updating the docs and CHANGELOG along with it.

One thing to sort out before merge: the new (world_count,) deprecation is only written as prose in the reset() docstrings, so the rendered API pages do not carry the versioned deprecation marker that the project's compatibility policy asks for.

Comment thread newton/_src/solvers/solver.py Outdated
Render the legacy mask deprecation in every solver reset API and make masked row clearing compile for scalar and transform state arrays.
@maxkra15
maxkra15 had a problem deploying to external-pr-approval August 1, 2026 18:25 — with GitHub Actions Error
@maxkra15
maxkra15 had a problem deploying to external-pr-approval August 1, 2026 18:25 — with GitHub Actions Error
@maxkra15
maxkra15 temporarily deployed to external-pr-approval August 1, 2026 19:16 — with GitHub Actions Inactive
@maxkra15
maxkra15 temporarily deployed to external-pr-approval August 1, 2026 19:16 — with GitHub Actions Inactive
eric-heiden
eric-heiden previously approved these changes Aug 2, 2026

@eric-heiden eric-heiden left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks!

Comment thread newton/_src/solvers/coupled/interface.py Outdated
@maxkra15
maxkra15 had a problem deploying to external-pr-approval August 3, 2026 14:37 — with GitHub Actions Error
@maxkra15
maxkra15 had a problem deploying to external-pr-approval August 3, 2026 14:37 — with GitHub Actions Error
@jcarius-nv
jcarius-nv merged commit 6e42521 into newton-physics:main Aug 3, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

IsaacLab Issues that are related to the Newton integration into IsaacLab.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants