Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Proposal of modularity#10

Open
jvonmuralt wants to merge 13 commits into
newton-physics:mainfrom
jvonmuralt:proposal
Open

Proposal of modularity#10
jvonmuralt wants to merge 13 commits into
newton-physics:mainfrom
jvonmuralt:proposal

Conversation

@jvonmuralt

@jvonmuralt jvonmuralt commented Apr 10, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

Release Notes

  • Breaking Changes

    • Replaced specialized actuator classes with a unified composable Actuator architecture. Update imports and code to use Actuator(controller=..., delay=..., clamping=[...]) instead of ActuatorPD, ActuatorPID, etc.
  • New Features

    • Introduced composable actuator components: separate Controller, Delay, and Clamping modules enable flexible force computation pipelines.
  • Documentation

    • Updated README with new composer-based workflow and API reference reflecting controller/delay/clamping architecture.

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The pull request restructures the actuator system from specialized classes (ActuatorPD, ActuatorPID, etc.) to a composable architecture where an Actuator combines a Controller, optional Delay, and optional Clamping components. Documentation, tests, and USD parsing are updated accordingly.

Changes

Cohort / File(s) Summary
Removed Old Actuator Classes
newton_actuators/_src/actuators/base.py, newton_actuators/_src/actuators/pd.py, newton_actuators/_src/actuators/pid.py, newton_actuators/_src/actuators/dc_motor.py, newton_actuators/_src/actuators/delayed_pd.py, newton_actuators/_src/actuators/net_mlp.py, newton_actuators/_src/actuators/net_lstm.py, newton_actuators/_src/actuators/remotized_pd.py, newton_actuators/_src/actuators/__init__.py
Deleted all specialized actuator implementations and their package-level exports; these are replaced by the new composable Actuator architecture.
New Compositor Architecture
newton_actuators/_src/actuator.py, newton_actuators/__init__.py
Added Actuator composer class and StateActuator dataclass; updated main package exports to expose Actuator, controllers, Delay, and clamping components instead of specialized actuator classes.
New Controller System
newton_actuators/_src/controllers/base.py, newton_actuators/_src/controllers/controller_pd.py, newton_actuators/_src/controllers/controller_pid.py, newton_actuators/_src/controllers/controller_net_mlp.py, newton_actuators/_src/controllers/controller_net_lstm.py, newton_actuators/_src/controllers/__init__.py
Added Controller base class defining compute/state/indexing hooks and five controller implementations extracted from the old specialized actuators.
New Delay Component
newton_actuators/_src/delay.py
Added standalone Delay component with circular-buffer state management and readiness gating, extracted from ActuatorDelayedPD.
New Clamping System
newton_actuators/_src/clamping/base.py, newton_actuators/_src/clamping/clamping_max_force.py, newton_actuators/_src/clamping/clamping_position_based.py, newton_actuators/_src/clamping/clamping_dc_motor.py, newton_actuators/_src/clamping/__init__.py
Added Clamping base class and three post-controller force-clamping implementations for box saturation, position-based lookup, and DC-motor velocity-dependent saturation.
Updated Kernels & Parsing
newton_actuators/_src/kernels.py, newton_actuators/_src/usd_parser.py
Removed old unified kernel implementations; replaced USD parsing inference-based actuator detection with schema-registry-driven component extraction, returning controller and component specs instead of actuator-class/kwargs.
Documentation & Tests
README.md, newton_actuators/tests/test_actuators.py
Updated README to document new composer API and component architecture; refactored tests to construct Actuator(...) with separated controller/delay/clamping and validate composed state/parsing outputs.

Sequence Diagram(s)

sequenceDiagram
    participant Sim as Simulation
    participant Actuator as Actuator<br/>(Composer)
    participant Delay as Delay<br/>(Optional)
    participant Controller as Controller
    participant Clamp as Clamping<br/>(Optional)
    participant Output as Force Output

    Sim->>Actuator: step(sim_state, sim_control, states, dt)
    
    alt Delay exists and is_ready()
        Actuator->>Delay: get_delayed_targets(...)
        Delay->>Actuator: return delayed_pos, delayed_vel, delayed_act
        Actuator->>Controller: compute(delayed_pos, delayed_vel, ...)
    else Delay exists but NOT ready
        Actuator->>Delay: update_state(...)
        Note over Actuator: Skip controller/clamp/output
    else No Delay
        Actuator->>Controller: compute(current_pos, current_vel, ...)
    end
    
    Controller->>Controller: force = kp*(target_pos - pos) + kd*(target_vel - vel)
    Controller->>Actuator: return computed_forces
    
    alt Clamping configured
        Actuator->>Clamp: modify_forces(computed_forces, ...)
        Clamp->>Clamp: apply saturation/lookup-table/DC-motor limits
        Clamp->>Actuator: return applied_forces
    else No Clamping
        Actuator->>Actuator: applied_forces = computed_forces
    end
    
    Actuator->>Output: scatter-add(applied_forces, indices, joint_f)
    
    alt Dual output enabled
        Actuator->>Output: scatter-add(computed_forces, indices, joint_f_computed)
    end
    
    Actuator->>Controller: update_state(current_state, next_state)
    Actuator->>Delay: update_state(target_pos, target_vel, ...)
    Actuator->>Sim: force output updated
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

The changes introduce a comprehensive architectural refactor spanning multiple new modules with dense logic (Actuator composer, Controller base, five controller implementations, Delay with circular buffering, three Clamping variants), deletion of seven old actuator classes, significant USD parser rewrite using schema registry, and extensive test refactoring. While individual cohorts follow consistent patterns, the breadth of heterogeneous replacements and the density of new state-management logic demand careful cross-module verification.

Poem

🐰 Once rigid actuators stood in a row,
Now composable parts steal the show!
Controllers and delays and clamps intertwine,
A symphony of forces, perfectly designed. ✨
The rabbit hops forward, architecture refined!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Proposal of modularity' is vague and non-descriptive, using generic phrasing that does not convey the specific architectural changes being made. Replace with a more specific title that describes the main architectural change, such as 'Refactor actuator architecture to composable controller and clamping components' or 'Introduce modular Actuator composer with pluggable controllers and clamping stages'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
newton_actuators/__init__.py (1)

6-25: ⚠️ Potential issue | 🟡 Minor

Fix import sorting to resolve pipeline failure.

The CI reports ruff check failed (I001): Import block is un-sorted or un-formatted. Imports need to be sorted alphabetically.

Suggested fix
-from ._src.actuator import Actuator, ActuatorState
+from ._src.actuator import Actuator, ActuatorState
+from ._src.clamping import (
+    Clamp,
+    Clamping,
+    DCMotorSaturation,
+    RemotizedClamp,
+)
 from ._src.controllers import (
     Controller,
     NetLSTMController,
     NetMLPController,
     PDController,
     PIDController,
 )
 from ._src.delay import Delay
-from ._src.clamping import (
-    Clamp,
-    DCMotorSaturation,
-    Clamping,
-    RemotizedClamp,
-)
 from ._src.usd_parser import (
     ParsedActuator,
     parse_actuator_prim,
 )
 from ._version import __version__

Note: The exact order depends on your ruff/isort configuration. Run ruff check --fix locally to auto-format.

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

In `@newton_actuators/__init__.py` around lines 6 - 25, The import block in
__init__.py is not alphabetically sorted per ruff/isort; reorder the import
groups and the names within each from-import to be alphabetical (e.g., modules:
from ._src.actuator, from ._src.clamping, from ._src.controllers, from
._src.delay, from ._src.usd_parser, then from ._version) and sort the
identifiers inside each import (e.g., Controller, NetLSTMController,
NetMLPController, PDController, PIDController; Clamp, Clamping,
DCMotorSaturation, RemotizedClamp; ParsedActuator, parse_actuator_prim;
Actuator, ActuatorState; __version__), then run ruff check --fix (or isort) to
auto-apply formatting.
🧹 Nitpick comments (3)
newton_actuators/__init__.py (1)

38-43: Delay is miscategorized under the "Clamping" section.

Per the Clamping base class docstring, Delay is passed separately to the Actuator, not as a Clamping component. Consider moving it to a separate category.

Suggested organization
     # Clamping
     "Clamping",
     "Clamp",
     "DCMotorSaturation",
-    "Delay",
     "RemotizedClamp",
+    # Delay
+    "Delay",
     # USD
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton_actuators/__init__.py` around lines 38 - 43, The list in __init__.py
incorrectly groups the Delay class under the Clamping section; remove "Delay"
from the Clamping block and add it to a new or appropriate section for
Actuator-level components (or a separate "Timing/Delay" category) so Delay is
exported and documented separately from Clamping; update the surrounding comment
header and any import/export order to reflect that Delay is associated with
Actuator usage (referencing the Delay symbol and the Clamping base class
docstring to guide placement).
newton_actuators/_src/clamping/remotized_clamp.py (1)

36-49: Consider validating that lookup_torques values are non-negative.

The kernel assumes limit is positive for symmetric clamping [-limit, limit]. If lookup_torques contains negative values, wp.clamp(forces[i], -limit, limit) would have an inverted range, producing undefined behavior.

Proposed validation in resolve_arguments or __init__
     def __init__(
         self,
         lookup_angles: wp.array | tuple[float, ...] | list[float],
         lookup_torques: wp.array | tuple[float, ...] | list[float],
     ):
         ...
         if len(lookup_angles) != len(lookup_torques):
             raise ValueError(...)
+        if any(t < 0 for t in lookup_torques if not isinstance(lookup_torques, wp.array)):
+            raise ValueError("lookup_torques must contain non-negative values")
         self.lookup_size = len(lookup_angles)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton_actuators/_src/clamping/remotized_clamp.py` around lines 36 - 49, The
kernel _remotized_clamp_kernel assumes the interpolated limit from
lookup_torques is non-negative but negative entries would invert the clamp
range; add validation of the lookup_torques array (e.g., in the class __init__
or the resolve_arguments function that prepares kernel args) to ensure all
values are >= 0 and raise a clear exception (or log and clamp negatives to zero)
if any negative values are found, and mention _interp_1d and lookup_torques in
the error message so callers can locate and correct the data.
newton_actuators/_src/controllers/pid.py (1)

108-113: Avoid storing pending state on the controller instance.

_next_integral is shared mutable scratch on PIDController, so a second compute() on the same instance can overwrite the first pending update before update_state() copies it. That makes state advancement depend on call ordering rather than the current_state / next_state objects the API already exposes. Prefer writing directly into next_state.integral or moving the scratch buffer into the state object.

Also applies to: 138-166

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

In `@newton_actuators/_src/controllers/pid.py` around lines 108 - 113, The
PIDController currently stores shared mutable scratch _next_integral (set in
set_indices), which can be overwritten across concurrent compute() calls;
instead remove _next_integral and write directly into the provided
next_state.integral (or move the scratch into the state object) wherever the
controller prepares per-timestep updates (e.g., in set_indices and compute());
update code paths that reference _next_integral (including the logic between
lines 138-166) to accept/write into next_state.integral and ensure
update_state() consumes that per-instance next_state, eliminating the
controller-level pending buffer.
🤖 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_actuators/_src/actuator.py`:
- Around line 4-14: The import block in newton_actuators/_src/actuator.py is not
sorted/formatted and imports an unused symbol; remove the unused "field" import
from dataclasses and reorder imports into standard groups (stdlib, third-party,
local) and alphabetically (e.g., from __future__, dataclasses, typing, then
numpy/warp, then local modules .controllers.base, .delay, .clamping.base), then
run the project's formatter/isort to ensure CI passes; keep the existing
"dataclass" and "Any" imports if they are used elsewhere in this module.
- Around line 17-29: The _scatter_add_kernel uses a non-atomic read-modify-write
(output[out_idx] = output[out_idx] + forces[i]) which can lose updates when
multiple threads target the same out_idx; replace that line with the Warp atomic
add primitive (e.g., call wp.atomic_add(output, out_idx, forces[i])) inside
_scatter_add_kernel so each thread updates output[out_idx] atomically and
prevents race conditions.

In `@newton_actuators/_src/clamping/dc_motor_saturation.py`:
- Around line 1-2: Update the SPDX copyright header in
newton_actuators/_src/clamping/dc_motor_saturation.py to match the project's
year convention (change "2026" to "2025") so it is consistent with other files
in the PR; modify the top-of-file comment lines containing
"SPDX-FileCopyrightText" and "SPDX-License-Identifier" accordingly.

In `@newton_actuators/_src/controllers/pid.py`:
- Around line 19-27: Warp kernels (_pid_force_kernel, _pd_force_kernel, and
_delay_buffer_state_kernel) declare concrete wp.array parameters (control_input,
constant_force) but compute() may pass None; fix by ensuring these parameters
are never None at kernel call sites: in the class constructor/compute()
materialize zero-filled wp.arrays for control_input and constant_force when None
is supplied, remove the optional | None type hints from the constructor and
kernel signatures (e.g., update compute(), __init__, and the kernel parameter
types for _pid_force_kernel), and update callers to always pass the zero arrays;
apply the same change for the corresponding names in pd.py (_pd_force_kernel and
its compute/constructor) and delay.py (_delay_buffer_state_kernel).

In `@newton_actuators/_src/usd_parser.py`:
- Around line 9-11: The import block in usd_parser.py is not sorted; reorder and
format the imports so they satisfy the linter (group standard library,
third‑party, then local imports) and alphabetize within groups — specifically
ensure the local imports for NetLSTMController, NetMLPController, PDController,
PIDController, Delay, Clamp, and DCMotorSaturation are sorted and formatted (or
run isort/black to auto-fix) so the import block passes CI.

---

Outside diff comments:
In `@newton_actuators/__init__.py`:
- Around line 6-25: The import block in __init__.py is not alphabetically sorted
per ruff/isort; reorder the import groups and the names within each from-import
to be alphabetical (e.g., modules: from ._src.actuator, from ._src.clamping,
from ._src.controllers, from ._src.delay, from ._src.usd_parser, then from
._version) and sort the identifiers inside each import (e.g., Controller,
NetLSTMController, NetMLPController, PDController, PIDController; Clamp,
Clamping, DCMotorSaturation, RemotizedClamp; ParsedActuator,
parse_actuator_prim; Actuator, ActuatorState; __version__), then run ruff check
--fix (or isort) to auto-apply formatting.

---

Nitpick comments:
In `@newton_actuators/__init__.py`:
- Around line 38-43: The list in __init__.py incorrectly groups the Delay class
under the Clamping section; remove "Delay" from the Clamping block and add it to
a new or appropriate section for Actuator-level components (or a separate
"Timing/Delay" category) so Delay is exported and documented separately from
Clamping; update the surrounding comment header and any import/export order to
reflect that Delay is associated with Actuator usage (referencing the Delay
symbol and the Clamping base class docstring to guide placement).

In `@newton_actuators/_src/clamping/remotized_clamp.py`:
- Around line 36-49: The kernel _remotized_clamp_kernel assumes the interpolated
limit from lookup_torques is non-negative but negative entries would invert the
clamp range; add validation of the lookup_torques array (e.g., in the class
__init__ or the resolve_arguments function that prepares kernel args) to ensure
all values are >= 0 and raise a clear exception (or log and clamp negatives to
zero) if any negative values are found, and mention _interp_1d and
lookup_torques in the error message so callers can locate and correct the data.

In `@newton_actuators/_src/controllers/pid.py`:
- Around line 108-113: The PIDController currently stores shared mutable scratch
_next_integral (set in set_indices), which can be overwritten across concurrent
compute() calls; instead remove _next_integral and write directly into the
provided next_state.integral (or move the scratch into the state object)
wherever the controller prepares per-timestep updates (e.g., in set_indices and
compute()); update code paths that reference _next_integral (including the logic
between lines 138-166) to accept/write into next_state.integral and ensure
update_state() consumes that per-instance next_state, eliminating the
controller-level pending buffer.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f6b8ea3-f0c9-4692-ac7f-19df01ff5871

📥 Commits

Reviewing files that changed from the base of the PR and between b2bcb65 and ce79c8c.

📒 Files selected for processing (27)
  • README.md
  • newton_actuators/__init__.py
  • newton_actuators/_src/actuator.py
  • newton_actuators/_src/actuators/__init__.py
  • newton_actuators/_src/actuators/base.py
  • newton_actuators/_src/actuators/dc_motor.py
  • newton_actuators/_src/actuators/delayed_pd.py
  • newton_actuators/_src/actuators/net_lstm.py
  • newton_actuators/_src/actuators/net_mlp.py
  • newton_actuators/_src/actuators/pd.py
  • newton_actuators/_src/actuators/pid.py
  • newton_actuators/_src/actuators/remotized_pd.py
  • newton_actuators/_src/clamping/__init__.py
  • newton_actuators/_src/clamping/base.py
  • newton_actuators/_src/clamping/clamp.py
  • newton_actuators/_src/clamping/dc_motor_saturation.py
  • newton_actuators/_src/clamping/remotized_clamp.py
  • newton_actuators/_src/controllers/__init__.py
  • newton_actuators/_src/controllers/base.py
  • newton_actuators/_src/controllers/net_lstm.py
  • newton_actuators/_src/controllers/net_mlp.py
  • newton_actuators/_src/controllers/pd.py
  • newton_actuators/_src/controllers/pid.py
  • newton_actuators/_src/delay.py
  • newton_actuators/_src/kernels.py
  • newton_actuators/_src/usd_parser.py
  • newton_actuators/tests/test_actuators.py
💤 Files with no reviewable changes (10)
  • newton_actuators/_src/actuators/init.py
  • newton_actuators/_src/actuators/dc_motor.py
  • newton_actuators/_src/kernels.py
  • newton_actuators/_src/actuators/base.py
  • newton_actuators/_src/actuators/pd.py
  • newton_actuators/_src/actuators/net_lstm.py
  • newton_actuators/_src/actuators/net_mlp.py
  • newton_actuators/_src/actuators/delayed_pd.py
  • newton_actuators/_src/actuators/remotized_pd.py
  • newton_actuators/_src/actuators/pid.py

Comment thread newton_actuators/_src/actuator.py Outdated
Comment thread newton_actuators/_src/actuator.py Outdated
Comment on lines +17 to +29
# TODO: replace with a Transmission class that applies gear ratios / linkage
# transforms before accumulating into the output array.
@wp.kernel
def _scatter_add_kernel(
forces: wp.array(dtype=float),
output_indices: wp.array(dtype=wp.uint32),
output: wp.array(dtype=float),
):
"""Accumulate forces into output at specified indices."""
i = wp.tid()
out_idx = output_indices[i]
output[out_idx] = output[out_idx] + forces[i]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if output_indices are expected to be unique or can have duplicates
rg -n "output_indices" --type py -C 3
# Check if there are any tests with duplicate output indices
rg -n "output_indices.*=.*\[" --type py

Repository: newton-physics/newton-actuators

Length of output: 22319


🏁 Script executed:

# Check for any documentation about multi-output or shared outputs
rg -i "multi.*output|shared.*output|duplicate.*index" --type py --type md

# Look for comments or issues related to the kernel or race conditions
rg -i "race|atomic|concurrent|thread.*safety" newton_actuators/ --type py

# Check if there's any usage where multiple Actuators write to the same output array
rg -n "output_indices.*reshape|output_indices.*flatten" --type py

Repository: newton-physics/newton-actuators

Length of output: 266


🏁 Script executed:

# Look at the multi-output test to understand the intended behavior
rg -n "test_multi_step_different_outputs" --type py -A 30

# Check the Actuator class initialization to understand multi-output shape
sed -n '70,130p' newton_actuators/_src/actuator.py

# Search for any reshaping or flattening of output_indices
rg -n "reshape|flatten" newton_actuators/_src/actuator.py

# Check if output arrays are pre-initialized with zeros
rg -n "output.*=.*0|wp.zeros" newton_actuators/_src/ --type py -B 2 -A 2

Repository: newton-physics/newton-actuators

Length of output: 7413


🏁 Script executed:

# Check how output array is used - does it get reset between calls?
rg -n "control_output_attr|joint_f" newton_actuators/_src/actuator.py -B 2 -A 2

# Look at the step() function to see the full kernel launch context
sed -n '200,270p' newton_actuators/_src/actuator.py

# Check if there's any mention of concurrent Actuators or shared outputs
rg -n "concurrent|parallel|multi.*actuator" newton_actuators/ --type py --type md

Repository: newton-physics/newton-actuators

Length of output: 4032


🏁 Script executed:

# Check if step() is designed to be called for multiple actuators on the same output
rg -n "step.*dim|for.*actuator.*step" newton_actuators/ --type py -B 2 -A 2

# Look at example usage or documentation showing multiple actuators
fd -e md -e rst . | head -5 | xargs cat 2>/dev/null | head -100

# Check if there's a test that creates multiple Actuators and calls step() sequentially
rg -n "actuator.*step|step.*output" newton_actuators/tests/ --type py -B 1 -A 1 | head -50

Repository: newton-physics/newton-actuators

Length of output: 10198


Use atomic add to prevent race conditions with duplicate output indices.

The kernel performs output[out_idx] = output[out_idx] + forces[i] using a non-atomic read-modify-write. If multiple threads write to the same output_idx (whether from duplicate indices within a single Actuator or multiple Actuators orchestrated to the same DOF), this creates a data race with lost updates. Replace with atomic addition:

Proposed fix
 `@wp.kernel`
 def _scatter_add_kernel(
     forces: wp.array(dtype=float),
     output_indices: wp.array(dtype=wp.uint32),
     output: wp.array(dtype=float),
 ):
     """Accumulate forces into output at specified indices."""
     i = wp.tid()
     out_idx = output_indices[i]
-    output[out_idx] = output[out_idx] + forces[i]
+    wp.atomic_add(output, out_idx, forces[i])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# TODO: replace with a Transmission class that applies gear ratios / linkage
# transforms before accumulating into the output array.
@wp.kernel
def _scatter_add_kernel(
forces: wp.array(dtype=float),
output_indices: wp.array(dtype=wp.uint32),
output: wp.array(dtype=float),
):
"""Accumulate forces into output at specified indices."""
i = wp.tid()
out_idx = output_indices[i]
output[out_idx] = output[out_idx] + forces[i]
# TODO: replace with a Transmission class that applies gear ratios / linkage
# transforms before accumulating into the output array.
`@wp.kernel`
def _scatter_add_kernel(
forces: wp.array(dtype=float),
output_indices: wp.array(dtype=wp.uint32),
output: wp.array(dtype=float),
):
"""Accumulate forces into output at specified indices."""
i = wp.tid()
out_idx = output_indices[i]
wp.atomic_add(output, out_idx, forces[i])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton_actuators/_src/actuator.py` around lines 17 - 29, The
_scatter_add_kernel uses a non-atomic read-modify-write (output[out_idx] =
output[out_idx] + forces[i]) which can lose updates when multiple threads target
the same out_idx; replace that line with the Warp atomic add primitive (e.g.,
call wp.atomic_add(output, out_idx, forces[i])) inside _scatter_add_kernel so
each thread updates output[out_idx] atomically and prevents race conditions.

Comment on lines +1 to +2
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
# SPDX-License-Identifier: Apache-2.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Copyright year inconsistency.

This file uses 2026 while other files in this PR use 2025. Consider aligning for consistency.

Suggested fix
-# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
+# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
# SPDX-License-Identifier: Apache-2.0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton_actuators/_src/clamping/dc_motor_saturation.py` around lines 1 - 2,
Update the SPDX copyright header in
newton_actuators/_src/clamping/dc_motor_saturation.py to match the project's
year convention (change "2026" to "2025") so it is consistent with other files
in the PR; modify the top-of-file comment lines containing
"SPDX-FileCopyrightText" and "SPDX-License-Identifier" accordingly.

Comment on lines +19 to +27
control_input: wp.array(dtype=float),
state_indices: wp.array(dtype=wp.uint32),
target_indices: wp.array(dtype=wp.uint32),
force_indices: wp.array(dtype=wp.uint32),
kp: wp.array(dtype=float),
ki: wp.array(dtype=float),
kd: wp.array(dtype=float),
integral_max: wp.array(dtype=float),
constant_force: wp.array(dtype=float),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "pid.py" | head -20

Repository: newton-physics/newton-actuators

Length of output: 117


🏁 Script executed:

cat -n ./newton_actuators/_src/controllers/pid.py

Repository: newton-physics/newton-actuators

Length of output: 6226


🏁 Script executed:

# Check if there are other Warp kernels in the codebase that handle optional/None parameters
rg -t py "@wp\.kernel" -A 30 | head -100

Repository: newton-physics/newton-actuators

Length of output: 6986


🌐 Web query:

Warp kernel optional parameters None typing.Any

💡 Result:

In NVIDIA Warp (@wp.kernel), kernel arguments must be statically typed, because kernels are compiled to native CPU/CUDA code. That means you can’t have “optional” kernel parameters in the Python sense (e.g., an argument whose type might be None at runtime). If you need “optional behavior”, you typically pass an extra flag/value (e.g., use_x: int) and branch inside the kernel, or write/overload separate kernels with different signatures. [1]

typing.Any in a kernel signature does not mean “any runtime value (including None)”; it’s a generic type placeholder that Warp will instantiate into concrete, compiled kernel variants based on the types you pass at launch time. [2]

Sources:
[1] Warp Basics (static typing / kernel params must be annotated) (nvidia.github.io)
[2] Warp Generics (typing.Any generic kernels) (nvidia.github.io)

Citations:


🏁 Script executed:

# Check if there are tests that verify the None case for control_input or constant_force
find . -type f -name "*.py" | xargs rg -l "PIDController|pid" | grep -i test

Repository: newton-physics/newton-actuators

Length of output: 117


🏁 Script executed:

cat -n ./newton_actuators/tests/test_actuators.py | head -200

Repository: newton-physics/newton-actuators

Length of output: 8161


🏁 Script executed:

cat -n ./newton_actuators/tests/test_actuators.py | sed -n '200,400p'

Repository: newton-physics/newton-actuators

Length of output: 9091


🏁 Script executed:

# Search for tests that use None for constant_force or control_input
rg -n "constant_force|control_input" ./newton_actuators/tests/test_actuators.py | head -20

Repository: newton-physics/newton-actuators

Length of output: 136


🏁 Script executed:

# Search for where PIDController is instantiated to see if None is ever passed
rg -B 3 -A 3 "PIDController(" ./newton_actuators/

Repository: newton-physics/newton-actuators

Length of output: 153


🏁 Script executed:

# Search for where PIDController is instantiated to see if None is ever passed
rg "PIDController\(" ./newton_actuators/

Repository: newton-physics/newton-actuators

Length of output: 393


🏁 Script executed:

# Check all PIDController instantiations in tests
rg -B 5 -A 10 "controller=PIDController" ./newton_actuators/tests/test_actuators.py

Repository: newton-physics/newton-actuators

Length of output: 1992


🏁 Script executed:

# Check if there's documentation or examples in the codebase about handling optional arrays
find . -type f -name "*.py" -o -name "*.md" | xargs rg -l "wp.launch" | head -5

Repository: newton-physics/newton-actuators

Length of output: 286


🏁 Script executed:

# Check how pd.py handles compute to see if it has similar pattern
cat -n ./newton_actuators/_src/controllers/pd.py | head -150

Repository: newton-physics/newton-actuators

Length of output: 4020


🏁 Script executed:

# Check the base Controller interface to understand the contract
find . -type f -name "base.py" -path "*/controllers/*"

Repository: newton-physics/newton-actuators

Length of output: 118


🏁 Script executed:

cat -n ./newton_actuators/_src/controllers/base.py

Repository: newton-physics/newton-actuators

Length of output: 4919


🏁 Script executed:

# Let's check if there are any issues reported or if this pattern works in practice
# by looking for any warp.launch calls that pass None
rg -B 5 -A 5 "wp.launch" ./newton_actuators/_src/delay.py | head -40

Repository: newton-physics/newton-actuators

Length of output: 375


Fix Warp kernel parameter types to handle optional arrays.

_pid_force_kernel declares control_input and constant_force as concrete wp.array(dtype=float) parameters (lines 19, 27) but compute() passes both as potentially None (lines 146, 154). Warp's kernels require static typing for compilation; they cannot accept None in concrete array slots. The runtime branching on these parameters (if constant_force: at line 46, if control_input: at line 50) will fail or behave unpredictably when None is passed.

Either:

  1. Always materialize zero-filled arrays and remove the | None type hints from the constructor and compute signature, or
  2. Use typing.Any for these parameters and split into explicit kernel variants for the None/not-None cases.

Same issue in _pd_force_kernel (pd.py lines 23, 36–41) and _delay_buffer_state_kernel (delay.py).

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

In `@newton_actuators/_src/controllers/pid.py` around lines 19 - 27, Warp kernels
(_pid_force_kernel, _pd_force_kernel, and _delay_buffer_state_kernel) declare
concrete wp.array parameters (control_input, constant_force) but compute() may
pass None; fix by ensuring these parameters are never None at kernel call sites:
in the class constructor/compute() materialize zero-filled wp.arrays for
control_input and constant_force when None is supplied, remove the optional |
None type hints from the constructor and kernel signatures (e.g., update
compute(), __init__, and the kernel parameter types for _pid_force_kernel), and
update callers to always pass the zero arrays; apply the same change for the
corresponding names in pd.py (_pd_force_kernel and its compute/constructor) and
delay.py (_delay_buffer_state_kernel).

Comment thread newton_actuators/_src/usd_parser.py Outdated
Comment thread newton_actuators/_src/actuator.py Outdated
"""Return True if all components can be captured in a CUDA graph."""
return self.controller.is_graphable() and all(c.is_graphable() for c in self.clamping)

def has_transmission(self) -> bool:

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.

This we will remove still right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I was thinking to add an interface already now, but just returning False for now. We can also fully remove it.

Comment thread newton_actuators/_src/usd_parser.py Outdated
controller_kwargs: dict[str, Any] = field(default_factory=dict)
component_specs: list[tuple[type, dict[str, Any]]] = field(default_factory=list)
target_paths: list[str] = field(default_factory=list)
transmission: list[float] | None = None

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.

This we'll remove still right?

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.

I agree, it may be best to remove transmission until it is actually supported

Comment thread newton_actuators/_src/usd_parser.py Outdated
schemas = infer_schemas_from_prim(prim)

schemas = get_schemas_from_prim(prim)
transmission = get_attribute(prim, "newton:actuator:transmission")

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.

This we would remove still right?

Comment thread newton_actuators/_src/usd_parser.py Outdated
component_specs=component_specs,
target_paths=target_paths,
kwargs=extract_kwargs_from_prim(prim, schemas),
transmission=list(transmission) if transmission else None,

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.

This we would remove still right?

Comment thread README.md Outdated
GPU-accelerated actuator library for physics simulations.

This library provides a collection of actuator implementations that integrate with physics simulation pipelines. Actuators read from simulation state arrays and write computed forces/torques back to control arrays.
This library provides composable actuator implementations that integrate with

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.

How about this instead which adds a bit more info of what we want to achieve?

This library provides composable actuator implementations that read physics simulation state, compute actuator forces, and write the forces back to control arrays for application to the simulation. The simulator does not need to be part of Newton: the library is designed to be reusable anywhere the caller can provide state arrays and consume forces. Each actuator instance is vectorized: a single actuator object operates on a batch of indices in global state and control arrays, allowing efficient integration into RL workflows. The goal is to provide canonical actuator models with support for differentiability and graphable execution where the underlying controller implementation supports it. The library is designed to be easy for users to customize and extend for their specific actuator models.

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.

Whats also missing here or in the arch is the fact that the actuators are vectorized no?

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.

Added a sentence now here about the vectorization.

Comment thread README.md Outdated
composes a controller with an optional delay and zero or more clamping objects.
The `step()` method runs:

1. **Delay** — read delayed targets from buffer (skipped if no delay or buffer still filling)

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.

The force is zero if still filling?

Comment thread README.md Outdated

1. **Delay** — read delayed targets from buffer (skipped if no delay or buffer still filling)
2. **Controller** — compute raw forces
3. **Clamping** — bound forces (e.g. `Clamp`, `DCMotorSaturation`)

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.

I would just repeat "Clamp raw forces (e.g....)"

Comment thread README.md Outdated
1. **Delay** — read delayed targets from buffer (skipped if no delay or buffer still filling)
2. **Controller** — compute raw forces
3. **Clamping** — bound forces (e.g. `Clamp`, `DCMotorSaturation`)
4. **Scatter-add** — accumulate forces into the output array

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.

Scatter we don't need to do w/o transmission right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we need, to write into the right places in the force array
though I dont think we need output and input indices separately, considering that no transmission, so I would just make indices

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.

Ah got it going from actuator space to dof space, thanks.

Simplifying indexing until we need a more advanced transmission setup makes sense.

Comment thread README.md
- `actuator.is_graphable()` — True if `step()` can be captured in a CUDA graph
- `actuator.state()` — return a new `ActuatorState` (None if stateless)
- `actuator.step(sim_state, sim_control, current_state, next_state, dt)` — one control step
- `state.reset()` — zero all internal buffers without reallocating

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.

Indexing TBD and this should be api links when we move to newton repo.

Comment thread README.md
1. **Create an actuator** by composing a controller with clamping
2. **Check statefulness**: call `actuator.is_stateful()`
3. **Initialize states**: for stateful actuators, create double-buffered states with `actuator.state()`
4. **Simulation loop**: call `actuator.step()` each timestep

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.

Could comment on substeps here too? But maybe TMI?

"Clamp",
"DCMotorSaturation",
"Clamping",
"RemotizedClamp",

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.

There was some discussion in the doc - but is "RemotizedClamp" a well-known term? Would "PositionBasedClamping" be more clear?

Comment on lines +84 to +91
act_input: Feedforward control input (may be None).
input_indices: Indices into positions/velocities.
target_indices: Indices into target arrays.
forces: Scratch buffer to write forces to. Shape (N,).
force_indices: Indices into forces buffer (typically sequential).
num_actuators: Number of actuators N.
state: Controller state (None if stateless).
dt: Timestep in seconds.

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.

Couple of comments/questions:

  1. Would feedforward_control be a better name for act_input? I wasn't sure what act_input was before reading the docstring.
  2. Similar to what is usually done in C, did we want to put all outputs at the end of the function signature, and give them an ending like forces_out and state_out so that there is a clear separation of what is input and what is output?

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.

Also, in the base class, should we have one additional "other_inputs" kind of argument, to cover when there are actuators which take inputs we haven't thought of? I am thinking of controllers which take gains as real-time inputs rather than assuming they are fixed parameters.

Comment on lines +99 to +101
def is_graphable(self) -> bool:
"""Return True if compute() can be captured in a CUDA graph."""
return True

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.

Should this be NotImplementedError for the base class?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think keeping True makes more sense because in general we are focusing on the graphable implementations. The ones which are not are an exception, they are using pytorch and exist only for the reason to be compatible with the IsaacLab policies. I think in the future the plan is to replace with warpnn based and then this is_graphable would not be relevant at all.

Comment on lines +19 to +20
The network must be callable as:
torques, (h_new, c_new) = network(input, (h, c))

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.

Do h and c have more meaningful names we could add?

Comment thread newton_actuators/_src/actuator.py Outdated
Comment on lines +17 to +18
# TODO: replace with a Transmission class that applies gear ratios / linkage
# transforms before accumulating into the output array.

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.

Question about this:

Our current design assumes that the gear-ratio is "baked in" to the controller. In that case, the transmission should not include gear-ratios, i.e. not what this comment would indicate? The transmission will only be used to model things like cabling, or massless linkages.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

in general yes, transmission is intended for things like that.
That being said, our design does not exclude for the user a possibility to add a gear box as a transmission mechanism and provide all parameters/state and targets in the motor space.

Comment thread newton_actuators/_src/usd_parser.py Outdated
controller_kwargs: dict[str, Any] = field(default_factory=dict)
component_specs: list[tuple[type, dict[str, Any]]] = field(default_factory=list)
target_paths: list[str] = field(default_factory=list)
transmission: list[float] | None = None

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.

I agree, it may be best to remove transmission until it is actually supported

Comment thread newton_actuators/__init__.py Outdated
"Clamping",
"Clamp",
"DCMotorSaturation",
"Delay",

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.

I guess this shouldn't go under the "clamping" header :)

Comment thread README.md Outdated
| Controller | Description | Stateful |
|---|---|---|
| `PDController` | Proportional-derivative controller | No |
| `PIDController` | PID controller with anti-windup integral | Yes |

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.

I think it is best to say something like "with integral clamping" to be explicit. There are other forms of anti-windup, such as "exponential forgetting" which are not clamping based.

Comment on lines +73 to +74
class ParsedActuator:
"""Result of parsing a USD actuator prim.

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.

I might be missing it - but is there a supported way to convert ParsedActuator into an Actuator?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

it is responsibility of the user code to correctly aggregate information and construct indices arrays and instantiate the actual Actuators. Please check how it is done in newton when the prev version of this library was integrated

@eric-heiden

Copy link
Copy Markdown
Member

I think we should keep the Newton naming principle here and prefer prefix-first names.

In Newton we generally want the category/type first and the specialization second, e.g.

  • SolverVBD instead of VBDSolver
  • SolverMuJoCo instead of MuJoCoSolver

Applied to the names introduced in this PR, that would suggest:

Original name Prefix-first version
ActuatorState StateActuator
PDController ControllerPD
PIDController ControllerPID
NetMLPController ControllerNetMLP
NetLSTMController ControllerNetLSTM
RemotizedClamp ClampRemotized
DCMotorSaturation SaturationDCMotor

I’m not attached to every individual replacement above, but I do think we should be consistent about the direction: prefix-first rather than suffix-first.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

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

⚠️ Outside diff range comments (1)
newton_actuators/tests/test_actuators.py (1)

1-1396: ⚠️ Potential issue | 🟡 Minor

Run ruff format on this file before merge.

CI is already red with “file would be reformatted”, so this test module needs a formatter pass as-is.

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

In `@newton_actuators/tests/test_actuators.py` around lines 1 - 1396, The file
fails ruff formatting; run the formatter (e.g., ruff format) over this test
module (the one containing classes like TestPDWithClampingMaxForce,
TestPDWithDelay, TestPIDWithClampingMaxForce, TestPDWithDCMotor,
TestPDWithClampingPositionBased, TestComposition, and TestControllerNetMLP/LSTM)
and commit the reformatted result so CI no longer reports "file would be
reformatted".
♻️ Duplicate comments (4)
newton_actuators/_src/controllers/controller_pid.py (1)

19-27: ⚠️ Potential issue | 🟠 Major

Don't launch _pid_force_kernel with None array inputs.

compute() can still pass act_input=None and self.constant_force=None, but the kernel signature requires concrete wp.array(...) arguments. The runtime if control_input / if constant_force checks do not fix that mismatch, so this path is still unsafe until you materialize zero arrays or split the kernel variants.

Also applies to: 45-53, 85-157

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

In `@newton_actuators/_src/controllers/controller_pid.py` around lines 19 - 27,
The kernel _pid_force_kernel must never be launched with Python None for array
parameters (e.g., control_input, constant_force); update compute() to ensure all
kernel arguments are concrete wp.array instances by either (a) creating
zero/empty wp.array placeholders for any None inputs (matching expected dtype
and length) before calling _pid_force_kernel, or (b) split into two kernel
variants and call the correct one when control_input or constant_force is None;
adjust the call sites in compute() and any places that pass
state_indices/target_indices/force_indices so they always supply valid wp.array
objects rather than None.
newton_actuators/_src/controllers/controller_pd.py (1)

17-24: ⚠️ Potential issue | 🟠 Major

Don't pass optional arrays into _pd_force_kernel.

control_input and constant_force are declared as concrete wp.array(...) kernel params, but compute() can still launch with act_input=None and self.constant_force=None. That keeps the old Warp typing issue alive here, and the in-kernel truthiness checks don't make the launch safe. Please materialize zero-filled arrays before launch, or split the kernel variants.

Also applies to: 35-41, 79-111

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

In `@newton_actuators/_src/controllers/controller_pd.py` around lines 17 - 24, The
kernel _pd_force_kernel currently declares control_input and constant_force as
concrete wp.array kernel params but compute() may call the kernel with
act_input=None or self.constant_force=None which is unsafe; before launching
_pd_force_kernel (and the other kernel variants referenced around lines 35-41
and 79-111), ensure you materialize zero-filled wp.array instances when
act_input or constant_force is None (e.g., create arrays of the correct
dtype/shape filled with zeros matching state_indices/target_indices length) or
alternatively provide a separate kernel overload that omits those parameters;
update the caller in compute() to replace None with those zero arrays so the
kernel never receives None for control_input/constant_force.
newton_actuators/__init__.py (1)

27-48: 🛠️ Refactor suggestion | 🟠 Major

Settle the public naming scheme before exporting it.

__all__ now bakes suffix-first names into the package API. Since the review thread already points toward prefix-first naming, merging these exports as-is turns that rename into a breaking change later.

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

In `@newton_actuators/__init__.py` around lines 27 - 48, The package exports in
__all__ use suffix-first names (ControllerPD, ControllerPID, ControllerNetMLP,
ControllerNetLSTM, ClampingMaxForce, ClampingPositionBased,
ClampingVelocityBased) which conflicts with the agreed prefix-first naming;
update __all__ to export the prefix-first variants (e.g. PDController,
PIDController, NetMLPController, NetLSTMController, MaxForceClamping or
PositionBasedClamping, VelocityBasedClamping) or remove the suffix-first entries
until the rename is completed across the codebase; ensure the exported names in
__all__ match the actual class/function identifiers (Actuator, StateActuator,
Controller, Delay, Clamping, ParsedActuator, parse_actuator_prim) so importing
modules won’t break.
newton_actuators/_src/actuator.py (1)

17-42: ⚠️ Potential issue | 🔴 Critical

Use atomic adds in both scatter kernels.

Both kernels perform non-atomic read/modify/write on shared output arrays. If two entries target the same DOF, one update can be lost. This was already raised earlier and still looks unresolved.

Suggested fix
 def _scatter_add_kernel(
     forces: wp.array(dtype=float),
     indices: wp.array(dtype=wp.uint32),
     output: wp.array(dtype=float),
 ):
     """Scatter-add forces into output at specified indices."""
     i = wp.tid()
     idx = indices[i]
-    output[idx] = output[idx] + forces[i]
+    wp.atomic_add(output, idx, forces[i])
@@
 def _scatter_add_dual_kernel(
     applied_forces: wp.array(dtype=float),
     computed_forces: wp.array(dtype=float),
     indices: wp.array(dtype=wp.uint32),
     applied_output: wp.array(dtype=float),
     computed_output: wp.array(dtype=float),
 ):
     """Scatter-add both applied and computed forces in one pass."""
     i = wp.tid()
     idx = indices[i]
-    applied_output[idx] = applied_output[idx] + applied_forces[i]
-    computed_output[idx] = computed_output[idx] + computed_forces[i]
+    wp.atomic_add(applied_output, idx, applied_forces[i])
+    wp.atomic_add(computed_output, idx, computed_forces[i])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton_actuators/_src/actuator.py` around lines 17 - 42, The scatter kernels
_scatter_add_kernel and _scatter_add_dual_kernel perform non-atomic
read/modify/write on shared output arrays and must use atomic additions to avoid
lost updates when multiple threads target the same index; replace the direct
updates (e.g., output[idx] = output[idx] + forces[i] and applied_output[idx] =
applied_output[idx] + applied_forces[i], computed_output[idx] =
computed_output[idx] + computed_forces[i]) with the Warp atomic add primitive
(e.g., wp.atomic.add) so each thread performs an atomic addition into output,
applied_output, and computed_output respectively.
🤖 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_actuators/_src/actuator.py`:
- Around line 180-187: The step method currently has an optional dt parameter
which is actually required by downstream callers (compute() and
delay.update_state()), so either remove the default to make dt a required
positional/keyword argument on Actuator.step or validate it at the top of step
and raise a clear exception; update the signature on def step(..., dt: float) ->
None to drop the "= None" or add an early check like "if dt is None: raise
ValueError('dt is required')" before any calls to compute() or
delay.update_state() so no None is propagated into time-based math (refer to
step, compute, and delay.update_state in your changes).
- Around line 206-249: If the actuator is stateful (self.is_stateful() true),
ensure step() checks that both current_act_state and next_act_state are present
and raise a clear API error immediately (e.g., ValueError/RuntimeError) instead
of proceeding; add the guard at the start of the section that currently sets
has_states and before calling delay.is_ready() or controller.compute(), so
delay.is_ready(delay_state) and self.controller.compute(...) never run when
current_act_state or next_act_state is missing.

In `@newton_actuators/_src/clamping/base.py`:
- Around line 50-74: The base class modify_forces method currently is a silent
no-op; change it to fail fast by raising a clear error (e.g.,
NotImplementedError) inside modify_forces so subclasses must implement it:
locate the modify_forces method in newton_actuators._src.clamping.base (the base
clamping class) and replace the empty implementation with a raised exception
that includes a descriptive message referencing modify_forces and the clamping
class to aid debugging.

In `@newton_actuators/_src/clamping/clamping_max_force.py`:
- Around line 29-39: Ensure negative per-actuator limits are rejected by
validating max_force in both resolve_arguments and __init__: in
resolve_arguments(cls, args) check args.get("max_force", math.inf) for any
negative entries and raise a ValueError if so (include the offending value in
the message); in __init__(self, max_force: wp.array) validate the provided
wp.array has all elements >= 0 (e.g., use a vectorized test or iterate to detect
any element < 0) and raise ValueError if any are negative; keep the attribute
name max_force and preserve the existing behavior when values are non-negative.

In `@newton_actuators/_src/clamping/clamping_position_based.py`:
- Around line 1-133: This file fails ruff formatting; run the project's
auto-formatter (ruff format) on
newton_actuators/_src/clamping/clamping_position_based.py to fix whitespace,
line breaks, and import formatting issues; after reformatting, ensure the module
still compiles and that symbols like _interp_1d, _remotized_clamp_kernel, and
class ClampingPositionBased (resolve_arguments, set_device, modify_forces)
remain unchanged except for formatting edits, then update the PR.
- Around line 97-110: The constructor currently keeps pre-built wp.array objects
in self.lookup_angles/self.lookup_torques which prevents set_device() from
migrating them to the target device; update set_device() to detect when
self.lookup_angles or self.lookup_torques are wp.array instances that live on a
different device and re-create or copy them onto the provided device (so
_remotized_clamp_kernel never receives mixed-device inputs). Specifically, in
set_device() check the device of self.lookup_angles and self.lookup_torques and,
if they exist but are not on the target device, migrate them to the target
device (using Warp's array/copy mechanism) before returning.

In `@newton_actuators/_src/clamping/clamping_velocity_based.py`:
- Around line 53-61: In resolve_arguments of ClampingVelocityBased, validate
that the provided "velocity_limit" is strictly greater than 0 (for scalars) or
every element is > 0 (for sequences/arrays) and raise a ValueError if any value
is <= 0 to prevent division-by-zero/inverted behavior; update both the
scalar/sequence overloads (the resolve_arguments implementation and the similar
block referenced at lines ~63-78) to perform this positive-check before
returning the dict and keep returning "saturation_effort", "velocity_limit", and
"max_force" as before.

In `@newton_actuators/_src/controllers/controller_net_lstm.py`:
- Around line 147-151: The code currently chooses torch_target_idx by object
identity (comparing target_indices is self._warp_sequential_indices) which fails
when an equivalent array is passed; replace this with logic that derives the
torch index tensor from target_indices itself (or uses an explicit flag).
Concretely, add or reuse a helper like _torch_indices_for(target_indices) that
returns the correct tensor (e.g., by comparing contents with
np.array_equal/torch.equal or by constructing indices from target_indices
values) and use torch_target_idx = self._torch_indices_for(target_indices)
instead of the identity check between _torch_sequential_indices and
_warp_sequential_indices; alternatively accept an explicit parameter (e.g.,
targets_are_sequential) and branch on that rather than object identity.

In `@newton_actuators/_src/controllers/controller_net_mlp.py`:
- Around line 135-139: Replace the identity check that picks torch_target_idx
(currently using "target_indices is self._warp_sequential_indices") with a
stable content-based approach: convert incoming target_indices to a torch tensor
(e.g., torch.as_tensor(target_indices)) or accept an explicit indexing-mode flag
from the caller, then select between self._torch_sequential_indices and
self._torch_input_indices based on the content/flag. Update the logic around
torch_target_idx, target_indices, self._warp_sequential_indices,
self._torch_sequential_indices and self._torch_input_indices so indexing no
longer relies on object identity.

In `@newton_actuators/_src/usd_parser.py`:
- Around line 136-154: The parser currently accepts an applied schema even when
none of its required attributes are authored; to fix this, after collecting
kwargs from entry.param_map but before treating the schema as present, invoke
entry.validate(kwargs) unconditionally (or check explicitly if no params were
authored and then call validate) and propagate/raise any validation error with
context (include prim path/name and entry.component_class.__name__) so the
failure is tied to the prim/schema; only when validate succeeds should you set
controller_class/controller_kwargs or append to component_specs (using the
existing symbols entry.param_map, get_attribute, entry.validate,
entry.is_controller, controller_class, controller_kwargs, component_specs, and
entry.component_class).

In `@README.md`:
- Around line 24-32: The fenced code block showing the Actuator tree (lines
containing "Actuator", "Controller", "Delay", "Clamping", "ClampingMaxForce",
"ClampingVelocityBased", "ClampingPositionBased") should include a language tag
to satisfy markdownlint; update the triple-backtick fence to use a language like
"text" (or another suitable language) immediately after the opening ``` so the
block is fenced as ```text.
- Around line 214-216: The example uses the same buffer for both current and
next state when calling actuator.step, which breaks the double-buffered pattern;
update the docs to call actuator.state() to create two buffers (e.g., state and
next_state), pass state as the current state and next_state as the next state to
actuator.step(sim_state, sim_control, state, next_state, dt=0.01), then after
each step swap or reassign so the next_state becomes the current for the
following iteration; reference the actuator.state() factory and
actuator.step(sim_state, sim_control, current_state, next_state, dt) when
describing the pattern.

---

Outside diff comments:
In `@newton_actuators/tests/test_actuators.py`:
- Around line 1-1396: The file fails ruff formatting; run the formatter (e.g.,
ruff format) over this test module (the one containing classes like
TestPDWithClampingMaxForce, TestPDWithDelay, TestPIDWithClampingMaxForce,
TestPDWithDCMotor, TestPDWithClampingPositionBased, TestComposition, and
TestControllerNetMLP/LSTM) and commit the reformatted result so CI no longer
reports "file would be reformatted".

---

Duplicate comments:
In `@newton_actuators/__init__.py`:
- Around line 27-48: The package exports in __all__ use suffix-first names
(ControllerPD, ControllerPID, ControllerNetMLP, ControllerNetLSTM,
ClampingMaxForce, ClampingPositionBased, ClampingVelocityBased) which conflicts
with the agreed prefix-first naming; update __all__ to export the prefix-first
variants (e.g. PDController, PIDController, NetMLPController, NetLSTMController,
MaxForceClamping or PositionBasedClamping, VelocityBasedClamping) or remove the
suffix-first entries until the rename is completed across the codebase; ensure
the exported names in __all__ match the actual class/function identifiers
(Actuator, StateActuator, Controller, Delay, Clamping, ParsedActuator,
parse_actuator_prim) so importing modules won’t break.

In `@newton_actuators/_src/actuator.py`:
- Around line 17-42: The scatter kernels _scatter_add_kernel and
_scatter_add_dual_kernel perform non-atomic read/modify/write on shared output
arrays and must use atomic additions to avoid lost updates when multiple threads
target the same index; replace the direct updates (e.g., output[idx] =
output[idx] + forces[i] and applied_output[idx] = applied_output[idx] +
applied_forces[i], computed_output[idx] = computed_output[idx] +
computed_forces[i]) with the Warp atomic add primitive (e.g., wp.atomic.add) so
each thread performs an atomic addition into output, applied_output, and
computed_output respectively.

In `@newton_actuators/_src/controllers/controller_pd.py`:
- Around line 17-24: The kernel _pd_force_kernel currently declares
control_input and constant_force as concrete wp.array kernel params but
compute() may call the kernel with act_input=None or self.constant_force=None
which is unsafe; before launching _pd_force_kernel (and the other kernel
variants referenced around lines 35-41 and 79-111), ensure you materialize
zero-filled wp.array instances when act_input or constant_force is None (e.g.,
create arrays of the correct dtype/shape filled with zeros matching
state_indices/target_indices length) or alternatively provide a separate kernel
overload that omits those parameters; update the caller in compute() to replace
None with those zero arrays so the kernel never receives None for
control_input/constant_force.

In `@newton_actuators/_src/controllers/controller_pid.py`:
- Around line 19-27: The kernel _pid_force_kernel must never be launched with
Python None for array parameters (e.g., control_input, constant_force); update
compute() to ensure all kernel arguments are concrete wp.array instances by
either (a) creating zero/empty wp.array placeholders for any None inputs
(matching expected dtype and length) before calling _pid_force_kernel, or (b)
split into two kernel variants and call the correct one when control_input or
constant_force is None; adjust the call sites in compute() and any places that
pass state_indices/target_indices/force_indices so they always supply valid
wp.array objects rather than None.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: da949fbd-46af-45be-8d65-03b7649f53e1

📥 Commits

Reviewing files that changed from the base of the PR and between ce79c8c and a36f985.

📒 Files selected for processing (15)
  • README.md
  • newton_actuators/__init__.py
  • newton_actuators/_src/actuator.py
  • newton_actuators/_src/clamping/__init__.py
  • newton_actuators/_src/clamping/base.py
  • newton_actuators/_src/clamping/clamping_max_force.py
  • newton_actuators/_src/clamping/clamping_position_based.py
  • newton_actuators/_src/clamping/clamping_velocity_based.py
  • newton_actuators/_src/controllers/__init__.py
  • newton_actuators/_src/controllers/controller_net_lstm.py
  • newton_actuators/_src/controllers/controller_net_mlp.py
  • newton_actuators/_src/controllers/controller_pd.py
  • newton_actuators/_src/controllers/controller_pid.py
  • newton_actuators/_src/usd_parser.py
  • newton_actuators/tests/test_actuators.py
✅ Files skipped from review due to trivial changes (1)
  • newton_actuators/_src/controllers/init.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • newton_actuators/_src/clamping/init.py

Comment on lines +180 to +187
def step(
self,
sim_state: Any,
sim_control: Any,
current_act_state: StateActuator | None = None,
next_act_state: StateActuator | None = None,
dt: float = None,
) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make dt required or validate it upfront.

The public signature makes dt look optional, but compute() and delay.update_state() always receive it. For PID/delay/stateful paths, omitting dt will fail later or propagate None into time-based math. Either remove the default or raise before dispatch.

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

In `@newton_actuators/_src/actuator.py` around lines 180 - 187, The step method
currently has an optional dt parameter which is actually required by downstream
callers (compute() and delay.update_state()), so either remove the default to
make dt a required positional/keyword argument on Actuator.step or validate it
at the top of step and raise a clear exception; update the signature on def
step(..., dt: float) -> None to drop the "= None" or add an early check like "if
dt is None: raise ValueError('dt is required')" before any calls to compute() or
delay.update_state() so no None is propagated into time-based math (refer to
step, compute, and delay.update_state in your changes).

Comment on lines +206 to +249
has_states = current_act_state is not None and next_act_state is not None

positions = getattr(sim_state, self.state_pos_attr)
velocities = getattr(sim_state, self.state_vel_attr)

orig_target_pos = getattr(sim_control, self.control_target_pos_attr)
orig_target_vel = getattr(sim_control, self.control_target_vel_attr)
orig_act_input = None
if self.control_input_attr is not None:
orig_act_input = getattr(sim_control, self.control_input_attr, None)

target_pos = orig_target_pos
target_vel = orig_target_vel
act_input = orig_act_input
target_indices = self.indices

# --- 1. Delay: read delayed targets ---
skip_compute = False
if self.delay is not None:
delay_state = current_act_state.delay_state if current_act_state else None

if self.delay.is_ready(delay_state):
target_pos, target_vel, act_input, target_indices = (
self.delay.get_delayed_targets(act_input, delay_state)
)
else:
skip_compute = True

if not skip_compute:
# --- 2. Controller: compute raw forces ---
ctrl_state = current_act_state.controller_state if current_act_state else None
self.controller.compute(
positions,
velocities,
target_pos,
target_vel,
act_input,
self.indices,
target_indices,
self._computed_forces,
self._sequential_indices,
self.num_actuators,
ctrl_state,
dt,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail fast when a stateful actuator is stepped without both state buffers.

step() still calls into delay.is_ready() / controller.compute() when current_act_state or next_act_state is missing. The new neural controllers dereference their state immediately, so this turns into a late AttributeError inside component code instead of a clear API error at the boundary. Require both buffers whenever self.is_stateful() is true.

Suggested guard
-        has_states = current_act_state is not None and next_act_state is not None
+        has_states = current_act_state is not None and next_act_state is not None
+        if self.is_stateful() and not has_states:
+            raise ValueError(
+                "Stateful actuators require both current_act_state and next_act_state"
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton_actuators/_src/actuator.py` around lines 206 - 249, If the actuator is
stateful (self.is_stateful() true), ensure step() checks that both
current_act_state and next_act_state are present and raise a clear API error
immediately (e.g., ValueError/RuntimeError) instead of proceeding; add the guard
at the start of the section that currently sets has_states and before calling
delay.is_ready() or controller.compute(), so delay.is_ready(delay_state) and
self.controller.compute(...) never run when current_act_state or next_act_state
is missing.

Comment on lines +50 to +74
def modify_forces(
self,
src_forces: wp.array,
dst_forces: wp.array,
positions: wp.array,
velocities: wp.array,
input_indices: wp.array,
num_actuators: int,
) -> None:
"""Read forces from src, apply clamping, write to dst.

When src and dst are the same array, this is an in-place update.
The Actuator uses different arrays for the first clamping
(to preserve the raw controller output) and the same array
for subsequent clampings.

Args:
src_forces: Input force buffer to read. Shape (N,).
dst_forces: Output force buffer to write. Shape (N,).
positions: Joint positions (global array).
velocities: Joint velocities (global array).
input_indices: Indices into positions/velocities.
num_actuators: Number of actuators N.
"""
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Fail fast in the base modify_forces() contract.

Leaving this as a no-op makes an incomplete custom clamp silently drop force updates. This should raise immediately instead of pretending the override exists.

Suggested change
     def modify_forces(
         self,
         src_forces: wp.array,
         dst_forces: wp.array,
         positions: wp.array,
         velocities: wp.array,
         input_indices: wp.array,
         num_actuators: int,
     ) -> None:
         """Read forces from src, apply clamping, write to dst.
@@
             velocities: Joint velocities (global array).
             input_indices: Indices into positions/velocities.
             num_actuators: Number of actuators N.
         """
-        pass
+        raise NotImplementedError(f"{type(self).__name__} must implement modify_forces")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton_actuators/_src/clamping/base.py` around lines 50 - 74, The base class
modify_forces method currently is a silent no-op; change it to fail fast by
raising a clear error (e.g., NotImplementedError) inside modify_forces so
subclasses must implement it: locate the modify_forces method in
newton_actuators._src.clamping.base (the base clamping class) and replace the
empty implementation with a raised exception that includes a descriptive message
referencing modify_forces and the clamping class to aid debugging.

Comment on lines +29 to +39
@classmethod
def resolve_arguments(cls, args: dict[str, Any]) -> dict[str, Any]:
return {"max_force": args.get("max_force", math.inf)}

def __init__(self, max_force: wp.array):
"""Initialize clamp dynamic.

Args:
max_force: Per-actuator force limits. Shape (N,).
"""
self.max_force = max_force

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate max_force is non-negative.

The kernel assumes each bound is [-max_force[i], +max_force[i]]. If any entry is negative, that interval is reversed and the clamp becomes incorrect. Please reject negative limits in resolve_arguments() and/or __init__().

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

In `@newton_actuators/_src/clamping/clamping_max_force.py` around lines 29 - 39,
Ensure negative per-actuator limits are rejected by validating max_force in both
resolve_arguments and __init__: in resolve_arguments(cls, args) check
args.get("max_force", math.inf) for any negative entries and raise a ValueError
if so (include the offending value in the message); in __init__(self, max_force:
wp.array) validate the provided wp.array has all elements >= 0 (e.g., use a
vectorized test or iterate to detect any element < 0) and raise ValueError if
any are negative; keep the attribute name max_force and preserve the existing
behavior when values are non-negative.

Comment on lines +1 to +133
# SPDX-FileCopyrightText: Copyright (c) 2025 The Newton Developers
# SPDX-License-Identifier: Apache-2.0

from typing import Any

import numpy as np
import warp as wp

from .base import Clamping


@wp.func
def _interp_1d(
x: float,
xs: wp.array(dtype=float),
ys: wp.array(dtype=float),
n: int,
) -> float:
"""Linearly interpolate (x -> y) from sorted sample arrays, clamping at boundaries."""
if n <= 0:
return 0.0
if x <= xs[0]:
return ys[0]
if x >= xs[n - 1]:
return ys[n - 1]
for k in range(n - 1):
if xs[k + 1] >= x:
dx = xs[k + 1] - xs[k]
if dx == 0.0:
return ys[k]
t = (x - xs[k]) / dx
return ys[k] + t * (ys[k + 1] - ys[k])
return ys[n - 1]


@wp.kernel
def _remotized_clamp_kernel(
current_pos: wp.array(dtype=float),
state_indices: wp.array(dtype=wp.uint32),
lookup_angles: wp.array(dtype=float),
lookup_torques: wp.array(dtype=float),
lookup_size: int,
src: wp.array(dtype=float),
dst: wp.array(dtype=float),
):
"""Angle-dependent clamping via interpolated lookup table: read src, write dst."""
i = wp.tid()
state_idx = state_indices[i]
limit = _interp_1d(current_pos[state_idx], lookup_angles, lookup_torques, lookup_size)
dst[i] = wp.clamp(src[i], -limit, limit)


class ClampingPositionBased(Clamping):
"""Angle-dependent torque clamping via lookup table.

Replaces a fixed ±max_force box clamp with angle-dependent torque
limits interpolated from a lookup table. Models remotized actuators
(e.g., linkage-driven joints) where the transmission ratio and thus
maximum output torque vary with joint angle.

This is a post-controller dynamic.
"""

SHARED_PARAMS = {"lookup_angles", "lookup_torques"}

@classmethod
def resolve_arguments(cls, args: dict[str, Any]) -> dict[str, Any]:
if "lookup_angles" not in args or "lookup_torques" not in args:
raise ValueError("ClampPositionBased requires 'lookup_angles' and 'lookup_torques' arguments")
return {
"lookup_angles": tuple(args["lookup_angles"]),
"lookup_torques": tuple(args["lookup_torques"]),
}

def __init__(
self,
lookup_angles: wp.array | tuple[float, ...] | list[float],
lookup_torques: wp.array | tuple[float, ...] | list[float],
):
"""Initialize remotized clamp dynamic.

Args:
lookup_angles: Sorted joint angles for the torque lookup table. Shape (K,).
lookup_torques: Max output torques corresponding to lookup_angles. Shape (K,).
"""
if len(lookup_angles) != len(lookup_torques):
raise ValueError(
f"lookup_angles length ({len(lookup_angles)}) must match "
f"lookup_torques length ({len(lookup_torques)})"
)
self.lookup_size = len(lookup_angles)
self._raw_angles = lookup_angles
self._raw_torques = lookup_torques
self.lookup_angles: wp.array | None = None
self.lookup_torques: wp.array | None = None

if isinstance(lookup_angles, wp.array):
self.lookup_angles = lookup_angles
if isinstance(lookup_torques, wp.array):
self.lookup_torques = lookup_torques

def set_device(self, device: wp.Device) -> None:
if self.lookup_angles is None:
self.lookup_angles = wp.array(
np.array(self._raw_angles, dtype=np.float32), device=device
)
if self.lookup_torques is None:
self.lookup_torques = wp.array(
np.array(self._raw_torques, dtype=np.float32), device=device
)

def modify_forces(
self,
src_forces: wp.array,
dst_forces: wp.array,
positions: wp.array,
velocities: wp.array,
input_indices: wp.array,
num_actuators: int,
) -> None:
wp.launch(
kernel=_remotized_clamp_kernel,
dim=num_actuators,
inputs=[
positions,
input_indices,
self.lookup_angles,
self.lookup_torques,
self.lookup_size,
src_forces,
],
outputs=[dst_forces],
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Run ruff format on this module before merge.

CI already reports “file would be reformatted” for this file.

🧰 Tools
🪛 GitHub Actions: Pull Request

[error] 1-1: Ruff format check failed: file would be reformatted.

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

In `@newton_actuators/_src/clamping/clamping_position_based.py` around lines 1 -
133, This file fails ruff formatting; run the project's auto-formatter (ruff
format) on newton_actuators/_src/clamping/clamping_position_based.py to fix
whitespace, line breaks, and import formatting issues; after reformatting,
ensure the module still compiles and that symbols like _interp_1d,
_remotized_clamp_kernel, and class ClampingPositionBased (resolve_arguments,
set_device, modify_forces) remain unchanged except for formatting edits, then
update the PR.

Comment on lines +147 to +151
torch_target_idx = (
self._torch_sequential_indices
if target_indices is self._warp_sequential_indices
else self._torch_input_indices
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't key target indexing off object identity.

This only works when the delayed-target path returns the exact same sequential_indices object. If an equivalent array is passed instead, the code falls back to self._torch_input_indices and reads compact targets with global DOF indices. Please derive the torch index tensor from target_indices itself, or pass an explicit “compact vs global target array” signal instead.

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

In `@newton_actuators/_src/controllers/controller_net_lstm.py` around lines 147 -
151, The code currently chooses torch_target_idx by object identity (comparing
target_indices is self._warp_sequential_indices) which fails when an equivalent
array is passed; replace this with logic that derives the torch index tensor
from target_indices itself (or uses an explicit flag). Concretely, add or reuse
a helper like _torch_indices_for(target_indices) that returns the correct tensor
(e.g., by comparing contents with np.array_equal/torch.equal or by constructing
indices from target_indices values) and use torch_target_idx =
self._torch_indices_for(target_indices) instead of the identity check between
_torch_sequential_indices and _warp_sequential_indices; alternatively accept an
explicit parameter (e.g., targets_are_sequential) and branch on that rather than
object identity.

Comment on lines +135 to +139
torch_target_idx = (
self._torch_sequential_indices
if target_indices is self._warp_sequential_indices
else self._torch_input_indices
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid identity-based target-index dispatch here too.

This branch assumes the compact-target case can be identified by target_indices is self._warp_sequential_indices. If that object identity ever changes, you'll index the compact target buffer with global actuator indices and get wrong inputs. Convert target_indices itself to a torch tensor, or make the caller pass an explicit indexing mode.

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

In `@newton_actuators/_src/controllers/controller_net_mlp.py` around lines 135 -
139, Replace the identity check that picks torch_target_idx (currently using
"target_indices is self._warp_sequential_indices") with a stable content-based
approach: convert incoming target_indices to a torch tensor (e.g.,
torch.as_tensor(target_indices)) or accept an explicit indexing-mode flag from
the caller, then select between self._torch_sequential_indices and
self._torch_input_indices based on the content/flag. Update the logic around
torch_target_idx, target_indices, self._warp_sequential_indices,
self._torch_sequential_indices and self._torch_input_indices so indexing no
longer relies on object identity.

Comment on lines +136 to +154
kwargs: dict[str, Any] = {}
for usd_name, kwarg_name in entry.param_map.items():
value = get_attribute(prim, f"newton:actuator:{usd_name}")
if value is not None:
kwargs[kwarg_name] = value

transmission = get_attribute(prim, "newton:actuator:transmission")
if entry.validate is not None:
entry.validate(kwargs)

if entry.is_controller:
if controller_class is not None:
raise ValueError(
f"Actuator prim has multiple controllers: "
f"{controller_class.__name__} and {entry.component_class.__name__}"
)
controller_class = entry.component_class
controller_kwargs = kwargs
else:
component_specs.append((entry.component_class, kwargs))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject incomplete applied schemas during parsing.

This loop accepts any applied schema even when none of its required attributes are authored, so you can end up returning controller_class with {} or component specs like (Delay, {}). That defers the failure until later construction and loses the prim/schema context that caused it. Please restore per-schema required-field validation here, especially for schemas such as ControllerNetMLPAPI / ControllerNetLSTMAPI where networkPath is mandatory.

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

In `@newton_actuators/_src/usd_parser.py` around lines 136 - 154, The parser
currently accepts an applied schema even when none of its required attributes
are authored; to fix this, after collecting kwargs from entry.param_map but
before treating the schema as present, invoke entry.validate(kwargs)
unconditionally (or check explicitly if no params were authored and then call
validate) and propagate/raise any validation error with context (include prim
path/name and entry.component_class.__name__) so the failure is tied to the
prim/schema; only when validate succeeds should you set
controller_class/controller_kwargs or append to component_specs (using the
existing symbols entry.param_map, get_attribute, entry.validate,
entry.is_controller, controller_class, controller_kwargs, component_specs, and
entry.component_class).

Comment thread README.md
Comment on lines +24 to +32
```
Actuator
├── Controller (control law that computes raw forces)
├── Delay (optional: delays control targets by N timesteps)
└── Clamping[] (post-controller force bounds)
├── ClampingMaxForce (±max_force box clamp)
├── ClampingVelocityBased (velocity-dependent saturation)
└── ClampingPositionBased (angle-dependent lookup)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a language to this fenced block.

markdownlint is already flagging this block. Mark it as text (or another suitable language) so CI passes cleanly.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 24-24: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@README.md` around lines 24 - 32, The fenced code block showing the Actuator
tree (lines containing "Actuator", "Controller", "Delay", "Clamping",
"ClampingMaxForce", "ClampingVelocityBased", "ClampingPositionBased") should
include a language tag to satisfy markdownlint; update the triple-backtick fence
to use a language like "text" (or another suitable language) immediately after
the opening ``` so the block is fenced as ```text.

Comment thread README.md
Comment on lines +214 to +216
state = actuator.state()
for step in range(num_steps):
lstm_actuator.step(sim_state, sim_control, state, state, dt=0.01)
actuator.step(sim_state, sim_control, state, state, dt=0.01)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use double-buffered state in the LSTM example.

The workflow above documents current/next buffers for stateful actuators, but this example passes the same state as both inputs. Showing the double-buffered pattern here will keep the docs aligned with the rest of the API and avoid readers cargo-culting in-place state updates.

Suggested doc update
-state = actuator.state()
+state_a = actuator.state()
+state_b = actuator.state()
+current, nxt = state_a, state_b
 for step in range(num_steps):
-    actuator.step(sim_state, sim_control, state, state, dt=0.01)
+    actuator.step(sim_state, sim_control, current, nxt, dt=0.01)
+    current, nxt = nxt, current
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
state = actuator.state()
for step in range(num_steps):
lstm_actuator.step(sim_state, sim_control, state, state, dt=0.01)
actuator.step(sim_state, sim_control, state, state, dt=0.01)
state_a = actuator.state()
state_b = actuator.state()
current, nxt = state_a, state_b
for step in range(num_steps):
actuator.step(sim_state, sim_control, current, nxt, dt=0.01)
current, nxt = nxt, current
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 214 - 216, The example uses the same buffer for both
current and next state when calling actuator.step, which breaks the
double-buffered pattern; update the docs to call actuator.state() to create two
buffers (e.g., state and next_state), pass state as the current state and
next_state as the next state to actuator.step(sim_state, sim_control, state,
next_state, dt=0.01), then after each step swap or reassign so the next_state
becomes the current for the following iteration; reference the actuator.state()
factory and actuator.step(sim_state, sim_control, current_state, next_state, dt)
when describing the pattern.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (8)
newton_actuators/_src/actuator.py (3)

18-42: ⚠️ Potential issue | 🟠 Major

Use atomic accumulation in both scatter kernels.

Warp provides wp.atomic_add() for per-element atomic updates, and the docs note that arr[i] += value maps to the same primitive. Both kernels currently do non-atomic read-modify-write accumulation, so duplicate actuator indices can drop contributions nondeterministically. (nvidia.github.io)

Suggested fix
 def _scatter_add_kernel(
@@
     """Scatter-add forces into output at specified indices."""
     i = wp.tid()
     idx = indices[i]
-    output[idx] = output[idx] + forces[i]
+    wp.atomic_add(output, idx, forces[i])
@@
 def _scatter_add_dual_kernel(
@@
     """Scatter-add both applied and computed forces in one pass."""
     i = wp.tid()
     idx = indices[i]
-    applied_output[idx] = applied_output[idx] + applied_forces[i]
-    computed_output[idx] = computed_output[idx] + computed_forces[i]
+    wp.atomic_add(applied_output, idx, applied_forces[i])
+    wp.atomic_add(computed_output, idx, computed_forces[i])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton_actuators/_src/actuator.py` around lines 18 - 42, The scatter kernels
perform non-atomic read-modify-write updates which can lose contributions for
duplicate indices; in _scatter_add_kernel replace the non-atomic accumulation of
output[idx] = output[idx] + forces[i] with a call to wp.atomic_add(output, idx,
forces[i]) (using the same signature Warp expects), and in
_scatter_add_dual_kernel replace both applied_output[idx] = applied_output[idx]
+ applied_forces[i] and computed_output[idx] = computed_output[idx] +
computed_forces[i] with wp.atomic_add(applied_output, idx, applied_forces[i])
and wp.atomic_add(computed_output, idx, computed_forces[i]) respectively so each
update is performed atomically.

180-187: ⚠️ Potential issue | 🟠 Major

Make dt required at the API boundary.

The signature still advertises dt as optional, but step() forwards it into time-based controller and delay code. Omitting it turns into a late failure or None leaking into math instead of a clear API error.

Suggested fix
     def step(
         self,
         sim_state: Any,
         sim_control: Any,
         current_act_state: StateActuator | None = None,
         next_act_state: StateActuator | None = None,
-        dt: float = None,
+        dt: float,
     ) -> None:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@newton_actuators/_src/actuator.py` around lines 180 - 187, The step(...) API
currently declares dt: float = None but forwards it into time-based controllers
and delay logic, allowing None to leak into math; make dt required at the API
boundary by changing the signature of step in the Actuator class/method (step)
to accept dt: float (non-optional), remove the default None, and add a defensive
check that raises a clear TypeError if dt is None when called; update any
callers of step to pass a float dt accordingly and adjust type hints for
StateActuator-related uses if needed so callers and downstream methods
(controllers/delay code) never receive None.

206-207: ⚠️ Potential issue | 🟠 Major

Fail fast when a stateful actuator is stepped without both buffers.

If the controller or delay is stateful, this path still enters component code with missing state or silently skips state advancement. That turns a boundary misuse into late AttributeErrors or permanent no-op delay filling instead of a clear error.

Suggested fix
         has_states = current_act_state is not None and next_act_state is not None
+        if self.is_stateful() and not has_states:
+            raise ValueError(
+                "Stateful actuators require both current_act_state and next_act_state"
+            )

Also applies to: 225-249, 287-301

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

In `@newton_actuators/_src/actuator.py` around lines 206 - 207, The code computes
has_states = current_act_state is not None and next_act_state is not None but
doesn't fail fast when a stateful component is stepped without both buffers;
update the stepping logic in the Actuator (the method that defines
has_states/current_act_state/next_act_state and the surrounding branches at the
same pattern around lines matching 225-249 and 287-301) to validate early: if
the controller or delay is stateful (check the same condition you use to decide
to use states) and either current_act_state or next_act_state is None, raise a
clear ValueError (or custom exception) indicating the missing state buffer and
which one is absent, rather than proceeding or silently skipping state
advancement. Ensure the error is raised before any component code runs so misuse
is detected immediately.
README.md (2)

214-216: ⚠️ Potential issue | 🟡 Minor

Use two state buffers in the LSTM example.

Passing the same object as both current and next state at Line 216 contradicts the double-buffered workflow documented above and encourages in-place updates for a stateful actuator.

Suggested fix
-state = actuator.state()
+state_a = actuator.state()
+state_b = actuator.state()
+current, nxt = state_a, state_b
 for step in range(num_steps):
-    actuator.step(sim_state, sim_control, state, state, dt=0.01)
+    actuator.step(sim_state, sim_control, current, nxt, dt=0.01)
+    current, nxt = nxt, current
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 214 - 216, The snippet passes the same buffer for
current and next state to actuator.step (creating in-place updates), which
breaks the double-buffered pattern; change to allocate two distinct buffers via
actuator.state() (e.g., state and next_state), call actuator.step(sim_state,
sim_control, state, next_state, dt=0.01), then swap or assign next_state to
state each loop iteration to preserve the double-buffered workflow used by the
LSTM actuator.

24-32: ⚠️ Potential issue | 🟡 Minor

Add a language to this fenced block.

markdownlint is already flagging the fence at Line 24, so CI will keep complaining until this tree example is fenced as text (or similar).

Suggested fix
-```
+```text
 Actuator
 ├── Controller      (control law that computes raw forces)
 ├── Delay           (optional: delays control targets by N timesteps)
 └── Clamping[]          (post-controller force bounds)
     ├── ClampingMaxForce       (±max_force box clamp)
     ├── ClampingDCMotor   (velocity-dependent saturation)
     └── ClampingPositionBased  (angle-dependent lookup)
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @README.md around lines 24 - 32, The fenced code block showing the Actuator
tree is missing a language tag; update the fence that starts before "Actuator"
and ends after "ClampingPositionBased" to include a language like text (e.g.

"Actuator" header and change the opening ``` to ```text and keep the contents
unchanged.
newton_actuators/_src/controllers/controller_pid.py (1)

19-27: ⚠️ Potential issue | 🔴 Critical

Don't pass None into concrete Warp array parameters.

Warp documents that kernel arguments are statically typed, and that typing.Any is the generic escape hatch when a kernel needs multiple concrete instantiations. Given that _pid_force_kernel declares concrete wp.array(...) slots, forwarding Python None for act_input or self.constant_force from compute() is not a supported launch pattern, so the default ControllerPID(..., constant_force=None) path can fail before those kernel-side if checks help. Materialize zero-filled arrays before launch and make the kernel read them unconditionally. (nvidia.github.io)

Suggested fix
 `@wp.kernel`
 def _pid_force_kernel(
@@
-    const_f = float(0.0)
-    if constant_force:
-        const_f = constant_force[i]
-
-    act = float(0.0)
-    if control_input:
-        act = control_input[target_idx]
+    const_f = constant_force[i]
+    act = control_input[target_idx]
@@
 class ControllerPID(Controller):
@@
         self.integral_max = integral_max
         self.constant_force = constant_force
         self._next_integral: wp.array | None = None
+        self._zero_act_input: wp.array | None = None
 
     def set_indices(self, input_indices: wp.array, sequential_indices: wp.array) -> None:
         num = len(input_indices)
         device = input_indices.device
         self._next_integral = wp.zeros(num, dtype=wp.float32, device=device)
+        self._zero_act_input = wp.zeros(num, dtype=wp.float32, device=device)
+        if self.constant_force is None:
+            self.constant_force = wp.zeros(num, dtype=wp.float32, device=device)
@@
     def compute(
@@
     ) -> None:
+        act_input = act_input if act_input is not None else self._zero_act_input
         wp.launch(
             kernel=_pid_force_kernel,
             dim=num_actuators,

Also applies to: 45-51, 91-92, 137-158

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

In `@newton_actuators/_src/controllers/controller_pid.py` around lines 19 - 27,
The kernel _pid_force_kernel and related warp kernels declare concrete wp.array
parameters but compute() may forward Python None for act_input or
self.constant_force; change ControllerPID.__init__/compute() to always
materialize zero-filled wp.array buffers (dtype matching kp/ki/kd and same
length as relevant indices) instead of passing None, and update compute() to
pass those arrays unconditionally to _pid_force_kernel (and any kernels
referenced at lines ~45-51, 91-92, 137-158) so the kernel reads arrays without
relying on Python-side None checks; ensure the arrays are allocated with
wp.zeros or equivalent with correct dtype (float or wp.uint32 where appropriate)
and lengths matching state_indices/target_indices/force_indices.
newton_actuators/_src/clamping/clamping_dc_motor.py (1)

31-35: ⚠️ Potential issue | 🟠 Major

Guard the torque-speed math against invalid or infinite limits.

velocity_limit is only checked for presence here, but the kernel divides by it, so zero or negative values still produce bad saturation when ClampingDCMotor(...) is constructed directly. The math.inf default for saturation_effort is also unsafe in this formula, because the cutoff case becomes inf * 0 and can turn the clamp bounds into NaN. Please validate positive velocity_limit in the component itself and special-case the unbounded path instead of feeding raw infinities into the kernel.

Also applies to: 52-59, 61-76

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

In `@newton_actuators/_src/clamping/clamping_dc_motor.py` around lines 31 - 35,
The kernel that computes max_torque/min_torque uses division by velocity_limit
and multiplies by saturation_effort, which can be zero/negative or math.inf and
produce NaN; update the ClampingDCMotor constructor (or component validation) to
assert/raise if any velocity_limit values are <= 0 and treat
saturation_effort==math.inf as the unbounded path instead of passing infinities
into the kernel, and then change the kernel logic (the computation around
max_torque/min_torque in clamping_dc_motor.py) to special-case the unbounded
saturation: if saturation is unbounded skip the sat*(...)/vel_lim math and use
±max_force directly (or otherwise compute safe finite bounds), ensuring no
division by zero or inf*0 occurs; apply the same guard/fix to the other similar
blocks referenced (around the other torque-speed computations in the file: lines
~52-59 and ~61-76).
newton_actuators/_src/usd_parser.py (1)

24-68: ⚠️ Potential issue | 🟠 Major

Reject incomplete applied schemas at parse time.

This path only enforces the validators that exist in SCHEMA_REGISTRY, so DelayAPI, ControllerNetMLPAPI, and ControllerNetLSTMAPI can still parse as empty kwargs and fail later during construction. Add required-field validators for schemas with mandatory authored attributes and wrap the resulting ValueError with the prim path/schema name so bad USD is rejected with context.

Suggested fix
+def _require_keys(schema_name: str, *keys: str):
+    def validate(kwargs: dict[str, Any]) -> None:
+        missing = [key for key in keys if key not in kwargs]
+        if missing:
+            raise ValueError(f"{schema_name} requires {', '.join(missing)}")
+    return validate
+
+
 def _validate_clamp_velocity_based(kwargs: dict[str, Any]) -> None:
     vel_lim = kwargs.get("velocity_limit")
     if vel_lim is not None and vel_lim <= 0.0:
         raise ValueError(
             f"ClampingDCMotorAPI requires velocity_limit > 0 (division by velocity_limit "
@@
     "DelayAPI": SchemaEntry(
         component_class=Delay,
         param_map={"delay": "delay"},
+        validate=_require_keys("DelayAPI", "delay"),
     ),
@@
     "ControllerNetMLPAPI": SchemaEntry(
         component_class=ControllerNetMLP,
         param_map={"networkPath": "network_path"},
         is_controller=True,
+        validate=_require_keys("ControllerNetMLPAPI", "network_path"),
     ),
     "ControllerNetLSTMAPI": SchemaEntry(
         component_class=ControllerNetLSTM,
         param_map={"networkPath": "network_path"},
         is_controller=True,
+        validate=_require_keys("ControllerNetLSTMAPI", "network_path"),
     ),
 }
@@
-        if entry.validate is not None:
-            entry.validate(kwargs)
+        if entry.validate is not None:
+            try:
+                entry.validate(kwargs)
+            except ValueError as exc:
+                raise ValueError(
+                    f"{prim.GetPath()}: invalid {schema_name} config: {exc}"
+                ) from exc

Also applies to: 142-154

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@newton_actuators/_src/actuator.py`:
- Around line 18-42: The scatter kernels perform non-atomic read-modify-write
updates which can lose contributions for duplicate indices; in
_scatter_add_kernel replace the non-atomic accumulation of output[idx] =
output[idx] + forces[i] with a call to wp.atomic_add(output, idx, forces[i])
(using the same signature Warp expects), and in _scatter_add_dual_kernel replace
both applied_output[idx] = applied_output[idx] + applied_forces[i] and
computed_output[idx] = computed_output[idx] + computed_forces[i] with
wp.atomic_add(applied_output, idx, applied_forces[i]) and
wp.atomic_add(computed_output, idx, computed_forces[i]) respectively so each
update is performed atomically.
- Around line 180-187: The step(...) API currently declares dt: float = None but
forwards it into time-based controllers and delay logic, allowing None to leak
into math; make dt required at the API boundary by changing the signature of
step in the Actuator class/method (step) to accept dt: float (non-optional),
remove the default None, and add a defensive check that raises a clear TypeError
if dt is None when called; update any callers of step to pass a float dt
accordingly and adjust type hints for StateActuator-related uses if needed so
callers and downstream methods (controllers/delay code) never receive None.
- Around line 206-207: The code computes has_states = current_act_state is not
None and next_act_state is not None but doesn't fail fast when a stateful
component is stepped without both buffers; update the stepping logic in the
Actuator (the method that defines has_states/current_act_state/next_act_state
and the surrounding branches at the same pattern around lines matching 225-249
and 287-301) to validate early: if the controller or delay is stateful (check
the same condition you use to decide to use states) and either current_act_state
or next_act_state is None, raise a clear ValueError (or custom exception)
indicating the missing state buffer and which one is absent, rather than
proceeding or silently skipping state advancement. Ensure the error is raised
before any component code runs so misuse is detected immediately.

In `@newton_actuators/_src/clamping/clamping_dc_motor.py`:
- Around line 31-35: The kernel that computes max_torque/min_torque uses
division by velocity_limit and multiplies by saturation_effort, which can be
zero/negative or math.inf and produce NaN; update the ClampingDCMotor
constructor (or component validation) to assert/raise if any velocity_limit
values are <= 0 and treat saturation_effort==math.inf as the unbounded path
instead of passing infinities into the kernel, and then change the kernel logic
(the computation around max_torque/min_torque in clamping_dc_motor.py) to
special-case the unbounded saturation: if saturation is unbounded skip the
sat*(...)/vel_lim math and use ±max_force directly (or otherwise compute safe
finite bounds), ensuring no division by zero or inf*0 occurs; apply the same
guard/fix to the other similar blocks referenced (around the other torque-speed
computations in the file: lines ~52-59 and ~61-76).

In `@newton_actuators/_src/controllers/controller_pid.py`:
- Around line 19-27: The kernel _pid_force_kernel and related warp kernels
declare concrete wp.array parameters but compute() may forward Python None for
act_input or self.constant_force; change ControllerPID.__init__/compute() to
always materialize zero-filled wp.array buffers (dtype matching kp/ki/kd and
same length as relevant indices) instead of passing None, and update compute()
to pass those arrays unconditionally to _pid_force_kernel (and any kernels
referenced at lines ~45-51, 91-92, 137-158) so the kernel reads arrays without
relying on Python-side None checks; ensure the arrays are allocated with
wp.zeros or equivalent with correct dtype (float or wp.uint32 where appropriate)
and lengths matching state_indices/target_indices/force_indices.

In `@README.md`:
- Around line 214-216: The snippet passes the same buffer for current and next
state to actuator.step (creating in-place updates), which breaks the
double-buffered pattern; change to allocate two distinct buffers via
actuator.state() (e.g., state and next_state), call actuator.step(sim_state,
sim_control, state, next_state, dt=0.01), then swap or assign next_state to
state each loop iteration to preserve the double-buffered workflow used by the
LSTM actuator.
- Around line 24-32: The fenced code block showing the Actuator tree is missing
a language tag; update the fence that starts before "Actuator" and ends after
"ClampingPositionBased" to include a language like text (e.g. ```text) so
markdownlint stops flagging it; locate the block containing the "Actuator"
header and change the opening ``` to ```text and keep the contents unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0b47c31-cd6d-4189-8231-e44b888eab28

📥 Commits

Reviewing files that changed from the base of the PR and between a36f985 and f40b4b6.

📒 Files selected for processing (8)
  • README.md
  • newton_actuators/__init__.py
  • newton_actuators/_src/actuator.py
  • newton_actuators/_src/clamping/__init__.py
  • newton_actuators/_src/clamping/clamping_dc_motor.py
  • newton_actuators/_src/controllers/controller_pid.py
  • newton_actuators/_src/usd_parser.py
  • newton_actuators/tests/test_actuators.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • newton_actuators/_src/clamping/init.py
  • newton_actuators/tests/test_actuators.py



@dataclass
class StateActuator:

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.

Can we make this class also nested under Actuator, i.e. Actuator.State? Just like you have it for Delay.State.

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

Overall looks good to me, the modular design is great!

When merging into Newton the type hints should be cleaned up, i.e. use wp.array[dtype] notation rather than wp.array(dtype=...). There are a bunch of linter errors. It would be nice to avoid Any type hints but try to be more precise (e.g. Controller.State could be an empty dataclass with the def reset(self) -> None: ... definition by default and used as base class for specific controller state classes. Then the controller_state in ActuatorState can use that Controller.State as type hint. We can iterate on this further once you merge into Newton.

There are also a couple of CodeRabbit comments that would be good to address.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants