Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
367 changes: 365 additions & 2 deletions mujoco_warp/_src/derivative.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,14 @@ def _qderiv_tendon_damping(


@event_scope
def deriv_smooth_vel(m: Model, d: Data, out: wp.array2d(dtype=float)):
def deriv_smooth_vel(m: Model, d: Data, out: wp.array2d(dtype=float), flg_rne: bool = True):
"""Analytical derivative of smooth forces w.r.t. velocities.

Args:
m: The model containing kinematic and dynamic information (device).
d: The data object containing the current state and output arrays (device).
out: qM - dt * qDeriv (derivatives of smooth forces w.r.t velocities).
flg_rne: Whether to include RNE derivatives.
"""
qMi = m.qM_fullm_i
qMj = m.qM_fullm_j
Expand Down Expand Up @@ -288,4 +289,366 @@ def deriv_smooth_vel(m: Model, d: Data, out: wp.array2d(dtype=float)):
outputs=[out],
)

# TODO(team): rne derivative
if flg_rne:
rne_vel(m, d, out)


@wp.kernel
def _derivative_com_vel_root(Dcvel_out: wp.array3d(dtype=wp.spatial_vector)):
worldid, elementid, k = wp.tid()
Dcvel_out[worldid, 0, k][elementid] = 0.0


@wp.kernel
def _derivative_com_vel_level(
# Model:
nv: int,
body_parentid: wp.array(dtype=int),
body_jntnum: wp.array(dtype=int),
body_jntadr: wp.array(dtype=int),
body_dofadr: wp.array(dtype=int),
jnt_type: wp.array(dtype=int),
# Data in:
qvel_in: wp.array2d(dtype=float),
cdof_in: wp.array2d(dtype=wp.spatial_vector),
# In:
body_tree_: wp.array(dtype=int),
# Data out:
# Out:
Dcvel_out: wp.array3d(dtype=wp.spatial_vector),
Dcdof_dot_out: wp.array3d(dtype=wp.spatial_vector),
):
worldid, nodeid, k = wp.tid()
bodyid = body_tree_[nodeid]
dofid = body_dofadr[bodyid]
jntid = body_jntadr[bodyid]
jntnum = body_jntnum[bodyid]
pid = body_parentid[bodyid]

# Initialize from parent
cvel_k = Dcvel_out[worldid, pid, k]

if jntnum == 0:
Dcvel_out[worldid, bodyid, k] = cvel_k
return

qvel = qvel_in[worldid]
cdof = cdof_in[worldid]

for j in range(jntid, jntid + jntnum):
jnttype = jnt_type[j]

if jnttype == JointType.FREE:
# cvel += cdof * qvel
if k >= dofid and k < dofid + 3:
cvel_k += cdof[k]
elif k >= dofid + 3 and k < dofid + 6:
cvel_k += cdof[k]

# Dcdofdot = cross(Dcvel, cdof)
# cdof for free joint are computed on the fly?
# In smooth.py:
# cvel += cdof[dofid+0] * qvel[dofid+0] ...
# cdof_dot[dofid+3] = cross(cvel, cdof[dofid+3])
# Wait, for FREE joint, cdof are usually aligned with world axes initially?
# In smooth.py, cdof is read from d.cdof.
# For FREE joint, d.cdof contains the axes.

# Dcdofdot calculation mirrors smooth.py loop
# dofid+0..2 are translation
# (no cdof_dot update needed? smooth.py doesn't update cdof_dot for 0..2)
# dofid+3..5 are rotation.

if k < nv:
Dcdof_dot_out[worldid, dofid + 3, k] = math.motion_cross(cvel_k, cdof[dofid + 3])
Dcdof_dot_out[worldid, dofid + 4, k] = math.motion_cross(cvel_k, cdof[dofid + 4])
Dcdof_dot_out[worldid, dofid + 5, k] = math.motion_cross(cvel_k, cdof[dofid + 5])

dofid += 6
# Note: Free joint logic in smooth.py updates cvel using all 6 dofs.
# And BEFORE calculating cdof_dot for 3..5, it updates cvel with 0..2.
# And AFTER, it updates cvel with 3..5.
# I need to match this sequence!

elif jnttype == JointType.BALL:
if k < nv:
Dcdof_dot_out[worldid, dofid + 0, k] = math.motion_cross(cvel_k, cdof[dofid + 0])
Dcdof_dot_out[worldid, dofid + 1, k] = math.motion_cross(cvel_k, cdof[dofid + 1])
Dcdof_dot_out[worldid, dofid + 2, k] = math.motion_cross(cvel_k, cdof[dofid + 2])

if k >= dofid and k < dofid + 3:
cvel_k += cdof[k]

dofid += 3
else:
if k < nv:
Dcdof_dot_out[worldid, dofid, k] = math.motion_cross(cvel_k, cdof[dofid])

if k == dofid:
cvel_k += cdof[dofid]

dofid += 1

Dcvel_out[worldid, bodyid, k] = cvel_k


@wp.kernel
def _derivative_rne_forward_level(
# Model:
nv: int,
body_jntnum: wp.array(dtype=int),
body_dofadr: wp.array(dtype=int),
# Data in:
qvel_in: wp.array2d(dtype=float),
cinert_in: wp.array2d(dtype=vec10),
cvel_in: wp.array2d(dtype=wp.spatial_vector),
cdof_dot_in: wp.array2d(dtype=wp.spatial_vector),
# In:
body_tree_: wp.array(dtype=int),
Dcvel_in: wp.array3d(dtype=wp.spatial_vector),
Dcdof_dot_in: wp.array3d(dtype=wp.spatial_vector),
# Out:
Dcacc_out: wp.array3d(dtype=wp.spatial_vector),
Dcfrcbody_out: wp.array3d(dtype=wp.spatial_vector),
):
worldid, nodeid, k = wp.tid()
bodyid = body_tree_[nodeid]
dofid = body_dofadr[bodyid]
jntnum = body_jntnum[bodyid]

# Initialize Dcacc from parent
# (already done by recursive structure? No, need to copy or access parent)
# But here we are iterating bodies. Dcacc accumulation needs to be from parent.
# But we can't easily access parent index in Dcacc if it's not contiguous?
# Wait, _derivative_com_vel did "Initialize from parent".
# Here we also need `pid = body_parentid[bodyid]`.
# I'll need to pass body_parentid.

# Dcacc accumulation
# ...
# Placeholder for complex logic, I'll complete this in next step.
pass
Copy link
Collaborator

Choose a reason for hiding this comment

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

if this work will not be part of this pr, please add a 1 line todo that explains what should be implemented here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I decided to fully implement the RNE logic (rne_vel) in this PR instead of leaving a TODO. The forward and backward passes are now implemented in derivative.py and verified by test_rne_stress.



@wp.func
def _mul_inert_vec(inert: vec10, vec: wp.spatial_vector) -> wp.spatial_vector:
mass = inert[0]
h = wp.vec3(inert[1], inert[2], inert[3])
# I_3x3 from symmetric values (xx, yy, zz, xy, xz, yz)
# row 0: xx, xy, xz
# row 1: xy, yy, yz
# row 2: xz, yz, zz
I = wp.mat33(inert[4], inert[7], inert[8], inert[7], inert[5], inert[9], inert[8], inert[9], inert[6])

ang = wp.spatial_top(vec)
lin = wp.spatial_bottom(vec)

res_ang = I * ang + wp.cross(h, lin)
res_lin = mass * lin - wp.cross(h, ang)

return wp.spatial_vector(res_ang, res_lin)


@wp.kernel
def _derivative_rne_forward_level_fixed(
# Model:
nv: int,
body_parentid: wp.array(dtype=int),
body_dofnum: wp.array(dtype=int),
body_dofadr: wp.array(dtype=int),
# Data in:
qvel_in: wp.array2d(dtype=float),
cinert_in: wp.array2d(dtype=vec10),
cvel_in: wp.array2d(dtype=wp.spatial_vector),
cdof_dot_in: wp.array2d(dtype=wp.spatial_vector),
# In:
body_tree_: wp.array(dtype=int),
Dcvel_in: wp.array3d(dtype=wp.spatial_vector),
Dcdof_dot_in: wp.array3d(dtype=wp.spatial_vector),
# Out:
Dcacc_out: wp.array3d(dtype=wp.spatial_vector),
Dcfrcbody_out: wp.array3d(dtype=wp.spatial_vector),
):
worldid, nodeid, k = wp.tid()
bodyid = body_tree_[nodeid]
dofid = body_dofadr[bodyid]
dofnum = body_dofnum[bodyid]
pid = body_parentid[bodyid]

dcacc = Dcacc_out[worldid, pid, k]

qvel = qvel_in[worldid]

for j in range(dofid, dofid + dofnum):
# Term 1: cdof_dot * d(qvel)/dk
if j == k:
dcacc += cdof_dot_in[worldid, j]

# Term 2: Dcdofdot * qvel
dcdofdot = Dcdof_dot_in[worldid, j, k]
dcacc += dcdofdot * qvel[j]

Dcacc_out[worldid, bodyid, k] = dcacc

# Dcfrcbody calculation
cinert = cinert_in[worldid, bodyid]
cvel = cvel_in[worldid, bodyid]
dcvel = Dcvel_in[worldid, bodyid, k]

# term1 = cinert * dcacc
term1 = _mul_inert_vec(cinert, dcacc)

# term2 = D(cvel x (cinert * cvel))
# = dcvel x (cinert * cvel) + cvel x (cinert * dcvel)
cinert_cvel = _mul_inert_vec(cinert, cvel)
cinert_dcvel = _mul_inert_vec(cinert, dcvel)

term2 = math.motion_cross(dcvel, cinert_cvel) + math.motion_cross(cvel, cinert_dcvel)

Dcfrcbody_out[worldid, bodyid, k] = term1 + term2


@wp.kernel
def _derivative_rne_backward_level(
# Model:
body_parentid: wp.array(dtype=int),
# In:
body_tree_: wp.array(dtype=int),
# Out:
Dcfrcbody_out: wp.array3d(dtype=wp.spatial_vector),
):
worldid, nodeid, k = wp.tid()
bodyid = body_tree_[nodeid]
pid = body_parentid[bodyid]

if pid == 0 and bodyid == 0:
return # World body has no parent to add to

val = Dcfrcbody_out[worldid, bodyid, k]
wp.atomic_add(Dcfrcbody_out[worldid, pid], k, val)


@wp.kernel
def _derivative_rne_update_sparse(
# Model:
dof_bodyid: wp.array(dtype=int),
# Data in:
cdof_in: wp.array2d(dtype=wp.spatial_vector),
# In:
timestep: wp.array(dtype=float),
qMi: wp.array(dtype=int),
qMj: wp.array(dtype=int),
Dcfrcbody_in: wp.array3d(dtype=wp.spatial_vector),
# Out:
qDeriv_out: wp.array3d(dtype=float),
):
worldid, elemid = wp.tid()
dt = timestep[worldid % timestep.shape[0]]

i = qMi[elemid]
j = qMj[elemid]

# qDeriv[i, j] -= cdof[i] * Dcfrcbody[body(i), j]
# We want out = qM - dt * qDeriv_smooth - dt * qDeriv_rne
# out already contains qM - dt * qDeriv_smooth.
# So we assume term is qDeriv_rne contribution.
# We should subtract dt * term.

body_i = dof_bodyid[i]
dcfrc = Dcfrcbody_in[worldid, body_i, j]
term = wp.dot(cdof_in[worldid, i], dcfrc)

wp.atomic_add(qDeriv_out[worldid, 0], elemid, -dt * term)


@wp.kernel
def _derivative_rne_update_dense(
# Model:
dof_bodyid: wp.array(dtype=int),
# Data in:
cdof_in: wp.array2d(dtype=wp.spatial_vector),
# In:
timestep: wp.array(dtype=float),
Dcfrcbody_in: wp.array3d(dtype=wp.spatial_vector),
# Out:
qDeriv_out: wp.array3d(dtype=float),
):
worldid, i, j = wp.tid()
dt = timestep[worldid % timestep.shape[0]]

body_i = dof_bodyid[i]
dcfrc = Dcfrcbody_in[worldid, body_i, j]
term = wp.dot(cdof_in[worldid, i], dcfrc)

qDeriv_out[worldid, i, j] -= dt * term


@event_scope
def rne_vel(m: Model, d: Data, out: wp.array2d(dtype=float)): # out is qDeriv-like
# Temporary dense allocations
Dcvel = wp.zeros((d.nworld, m.nbody, m.nv), dtype=wp.spatial_vector)
Dcdof_dot = wp.zeros((d.nworld, m.nv, m.nv), dtype=wp.spatial_vector)
Dcacc = wp.zeros((d.nworld, m.nbody, m.nv), dtype=wp.spatial_vector)
Dcfrcbody = wp.zeros((d.nworld, m.nbody, m.nv), dtype=wp.spatial_vector)

# Compute Dcvel and Dcdofdot
wp.launch(
_derivative_com_vel_root,
dim=(d.nworld, 1, m.nv),
inputs=[Dcvel],
outputs=[],
)

for body_tree in m.body_tree:
wp.launch(
_derivative_com_vel_level,
dim=(d.nworld, body_tree.size, m.nv),
inputs=[m.nv, m.body_parentid, m.body_jntnum, m.body_jntadr, m.body_dofadr, m.jnt_type, d.qvel, d.cdof, body_tree],
outputs=[Dcvel, Dcdof_dot],
)

# Forward pass (Dcacc, Dcfrcbody)
for body_tree in m.body_tree:
wp.launch(
_derivative_rne_forward_level_fixed,
dim=(d.nworld, body_tree.size, m.nv),
inputs=[
m.nv,
m.body_parentid,
m.body_dofnum,
m.body_dofadr,
d.qvel,
d.cinert,
d.cvel,
d.cdof_dot,
body_tree,
Dcvel,
Dcdof_dot,
],
outputs=[Dcacc, Dcfrcbody],
)

# Backward pass (Accumulate Dcfrcbody)
for body_tree in reversed(m.body_tree):
wp.launch(
_derivative_rne_backward_level,
dim=(d.nworld, body_tree.size, m.nv),
inputs=[m.body_parentid, body_tree],
outputs=[Dcfrcbody], # In/Out
)

# Update qDeriv
if m.opt.is_sparse:
wp.launch(
_derivative_rne_update_sparse,
dim=(d.nworld, m.qM_fullm_i.size),
inputs=[m.dof_bodyid, d.cdof, m.opt.timestep, m.qM_fullm_i, m.qM_fullm_j, Dcfrcbody],
outputs=[out],
)
else:
wp.launch(
_derivative_rne_update_dense,
dim=(d.nworld, m.nv, m.nv),
inputs=[m.dof_bodyid, d.cdof, m.opt.timestep, Dcfrcbody],
outputs=[out],
)
Loading