Evaluate the joint PD inside mj_step via MuJoCo actuators - #2
Open
T-K-233 wants to merge 1 commit into
Open
Conversation
MujocoSystem computed the MIT PD in write() and injected it as qfrc_applied, so the PD was sampled once per controller_manager tick. That made the CM rate the PD rate in sim -- unlike real hardware, where the drive firmware owns the inner loop -- and it bypassed MuJoCo's actuator machinery entirely, so the integrator never got to treat the damping term implicitly. Now each joint's actuator is turned into a position servo at init (biastype affine; a plain MJCF <motor> is gaintype fixed / gainprm 1 with no bias, so setting biasprm to (0, -kp, -kv) makes it behave exactly like <position kp kv>), and write() updates gainprm/biasprm from the stiffness and damping command interfaces plus ctrl from the position command. MuJoCo then re-evaluates the feedback every physics step. This is the architecture gz_ros2_control already uses: write() deposits a setpoint and the simulator's own PID runs in PreUpdate, every physics iteration. It is also what mjlab trains against, so it closes a sim/training gap as well as a sim/real one. Only the terms that are constant over a tick stay a generalized applied force -- the velocity target and the torque feedforward, both zero on the RL and standby paths. Force clamping now comes from the MJCF's own forcerange, applied inside mj_step. read() reports qfrc_actuator + qfrc_applied so effort still accounts for both paths. A joint whose MJCF declares no actuator falls back to the old behaviour with a warning, so models without actuators keep working. Measured in the Lite arms sim, worst-joint peak-to-peak after standby settles: 200 Hz, PD in mj_step 0.00065 rad quiescent 200 Hz, PD in write() 0.0371 rad 25-37 Hz wrist chatter 400 Hz, PD in write() 0.0024 rad quiescent So the PD is not only chatter-free at the lower rate, it is 3.6x quieter than the previous best arrangement -- and sim and real can now share one CM rate. Requires MJCFs whose actuators are position servos (Lite-Description#13). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012QKJQ9hze1hMWXJpdPcjp3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
MujocoSystem::write()computed the MIT PD itself and injected it asqfrc_applied:Two consequences:
controller_managertick, so lowering the CM rate destabilised joints that are perfectly stable on the robot.ctrl, nogainprm, and critically no implicit integration of the damping term, which is what keeps a stiff PD stable at a coarse timestep.Change
Each joint's actuator is converted into a position servo at init —
biastype=affine, since a plain MJCF<motor>isgaintype=fixed/gainprm=1with no bias, andbiasprm=(0,−kp,−kv)makes it behave exactly like<position kp kv>.write()then updatesgainprm/biasprmfrom the stiffness/damping command interfaces andctrlfrom the position command, and MuJoCo re-evaluates the feedback every physics step.Only the terms that are constant over a tick remain a generalized applied force — the velocity target and torque feedforward, both zero on the RL and standby paths. Force clamping now comes from the MJCF's own
forcerange, applied insidemj_step.read()reportsqfrc_actuator + qfrc_appliedso effort still accounts for both paths.This is the architecture
gz_ros2_controlalready uses:write()deposits a setpoint and the simulator's own PID runs inPreUpdate, every physics iteration. It is also what mjlab trains against, so it closes a sim/training gap as well as a sim/real one.Measured
Lite arms sim, worst-joint peak-to-peak after standby settles:
mj_stepwrite()write()Not only chatter-free at the lower rate but 3.6× quieter than the previous best arrangement — and sim and real can now share one CM rate.
Compatibility
A joint whose MJCF declares no actuator falls back to the previous
qfrc_appliedbehaviour with a warning, so models without actuators keep working unchanged.Depends on MJCFs whose actuators are position servos: Berkeley-Humanoids/Lite-Description#13. Companion CM-rate change: Berkeley-Humanoids/humanoid_control_ros2#24.
🤖 Generated with Claude Code
https://claude.ai/code/session_012QKJQ9hze1hMWXJpdPcjp3