Skip to content

Commit de48f41

Browse files
yuvaltassacopybara-github
authored andcommitted
Add missing term in mj_jacDot
PiperOrigin-RevId: 735686836 Change-Id: I813ca46f71e368ddc97a6d2e540102137b31dbcf
1 parent 3b27f30 commit de48f41

File tree

4 files changed

+88
-42
lines changed

4 files changed

+88
-42
lines changed

doc/changelog.rst

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
Changelog
33
=========
44

5+
Upcoming version (not yet released)
6+
-----------------------------------
7+
8+
Bug fixes
9+
^^^^^^^^^
10+
- :ref:`mj_jacDot` was missing a term that accounts for the motion of the point with respect to
11+
which the Jacobian is computed, now fixed.
12+
513
Version 3.3.0 (Feb 26, 2025)
614
----------------------------
715

src/engine/engine_core_smooth.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -1893,7 +1893,7 @@ void mj_comVel(const mjModel* m, mjData* d) {
18931893

18941894
// assign cvel, cdofdot
18951895
mju_copy(d->cvel+6*i, cvel, 6);
1896-
mju_copy(d->cdof_dot+6*bda, cdofdot, 6*m->body_dofnum[i]);
1896+
mju_copy(d->cdof_dot+6*bda, cdofdot, 6*dofnum);
18971897
}
18981898
}
18991899

src/engine/engine_support.c

+18-8
Original file line numberDiff line numberDiff line change
@@ -811,11 +811,14 @@ void mj_jacDot(const mjModel* m, const mjData* d,
811811
mjtNum* jacp, mjtNum* jacr, const mjtNum point[3], int body) {
812812
int nv = m->nv;
813813
mjtNum offset[3];
814+
mjtNum pvel[6]; // point velocity (rot:lin order)
814815

815-
// clear jacobians, compute offset if required
816+
// clear jacobians, compute offset and pvel if required
816817
if (jacp) {
817818
mju_zero(jacp, 3*nv);
818-
mju_sub3(offset, point, d->subtree_com+3*m->body_rootid[body]);
819+
const mjtNum* com = d->subtree_com+3*m->body_rootid[body];
820+
mju_sub3(offset, point, com);
821+
mju_transformSpatial(pvel, d->cvel+6*body, 0, point, com, 0);
819822
}
820823
if (jacr) {
821824
mju_zero(jacr, 3*nv);
@@ -838,6 +841,7 @@ void mj_jacDot(const mjModel* m, const mjData* d,
838841
while (i >= 0) {
839842
mjtNum cdof_dot[6];
840843
mju_copy(cdof_dot, d->cdof_dot+6*i, 6);
844+
mjtNum* cdof = d->cdof+6*i;
841845

842846
// check for quaternion
843847
mjtJoint type = m->jnt_type[m->dof_jntid[i]];
@@ -846,7 +850,7 @@ void mj_jacDot(const mjModel* m, const mjData* d,
846850

847851
// compute cdof_dot for quaternion (use current body cvel)
848852
if (is_quat) {
849-
mju_crossMotion(cdof_dot, d->cvel+6*m->dof_bodyid[i], d->cdof+6*i);
853+
mju_crossMotion(cdof_dot, d->cvel+6*m->dof_bodyid[i], cdof);
850854
}
851855

852856
// construct rotation jacobian
@@ -858,11 +862,17 @@ void mj_jacDot(const mjModel* m, const mjData* d,
858862

859863
// construct translation jacobian (correct for rotation)
860864
if (jacp) {
861-
mjtNum tmp[3] = {0};
862-
mju_cross(tmp, cdof_dot, offset);
863-
jacp[i+0*nv] += cdof_dot[3] + tmp[0];
864-
jacp[i+1*nv] += cdof_dot[4] + tmp[1];
865-
jacp[i+2*nv] += cdof_dot[5] + tmp[2];
865+
// first correction term, account for varying cdof
866+
mjtNum tmp1[3];
867+
mju_cross(tmp1, cdof_dot, offset);
868+
869+
// second correction term, account for point translational velocity
870+
mjtNum tmp2[3];
871+
mju_cross(tmp2, cdof, pvel + 3);
872+
873+
jacp[i+0*nv] += cdof_dot[3] + tmp1[0] + tmp2[0];
874+
jacp[i+1*nv] += cdof_dot[4] + tmp1[1] + tmp2[1];
875+
jacp[i+2*nv] += cdof_dot[5] + tmp1[2] + tmp2[2];
866876
}
867877

868878
// advance to parent dof

test/engine/engine_support_test.cc

+61-33
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ static constexpr char kQuat[] = R"(
288288
<body name="query">
289289
<joint type="ball"/>
290290
<geom size="1"/>
291+
<site name="query" pos=".1 .2 .3"/>
291292
</body>
292293
</worldbody>
293294
<keyframe>
@@ -315,6 +316,7 @@ static constexpr char kFreeBall[] = R"(
315316
<body name="query" pos="0 .2 0">
316317
<joint type="slide" axis="1 1 1"/>
317318
<geom size=".05"/>
319+
<site name="query" pos=".1 .2 .3"/>
318320
</body>
319321
</body>
320322
</body>
@@ -350,6 +352,7 @@ static constexpr char kQuatlessPendulum[] = R"(
350352
<body name="query" pos="0 .1 0">
351353
<joint axis="1 0 0"/>
352354
<geom type="capsule" size="0.02" fromto="0 0 0 0 .1 0"/>
355+
<site name="query" pos=".1 0 0"/>
353356
</body>
354357
</body>
355358
</body>
@@ -373,6 +376,7 @@ static constexpr char kTelescope[] = R"(
373376
<body pos=".1 .02 0" name="query">
374377
<joint type="slide" axis="1 0 0"/>
375378
<geom type="capsule" size="0.02" fromto="0 0 0 .1 0 0"/>
379+
<site name="query" pos=".1 0 0"/>
376380
</body>
377381
</body>
378382
</body>
@@ -384,12 +388,27 @@ static constexpr char kTelescope[] = R"(
384388
</mujoco>
385389
)";
386390

391+
static constexpr char kHinge[] = R"(
392+
<mujoco>
393+
<worldbody>
394+
<body name="query">
395+
<joint name="link1" axis="0 1 0"/>
396+
<geom type="capsule" size=".02" fromto="0 0 0 0 0 -1"/>
397+
<site name="query" pos="0 0 -1"/>
398+
</body>
399+
</worldbody>
400+
401+
<keyframe>
402+
<key qpos="1" qvel="1"/>
403+
</keyframe>
404+
</mujoco>
405+
)";
406+
387407
// compare mj_jacDot with finite-differenced mj_jac
388408
TEST_F(JacobianTest, JacDot) {
389-
for (auto xml : {kQuat, kFreeBall, kQuatlessPendulum, kTelescope}) {
409+
for (auto xml : {kHinge, kQuat, kTelescope, kFreeBall, kQuatlessPendulum}) {
390410
mjModel* model = LoadModelFromString(xml);
391411
int nv = model->nv;
392-
mjtNum point[3] = {.01, .02, .03};
393412
mjData* data = mj_makeData(model);
394413

395414
// load keyframe if present, step for a bit
@@ -407,34 +426,43 @@ TEST_F(JacobianTest, JacDot) {
407426
int bodyid = mj_name2id(model, mjOBJ_BODY, "query");
408427
EXPECT_GT(bodyid, 0);
409428

429+
// get site position
430+
int siteid = mj_name2id(model, mjOBJ_SITE, "query");
431+
EXPECT_GT(siteid, -1);
432+
mjtNum point[3];
433+
mju_copy3(point, data->site_xpos+3*siteid);
434+
410435
// jac, jac_dot
411-
mj_markStack(data);
412-
mjtNum* jac = mj_stackAllocNum(data, 6*nv);
413-
mj_jac(model, data, jac, jac+3*nv, point, bodyid);
414-
mjtNum* jac_dot = mj_stackAllocNum(data, 6*nv);
415-
mj_jacDot(model, data, jac_dot, jac_dot+3*nv, point, bodyid);
436+
vector<mjtNum> jacp(3*nv);
437+
vector<mjtNum> jacr(3*nv);
438+
mj_jac(model, data, jacp.data(), jacr.data(), point, bodyid);
439+
vector<mjtNum> jacp_dot(3*nv);
440+
vector<mjtNum> jacr_dot(3*nv);
441+
mj_jacDot(model, data, jacp_dot.data(), jacr_dot.data(), point, bodyid);
416442

417443
// jac_h: jacobian after integrating qpos with a timestep of h
418444
mjtNum h = 1e-7;
419445
mj_integratePos(model, data->qpos, data->qvel, h);
420446
mj_kinematics(model, data);
421447
mj_comPos(model, data);
422-
mjtNum* jac_h = mj_stackAllocNum(data, 6*nv);;
423-
mj_jac(model, data, jac_h, jac_h+3*nv, point, bodyid);
448+
vector<mjtNum> jacp_h(3*nv);
449+
vector<mjtNum> jacr_h(3*nv);
450+
mju_copy3(point, data->site_xpos+3*siteid); // get updated site position
451+
mj_jac(model, data, jacp_h.data(), jacr_h.data(), point, bodyid);
424452

425453
// jac_dot_h finite-difference approximation
426-
mjtNum* jac_dot_h = mj_stackAllocNum(data, 6*nv);;
427-
mju_sub(jac_dot_h, jac_h, jac, 6*nv);
428-
mju_scl(jac_dot_h, jac_dot_h, 1/h, 6*nv);
454+
vector<mjtNum> jacp_dot_h(3*nv);
455+
mju_sub(jacp_dot_h.data(), jacp_h.data(), jacp.data(), 3*nv);
456+
mju_scl(jacp_dot_h.data(), jacp_dot_h.data(), 1/h, 3*nv);
457+
vector<mjtNum> jacr_dot_h(3*nv);
458+
mju_sub(jacr_dot_h.data(), jacr_h.data(), jacr.data(), 3*nv);
459+
mju_scl(jacr_dot_h.data(), jacr_dot_h.data(), 1/h, 3*nv);
429460

430461
// compare finite-differenced and analytic
431462
mjtNum tol = 1e-5;
432-
for (int j=0; j < 6; j++) {
433-
EXPECT_THAT(AsVector(jac_dot_h + j*nv, nv),
434-
Pointwise(DoubleNear(tol), AsVector(jac_dot + j*nv, nv)));
435-
}
463+
EXPECT_THAT(jacp_dot, Pointwise(DoubleNear(tol), jacp_dot_h));
464+
EXPECT_THAT(jacr_dot, Pointwise(DoubleNear(tol), jacr_dot_h));
436465

437-
mj_freeStack(data);
438466
mj_deleteData(data);
439467
mj_deleteModel(model);
440468
}
@@ -654,19 +682,19 @@ TEST_F(SupportTest, GetSetStateStepEqual) {
654682
int size = mj_stateSize(model, spec);
655683

656684
// save the initial state and step
657-
std::vector<mjtNum> state0a(size);
685+
vector<mjtNum> state0a(size);
658686
mj_getState(model, data, state0a.data(), spec);
659687

660688
// get the initial state, expect equality
661-
std::vector<mjtNum> state0b(size);
689+
vector<mjtNum> state0b(size);
662690
mj_getState(model, data, state0b.data(), spec);
663691
EXPECT_EQ(state0a, state0b);
664692

665693
// take one step
666694
mj_step(model, data);
667695

668696
// save the resulting state
669-
std::vector<mjtNum> state1a(size);
697+
vector<mjtNum> state1a(size);
670698
mj_getState(model, data, state1a.data(), spec);
671699

672700
// expect the state to be different after stepping
@@ -675,7 +703,7 @@ TEST_F(SupportTest, GetSetStateStepEqual) {
675703
// reset to the saved state, step again, get the resulting state
676704
mj_setState(model, data, state0a.data(), spec);
677705
mj_step(model, data);
678-
std::vector<mjtNum> state1b(size);
706+
vector<mjtNum> state1b(size);
679707
mj_getState(model, data, state1b.data(), spec);
680708

681709
// expect the state to be the same after re-stepping
@@ -701,13 +729,13 @@ TEST_F(InertiaTest, DenseSameAsSparse) {
701729
}
702730

703731
// dense zero matrix
704-
std::vector<mjtNum> dst_sparse(nv * nv, 0.0);
732+
vector<mjtNum> dst_sparse(nv * nv, 0.0);
705733

706734
// sparse zero matrix
707-
std::vector<mjtNum> dst_dense(nv * nv, 0.0);
708-
std::vector<int> rownnz(nv, nv);
709-
std::vector<int> rowadr(nv, 0);
710-
std::vector<int> colind(nv * nv, 0);
735+
vector<mjtNum> dst_dense(nv * nv, 0.0);
736+
vector<int> rownnz(nv, nv);
737+
vector<int> rowadr(nv, 0);
738+
vector<int> colind(nv * nv, 0);
711739

712740
// set sparse structure
713741
for (int i = 0; i < nv; i++) {
@@ -902,44 +930,44 @@ TEST_F(SupportTest, GeomDistance) {
902930
EXPECT_EQ(mj_geomDistance(model, data, 0, 1, distmax, nullptr), 0.5);
903931
mjtNum fromto[6];
904932
EXPECT_EQ(mj_geomDistance(model, data, 0, 1, distmax, fromto), 0.5);
905-
EXPECT_THAT(fromto, Pointwise(Eq(), std::vector<mjtNum>{0, 0, 0, 0, 0, 0}));
933+
EXPECT_THAT(fromto, Pointwise(Eq(), vector<mjtNum>{0, 0, 0, 0, 0, 0}));
906934

907935
// plane-sphere
908936
distmax = 1.0;
909937
EXPECT_DOUBLE_EQ(mj_geomDistance(model, data, 0, 1, 1.0, fromto), 0.8);
910938
mjtNum eps = 1e-12;
911939
EXPECT_THAT(fromto, Pointwise(DoubleNear(eps),
912-
std::vector<mjtNum>{0, 0, 0, 0, 0, 0.8}));
940+
vector<mjtNum>{0, 0, 0, 0, 0, 0.8}));
913941

914942
// sphere-plane
915943
EXPECT_DOUBLE_EQ(mj_geomDistance(model, data, 1, 0, 1.0, fromto), 0.8);
916944
EXPECT_THAT(fromto, Pointwise(DoubleNear(eps),
917-
std::vector<mjtNum>{0, 0, 0.8, 0, 0, 0}));
945+
vector<mjtNum>{0, 0, 0.8, 0, 0, 0}));
918946

919947
// sphere-sphere
920948
EXPECT_DOUBLE_EQ(mj_geomDistance(model, data, 1, 2, 1.0, fromto), 0.5);
921949
EXPECT_THAT(fromto, Pointwise(DoubleNear(eps),
922-
std::vector<mjtNum>{.2, 0, 1, .7, 0, 1}));
950+
vector<mjtNum>{.2, 0, 1, .7, 0, 1}));
923951

924952
// sphere-sphere, flipped order
925953
EXPECT_DOUBLE_EQ(mj_geomDistance(model, data, 2, 1, 1.0, fromto), 0.5);
926954
EXPECT_THAT(fromto, Pointwise(DoubleNear(eps),
927-
std::vector<mjtNum>{.7, 0, 1, .2, 0, 1}));
955+
vector<mjtNum>{.7, 0, 1, .2, 0, 1}));
928956

929957
// mesh-sphere (close distmax)
930958
distmax = 0.701;
931959
eps = model->opt.ccd_tolerance;
932960
EXPECT_THAT(mj_geomDistance(model, data, 3, 1, distmax, fromto),
933961
DoubleNear(0.7, eps));
934962
EXPECT_THAT(fromto, Pointwise(DoubleNear(eps),
935-
std::vector<mjtNum>{0, 0, .8, 0, 0, .1}));
963+
vector<mjtNum>{0, 0, .8, 0, 0, .1}));
936964

937965
// mesh-sphere (far distmax)
938966
distmax = 1.0;
939967
EXPECT_THAT(mj_geomDistance(model, data, 3, 1, distmax, fromto),
940968
DoubleNear(0.7, eps));
941969
EXPECT_THAT(fromto, Pointwise(DoubleNear(eps),
942-
std::vector<mjtNum>{0, 0, .8, 0, 0, .1}));
970+
vector<mjtNum>{0, 0, .8, 0, 0, .1}));
943971

944972
mj_deleteData(data);
945973
mj_deleteModel(model);

0 commit comments

Comments
 (0)