Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
// Copyright 2022 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "engine/engine_derivative.h"
#include <stddef.h>
#include <mujoco/mjdata.h>
#include <mujoco/mjmodel.h>
#include <mujoco/mjsan.h> // IWYU pragma: keep
#include "engine/engine_core_constraint.h"
#include "engine/engine_core_smooth.h"
#include "engine/engine_core_util.h"
#include "engine/engine_crossplatform.h"
#include "engine/engine_inline.h"
#include "engine/engine_memory.h"
#include "engine/engine_passive.h"
#include "engine/engine_sleep.h"
#include "engine/engine_support.h"
#include "engine/engine_util_blas.h"
#include "engine/engine_util_errmem.h"
#include "engine/engine_util_misc.h"
#include "engine/engine_util_solve.h"
#include "engine/engine_util_spatial.h"
#include "engine/engine_util_sparse.h"
//------------------------- derivatives of spatial algebra -----------------------------------------
// derivatives of cross product, Da and Db are 3x3
static void mjd_cross(const mjtNum a[3], const mjtNum b[3],
mjtNum* restrict Da, mjtNum* restrict Db) {
// derivative w.r.t a
if (Da) {
mju_zero(Da, 9);
Da[1] = b[2];
Da[2] = -b[1];
Da[3] = -b[2];
Da[5] = b[0];
Da[6] = b[1];
Da[7] = -b[0];
}
// derivative w.r.t b
if (Db) {
mju_zero(Db, 9);
Db[1] = -a[2];
Db[2] = a[1];
Db[3] = a[2];
Db[5] = -a[0];
Db[6] = -a[1];
Db[7] = a[0];
}
}
// derivative of mju_crossMotion w.r.t velocity
static void mjd_crossMotion_vel(mjtNum D[36], const mjtNum v[6]) {
mju_zero(D, 36);
// res[0] = -vel[2]*v[1] + vel[1]*v[2]
D[0 + 2] = -v[1];
D[0 + 1] = v[2];
// res[1] = vel[2]*v[0] - vel[0]*v[2]
D[6 + 2] = v[0];
D[6 + 0] = -v[2];
// res[2] = -vel[1]*v[0] + vel[0]*v[1]
D[12 + 1] = -v[0];
D[12 + 0] = v[1];
// res[3] = -vel[2]*v[4] + vel[1]*v[5] - vel[5]*v[1] + vel[4]*v[2]
D[18 + 2] = -v[4];
D[18 + 1] = v[5];
D[18 + 5] = -v[1];
D[18 + 4] = v[2];
// res[4] = vel[2]*v[3] - vel[0]*v[5] + vel[5]*v[0] - vel[3]*v[2]
D[24 + 2] = v[3];
D[24 + 0] = -v[5];
D[24 + 5] = v[0];
D[24 + 3] = -v[2];
// res[5] = -vel[1]*v[3] + vel[0]*v[4] - vel[4]*v[0] + vel[3]*v[1]
D[30 + 1] = -v[3];
D[30 + 0] = v[4];
D[30 + 4] = -v[0];
D[30 + 3] = v[1];
}
// derivative of mju_crossForce w.r.t. velocity
static void mjd_crossForce_vel(mjtNum D[36], const mjtNum f[6]) {
mju_zero(D, 36);
// res[0] = -vel[2]*f[1] + vel[1]*f[2] - vel[5]*f[4] + vel[4]*f[5]
D[0 + 2] = -f[1];
D[0 + 1] = f[2];
D[0 + 5] = -f[4];
D[0 + 4] = f[5];
// res[1] = vel[2]*f[0] - vel[0]*f[2] + vel[5]*f[3] - vel[3]*f[5]
D[6 + 2] = f[0];
D[6 + 0] = -f[2];
D[6 + 5] = f[3];
D[6 + 3] = -f[5];
// res[2] = -vel[1]*f[0] + vel[0]*f[1] - vel[4]*f[3] + vel[3]*f[4]
D[12 + 1] = -f[0];
D[12 + 0] = f[1];
D[12 + 4] = -f[3];
D[12 + 3] = f[4];
// res[3] = -vel[2]*f[4] + vel[1]*f[5]
D[18 + 2] = -f[4];
D[18 + 1] = f[5];
// res[4] = vel[2]*f[3] - vel[0]*f[5]
D[24 + 2] = f[3];
D[24 + 0] = -f[5];
// res[5] = -vel[1]*f[3] + vel[0]*f[4]
D[30 + 1] = -f[3];
D[30 + 0] = f[4];
}
// derivative of mju_crossForce w.r.t. force
static void mjd_crossForce_frc(mjtNum D[36], const mjtNum vel[6]) {
mju_zero(D, 36);
// res[0] = -vel[2]*f[1] + vel[1]*f[2] - vel[5]*f[4] + vel[4]*f[5]
D[0 + 1] = -vel[2];
D[0 + 2] = vel[1];
D[0 + 4] = -vel[5];
D[0 + 5] = vel[4];
// res[1] = vel[2]*f[0] - vel[0]*f[2] + vel[5]*f[3] - vel[3]*f[5]
D[6 + 0] = vel[2];
D[6 + 2] = -vel[0];
D[6 + 3] = vel[5];
D[6 + 5] = -vel[3];
// res[2] = -vel[1]*f[0] + vel[0]*f[1] - vel[4]*f[3] + vel[3]*f[4]
D[12 + 0] = -vel[1];
D[12 + 1] = vel[0];
D[12 + 3] = -vel[4];
D[12 + 4] = vel[3];
// res[3] = -vel[2]*f[4] + vel[1]*f[5]
D[18 + 4] = -vel[2];
D[18 + 5] = vel[1];
// res[4] = vel[2]*f[3] - vel[0]*f[5]
D[24 + 3] = vel[2];
D[24 + 5] = -vel[0];
// res[5] = -vel[1]*f[3] + vel[0]*f[4]
D[30 + 3] = -vel[1];
D[30 + 4] = vel[0];
}
// derivative of mju_mulInertVec w.r.t vel
static void mjd_mulInertVec_vel(mjtNum D[36], const mjtNum i[10]) {
mju_zero(D, 36);
// res[0] = i[0]*v[0] + i[3]*v[1] + i[4]*v[2] - i[8]*v[4] + i[7]*v[5]
D[0 + 0] = i[0];
D[0 + 1] = i[3];
D[0 + 2] = i[4];
D[0 + 4] = -i[8];
D[0 + 5] = i[7];
// res[1] = i[3]*v[0] + i[1]*v[1] + i[5]*v[2] + i[8]*v[3] - i[6]*v[5]
D[6 + 0] = i[3];
D[6 + 1] = i[1];
D[6 + 2] = i[5];
D[6 + 3] = i[8];
D[6 + 5] = -i[6];
// res[2] = i[4]*v[0] + i[5]*v[1] + i[2]*v[2] - i[7]*v[3] + i[6]*v[4]
D[12 + 0] = i[4];
D[12 + 1] = i[5];
D[12 + 2] = i[2];
D[12 + 3] = -i[7];
D[12 + 4] = i[6];
// res[3] = i[8]*v[1] - i[7]*v[2] + i[9]*v[3]
D[18 + 1] = i[8];
D[18 + 2] = -i[7];
D[18 + 3] = i[9];
// res[4] = i[6]*v[2] - i[8]*v[0] + i[9]*v[4]
D[24 + 2] = i[6];
D[24 + 0] = -i[8];
D[24 + 4] = i[9];
// res[5] = i[7]*v[0] - i[6]*v[1] + i[9]*v[5]
D[30 + 0] = i[7];
D[30 + 1] = -i[6];
D[30 + 5] = i[9];
}
// derivative of mju_subQuat w.r.t inputs
void mjd_subQuat(const mjtNum qa[4], const mjtNum qb[4], mjtNum Da[9], mjtNum Db[9]) {
// no outputs, quick return
if (!Da && !Db) {
return;
}
// compute axis-angle quaternion difference
mjtNum axis[3];
mju_subQuat(axis, qa, qb);
// normalize axis, get half-angle
mjtNum half_angle = 0.5 * mju_normalize3(axis);
// identity
mjtNum Da_tmp[9] = {
1, 0, 0,
0, 1, 0,
0, 0, 1
};
// add term linear in cross product matrix K
mjtNum K[9] = {
0, -axis[2], axis[1],
axis[2], 0, -axis[0],
-axis[1], axis[0], 0
};
mju_addToScl(Da_tmp, K, half_angle, 9);
// add term linear in K * K
mjtNum KK[9];
mji_mulMatMat3(KK, K, K);
mjtNum coef = 1.0 - (half_angle < 6e-8 ? 1.0 : half_angle / mju_tan(half_angle));
mju_addToScl(Da_tmp, KK, coef, 9);
if (Da) {
mju_copy9(Da, Da_tmp);
}
if (Db) { // Db = -Da^T
mju_transpose(Db, Da_tmp, 3, 3);
mju_scl(Db, Db, -1.0, 9);
}
}
// derivative of mju_quatIntegrate w.r.t scaled velocity
// reference: https://arxiv.org/abs/1711.02508, Eq. 183
void mjd_quatIntegrate(const mjtNum vel[3], mjtNum scale,
mjtNum Dquat[9], mjtNum Dvel[9], mjtNum Dscale[3]) {
// scaled velocity
mjtNum s[3] = {scale*vel[0], scale*vel[1], scale*vel[2]};
// 3 basis matrices
mjtNum eye[9] = {
1, 0, 0,
0, 1, 0,
0, 0, 1
};
mjtNum cross[9] = {
0, s[2], -s[1],
-s[2], 0, s[0],
s[1], -s[0], 0
};
mjtNum outer[9] = {
s[0]*s[0], s[0]*s[1], s[0]*s[2],
s[1]*s[0], s[1]*s[1], s[1]*s[2],
s[2]*s[0], s[2]*s[1], s[2]*s[2]
};
// squared norm, norm of s
mjtNum xx = mju_dot3(s, s);
mjtNum x = mju_sqrt(xx);
// 4 coefficients: a=cos(x), b=sin(x)/x, c=(1-cos(x))/x^2, d=(x-sin(x))/x^3
mjtNum a = mju_cos(x);
mjtNum b, c, d;
// x is not small: use full expressions
if (mju_abs(x) > 1.0/32) {
b = mju_sin(x) / x;
c = (1.0 - a) / xx;
d = (1.0 - b) / xx;
}
// |x| <= 1/32: use 6th order Taylor expansion (Horner form)
else {
b = 1 + xx/6 * (xx/20 * (1 - xx/42) - 1);
c = (1 + xx/12 * (xx/30 * (1 - xx/56) - 1)) / 2;
d = (1 + xx/20 * (xx/42 * (1 - xx/72) - 1)) / 6;
}
// derivatives
mjtNum Dvel_[9];
for (int i=0; i < 9; i++) {
if (Dquat) Dquat[i] = a*eye[i] + b*cross[i] + c*outer[i];
if (Dvel || Dscale) Dvel_[i] = b*eye[i] + c*cross[i] + d*outer[i];
}
if (Dvel) mju_copy9(Dvel, Dvel_);
if (Dscale) mji_mulMatVec3(Dscale, Dvel_, vel);
}
//------------------------- dense derivatives of component functions -------------------------------
// no longer used, except in tests
// derivative of cvel, cdof_dot w.r.t qvel (dense version)
static void mjd_comVel_vel_dense(const mjModel* m, mjData* d, mjtNum* Dcvel, mjtNum* Dcdofdot) {
int nv = m->nv, nbody = m->nbody;
mjtNum mat[36];
// clear Dcvel
mju_zero(Dcvel, nbody*6*nv);
// forward pass over bodies: accumulate Dcvel, set Dcdofdot
for (int i=1; i < nbody; i++) {
// Dcvel = Dcvel_parent
mju_copy(Dcvel+i*6*nv, Dcvel+m->body_parentid[i]*6*nv, 6*nv);
// Dcvel += D(cdof * qvel), Dcdofdot = D(cvel x cdof)
for (int j=m->body_dofadr[i]; j < m->body_dofadr[i]+m->body_dofnum[i]; j++) {
switch ((mjtJoint) m->jnt_type[m->dof_jntid[j]]) {
case mjJNT_FREE:
// Dcdofdot = 0
mju_zero(Dcdofdot+j*6*nv, 18*nv);
// Dcvel += cdof * (D qvel)
for (int k=0; k < 6; k++) {
Dcvel[i*6*nv + k*nv + j+0] += d->cdof[(j+0)*6 + k];
Dcvel[i*6*nv + k*nv + j+1] += d->cdof[(j+1)*6 + k];
Dcvel[i*6*nv + k*nv + j+2] += d->cdof[(j+2)*6 + k];
}
// continue with rotations
j += 3;
mjFALLTHROUGH;
case mjJNT_BALL:
// Dcdofdot = D crossMotion(cvel, cdof)
for (int k=0; k < 3; k++) {
mjd_crossMotion_vel(mat, d->cdof+6*(j+k));
mju_mulMatMat(Dcdofdot+(j+k)*6*nv, mat, Dcvel+i*6*nv, 6, 6, nv);
}
// Dcvel += cdof * (D qvel)
for (int k=0; k < 6; k++) {
Dcvel[i*6*nv + k*nv + j+0] += d->cdof[(j+0)*6 + k];
Dcvel[i*6*nv + k*nv + j+1] += d->cdof[(j+1)*6 + k];
Dcvel[i*6*nv + k*nv + j+2] += d->cdof[(j+2)*6 + k];
}
// adjust for 3-dof joint
j += 2;
break;
default:
// Dcdofdot = D crossMotion(cvel, cdof) * Dcvel
mjd_crossMotion_vel(mat, d->cdof+6*j);
mju_mulMatMat(Dcdofdot+j*6*nv, mat, Dcvel+i*6*nv, 6, 6, nv);
// Dcvel += cdof * (D qvel)
for (int k=0; k < 6; k++) {
Dcvel[i*6*nv + k*nv + j] += d->cdof[j*6 + k];
}
}
}
}
}
// subtract (d qfrc_bias / d qvel) from qDeriv (dense version)
void mjd_rne_vel_dense(const mjModel* m, mjData* d) {
int nv = m->nv, nbody = m->nbody;
mjtNum mat[36], mat1[36], mat2[36], dmul[36], tmp[6];
mj_markStack(d);
mjtNum* Dcvel = mjSTACKALLOC(d, nbody*6*nv, mjtNum);
mjtNum* Dcdofdot = mjSTACKALLOC(d, nv*6*nv, mjtNum);
mjtNum* Dcacc = mjSTACKALLOC(d, nbody*6*nv, mjtNum);
mjtNum* Dcfrcbody = mjSTACKALLOC(d, nbody*6*nv, mjtNum);
mjtNum* row = mjSTACKALLOC(d, nv, mjtNum);
// compute Dcvel and Dcdofdot
mjd_comVel_vel_dense(m, d, Dcvel, Dcdofdot);
// clear Dcacc
mju_zero(Dcacc, nbody*6*nv);
// forward pass over bodies: accumulate Dcacc, set Dcfrcbody
for (int i=1; i < nbody; i++) {
// Dcacc = Dcacc_parent
mju_copy(Dcacc + i*6*nv, Dcacc + m->body_parentid[i]*6*nv, 6*nv);
// Dcacc += D(cdofdot * qvel)
for (int j=m->body_dofadr[i]; j < m->body_dofadr[i]+m->body_dofnum[i]; j++) {
// Dcacc += cdofdot * (D qvel)
for (int k=0; k < 6; k++) {
Dcacc[i*6*nv + k*nv + j] += d->cdof_dot[j*6 + k];
}
// Dcacc += (D cdofdot) * qvel
mju_addToScl(Dcacc+i*6*nv, Dcdofdot+j*6*nv, d->qvel[j], 6*nv);
}
//---------- Dcfrcbody = D(cinert * cacc + cvel x (cinert * cvel))
// Dcfrcbody = (D mul / D cacc) * Dcacc
mjd_mulInertVec_vel(dmul, d->cinert+10*i);
mju_mulMatMat(Dcfrcbody+i*6*nv, dmul, Dcacc+i*6*nv, 6, 6, nv);
// mat = (D cross / D cvel) + (D cross / D mul) * (D mul / D cvel)
mju_mulInertVec(tmp, d->cinert+10*i, d->cvel+i*6);
mjd_crossForce_vel(mat, tmp);
mjd_crossForce_frc(mat1, d->cvel+i*6);
mju_mulMatMat(mat2, mat1, dmul, 6, 6, 6);
mju_addTo(mat, mat2, 36);
// Dcfrcbody += mat * Dcvel (use body 0 as temp)
mju_mulMatMat(Dcfrcbody, mat, Dcvel+i*6*nv, 6, 6, nv);
mju_addTo(Dcfrcbody+i*6*nv, Dcfrcbody, 6*nv);
}
// clear world Dcfrcbody, for style
mju_zero(Dcfrcbody, 6*nv);
// backward pass over bodies: accumulate Dcfrcbody
for (int i=nbody-1; i > 0; i--) {
if (m->body_parentid[i]) {
mju_addTo(Dcfrcbody+m->body_parentid[i]*6*nv, Dcfrcbody+i*6*nv, 6*nv);
}
}
// qDeriv -= D(cdof * cfrc_body)
for (int i=0; i < nv; i++) {
for (int k=0; k < 6; k++) {
// compute D(cdof * cfrc_body), store in row
mju_scl(row, Dcfrcbody + (m->dof_bodyid[i]*6+k)*nv, d->cdof[i*6+k], nv);
// dense to sparse: qDeriv -= row
int end = m->D_rowadr[i] + m->D_rownnz[i];
for (int adr=m->D_rowadr[i]; adr < end; adr++) {
d->qDeriv[adr] -= row[m->D_colind[adr]];
}
}
}
mj_freeStack(d);
}
//------------------------- sparse derivatives of component functions ------------------------------
// internal sparse format: dense body/dof x sparse dof x 6 (inner size is 6)
// copy sparse B-row from parent, shared ancestors only
static void copyFromParent(const mjModel* m, mjData* d, mjtNum* mat, int n) {
// return if this is world or parent is world
if (n == 0 || m->body_weldid[m->body_parentid[n]] == 0) {
return;
}
// count dofs in ancestors
int ndof = 0;
int np = m->body_weldid[m->body_parentid[n]];
while (np > 0) {
// add self dofs
ndof += m->body_dofnum[np];
// advance to parent
np = m->body_weldid[m->body_parentid[np]];
}
// copy: guaranteed to be at beginning of sparse array, due to sorting
mju_copy(mat + 6*m->B_rowadr[n], mat + 6*m->B_rowadr[m->body_parentid[n]], 6*ndof);
}
// add sparse B-row to parent, all overlapping nonzeros
static void addToParent(const mjModel* m, mjData* d, mjtNum* mat, int n) {
// return if this is world or parent is world
if (n == 0 || m->body_weldid[m->body_parentid[n]] == 0) {
return;
}
// find matching nonzeros
int np = m->body_parentid[n];
int i = 0, ip = 0;
while (i < m->B_rownnz[n] && ip < m->B_rownnz[np]) {
// columns match
if (m->B_colind[m->B_rowadr[n] + i] == m->B_colind[m->B_rowadr[np] + ip]) {
mju_addTo(mat + 6*(m->B_rowadr[np] + ip), mat + 6*(m->B_rowadr[n] + i), 6);
// advance both
i++;
ip++;
}
// mismatch columns: advance parent
else if (m->B_colind[m->B_rowadr[n] + i] > m->B_colind[m->B_rowadr[np] + ip]) {
ip++;
}
// child nonzeroes must be subset of parent; SHOULD NOT OCCUR
else {
mjERROR("child nonzeroes must be subset of parent");
}
}
}
// derivative of cvel, cdof_dot w.r.t qvel
static void mjd_comVel_vel(const mjModel* m, mjData* d, mjtNum* Dcvel, mjtNum* Dcdofdot) {
int nv = m->nv, nM = m->nM;
int sleep_filter = mjENABLED(mjENBL_SLEEP) && d->nbody_awake < m->nbody;
int nbody = sleep_filter ? d->nbody_awake : m->nbody;
int* Badr = m->B_rowadr, * Dadr = m->D_rowadr;
mjtNum mat[36], matT[36]; // 6x6 matrices
// forward pass over bodies: accumulate Dcvel, set Dcdofdot
for (int b=1; b < nbody; b++) {
int i = sleep_filter ? d->body_awake_ind[b] : b;
// Dcvel = Dcvel_parent
copyFromParent(m, d, Dcvel, i);
// process all dofs of this body
int doflast = m->body_dofadr[i] + m->body_dofnum[i];
for (int j = m->body_dofadr[i]; j < doflast; j++) {
// number of dof ancestors of dof j
int Jadr = (j < nv - 1 ? m->dof_Madr[j + 1] : nM) - (m->dof_Madr[j] + 1);
// Dcvel += D(cdof * qvel), Dcdofdot = D(cvel x cdof)
switch ((mjtJoint) m->jnt_type[m->dof_jntid[j]]) {
case mjJNT_FREE:
// Dcdofdot = 0 (already cleared)
// Dcvel += cdof * D(qvel)
mju_addTo(Dcvel + 6*(Badr[i] + Jadr + 0), d->cdof + 6*(j + 0), 6);
mju_addTo(Dcvel + 6*(Badr[i] + Jadr + 1), d->cdof + 6*(j + 1), 6);
mju_addTo(Dcvel + 6*(Badr[i] + Jadr + 2), d->cdof + 6*(j + 2), 6);
// continue with rotations
j += 3;
Jadr += 3;
mjFALLTHROUGH;
case mjJNT_BALL:
// Dcdofdot = Dcvel * D crossMotion(cvel, cdof)
for (int dj=0; dj < 3; dj++) {
mjd_crossMotion_vel(mat, d->cdof + 6 * (j + dj));
mju_transpose(matT, mat, 6, 6);
mju_mulMatMat(Dcdofdot + 6*Dadr[j + dj], Dcvel + 6*Badr[i], matT, Jadr + dj, 6, 6);
}
// Dcvel += cdof * (D qvel)
mju_addTo(Dcvel + 6*(Badr[i] + Jadr + 0), d->cdof + 6*(j + 0), 6);
mju_addTo(Dcvel + 6*(Badr[i] + Jadr + 1), d->cdof + 6*(j + 1), 6);
mju_addTo(Dcvel + 6*(Badr[i] + Jadr + 2), d->cdof + 6*(j + 2), 6);
// adjust for 3-dof joint
j += 2;
break;
case mjJNT_HINGE:
case mjJNT_SLIDE:
// Dcdofdot = D crossMotion(cvel, cdof) * Dcvel
mjd_crossMotion_vel(mat, d->cdof + 6 * j);
mju_transpose(matT, mat, 6, 6);
mju_mulMatMat(Dcdofdot + 6*Dadr[j], Dcvel + 6*Badr[i], matT, Jadr, 6, 6);
// Dcvel += cdof * (D qvel)
mju_addTo(Dcvel + 6*(Badr[i] + Jadr), d->cdof + 6*j, 6);
break;
default:
mjERROR("unknown joint type");
}
}
}
}
// subtract d qfrc_bias / d qvel from qDeriv
static void mjd_rne_vel(const mjModel* m, mjData* d) {
int nM = m->nM;
int sleep_filter = mjENABLED(mjENBL_SLEEP) && d->nbody_awake < m->nbody;
int nbody = sleep_filter ? d->nbody_awake : m->nbody;
int nparent = sleep_filter ? d->nparent_awake : m->nbody;
int mnv = m->nv;
int nv = sleep_filter ? d->nv_awake : mnv;
const int* Badr = m->B_rowadr;
const int* Dadr = m->D_rowadr;
const int* Bnnz = m->B_rownnz;
mjtNum mat[36], mat1[36], mat2[36], dmul[36], tmp[6];
mj_markStack(d);
mjtNum* Dcdofdot = mjSTACKALLOC(d, 6*m->nD, mjtNum);
mjtNum* Dcvel = mjSTACKALLOC(d, 6*m->nB, mjtNum);
mjtNum* Dcacc = mjSTACKALLOC(d, 6*m->nB, mjtNum);
mjtNum* Dcfrcbody = mjSTACKALLOC(d, 6*m->nB, mjtNum);
mjtNum* row = mjSTACKALLOC(d, m->nv, mjtNum);
// clear
if (!sleep_filter) {
mju_zero(Dcdofdot, 6*m->nD);
mju_zero(Dcvel, 6*m->nB);
mju_zero(Dcacc, 6*m->nB);
mju_zero(Dcfrcbody, 6*m->nB);
} else {
for (int i = 0; i < nv; i++) {
int dof = d->dof_awake_ind[i];
mju_zero(Dcdofdot + 6*m->D_rowadr[dof], 6*m->D_rownnz[dof]);
}
for (int i = 0; i < nbody; i++) {
int body = d->body_awake_ind[i];
int adr = 6*m->B_rowadr[body];
int nnz = 6*m->B_rownnz[body];
mju_zero(Dcvel + adr, nnz);
mju_zero(Dcacc + adr, nnz);
mju_zero(Dcfrcbody + adr, nnz);
}
}
// compute Dcvel and Dcdofdot
mjd_comVel_vel(m, d, Dcvel, Dcdofdot);
// forward pass over bodies: accumulate Dcacc, set Dcfrcbody
for (int b=1; b < nbody; b++) {
int i = sleep_filter ? d->body_awake_ind[b] : b;
// Dcacc = Dcacc_parent
copyFromParent(m, d, Dcacc, i);
// process all dofs of this body
int doflast = m->body_dofadr[i] + m->body_dofnum[i];
for (int j=m->body_dofadr[i]; j < doflast; j++) {
// number of dof ancestors of dof j
int Jadr = (j < mnv - 1 ? m->dof_Madr[j + 1] : nM) - (m->dof_Madr[j] + 1);
// Dcacc += cdofdot * (D qvel)
mju_addTo(Dcacc + 6*(Badr[i] + Jadr), d->cdof_dot + 6*j, 6);
// Dcacc += (D cdofdot) * qvel
// Dcacc[row i] and Dcdofdot[row j] have identical sparsity
mju_addToScl(Dcacc + 6*Badr[i], Dcdofdot + 6*Dadr[j], d->qvel[j], 6*Bnnz[i]);
}
//---------- Dcfrcbody = D(cinert * cacc + cvel x (cinert * cvel))
// Dcfrcbody = (D mul / D cacc) * Dcacc
mjd_mulInertVec_vel(dmul, d->cinert + 10*i);
mju_transpose(mat1, dmul, 6, 6);
mju_mulMatMat(Dcfrcbody + 6*Badr[i], Dcacc + 6*Badr[i], mat1, Bnnz[i], 6, 6);
// mat = (D cross / D cvel) + (D cross / D mul) * (D mul / D cvel)
mju_mulInertVec(tmp, d->cinert + 10*i, d->cvel + i*6);
mjd_crossForce_vel(mat, tmp);
mjd_crossForce_frc(mat1, d->cvel + i*6);
mju_mulMatMat(mat2, mat1, dmul, 6, 6, 6);
mju_addTo(mat, mat2, 36);
// Dcfrcbody += mat * Dcvel (use worldbody as temp)
mju_transpose(mat1, mat, 6, 6);
mju_mulMatMat(Dcfrcbody, Dcvel + 6*Badr[i], mat1, Bnnz[i], 6, 6);
mju_addTo(Dcfrcbody + 6*Badr[i], Dcfrcbody, 6*Bnnz[i]);
}
// clear worldbody Dcfrcbody
mju_zero(Dcfrcbody, 6*Bnnz[0]);
// backward pass over bodies: accumulate Dcfrcbody
for (int b=nparent-1; b > 0; b--) {
int i = sleep_filter ? d->parent_awake_ind[b] : b;
addToParent(m, d, Dcfrcbody, i);
}
// process all dofs, update qDeriv
for (int v=0; v < nv; v++) {
int j = sleep_filter ? d->dof_awake_ind[v] : v;
// get body index
int i = m->dof_bodyid[j];
// qDeriv -= D(cdof * cfrc_body)
mju_mulMatVec(row, Dcfrcbody + 6*Badr[i], d->cdof + 6*j, Bnnz[i], 6);
mju_subFrom(d->qDeriv + Dadr[j], row, Bnnz[i]);
}
mj_freeStack(d);
}
// 3x3 sub-blocks of (d qfrc_bias / d qvel) for a free rigid subtree
// outputs the two 3x3 blocks lin and rot such that the rotational columns
// of the full 6x6 bias Jacobian B are [-mass*lin; rot] (linear columns are zero)
//
// derivation: let R = xmat, s = subtree_com - xpos, w = R*qvel[rot], and Iw be the
// composite world inertia about the subtree CoM. with qacc = 0,
// the CoM acceleration is w x (w x s) and the world bias force/torque at the CoM are
// f = mass * w x (w x s), tau = w x Iw*w
// projected onto the joint coordinates: bias = [f; R'*(s x f + tau)]. differentiating
// w.r.t. the rotational dofs (through w = R*qvel[rot]), with K = [w x s]_x + [w]_x [s]_x:
// d f / d w = -mass * K => lin = K * R
// d tau / d w = [w]_x Iw - [Iw*w]_x => rot = R' * (-mass*[s]_x K + d tau/d w) * R
static void freeBias_vel_blocks(mjtNum mass, const mjtNum R[9], const mjtNum Iw[9],
const mjtNum s[3], const mjtNum qvel_rot[3],
mjtNum lin[9], mjtNum rot[9]) {
// world-frame angular velocity
mjtNum w[3];
mji_mulMatVec3(w, R, qvel_rot);
// intermediate vectors: ws = w x s (CoM offset velocity), Iww = Iw * w (angular momentum)
mjtNum ws[3], Iww[3];
mji_cross(ws, w, s);
mji_mulMatVec3(Iww, Iw, w);
// K = [w x s]_x + [w]_x [s]_x = s w^T - (w . s) I + [ws]_x
mjtNum w_dot_s = w[0]*s[0] + w[1]*s[1] + w[2]*s[2];
mjtNum K[9];
K[0] = s[0]*w[0] - w_dot_s;
K[1] = s[0]*w[1] - ws[2];
K[2] = s[0]*w[2] + ws[1];
K[3] = s[1]*w[0] + ws[2];
K[4] = s[1]*w[1] - w_dot_s;
K[5] = s[1]*w[2] - ws[0];
K[6] = s[2]*w[0] - ws[1];
K[7] = s[2]*w[1] + ws[0];
K[8] = s[2]*w[2] - w_dot_s;
// lin = K * R
mji_mulMatMat3(lin, K, R);
// C = -mass * [s]_x K + [w]_x Iw - [Iww]_x, column by column
// the last term (-[Iww]_x) is the negated cross-product matrix, added via ternaries
mjtNum C[9];
for (int c=0; c < 3; c++) {
mjtNum s_x_K_row0 = s[1]*K[6+c] - s[2]*K[3+c];
mjtNum s_x_K_row1 = s[2]*K[c] - s[0]*K[6+c];
mjtNum s_x_K_row2 = s[0]*K[3+c] - s[1]*K[c];
mjtNum w_x_Iw_row0 = w[1]*Iw[6+c] - w[2]*Iw[3+c];
mjtNum w_x_Iw_row1 = w[2]*Iw[c] - w[0]*Iw[6+c];
mjtNum w_x_Iw_row2 = w[0]*Iw[3+c] - w[1]*Iw[c];
C[c] = -mass * s_x_K_row0 + w_x_Iw_row0 + (c == 1 ? Iww[2] : (c == 2 ? -Iww[1] : 0));
C[3 + c] = -mass * s_x_K_row1 + w_x_Iw_row1 + (c == 0 ? -Iww[2] : (c == 2 ? Iww[0] : 0));
C[6 + c] = -mass * s_x_K_row2 + w_x_Iw_row2 + (c == 0 ? Iww[1] : (c == 1 ? -Iww[0] : 0));
}
// rot = R^T * C * R
mjtNum tmp[9];
mji_mulMatTMat3(tmp, R, C);
mji_mulMatMat3(rot, tmp, R);
}
// 6x6 block B = d qfrc_bias / d qvel for a free rigid subtree
// assembles the full 6x6 from the 3x3 sub-blocks computed by freeBias_vel_blocks
// rows/cols ordered like the free joint dofs: [linear(3); rotational(3)]
// linear columns are zero: the bias force does not depend on linear velocity
// requires valid d->crb, computed by mj_crb
void mjd_freeBias_vel(const mjModel* m, const mjData* d, int jnt, mjtNum B[36]) {
int body = m->jnt_bodyid[jnt];
int adr = m->jnt_dofadr[jnt];
const mjtNum* crb = d->crb + 10*body;
mjtNum mass = crb[9];
// composite world inertia about the subtree CoM, already accumulated by mj_crb
mjtNum Iw[9] = {crb[0], crb[3], crb[4],
crb[3], crb[1], crb[5],
crb[4], crb[5], crb[2]};
mjtNum s[3], lin[9], rot[9];
mji_sub3(s, d->subtree_com + 3*body, d->xpos + 3*body);
freeBias_vel_blocks(mass, d->xmat + 9*body, Iw, s, d->qvel + adr + 3, lin, rot);
mju_zero(B, 36);
for (int r=0; r < 3; r++) {
for (int c=0; c < 3; c++) {
B[6*(r+0) + 3+c] = -mass * lin[3*r+c];
B[6*(r+3) + 3+c] = rot[3*r+c];
}
}
}
// return 1 if body is the root of a free rigid subtree (single free joint, fixed descendants)
mjtBool mj_isFreeBody(const mjModel* m, int body) {
// must have exactly one joint, of free type
if (m->body_jntnum[body] != 1 || m->jnt_type[m->body_jntadr[body]] != mjJNT_FREE) {
return false;
}
int adr = m->jnt_dofadr[m->body_jntadr[body]];
// descendants must not add degrees of freedom
return m->tree_dofnum[m->dof_treeid[adr]] == 6;
}
// 6x6 block A = M - h * (d qfrc_smooth / d qvel) for the free joint of a rigid subtree
// returns 1 and writes A if jnt is the free joint of an awake rigid subtree, 0 otherwise
// requires valid d->qDeriv rows for the block, computed with flg_bias = 0; the bias
// derivative excluded from qDeriv is added here via mjd_freeBias_vel
int mjd_freeMhat(const mjModel* m, const mjData* d, int jnt, mjtNum h, mjtNum A[36],
int flg_discrete) {
int body = m->jnt_bodyid[jnt];
int adr = m->jnt_dofadr[jnt];
// must be a free rigid subtree, awake
if (!mj_isFreeBody(m, body) || !d->tree_awake[m->dof_treeid[adr]]) {
return 0;
}
// A = M block (gather from sparse lower triangle)
mju_zero(A, 36);
for (int r=0; r < 6; r++) {
int rowadr = m->M_rowadr[adr+r];
int rownnz = m->M_rownnz[adr+r];
for (int k=0; k < rownnz; k++) {
int c = m->M_colind[rowadr+k] - adr;
A[6*r+c] = A[6*c+r] = d->M[rowadr+k];
}
}
// discrete: the metric block is M + diag + fluid, assembled from the efm arrays
// (qDeriv is not computed under discrete)
if (flg_discrete) {
// per-dof diagonal metric terms
if (d->efm_diag) {
for (int r=0; r < 6; r++) {
A[6*r+r] += d->efm_diag[adr+r];
}
}
// fluid drag blocks in this joint's rows of M's pattern
if (d->efm_fluid) {
for (int r=0; r < 6; r++) {
int rowadr = m->M_rowadr[adr+r];
int rownnz = m->M_rownnz[adr+r];
for (int k=0; k < rownnz; k++) {
int c = m->M_colind[rowadr+k] - adr;
A[6*r+c] += d->efm_fluid[rowadr+k];
if (c != r) {
A[6*c+r] += d->efm_fluid[rowadr+k];
}
}
}
}
}
// implicit-family: A -= h * qDeriv block (actuator and passive derivatives)
else {
for (int r=0; r < 6; r++) {
int rowadr = m->D_rowadr[adr+r];
int rownnz = m->D_rownnz[adr+r];
for (int k=0; k < rownnz; k++) {
int c = m->D_colind[rowadr+k] - adr;
A[6*r+c] -= h * d->qDeriv[rowadr+k];
}
}
}
// A -= h * d(qfrc_smooth)/d(qvel) for the bias term missing from qDeriv;
// qfrc_smooth includes -qfrc_bias, so subtracting its derivative adds +h*B
mjtNum B[36];
mjd_freeBias_vel(m, d, jnt, B);
mju_addToScl(A, B, h, 36);
return 1;
}
// can this free rigid subtree take the local gyroscopic treatment under discrete:
// its rows of the solve must be decoupled -- no constraint Jacobian support, no flex
// CSR row, no tendon or actuator metric terms on its 6 dofs. Structural: reads the
// constraint Jacobian, valid from mj_makeConstraint on
int mjd_freeGyroPossible(const mjModel* m, const mjData* d, int jnt) {
int adr = m->jnt_dofadr[jnt];
if (m->jnt_type[jnt] != mjJNT_FREE || !mj_isFreeBody(m, m->jnt_bodyid[jnt])) {
return 0;
}
for (int r=0; r < 6; r++) {
if (d->nefmK && d->efm_K_rownnz[adr+r]) {
return 0;
}
}
// Matrix-free flex attachments can couple this free body without any CSR or constraint row.
int tree = m->body_treeid[m->jnt_bodyid[jnt]];
for (int f=0; f < m->nflex; f++) {
if (!mj_effFlexStiffPossible(m, f)) continue;
const int* bodies = m->flex_interp[f] ? m->flex_nodebodyid : m->flex_vertbodyid;
int vertadr = m->flex_interp[f] ? m->flex_nodeadr[f] : m->flex_vertadr[f];
int num = m->flex_interp[f] ? m->flex_nodenum[f] : m->flex_vertnum[f];
for (int v=vertadr; v < vertadr+num; v++) {
if (m->body_treeid[bodies[v]] == tree) return 0;
}
}
// constraint Jacobian support on any of the 6 dofs couples them to the solve
int nefc = d->nefc;
if (mj_isSparse(m)) {
for (int i=0; i < nefc; i++) {
int end = d->efc_J_rowadr[i] + d->efc_J_rownnz[i];
for (int j=d->efc_J_rowadr[i]; j < end; j++) {
int c = d->efc_J_colind[j];
if (c >= adr+6) {
break;
}
if (c >= adr) {
return 0;
}
}
}
} else {
int nv = m->nv;
for (int i=0; i < nefc; i++) {
const mjtNum* row = d->efc_J + nv*i + adr;
for (int r=0; r < 6; r++) {
if (row[r]) {
return 0;
}
}
}
}
// any tendon with metric terms touching these dofs couples them to other trees
for (int t=0; t < d->nefmT; t++) {
if (!d->efm_ts[t]) {
continue;
}
int i = d->efm_tid[t];
int end = m->ten_J_rowadr[i] + m->ten_J_rownnz[i];
for (int j=m->ten_J_rowadr[i]; j < end; j++) {
int c = m->ten_J_colind[j];
if (c >= adr && c < adr+6) {
return 0;
}
}
}
for (int t=0; t < d->nefmA; t++) {
int i = d->efm_aid[t];
int oadr = m->actuator_outadr[i];
for (int k=0; k < m->actuator_outnum[i]; k++) {
int r = oadr + k;
int end = d->moment_rowadr[r] + d->moment_rownnz[r];
for (int j=d->moment_rowadr[r]; j < end; j++) {
int c = d->moment_colind[j];
if (c >= adr && c < adr+6) {
return 0;
}
}
}
}
return 1;
}
//--------------------- utility functions for (d force / d vel) Jacobians --------------------------
// add J'*B*J to res, in qDeriv's sparsity layout
static void addJTBJ(const mjModel* m, mjData* d, mjtNum* res, const mjtNum* J,
const mjtNum* B, int n) {
int nv = m->nv;
// allocate dense row
mj_markStack(d);
mjtNum* row = mjSTACKALLOC(d, nv, mjtNum);
// process non-zero elements of B
for (int i=0; i < n; i++) {
for (int j=0; j < n; j++) {
if (!B[i*n+j]) {
continue;
}
// process non-zero elements of J(i,:)
for (int k=0; k < nv; k++) {
if (J[i*nv+k]) {
// row = J(i,k)*B(i,j)*J(j,:)
mju_scl(row, J+j*nv, J[i*nv+k] * B[i*n+j], nv);
// add row to res(k,:)
int rownnz_k = m->D_rownnz[k];
for (int s=0; s < rownnz_k; s++) {
int adr = m->D_rowadr[k] + s;
res[adr] += row[m->D_colind[adr]];
}
}
}
}
}
mj_freeStack(d);
}
// add J'*B*J to res (in qDeriv's sparsity layout), sparse version
static void addJTBJSparse(
const mjModel* m, mjData* d, mjtNum* res, const mjtNum* J,
const mjtNum* B, int n, int offset,
const int* J_rownnz, const int* J_rowadr, const int* J_colind) {
// compute res(k,p) += sum_{i,j} ( J(i,k)*B(i,j)*J(j,p) )
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (!B[i*n+j]) {
continue;
}
// loop over non-zero elements of J(i,:)
int nnz_i = J_rownnz[offset+i];
int adr_i = J_rowadr[offset+i];
int nnz_j = J_rownnz[offset+j];
int adr_j = J_rowadr[offset+j];
for (int k = 0; k < nnz_i; k++) {
int ik = adr_i + k;
int colik = J_colind[ik];
// res(k,:) += J(j,:) * J(i,k)*B(i,j)
mju_addToSclSparseInc(res + m->D_rowadr[colik], J + adr_j,
m->D_rownnz[colik], m->D_colind + m->D_rowadr[colik],
nnz_j, J_colind + adr_j,
J[ik]*B[i*n+j]);
}
}
}
}
//----------------------------- derivatives of actuator forces -------------------------------------
// derivative of mju_muscleGainLength w.r.t length
static mjtNum mjd_muscleGainLength(mjtNum length, mjtNum lmin, mjtNum lmax) {
if (lmin <= length && length <= lmax) {
// mid-ranges (maximum is at 1.0)
mjtNum a = 0.5*(lmin+1);
mjtNum b = 0.5*(1+lmax);
if (length <= a) {
// FL = 0.5*x*x
mjtNum x = (length-lmin) / mju_max(mjMINVAL, a-lmin);
return x / mju_max(mjMINVAL, a-lmin);
} else if (length <= 1) {
// FL = 1 - 0.5*x*x, x decreasing in length
mjtNum x = (1-length) / mju_max(mjMINVAL, 1-a);
return x / mju_max(mjMINVAL, 1-a);
} else if (length <= b) {
// FL = 1 - 0.5*x*x
mjtNum x = (length-1) / mju_max(mjMINVAL, b-1);
return -x / mju_max(mjMINVAL, b-1);
} else {
// FL = 0.5*x*x, x decreasing in length
mjtNum x = (lmax-length) / mju_max(mjMINVAL, lmax-b);
return -x / mju_max(mjMINVAL, lmax-b);
}
}
return 0;
}
// derivative of mju_muscleGain w.r.t length
static mjtNum mjd_muscleGain_len(mjtNum len, mjtNum vel, const mjtNum lengthrange[2], mjtNum acc0,
const mjtNum prm[9]) {
// unpack parameters
mjtNum range[2] = {prm[0], prm[1]};
mjtNum force = prm[2];
mjtNum scale = prm[3];
mjtNum lmin = prm[4];
mjtNum lmax = prm[5];
mjtNum vmax = prm[6];
mjtNum fvmax = prm[8];
// scale force if negative
if (force < 0) {
force = scale / mju_max(mjMINVAL, acc0);
}
// optimum length
mjtNum L0 = (lengthrange[1]-lengthrange[0]) / mju_max(mjMINVAL, range[1]-range[0]);
// normalized length and velocity
mjtNum L = range[0] + (len-lengthrange[0]) / mju_max(mjMINVAL, L0);
mjtNum V = vel / mju_max(mjMINVAL, L0*vmax);
// length curve derivative
mjtNum dFL = mjd_muscleGainLength(L, lmin, lmax);
// velocity curve
mjtNum FV;
mjtNum y = fvmax-1;
if (V <= -1) {
FV = 0;
} else if (V <= 0) {
FV = (V+1)*(V+1);
} else if (V <= y) {
FV = fvmax - (y-V)*(y-V) / mju_max(mjMINVAL, y);
} else {
FV = fvmax;
}
// compute dFL*FV and scale, make it negative
return -force*dFL*FV/mju_max(mjMINVAL, L0);
}
// derivative of mju_muscleBias w.r.t length
static mjtNum mjd_muscleBias_len(mjtNum len, const mjtNum lengthrange[2], mjtNum acc0,
const mjtNum prm[9]) {
// unpack parameters
mjtNum range[2] = {prm[0], prm[1]};
mjtNum force = prm[2];
mjtNum scale = prm[3];
mjtNum lmax = prm[5];
mjtNum fpmax = prm[7];
// scale force if negative
if (force < 0) {
force = scale / mju_max(mjMINVAL, acc0);
}
// optimum length
mjtNum L0 = (lengthrange[1]-lengthrange[0]) / mju_max(mjMINVAL, range[1]-range[0]);
// normalized length
mjtNum L = range[0] + (len-lengthrange[0]) / mju_max(mjMINVAL, L0);
// derivative of the passive curve: half-quadratic to (L0+lmax)/2, linear beyond
mjtNum b = 0.5*(1+lmax);
mjtNum dFP;
if (L <= 1) {
dFP = 0;
} else if (L <= b) {
// FP = 0.5*x*x
mjtNum x = (L-1) / mju_max(mjMINVAL, b-1);
dFP = x / mju_max(mjMINVAL, b-1);
} else {
// FP = 0.5 + x
dFP = 1 / mju_max(mjMINVAL, b-1);
}
// scale, make it negative
return -force*fpmax*dFP/mju_max(mjMINVAL, L0);
}
// derivative of mju_muscleGain w.r.t velocity
static mjtNum mjd_muscleGain_vel(mjtNum len, mjtNum vel, const mjtNum lengthrange[2], mjtNum acc0,
const mjtNum prm[9]) {
// unpack parameters
mjtNum range[2] = {prm[0], prm[1]};
mjtNum force = prm[2];
mjtNum scale = prm[3];
mjtNum lmin = prm[4];
mjtNum lmax = prm[5];
mjtNum vmax = prm[6];
mjtNum fvmax = prm[8];
// scale force if negative
if (force < 0) {
force = scale / mju_max(mjMINVAL, acc0);
}
// optimum length
mjtNum L0 = (lengthrange[1]-lengthrange[0]) / mju_max(mjMINVAL, range[1]-range[0]);
// normalized length and velocity
mjtNum L = range[0] + (len-lengthrange[0]) / mju_max(mjMINVAL, L0);
mjtNum V = vel / mju_max(mjMINVAL, L0*vmax);
// length curve
mjtNum FL = mju_muscleGainLength(L, lmin, lmax);
// velocity curve
mjtNum dFV;
mjtNum y = fvmax-1;
if (V <= -1) {
// FV = 0
dFV = 0;
} else if (V <= 0) {
// FV = (V+1)*(V+1)
dFV = 2*V + 2;
} else if (V <= y) {
// FV = fvmax - (y-V)*(y-V) / mju_max(mjMINVAL, y)
dFV = (-2*V + 2*y) / mju_max(mjMINVAL, y);
} else {
// FV = fvmax
dFV = 0;
}
// compute FVL and scale, make it negative
return -force*FL*dFV/mju_max(mjMINVAL, L0*vmax);
}
//--------------------- utility functions for (d force / d pos) * vec Jacobians --------------------
// add J'*B*J*vec to res, sparse version
static void addJTBJ_mulSparse(const mjModel* m, mjData* d, mjtNum* res, const mjtNum* vec,
const int* J_rownnz, const int* J_rowadr, const int* J_colind,
const mjtNum* J, const mjtNum* B, int n) {
// allocate temp vectors
mj_markStack(d);
mjtNum* Jv = mjSTACKALLOC(d, n, mjtNum);
mjtNum* BJv = mjSTACKALLOC(d, n, mjtNum);
// Jv = J*vec (Sparse Matrix-Vector Multiplication)
mju_zero(Jv, n);
for (int i=0; i < n; i++) {
int nnz = J_rownnz[i];
int adr = J_rowadr[i];
for (int k=0; k < nnz; k++) {
Jv[i] += J[adr + k] * vec[J_colind[adr + k]];
}
}
// BJv = B*Jv (Dense Matrix-Vector Multiplication)
mju_mulMatVec(BJv, B, Jv, n, n);
// res += J'*BJv (Sparse Transpose Matrix-Vector Multiplication)
for (int i=0; i < n; i++) {
int nnz = J_rownnz[i];
int adr = J_rowadr[i];
mjtNum val = BJv[i];
for (int k=0; k < nnz; k++) {
res[J_colind[adr + k]] += J[adr + k] * val;
}
}
mj_freeStack(d);
}
// shared kernel for flex interpolation derivatives, scale = s1 + s2*damping
// res: output vector (res += J'*K*J*vec), or NULL for cache-only mode
// vec: input vector, or NULL for cache-only mode
// K_rot_cache: if non-NULL, use pre-cached K_rot values instead of computing them
// K_rot_out: if non-NULL and K_rot_cache is NULL, store computed K_rot values here
static void mjd_flexInterp_kernel(const mjModel* m, mjData* d,
mjtNum* res, const mjtNum* vec, mjtNum s1, mjtNum s2,
const mjtNum* K_rot_cache, mjtNum* K_rot_out) {
int nv = m->nv;
// compute upper bounds across all interpolated flexes
int max_nodenum = 0;
int max_npe = 0; // max nodes per element (3D cell or 2D face)
for (int f = 0; f < m->nflex; f++) {
if (!m->flex_interp[f]) continue;
if (m->flex_rigid[f]) continue;
int order = m->flex_interp[f];
int shell_mode = order < 0;
order = order < 0 ? -order : order;
int npe;
if (shell_mode) {
npe = (order+1)*(order+1);
} else {
npe = (order+1)*(order+1)*(order+1);
}
if (npe > max_npe) max_npe = npe;
if (m->flex_nodenum[f] > max_nodenum) max_nodenum = m->flex_nodenum[f];
}
// nothing to do
if (max_npe == 0) {
return;
}
int max_dim_c = 3 * max_npe;
// single unconditional markStack
mj_markStack(d);
// per-flex node positions (upper bound)
mjtNum* xpos = mjSTACKALLOC(d, 3*max_nodenum, mjtNum);
// per-element arrays (upper bound)
mjtNum* xpos_c = mjSTACKALLOC(d, 3*max_npe, mjtNum);
mjtNum* K_rot_cell = mjSTACKALLOC(d, max_dim_c*max_dim_c, mjtNum);
// sparse Jacobian for one cell (upper bound)
int* J_rownnz = mjSTACKALLOC(d, max_dim_c, int);
int* J_rowadr = mjSTACKALLOC(d, max_dim_c, int);
mjtNum* J_val = mjSTACKALLOC(d, max_dim_c*nv, mjtNum);
int* J_colind = mjSTACKALLOC(d, max_dim_c*nv, int);
// temp allocations for chain
int* chain_colind = mjSTACKALLOC(d, nv, int);
mjtNum* blk_jac = mjSTACKALLOC(d, 3*nv, mjtNum);
// temp buffer for fast path element result
mjtNum* fast_tmp = mjSTACKALLOC(d, max_dim_c, mjtNum);
// loop over flexes
for (int f=0; f < m->nflex; f++) {
// only process flex_interp
if (!m->flex_interp[f]) {
continue;
}
// get stiffness and damping
int stiffnessadr = m->flex_stiffnessadr[f];
if (stiffnessadr < 0) {
continue;
}
mjtNum* K = m->flex_stiffness + stiffnessadr;
// skip if rigid or no stiffness
if (m->flex_rigid[f] || K[0] == 0) {
continue;
}
// skip if strain constraints present (stiffness handled by constraint solver)
if (m->flex_edgeequality[f] == 3) {
continue;
}
// compute scale
mjtNum damping = m->flex_damping[f];
mjtNum scale = s1 + s2 * damping;
// skip if scale is zero
if (scale == 0) {
continue;
}
int order = m->flex_interp[f];
int shell_mode = order < 0;
order = order < 0 ? -order : order;
int cx = m->flex_cellnum[3*f+0];
int cy = m->flex_cellnum[3*f+1];
int cz = m->flex_cellnum[3*f+2];
int* bodyid = m->flex_nodebodyid + m->flex_nodeadr[f];
// determine element type: 2D boundary quads (shell) or 3D cells (volume)
int npe;
int nelem_fe;
if (shell_mode) {
npe = (order+1)*(order+1);
nelem_fe = 2*(cy*cz + cx*cz + cx*cy);
} else {
npe = (order+1)*(order+1)*(order+1);
nelem_fe = cx * cy * cz;
}
int dim_e = 3 * npe;
// gather raw node positions (unrotated)
mju_flexGatherState(m, d, f, xpos, NULL);
// check if centered fast path applies: centered, all nodes on simple slider
// bodies (body_simple == 2 means diag M with sliders only, J = R_body per node)
int use_fast_path = m->flex_centered[f];
if (use_fast_path) {
int nodenum_f = m->flex_nodenum[f];
for (int n = 0; n < nodenum_f; n++) {
if (m->body_simple[bodyid[n]] != 2) {
use_fast_path = 0;
break;
}
}
}
// loop over finite elements
for (int fe = 0; fe < nelem_fe; fe++) {
// get element stiffness
mjtNum* k_elem = K + fe * 3*npe * 3*npe;
// skip empty elements: stiffness buffer is zero-initialized at compile time
// (user_model.cc), and non-empty elements have strictly positive diagonal
if (k_elem[0] == 0) {
continue;
}
// use cached K_rot or compute from scratch
int gindices[125]; // element node indices (max npe = 125 for quadratic 3D)
int krot_adr = stiffnessadr + fe * dim_e * dim_e;
if (K_rot_cache) {
// read K_rot from cache and apply scale
for (int i = 0; i < dim_e*dim_e; i++) {
K_rot_cell[i] = scale * K_rot_cache[krot_adr + i];
}
// recompute gindices (cheap index-only call, no rotation)
if (shell_mode) {
mju_flexGatherFaceState(order, cx, cy, cz, fe, NULL, NULL, NULL,
NULL, NULL, NULL, gindices, NULL);
} else {
int ci = fe / (cy * cz);
int cj = (fe / cz) % cy;
int ck = fe % cz;
mju_flexGatherCellState(order, cy, cz, ci, cj, ck, NULL, NULL, NULL,
NULL, NULL, NULL, gindices, NULL);
}
} else {
// gather element-local node positions and rotation
mjtNum quat[4];
if (shell_mode) {
mju_flexGatherFaceState(order, cx, cy, cz, fe, xpos, NULL, NULL,
xpos_c, NULL, NULL, gindices, quat);
} else {
int ci = fe / (cy * cz);
int cj = (fe / cz) % cy;
int ck = fe % cz;
mju_flexGatherCellState(order, cy, cz, ci, cj, ck, xpos, NULL, NULL,
xpos_c, NULL, NULL, gindices, quat);
}
// R = R_global2local, RT = R_local2global
mjtNum R[9], RT[9];
mju_quat2Mat(R, quat);
mju_transpose(RT, R, 3, 3);
// compute K_rot = RT * K_elem * R (block-wise)
mju_zero(K_rot_cell, dim_e*dim_e);
for (int a = 0; a < npe; a++) {
for (int b = 0; b < npe; b++) {
mjtNum blk[9], tmp[9];
// get K_elem(a,b) 3x3 block
int adr_cell = (3*a)*(3*npe) + 3*b;
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 3; c++) {
blk[3*r+c] = k_elem[adr_cell + r*(3*npe) + c];
}
}
// tmp = K * R
mji_mulMatMat3(tmp, blk, R);
// blk = RT * tmp = RT * K * R
mji_mulMatMat3(blk, RT, tmp);
// store in K_rot_cell at (a, b)
int adr_out = (3*a)*dim_e + 3*b;
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 3; c++) {
K_rot_cell[adr_out + r*dim_e + c] = scale * blk[3*r+c];
}
}
}
}
// optionally store unscaled K_rot to output cache
if (K_rot_out) {
for (int i = 0; i < dim_e*dim_e; i++) {
K_rot_out[krot_adr + i] = K_rot_cell[i] / scale;
}
}
}
// skip Jacobian construction and op when only caching (res == NULL)
if (!res) {
continue;
}
// fast path: centered flex with 3 translational DOFs per body
// J = R_body (not I when body is rotated), so J'*K*J*vec = R^T*K*(R*vec)
if (use_fast_path) {
// apply R_body to vec for each node
for (int n = 0; n < npe; n++) {
int dof = m->body_dofadr[bodyid[gindices[n]]];
mji_mulMatVec3(fast_tmp + 3*n, d->xmat + 9*bodyid[gindices[n]], vec + dof);
}
// compute R^T * K_rot * (R*vec) and add to res
for (int a = 0; a < npe; a++) {
int dof_a = m->body_dofadr[bodyid[gindices[a]]];
mjtNum row[3] = {0, 0, 0};
for (int b = 0; b < npe; b++) {
int adr = (3*a)*dim_e + 3*b;
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 3; c++) {
row[r] += K_rot_cell[adr + r*dim_e + c] * fast_tmp[3*b + c];
}
}
}
// apply R^T and add to res
mjtNum qfrc_loc[3];
mji_mulMatTVec3(qfrc_loc, d->xmat + 9*bodyid[gindices[a]], row);
for (int r = 0; r < 3; r++) {
res[dof_a + r] += qfrc_loc[r];
}
}
} else {
// general path: construct sparse Jacobian for this element's nodes
int current_adr = 0;
for (int n = 0; n < npe; n++) {
int bid = bodyid[gindices[n]];
int chain_nnz = mj_bodyChain(m, bid, chain_colind);
mj_jacSparse(m, d, blk_jac, NULL, xpos+3*gindices[n], bid,
chain_nnz, chain_colind, /*flg_skipcommon=*/0);
for (int r = 0; r < 3; r++) {
int row_idx = 3*n + r;
J_rownnz[row_idx] = chain_nnz;
J_rowadr[row_idx] = current_adr;
for (int idx = 0; idx < chain_nnz; idx++) {
J_colind[current_adr] = chain_colind[idx];
J_val[current_adr] = blk_jac[r*chain_nnz + idx];
current_adr++;
}
}
}
// res += J'*K_rot*J*vec
addJTBJ_mulSparse(m, d, res, vec, J_rownnz, J_rowadr, J_colind,
J_val, K_rot_cell, dim_e);
}
}
}
mj_freeStack(d);
}
// compute res += (s1 + s2*damping) * J'*K*J * vec, for all interpolated flexes
// K_rot_cache: if non-NULL, use pre-cached K_rot (same layout as m->flex_stiffness)
void mjd_flexInterp_mul(const mjModel* m, mjData* d, mjtNum* res, const mjtNum* vec,
mjtNum s1, mjtNum s2, const mjtNum* K_rot_cache) {
mjd_flexInterp_kernel(m, d, res, vec, s1, s2, K_rot_cache, NULL);
}
// precompute unscaled K_rot for all elements into cache (same layout as m->flex_stiffness)
void mjd_flexInterp_cacheKrot(const mjModel* m, mjData* d, mjtNum* K_rot_out) {
// use s1=1, s2=0 so scale=1 and K_rot_out gets unscaled values
mjd_flexInterp_kernel(m, d, NULL, NULL, 1, 0, NULL, K_rot_out);
}
// compute res += scale * K_bend * vec for standard (non-interp) flex bending
// scale = s1 + s2 * flex_damping[f] per flex
// for stiffness+damping: s1=h^2, s2=h => scale = h^2 + h*damping
// for stiffness only: s1=h, s2=0 => scale = h
static void flexBend_mul(const mjModel* m, mjData* d, mjtNum* res, const mjtNum* vec,
mjtNum s1, mjtNum s2, int first, int last) {
for (int f = first; f < last; f++) {
// skip interp, rigid, or non-2D
if (m->flex_interp[f] || m->flex_rigid[f] || m->flex_dim[f] != 2) {
continue;
}
int bendingadr = m->flex_bendingadr[f];
if (bendingadr < 0) {
continue;
}
mjtNum scale = s1 + s2 * m->flex_damping[f];
if (!scale) {
continue;
}
const mjtNum* b = m->flex_bending + bendingadr;
int vertnum = m->flex_vertnum[f];
int edgenum = m->flex_edgenum[f];
int edgeadr = m->flex_edgeadr[f];
mj_markStack(d);
mjtNum* wvec = mjSTACKALLOC(d, 3*vertnum, mjtNum);
mjtNum* wres = mjSTACKALLOC(d, 3*vertnum, mjtNum);
mju_zero(wres, 3*vertnum);
mj_flexGather(m, d, f, wvec, vec);
for (int e = 0; e < edgenum; e++) {
const int* edge = m->flex_edge + 2*(e + edgeadr);
const int* flap = m->flex_edgeflap + 2*(e + edgeadr);
int v[4] = {edge[0], edge[1], flap[0], flap[1]};
// skip boundary edges (no second flap vertex)
if (v[3] == -1) {
continue;
}
// apply 4x4 bending stencil in world space
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
mjtNum q = b[17*e + 4*i + j];
for (int x = 0; x < 3; x++) {
wres[3*v[i] + x] += q * wvec[3*v[j] + x];
}
}
}
}
mj_flexScatter(m, d, f, res, wres, scale);
mj_freeStack(d);
}
}
void mjd_flexBend_mul(const mjModel* m, mjData* d, mjtNum* res, const mjtNum* vec,
mjtNum s1, mjtNum s2) {
flexBend_mul(m, d, res, vec, s1, s2, 0, m->nflex);
}
// compute res += (s1 + s2*flex_damping) * K_stretch * vec for standard flexes
// SNH uses its exact Hessian, which can be indefinite; StVK keeps the material term
// and tensile geometric stiffness. For articulated attachments the pullback J'KJ
// omits derivatives of the attachment Jacobian.
static void flexStretch_mul(const mjModel* m, mjData* d, mjtNum* res, const mjtNum* vec,
mjtNum s1, mjtNum s2, int first, int last) {
for (int f = first; f < last; f++) {
// skip interp, rigid, or 1D
if (m->flex_interp[f] || m->flex_rigid[f] || m->flex_dim[f] < 2) {
continue;
}
int stiffnessadr = m->flex_stiffnessadr[f];
if (stiffnessadr < 0 || m->flex_stiffness[stiffnessadr] == 0) {
continue;
}
mjtNum scale = s1 + s2 * m->flex_damping[f];
if (!scale) {
continue;
}
int dim = m->flex_dim[f];
int nedge = (dim == 2) ? 3 : 6;
const int (*edge)[2] = mj_stretchEdges[dim-2];
const int* elem = m->flex_elem + m->flex_elemdataadr[f];
const mjtNum* xpos = d->flexvert_xpos + 3*m->flex_vertadr[f];
const mjtNum* k = m->flex_stiffness + stiffnessadr;
int snh = dim == 3 && k[21] != 0;
int stride = dim == 3 ? 24 : 21;
const int* edgeelem = m->flex_elemedge + m->flex_elemedgeadr[f];
const mjtNum* deformed = d->flexedge_length + m->flex_edgeadr[f];
const mjtNum* reference = m->flexedge_length0 + m->flex_edgeadr[f];
int elemnum = m->flex_elemnum[f];
int nvert = m->flex_vertnum[f];
mj_markStack(d);
mjtNum* wvec = mjSTACKALLOC(d, 3*nvert, mjtNum);
mjtNum* wres = mjSTACKALLOC(d, 3*nvert, mjtNum);
mj_flexGather(m, d, f, wvec, vec);
mju_zero(wres, 3*nvert);
for (int t = 0; t < elemnum; t++) {
const int* vert = elem + (dim+1)*t;
// current edge vectors and their world-frame variations from the attachment Jacobians
mjtNum dvec[6][3], dw[6][3];
mj_stretchEdgeVectors(dvec, xpos, vert, dim);
if (snh) {
mjtNum metric[36], tension[6], grad[4][3], velocity[4][3], result[4][3];
mjtNum pressure = mj_snhStiffness(metric, tension, grad, dvec, k + 24*t,
edgeelem + 6*t, deformed, reference);
for (int v = 0; v < 4; v++) {
mju_copy3(velocity[v], wvec + 3*vert[v]);
}
mj_snhStiffnessMul(result, metric, tension, dvec, grad, pressure, k + 24*t,
velocity, scale);
for (int v = 0; v < 4; v++) {
mju_addTo3(wres + 3*vert[v], result[v]);
}
continue;
}
for (int e = 0; e < nedge; e++) {
int v0 = vert[edge[e][0]], v1 = vert[edge[e][1]];
const mjtNum* w0 = wvec + 3*v0;
const mjtNum* w1 = wvec + 3*v1;
for (int x = 0; x < 3; x++) {
dw[e][x] = w0[x] - w1[x];
}
}
// shared StVK material and tensile geometric stiffness
mjtNum metric[36], tension[6], result[6][3];
mj_stretchStiffness(metric, tension, k + stride*t, edgeelem + t*nedge,
deformed, reference, nedge);
mj_stretchStiffnessMul(result, metric, tension, dvec, dw, nedge, scale);
// accumulate world-frame edge contributions before applying the attachment Jacobians
for (int e = 0; e < nedge; e++) {
int v0 = vert[edge[e][0]], v1 = vert[edge[e][1]];
for (int x = 0; x < 3; x++) {
wres[3*v0+x] += result[e][x];
wres[3*v1+x] -= result[e][x];
}
}
}
mj_flexScatter(m, d, f, res, wres, 1);
mj_freeStack(d);
}
}
void mjd_flexStretch_mul(const mjModel* m, mjData* d, mjtNum* res, const mjtNum* vec,
mjtNum s1, mjtNum s2) {
flexStretch_mul(m, d, res, vec, s1, s2, 0, m->nflex);
}
// returns true if the interpolated flex is processed by mjd_flexInterp_mul
static mjtBool flexInterp_processed(const mjModel* m, int f) {
if (!m->flex_interp[f]) {
return 0;
}
int sa = m->flex_stiffnessadr[f];
if (sa < 0 || m->flex_rigid[f] || m->flex_stiffness[sa] == 0) {
return 0;
}
if (m->flex_edgeequality[f] == 3) {
return 0;
}
return 1;
}
// can ALL operator-processed interp flexes be assembled to dof-level CSR? Requires every node
// to sit on a simple slider body (body_simple == 2: sliders only, so the point Jacobian is I3
// regardless of the node offset -- flex_centered is NOT required) or to be fixed (0 dofs, row
// dropped like a pinned vertex). All-or-nothing: mjd_flexInterp_mul applies every interp flex,
// so the CSR can replace the operator only if it covers them all.
mjtBool mjd_flexInterpAssemblable(const mjModel* m) {
for (int f = 0; f < m->nflex; f++) {
if (!flexInterp_processed(m, f)) {
continue;
}
const int* bodyid = m->flex_nodebodyid + m->flex_nodeadr[f];
for (int n = 0; n < m->flex_nodenum[f]; n++) {
int dofnum = m->body_dofnum[bodyid[n]];
if (dofnum == 0) {
continue;
}
if (m->body_simple[bodyid[n]] != 2 || dofnum != 3) {
return 0;
}
}
}
return 1;
}
// element walk shared by the interp assembly passes: geometry of interp finite elements
// (mirrors mjd_flexInterp_kernel; index-only gather calls)
#define FLEXINTERP_WALK(f, body) \
{ \
int order_ = m->flex_interp[f]; \
int shell_ = order_ < 0; \
order_ = order_ < 0 ? -order_ : order_; \
int cx_ = m->flex_cellnum[3 * (f) + 0]; \
int cy_ = m->flex_cellnum[3 * (f) + 1]; \
int cz_ = m->flex_cellnum[3 * (f) + 2]; \
int npe = shell_ ? (order_ + 1) * (order_ + 1) \
: (order_ + 1) * (order_ + 1) * (order_ + 1); \
int nelem_fe_ = \
shell_ ? 2 * (cy_ * cz_ + cx_ * cz_ + cx_ * cy_) : cx_ * cy_ * cz_; \
const mjtNum* K_ = m->flex_stiffness + m->flex_stiffnessadr[f]; \
int gindices[125]; \
for (int fe = 0; fe < nelem_fe_; fe++) { \
if (K_[(size_t)fe * 3 * npe * 3 * npe] == 0) continue; \
if (shell_) { \
mju_flexGatherFaceState(order_, cx_, cy_, cz_, fe, NULL, NULL, NULL, \
NULL, NULL, NULL, gindices, NULL); \
} else { \
int ci_ = fe / (cy_ * cz_), cj_ = (fe / cz_) % cy_, ck_ = fe % cz_; \
mju_flexGatherCellState(order_, cy_, cz_, ci_, cj_, ck_, NULL, NULL, \
NULL, NULL, NULL, NULL, gindices, NULL); \
} \
body \
} \
}
// does any flex use the passive contact path? Distinct from elasticity: an empty CSR is valid for
// elastic models (matrix-free operators) but means "nothing" for a contact-only flex.
static mjtBool flexPassiveContact_any(const mjModel* m) {
for (int f = 0; f < m->nflex; f++) {
if (mj_effFlexContactPossible(m, f)) {
return 1;
}
}
return 0;
}
// does ANY flex contribute assemblable implicit stiffness? (cheap existence check for the
// solver gate: stretch stiffness on a standard flex, or -- when Krot will be supplied -- an
// operator-processed interp flex)
mjtBool mjd_flexStiff_any(const mjModel* m, int flg_interp) {
for (int f = 0; f < m->nflex; f++) {
if (flg_interp && flexInterp_processed(m, f)) {
return 1;
}
if (!m->flex_interp[f] && !m->flex_rigid[f] && m->flex_dim[f] >= 2 &&
m->flex_stiffnessadr[f] >= 0 && m->flex_stiffness[m->flex_stiffnessadr[f]] != 0 &&
mj_flexSimple(m, f)) {
return 1;
}
}
return 0;
}
// does this standard flex contribute implicit stiffness under the given term flags?
static mjtBool flexStiff_active(const mjModel* m, int f, int flg_bend, int flg_stretch) {
if (m->flex_interp[f] || m->flex_rigid[f] || m->flex_dim[f] < 2 ||
!mj_flexSimple(m, f)) {
return 0;
}
int bend = flg_bend && m->flex_bendingadr[f] >= 0;
int stretch = flg_stretch && m->flex_stiffnessadr[f] >= 0 &&
m->flex_stiffness[m->flex_stiffnessadr[f]] != 0;
return bend || stretch;
}
// The flex-contact law (see engine_derivative.h). lam/k is taken as 0 when the multiplier is,
// so a massless pair (k = 0) yields no force rather than a NaN.
mjtNum mjd_flexContactSlack(mjtNum k, mjtNum gap, mjtNum lam) {
mjtNum t = gap - (lam ? lam/k : 0);
return t > 0 ? t : 0;
}
mjtNum mjd_flexContactResidual(mjtNum k, mjtNum gap, mjtNum s, mjtNum lam) {
return gap - s - (lam ? lam/k : 0);
}
mjtNum mjd_flexVertMass(const mjModel* m, const mjData* d, int gv) {
int b = m->body_weldid[m->flex_vertbodyid[gv]];
if (m->body_dofnum[b] != 3) {
return 0;
}
int da = m->body_dofadr[b];
return d->M[m->M_rowadr[da] + m->M_rownnz[da] - 1]; // diagonal: the point mass
}
mjtNum mjd_flexContactStiffness(const mjModel* m, const mjData* d, const mjContact* con) {
mjtNum mmin = 0;
for (int side = 0; side < 2; side++) {
int f = con->flex[side];
if (f < 0) {
continue;
}
int gv[8], ngv = 0;
if (con->vert[side] >= 0) {
gv[ngv++] = m->flex_vertadr[f] + con->vert[side];
} else if (con->elem[side] >= 0) {
int nvrt = m->flex_dim[f] + 1;
const int* e = m->flex_elem + m->flex_elemdataadr[f] + nvrt*con->elem[side];
for (int j = 0; j < nvrt && ngv < 8; j++) {
gv[ngv++] = m->flex_vertadr[f] + e[j];
}
}
for (int j = 0; j < ngv; j++) {
mjtNum mv = mjd_flexVertMass(m, d, gv[j]);
if (mv > 0 && (mmin == 0 || mv < mmin)) {
mmin = mv;
}
}
}
return mjFLEXCONTACT_OMEGA2 * mmin; // 0 if every participant is massless: no stiffness, no NaN
}
// assemble the standard-flex implicit stiffness K = (s1 + s2*damping) * (K_bend + K_stretch)
// into dof-level CSR (same terms mjd_flexBend_mul / mjd_flexStretch_mul apply matrix-free; the
// matrix is constant during a solve, so assembling once and applying as a sparse matvec avoids
// re-walking stencils and re-unpacking metrics on every apply). Rows/columns exist only on the
// dofs of unpinned vertices of standard dim>=2 flexes with bending or stretch stiffness.
// Phase 1 (colind == NULL): fill rownnz/rowadr over nv, return total nnz.
// Phase 2: fill colind and val (rownnz/rowadr must come from phase 1).
// Interp flexes are assembled iff Krot (the mjd_flexInterp_cacheKrot cache) is non-NULL and
// they qualify for the centered fast path (caller checks mjd_flexInterpAssemblable): rows on
// node body dofs, corotated 3x3 blocks, with the operator's NEGATED sign convention folded in
// so one CSR replaces all three matrix-free operators uniformly.
int mjd_flexStiff_assemble(const mjModel* m, mjData* d, int* rownnz, int* rowadr,
int* colind, mjtNum* val, mjtNum s1, mjtNum s2,
int flg_bend, int flg_stretch, const mjtNum* Krot) {
int nv = m->nv;
mj_markStack(d);
// collect participating bodies: bodies with 3 dofs get one slot each
int* bodyslot = mjSTACKALLOC(d, m->nbody, int);
for (int b = 0; b < m->nbody; b++) {
bodyslot[b] = -1;
}
for (int f = 0; f < m->nflex; f++) {
// contact no longer forces vertex slots: a contact-only flex rides the rank-1 class
if (!flexStiff_active(m, f, flg_bend, flg_stretch)) {
continue;
}
for (int lv = 0; lv < m->flex_vertnum[f]; lv++) {
int gv = m->flex_vertadr[f] + lv;
int b = m->body_weldid[m->flex_vertbodyid[gv]];
if (m->body_dofnum[b] == 3) {
bodyslot[b] = 1;
}
}
}
// interp nodes participate when the caller supplies the K_rot cache (centered fast path)
if (Krot) {
for (int f = 0; f < m->nflex; f++) {
if (!flexInterp_processed(m, f)) {
continue;
}
const int* bodyid = m->flex_nodebodyid + m->flex_nodeadr[f];
for (int ln = 0; ln < m->flex_nodenum[f]; ln++) {
int b = bodyid[ln];
if (m->body_dofnum[b] == 3) {
bodyslot[b] = 1;
}
}
}
}
int nvert = 0;
for (int b = 0; b < m->nbody; b++) {
if (bodyslot[b] > 0) {
bodyslot[b] = nvert++;
}
}
int* vslot = mjSTACKALLOC(d, m->nflexvert > 0 ? m->nflexvert : 1, int);
for (int i = 0; i < m->nflexvert; i++) {
int b = m->flex_vertbodyid[i];
vslot[i] = (b >= 0) ? bodyslot[m->body_weldid[b]] : -1;
}
int* nslot = mjSTACKALLOC(d, m->nflexnode > 0 ? (int)m->nflexnode : 1, int);
for (int i = 0; i < m->nflexnode; i++) {
int b = m->flex_nodebodyid[i];
nslot[i] = (b >= 0) ? bodyslot[b] : -1;
}
if (!nvert) {
mju_zeroInt(rownnz, nv);
if (rowadr) mju_zeroInt(rowadr, nv);
mj_freeStack(d);
return 0;
}
int* vdof = mjSTACKALLOC(d, nvert, int);
for (int b = 0; b < m->nbody; b++) {
if (bodyslot[b] >= 0) {
vdof[bodyslot[b]] = m->body_dofadr[b];
}
}
// count stencil incidence per vertex slot (with duplicates)
int* ncand = mjSTACKALLOC(d, nvert, int);
mju_zeroInt(ncand, nvert);
for (int f = 0; f < m->nflex; f++) {
if (!flexStiff_active(m, f, flg_bend, flg_stretch)) {
continue;
}
int dim = m->flex_dim[f], nvrt = dim + 1;
// bending stencils: 4-vertex flaps per edge
if (flg_bend && m->flex_bendingadr[f] >= 0) {
for (int e = 0; e < m->flex_edgenum[f]; e++) {
const int* edge = m->flex_edge + 2*(e + m->flex_edgeadr[f]);
const int* flap = m->flex_edgeflap + 2*(e + m->flex_edgeadr[f]);
if (flap[1] == -1) continue;
int v[4] = {edge[0], edge[1], flap[0], flap[1]};
for (int i = 0; i < 4; i++) {
int si = vslot[m->flex_vertadr[f] + v[i]];
if (si >= 0) ncand[si] += 4;
}
}
}
// stretch elements: (dim+1)-vertex cliques
if (flg_stretch && m->flex_stiffnessadr[f] >= 0 &&
m->flex_stiffness[m->flex_stiffnessadr[f]] != 0) {
const int* elem = m->flex_elem + m->flex_elemdataadr[f];
for (int t = 0; t < m->flex_elemnum[f]; t++) {
for (int i = 0; i < nvrt; i++) {
int si = vslot[m->flex_vertadr[f] + elem[(dim+1)*t + i]];
if (si >= 0) ncand[si] += nvrt;
}
}
}
}
// interp stencils: npe-node element cliques (counting)
if (Krot) {
for (int f = 0; f < m->nflex; f++) {
if (!flexInterp_processed(m, f)) {
continue;
}
FLEXINTERP_WALK(f, {
for (int i = 0; i < npe; i++) {
int si = nslot[m->flex_nodeadr[f] + gindices[i]];
if (si >= 0) ncand[si] += npe;
}
})
}
}
// gather candidate neighbor lists (vertex slots, with duplicates)
int* cadr = mjSTACKALLOC(d, nvert + 1, int);
cadr[0] = 0;
for (int s = 0; s < nvert; s++) {
cadr[s+1] = cadr[s] + ncand[s];
}
int* cand = mjSTACKALLOC(d, cadr[nvert] > 0 ? cadr[nvert] : 1, int);
mju_zeroInt(ncand, nvert);
for (int f = 0; f < m->nflex; f++) {
if (!flexStiff_active(m, f, flg_bend, flg_stretch)) {
continue;
}
int dim = m->flex_dim[f], nvrt = dim + 1;
if (flg_bend && m->flex_bendingadr[f] >= 0) {
for (int e = 0; e < m->flex_edgenum[f]; e++) {
const int* edge = m->flex_edge + 2*(e + m->flex_edgeadr[f]);
const int* flap = m->flex_edgeflap + 2*(e + m->flex_edgeadr[f]);
if (flap[1] == -1) continue;
int v[4] = {edge[0], edge[1], flap[0], flap[1]};
for (int i = 0; i < 4; i++) {
int si = vslot[m->flex_vertadr[f] + v[i]];
if (si < 0) continue;
for (int j = 0; j < 4; j++) {
int sj = vslot[m->flex_vertadr[f] + v[j]];
if (sj >= 0) cand[cadr[si] + ncand[si]++] = sj;
}
}
}
}
if (flg_stretch && m->flex_stiffnessadr[f] >= 0 &&
m->flex_stiffness[m->flex_stiffnessadr[f]] != 0) {
const int* elem = m->flex_elem + m->flex_elemdataadr[f];
for (int t = 0; t < m->flex_elemnum[f]; t++) {
const int* vert = elem + (dim+1)*t;
for (int i = 0; i < nvrt; i++) {
int si = vslot[m->flex_vertadr[f] + vert[i]];
if (si < 0) continue;
for (int j = 0; j < nvrt; j++) {
int sj = vslot[m->flex_vertadr[f] + vert[j]];
if (sj >= 0) cand[cadr[si] + ncand[si]++] = sj;
}
}
}
}
}
// interp stencils: npe-node element cliques (filling)
if (Krot) {
for (int f = 0; f < m->nflex; f++) {
if (!flexInterp_processed(m, f)) {
continue;
}
FLEXINTERP_WALK(f, {
for (int i = 0; i < npe; i++) {
int si = nslot[m->flex_nodeadr[f] + gindices[i]];
if (si < 0) continue;
for (int j = 0; j < npe; j++) {
int sj = nslot[m->flex_nodeadr[f] + gindices[j]];
if (sj >= 0) cand[cadr[si] + ncand[si]++] = sj;
}
}
})
}
}
// passive contacts (filling)
// per vertex: sort by neighbor dofadr, unique -> neighbor lists
int* nadr = mjSTACKALLOC(d, nvert + 1, int);
int* neigh = mjSTACKALLOC(d, cadr[nvert] > 0 ? cadr[nvert] : 1, int);
nadr[0] = 0;
for (int s = 0; s < nvert; s++) {
int* c = cand + cadr[s];
int n = ncand[s];
// insertion sort by dof address (stencil-local lists are short)
for (int i = 1; i < n; i++) {
int key = c[i], kd = vdof[key], j = i - 1;
while (j >= 0 && vdof[c[j]] > kd) {
c[j+1] = c[j];
j--;
}
c[j+1] = key;
}
int nn = 0;
for (int i = 0; i < n; i++) {
if (nn == 0 || neigh[nadr[s] + nn - 1] != c[i]) {
neigh[nadr[s] + nn++] = c[i];
}
}
nadr[s+1] = nadr[s] + nn;
}
// dof-level CSR structure: each vertex contributes 3 rows of 3*nneigh entries
mju_zeroInt(rownnz, nv);
int nnz = 0;
for (int s = 0; s < nvert; s++) {
int nn = nadr[s+1] - nadr[s];
for (int k = 0; k < 3; k++) {
rownnz[vdof[s] + k] = 3*nn;
}
nnz += 9*nn;
}
if (rowadr) {
rowadr[0] = 0;
for (int i = 1; i < nv; i++) {
rowadr[i] = rowadr[i-1] + rownnz[i-1];
}
}
// phase 1: structure only
if (!colind) {
mj_freeStack(d);
return nnz;
}
// column indices (per vertex row: the 3 dofs of each neighbor, neighbor-dof-sorted)
for (int s = 0; s < nvert; s++) {
for (int k = 0; k < 3; k++) {
int adr = rowadr[vdof[s] + k];
for (int j = nadr[s]; j < nadr[s+1]; j++) {
for (int c = 0; c < 3; c++) {
colind[adr++] = vdof[neigh[j]] + c;
}
}
}
}
mju_zero(val, nnz);
// block accumulation helper data: find neighbor position by binary search on dofadr
#define FLEXSTIFF_BLOCK(si, sj, pos) \
{ \
int lo = nadr[si], hi = nadr[si + 1] - 1; \
pos = -1; \
while (lo <= hi) { \
int mid = (lo + hi) / 2; \
if (vdof[neigh[mid]] == vdof[sj]) { \
pos = mid - nadr[si]; \
break; \
} \
if (vdof[neigh[mid]] < vdof[sj]) \
lo = mid + 1; \
else \
hi = mid - 1; \
} \
}
// values: bending (Q_ij * R_bi^T * R_bj per 4-vertex stencil) and stretch (GN blocks per element)
for (int f = 0; f < m->nflex; f++) {
if (!flexStiff_active(m, f, flg_bend, flg_stretch)) {
continue;
}
mjtNum scale = s1 + s2*m->flex_damping[f];
if (!scale) {
continue;
}
int dim = m->flex_dim[f], nvrt = dim + 1;
int nedge = (dim == 2) ? 3 : 6;
if (flg_bend && m->flex_bendingadr[f] >= 0) {
const mjtNum* b = m->flex_bending + m->flex_bendingadr[f];
for (int e = 0; e < m->flex_edgenum[f]; e++) {
const int* edge = m->flex_edge + 2*(e + m->flex_edgeadr[f]);
const int* flap = m->flex_edgeflap + 2*(e + m->flex_edgeadr[f]);
if (flap[1] == -1) continue;
int v[4] = {edge[0], edge[1], flap[0], flap[1]};
for (int i = 0; i < 4; i++) {
int si = vslot[m->flex_vertadr[f] + v[i]];
if (si < 0) continue;
int bi = m->body_weldid[m->flex_vertbodyid[m->flex_vertadr[f] + v[i]]];
const mjtNum* qi = d->xquat + 4*bi;
for (int j = 0; j < 4; j++) {
int sj = vslot[m->flex_vertadr[f] + v[j]];
if (sj < 0) continue;
mjtNum q = scale*b[17*e + 4*i + j];
if (!q) continue;
int bj = m->body_weldid[m->flex_vertbodyid[m->flex_vertadr[f] + v[j]]];
const mjtNum* qj = d->xquat + 4*bj;
int pos;
FLEXSTIFF_BLOCK(si, sj, pos);
// fast path: when bi and bj share the same body orientation, R_bi^T * R_bj = I3
if (bi == bj || (qi[0] == qj[0] && qi[1] == qj[1] &&
qi[2] == qj[2] && qi[3] == qj[3])) {
val[rowadr[vdof[si] + 0] + 3*pos + 0] += q;
val[rowadr[vdof[si] + 1] + 3*pos + 1] += q;
val[rowadr[vdof[si] + 2] + 3*pos + 2] += q;
} else {
mjtNum blkd[9];
mji_mulMatTMat3(blkd, d->xmat + 9*bi, d->xmat + 9*bj);
for (int k = 0; k < 3; k++) {
mji_addToScl3(val + rowadr[vdof[si] + k] + 3*pos, blkd + 3*k, q);
}
}
}
}
}
}
if (flg_stretch && m->flex_stiffnessadr[f] >= 0 &&
m->flex_stiffness[m->flex_stiffnessadr[f]] != 0) {
const int* elem = m->flex_elem + m->flex_elemdataadr[f];
const mjtNum* xpos = d->flexvert_xpos + 3*m->flex_vertadr[f];
const mjtNum* kk = m->flex_stiffness + m->flex_stiffnessadr[f];
int snh = dim == 3 && kk[21] != 0;
int stride = dim == 3 ? 24 : 21;
const int* eelem = m->flex_elemedge + m->flex_elemedgeadr[f];
const mjtNum* elen = d->flexedge_length + m->flex_edgeadr[f];
const mjtNum* elen0 = m->flexedge_length0 + m->flex_edgeadr[f];
for (int t = 0; t < m->flex_elemnum[f]; t++) {
const int* vert = elem + (dim+1)*t;
mjtNum dvec[6][3], metric[36], tension[6], grad[4][3], pressure = 0;
mj_stretchEdgeVectors(dvec, xpos, vert, dim);
if (snh) {
pressure = mj_snhStiffness(metric, tension, grad, dvec, kk + 24*t,
eelem + 6*t, elen, elen0);
} else {
mj_stretchStiffness(metric, tension, kk + stride*t, eelem + t*nedge,
elen, elen0, nedge);
}
// assemble the same material and geometric stiffness used by the operator
for (int i = 0; i < nvrt; i++) {
int si = vslot[m->flex_vertadr[f] + vert[i]];
if (si < 0) continue;
for (int j = 0; j < nvrt; j++) {
int sj = vslot[m->flex_vertadr[f] + vert[j]];
if (sj < 0) continue;
mjtNum blk[9];
if (snh) {
mj_snhStiffnessBlock(blk, metric, tension, dvec, grad, pressure, kk + 24*t,
i, j, scale);
} else {
mj_stretchStiffnessBlock(blk, metric, tension, dvec, dim, i, j, scale);
}
// blk is world-space but the destination dofs are the vertex bodies' own (possibly
// rotated) slide axes: blk_dof = R_bi^T * blk_world * R_bj, matching the force path
int bi = m->body_weldid[m->flex_vertbodyid[m->flex_vertadr[f] + vert[i]]];
int bj = m->body_weldid[m->flex_vertbodyid[m->flex_vertadr[f] + vert[j]]];
mjtNum tmp[9], blkd[9];
mji_mulMatMat3(tmp, blk, d->xmat + 9*bj); // tmp = blk * R_bj
mji_mulMatTMat3(blkd, d->xmat + 9*bi, tmp); // blkd = R_bi^T * tmp
int pos;
FLEXSTIFF_BLOCK(si, sj, pos);
for (int k = 0; k < 3; k++) {
for (int c = 0; c < 3; c++) {
val[rowadr[vdof[si] + k] + 3*pos + c] += blkd[3*k+c];
}
}
}
}
}
}
}
// values: interp (corotated K_rot 3x3 node blocks per element). The interp operator's sign
// convention is opposite to bend/stretch -- FlexBmulAdd calls it with negated scales -- so
// the negation is folded in here and one CSR replaces all three operators uniformly.
if (Krot) {
for (int f = 0; f < m->nflex; f++) {
if (!flexInterp_processed(m, f)) {
continue;
}
mjtNum iscale = -(s1 + s2*m->flex_damping[f]);
if (!iscale) {
continue;
}
FLEXINTERP_WALK(f, {
int dim_e = 3*npe;
const mjtNum* kb = Krot + m->flex_stiffnessadr[f] + (size_t)fe*dim_e*dim_e;
for (int i = 0; i < npe; i++) {
int si = nslot[m->flex_nodeadr[f] + gindices[i]];
if (si < 0) continue;
int bi = m->flex_nodebodyid[m->flex_nodeadr[f] + gindices[i]];
for (int j = 0; j < npe; j++) {
int sj = nslot[m->flex_nodeadr[f] + gindices[j]];
if (sj < 0) continue;
int bj = m->flex_nodebodyid[m->flex_nodeadr[f] + gindices[j]];
// extract K_rot 3x3 block for (i,j)
mjtNum kb_ij[9];
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 3; c++) {
kb_ij[3*r+c] = iscale*kb[(3*i + r)*dim_e + 3*j + c];
}
}
// apply R_bi^T * kb_ij * R_bj to transform to DOF space
mjtNum tmp[9];
mjtNum blk[9];
mji_mulMatMat3(tmp, kb_ij, d->xmat + 9*bj); // tmp = kb * R_bj
mji_mulMatTMat3(blk, d->xmat + 9*bi, tmp); // blk = R_bi^T * tmp
int pos;
FLEXSTIFF_BLOCK(si, sj, pos);
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 3; c++) {
val[rowadr[vdof[si] + r] + 3*pos + c] += blk[3*r+c];
}
}
}
}
})
}
}
#undef FLEXSTIFF_BLOCK
#undef FLEXINTERP_WALK
mj_freeStack(d);
return nnz;
}
// per-actuator skip conditions shared by the qDeriv and discrete-metric assemblers:
// disabled, sleeping, or force-clamped actuators contribute no derivative
static int actuatorDerivSkip(const mjModel* m, const mjData* d, int i, int sleep_filter) {
// skip if disabled
if (mj_actuatorDisabled(m, i)) {
return 1;
}
// skip if sleeping
if (sleep_filter && mj_sleepState(m, d, mjOBJ_ACTUATOR, i) == mjS_ASLEEP) {
return 1;
}
// skip if force is clamped by forcerange
if (m->actuator_forcelimited[i]) {
int oadr = m->actuator_outadr[i];
const mjtNum* range = m->actuator_forcerange + 2*i;
// SO3: force is norm-clamped (approximation: saturated force still varies tangentially)
if (m->actuator_gaintype[i] == mjGAIN_SO3) {
if (mju_norm3(d->actuator_force + oadr) >= range[1]) {
return 1;
}
} else {
mjtNum force = d->actuator_force[oadr];
if (force <= range[0] || force >= range[1]) {
return 1;
}
}
}
return 0;
}
// input of actuator i: ctrl, or (next-)activation
static mjtNum actuatorInput(const mjModel* m, const mjData* d, int i) {
if (m->actuator_dyntype[i] == mjDYN_NONE) {
int adr = m->actuator_ctrladr[i];
mjtNum ctrl;
if (m->actuator_delay[i]) {
mjtNum ctrl_buf[4] = {0};
const mjtNum* ptr = mj_readCtrl(m, d, i, d->time, ctrl_buf, -1);
ctrl = ptr ? ptr[0] : ctrl_buf[0];
} else {
ctrl = d->ctrl[adr];
}
if (!mjDISABLED(mjDSBL_CLAMPCTRL) && m->actuator_ctrllimited[adr]) {
ctrl = mju_clip(ctrl, m->actuator_ctrlrange[2*adr], m->actuator_ctrlrange[2*adr+1]);
}
return ctrl;
}
int act_adr = m->actuator_actadr[i] + m->actuator_actnum[i] - 1;
mjtNum act = d->act[act_adr];
// use next activation if actearly is set (matching forward pass)
if (m->actuator_actearly[i]) {
act = mj_nextActivation(m, d, i, act_adr, d->act_dot[act_adr]);
}
return act;
}
// d(force)/d(length) of actuator i: all gain and bias types
static mjtNum actuatorLenDeriv(const mjModel* m, const mjData* d, int i) {
int oadr = m->actuator_outadr[i];
mjtNum bias_len = 0, gain_len = 0;
// affine and SO3 servo bias
if (m->actuator_biastype[i] == mjBIAS_AFFINE || m->actuator_biastype[i] == mjBIAS_SO3) {
bias_len = (m->actuator_biasprm + mjNBIAS*i)[1];
}
// muscle bias: passive force-length curve
else if (m->actuator_biastype[i] == mjBIAS_MUSCLE) {
bias_len = mjd_muscleBias_len(d->actuator_length[oadr],
m->actuator_lengthrange+2*oadr,
m->actuator_acc0[oadr],
m->actuator_biasprm + mjNBIAS*i);
}
// affine gain
if (m->actuator_gaintype[i] == mjGAIN_AFFINE) {
gain_len = (m->actuator_gainprm + mjNGAIN*i)[1];
}
// muscle gain: active force-length curve
else if (m->actuator_gaintype[i] == mjGAIN_MUSCLE) {
gain_len = mjd_muscleGain_len(d->actuator_length[oadr],
d->actuator_velocity[oadr],
m->actuator_lengthrange+2*oadr,
m->actuator_acc0[oadr],
m->actuator_gainprm + mjNGAIN*i);
}
// DC motor controller position loop
else if (m->actuator_gaintype[i] == mjGAIN_DCMOTOR) {
const mjtNum* dynprm = m->actuator_dynprm + mjNDYN*i;
const mjtNum* gainprm = m->actuator_gainprm + mjNGAIN*i;
mjtNum te = dynprm[0];
// controller length derivative dV/dl: torque-space kp through the tau->V map; Vmax
// clipping and the O(h*ki) integral-state chain are ignored, matching the velocity
// derivative's treatment of the saturations
mjtNum dVdl = 0;
if (m->actuator_ctrlspec[i] & (mjINPUT_POS | mjINPUT_VEL | mjINPUT_FF)) {
mjtNum R = mju_max(mjMINVAL, gainprm[0]);
mjtNum K = gainprm[1]; // K > 0 on this path, enforced by the compiler
dVdl = -gainprm[4]*R/K;
}
if (te > 0) {
// stateful current with actearly: d(K*next_act)/d(length) through act_dot
mjtNum R = mju_max(mjMINVAL, gainprm[0]);
mjtNum K = gainprm[1];
mjtNum s = 1 - mju_exp(-m->opt.timestep / te);
bias_len += K * dVdl * s / R;
} else if (dVdl != 0) {
// stateless: K/R * dVdl = -kp exactly, the motor parameters cancel
mjtNum R = mju_max(mjMINVAL, gainprm[0]);
mjtNum K = gainprm[1];
bias_len += K * dVdl / R;
}
}
// force = gain .* [ctrl/act]
if (gain_len != 0) {
bias_len += gain_len * actuatorInput(m, d, i);
}
return bias_len;
}
// d(force)/d(velocity) of actuator i: all gain and bias types
static mjtNum actuatorVelDeriv(const mjModel* m, const mjData* d, int i) {
int oadr = m->actuator_outadr[i];
mjtNum bias_vel = 0, gain_vel = 0;
// affine bias
if (m->actuator_biastype[i] == mjBIAS_AFFINE) {
// extract bias info: prm = [const, kp, kv]
bias_vel = (m->actuator_biasprm + mjNBIAS*i)[2];
}
// SO3 geodesic servo: kv term, applied to each output row below
else if (m->actuator_biastype[i] == mjBIAS_SO3) {
bias_vel = (m->actuator_biasprm + mjNBIAS*i)[2];
}
// DC motor bias (back-EMF)
else if (m->actuator_biastype[i] == mjBIAS_DCMOTOR) {
const mjtNum* dynprm = m->actuator_dynprm + mjNDYN*i;
const mjtNum* gainprm = m->actuator_gainprm + mjNGAIN*i;
if (dynprm[0] <= 0) {
mjtNum R = mj_dcmotorResistance(m, d, i);
mjtNum K = gainprm[1];
bias_vel -= K * K / R;
}
}
// affine gain
if (m->actuator_gaintype[i] == mjGAIN_AFFINE) {
// extract bias info: prm = [const, kp, kv]
gain_vel = (m->actuator_gainprm + mjNGAIN*i)[2];
}
// muscle gain
else if (m->actuator_gaintype[i] == mjGAIN_MUSCLE) {
gain_vel = mjd_muscleGain_vel(d->actuator_length[oadr],
d->actuator_velocity[oadr],
m->actuator_lengthrange+2*oadr,
m->actuator_acc0[oadr],
m->actuator_gainprm + mjNGAIN*i);
}
// DC motor controller damping and LuGre micro-damping
else if (m->actuator_gaintype[i] == mjGAIN_DCMOTOR) {
const mjtNum* dynprm = m->actuator_dynprm + mjNDYN*i;
const mjtNum* gainprm = m->actuator_gainprm + mjNGAIN*i;
mjtNum te = dynprm[0];
// controller velocity derivative dV/dw: torque-space kd through the tau->V map using
// nameplate resistance, plus back-EMF compensation K, which cancels the physical -K^2/R
// back-EMF bias term so net damping is -kd*R0/R; Vmax clipping is ignored here
mjtNum dVdw = 0;
if (m->actuator_ctrlspec[i] & (mjINPUT_POS | mjINPUT_VEL | mjINPUT_FF)) {
mjtNum R0 = mju_max(mjMINVAL, gainprm[0]);
mjtNum K = gainprm[1]; // K > 0 on this path, enforced by the compiler
dVdw = -gainprm[6]*R0/K + K;
}
if (te > 0) {
// stateful current with actearly: d(K*next_act)/dω
// includes both back-EMF (-K) and controller (dVdw) through act_dot
mjtNum R = mj_dcmotorResistance(m, d, i);
mjtNum K = gainprm[1];
mjtNum s = 1 - mju_exp(-m->opt.timestep / te);
bias_vel += K * (dVdw - K) * s / R;
} else if (dVdw != 0) {
// stateless: controller terms only (back-EMF handled in bias block)
mjtNum R = mj_dcmotorResistance(m, d, i);
mjtNum K = gainprm[1];
bias_vel += K * dVdw / R;
}
// LuGre: force includes -sigma1*z_dot, z_dot = a*z + v
// d(sigma1*z_dot)/dv = sigma1*(da/dv*z + 1), ignoring higher-order da/dv*z
mjtNum sigma1 = dynprm[6];
if (sigma1 > 0) {
bias_vel -= sigma1;
}
}
// force = gain .* [ctrl/act]
if (gain_vel != 0) {
bias_vel += gain_vel * actuatorInput(m, d, i);
}
return bias_vel;
}
// add (d qfrc_actuator / d qvel) to qDeriv
void mjd_actuator_vel(const mjModel* m, mjData* d) {
int nactuator = m->nactuator;
int sleep_filter = mjENABLED(mjENBL_SLEEP) && d->ntree_awake < m->ntree;
// disabled: nothing to add
if (mjDISABLED(mjDSBL_ACTUATION)) {
return;
}
// process actuators
for (int i=0; i < nactuator; i++) {
if (actuatorDerivSkip(m, d, i, sleep_filter)) {
continue;
}
// combined scalar derivative
mjtNum bias_vel = actuatorVelDeriv(m, d, i);
// add, once per output row
if (bias_vel != 0) {
int oadr = m->actuator_outadr[i];
for (int k=0; k < m->actuator_outnum[i]; k++) {
addJTBJSparse(m, d, d->qDeriv, d->actuator_moment, &bias_vel, 1, oadr+k,
d->moment_rownnz, d->moment_rowadr, d->moment_colind);
}
}
}
}
//----------------- utilities for ellipsoid-based fluid force derivatives --------------------------
static inline mjtNum pow2(const mjtNum val) {
return val*val;
}
static inline mjtNum ellipsoid_max_moment(const mjtNum size[3], const int dir) {
const mjtNum d0 = size[dir];
const mjtNum d1 = size[(dir+1) % 3];
const mjtNum d2 = size[(dir+2) % 3];
return 8.0/15.0 * mjPI * d0 * pow2(pow2(mju_max(d1, d2)));
}
// add 3x3 matrix D to one of the four quadrants of the 6x6 matrix B
// row_quad and col_quad should be either 0 or 1 (not checked)
static void addToQuadrant(mjtNum* restrict B, const mjtNum D[9], int col_quad, int row_quad) {
int r = 3*row_quad, c = 3*col_quad;
B[6*(c+0) + r+0] += D[0];
B[6*(c+0) + r+1] += D[1];
B[6*(c+0) + r+2] += D[2];
B[6*(c+1) + r+0] += D[3];
B[6*(c+1) + r+1] += D[4];
B[6*(c+1) + r+2] += D[5];
B[6*(c+2) + r+0] += D[6];
B[6*(c+2) + r+1] += D[7];
B[6*(c+2) + r+2] += D[8];
}
//----------------- components of ellipsoid-based fluid force derivatives --------------------------
// forces due to fluid mass moving with the body, B is 6x6
static void mjd_addedMassForces(
mjtNum* restrict B, const mjtNum local_vels[6], const mjtNum fluid_density,
const mjtNum virtual_mass[3], const mjtNum virtual_inertia[3]) {
const mjtNum lin_vel[3] = {local_vels[3], local_vels[4], local_vels[5]};
const mjtNum ang_vel[3] = {local_vels[0], local_vels[1], local_vels[2]};
const mjtNum virtual_lin_mom[3] = {
fluid_density * virtual_mass[0] * lin_vel[0],
fluid_density * virtual_mass[1] * lin_vel[1],
fluid_density * virtual_mass[2] * lin_vel[2]
};
const mjtNum virtual_ang_mom[3] = {
fluid_density * virtual_inertia[0] * ang_vel[0],
fluid_density * virtual_inertia[1] * ang_vel[1],
fluid_density * virtual_inertia[2] * ang_vel[2]
};
mjtNum Da[9];
mjtNum Db[9];
// force[:3] += cross(virtual_ang_mom, ang_vel)
mjd_cross(virtual_ang_mom, ang_vel, Da, Db);
addToQuadrant(B, Db, 0, 0);
for (int i=0; i < 9; ++i) {
Da[i] *= fluid_density * virtual_inertia[i % 3];
}
addToQuadrant(B, Da, 0, 0);
// force[:3] += cross(virtual_lin_mom, lin_vel)
mjd_cross(virtual_lin_mom, lin_vel, Da, Db);
addToQuadrant(B, Db, 0, 1);
for (int i=0; i < 9; ++i) {
Da[i] *= fluid_density * virtual_mass[i % 3];
}
addToQuadrant(B, Da, 0, 1);
// force[3:] += cross(virtual_lin_mom, ang_vel)
mjd_cross(virtual_lin_mom, ang_vel, Da, Db);
addToQuadrant(B, Db, 1, 0);
for (int i=0; i < 9; ++i) {
Da[i] *= fluid_density * virtual_mass[i % 3];
}
addToQuadrant(B, Da, 1, 1);
}
// torque due to motion in the fluid, D is 3x3
static inline void mjd_viscous_torque(
mjtNum* restrict D, const mjtNum lvel[6], const mjtNum fluid_density,
const mjtNum fluid_viscosity, const mjtNum size[3],
const mjtNum slender_drag_coef, const mjtNum ang_drag_coef) {
const mjtNum d_max = mju_max(mju_max(size[0], size[1]), size[2]);
const mjtNum d_min = mju_min(mju_min(size[0], size[1]), size[2]);
const mjtNum d_mid = size[0] + size[1] + size[2] - d_max - d_min;
// viscous force and torque in Stokes flow, analytical for spherical bodies
const mjtNum eq_sphere_D = 2.0/3.0 * (size[0] + size[1] + size[2]);
const mjtNum lin_visc_torq_coef = mjPI * eq_sphere_D*eq_sphere_D*eq_sphere_D;
// moments of inertia used to compute angular quadratic drag
const mjtNum I_max = 8.0/15.0 * mjPI * d_mid * (d_max*d_max)*(d_max*d_max);
const mjtNum II[3] = {
ellipsoid_max_moment(size, 0),
ellipsoid_max_moment(size, 1),
ellipsoid_max_moment(size, 2)
};
const mjtNum x = lvel[0], y = lvel[1], z = lvel[2];
const mjtNum mom_coef[3] = {
ang_drag_coef*II[0] + slender_drag_coef*(I_max - II[0]),
ang_drag_coef*II[1] + slender_drag_coef*(I_max - II[1]),
ang_drag_coef*II[2] + slender_drag_coef*(I_max - II[2])
};
const mjtNum mom_visc[3] = {
x * mom_coef[0],
y * mom_coef[1],
z * mom_coef[2]
};
const mjtNum density = fluid_density / mju_max(mjMINVAL, mju_norm3(mom_visc));
// -density * [x, y, z] * mom_coef^2
const mjtNum mom_sq[3] = {
-density * x * mom_coef[0] * mom_coef[0],
-density * y * mom_coef[1] * mom_coef[1],
-density * z * mom_coef[2] * mom_coef[2]
};
const mjtNum lin_coef = fluid_viscosity * lin_visc_torq_coef;
// initialize
mju_zero(D, 9);
// set diagonal
D[0] = D[4] = D[8] = x*mom_sq[0] + y*mom_sq[1] + z*mom_sq[2] - lin_coef;
// add outer product
mju_addToScl3(D, mom_sq, x);
mju_addToScl3(D+3, mom_sq, y);
mju_addToScl3(D+6, mom_sq, z);
}
// drag due to motion in the fluid, D is 3x3
static inline void mjd_viscous_drag(
mjtNum* restrict D, const mjtNum lvel[6], const mjtNum fluid_density,
const mjtNum fluid_viscosity, const mjtNum size[3],
const mjtNum blunt_drag_coef, const mjtNum slender_drag_coef) {
const mjtNum d_max = mju_max(mju_max(size[0], size[1]), size[2]);
const mjtNum d_min = mju_min(mju_min(size[0], size[1]), size[2]);
const mjtNum d_mid = size[0] + size[1] + size[2] - d_max - d_min;
// viscous force and torque in Stokes flow, analytical for spherical bodies
const mjtNum eq_sphere_D = 2.0/3.0 * (size[0] + size[1] + size[2]);
const mjtNum A_max = mjPI * d_max * d_mid;
const mjtNum inv_dmax = d_max > mjMINVAL ? 1.0 / d_max : 0.0;
const mjtNum s0 = size[0] * inv_dmax;
const mjtNum s1 = size[1] * inv_dmax;
const mjtNum s2 = size[2] * inv_dmax;
const mjtNum norm = mju_norm3(lvel+3);
const mjtNum inv_norm = norm > mjMINVAL ? 1.0 / norm : 0.0;
const mjtNum x = lvel[3] * inv_norm;
const mjtNum y = lvel[4] * inv_norm;
const mjtNum z = lvel[5] * inv_norm;
const mjtNum xx = x*x, yy = y*y, zz = z*z, xy=x*y, yz=y*z, xz=x*z;
const mjtNum a_raw = pow2(s1 * s2);
const mjtNum b_raw = pow2(s2 * s0);
const mjtNum c_raw = pow2(s0 * s1);
const mjtNum proj_num = a_raw * xx + b_raw * yy + c_raw * zz;
const mjtNum inv_proj_num = proj_num > mjMINVAL ? 1.0 / proj_num : 0.0;
const mjtNum a = a_raw * inv_proj_num;
const mjtNum b = b_raw * inv_proj_num;
const mjtNum c = c_raw * inv_proj_num;
const mjtNum aa = a*a, bb = b*b, cc = c*c;
const mjtNum proj_denom = aa*xx + bb*yy + cc*zz;
const mjtNum area_scale = d_max * d_max * mju_sqrt(proj_num);
const mjtNum dA_coef = proj_denom > mjMINVAL ?
mjPI * area_scale / mju_sqrt(proj_denom) : 0.0;
const mjtNum A_proj = mjPI * area_scale * mju_sqrt(proj_denom);
const mjtNum lin_coef = fluid_viscosity * 3.0 * mjPI * eq_sphere_D;
const mjtNum quad_coef = fluid_density * (
A_proj*blunt_drag_coef + slender_drag_coef*(A_max - A_proj));
const mjtNum Aproj_coef = fluid_density * norm * (blunt_drag_coef - slender_drag_coef);
const mjtNum dAproj_dv[3] = {
Aproj_coef * dA_coef * a * x * (b * yy * (a - b) + c * zz * (a - c)),
Aproj_coef * dA_coef * b * y * (a * xx * (b - a) + c * zz * (b - c)),
Aproj_coef * dA_coef * c * z * (a * xx * (c - a) + b * yy * (c - b))
};
// outer product
D[0] = xx; D[1] = xy; D[2] = xz;
D[3] = xy; D[4] = yy; D[5] = yz;
D[6] = xz; D[7] = yz; D[8] = zz;
// diag(D) += dot([x y z], [x y z])
mjtNum inner = xx + yy + zz;
D[0] += inner;
D[4] += inner;
D[8] += inner;
// scale by -quad_coef*norm
mju_scl(D, D, -quad_coef*norm, 9);
// D += outer_product(-[x y z], dAproj_dv)
mju_addToScl3(D+0, dAproj_dv, -x);
mju_addToScl3(D+3, dAproj_dv, -y);
mju_addToScl3(D+6, dAproj_dv, -z);
// diag(D) -= lin_coef
D[0] -= lin_coef;
D[4] -= lin_coef;
D[8] -= lin_coef;
}
// Kutta lift due to motion in the fluid, D is 3x3
static inline void mjd_kutta_lift(
mjtNum* restrict D, const mjtNum lvel[6], const mjtNum fluid_density,
const mjtNum size[3], const mjtNum kutta_lift_coef) {
const mjtNum d_max = mju_max(mju_max(size[0], size[1]), size[2]);
const mjtNum inv_dmax = d_max > mjMINVAL ? 1.0 / d_max : 0.0;
const mjtNum s0 = size[0] * inv_dmax;
const mjtNum s1 = size[1] * inv_dmax;
const mjtNum s2 = size[2] * inv_dmax;
const mjtNum norm = mju_norm3(lvel+3);
const mjtNum inv_norm = norm > mjMINVAL ? 1.0 / norm : 0.0;
const mjtNum x = lvel[3] * inv_norm;
const mjtNum y = lvel[4] * inv_norm;
const mjtNum z = lvel[5] * inv_norm;
const mjtNum xx = x*x, yy = y*y, zz = z*z, xy=x*y, yz=y*z, xz=x*z;
const mjtNum a_raw = pow2(s1 * s2);
const mjtNum b_raw = pow2(s2 * s0);
const mjtNum c_raw = pow2(s0 * s1);
const mjtNum proj_num = a_raw * xx + b_raw * yy + c_raw * zz;
const mjtNum inv_proj_num = proj_num > mjMINVAL ? 1.0 / proj_num : 0.0;
const mjtNum a = a_raw * inv_proj_num;
const mjtNum b = b_raw * inv_proj_num;
const mjtNum c = c_raw * inv_proj_num;
const mjtNum aa = a*a, bb = b*b, cc = c*c;
const mjtNum proj_denom = aa * xx + bb * yy + cc * zz;
const mjtNum area_scale = d_max * d_max * mju_sqrt(proj_num);
const mjtNum df_denom = proj_denom > mjMINVAL ?
mjPI * kutta_lift_coef * fluid_density * area_scale * norm / mju_sqrt(proj_denom) : 0.0;
const mjtNum dfx_coef = yy * (a - b) + zz * (a - c);
const mjtNum dfy_coef = xx * (b - a) + zz * (b - c);
const mjtNum dfz_coef = xx * (c - a) + yy * (c - b);
const mjtNum proj_term = proj_denom > mjMINVAL ? 1.0 / proj_denom : 0.0;
const mjtNum cos_term = 1.0;
// cosA = proj_num/(norm*proj_denom), A_proj = pi*sqrt(proj_denom/proj_num)
// F = cosA * A_proj * (([a,b,c] * vel) \times vel) \times vel
// derivative obtained with SymPy
D[0] = a-a; D[1] = b-a; D[2] = c-a;
D[3] = a-b; D[4] = b-b; D[5] = c-b;
D[6] = a-c; D[7] = b-c; D[8] = c-c;
mju_scl(D, D, 2.0, 9);
const mjtNum inner_term[3] = {
aa * proj_term - a + cos_term,
bb * proj_term - b + cos_term,
cc * proj_term - c + cos_term
};
mju_addToScl3(D + 0, inner_term, dfx_coef);
mju_addToScl3(D + 3, inner_term, dfy_coef);
mju_addToScl3(D + 6, inner_term, dfz_coef);
D[0] *= xx; D[1] *= xy; D[2] *= xz;
D[3] *= xy; D[4] *= yy; D[5] *= yz;
D[6] *= xz; D[7] *= yz; D[8] *= zz;
D[0] -= dfx_coef;
D[4] -= dfy_coef;
D[8] -= dfz_coef;
mju_scl(D, D, df_denom, 9);
}
// Magnus force due to motion in the fluid, B is 6x6
static inline void mjd_magnus_force(
mjtNum* restrict B, const mjtNum lvel[6], const mjtNum fluid_density,
const mjtNum size[3], const mjtNum magnus_lift_coef) {
const mjtNum volume = 4.0/3.0 * mjPI * size[0] * size[1] * size[2];
// magnus_coef = magnus_lift_coef * fluid_density * volume
const mjtNum magnus_coef = magnus_lift_coef * fluid_density * volume;
mjtNum D_lin[9], D_ang[9];
// premultiply by magnus_coef
const mjtNum lin_vel[3] = {
magnus_coef * lvel[3], magnus_coef * lvel[4], magnus_coef * lvel[5]
};
const mjtNum ang_vel[3] = {
magnus_coef * lvel[0], magnus_coef * lvel[1], magnus_coef * lvel[2]
};
// force[3:] += magnus_coef * cross(ang_vel, lin_vel)
mjd_cross(ang_vel, lin_vel, D_ang, D_lin);
addToQuadrant(B, D_ang, 1, 0);
addToQuadrant(B, D_lin, 1, 1);
}
//----------------- fluid force derivatives, ellipsoid and inertia-box models ----------------------
// fluid forces based on ellipsoid approximation
void mjd_ellipsoidFluid(const mjModel* m, mjData* d, mjtNum* res, int bodyid,
int flg_dragonly) {
mj_markStack(d);
int nv = m->nv;
int nnz = nv;
int rownnz[6], rowadr[6];
mjtNum* J = mjSTACKALLOC(d, 6*nv, mjtNum);
mjtNum* tmp = mjSTACKALLOC(d, 3*nv, mjtNum);
int* colind = mjSTACKALLOC(d, 6*nv, int);
int* colind_compressed = mjSTACKALLOC(d, 6*nv, int);
mjtNum lvel[6], wind[6], lwind[6];
mjtNum geom_interaction_coef, magnus_lift_coef, kutta_lift_coef;
mjtNum semiaxes[3], virtual_mass[3], virtual_inertia[3];
mjtNum blunt_drag_coef, slender_drag_coef, ang_drag_coef;
if (mj_isSparse(m)) {
// get sparse body Jacobian structure
nnz = mj_bodyChain(m, bodyid, colind);
// prepare rownnz, rowadr, colind for all 6 rows
for (int i=0; i < 6; i++) {
rownnz[i] = nnz;
rowadr[i] = i == 0 ? 0 : rowadr[i-1] + nnz;
for (int k=0; k < nnz; k++) {
colind_compressed[i*nnz+k] = colind[k];
}
}
}
for (int j=0; j < m->body_geomnum[bodyid]; j++) {
const int geomid = m->body_geomadr[bodyid] + j;
mju_geomSemiAxes(semiaxes, m->geom_size + 3*geomid, m->geom_type[geomid]);
readFluidGeomInteraction(
m->geom_fluid + mjNFLUID*geomid, &geom_interaction_coef,
&blunt_drag_coef, &slender_drag_coef, &ang_drag_coef,
&kutta_lift_coef, &magnus_lift_coef,
virtual_mass, virtual_inertia);
// scales all forces, read from MJCF as boolean (0.0 or 1.0)
if (geom_interaction_coef == 0.0) {
continue;
}
// map from CoM-centered to local body-centered 6D velocity
mj_objectVelocity(m, d, mjOBJ_GEOM, geomid, lvel, 1);
// compute wind in local coordinates
mju_zero(wind, 6);
mju_copy3(wind+3, m->opt.wind);
mju_transformSpatial(lwind, wind, 0,
d->geom_xpos + 3*geomid, // Frame of ref's origin.
d->subtree_com + 3*m->body_rootid[bodyid],
d->geom_xmat + 9*geomid); // Frame of ref's orientation.
// subtract translational component from grom velocity
mju_subFrom3(lvel+3, lwind+3);
// get geom global Jacobian: rotation then translation
if (mj_isSparse(m)) {
mj_jacSparse(m, d, J+3*nnz, J, d->geom_xpos+3*geomid, m->geom_bodyid[geomid], nnz, colind,
/*flg_skipcommon=*/0);
} else {
mj_jacGeom(m, d, J+3*nv, J, geomid);
}
// rotate (compressed) Jacobian to local frame
mju_mulMatTMat(tmp, d->geom_xmat+9*geomid, J, 3, 3, nnz);
mju_copy(J, tmp, 3*nnz);
mju_mulMatTMat(tmp, d->geom_xmat+9*geomid, J+3*nnz, 3, 3, nnz);
mju_copy(J+3*nnz, tmp, 3*nnz);
mjtNum B[36], D[9];
mju_zero(B, 36);
// lift and added-mass terms: asymmetric, excluded from the drag-only (metric) variant
if (!flg_dragonly) {
mjd_magnus_force(B, lvel, m->opt.density, semiaxes, magnus_lift_coef);
mjd_kutta_lift(D, lvel, m->opt.density, semiaxes, kutta_lift_coef);
addToQuadrant(B, D, 1, 1);
}
mjd_viscous_drag(D, lvel, m->opt.density, m->opt.viscosity, semiaxes,
blunt_drag_coef, slender_drag_coef);
addToQuadrant(B, D, 1, 1);
mjd_viscous_torque(D, lvel, m->opt.density, m->opt.viscosity, semiaxes,
slender_drag_coef, ang_drag_coef);
addToQuadrant(B, D, 0, 0);
if (!flg_dragonly) {
mjd_addedMassForces(B, lvel, m->opt.density, virtual_mass, virtual_inertia);
}
// make B symmetric for the metric, or if integrator is IMPLICITFAST,
// except for free rigid subtrees, including fluid geoms on fixed descendants
if (flg_dragonly ||
(m->opt.integrator == mjINT_IMPLICITFAST && !mj_isFreeBody(m, m->body_rootid[bodyid]))) {
mju_symmetrize(B, B, 6);
}
if (mj_isSparse(m)) {
addJTBJSparse(m, d, res, J, B, 6, 0, rownnz, rowadr, colind_compressed);
} else {
addJTBJ(m, d, res, J, B, 6);
}
}
mj_freeStack(d);
}
// fluid forces based on inertia-box approximation
void mjd_inertiaBoxFluid(const mjModel* m, mjData* d, mjtNum* res, int i) {
mj_markStack(d);
int nv = m->nv;
int rownnz[6], rowadr[6];
mjtNum* J = mjSTACKALLOC(d, 6*nv, mjtNum);
mjtNum* tmp = mjSTACKALLOC(d, 3*nv, mjtNum);
int* colind = mjSTACKALLOC(d, 6*nv, int);
mjtNum lvel[6], wind[6], lwind[6], box[3], B;
mjtNum* inertia = m->body_inertia + 3*i;
// equivalent inertia box
box[0] = mju_sqrt(mju_max(mjMINVAL,
(inertia[1] + inertia[2] - inertia[0])) / m->body_mass[i] * 6.0);
box[1] = mju_sqrt(mju_max(mjMINVAL,
(inertia[0] + inertia[2] - inertia[1])) / m->body_mass[i] * 6.0);
box[2] = mju_sqrt(mju_max(mjMINVAL,
(inertia[0] + inertia[1] - inertia[2])) / m->body_mass[i] * 6.0);
// map from CoM-centered to local body-centered 6D velocity
mj_objectVelocity(m, d, mjOBJ_BODY, i, lvel, 1);
// compute wind in local coordinates
mju_zero(wind, 6);
mju_copy3(wind+3, m->opt.wind);
mju_transformSpatial(lwind, wind, 0, d->xipos+3*i,
d->subtree_com+3*m->body_rootid[i], d->ximat+9*i);
// subtract translational component from body velocity
mju_subFrom3(lvel+3, lwind+3);
// init with dense
int nnz = nv;
// sparse Jacobian
if (mj_isSparse(m)) {
// get sparse body Jacobian structure
nnz = mj_bodyChain(m, i, colind);
// get sparse jacBodyCom
mj_jacSparse(m, d, J+3*nnz, J, d->xipos+3*i, i, nnz, colind, /*flg_skipcommon=*/0);
// prepare rownnz, rowadr, colind for all 6 rows
rownnz[0] = nnz;
rowadr[0] = 0;
for (int j=1; j < 6; j++) {
rownnz[j] = nnz;
rowadr[j] = rowadr[j-1] + nnz;
for (int k=0; k < nnz; k++) {
colind[j*nnz+k] = colind[k];
}
}
}
// dense Jacobian
else {
mj_jacBodyCom(m, d, J+3*nv, J, i);
}
// rotate (compressed) Jacobian to local frame
mju_mulMatTMat(tmp, d->ximat+9*i, J, 3, 3, nnz);
mju_copy(J, tmp, 3*nnz);
mju_mulMatTMat(tmp, d->ximat+9*i, J+3*nnz, 3, 3, nnz);
mju_copy(J+3*nnz, tmp, 3*nnz);
// add viscous force and torque
if (m->opt.viscosity > 0) {
// diameter of sphere approximation
mjtNum diam = (box[0] + box[1] + box[2])/3.0;
// mju_scl3(lfrc, lvel, -mjPI*diam*diam*diam*m->opt.viscosity)
B = -mjPI*diam*diam*diam*m->opt.viscosity;
for (int j=0; j < 3; j++) {
if (mj_isSparse(m)) {
addJTBJSparse(m, d, res, J, &B, 1, j, rownnz, rowadr, colind);
} else {
addJTBJ(m, d, res, J+j*nv, &B, 1);
}
}
// mju_scl3(lfrc+3, lvel+3, -3.0*mjPI*diam*m->opt.viscosity);
B = -3.0*mjPI*diam*m->opt.viscosity;
for (int j=0; j < 3; j++) {
if (mj_isSparse(m)) {
addJTBJSparse(m, d, res, J, &B, 1, 3+j, rownnz, rowadr, colind);
} else {
addJTBJ(m, d, res, J+3*nv+j*nv, &B, 1);
}
}
}
// add lift and drag force and torque
if (m->opt.density > 0) {
// lfrc[0] -= m->opt.density*box[0]*(box[1]*box[1]*box[1]*box[1]+box[2]*box[2]*box[2]*box[2])*
// mju_abs(lvel[0])*lvel[0]/64.0;
B = -m->opt.density*box[0]*(box[1]*box[1]*box[1]*box[1]+box[2]*box[2]*box[2]*box[2])*
2*mju_abs(lvel[0])/64.0;
if (mj_isSparse(m)) {
addJTBJSparse(m, d, res, J, &B, 1, 0, rownnz, rowadr, colind);
} else {
addJTBJ(m, d, res, J, &B, 1);
}
// lfrc[1] -= m->opt.density*box[1]*(box[0]*box[0]*box[0]*box[0]+box[2]*box[2]*box[2]*box[2])*
// mju_abs(lvel[1])*lvel[1]/64.0;
B = -m->opt.density*box[1]*(box[0]*box[0]*box[0]*box[0]+box[2]*box[2]*box[2]*box[2])*
2*mju_abs(lvel[1])/64.0;
if (mj_isSparse(m)) {
addJTBJSparse(m, d, res, J, &B, 1, 1, rownnz, rowadr, colind);
} else {
addJTBJ(m, d, res, J+nv, &B, 1);
}
// lfrc[2] -= m->opt.density*box[2]*(box[0]*box[0]*box[0]*box[0]+box[1]*box[1]*box[1]*box[1])*
// mju_abs(lvel[2])*lvel[2]/64.0;
B = -m->opt.density*box[2]*(box[0]*box[0]*box[0]*box[0]+box[1]*box[1]*box[1]*box[1])*
2*mju_abs(lvel[2])/64.0;
if (mj_isSparse(m)) {
addJTBJSparse(m, d, res, J, &B, 1, 2, rownnz, rowadr, colind);
} else {
addJTBJ(m, d, res, J+2*nv, &B, 1);
}
// lfrc[3] -= 0.5*m->opt.density*box[1]*box[2]*mju_abs(lvel[3])*lvel[3];
B = -0.5*m->opt.density*box[1]*box[2]*2*mju_abs(lvel[3]);
if (mj_isSparse(m)) {
addJTBJSparse(m, d, res, J, &B, 1, 3, rownnz, rowadr, colind);
} else {
addJTBJ(m, d, res, J+3*nv, &B, 1);
}
// lfrc[4] -= 0.5*m->opt.density*box[0]*box[2]*mju_abs(lvel[4])*lvel[4];
B = -0.5*m->opt.density*box[0]*box[2]*2*mju_abs(lvel[4]);
if (mj_isSparse(m)) {
addJTBJSparse(m, d, res, J, &B, 1, 4, rownnz, rowadr, colind);
} else {
addJTBJ(m, d, res, J+4*nv, &B, 1);
}
// lfrc[5] -= 0.5*m->opt.density*box[0]*box[1]*mju_abs(lvel[5])*lvel[5];
B = -0.5*m->opt.density*box[0]*box[1]*2*mju_abs(lvel[5]);
if (mj_isSparse(m)) {
addJTBJSparse(m, d, res, J, &B, 1, 5, rownnz, rowadr, colind);
} else {
addJTBJ(m, d, res, J+5*nv, &B, 1);
}
}
mj_freeStack(d);
}
//------------------------- derivatives of passive forces ------------------------------------------
// add (d qfrc_passive / d qvel) to qDeriv
void mjd_passive_vel(const mjModel* m, mjData* d) {
// all disabled: nothing to add
if (mjDISABLED(mjDSBL_SPRING) && mjDISABLED(mjDSBL_DAMPER)) {
return;
}
int sleep_filter = mjENABLED(mjENBL_SLEEP) && d->ntree_awake < m->ntree;
int nbody = sleep_filter ? d->nbody_awake : m->nbody;
// fluid drag model, either body-level (inertia box) or geom-level (ellipsoid)
if (m->opt.viscosity > 0 || m->opt.density > 0) {
for (int b=0; b < nbody; b++) {
int i = sleep_filter ? d->body_awake_ind[b] : b;
if (m->body_mass[i] < mjMINVAL) {
continue;
}
int use_ellipsoid_model = 0;
// if any child geom uses the ellipsoid model, inertia-box model is disabled for parent body
for (int j=0; j < m->body_geomnum[i] && use_ellipsoid_model == 0; j++) {
const int geomid = m->body_geomadr[i] + j;
use_ellipsoid_model += (m->geom_fluid[mjNFLUID*geomid] > 0);
}
if (use_ellipsoid_model) {
mjd_ellipsoidFluid(m, d, d->qDeriv, i, /*flg_dragonly=*/0);
} else {
mjd_inertiaBoxFluid(m, d, d->qDeriv, i);
}
}
}
// disabled: nothing to add
if (mjDISABLED(mjDSBL_DAMPER)) {
return;
}
// dof damping
int nv = m->nv;
int nv_awake = sleep_filter ? d->nv_awake : nv;
for (int j = 0; j < nv_awake; j++) {
int i = sleep_filter ? d->dof_awake_ind[j] : j;
mjtNum v = d->qvel[i];
mjtNum poly[mjNPOLY];
mju_copy(poly, m->dof_dampingpoly + mjNPOLY*i, mjNPOLY);
mjtNum damping = m->dof_damping[i] + mj_actuatorDamping(m, mjOBJ_JOINT, m->dof_jntid[i], poly);
int adr = m->D_rowadr[i] + m->D_diag[i];
d->qDeriv[adr] -= mjd_xPolyForce(damping, poly, v, mjNPOLY, 1);
}
// flex edge damping
for (int f=0; f < m->nflex; f++) {
mjtNum B = -m->flex_edgedamping[f];
if (m->flex_rigid[f] || !B) {
continue;
}
int flex_edgeadr = m->flex_edgeadr[f];
int flex_edgenum = m->flex_edgenum[f];
// process non-rigid edges of this flex
for (int e=flex_edgeadr; e < flex_edgeadr+flex_edgenum; e++) {
// skip rigid
if (m->flexedge_rigid[e]) {
continue;
}
// always sparse
addJTBJSparse(m, d, d->qDeriv, d->flexedge_J, &B, 1, e,
m->flexedge_J_rownnz, m->flexedge_J_rowadr, m->flexedge_J_colind);
}
}
// tendon damping
int ntendon = m->ntendon;
for (int i=0; i < ntendon; i++) {
// skip tendon in one or two sleeping trees
if (sleep_filter) {
int treenum = m->tendon_treenum[i];
int id1 = m->tendon_treeid[2*i];
if (treenum == 1 && !d->tree_awake[id1]) continue;
int id2 = m->tendon_treeid[2*i+1];
if (treenum == 2 && !d->tree_awake[id1] && !d->tree_awake[id2]) continue;
}
mjtNum v = d->ten_velocity[i];
mjtNum poly[mjNPOLY];
mju_copy(poly, m->tendon_dampingpoly+mjNPOLY*i, mjNPOLY);
mjtNum damping = m->tendon_damping[i] + mj_actuatorDamping(m, mjOBJ_TENDON, i, poly);
mjtNum B = -mjd_xPolyForce(damping, poly, v, mjNPOLY, 1);
if (!B) {
continue;
}
// add sparse
addJTBJSparse(m, d, d->qDeriv, d->ten_J, &B, 1, i,
m->ten_J_rownnz, m->ten_J_rowadr, m->ten_J_colind);
}
}
//------------------------- main entry points ------------------------------------------------------
// analytical derivative of smooth forces w.r.t velocities:
// d->qDeriv = d (qfrc_actuator + qfrc_passive - [qfrc_bias]) / d qvel
void mjd_smooth_vel(const mjModel* m, mjData* d, int flg_bias) {
int sleep_filter = mjENABLED(mjENBL_SLEEP) && d->nv_awake < m->nv;
// clear qDeriv
if (!sleep_filter) {
mju_zero(d->qDeriv, m->nD);
} else {
mju_zeroSparse(d->qDeriv, m->D_rownnz, m->D_rowadr, d->dof_awake_ind, d->nv_awake);
}
// qDeriv += d qfrc_actuator / d qvel
mjd_actuator_vel(m, d);
// qDeriv += d qfrc_passive / d qvel
mjd_passive_vel(m, d);
// qDeriv -= d qfrc_bias / d qvel; optional
if (flg_bias) {
mjd_rne_vel(m, d);
}
}
//------------------- implicit effective metric Mtilde = M + h*D + h^2*K --------------------------
// The flex part is K = (h^2 + h*damping) * (K_bend + K_stretch), the PSD implicit flex stiffness,
// whose h^2 and h*damping parts enter only when the spring and damper forces are enabled;
// the diagonal part holds joint damping and stiffness (h*D + h^2*K per dof, clamped PSD). Built
// once per step on the arena by mjd_effBuild (under integrator=discrete), then consumed
// uniformly: the smooth acceleration, the constraint solver and inverse dynamics all see the
// same metric.
// arena allocation with hard failure (mirrors stack overflow semantics)
static void* effAlloc(mjData* d, size_t bytes, size_t align) {
void* p = mj_arenaAllocByte(d, bytes, align);
if (!p) {
mjERROR("arena overflow in implicit effective metric");
}
return p;
}
#define EFMALLOC(type, n) (type*) effAlloc(d, sizeof(type)*(size_t)(n), _Alignof(type))
// yield the next live rank-1 term of the metric; see the declaration for the contract
int mjd_effRank1Next(const mjModel* m, const mjData* d, mjEffRank1Iter* it,
mjEffRank1* e, int flg_contact) {
// class 0: one entry per tendon with metric terms
while (it->cls == 0) {
if (it->i >= d->nefmT) {
it->cls = 1;
it->i = 0;
break;
}
int t = it->i++;
mjtNum s = d->efm_ts[t];
int id = d->efm_tid[t];
int nnz = m->ten_J_rownnz[id];
if (!s || !nnz) {
continue;
}
e->val = d->ten_J + m->ten_J_rowadr[id];
e->colind = m->ten_J_colind + m->ten_J_rowadr[id];
e->nnz = nnz;
e->scale = s;
return 1;
}
// class 1: one entry per output row of each actuator with metric terms
while (it->cls == 1) {
if (it->i >= d->nefmA) {
it->cls = 2;
it->i = 0;
it->k = 0; // class 2 walks the packed rows by offset
break;
}
int id = d->efm_aid[it->i];
if (it->k >= m->actuator_outnum[id]) {
it->i++;
it->k = 0;
continue;
}
mjtNum s = d->efm_as[it->i];
int r = m->actuator_outadr[id] + it->k++;
int nnz = d->moment_rownnz[r];
if (!s || !nnz) {
continue;
}
e->val = d->actuator_moment + d->moment_rowadr[r];
e->colind = d->moment_colind + d->moment_rowadr[r];
e->nnz = nnz;
e->scale = s;
return 1;
}
// class 2: one entry per passive flex contact, published by effContactBuild. Skipped
// wholesale when the caller accounts for contact separately
while (it->cls == 2) {
if (!flg_contact || it->k >= d->nefmcon) {
it->cls = 3; // not a producer class: the cursor's done state
break;
}
int adr = it->k, nnz = d->efm_con_ind[adr]; // packed row: header then entries
e->nnz = nnz;
e->scale = d->efm_con_val[adr];
e->colind = d->efm_con_ind + adr + 2;
e->val = d->efm_con_val + adr + 2;
it->k = adr + 2 + nnz;
return 1;
}
return 0;
}
// res += B*vec, the stiffness part of the metric. The per-dof diagonal classes are applied
// from efm_diag (scales pre-folded); the stretch (and, when assemblable, bending and interp)
// part from the per-step CSR; terms not in the CSR fall back to the matrix-free operators.
void mjd_effMulAdd(const mjModel* m, mjData* d, mjtNum* res, const mjtNum* vec, int flg_contact) {
mjtNum h = m->opt.timestep;
if (d->efm_diag) {
int nv = m->nv;
for (int i=0; i < nv; i++) {
res[i] += d->efm_diag[i] * vec[i];
}
}
// fluid drag blocks: res += F*vec, F symmetric in M's lower-triangle pattern
if (d->efm_fluid) {
int nv = m->nv;
for (int i=0; i < nv; i++) {
int start = m->M_rowadr[i];
int diag = start + m->M_rownnz[i] - 1;
mjtNum acc = d->efm_fluid[diag] * vec[i];
for (int a=start; a < diag; a++) {
int c = m->M_colind[a];
mjtNum F = d->efm_fluid[a];
acc += F * vec[c];
res[c] += F * vec[i];
}
res[i] += acc;
}
}
// rank-1 terms (tendon, actuator): res += scale * row' * (row * vec)
mjEffRank1Iter it = {0};
mjEffRank1 e;
while (mjd_effRank1Next(m, d, &it, &e, /*flg_contact=*/0)) {
mjtNum dot = 0;
for (int j=0; j < e.nnz; j++) {
dot += e.val[j] * vec[e.colind[j]];
}
dot *= e.scale;
for (int j=0; j < e.nnz; j++) {
res[e.colind[j]] += dot * e.val[j];
}
}
// contact class: the same rank-1 apply over the packed rows, walked inline; the iterator's
// per-row call is measurable with thousands of published pairs
if (flg_contact) {
const int* ind = d->efm_con_ind;
const mjtNum* val = d->efm_con_val;
for (int adr=0; adr < d->nefmcon; ) {
int nnz = ind[adr];
const int* ci = ind + adr + 2;
const mjtNum* rv = val + adr + 2;
mjtNum dot = 0;
for (int j=0; j < nnz; j++) {
dot += rv[j] * vec[ci[j]];
}
dot *= val[adr];
for (int j=0; j < nnz; j++) {
res[ci[j]] += dot * rv[j];
}
adr += 2 + nnz;
}
}
if (d->nefmK && d->nefmdof) {
// the stiffness rows come in vertex triples with 3x3 blocks per neighbour (the assembly
// writes the three dofs of each neighbour in turn), so one column index per block and the
// three rows share the vector loads
const int* rowadr = d->efm_K_rowadr;
const int* rownnz = d->efm_K_rownnz;
const int* colind = d->efm_K_colind;
const mjtNum* val = d->efm_K_val;
for (int k=0; k < d->nefmdof; k++) {
int i = d->efm_dofid[k];
const mjtNum* v0 = val + rowadr[i];
const mjtNum* v1 = val + rowadr[i+1];
const mjtNum* v2 = val + rowadr[i+2];
const int* ci = colind + rowadr[i];
int nn = rownnz[i] / 3;
mjtNum r0 = 0, r1 = 0, r2 = 0;
for (int j=0; j < nn; j++) {
int c = ci[3*j];
mjtNum x0 = vec[c], x1 = vec[c+1], x2 = vec[c+2];
r0 += v0[3*j]*x0 + v0[3*j+1]*x1 + v0[3*j+2]*x2;
r1 += v1[3*j]*x0 + v1[3*j+1]*x1 + v1[3*j+2]*x2;
r2 += v2[3*j]*x0 + v2[3*j+1]*x1 + v2[3*j+2]*x2;
}
res[i] += r0;
res[i+1] += r1;
res[i+2] += r2;
}
} else if (d->nefmK) {
int nv = m->nv;
for (int i=0; i < nv; i++) {
int nnz = d->efm_K_rownnz[i];
if (!nnz) {
continue;
}
res[i] += mju_dotSparse(d->efm_K_val + d->efm_K_rowadr[i], vec, nnz,
d->efm_K_colind + d->efm_K_rowadr[i]);
}
}
// A standard flex is assembled only when all its attachments have fixed-frame XYZ
// mappings. Dispatch general flexes individually: another flex's CSR must not suppress
// their operators. Both the stiffness and damping parts follow the passive-force flags.
mjtNum s1 = mjDISABLED(mjDSBL_SPRING) ? 0 : h*h;
mjtNum s2 = mjDISABLED(mjDSBL_DAMPER) ? 0 : h;
for (int f=0; f < m->nflex; f++) {
if (m->flex_interp[f] || m->flex_rigid[f] || m->flex_dim[f] < 2) continue;
if (!d->nefmK || !mj_flexSimple(m, f)) {
flexBend_mul(m, d, res, vec, s1, s2, f, f+1);
flexStretch_mul(m, d, res, vec, s1, s2, f, f+1);
}
}
if (!d->nefmK || !mjd_flexInterpAssemblable(m)) {
mjd_flexInterp_mul(m, d, res, vec, -s1, -s2, d->flexelem_krot);
}
}
// the unfactored 3x3 diagonal block of M + K for the covered dof triple starting at i
static void effBlockRaw(const mjModel* m, const mjData* d, int i, mjtNum* Bk) {
mju_zero(Bk, 9);
for (int r = 0; r < 3; r++) {
int row = i + r;
for (int a = m->M_rowadr[row]; a < m->M_rowadr[row] + m->M_rownnz[row]; a++) {
int c = m->M_colind[a];
if (c >= i && c < i+3) Bk[3*r + (c-i)] += d->M[a];
}
for (int a = d->efm_K_rowadr[row]; a < d->efm_K_rowadr[row] + d->efm_K_rownnz[row]; a++) {
int c = d->efm_K_colind[a];
if (c >= i && c < i+3) Bk[3*r + (c-i)] += d->efm_K_val[a];
}
}
}
// Build and factor the per-vertex 3x3 diagonal blocks of the flex part of (M + K), stored in
// d->efm_L, 9 numbers per covered vertex: O(n) to build and apply, approximate where the sparse
// factorization it replaces was exact. Both consumers use the blocks as a preconditioner: the CG
// constraint solver (Mgrad = Mtilde \ grad) and the qacc_smooth PCG in mjd_effSolve, which
// supplies the accuracy.
static void effBlocks(const mjModel* m, mjData* d) {
int nv = m->nv;
// covered dofs come in contiguous triples (the 3 slide dofs of one flex point), but the first
// one need not be at a multiple of 3: any joint declared before the flex shifts them. Walk the
// covered rows rather than striding the dof index, which would straddle point boundaries.
int nb = 0;
for (int i = 0; i < nv; ) {
if (d->efm_K_rownnz[i]) { nb++; i += 3; } else { i++; }
}
d->nefmdof = 0;
mjtNum* B = (mjtNum*) effAlloc(d, sizeof(mjtNum)*9*(nb > 0 ? nb : 1), _Alignof(mjtNum));
int* adr = (int*) effAlloc(d, sizeof(int)*(nb > 0 ? nb : 1), _Alignof(int));
int k = 0;
for (int i = 0; i < nv; ) {
if (!d->efm_K_rownnz[i]) {
i++;
continue;
}
mjtNum* Bk = B + 9*k;
effBlockRaw(m, d, i, Bk);
mju_cholFactor(Bk, 3, mjMINVAL);
adr[k++] = i;
i += 3;
}
d->efm_L = B;
d->efm_dofid = adr;
d->nefmdof = nb;
d->nefmL = 9*nb;
}
// Apply the metric preconditioner: the per-step 3x3 blocks when they exist, else the constant
// bending factor from mj_setConst, on the dofs they cover; M^-1 on all other dofs. PCG requires
// symmetry, so covered and uncovered dofs must not see each other: zeroing the covered entries
// of the right-hand side before the qLD sweep keeps the uncovered rows from reading them.
// x = (L L') \ b for one 3x3 factor from mju_cholFactor, in mju_cholSolve's arithmetic; b may
// alias x
static inline void chol3Solve(mjtNum* x, const mjtNum* L, const mjtNum* b) {
mjtNum r0 = b[0] / L[0];
mjtNum r1 = (b[1] - L[3]*r0) / L[4];
mjtNum r2 = (b[2] - (L[6]*r0 + L[7]*r1)) / L[8];
r2 /= L[8];
r1 = (r1 - L[7]*r2) / L[4];
r0 -= L[3]*r1;
r0 -= L[6]*r2;
r0 /= L[0];
x[0] = r0;
x[1] = r1;
x[2] = r2;
}
static void effBlockApply(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* b,
const mjtNum* L) {
int nv = m->nv;
int nbd = m->nefm0dof;
int flg_bend = nbd && !d->nefmdof;
// every dof a covered triple: the blocks are the whole preconditioner
if (3*d->nefmdof == nv && !flg_bend) {
for (int k = 0; k < d->nefmdof; k++) {
int i = d->efm_dofid[k];
chol3Solve(x + i, L + 9*k, b + i);
}
return;
}
mj_markStack(d);
mjtNum* rhs = mjSTACKALLOC(d, nv, mjtNum);
mju_copy(rhs, b, nv); // b may alias x, which the sweep below overwrites
// dofs no factor covers: backbone solve, using the qH backbone factor when diagonal
// terms exist, else qLD
mju_copy(x, rhs, nv);
for (int k = 0; k < d->nefmdof; k++) {
mju_zero(x + d->efm_dofid[k], 3);
}
if (flg_bend) {
for (int i = 0; i < nbd; i++) {
x[m->efm0_dofid[i]] = 0;
}
}
// TODO(team): restrict solve to awake trees when island sleep filtering is active
if (d->efm_diag) {
mj_solveLD(x, d->qH, d->qHDiagInv, nv, 1, m->M_rownnz, m->M_rowadr, m->M_colind, NULL);
} else {
mj_solveLD(x, d->qLD, d->qLDiagInv, nv, 1, m->M_rownnz, m->M_rowadr, m->M_colind, NULL);
}
// per-step stiffness: 3x3 blocks
for (int k = 0; k < d->nefmdof; k++) {
int i = d->efm_dofid[k];
chol3Solve(x + i, L + 9*k, rhs + i);
}
// bending-only: exact (M + K_bend)^-1 on the dofs the constant factor covers
if (flg_bend) {
mjtNum* bfr = mjSTACKALLOC(d, nbd, mjtNum);
mjtNum* bfz = mjSTACKALLOC(d, nbd, mjtNum);
for (int i = 0; i < nbd; i++) {
bfr[i] = rhs[m->efm0_dofid[i]];
}
mju_cholSolveSparse(bfz, m->efm0_L, bfr, nbd,
m->efm0_L_rownnz, m->efm0_L_rowadr, m->efm0_L_colind);
for (int i = 0; i < nbd; i++) {
x[m->efm0_dofid[i]] = bfz[i];
}
}
mj_freeStack(d);
}
// accurate solve of (M + K) x = b by PCG with the 3x3 block preconditioner, converging the
// relative residual to opt.tolerance; used for qacc_smooth. Reaching opt.iterations means the
// metric is too ill-conditioned for the blocks: warn (mjWARN_INERTIA, worst-residual dof) and
// return x under-converged.
void mjd_effSolve(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* b) {
if (!d->efm_active) {
mjd_effPrec(m, d, x, b);
return;
}
// backbone-only metric (no tendon, actuator or flex couplings): the qH solve inside the
// block-apply is the exact metric solve, skip the iteration. Beyond the saved iterations,
// the direct solve is block-decoupled across trees, which sleeping trees rely on
int flex_any = 0;
for (int f=0; f < m->nflex; f++) {
flex_any = flex_any || mj_effFlexPossible(m, f);
}
if (!d->nefmT && !d->nefmA && !flex_any) {
effBlockApply(m, d, x, b, d->efm_L);
return;
}
int nv = m->nv;
mj_markStack(d);
mjtNum* r = mjSTACKALLOC(d, nv, mjtNum);
mjtNum* z = mjSTACKALLOC(d, nv, mjtNum);
mjtNum* p = mjSTACKALLOC(d, nv, mjtNum);
mjtNum* Ap = mjSTACKALLOC(d, nv, mjtNum);
mju_copy(r, b, nv); // before zeroing x: b may alias x
mju_zero(x, nv);
mjtNum bn = mju_dot(r, r, nv);
if (bn > mjMINVAL) {
// converge the relative residual to opt.tolerance; both sides are squared norms
#ifdef mjUSESINGLE
// float cannot reach a 1e-8 relative residual (eps ~1.2e-7): without a floor every step of
// every covered model would run to opt.iterations and then warn.
mjtNum tolerance = mju_max(m->opt.tolerance, 1e-5);
#else
mjtNum tolerance = m->opt.tolerance;
#endif
mjtNum tol = tolerance*tolerance*bn;
int capped = 1; // cleared by either exit below; still set means the cap was reached
effBlockApply(m, d, z, r, d->efm_L);
mju_copy(p, z, nv);
mjtNum rz = mju_dot(r, z, nv);
for (int it = 0; it < m->opt.iterations; it++) {
mju_mulSymVecSparse(Ap, d->M, p, nv, m->M_rownnz, m->M_rowadr, m->M_colind);
mjd_effMulAdd(m, d, Ap, p, /*flg_contact=*/1);
mjtNum pAp = mju_dot(p, Ap, nv);
// curvature breakdown: the metric has no curvature along p, so no further progress is
// possible and x is the best available. Not a budget failure, so it does not warn.
if (pAp <= 0) { capped = 0; break; }
mjtNum alpha = rz/pAp;
mju_addToScl(x, p, alpha, nv);
mju_addToScl(r, Ap, -alpha, nv);
if (mju_dot(r, r, nv) < tol) { capped = 0; break; }
effBlockApply(m, d, z, r, d->efm_L);
mjtNum rznew = mju_dot(r, z, nv);
mju_addScl(p, z, p, rznew/rz, nv);
rz = rznew;
}
// Ran out of iterations with the residual still above tolerance: the metric is too
// ill-conditioned for the blocks to solve within the budget. Blame the dof carrying the
// largest residual.
// mjWARN_INERTIA is the closest existing warning (reusing it avoids an ABI addition), but on
// its own it points the user at their inertia when the cause is the flex stiffness, so say so
// first. Gate on the same first-time condition mj_warning uses, or a model that fails every
// step would print this thousands of times a second.
if (capped && mju_dot(r, r, nv) >= tol) {
int worst = 0;
for (int i = 1; i < nv; i++) {
if (mju_abs(r[i]) > mju_abs(r[worst])) worst = i;
}
if (!d->warning[mjWARN_INERTIA].number) {
mju_warning("Flex stiffness is too ill-conditioned for the effective-metric block "
"preconditioner: the M+K solve ran out of iterations at a relative residual "
"of %.2e and qacc_smooth is under-converged. Reported as a singular inertia "
"below, because M+K is the effective inertia.",
mju_sqrt(mju_dot(r, r, nv)/bn));
}
mj_warning(d, mjWARN_INERTIA, worst);
}
}
mj_freeStack(d);
}
void mjd_effPrec(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* b) {
int nv = m->nv;
// active metric: the prefactored 3x3 blocks are the preconditioner
if (d->efm_active) {
effBlockApply(m, d, x, b, d->efm_L);
return;
}
// inactive metric: x = M \ b, which is exact
if (x != b) {
mju_copy(x, b, nv);
}
mj_solveLD(x, d->qLD, d->qLDiagInv, nv, 1, m->M_rownnz, m->M_rowadr, m->M_colind, NULL);
}
// fold the metric's rank-1 classes and the efc rows (quadratic zone) into a copy of the
// preconditioner blocks, factored into L (9*nefmdof); only each term's per-vertex 3x3 diagonal
// survives, as for the elastic part. Returns 0 when nothing is covered, leaving L untouched.
int mjd_effPrecFold(const mjModel* m, mjData* d, mjtNum* L,
int nefc, const mjtNum* efc_D, int is_sparse,
const mjtNum* J, const int* J_rownnz, const int* J_rowadr,
const int* J_colind) {
if (!d->nefmdof) {
return 0;
}
mj_markStack(d);
int nv = m->nv;
mjtNum* Badd = mjSTACKALLOC(d, 9*d->nefmdof, mjtNum);
int* blk = mjSTACKALLOC(d, nv, int);
mju_zero(Badd, 9*d->nefmdof);
for (int i=0; i < nv; i++) {
blk[i] = -1;
}
for (int k=0; k < d->nefmdof; k++) {
for (int c=0; c < 3; c++) {
blk[d->efm_dofid[k] + c] = k;
}
}
// rank-1 classes: scale * v v', restricted to each covered block. A term's coupling between
// DIFFERENT vertices is off-diagonal and cannot be represented here; only its self-terms land
mjEffRank1Iter it = {0};
mjEffRank1 e;
while (mjd_effRank1Next(m, d, &it, &e, /*flg_contact=*/1)) {
for (int a=0; a < e.nnz; a++) {
int ia = e.colind[a], k = blk[ia];
if (k < 0) {
continue;
}
int base = d->efm_dofid[k];
for (int b=0; b < e.nnz; b++) {
int ib = e.colind[b];
if (blk[ib] == k) {
Badd[9*k + 3*(ia-base) + (ib-base)] += e.scale * e.val[a] * e.val[b];
}
}
}
}
// efc rows: the same blocks of J'*D*J, over every row the solve carries, active or not. The
// rows are the pairs inside their wall, the likely active set, and rows switch state in
// nearly every CG iteration, so a fold of the rows active at the start left the CG with a
// preconditioner that matched neither iterate: on the reef knot the mean iterations per
// solve fell from 212 to 59 when every row was folded
for (int r=0; r < nefc; r++) {
if (!efc_D[r]) {
continue;
}
mjtNum D = efc_D[r];
if (is_sparse) {
int adr = J_rowadr[r], nnz = J_rownnz[r];
for (int a=0; a < nnz; a++) {
int ia = J_colind[adr+a], k = blk[ia];
if (k < 0) {
continue;
}
int base = d->efm_dofid[k];
for (int b=0; b < nnz; b++) {
int ib = J_colind[adr+b];
if (blk[ib] == k) {
Badd[9*k + 3*(ia-base) + (ib-base)] += D * J[adr+a] * J[adr+b];
}
}
}
} else {
const mjtNum* Jr = J + (size_t)r*nv;
for (int k=0; k < d->nefmdof; k++) {
int base = d->efm_dofid[k];
for (int a=0; a < 3; a++) {
if (!Jr[base+a]) {
continue;
}
for (int b=0; b < 3; b++) {
Badd[9*k + 3*a + b] += D * Jr[base+a] * Jr[base+b];
}
}
}
}
}
for (int k=0; k < d->nefmdof; k++) {
mjtNum* Bk = L + 9*k;
effBlockRaw(m, d, d->efm_dofid[k], Bk);
mju_addTo(Bk, Badd + 9*k, 9);
mju_cholFactor(Bk, 3, mjMINVAL);
}
mj_freeStack(d);
return 1;
}
// mjd_effPrec against caller-supplied factored blocks instead of the shared d->efm_L
void mjd_effPrecBlocks(const mjModel* m, mjData* d, mjtNum* x, const mjtNum* b,
const mjtNum* L) {
if (d->efm_active) {
effBlockApply(m, d, x, b, L);
return;
}
mjd_effPrec(m, d, x, b);
}
// can this dof contribute a damping term to the metric diagonal (model-level check;
// values are velocity-dependent and computed in mjd_effShift)
static int dofDampPossible(const mjModel* m, int i) {
if (mjDISABLED(mjDSBL_DAMPER)) {
return 0;
}
return m->dof_damping[i] > 0 ||
!mju_isZero(m->dof_dampingpoly + mjNPOLY*i, mjNPOLY) ||
m->jnt_actuatorid[m->dof_jntid[i]] != -1;
}
// fill efm_ck with h * d(spring force)/d(displacement) per dof, clamped PSD-safe;
// mirrors the joint-spring structure of mj_springdamper; returns any-nonzero
static int effDiagStiff(const mjModel* m, mjData* d) {
int any = 0, njnt = m->njnt;
mjtNum h = m->opt.timestep;
mju_zero(d->efm_ck, m->nv);
if (mjDISABLED(mjDSBL_SPRING)) {
return 0;
}
for (int j=0; j < njnt; j++) {
mjtNum stiffness = m->jnt_stiffness[j];
const mjtNum* spoly = m->jnt_stiffnesspoly + mjNPOLY*j;
if (stiffness == 0 && mju_isZero(spoly, mjNPOLY)) {
continue;
}
int padr = m->jnt_qposadr[j];
int dadr = m->jnt_dofadr[j];
mjtNum k;
switch ((mjtJoint) m->jnt_type[j]) {
case mjJNT_FREE:
// translation: radial derivative of the spring, isotropic on the 3 slide dofs
{
mjtNum dif[3];
mju_sub3(dif, d->qpos+padr, m->qpos_spring+padr);
k = mju_max(0, mjd_xPolyForce(stiffness, spoly, mju_norm3(dif), mjNPOLY, 0));
d->efm_ck[dadr] = d->efm_ck[dadr+1] = d->efm_ck[dadr+2] = h*k;
any = any || k > 0;
}
// continue with rotations
dadr += 3;
padr += 3;
mjFALLTHROUGH;
case mjJNT_BALL:
// rotation: small-angle isotropic treatment of the quaternion spring
{
mjtNum dif[3], quat[4];
mju_copy4(quat, d->qpos+padr);
mju_normalize4(quat);
mju_subQuat(dif, quat, m->qpos_spring + padr);
k = mju_max(0, mjd_xPolyForce(stiffness, spoly, mju_norm3(dif), mjNPOLY, 0));
d->efm_ck[dadr] = d->efm_ck[dadr+1] = d->efm_ck[dadr+2] = h*k;
any = any || k > 0;
}
break;
case mjJNT_SLIDE:
case mjJNT_HINGE:
{
mjtNum x = d->qpos[padr] - m->qpos_spring[padr];
k = mju_max(0, mjd_xPolyForce(stiffness, spoly, x, mjNPOLY, 0));
d->efm_ck[dadr] = h*k;
any = any || k > 0;
}
break;
}
}
return any;
}
// finalize efm_diag = h*D + h^2*K: the damping derivative is velocity-dependent,
// evaluated here at the velocity stage, matching mj_EulerSkip's coverage but clamped
static void effDiagDamp(const mjModel* m, mjData* d) {
int nv = m->nv;
mjtNum h = m->opt.timestep;
int damper = !mjDISABLED(mjDSBL_DAMPER);
for (int i=0; i < nv; i++) {
mjtNum damp_deriv = 0;
if (damper) {
mjtNum poly[mjNPOLY];
mju_copy(poly, m->dof_dampingpoly + mjNPOLY*i, mjNPOLY);
mjtNum damping = m->dof_damping[i]
+ mj_actuatorDamping(m, mjOBJ_JOINT, m->dof_jntid[i], poly);
damp_deriv = mju_max(0, mjd_xPolyForce(damping, poly, d->qvel[i], mjNPOLY, 1));
}
d->efm_diag[i] = h*damp_deriv + h*d->efm_ck[i];
}
}
// refresh the velocity-stage values of the active metric: the smooth-force shift
// c = -h*K*qvel and the diagonal (its damping part is velocity-dependent). No allocation
// here, mirroring the efc value refresh pattern. Called from the velocity stage only,
// after the derived velocities (ten_velocity, body velocities) it reads are computed;
// every consumer of the values runs at the actuation stage or later
void mjd_effShift(const mjModel* m, mjData* d) {
if (!d->efm_active) {
return;
}
mjtNum h = m->opt.timestep;
mju_zero(d->efm_c, m->nv);
// flex stiffness shift, absent when the spring force is disabled
if (!mjDISABLED(mjDSBL_SPRING)) {
mjd_flexInterp_mul(m, d, d->efm_c, d->qvel, h, 0, d->flexelem_krot);
mjd_flexBend_mul(m, d, d->efm_c, d->qvel, -h, 0);
mjd_flexStretch_mul(m, d, d->efm_c, d->qvel, -h, 0);
}
// contact: -h*K_contact*v from the packed rank-1 rows (see effContactBuild), so the shift
// and the operator apply one definition of contact. Each row's scale already carries the h^2
// of the effective stiffness, so the shift's -h*K*v is -(1/h) * scale * row'(row.v)
for (int adr=0; adr < d->nefmcon; ) {
int nnz = d->efm_con_ind[adr];
const int* colind = d->efm_con_ind + adr + 2;
const mjtNum* val = d->efm_con_val + adr + 2;
mjtNum dot = 0;
for (int a=0; a < nnz; a++) {
dot += val[a] * d->qvel[colind[a]];
}
dot *= -d->efm_con_val[adr] / h;
for (int a=0; a < nnz; a++) {
d->efm_c[colind[a]] += dot * val[a];
}
adr += 2 + nnz;
}
// per-dof diagonal classes
if (d->efm_diag) {
int nv = m->nv;
effDiagDamp(m, d);
// stiffness shift
for (int i=0; i < nv; i++) {
d->efm_c[i] -= d->efm_ck[i] * d->qvel[i];
}
}
// tendon values: stiffness at the current deadband displacement, damping at the current
// tendon velocity, both clamped PSD-safe, mirroring the passive-force expressions
for (int t=0; t < d->nefmT; t++) {
int i = d->efm_tid[t];
mjtNum k = 0;
if (!mjDISABLED(mjDSBL_SPRING)) {
mjtNum length = d->ten_length[i];
mjtNum lower = m->tendon_lengthspring[2*i];
mjtNum upper = m->tendon_lengthspring[2*i+1];
mjtNum x = (length > upper) ? length - upper : (length < lower) ? length - lower : 0;
if (x) {
k = mju_max(0, mjd_xPolyForce(m->tendon_stiffness[i],
m->tendon_stiffnesspoly + mjNPOLY*i, x, mjNPOLY, 0));
}
}
mjtNum b = 0;
if (!mjDISABLED(mjDSBL_DAMPER)) {
mjtNum dpoly[mjNPOLY];
mju_copy(dpoly, m->tendon_dampingpoly + mjNPOLY*i, mjNPOLY);
mjtNum damping = m->tendon_damping[i] + mj_actuatorDamping(m, mjOBJ_TENDON, i, dpoly);
b = mju_max(0, mjd_xPolyForce(damping, dpoly, d->ten_velocity[i], mjNPOLY, 1));
}
d->efm_ts[t] = h*h*k + h*b;
d->efm_tk[t] = h*k;
// stiffness shift: c -= h*k * ten_velocity * J'
if (k) {
mjtNum ckv = d->efm_tk[t] * d->ten_velocity[i];
int end = m->ten_J_rowadr[i] + m->ten_J_rownnz[i];
for (int j=m->ten_J_rowadr[i]; j < end; j++) {
d->efm_c[m->ten_J_colind[j]] -= ckv * d->ten_J[j];
}
}
}
// fluid drag blocks: assemble the drag-only passive-fluid velocity derivative into the
// assemble the drag derivatives into stack scratch in qDeriv's shape (qDeriv itself is
// user-facing), gather into M's sparsity pattern, scale by -h. Drag derivatives are
// symmetric dissipative by construction; lift and added-mass terms are excluded and
// integrate explicitly
if (d->efm_fluid) {
mj_markStack(d);
mjtNum* scratch = mjSTACKALLOC(d, m->nD, mjtNum);
mju_zero(scratch, m->nD);
int sleep_filter = mjENABLED(mjENBL_SLEEP) && d->ntree_awake < m->ntree;
int nbody = sleep_filter ? d->nbody_awake : m->nbody;
for (int b=0; b < nbody; b++) {
int i = sleep_filter ? d->body_awake_ind[b] : b;
if (m->body_mass[i] < mjMINVAL) {
continue;
}
int use_ellipsoid_model = 0;
for (int j=0; j < m->body_geomnum[i] && use_ellipsoid_model == 0; j++) {
use_ellipsoid_model += (m->geom_fluid[mjNFLUID*(m->body_geomadr[i] + j)] > 0);
}
if (use_ellipsoid_model) {
mjd_ellipsoidFluid(m, d, scratch, i, /*flg_dragonly=*/1);
} else {
mjd_inertiaBoxFluid(m, d, scratch, i);
}
}
mju_gather(d->efm_fluid, scratch, m->mapD2M, m->nC);
mju_scl(d->efm_fluid, d->efm_fluid, -m->opt.timestep, m->nC);
mj_freeStack(d);
}
}
// actuation-stage refresh of the metric: actuator gain scalars (ctrl- and state-dependent,
// so evaluated after mj_fwdActuation), their smooth-force shift, and the backbone factor
// qH = M + diag(h*D + h^2*K) + tendon and actuator diagonals. The backbone is the part of
// the metric inside M's kinematic-tree sparsity -- every class's diagonal projection plus
// the fluid blocks -- so mj_factorI factors it with no fill-in; it is the exact metric
// when no coupling terms exist, the preconditioner otherwise, and the only metric the dual
// solvers ever see (mj_makeY). Idempotent: actuation-stage objects are rebuilt from
// scratch, so re-running the stage (mj_forwardSkip) is safe. Under sleep, rows of sleeping
// trees may hold stale M values: harmless, M and its factor are block-diagonal by tree and
// only awake rows are ever gathered or solved
void mjd_effActuation(const mjModel* m, mjData* d) {
if (!d->efm_active) {
return;
}
int nv = m->nv;
mjtNum h = m->opt.timestep;
// actuator gains, clamped to the stabilizing sign
d->nefmA = 0;
if (d->efm_aid) {
mju_zero(d->efm_ca, nv);
int sleep_filter = mjENABLED(mjENBL_SLEEP) && d->ntree_awake < m->ntree;
for (int i=0; i < m->nactuator; i++) {
if (!mj_effActuatorPossible(m, i) || actuatorDerivSkip(m, d, i, sleep_filter)) {
continue;
}
mjtNum gv = mju_max(0, -actuatorVelDeriv(m, d, i));
mjtNum gp = mju_max(0, -actuatorLenDeriv(m, d, i));
if (!gv && !gp) {
continue;
}
int t = d->nefmA++;
d->efm_aid[t] = i;
d->efm_as[t] = h*h*gp + h*gv;
d->efm_ak[t] = h*gp;
// stiffness shift: ca -= h*gp * actuator_velocity * moment'
if (gp) {
int oadr = m->actuator_outadr[i];
for (int k=0; k < m->actuator_outnum[i]; k++) {
int r = oadr + k;
mjtNum ckv = d->efm_ak[t] * d->actuator_velocity[r];
int end = d->moment_rowadr[r] + d->moment_rownnz[r];
for (int j=d->moment_rowadr[r]; j < end; j++) {
d->efm_ca[d->moment_colind[j]] -= ckv * d->actuator_moment[j];
}
}
}
}
}
// factor the backbone qH = M + all metric diagonals
if (d->efm_diag) {
mju_copy(d->qH, d->M, m->nC);
for (int i=0; i < nv; i++) {
d->qH[m->M_rowadr[i] + m->M_rownnz[i] - 1] += d->efm_diag[i];
}
for (int t=0; t < d->nefmT; t++) {
mjtNum s = d->efm_ts[t];
if (!s) {
continue;
}
int i = d->efm_tid[t];
int end = m->ten_J_rowadr[i] + m->ten_J_rownnz[i];
for (int j=m->ten_J_rowadr[i]; j < end; j++) {
int c = m->ten_J_colind[j];
d->qH[m->M_rowadr[c] + m->M_rownnz[c] - 1] += s * d->ten_J[j] * d->ten_J[j];
}
}
for (int t=0; t < d->nefmA; t++) {
mjtNum s = d->efm_as[t];
int i = d->efm_aid[t];
int oadr = m->actuator_outadr[i];
for (int k=0; k < m->actuator_outnum[i]; k++) {
int r = oadr + k;
int end = d->moment_rowadr[r] + d->moment_rownnz[r];
for (int j=d->moment_rowadr[r]; j < end; j++) {
int c = d->moment_colind[j];
d->qH[m->M_rowadr[c] + m->M_rownnz[c] - 1] +=
s * d->actuator_moment[j] * d->actuator_moment[j];
}
}
}
// fluid drag blocks share M's sparsity: add the full pattern
if (d->efm_fluid) {
mju_addTo(d->qH, d->efm_fluid, m->nC);
}
// record the diagonal additions for metric-consistent regularization, then factor
for (int i=0; i < nv; i++) {
int diag = m->M_rowadr[i] + m->M_rownnz[i] - 1;
d->efm_sdiag[i] = d->qH[diag] - d->M[diag];
}
mj_factorI(d->qH, d->qHDiagInv, nv, m->M_rownnz, m->M_rowadr, m->M_colind, NULL);
}
}
// island-local metric product res += S*vec: the diagonal classes and the island's tendons,
// with vectors in island-local dof coordinates. Flex terms never reach the island path:
// models with flex metric terms force a monolithic solve
void mjd_effMulAddIsland(const mjModel* m, const mjData* d, mjtNum* res, const mjtNum* vec,
int island) {
int nv = d->island_nv[island];
int idofadr = d->island_idofadr[island];
const int* idof2dof = d->map_idof2dof + idofadr;
if (d->efm_diag) {
for (int k=0; k < nv; k++) {
res[k] += d->efm_diag[idof2dof[k]] * vec[k];
}
}
// fluid drag blocks: M-row columns are ancestor dofs, guaranteed in the same island
if (d->efm_fluid) {
for (int k=0; k < nv; k++) {
int i = idof2dof[k];
int start = m->M_rowadr[i];
int diag = start + m->M_rownnz[i] - 1;
mjtNum acc = d->efm_fluid[diag] * vec[k];
for (int a=start; a < diag; a++) {
int kc = d->map_dof2idof[m->M_colind[a]] - idofadr;
mjtNum F = d->efm_fluid[a];
acc += F * vec[kc];
res[kc] += F * vec[k];
}
res[k] += acc;
}
}
// the island's rank-1 terms: every tree an entry touches shares the island (union-find
// merges the support), so the first column decides membership
mjEffRank1Iter it = {0};
mjEffRank1 e;
while (mjd_effRank1Next(m, d, &it, &e, /*flg_contact=*/1)) {
if (d->tree_island[m->dof_treeid[e.colind[0]]] != island) {
continue;
}
mjtNum dot = 0;
for (int j=0; j < e.nnz; j++) {
dot += e.val[j] * vec[d->map_dof2idof[e.colind[j]] - idofadr];
}
dot *= e.scale;
for (int j=0; j < e.nnz; j++) {
res[d->map_dof2idof[e.colind[j]] - idofadr] += dot * e.val[j];
}
}
}
// publish passive flex contact as the metric's rank-1 contact class, one packed row per pair
// ([nnz, conid, colind...] / [scale, force, val...], see engine_derivative.h): matrix-free
// contact costs O(nnz) per pair where the stiffness CSR cost O(nnz^2), and the CSR's sparsity no
// longer grows with the contact set
static void effContactBuild(const mjModel* m, mjData* d, mjtNum scale) {
d->nefmcon = 0;
if (!d->ncon || !flexPassiveContact_any(m)) {
return;
}
int nv = m->nv, issparse = mj_isSparse(m);
mj_markStack(d);
mjtNum* jacdif = mjSTACKALLOC(d, 3*nv, mjtNum);
mjtNum* jac1 = mjSTACKALLOC(d, 3*nv, mjtNum);
mjtNum* jac2 = mjSTACKALLOC(d, 3*nv, mjtNum);
mjtNum* jacn = mjSTACKALLOC(d, 3*nv, mjtNum);
int* chain = mjSTACKALLOC(d, nv, int);
// pass 1: packed length, two header entries plus nnz per row
for (int i = 0; i < d->ncon; i++) {
const mjContact* con = d->contact + i;
if (con->exclude != 4 || mjd_flexContactStiffness(m, d, con) <= 0) {
continue;
}
int NV = mj_contactJacobian(m, d, con, con->dim, jacdif, NULL, jac1, jac2, NULL, NULL, chain);
if (NV) {
d->nefmcon += 2 + NV; // an upper bound in a dense model, trimmed to `adr` below
} else {
d->contact[i].exclude = 3; // affects no dofs, as the passive force used to mark it
}
}
if (!d->nefmcon) {
mj_freeStack(d);
return;
}
d->efm_con_ind = EFMALLOC(int, d->nefmcon);
d->efm_con_val = EFMALLOC(mjtNum, d->nefmcon);
// pass 2: fill, rows packed back to back in both arrays
int adr = 0;
for (int i = 0; i < d->ncon; i++) {
const mjContact* con = d->contact + i;
if (con->exclude != 4) {
continue;
}
mjtNum k = mjd_flexContactStiffness(m, d, con);
if (k <= 0) {
continue;
}
int NV = mj_contactJacobian(m, d, con, con->dim, jacdif, NULL, jac1, jac2, NULL, NULL, chain);
if (!NV) {
continue;
}
mju_mulMatMat(jacn, con->frame, jacdif, con->dim > 1 ? 3 : 1, 3, NV);
mjtNum s = mjd_flexContactSlack(k, con->dist, /*lam=*/0);
int nnz = 0;
for (int a = 0; a < NV; a++) {
// a dense model gets no chain: every dof is returned, so compact the row on its nonzeros
if (!issparse && jacn[a] == 0) {
continue;
}
d->efm_con_ind[adr + 2 + nnz] = issparse ? chain[a] : a;
d->efm_con_val[adr + 2 + nnz] = jacn[a];
nnz++;
}
d->efm_con_ind[adr] = nnz;
d->efm_con_ind[adr + 1] = i;
d->efm_con_val[adr] = scale * k;
d->efm_con_val[adr + 1] = -k*mjd_flexContactResidual(k, con->dist, s, /*lam=*/0);
adr += 2 + nnz;
}
d->nefmcon = adr; // the dense compaction can land below the bound counted above
mj_freeStack(d);
}
// apply the published rows' forces, res += force * row: the passive stage takes its contact
// force from the rows the metric build published
void mjd_effContactForce(const mjData* d, mjtNum* res) {
const int* ind = d->efm_con_ind;
const mjtNum* val = d->efm_con_val;
for (int adr = 0; adr < d->nefmcon; ) {
int nnz = ind[adr];
mjtNum f = val[adr + 1];
const int* colind = ind + adr + 2;
const mjtNum* row = val + adr + 2;
for (int a = 0; a < nnz; a++) {
// fused multiply-add: rounding the product first moves the results by an ulp
res[colind[a]] += f * row[a];
}
adr += 2 + nnz;
}
}
// build the per-step implicit effective metric on the arena, or deactivate it. The gate
// decision (integrator=discrete) is the caller's: the metric module has no dependency on
// the solver configuration beyond what it is told here.
void mjd_effBuild(const mjModel* m, mjData* d, int active, int flg_factor) {
int nv = m->nv;
d->efm_active = 0;
d->nefmK = 0;
d->nefmcon = 0;
d->nefmT = 0;
d->nefmdof = 0;
d->nefmL = 0;
d->nefmA = 0;
d->efm_diag = NULL;
d->efm_ck = NULL;
d->efm_sdiag = NULL;
d->efm_fluid = NULL;
d->efm_tid = NULL;
d->efm_ts = NULL;
d->efm_tk = NULL;
d->efm_aid = NULL;
d->efm_as = NULL;
d->efm_ak = NULL;
d->efm_ca = NULL;
if (!active) {
return;
}
mjtNum h = m->opt.timestep;
// corotated element stiffness cache (used by the shift and the matrix-free fallback)
mju_zero(d->flexelem_krot, m->nflexstiffness);
mjd_flexInterp_cacheKrot(m, d, d->flexelem_krot);
// smooth-force shift c = h*K*qvel (values refreshed by mjd_effShift in the velocity stage)
d->efm_c = EFMALLOC(mjtNum, nv);
// per-dof diagonal classes (joint damping/stiffness, joint-transmission actuator damping):
// position-dependent stiffness assembled here; the velocity-dependent damping part and the
// qH backbone factor are refreshed by mjd_effShift in the velocity stage
d->efm_ck = EFMALLOC(mjtNum, nv);
int any_diag = effDiagStiff(m, d);
for (int i=0; !any_diag && i < nv; i++) {
any_diag = dofDampPossible(m, i);
}
// tendons with metric terms: id list here, values (velocity-dependent) in mjd_effShift.
// Sleeping tendons are excluded, matching the passive-force filter; the wake rule
// (mj_wakeTendon) keeps metric-coupled tendon pairs awake or asleep together. Under the
// dual solver the class is excluded (mj_effCouplings): an empty list removes its metric
// terms and its shift together, so the tendon forces integrate explicitly
int sleep_filter = mjENABLED(mjENBL_SLEEP) && d->ntree_awake < m->ntree;
if (m->ntendon && mj_effCouplings(m)) {
d->efm_tid = EFMALLOC(int, m->ntendon);
d->efm_ts = EFMALLOC(mjtNum, m->ntendon);
d->efm_tk = EFMALLOC(mjtNum, m->ntendon);
for (int i=0; i < m->ntendon; i++) {
if (!mj_effTendonPossible(m, i)) {
continue;
}
if (sleep_filter && mj_sleepState(m, d, mjOBJ_TENDON, i) != mjS_AWAKE) {
continue;
}
d->efm_tid[d->nefmT++] = i;
}
}
// metric-possible actuators: allocation here, values (ctrl- and state-dependent) in
// mjd_effActuation at the actuation stage. Under the dual solver the class is excluded
// (mj_effCouplings, as for tendons): no allocation, so mjd_effActuation adds nothing
int any_act = 0;
if (mj_effCouplings(m)) {
for (int i=0; i < m->nactuator; i++) {
if (mj_effActuatorPossible(m, i)) {
any_act = 1;
break;
}
}
}
if (any_act) {
d->efm_aid = EFMALLOC(int, m->nactuator);
d->efm_as = EFMALLOC(mjtNum, m->nactuator);
d->efm_ak = EFMALLOC(mjtNum, m->nactuator);
d->efm_ca = EFMALLOC(mjtNum, nv);
mju_zero(d->efm_ca, nv);
}
// the tendon and actuator diagonals join the qH backbone: force the diagonal machinery on.
// Fluid drag and passive flex contact enter only while their forces are applied: mj_passive
// skips them, like every passive force, when both the spring and damper forces are disabled
int passive = !(mjDISABLED(mjDSBL_SPRING) && mjDISABLED(mjDSBL_DAMPER));
int any_fluid = passive && (m->opt.viscosity > 0 || m->opt.density > 0);
d->efm_diag = (any_diag || d->nefmT || any_act || any_fluid) ? EFMALLOC(mjtNum, nv) : NULL;
d->efm_sdiag = d->efm_diag ? EFMALLOC(mjtNum, nv) : NULL;
d->efm_fluid = any_fluid ? EFMALLOC(mjtNum, m->nC) : NULL;
// assemble the standard-flex part of B into CSR (constant during the step). With stretch or
// assemblable interp present, assemble the FULL matrix (bending included): one CSR then
// serves both the matvec and the per-step factor. Bending-only models keep the stencil
// operator + the constant mj_setConst factor. The stiffness (s1) and damping (s2) parts enter
// only when their forces are enabled; the structure does not depend on the flags
mjtNum s1 = mjDISABLED(mjDSBL_SPRING) ? 0 : h*h;
mjtNum s2 = mjDISABLED(mjDSBL_DAMPER) ? 0 : h;
const mjtNum* krot = mjd_flexInterpAssemblable(m) ? d->flexelem_krot : NULL;
d->efm_K_rownnz = EFMALLOC(int, nv);
d->efm_K_rowadr = EFMALLOC(int, nv);
// Newton consumes the metric as an explicit sparse matrix inside its Hessian: force full
// CSR assembly, including bending-only flexes which otherwise keep the matrix-free stencil
// plus the constant mj_setConst factor (non-assemblable interp flexes are rejected by
// mj_checkDiscrete under Newton)
int assemble_any = mjd_flexStiff_any(m, krot != NULL) || flexPassiveContact_any(m);
if (m->opt.solver == mjSOL_NEWTON) {
for (int f=0; !assemble_any && f < m->nflex; f++) {
assemble_any = flexStiff_active(m, f, /*flg_bend=*/1, /*flg_stretch=*/1);
}
}
if (assemble_any) {
d->nefmK = mjd_flexStiff_assemble(m, d, d->efm_K_rownnz, d->efm_K_rowadr,
NULL, NULL, s1, s2, /*bend*/ 1, /*stretch*/ 1, krot);
}
// passive flex contact is a rank-1 class, not CSR entries (see effContactBuild)
if (passive) {
effContactBuild(m, d, h*h);
}
if (d->nefmK) {
d->efm_K_colind = EFMALLOC(int, d->nefmK);
d->efm_K_val = EFMALLOC(mjtNum, d->nefmK);
mjd_flexStiff_assemble(m, d, d->efm_K_rownnz, d->efm_K_rowadr,
d->efm_K_colind, d->efm_K_val, s1, s2,
/*bend*/ 1, /*stretch*/ 1, krot);
// per-step factor of the flex block of (M + K): the stiffness is constant during the
// step, so one factorization here turns every preconditioner application into a direct
// solve (the stiff flex block stops being iterated on). Consumers that only multiply
// (inverse dynamics) skip it.
if (flg_factor) {
effBlocks(m, d);
}
} else {
mju_zeroInt(d->efm_K_rownnz, nv);
mju_zeroInt(d->efm_K_rowadr, nv);
}
d->efm_active = 1;
}
2717294d7This file is pinned to the selected release. Download the original to open it in your design tool.