Recompute sameframe flags in mj_setConst.

The `body_sameframe`, `geom_sameframe`, and `site_sameframe` flags are now recomputed within `mj_setConst` based on the current model geometry. This allows these flags to be updated if the model's body/geom/site frames are modified after the initial compilation. Tests are added to verify the correct recomputation and its effect on forward kinematics. The compiler also checks that its initial computation matches the result of `mj_setConst`.

Fixes #3029, #1628.

PiperOrigin-RevId: 941758649
Change-Id: I7b995f8c67ad305bca5b52732436ba578c905d73
This commit is contained in:
Yuval Tassa
2026-07-02 10:48:21 -07:00
committed by Copybara-Service
parent 0dfa4b509a
commit 14c0b0c92b
3 changed files with 258 additions and 0 deletions
+2
View File
@@ -10,6 +10,8 @@ General
- Added Nesterov momentum extrapolation with adaptive gradient restart (O'Donoghue-Candès) to the PGS solver,
significantly improving convergence. Overall PGS now requires ~2x fewer iterations.
- :ref:`mj_encode` now supports encoding of MJB and TXT files.
- :ref:`mj_setConst` now recomputes the ``mjModel.{body,geom,site}_sameframe`` flags, to account for changes in
body/geom/site frames after compilation.
- The :el:`attach` element now supports self-attachment (attaching elements of the current model to itself) by omitting
the :at:`model` attribute. It also supports attaching a frame via the new :at:`frame` attribute, which is mutually
exclusive with :at:`body`.
+109
View File
@@ -82,6 +82,112 @@ static int GetWrapBodyTreeId(const mjModel* m, int wrap_index) {
return (bodyid != -1) ? m->body_treeid[bodyid] : -1;
}
// tolerance for frame comparison, must match compiler's kFrameEps
static const mjtNum kSameFrameEps = 1e-6;
// return 1 if all 3 elements are near zero
static int isNullVec3(const mjtNum v[3]) {
return mju_abs(v[0]) < kSameFrameEps &&
mju_abs(v[1]) < kSameFrameEps &&
mju_abs(v[2]) < kSameFrameEps;
}
// return 1 if quaternion is near identity (1,0,0,0), accounting for double-cover
static int isNullQuat(const mjtNum q[4]) {
int plus = mju_abs(q[0] - 1) < kSameFrameEps &&
mju_abs(q[1]) < kSameFrameEps &&
mju_abs(q[2]) < kSameFrameEps &&
mju_abs(q[3]) < kSameFrameEps;
int minus = mju_abs(q[0] + 1) < kSameFrameEps &&
mju_abs(q[1]) < kSameFrameEps &&
mju_abs(q[2]) < kSameFrameEps &&
mju_abs(q[3]) < kSameFrameEps;
return plus || minus;
}
// return 1 if two quaternions are near equal, accounting for double-cover
static int isSameQuat(const mjtNum q1[4], const mjtNum q2[4]) {
int plus = mju_abs(q1[0] - q2[0]) < kSameFrameEps &&
mju_abs(q1[1] - q2[1]) < kSameFrameEps &&
mju_abs(q1[2] - q2[2]) < kSameFrameEps &&
mju_abs(q1[3] - q2[3]) < kSameFrameEps;
int minus = mju_abs(q1[0] + q2[0]) < kSameFrameEps &&
mju_abs(q1[1] + q2[1]) < kSameFrameEps &&
mju_abs(q1[2] + q2[2]) < kSameFrameEps &&
mju_abs(q1[3] + q2[3]) < kSameFrameEps;
return plus || minus;
}
// return 1 if two 3-vectors are near equal
static int isSameVec3(const mjtNum v1[3], const mjtNum v2[3]) {
return mju_abs(v1[0] - v2[0]) < kSameFrameEps &&
mju_abs(v1[1] - v2[1]) < kSameFrameEps &&
mju_abs(v1[2] - v2[2]) < kSameFrameEps;
}
// recompute body_sameframe, geom_sameframe, site_sameframe from model geometry
static void setSameframe(mjModel* m) {
// body_sameframe: compare body inertial frame to body frame
for (int i=1; i < m->nbody; i++) {
mjtNum* ipos = m->body_ipos+3*i;
mjtNum* iquat = m->body_iquat+4*i;
if (isNullVec3(ipos) && isNullQuat(iquat)) {
m->body_sameframe[i] = mjSAMEFRAME_BODY;
} else if (isNullQuat(iquat)) {
m->body_sameframe[i] = mjSAMEFRAME_BODYROT;
} else {
m->body_sameframe[i] = mjSAMEFRAME_NONE;
}
}
// geom_sameframe: compare geom frame to body and inertial frames
for (int i=0; i < m->ngeom; i++) {
int b = m->geom_bodyid[i];
mjtNum* gpos = m->geom_pos+3*i;
mjtNum* gquat = m->geom_quat+4*i;
mjtNum* ipos = m->body_ipos+3*b;
mjtNum* iquat = m->body_iquat+4*b;
if (isNullVec3(gpos) && isNullQuat(gquat)) {
m->geom_sameframe[i] = mjSAMEFRAME_BODY;
} else if (isNullQuat(gquat)) {
m->geom_sameframe[i] = mjSAMEFRAME_BODYROT;
} else if (isSameVec3(gpos, ipos) && isSameQuat(gquat, iquat)) {
m->geom_sameframe[i] = mjSAMEFRAME_INERTIA;
} else if (isSameQuat(gquat, iquat)) {
m->geom_sameframe[i] = mjSAMEFRAME_INERTIAROT;
} else {
m->geom_sameframe[i] = mjSAMEFRAME_NONE;
}
}
// site_sameframe: compare site frame to body and inertial frames
for (int i=0; i < m->nsite; i++) {
int b = m->site_bodyid[i];
mjtNum* spos = m->site_pos+3*i;
mjtNum* squat = m->site_quat+4*i;
mjtNum* ipos = m->body_ipos+3*b;
mjtNum* iquat = m->body_iquat+4*b;
if (isNullVec3(spos) && isNullQuat(squat)) {
m->site_sameframe[i] = mjSAMEFRAME_BODY;
} else if (isNullQuat(squat)) {
m->site_sameframe[i] = mjSAMEFRAME_BODYROT;
} else if (isSameVec3(spos, ipos) && isSameQuat(squat, iquat)) {
m->site_sameframe[i] = mjSAMEFRAME_INERTIA;
} else if (isSameQuat(squat, iquat)) {
m->site_sameframe[i] = mjSAMEFRAME_INERTIAROT;
} else {
m->site_sameframe[i] = mjSAMEFRAME_NONE;
}
}
}
// set fixed quantities (do not depend on qpos0)
static void setFixed(mjModel* m, mjData* d) {
mj_markStack(d);
@@ -1215,6 +1321,9 @@ static void setSpring(mjModel* m, mjData* d) {
// entry point: set all remaining constant fields of mjModel, except for lengthrange
void mj_setConst(mjModel* m, mjData* d) {
// recompute sameframe flags from current model geometry
setSameframe(m);
// set fixed quantities
setFixed(m, d);
+147
View File
@@ -457,5 +457,152 @@ TEST_F(SetConstTest, DofLength) {
EXPECT_NEAR(model->dof_length[10], 5, tol);
}
TEST_F(SetConstTest, BodySameframeRecomputed) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body name="B1">
<joint type="slide"/>
<geom size=".1"/>
</body>
</worldbody>
</mujoco>
)";
char error[1024];
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(m.get(), NotNull()) << error;
MjDataPtr d(mj_makeData(m.get()));
int b = mj_name2id(m.get(), mjOBJ_BODY, "B1");
// initially sameframe should be BODY (ipos=0, iquat=identity)
EXPECT_EQ(m->body_sameframe[b], mjSAMEFRAME_BODY);
// perturb body_ipos, call mj_setConst
m->body_ipos[3*b+0] = 1.0;
mj_setConst(m.get(), d.get());
EXPECT_EQ(m->body_sameframe[b], mjSAMEFRAME_BODYROT);
// also perturb body_iquat
m->body_iquat[4*b+0] = 0.5;
m->body_iquat[4*b+1] = 0.5;
m->body_iquat[4*b+2] = 0.5;
m->body_iquat[4*b+3] = 0.5;
mj_setConst(m.get(), d.get());
EXPECT_EQ(m->body_sameframe[b], mjSAMEFRAME_NONE);
// restore to identity, should go back to BODY
m->body_ipos[3*b+0] = 0;
m->body_iquat[4*b+0] = 1;
m->body_iquat[4*b+1] = 0;
m->body_iquat[4*b+2] = 0;
m->body_iquat[4*b+3] = 0;
mj_setConst(m.get(), d.get());
EXPECT_EQ(m->body_sameframe[b], mjSAMEFRAME_BODY);
}
TEST_F(SetConstTest, GeomSameframeRecomputed) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body name="B1">
<joint type="slide"/>
<geom name="G1" size=".1"/>
</body>
</worldbody>
</mujoco>
)";
char error[1024];
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(m.get(), NotNull()) << error;
MjDataPtr d(mj_makeData(m.get()));
int g = mj_name2id(m.get(), mjOBJ_GEOM, "G1");
// initially sameframe should be BODY
EXPECT_EQ(m->geom_sameframe[g], mjSAMEFRAME_BODY);
// perturb geom_pos
m->geom_pos[3*g+1] = 0.5;
mj_setConst(m.get(), d.get());
EXPECT_EQ(m->geom_sameframe[g], mjSAMEFRAME_BODYROT);
// restore, should go back to BODY
m->geom_pos[3*g+1] = 0;
mj_setConst(m.get(), d.get());
EXPECT_EQ(m->geom_sameframe[g], mjSAMEFRAME_BODY);
}
TEST_F(SetConstTest, SiteSameframeRecomputed) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body name="B1">
<joint type="slide"/>
<geom size=".1"/>
<site name="S1"/>
</body>
</worldbody>
</mujoco>
)";
char error[1024];
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(m.get(), NotNull()) << error;
MjDataPtr d(mj_makeData(m.get()));
int s = mj_name2id(m.get(), mjOBJ_SITE, "S1");
// initially sameframe should be BODY
EXPECT_EQ(m->site_sameframe[s], mjSAMEFRAME_BODY);
// perturb site_pos
m->site_pos[3*s+2] = 0.3;
mj_setConst(m.get(), d.get());
EXPECT_EQ(m->site_sameframe[s], mjSAMEFRAME_BODYROT);
// restore
m->site_pos[3*s+2] = 0;
mj_setConst(m.get(), d.get());
EXPECT_EQ(m->site_sameframe[s], mjSAMEFRAME_BODY);
}
TEST_F(SetConstTest, SameframeKinematicsCorrect) {
constexpr char xml[] = R"(
<mujoco>
<worldbody>
<body name="B1" pos="1 0 0">
<joint type="slide" axis="1 0 0"/>
<geom name="G1" size=".1"/>
<site name="S1"/>
</body>
</worldbody>
</mujoco>
)";
char error[1024];
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(m.get(), NotNull()) << error;
MjDataPtr d(mj_makeData(m.get()));
int b = mj_name2id(m.get(), mjOBJ_BODY, "B1");
int g = mj_name2id(m.get(), mjOBJ_GEOM, "G1");
// perturb body inertial offset, breaking sameframe
m->body_ipos[3*b+1] = 0.5;
mj_setConst(m.get(), d.get());
EXPECT_EQ(m->body_sameframe[b], mjSAMEFRAME_BODYROT);
// run forward kinematics, check that xipos != xpos
mj_forward(m.get(), d.get());
EXPECT_NEAR(d->xipos[3*b+1], 0.5, MjTol(1e-10, 1e-6));
EXPECT_NEAR(d->xpos[3*b+1], 0.0, MjTol(1e-10, 1e-6));
// perturb geom_pos, check geom global position
m->geom_pos[3*g+2] = 0.3;
mj_setConst(m.get(), d.get());
EXPECT_NE(m->geom_sameframe[g], mjSAMEFRAME_BODY);
mj_forward(m.get(), d.get());
EXPECT_NEAR(d->geom_xpos[3*g+2], 0.3, MjTol(1e-10, 1e-6));
}
} // namespace
} // namespace mujoco