Repository navigation
Reporting motor impulse under implicitfast: am I separating the force samples correctly? #3652
IntroHi! I am a solodev / researcher hobbyist (my masters was in chemistry lol). The project I'm doing right now is creating true physics locomotion with godot, rapier, and mujoco - all without RL, training, or hidden/extra forces moving the body. My setupMuJoCo: 3.11.0, native CPU implementation. MJX was not used. My questionHi, I’m Cole. I’ve been building LoColemotion, a portable Rust locomotion SDK with native adapters for Godot/Jolt, Rapier/Parry and MuJoCo. The other half of the project is an experiment harness that preregisters the questions and pass conditions, binds the source and runtime, requires qualification and authorization, and retains failed and invalid attempts. I’d appreciate a check on how I’m describing one MuJoCo integration finding. The retained study used MuJoCo 3.11.0 on Windows, the official Python wheel, and five 1/600 s implicitfast steps per 1/120 s controller command. In the first declared unloaded scalar cell, I retained the actuator force after mj_step2, before a subsequent mj_forward, separately from the force recomputed at the post-step state. The first internal step gave:
For the unsaturated portion of this exact fixture, the post-state force multiplied by the timestep agrees with the momentum change. The force captured before that recomputation still matters for identifying saturation and checking the force-time budget. An earlier attempt sampled the wrong stage for its declared evaluation. The harness kept that attempt invalid; the corrected study had its own identity and passed its complete declared 24-cell grid. [Short review packet, figure, exact profiles and original CSV](https://github.com/Slagathore/LoColemotion/blob/main/sdk/docs/ENGINE_REVIEW_PACKET.md) Is this a sound way to distinguish the force observation used for budgeting from the effective motor impulse under implicitfast? Is there a recommended native measurement approach I should use before extending this reporting to models with contacts? Minimal model and/or code that explain my questionThis is the complete one-hinge model used for the first declared cell ( The Python below is a shortened extraction of the original sampling sequence. I have not run this extraction as a separate experiment; the observations in my question come from the retained qualified run linked below. Save the XML as Model: Complete MJCF<mujoco model="sporespore_mujoco_velocity_only_vh4">
<compiler angle="radian" inertiafromgeom="true"/>
<option timestep="0.0016666666666666668" gravity="0 0 0" integrator="implicitfast" solver="Newton" iterations="20" ls_iterations="7" cone="elliptic"/>
<worldbody>
<body name="child" pos="0 0 0">
<joint name="hinge" type="hinge" pos="0 0 0" axis="1 0 0"
armature="0.01" damping="0" limited="false"/>
<geom name="child_geom" type="box" pos="0 0 0" size=".1 .1 .1"
mass="1" contype="0" conaffinity="0"/>
</body>
</worldbody>
<actuator>
<velocity name="motor" joint="hinge" gear="1 0 0 0 0 0"
ctrllimited="false" kv="10"
forcelimited="true"
forcerange="-6 6"/>
</actuator>
</mujoco>Code: Python sampling sequence (MuJoCo 3.11.0)import mujoco
import numpy as np
# Use the full XML shown above, saved as model.xml beside this script.
if mujoco.__version__ != "3.11.0":
raise RuntimeError("This example targets MuJoCo 3.11.0.")
model = mujoco.MjModel.from_xml_path("model.xml")
data = mujoco.MjData(model)
observation = mujoco.MjData(model)
mass = np.empty((model.nv, model.nv), dtype=np.float64, order="C")
joint = model.joint("hinge").id
actuator = model.actuator("motor").id
qpos = int(model.jnt_qposadr[joint])
dof = int(model.jnt_dofadr[joint])
dt = float(model.opt.timestep)
data.qvel[dof] = 0.0
mujoco.mj_forward(model, data)
print("step,time_s,completed_force_Nm,recomputed_force_Nm,motor_impulse_Nms,qvel_rad_s")
# Three outer commands, each held over five internal physics steps.
for outer in range(3):
for substep in range(5):
mujoco.mj_step1(model, data)
velocity_before = float(data.qvel[dof])
mujoco.mj_fullM(model, data, mass)
inertia_before = float(mass[dof, dof])
data.ctrl[actuator] = -0.75
data.qfrc_applied.fill(0.0)
mujoco.mj_step2(model, data)
# Retain these before any later force recomputation.
completed_force = float(data.actuator_force[actuator])
velocity_after = float(data.qvel[dof])
impulse = inertia_before * (velocity_after - velocity_before)
# Recompute on separate data, preserving the stepping data.
observation.time = data.time
observation.qpos[:] = data.qpos
observation.qvel[:] = data.qvel
observation.act[:] = data.act
observation.qacc_warmstart[:] = data.qacc_warmstart
observation.ctrl[:] = data.ctrl
observation.qfrc_applied.fill(0.0)
observation.xfrc_applied.fill(0.0)
mujoco.mj_forward(model, observation)
recomputed_force = float(observation.actuator_force[actuator])
step = outer * 5 + substep
print(f"{step},{data.time:.17g},{completed_force:.17g},"
f"{recomputed_force:.17g},{impulse:.17g},{velocity_after:.17g}")For this unloaded, one-DoF fixture, the momentum-inferred motor impulse is The first three rows of the original retained data, rounded for readability:
Review packet and retained declarations/closures Original model and sampling implementation This figure shows the first 12 internal steps of the first declared VH4 cell,
Confirmations
|
Replies: 1 comment 1 reply
|
Yes—the split you are making is real, and for an unloaded hinge with In the pipeline docs, Calling A couple of tightenings I would make before contacts:
So: keep both samples, document which one is “completed step” vs “post-state evaluate,” and switch impulse accounting to net generalized impulse as soon as contacts are on. The unloaded fixture logic looks right. |

Yes—the split you are making is real, and for an unloaded hinge with
implicitfastit is a sound way to separate “force that participated in this substep” from “force evaluated at the post-step state.”In the pipeline docs,
mj_step1runs through the position/velocity stages and stops before forces are finished;mj_step2runs actuation → acceleration → constraints → integration. Actuator outputs are written inmj_fwdActuationintomjData.actuator_force(and assembled intomjData.qfrc_actuator). So the value you read immediately aftermj_step2, before another forward pass, is the force that belonged to that completed step.Calling
mj_forwardon the updatedqpos/qvel(yourobservationcopy) r…