Skip to content

Clamp viewer picking force to prevent explosion on light objects#2385

Merged
mzamoramora-nvidia merged 3 commits into
newton-physics:mainfrom
mzamoramora-nvidia:mzamoramora/fix-picking-force-clamp
Apr 9, 2026
Merged

Clamp viewer picking force to prevent explosion on light objects#2385
mzamoramora-nvidia merged 3 commits into
newton-physics:mainfrom
mzamoramora-nvidia:mzamoramora/fix-picking-force-clamp

Conversation

@mzamoramora-nvidia

@mzamoramora-nvidia mzamoramora-nvidia commented Apr 9, 2026

Copy link
Copy Markdown
Member

Description

Clamp the viewer picking force magnitude to 5g of the picked body's weight, preventing a positive feedback loop where the spring-damper force diverges on light objects bouncing off stiff contacts.

Closes #2361

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

Manual verification with examples that use the viewer picker:

uv run -m newton.examples nut_bolt_hydro                          # reproducer — pick the nut along bolt threads
uv run -m newton.examples basic_urdf                               # light objects on a table
uv run -m newton.examples brick_stacking                           # moderate mass, stacking contacts
uv run -m newton.examples basic_shapes                             # primitive shapes — general picking regression
uv run --extra torch-cu12 -m newton.examples robot_anymal_c_walk   # quadruped — pick legs/body during walk

Bug fix

The viewer's picking spring-damper force enters a positive feedback loop on light objects (e.g. the 64g M20 nut). When the picked body bounces off a stiff contact, the damping term produces a disproportionate opposing force, driving the body back harder and creating a runaway cycle that diverges within ~20 frames.

Steps to reproduce:

  1. uv run -m newton.examples nut_bolt_hydro
  2. Pick the nut (not the bolt) with the mouse
  3. Drag it along the bolt threads
  4. Observe the nut explodes after a few frames

The fix clamps the picking force to 5g of the body's weight (4 lines, no API changes). The clamp only activates when the force is extreme — heavier objects are virtually unaffected.

Summary by CodeRabbit

  • Bug Fixes
    • Prevents extreme forces when picking light objects near stiff contacts by clamping pick force using an effective per-body mass (uses articulation total mass when linked).
  • New Features
    • Exposes a configurable pick max acceleration (default 5g) so users can tune the clamp.

@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Clamp viewer picking forces using a per-body effective mass (body mass or summed articulation mass). The picking kernel now rescales applied forces to at most pick_max_acceleration × g × effective_mass (default 5g) to prevent explosive responses when picking light or articulated bodies near stiff contacts.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added Unreleased → Fixed entry documenting that viewer picking force is clamped to a per-body "effective mass" limit (default 5g).
Picking kernel
newton/_src/viewer/kernels.py
Added pick_max_acceleration field to PickingState and extended apply_picking_force_kernel signature with pick_effective_mass. Force magnitude is clamped using max_force = pick_max_acceleration * 9.81 * pick_effective_mass[pick_body], and the clamped force is used for torque/accumulation.
Picking logic / mass computation
newton/_src/viewer/picking.py
Added pick_max_acceleration parameter to Picking.__init__, stored in pick state. Implemented _compute_effective_mass(model) to produce per-body effective masses (body mass or summed articulation mass), stored as self._pick_effective_mass, and passed to the kernel invocation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements the core fix for issue #2361 by clamping picking force, but concerns raised about regression on heavier objects (robot_g1 fingers) remain unresolved in the current implementation. Adjust the pick_max_acceleration clamp value (currently 5g) to a higher threshold (e.g., 10g) to restore picker functionality for heavier objects while maintaining stability for light objects.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main change: clamping viewer picking force to prevent explosions on light objects.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the force clamping mechanism for picking: CHANGELOG update, kernel modifications, and Picking class enhancements with effective mass computation.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% 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.

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

Clean, minimal fix — correct placement (before torque computation), safe from division-by-zero, and the 5g heuristic is well-chosen so heavier objects are unaffected.

Minor: the PR branch may need a rebase to resolve a CHANGELOG merge conflict with recent main entries.

@mzamoramora-nvidia mzamoramora-nvidia force-pushed the mzamoramora/fix-picking-force-clamp branch from 462fcc0 to ca85bd1 Compare April 9, 2026 07:32
adenzler-nvidia
adenzler-nvidia previously approved these changes Apr 9, 2026
@codecov

codecov Bot commented Apr 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 1 line in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
newton/_src/viewer/picking.py 96.15% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Kenny-Vilella

Copy link
Copy Markdown
Member

One issue that I see is that when picking for example the finger of a humanoid, we will never manage to pick it up because the force is so small.
This is a clear regression compare to the previous behavior.

@adenzler-nvidia adenzler-nvidia dismissed their stale review April 9, 2026 09:27

see Kenny's comment

@mzamoramora-nvidia

mzamoramora-nvidia commented Apr 9, 2026

Copy link
Copy Markdown
Member Author

@Kenny-Vilella which example did you try for the finger of the humanoid? We can also increase the clamp to be a bit higher (10x).

@Kenny-Vilella

Copy link
Copy Markdown
Member

For future reference, the most serious case was with the robot_g1 example.
But I am sure that some other examples has the same issue.

@mzamoramora-nvidia

Copy link
Copy Markdown
Member Author

@Kenny-Vilella thanks for testing!! If I remove that clamp (revert all changes), I am not able to move the fingers of the robot_g1 with the picker. Was that working at some point?

@adenzler-nvidia

Copy link
Copy Markdown
Member

I can confirm Kenny's findings, g1 does move nicely before but not with this fix.

@mzamoramora-nvidia mzamoramora-nvidia force-pushed the mzamoramora/fix-picking-force-clamp branch from 466bdc2 to 23af427 Compare April 9, 2026 11:44

@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/viewer/picking.py (1)

103-111: Tighten type annotations for nullable input and Warp array typing

At Line 110 the function accepts None, so the parameter type should reflect that. Also prefer typed Warp array annotations for consistency.

Proposed annotation update
-    def _compute_effective_mass(model: newton.Model) -> wp.array:
+    def _compute_effective_mass(model: newton.Model | None) -> wp.array[float]:

As per coding guidelines: "Use PEP 604 unions (x | None, not Optional[x]) in type annotations." and "Annotate Warp arrays with bracket syntax (wp.array[wp.vec3], wp.array2d[float], wp.array[Any]), not the parenthesized form."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton/_src/viewer/picking.py` around lines 103 - 111, Update the
_compute_effective_mass annotation to reflect that model can be None and use
Warp's bracketed array typing: change the parameter type to "newton.Model |
None" and the return type to "wp.array[float]". Locate the function
_compute_effective_mass in picking.py and replace the current signature
accordingly so callers and type checkers know the input is nullable and the
function returns a typed Warp array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@newton/_src/viewer/picking.py`:
- Around line 120-138: The code only marks articulation membership using
joint_child_np, so root/parent-only bodies that never appear as children are
left with body_art == -1 and their mass is omitted; modify the initialization
loop over joints to also mark parent bodies (e.g., use joint_parent_np[j] when
parent >= 0) with joint_art_np[j] so those bodies are included in art_mass, then
the subsequent art_mass aggregation and effective[b] assignment (which reference
body_art, body_mass_np, and art_mass) will correctly include root/parent-only
bodies.

---

Nitpick comments:
In `@newton/_src/viewer/picking.py`:
- Around line 103-111: Update the _compute_effective_mass annotation to reflect
that model can be None and use Warp's bracketed array typing: change the
parameter type to "newton.Model | None" and the return type to
"wp.array[float]". Locate the function _compute_effective_mass in picking.py and
replace the current signature accordingly so callers and type checkers know the
input is nullable and the function returns a typed Warp array.
🪄 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: 9650b92c-9e53-4b1e-bd36-42cf3cde57da

📥 Commits

Reviewing files that changed from the base of the PR and between ca85bd1 and 23af427.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • newton/_src/viewer/kernels.py
  • newton/_src/viewer/picking.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/_src/viewer/kernels.py

Comment thread newton/_src/viewer/picking.py
@mzamoramora-nvidia

mzamoramora-nvidia commented Apr 9, 2026

Copy link
Copy Markdown
Member Author

@Kenny-Vilella @adenzler-nvidia with the current changes, users should be able to lift a robot even if they pick from a finger.

adenzler-nvidia
adenzler-nvidia previously approved these changes Apr 9, 2026

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

Looks good — the force clamping logic is correct, the articulation mass approach is well-motivated, and the model is None case is handled.

Two minor suggestions, both non-blocking:

  1. The 5.0 factor in the kernel (5.0 * 9.81 * pick_effective_mass[pick_body]) is a tuning constant baked into the kernel. Consider exposing it as a constructor parameter (like pick_stiffness / pick_damping) so users can adjust it, or at minimum extracting it to a named constant.

  2. In _compute_effective_mass, the fallback art_start_np[a + 1] if a + 1 < len(art_start_np) else model.joint_count is unreachable — articulation_start always has a sentinel entry from finalize(). Could simplify to art_start_np[a + 1].

@adenzler-nvidia adenzler-nvidia added this pull request to the merge queue Apr 9, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Apr 9, 2026
…for linked bodies, own mass for free bodies)
Extract the hardcoded 5g force clamp into a configurable
pick_max_acceleration field on PickingState (default 5.0).

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

LGTM — the pick_max_acceleration parameter is a clean addition, docstring follows project conventions with SI units, and the kernel now reads from pick_state instead of a hardcoded constant.

@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/viewer/picking.py (1)

107-145: Implementation is correct — optional numpy optimization available.

The articulation mass aggregation logic correctly handles all cases:

  • Free bodies retain their own mass
  • Articulated bodies get the total mass of their articulation
  • The joint_child-only traversal is sufficient since Newton's model ensures every articulation body (including root) appears as a child of exactly one joint.

For large models, the Python loops could be replaced with NumPy vectorized operations for marginal performance gains, though this is initialization-time code that runs once.

♻️ Optional: NumPy-based aggregation
         if model.joint_count > 0:
             joint_child_np = model.joint_child.numpy()
             joint_art_np = model.joint_articulation.numpy()

             # Map each body to its articulation index (-1 if free)
             body_art = np.full(model.body_count, -1, dtype=np.int32)
-            for j in range(model.joint_count):
-                child = joint_child_np[j]
-                if child >= 0:
-                    body_art[child] = joint_art_np[j]
+            valid_children = joint_child_np >= 0
+            body_art[joint_child_np[valid_children]] = joint_art_np[valid_children]

-            # Sum mass per articulation
-            art_mass = {}
-            for b in range(model.body_count):
-                a = body_art[b]
-                if a >= 0:
-                    art_mass[a] = art_mass.get(a, 0.0) + body_mass_np[b]
-
-            # Assign total articulation mass to each body in that articulation
-            for b in range(model.body_count):
-                a = body_art[b]
-                if a >= 0:
-                    effective[b] = art_mass[a]
+            # Sum mass per articulation and assign back
+            in_art = body_art >= 0
+            if np.any(in_art):
+                art_ids = body_art[in_art]
+                art_mass_sum = np.bincount(art_ids, weights=body_mass_np[in_art])
+                effective[in_art] = art_mass_sum[art_ids]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton/_src/viewer/picking.py` around lines 107 - 145, The current
_compute_effective_mass implementation is correct but can be optimized by
replacing the Python loops that build body_art, compute art_mass, and assign
per-body effective masses with NumPy vectorized ops: create body_art from
joint_child_np/joint_art_np by initializing body_art = np.full(...) and using
body_art[joint_child_np >= 0] = joint_art_np[...] (or equivalent indexed
assignment), compute articulation masses with np.bincount on joint_art_np
filtered by child indices and weights from body_mass_np (ensuring the bincount
length covers max articulation index), then assign effective = np.where(body_art
>= 0, art_mass_lookup[body_art], body_mass_np); keep returning
wp.array(effective, dtype=float, device=model.device) and preserve behavior for
model is None and free bodies.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@newton/_src/viewer/picking.py`:
- Around line 107-145: The current _compute_effective_mass implementation is
correct but can be optimized by replacing the Python loops that build body_art,
compute art_mass, and assign per-body effective masses with NumPy vectorized
ops: create body_art from joint_child_np/joint_art_np by initializing body_art =
np.full(...) and using body_art[joint_child_np >= 0] = joint_art_np[...] (or
equivalent indexed assignment), compute articulation masses with np.bincount on
joint_art_np filtered by child indices and weights from body_mass_np (ensuring
the bincount length covers max articulation index), then assign effective =
np.where(body_art >= 0, art_mass_lookup[body_art], body_mass_np); keep returning
wp.array(effective, dtype=float, device=model.device) and preserve behavior for
model is None and free bodies.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 9c6d6ce6-5483-4cde-aef3-2b9e92e4ce00

📥 Commits

Reviewing files that changed from the base of the PR and between 23af427 and 9413232.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • newton/_src/viewer/kernels.py
  • newton/_src/viewer/picking.py
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton/_src/viewer/kernels.py

@adenzler-nvidia adenzler-nvidia added this pull request to the merge queue Apr 9, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Apr 9, 2026
@mzamoramora-nvidia mzamoramora-nvidia added this pull request to the merge queue Apr 9, 2026
Merged via the queue into newton-physics:main with commit 124268e Apr 9, 2026
25 checks passed
adenzler-nvidia added a commit to adenzler-nvidia/newton that referenced this pull request Apr 9, 2026
The CollisionPipeline fix was not cherry-picked but its changelog
entry came along via a merge conflict resolution in newton-physics#2385.
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] Contact in nut_bolt_hydro example may lead to explosion

3 participants