Files
Mujoco_WASM/wasm/tests/bindings_test.ts
T
2026-02-23 17:40:29 -03:00

2562 lines
84 KiB
TypeScript

// Copyright 2025 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.
import 'jasmine';
import {MainModule, MjContact, MjContactVec, MjData, MjLROpt, MjModel,
MjOption, MjsGeom, MjSolverStat, MjSpec, MjStatistic, MjTimerStat, MjvCamera,
MjvFigure, MjvGeom, MjvGLCamera, MjvLight, MjvOption, MjvPerturb, MjvScene,
MjWarningStat, MjVFS, Uint8Buffer} from '../dist/mujoco.js';
import loadMujoco from '../dist/mujoco.js'
function assertExists<T>(value: T | null | undefined, message?: string):
asserts value is T {
if (value === null || value === undefined) {
throw new Error(message ?? 'Expected value to be defined.');
}
}
// Corresponds to bindings_test.py:TEST_XML
const TEST_XML = `
<mujoco model="test">
<compiler coordinate="local" angle="radian" eulerseq="xyz"/>
<size nkey="2"/>
<option timestep="0.002" gravity="0 0 -9.81"/>
<visual>
<global fovy="50" />
<quality shadowsize="51" />
</visual>
<worldbody>
<geom name="myplane" type="plane" size="10 10 1" user="1 2 3"/>
<body name="mybox" pos="0 0 0.1">
<geom name="mybox" type="box" size="0.1 0.1 0.1" mass="0.25"/>
<freejoint name="myfree"/>
</body>
<body name="myhinge-body" pos="0 0 1">
<inertial pos="0 0 0" mass="1" diaginertia="1 1 1"/>
<site pos="0 0 -1" name="mysite" type="sphere"/>
<joint name="myhinge" type="hinge" axis="0 1 0" damping="1"/>
</body>
<body name="myball-body" pos="2 0 1">
<inertial pos="0 0 0" mass="1" diaginertia="1 1 1"/>
<joint name="myball" type="ball"/>
</body>
<body name="mocap-body" mocap="true" pos="42 0 42">
<geom name="mocap-sphere" type="sphere" size="0.1"/>
</body>
</worldbody>
<actuator>
<position name="myactuator" joint="myhinge"/>
</actuator>
<sensor>
<jointvel name="myjointvel" joint="myhinge"/>
<accelerometer name="myaccelerometer" site="mysite"/>
</sensor>
</mujoco>
`;
type TypedArray =|Int8Array|Uint8Array|Uint8ClampedArray|Int16Array|Uint16Array|
Int32Array|Uint32Array|Float32Array|Float64Array;
function norm(arr: number[]): number {
return Math.sqrt(arr.reduce((acc, val) => acc + val * val, 0));
}
function expectArraysClose(arr1: any, arr2: TypedArray, precision = 1) {
expect(arr1.length).toEqual(arr2.length);
for (let i = 0; i < arr1.length; i++) {
expect(arr1[i]).toBeCloseTo(arr2[i], precision);
}
}
function expectArraysEqual(arr1: any, arr2: TypedArray) {
expect(arr1.length).toEqual(arr2.length);
for (let i = 0; i < arr1.length; i++) {
expect(arr1[i]).toEqual(arr2[i]);
}
}
describe('MuJoCo WASM Bindings', () => {
let mujoco: MainModule;
let model: MjModel|null = null;
let data: MjData|null = null;
beforeAll(async () => {
mujoco = await loadMujoco();
});
function unlinkXMLFile(filename: string) {
try {
(mujoco as any).FS.unlink(filename);
} catch (e) {
console.warn(`Failed to unlink temporary XML file: ${e}`);
}
}
function writeXMLFile(filename: string, xmlContent: string) {
try {
(mujoco as any).FS.writeFile(filename, xmlContent);
} catch (e) {
throw new Error(`Failed to write temporary XML file: ${e}`);
}
}
beforeEach(() => {
const tempXmlFilename = '/tmp/model.xml';
writeXMLFile(tempXmlFilename, TEST_XML);
model = mujoco.MjModel!.mj_loadXML(tempXmlFilename);
if (!model) {
unlinkXMLFile(tempXmlFilename);
throw new Error('Failed to load model from XML');
}
unlinkXMLFile(tempXmlFilename);
data = new mujoco.MjData(model);
if (!data) {
throw new Error('Failed to create data from model');
}
});
afterEach(() => {
model?.delete();
data?.delete();
});
describe('Buffer API', () => {
it('should construct from an element count', () => {
const buf = new mujoco.DoubleBuffer(5);
expect(buf.GetElementCount()).toBe(5);
expect(buf.GetPointer()).toBeDefined();
expect(buf.GetView()).toBeDefined();
});
it('should construct from a Javascript array', () => {
const array: number[] = [0, 1, 4, 9, 16];
const buf = mujoco.DoubleBuffer.FromArray(array);
expect(buf.GetElementCount()).toBe(5);
expect(buf.GetPointer()).toBeDefined();
expect(buf.GetView()).toBeDefined();
expectArraysClose(buf.GetView(), new Float64Array(array));
});
it('should construct from Float64Array', () => {
const array = new Float64Array([1 / 11, 7 / 11]);
const buf = mujoco.DoubleBuffer.FromArray(array);
expect(buf.GetElementCount()).toBe(2);
expect(buf.GetPointer()).toBeDefined();
expect(buf.GetView()).toBeDefined();
expectArraysClose(buf.GetView(), array);
});
});
describe('mj_addM', () => {
let simpleModel: MjModel|null = null;
let simpleData: MjData|null = null;
const tempXmlFilename = '/tmp/simple_model.xml';
// A simpler model with nv=1 and nM=1 to avoid WASM binding bugs.
const simpleXmlContent = `
<mujoco>
<worldbody>
<body name="box" pos="0 0 0.5">
<joint type="slide" axis="1 0 0"/>
<geom type="box" size="0.1 0.1 0.1" mass="1"/>
</body>
</worldbody>
</mujoco>`;
beforeEach(() => {
writeXMLFile(tempXmlFilename, simpleXmlContent);
simpleModel = mujoco.MjModel!.mj_loadXML(tempXmlFilename);
assertExists(simpleModel);
simpleData = new mujoco.MjData(simpleModel);
assertExists(simpleData);
});
afterEach(() => {
simpleModel?.delete();
simpleData?.delete();
unlinkXMLFile(tempXmlFilename);
});
it('should compute the sparse inertia matrix', () => {
const nM = simpleModel!.nM;
const dstSparse = new mujoco.DoubleBuffer(nM);
try {
mujoco.mj_forward(simpleModel!, simpleData!);
mujoco.mj_addM(
simpleModel!, simpleData!, dstSparse, simpleModel!.M_rownnz,
simpleModel!.M_rowadr, simpleModel!.M_colind);
expect(dstSparse.GetView().length).toBe(1);
expect(dstSparse.GetView()[0]).toBeCloseTo(1.0);
} finally {
dstSparse.delete();
}
});
it('should throw an error for incorrect sparse matrix dimensions', () => {
const nM = simpleModel!.nM;
const dstSparse = new mujoco.DoubleBuffer(nM + 1);
try {
expect(
() => mujoco.mj_addM(
simpleModel!, simpleData!, dstSparse, simpleModel!.M_rownnz,
simpleModel!.M_rowadr, simpleModel!.M_colind))
.toThrowError(
'MuJoCo Error: [mj_addM] dst must have size 1, got 2');
} finally {
dstSparse.delete();
}
});
it('should compute the sparse inertia matrix with null pointers', () => {
const nM = simpleModel!.nM;
const dstSparse = new mujoco.DoubleBuffer(nM);
try {
mujoco.mj_forward(simpleModel!, simpleData!);
mujoco.mj_addM(simpleModel!, simpleData!, dstSparse, null, null, null);
expect(dstSparse.GetView().length).toBe(1);
} finally {
dstSparse.delete();
}
});
});
it('should compute geom distance without returning fromto', () => {
mujoco.mj_forward(model!, data!);
const dist = mujoco.mj_geomDistance(model!, data!, 0, 2, 200, null);
expect(dist).toEqual(41.9);
});
it('should handle box QP solver with null optionals', () => {
const n = 5;
const res = new mujoco.DoubleBuffer(n);
const r = new mujoco.DoubleBuffer(n * (n + 7));
const h = new mujoco.DoubleBuffer(n * n);
const g = mujoco.DoubleBuffer.FromArray(new Array(n).fill(1));
try {
for (let i = 0; i < n; i++) {
h.GetView()[i * (n + 1)] = 1;
}
const rank = mujoco.mju_boxQP(
res, r, null, h.GetView(), g.GetView(), null as any, null as any);
expect(rank).toBeGreaterThan(-1);
} finally {
res.delete();
r.delete();
h.delete();
g.delete();
}
});
// Corresponds to engine_core_constraint_test.cc:TEST_F(CoreConstraintTest,
// ConstraintUpdateImpl)
it('should correctly compute constraint cost', () => {
const xmlString = `
<mujoco>
<option>
<flag island="enable"/>
</option>
<default>
<geom size=".1"/>
</default>
<visual>
<headlight diffuse=".9 .9 .9"/>
</visual>
<worldbody>
<body>
<joint type="slide" axis="0 0 1" range="0 1" limited="true"/>
<geom/>
</body>
<body pos=".25 0 0">
<joint type="slide" axis="1 0 0"/>
<geom/>
</body>
<body pos="0 0 0.25">
<joint type="slide" axis="0 0 1"/>
<geom/>
<body pos="0 -.15 0">
<joint name="hinge1" axis="0 1 0"/>
<geom type="capsule" size="0.03" fromto="0 0 0 -.2 0 0"/>
<body pos="-.2 0 0">
<joint axis="0 1 0"/>
<geom type="capsule" size="0.03" fromto="0 0 0 -.2 0 0"/>
</body>
</body>
</body>
<body pos=".5 0 0">
<joint type="slide" axis="0 0 1" frictionloss="15"/>
<geom type="box" size=".08 .08 .02" euler="0 10 0"/>
</body>
<body pos="-.5 0 0">
<joint axis="0 1 0" frictionloss=".01"/>
<geom type="capsule" size="0.03" fromto="0 0 0 -.2 0 0"/>
</body>
<body pos="0 0 .5">
<joint name="hinge2" axis="0 1 0"/>
<geom type="box" size=".08 .02 .08"/>
</body>
<body pos=".5 0 .1">
<freejoint/>
<geom type="box" size=".03 .03 .03" pos="0.01 0.01 0.01"/>
</body>
<site name="0" pos="-.45 -.05 .35"/>
<body pos="-.5 0 .3" name="connect">
<freejoint/>
<geom type="box" size=".05 .05 .05"/>
<site name="1" pos=".05 -.05 .05"/>
</body>
</worldbody>
<equality>
<joint joint1="hinge1" joint2="hinge2"/>
<connect body1="connect" body2="world" anchor="-.05 -.05 .05"/>
<connect site1="0" site2="1"/>
</equality>
</mujoco>
`;
const tempXmlFilename = '/tmp/model_c.xml';
writeXMLFile(tempXmlFilename, xmlString);
const model = mujoco.MjModel!.mj_loadXML(tempXmlFilename);
expect(model).not.toBeNull();
const data = new mujoco.MjData(model!);
expect(data).not.toBeNull();
unlinkXMLFile(tempXmlFilename);
try {
mujoco.mj_resetData(model, data!);
let steps = 0;
while (data!.ncon === 0 && steps < 100) {
mujoco.mj_step(model!, data!);
steps++;
}
mujoco.mj_forward(model!, data!);
const res = new mujoco.DoubleBuffer(data!.nefc);
mujoco.mj_mulJacVec(model!, data!, res, data!.qacc);
mujoco.mju_subFrom(res, data!.efc_aref);
const cost = mujoco.DoubleBuffer.FromArray([0]);
mujoco.mj_constraintUpdate(
model!, data!, res.GetView(), cost, /*flg_coneHessian=*/ 1);
expect(cost.GetView()[0]).toBeCloseTo(3355.837);
res.delete();
cost.delete();
} finally {
data!.delete();
model!.delete();
}
});
it('should compute body jacobian', () => {
mujoco.mj_forward(model!, data!);
const bodyId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_BODY.value, 'mybox');
const point = [0.1, 0.2, 0.3];
const jacp = new mujoco.DoubleBuffer(3 * model!.nv);
const jacr = new mujoco.DoubleBuffer(3 * model!.nv);
try {
mujoco.mj_jac(model!, data!, jacp, jacr, point, bodyId);
expect(norm(jacp.GetView())).toBeGreaterThan(0);
expect(norm(jacr.GetView())).toBeGreaterThan(0);
expect(() => mujoco.mj_jac(model!, data!, null, null, point, bodyId))
.not.toThrow();
} finally {
jacp.delete();
jacr.delete();
}
});
it('should compute body frame jacobian', () => {
mujoco.mj_forward(model!, data!);
const bodyId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_BODY.value, 'mybox');
const jacp = new mujoco.DoubleBuffer(3 * model!.nv);
const jacr = new mujoco.DoubleBuffer(3 * model!.nv);
try {
mujoco.mj_jacBody(model!, data!, jacp, jacr, bodyId);
expect(norm(jacp.GetView())).toBeGreaterThan(0);
expect(norm(jacr.GetView())).toBeGreaterThan(0);
expect(() => mujoco.mj_jacBody(model!, data!, null, null, bodyId))
.not.toThrow();
} finally {
jacp.delete();
jacr.delete();
}
});
it('should compute body CoM jacobian', () => {
mujoco.mj_forward(model!, data!);
const bodyId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_BODY.value, 'mybox');
const jacp = new mujoco.DoubleBuffer(3 * model!.nv);
const jacr = new mujoco.DoubleBuffer(3 * model!.nv);
try {
mujoco.mj_jacBodyCom(model!, data!, jacp, jacr, bodyId);
expect(norm(jacp.GetView())).toBeGreaterThan(0);
expect(norm(jacr.GetView())).toBeGreaterThan(0);
expect(() => mujoco.mj_jacBodyCom(model!, data!, null, null, bodyId))
.not.toThrow();
} finally {
jacp.delete();
jacr.delete();
}
});
it('should compute geom jacobian', () => {
mujoco.mj_forward(model!, data!);
const geomId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_GEOM.value, 'mybox');
const jacp = new mujoco.DoubleBuffer(3 * model!.nv);
const jacr = new mujoco.DoubleBuffer(3 * model!.nv);
try {
mujoco.mj_jacGeom(model!, data!, jacp, jacr, geomId);
expect(norm(jacp.GetView())).toBeGreaterThan(0);
expect(norm(jacr.GetView())).toBeGreaterThan(0);
expect(() => mujoco.mj_jacGeom(model!, data!, null, null, geomId))
.not.toThrow();
} finally {
jacp.delete();
jacr.delete();
}
});
it('should compute point-axis jacobian', () => {
mujoco.mj_forward(model!, data!);
const bodyId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_BODY.value, 'mybox');
const point = [0.1, 0.2, 0.3];
const axis = [0, 0, 1];
const jacPoint = new mujoco.DoubleBuffer(3 * model!.nv);
const jacAxis = new mujoco.DoubleBuffer(3 * model!.nv);
try {
mujoco.mj_jacPointAxis(
model!, data!, jacPoint, jacAxis, point, axis, bodyId);
expect(norm(jacPoint.GetView())).toBeGreaterThan(0);
expect(norm(jacAxis.GetView())).toBeGreaterThan(0);
expect(
() => mujoco.mj_jacPointAxis(
model!, data!, null, null, point, axis, bodyId))
.not.toThrow();
} finally {
jacPoint.delete();
jacAxis.delete();
}
});
it('should compute jacobian time derivative', () => {
mujoco.mj_forward(model!, data!);
const bodyId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_BODY.value, 'mybox');
const point = [0.1, 0.2, 0.3];
const jacp = new mujoco.DoubleBuffer(3 * model!.nv);
const jacr = new mujoco.DoubleBuffer(3 * model!.nv);
try {
mujoco.mj_jacDot(model!, data!, jacp, jacr, point, bodyId);
expect(jacp.GetView().length).toBe(3 * model!.nv);
expect(jacr.GetView().length).toBe(3 * model!.nv);
expect(() => mujoco.mj_jacDot(model!, data!, null, null, point, bodyId))
.not.toThrow();
} finally {
jacp.delete();
jacr.delete();
}
});
it('should apply external force and torque', () => {
const force = [1, .5, 1];
const torque = [.3, 1, .22];
const point = [0, 0, 0];
const bodyId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_BODY.value, 'mybox');
const qfrcTarget = new mujoco.DoubleBuffer(model!.nv);
try {
mujoco.mj_forward(model!, data!);
mujoco.mj_applyFT(
model!, data!, force, torque, point, bodyId, qfrcTarget);
expect(norm(qfrcTarget.GetView())).toBeGreaterThan(0);
qfrcTarget.GetView().fill(0);
mujoco.mj_applyFT(
model!, data!, null as any, null as any, point, bodyId, qfrcTarget);
expect(norm(qfrcTarget.GetView())).toEqual(0);
} finally {
qfrcTarget.delete();
}
});
it('should compute finite-differenced transition matrices', () => {
const eps = 1e-6;
const flg_centered = 0;
const dim = 2 * model!.nv + model!.na;
const A = new mujoco.DoubleBuffer(dim * dim);
const B = new mujoco.DoubleBuffer(dim * model!.nu);
const C = new mujoco.DoubleBuffer(model!.nsensordata * dim);
const D = new mujoco.DoubleBuffer(model!.nsensordata * model!.nu);
try {
mujoco.mjd_transitionFD(model!, data!, eps, flg_centered, A, B, C, D);
expect(norm(A.GetView())).toBeGreaterThan(0);
expect(norm(B.GetView())).toBeGreaterThan(0);
expect(norm(C.GetView())).toBeGreaterThan(0);
expect(
() => mujoco.mjd_transitionFD(
model!, data!, eps, flg_centered, null, null, null, null))
.not.toThrow();
} finally {
A.delete();
B.delete();
C.delete();
D.delete();
}
});
it('should solve a system of linear equations', () => {
const n = 2;
const mat = [4, 1, 1, 3];
const vec = [1, 2];
const res = new mujoco.DoubleBuffer(n);
const expected = new Float64Array([1 / 11, 7 / 11]);
const matFactor = mujoco.DoubleBuffer.FromArray(mat);
try {
const rank = mujoco.mju_cholFactor(matFactor, 1e-9);
expect(rank).toBe(n);
mujoco.mju_cholSolve(res, matFactor.GetView(), vec);
const result = res.GetView();
expect(result[0]).toBeCloseTo(expected[0]);
expect(result[1]).toBeCloseTo(expected[1]);
} finally {
res.delete();
matFactor.delete();
}
});
it('should update a Cholesky factorization', () => {
const n = 2;
const mat = [4, 1, 1, 3];
const x = [1, 1];
const matFactor = mujoco.DoubleBuffer.FromArray(mat);
const xBuf = mujoco.DoubleBuffer.FromArray(x);
const res = new mujoco.DoubleBuffer(n);
try {
// Factorize original matrix.
mujoco.mju_cholFactor(matFactor, 1e-9);
// Update factorization with x.
const flg_plus = 1;
mujoco.mju_cholUpdate(matFactor, xBuf.GetView(), flg_plus);
// Solve a system with the updated factor to verify.
// A' = A + x*x' = [[5, 2], [2, 4]]
// A' * y = vec => [[5, 2], [2, 4]] * y = [7, 6]
// Solution is y = [1, 1].
const vec = [7, 6];
mujoco.mju_cholSolve(res, matFactor.GetView(), vec);
const result = res.GetView();
const expectedSolution = new Float64Array([1, 1]);
expect(result[0]).toBeCloseTo(expectedSolution[0]);
expect(result[1]).toBeCloseTo(expectedSolution[1]);
} finally {
matFactor.delete();
xBuf.delete();
res.delete();
}
});
it('should multiply a transposed matrix by another matrix', () => {
const r1 = 3, c1 = 2, c2 = 2;
const mat1 = mujoco.DoubleBuffer.FromArray([1, 4, 2, 5, 3, 6]);
const mat2 = mujoco.DoubleBuffer.FromArray([7, 8, 9, 10, 11, 12]);
const res = new mujoco.DoubleBuffer(c1 * c2);
const expected = new Float64Array([58, 64, 139, 154]);
try {
mujoco.mju_mulMatTMat(res, mat1.GetView(), mat2.GetView(), r1, c1, c2);
expectArraysClose(res.GetView(), expected);
} finally {
mat1.delete();
mat2.delete();
res.delete();
}
});
it('should throw an error because of incompatible matrix sizes', () => {
const r1 = 3, c1 = 2, c2 = 2;
const mat1 = mujoco.DoubleBuffer.FromArray([1, 4, 2, 5, 3, 6]);
const mat2 = mujoco.DoubleBuffer.FromArray([7, 8, 9, 10, 11]);
const res = new mujoco.DoubleBuffer(c1 * c2);
try {
expect(
() => mujoco.mju_mulMatTMat(
res, mat1.GetView(), mat2.GetView(), r1, c1, c2))
.toThrowError(
'MuJoCo Error: [mju_mulMatTMat] mat2 must have size 6, got 5');
} finally {
mat1.delete();
mat2.delete();
res.delete();
}
});
it('should convert a dense matrix to sparse and return non-zero count',
() => {
const nr = 2;
const nc = 3;
const mat = [0.0, 1.0, 0.0, 2.0, 0.0, 3.0];
const rownnz = new mujoco.IntBuffer(nr);
const rowadr = new mujoco.IntBuffer(nr);
const colind = new mujoco.IntBuffer(nc);
const res = new mujoco.DoubleBuffer(nc);
try {
const nnz =
mujoco.mju_dense2sparse(res, mat, nr, nc, rownnz, rowadr, colind);
expectArraysEqual(res.GetView(), new Float64Array([1.0, 2.0, 3.0]));
expectArraysEqual(rownnz.GetView(), new Int32Array([1, 2]));
expectArraysEqual(rowadr.GetView(), new Int32Array([0, 1]));
expectArraysEqual(colind.GetView(), new Int32Array([1, 0, 2]));
} finally {
res.delete();
rownnz.delete();
rowadr.delete();
colind.delete();
}
});
it('should convert a sparse matrix to a dense matrix', () => {
const nr = 2;
const nc = 3;
const mat = [1.0, 2.0, 3.0];
const rownnz = [1, 2];
const rowadr = [0, 1];
const colind = [1, 0, 2];
const res = new mujoco.DoubleBuffer(nr * nc);
const expected = new Float64Array([0.0, 1.0, 0.0, 2.0, 0.0, 3.0]);
try {
mujoco.mju_sparse2dense(res, mat, nr, nc, rownnz, rowadr, colind);
expectArraysEqual(res.GetView(), expected);
} finally {
res.delete();
}
});
it('should throw an error when mju_eye is called with a null argument', () => {
expect(() => {
mujoco.mju_eye(null as any);
})
.toThrowError(
'MuJoCo Error: [mju_eye] Invalid argument. Expected a TypedArray or WasmBuffer, got null.');
});
it('should return undefined', () => {
const spec = mujoco.parseXMLString(TEST_XML);
const body = mujoco.mjs_findBody(spec, 'some_name_that_doesnt_exist');
expect(body).toBeUndefined();
body?.delete();
spec?.delete();
});
it('should check constants values', () => {
expect(mujoco.mjNEQDATA).toBe(11);
expect(mujoco.get_mjDISABLESTRING()).toEqual([
'Constraint', 'Equality', 'Frictionloss', 'Limit', 'Contact', 'Spring',
'Damper', 'Gravity', 'Clampctrl', 'Warmstart', 'Filterparent',
'Actuation', 'Refsafe', 'Sensor', 'Midphase', 'Eulerdamp', 'AutoReset',
'NativeCCD', 'Island'
]);
expect(mujoco.get_mjRNDSTRING()).toEqual([
['Shadow', '1', 'S'], ['Wireframe', '0', 'W'], ['Reflection', '1', 'R'],
['Additive', '0', 'L'], ['Skybox', '1', 'K'], ['Fog', '0', 'G'],
['Haze', '1', '/'], ['Depth', '0', ''], ['Segment', '0', ','],
['Id Color', '0', ''], ['Cull Face', '1', '']
]);
expect(mujoco.get_mjFRAMESTRING().length)
.toEqual(mujoco.mjtFrame.mjNFRAME.value);
expect(mujoco.get_mjVISSTRING().length)
.toEqual(mujoco.mjtVisFlag.mjNVISFLAG.value);
expect(mujoco.get_mjVISSTRING()[mujoco.mjtVisFlag.mjVIS_INERTIA.value])
.toEqual(['Inertia', '0', 'I']);
});
it('should create a spec from XML', () => {
const spec = mujoco.parseXMLString(TEST_XML);
try {
expect(spec).toBeDefined();
expect(spec!.modelname).toEqual('test');
} finally {
spec?.delete();
}
});
it('should return the max value', () => {
expect(mujoco.mju_max(10, 2)).toEqual(10);
});
// Corresponds to bindings_test.py:test_mju_rotVecQuat
it('should rotate a vector by a quaternion', () => {
const res = mujoco.DoubleBuffer.FromArray([0, 0, 0]);
const vec = [1, 0, 0];
const angle = 7 * Math.PI / 12;
const quat = [Math.cos(angle), 0, 0, Math.sin(angle)];
const expected = new Float64Array([-0.8660377, -0.5, 0]);
try {
mujoco.mju_rotVecQuat(res, vec, quat);
expectArraysClose(res.GetView(), expected, 4);
} finally {
res.delete();
}
});
it('should get correct geom name', () => {
const spec = mujoco.parseXMLString(TEST_XML);
const geomEl =
mujoco.mjs_findElement(spec, mujoco.mjtObj.mjOBJ_GEOM, 'myplane');
try {
assertExists(geomEl);
const geom = mujoco.mjs_asGeom(geomEl);
assertExists(geom);
const geomName = mujoco.mjs_getName(geom.element);
expect(geomName).toEqual('myplane');
mujoco.mjs_setName(geom.element, 'myplane2');
expect(mujoco.mjs_getName(geom.element)).toEqual('myplane2');
} finally {
spec?.delete();
}
});
it('should override geom userdata', () => {
const spec = mujoco.parseXMLString(TEST_XML);
const geomEl =
mujoco.mjs_findElement(spec, mujoco.mjtObj.mjOBJ_GEOM, 'myplane');
try {
assertExists(geomEl);
const geom = mujoco.mjs_asGeom(geomEl);
assertExists(geom);
geom.userdata.set(0, 10);
expect(geom.userdata.get(0)).toEqual(10);
} finally {
spec.delete();
}
})
it('should override model geom_rgba', () => {
const newValues = new Float32Array([0.1, 0.2, 0.3, 0.4]);
model!.geom_rgba.set(newValues);
const expected = new Float32Array(
[0.1, 0.2, 0.3, 0.4, 0.5, 0.5, 0.5, 1, 0.5, 0.5, 0.5, 1]);
expectArraysClose(model!.geom_rgba, expected);
})
it('should add a scaled vector to another vector', () => {
const res = mujoco.DoubleBuffer.FromArray([1, 2, 3]);
const vec = [4, 5, 6];
const scale = 2;
const expected = new Float64Array([9, 12, 15]);
try {
mujoco.mju_addToScl(res, vec, scale);
expectArraysClose(res.GetView(), expected, 4);
} finally {
res.delete();
}
});
it('should throw an error when mju_addToScl is called with incompatible sizes',
() => {
const res = mujoco.DoubleBuffer.FromArray([1, 2, 3]);
const vec = [4, 5];
const scale = 2;
try {
expect(() => {
mujoco.mju_addToScl(res, vec, scale);
})
.toThrowError(
'MuJoCo Error: [mju_addToScl] res and vec must have equal size, got 3 and 2');
} finally {
res.delete();
}
});
it('should sort an array with insertion sort', () => {
const arr = mujoco.DoubleBuffer.FromArray([5, 2, 8, 1, 9]);
const expected = new Float64Array([1, 2, 5, 8, 9]);
try {
mujoco.mju_insertionSort(arr);
expectArraysEqual(arr.GetView(), expected);
} finally {
arr.delete();
}
});
it('should find the attached spec', () => {
const bXml = `
<mujoco>
<worldbody>
<body name="b" pos="0 0 0.1">
<geom rgba="0 .9 0 1" name="attached_geom_b" type="box" size="0.1 0.1 0.1" mass="0.25"/>
<freejoint name="bfree"/>
</body>
</worldbody>
</mujoco>
`;
const xmlWithAttachedSpec = `
<mujoco>
<asset>
<model name="b" file="b.xml" />
</asset>
<worldbody>
<attach model="b" body="b" prefix="b" />
</worldbody>
</mujoco>
`;
const mainXmlFilename = 'main.xml';
const bXmlFilename = 'b.xml';
writeXMLFile(mainXmlFilename, xmlWithAttachedSpec);
writeXMLFile(bXmlFilename, bXml);
const spec = mujoco.parseXMLString(xmlWithAttachedSpec);
const attachedSpec = mujoco.mjs_findSpec(spec, 'b');
try {
assertExists(attachedSpec);
const geomEl = mujoco.mjs_findElement(
attachedSpec, mujoco.mjtObj.mjOBJ_GEOM, 'attached_geom_b');
assertExists(geomEl);
const geom = mujoco.mjs_asGeom(geomEl);
expect(geom).toBeDefined();
expect(mujoco.mjs_getName(geom!.element)).toEqual('attached_geom_b');
} finally {
attachedSpec?.delete();
spec?.delete();
unlinkXMLFile(mainXmlFilename);
unlinkXMLFile(bXmlFilename);
}
});
// Corresponds to bindings_test.py:test_load_xml_can_handle_name_clash
it('should handle name clashes when loading XML with includes', () => {
const xml1 = `
<mujoco>
<worldbody>
<geom name="plane" type="plane" size="1 1 1"/>
<include file="model_.xml"/>
<include file="model__.xml"/>
</worldbody>
</mujoco>
`;
const xml2 = `<mujoco><geom name="box" type="box" size="1 1 1"/></mujoco>`;
const xml3 = `<mujoco><geom name="ball" type="sphere" size="1"/></mujoco>`;
const modelXmlFilename = 'model.xml';
const model1XmlFilename = 'model_.xml';
const model2XmlFilename = 'model__.xml';
writeXMLFile(modelXmlFilename, xml1);
writeXMLFile(model1XmlFilename, xml2);
writeXMLFile(model2XmlFilename, xml3);
const model = mujoco.MjModel!.mj_loadXML(modelXmlFilename);
try {
expect(model).toBeDefined();
expect(mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_GEOM.value, 'plane'))
.toBe(0);
expect(mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_GEOM.value, 'box'))
.toBe(1);
expect(mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_GEOM.value, 'ball'))
.toBe(2);
} finally {
model?.delete();
unlinkXMLFile(modelXmlFilename);
unlinkXMLFile(model1XmlFilename);
unlinkXMLFile(model2XmlFilename);
}
});
// Corresponds to bindings_test.py:test_can_read_array
it('should read an array from the model', () => {
const expected =
new Float64Array([0, 0, 0, 0, 0, 0.1, 0, 0, 1, 2, 0, 1, 42, 0, 42]);
const bodyPos = new Float64Array(model!.body_pos);
expectArraysEqual(bodyPos, expected);
});
// Corresponds to bindings_test.py:test_can_set_array
it('should set an array from the data', () => {
const value = 0.12345;
data!.qpos.fill(value);
const expected = new Float64Array(data!.qpos.length).fill(value);
expectArraysEqual(data!.qpos, expected);
});
// Corresponds to bindings_test.py:test_array_is_a_view
it('should check that array is a view', () => {
const qposRef = data!.qpos;
const value = 0.789;
data!.qpos.fill(value);
const expected = new Float64Array(data!.qpos.length).fill(value);
expectArraysEqual(qposRef, expected);
});
// Corresponds to bindings_test.py:test_mjmodel_can_read_and_write_opt
it('should read and write MjOption', () => {
expect(model!.opt.timestep).toEqual(0.002);
expectArraysEqual(model!.opt.gravity, new Float64Array([0, 0, -9.81]));
const optRef = model!.opt;
model!.opt.timestep = 0.001;
expect(optRef.timestep).toEqual(0.001);
const gravityRef = optRef.gravity;
model!.opt.gravity[1] = 0.1;
expectArraysEqual(gravityRef, new Float64Array([0, 0.1, -9.81]));
model!.opt.gravity.fill(0.2);
expectArraysEqual(gravityRef, new Float64Array([0.2, 0.2, 0.2]));
});
// Corresponds to bindings_test.py:test_mjmodel_can_read_and_write_stat
it('should read and write MjStat', () => {
expect(model!.stat.meanmass).not.toEqual(0);
const statRef = model!.stat;
model!.stat.meanmass = 1.2;
expect(statRef.meanmass).toEqual(1.2);
});
// Corresponds to bindings_test.py:test_mjmodel_can_read_and_write_vis
it('should read and write MjVis', () => {
expect(model!.vis.quality.shadowsize).toEqual(51);
const visRef = model!.vis;
model!.vis.quality.shadowsize = 100;
expect(visRef.quality.shadowsize).toEqual(100);
});
// Corresponds to bindings_test.py:test_mjmodel_can_access_names_directly
it('should access names directly from the model', () => {
const modelName = new TextDecoder().decode(
model!.names.slice(0, model!.names.indexOf(0)));
expect(modelName).toEqual('test');
const startGeomNameIndex = model!.name_geomadr[0];
const endGeomNameIndex = model!.names.indexOf(0, startGeomNameIndex);
const geomName = new TextDecoder().decode(
model!.names.slice(startGeomNameIndex, endGeomNameIndex));
expect(geomName).toEqual('myplane');
});
// Corresponds to bindings_test.py:test_mjmodel_names_doesnt_copy
it('should not copy names when accessing them multiple times', () => {
const names1 = model!.names;
const names2 = model!.names;
expect(names1).toEqual(names2);
});
// Corresponds to bindings_test.py:test_mjoption_can_make_default
it('should create a default MjOption', () => {
const opt = new mujoco.MjOption();
expect(opt.timestep).toEqual(0.002);
expectArraysEqual(opt.gravity, new Float64Array([0, 0, -9.81]));
});
// Corresponds to bindings_test.py:test_mjoption_can_copy
it('should copy MjOption', () => {
const opt1 = new mujoco.MjOption();
opt1.timestep = 0.001;
opt1.gravity.set([2, 2, 2]);
const opt2 = opt1.copy();
expect(opt2.timestep).toEqual(0.001);
expectArraysEqual(opt2.gravity, new Float64Array([2, 2, 2]));
opt1.timestep = 0.005;
opt1.gravity.set([5, 5, 5]);
expect(opt2.timestep).toEqual(0.001);
expectArraysEqual(opt2.gravity, new Float64Array([2, 2, 2]));
});
// Corresponds to bindings_test.py:test_mjdata_can_read_warning_array
it('should read warning array from MjData', () => {
expect(data!.warning.size()).toEqual(mujoco.mjtWarning.mjNWARNING.value);
data!.qpos[0] = NaN;
mujoco.mj_checkPos(model!, data!);
expect(data!.warning.get(mujoco.mjtWarning.mjWARN_BADQPOS.value)!.number)
.toEqual(1);
});
// Corresponds to bindings_test.py:test_mjcontact_can_copy
it('should copy MjContact', () => {
mujoco.mj_forward(model!, data!!);
const contacts: MjContactVec = data!.contact;
const originalContact = contacts.get(0)!;
const originalPos = new Float64Array(originalContact.pos);
const copiedContact = originalContact.copy();
copiedContact.delete();
expectArraysClose(originalContact.pos, originalPos);
originalContact.delete();
});
// Corresponds to bindings_test.py:test_mj_step
it('should step the simulation forward', () => {
const displacement = 0.55;
data!.qpos[2] += displacement;
mujoco.mj_forward(model!, data!);
const gravity = -model!.opt.gravity[2];
const expectedContactTime = Math.sqrt(2 * displacement / gravity);
model!.opt.timestep = 2 ** -9;
expect(data!.time).toEqual(0);
while (data!.time < expectedContactTime) {
expect(data!.ncon).toEqual(0);
expect(data!.efc_type.length).toEqual(0);
const prevTime = data!.time;
mujoco.mj_step(model!, data!);
expect(data!.time).toEqual(prevTime + model!.opt.timestep);
}
mujoco.mj_forward(model!, data!);
const contact = data!.contact;
expect(data!.ncon).toEqual(4);
expect(data!.efc_type.length).toEqual(16);
expectArraysClose(
contact.get(0)!.pos.slice(0, 2), new Float64Array([-0.1, -0.1]));
expectArraysClose(
contact.get(1)!.pos.slice(0, 2), new Float64Array([0.1, -0.1]));
expectArraysClose(
contact.get(2)!.pos.slice(0, 2), new Float64Array([-0.1, 0.1]));
expectArraysClose(
contact.get(3)!.pos.slice(0, 2), new Float64Array([0.1, 0.1]));
mujoco.mj_resetData(model!, data!);
expect(data!.ncon).toEqual(0);
expect(data!.efc_type.length).toEqual(0);
});
// Corresponds to bindings_test.py:test_mj_struct_equality_array
it('should check MjContact equality with array', () => {
const contact1 = new mujoco.MjContact();
const contact2 = new mujoco.MjContact();
try {
contact1.H[3] = 1;
expect(contact1.H).not.toEqual(contact2.H);
contact2.H[3] = 1;
expect(contact1).toEqual(contact2);
} finally {
contact1.delete();
contact2.delete();
}
});
// Corresponds to bindings_test.py:test_mj_struct_list_equality
it('should check MjContactVec equality', () => {
const tempXmlFilename2 = '/tmp/model2.xml';
writeXMLFile(tempXmlFilename2, TEST_XML);
const model2 = mujoco.MjModel!.mj_loadXML(tempXmlFilename2);
const data2 = new mujoco.MjData(model2);
try {
mujoco.mj_forward(model!, data!);
expect(data!.ncon).toEqual(4);
mujoco.mj_forward(model2, data2);
expect(data2.ncon).toEqual(4);
expect(data2.contact).toEqual(data!.contact);
data!.qpos[3] = Math.cos(Math.PI / 8);
data!.qpos[4] = Math.sin(Math.PI / 8);
data!.qpos[5] = 0;
data!.qpos[6] = 0;
data!.qpos[2] = (Math.sqrt(2) - 1) * 0.1 - 1e-6;
mujoco.mj_forward(model!, data!);
expect(data!.ncon).toEqual(2);
expect(data2.contact.size()).not.toEqual(data!.contact.size());
expect(data!.contact).not.toBe(data!.warning);
} finally {
model2.delete();
data2.delete();
unlinkXMLFile(tempXmlFilename2);
}
});
// Corresponds to bindings_test.py:test_getsetstate
it('should get and set the state', () => {
mujoco.mj_step(model!, data!);
const invalidSig = 2 ** mujoco.mjtState.mjNSTATE.value;
expect(() => {
mujoco.mj_stateSize(model!, invalidSig);
})
.toThrowError(
'MuJoCo Error: mj_stateSize: invalid state signature 16384 >= 2^mjNSTATE');
const sig = mujoco.mjtState.mjSTATE_INTEGRATION.value;
const size = mujoco.mj_stateSize(model!, sig);
const stateBadSize = mujoco.DoubleBuffer.FromArray([size]);
expect(() => {
mujoco.mj_getState(model!, data!, stateBadSize, sig);
})
.toThrowError(
'MuJoCo Error: [mj_getState] state must have size 81, got 1');
const state0 = new mujoco.DoubleBuffer(size);
mujoco.mj_getState(model!, data!, state0, sig);
mujoco.mj_step(model!, data!);
const state1a = mujoco.DoubleBuffer.FromArray(new Array(size).fill(1));
mujoco.mj_getState(model!, data!, state1a, sig);
mujoco.mj_setState(model!, data!, state0.GetView(), sig);
mujoco.mj_step(model!, data!);
const state1b = mujoco.DoubleBuffer.FromArray(new Array(size).fill(2));
mujoco.mj_getState(model!, data!, state1b, sig);
expectArraysEqual(state1a.GetView(), state1b.GetView());
});
// Corresponds to bindings_test.py:test_mj_setKeyframe
it('should set and reset a keyframe', () => {
mujoco.mj_step(model!, data!);
const invalidKey = 2;
expect(() => {
mujoco.mj_setKeyframe(model!, data!, invalidKey);
})
.toThrowError(
'MuJoCo Error: mj_setKeyframe: index must be smaller than 2 (keyframes allocated in model)');
const validKey = 1;
const time = data!.time;
const qpos = new Float64Array(data!.qpos);
const qvel = new Float64Array(data!.qvel);
const act = new Float64Array(data!.act);
mujoco.mj_setKeyframe(model!, data!, validKey);
mujoco.mj_step(model!, data!);
expect(time).not.toEqual(data!.time);
mujoco.mj_resetDataKeyframe(model!, data!, validKey);
expect(time).toEqual(data!.time);
expectArraysEqual(qpos, data!.qpos);
expectArraysEqual(qvel, data!.qvel);
expectArraysEqual(act, data!.act);
});
// Corresponds to bindings_test.py:test_mj_angmomMat
it('should compute angular momentum matrix', () => {
data!.qvel.fill(1);
mujoco.mj_forward(model!, data!);
mujoco.mj_subtreeVel(model!, data!);
const mat = new mujoco.DoubleBuffer(3 * model!.nv);
try {
mujoco.mj_angmomMat(model!, data!, mat, 0);
const qvel = new Float64Array(data!.qvel);
const subtreeAngmom = new Float64Array(data!.subtree_angmom.slice(0, 3));
const matView = mat.GetView();
const result = new Float64Array(3).fill(0);
for (let i = 0; i < 3; i++) {
for (let j = 0; j < model!.nv; j++) {
result[i] += matView[i * model!.nv + j] * qvel[j];
}
}
expectArraysClose(result, subtreeAngmom);
} finally {
mat.delete();
}
});
// Corresponds to bindings_test.py:test_mj_jacSite
it('should compute site jacobian', () => {
mujoco.mj_forward(model!, data!);
const siteId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_SITE.value, 'mysite');
const jacp = new mujoco.DoubleBuffer(3 * model!.nv);
const jacr = new mujoco.DoubleBuffer(3 * model!.nv);
try {
mujoco.mj_jacSite(model!, data!, jacp, null, siteId);
const expectedJacp = new Float64Array(3 * model!.nv).fill(0);
expectedJacp[6] = -1;
expectArraysClose(jacp.GetView(), expectedJacp);
mujoco.mj_jacSite(model!, data!, null, jacr, siteId);
const expectedJacr = new Float64Array(3 * model!.nv).fill(0);
expectedJacr[1 * model!.nv + 6] = 1;
expectArraysClose(jacr.GetView(), expectedJacr, 2);
jacp.GetView().fill(0);
jacr.GetView().fill(0);
mujoco.mj_jacSite(model!, data!, jacp, jacr, siteId);
expectArraysClose(jacp.GetView(), expectedJacp, 2);
expectArraysClose(jacr.GetView(), expectedJacr, 2);
const badJacp = new mujoco.DoubleBuffer(3 * 6);
try {
expect(() => {
mujoco.mj_jacSite(model!, data!, badJacp, null, siteId);
})
.toThrowError(
'MuJoCo Error: [mj_jacSite] jacp must have size 30, got 18');
} finally {
badJacp.delete();
}
const badJacr = new mujoco.DoubleBuffer(4 * 7);
try {
expect(() => {
mujoco.mj_jacSite(model!, data!, null, badJacr, siteId);
})
.toThrowError(
'MuJoCo Error: [mj_jacSite] jacr must have size 30, got 28');
} finally {
badJacr.delete();
}
} finally {
jacp.delete();
jacr.delete();
}
});
// Corresponds to bindings_test.py:test_can_initialize_mjv_structs
it('should initialize mjv structs', () => {
expect(new mujoco.MjvScene()).toBeDefined();
expect(new mujoco.MjvCamera()).toBeDefined();
expect(new mujoco.MjvGLCamera()).toBeDefined();
expect(new mujoco.MjvGeom()).toBeDefined();
expect(new mujoco.MjvLight()).toBeDefined();
expect(new mujoco.MjvOption()).toBeDefined();
expect(new mujoco.MjvFigure()).toBeDefined();
expect(new mujoco.MjvScene(model, 100)).toBeDefined();
});
// Corresponds to bindings_test.py:test_mjv_camera
it('should handle MjvCamera correctly', () => {
const camera = new mujoco.MjvCamera();
camera.type = mujoco.mjtCamera.mjCAMERA_TRACKING.value;
camera.fixedcamid = 2 ** 31 - 1;
expect(camera.fixedcamid).toEqual(2 ** 31 - 1);
});
// Corresponds to bindings_test.py:test_mjv_scene
it('should handle MjvScene correctly', () => {
const scene = new mujoco.MjvScene(model, 100);
expect(scene.ngeom).toEqual(0);
expect(scene.maxgeom).toEqual(100);
expect(scene.geoms.size()).toEqual(0);
mujoco.mj_forward(model!, data!);
mujoco.mjv_updateScene(
model!, data!, new mujoco.MjvOption(), new mujoco.MjvPerturb(),
new mujoco.MjvCamera(), mujoco.mjtCatBit.mjCAT_ALL.value, scene);
expect(scene.geoms.size()).toEqual(scene.ngeom);
expect(scene.ngeom).toBeGreaterThan(0);
});
// Corresponds to bindings_test.py:test_mjv_scene_without_model
it('should initialize MjvScene without a model', () => {
const scene = new mujoco.MjvScene();
expect(scene.scale).toEqual(1.0);
expect(scene.maxgeom).toEqual(0);
});
// Corresponds to bindings_test.py:test_inverse_fd_none
it('should compute inverse dynamics derivatives with null outputs', () => {
const eps = 1e-6;
const flg_centered = 0;
expect(
() => mujoco.mjd_inverseFD(
model!, data!, eps, flg_centered, null, null, null, null, null,
null, null))
.not.toThrow();
});
// Corresponds to bindings_test.py:test_inverse_fd
it('should compute inverse dynamics derivatives', () => {
const eps = 1e-6;
const flg_centered = 0;
const nv = model!.nv;
const nsensordata = model!.nsensordata;
const nM = model!.nM;
const dfDq = new mujoco.DoubleBuffer(nv * nv);
const dfDv = new mujoco.DoubleBuffer(nv * nv);
const dfDa = new mujoco.DoubleBuffer(nv * nv);
const dsDq = new mujoco.DoubleBuffer(nv * nsensordata);
const dsDv = new mujoco.DoubleBuffer(nv * nsensordata);
const dsDa = new mujoco.DoubleBuffer(nv * nsensordata);
const dmDq = new mujoco.DoubleBuffer(nv * nM);
try {
mujoco.mjd_inverseFD(
model!, data!, eps, flg_centered, dfDq, dfDv, dfDa, dsDq, dsDv, dsDa,
dmDq);
expect(norm(dfDq.GetView())).toBeGreaterThan(eps);
expect(norm(dfDv.GetView())).toBeGreaterThan(eps);
expect(norm(dfDa.GetView())).toBeGreaterThan(eps);
expect(norm(dsDq.GetView())).toBeGreaterThan(eps);
expect(norm(dsDv.GetView())).toBeGreaterThan(eps);
expect(norm(dsDa.GetView())).toBeGreaterThan(eps);
} finally {
dfDq.delete();
dfDv.delete();
dfDa.delete();
dsDq.delete();
dsDv.delete();
dsDa.delete();
dmDq.delete();
}
});
// Corresponds to bindings_test.py:test_geom_distance
it('should compute geom distance', () => {
mujoco.mj_forward(model!, data!);
const fromto = new mujoco.DoubleBuffer(6);
try {
const dist = mujoco.mj_geomDistance(model!, data!, 0, 2, 200, fromto);
expect(dist).toEqual(41.9);
expectArraysClose(
fromto.GetView(),
new Float64Array([42.0, 0.0, 0.0, 42.0, 0.0, 41.9]));
} finally {
fromto.delete();
}
});
// Corresponds to bindings_test.py:test_mjd_sub_quat
it('should compute sub quaternion derivatives', () => {
const quat1 = [0.2, 0.3, 0.3, 0.4];
const quat2 = [0.1, 0.2, 0.4, 0.5];
const d1 = new mujoco.DoubleBuffer(9);
const d2 = new mujoco.DoubleBuffer(9);
const d3 = new mujoco.DoubleBuffer(9);
const d4 = new mujoco.DoubleBuffer(9);
try {
mujoco.mjd_subQuat(quat1, quat2, d1, d2);
mujoco.mjd_subQuat(quat1, quat2, null, d3);
mujoco.mjd_subQuat(quat1, quat2, d4, null);
expectArraysEqual(d2.GetView(), d3.GetView());
expectArraysEqual(d1.GetView(), d4.GetView());
} finally {
d1.delete();
d2.delete();
d3.delete();
d4.delete();
}
});
// Corresponds to bindings_test.py:test_mjd_quat_integrate
it('should compute quaternion derivatives for integration', () => {
const scale = 0.1;
const vel = [0.2, 0.3, 0.3];
const dQuat = new mujoco.DoubleBuffer(9);
const dVel = new mujoco.DoubleBuffer(9);
const dH = new mujoco.DoubleBuffer(3);
try {
mujoco.mjd_quatIntegrate(vel, scale, dQuat, dVel, dH);
expect(norm(dQuat.GetView())).toBeGreaterThan(0);
expect(norm(dVel.GetView())).toBeGreaterThan(0);
expect(norm(dH.GetView())).toBeGreaterThan(0);
} finally {
dQuat.delete();
dVel.delete();
dH.delete();
}
});
// Corresponds to bindings_test.py:test_banded
it('should handle banded matrices', () => {
const nTotal = 4;
const nBand = 1;
const nDense = 1;
const dense = [
1.0,
0,
0,
0.1,
0,
2.0,
0,
0.2,
0,
0,
3.0,
0.3,
0.1,
0.2,
0.3,
4.0,
];
const band =
new mujoco.DoubleBuffer(nBand * (nTotal - nDense) + nDense * nTotal);
const vec = mujoco.DoubleBuffer.FromArray([2.0, 2.0, 3.0, 4.0]);
const res = new mujoco.DoubleBuffer(4);
try {
mujoco.mju_dense2Band(band, dense, nTotal, nBand, nDense);
for (let i = 0; i < 4; i++) {
const index = mujoco.mju_bandDiag(i, nTotal, nBand, nDense);
expect(band.GetView()[index]).toEqual(i + 1);
}
const dense2 = new mujoco.DoubleBuffer(nTotal * nTotal);
const flgSym = 1;
mujoco.mju_band2Dense(
dense2, band.GetView(), nTotal, nBand, nDense, flgSym);
expectArraysEqual(new Float64Array(dense), dense2.GetView());
const nVec = 1;
mujoco.mju_bandMulMatVec(
res, band.GetView(), vec.GetView(), nTotal, nBand, nDense, nVec,
flgSym);
const expected = new Float64Array([2.4, 4.8, 10.2, 17.5]);
expectArraysClose(res.GetView(), expected);
const diagAdd = 0;
const diagMul = 0;
mujoco.mju_cholFactorBand(band, nTotal, nBand, nDense, diagAdd, diagMul);
mujoco.mju_cholSolveBand(
res, band.GetView(), vec.GetView(), nTotal, nBand, nDense);
const expectedSolved = new Float64Array([1.9111, 0.9111, 0.9111, 0.8333]);
expectArraysClose(res.GetView(), expectedSolved);
} finally {
band.delete();
vec.delete();
res.delete();
}
});
// Corresponds to bindings_test.py:test_mju_box_qp
it('should handle box QP solver', () => {
const n = 5;
const res = new mujoco.DoubleBuffer(n);
const r = new mujoco.DoubleBuffer(n * (n + 7));
const index = new mujoco.IntBuffer(n);
const h = new mujoco.DoubleBuffer(n * n);
const g = mujoco.DoubleBuffer.FromArray(new Array(n).fill(1));
const lower = new Array(n).fill(-1);
const upper = new Array(n).fill(1);
try {
for (let i = 0; i < n; i++) {
h.GetView()[i * (n + 1)] = 1;
}
const rank = mujoco.mju_boxQP(
res, r, index, h.GetView(), g.GetView(), lower, upper);
expect(rank).toBeGreaterThan(-1);
} finally {
res.delete();
r.delete();
index.delete();
h.delete();
g.delete();
}
});
// Corresponds to bindings_test.py:test_mju_fill
it('should fill an array with a value', () => {
const res = new mujoco.DoubleBuffer(3);
try {
mujoco.mju_fill(res, 1.5);
expectArraysEqual(res.GetView(), new Float64Array([1.5, 1.5, 1.5]));
} finally {
res.delete();
}
});
// Corresponds to bindings_test.py:test_mju_eye
it('should create an identity matrix', () => {
const eye3 = new mujoco.DoubleBuffer(3 * 3);
try {
mujoco.mju_eye(eye3);
const expected = new Float64Array([
1,
0,
0,
0,
1,
0,
0,
0,
1,
]);
expectArraysEqual(eye3.GetView(), expected);
} finally {
eye3.delete();
}
});
// Corresponds to bindings_test.py:test_mju_symmetrize
it('should symmetrize a matrix', () => {
const mat = [
0, 0.066, 0.13, 0.2, 0.26, 0.33, 0.4, 0.46, 0.53, 0.6, 0.66, 0.73, 0.8,
0.86, 0.93, 1
];
const res = new mujoco.DoubleBuffer(16);
try {
mujoco.mju_symmetrize(res, mat, 4);
const expected = new Float64Array([
0, 0.163, 0.33, 0.5, 0.163, 0.33, 0.5, 0.66, 0.33, 0.5, 0.66, 0.83, 0.5,
0.66, 0.8, 1
]);
expectArraysClose(res.GetView(), expected);
} finally {
res.delete();
}
});
// Corresponds to bindings_test.py:test_mju_clip
it('should clip a value', () => {
expect(mujoco.mju_clip(1.5, 1.0, 2.0)).toEqual(1.5);
expect(mujoco.mju_clip(1.5, 2.0, 3.0)).toEqual(2.0);
expect(mujoco.mju_clip(1.5, 0.0, 1.0)).toEqual(1.0);
});
// Corresponds to bindings_test.py:test_mju_mul_vec_mat_vec
it('should multiply a vector by a matrix and a vector', () => {
const vec1 = [1.0, 2.0, 3.0];
const vec2 = [3.0, 2.0, 1.0];
const mat = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
expect(mujoco.mju_mulVecMatVec(vec1, mat, vec2)).toEqual(204.0);
});
// Corresponds to bindings_test.py:test_mju_dense_to_sparse
it('should convert a dense matrix to a sparse matrix', () => {
const mat = [0.0, 1.0, 0.0, 2.0, 0.0, 3.0];
const res = new mujoco.DoubleBuffer(3);
const rowNnz = new mujoco.IntBuffer(2);
const rowAdr = new mujoco.IntBuffer(2);
const colInd = new mujoco.IntBuffer(3);
try {
const status =
mujoco.mju_dense2sparse(res, mat, 2, 3, rowNnz, rowAdr, colInd);
expect(status).toEqual(0);
expectArraysEqual(res.GetView(), new Float64Array([1.0, 2.0, 3.0]));
expectArraysEqual(rowNnz.GetView(), new Int32Array([1, 2]));
expectArraysEqual(rowAdr.GetView(), new Int32Array([0, 1]));
expectArraysEqual(colInd.GetView(), new Int32Array([1, 0, 2]));
} finally {
rowNnz.delete();
rowAdr.delete();
colInd.delete();
}
});
// Corresponds to bindings_test.py:test_mju_sparse_to_dense
it('should convert a sparse matrix to a dense matrix', () => {
const mat = [1.0, 2.0, 3.0];
const expected = new Float64Array([0.0, 1.0, 0.0, 2.0, 0.0, 3.0]);
const rowNnz = [1, 2];
const rowAdr = [0, 1];
const colInd = [1, 0, 2];
const res = new mujoco.DoubleBuffer(6);
try {
mujoco.mju_sparse2dense(res, mat, 2, 3, rowNnz, rowAdr, colInd);
expectArraysEqual(res.GetView(), expected);
} finally {
res.delete();
}
});
// Corresponds to bindings_test.py:test_mju_euler_to_quat
it('should convert euler to quaternion', () => {
const quat = new mujoco.DoubleBuffer(4);
const euler = [0, Math.PI / 2, 0];
const seq = 'xyz';
try {
mujoco.mju_euler2Quat(quat, euler, seq);
const expectedQuat = [Math.sqrt(0.5), 0, Math.sqrt(0.5), 0.0];
expectArraysClose(quat.GetView(), new Float64Array(expectedQuat));
expect(() => {
mujoco.mju_euler2Quat(quat, euler, 'xy');
})
.toThrowError(
'MuJoCo Error: mju_euler2Quat: seq must contain exactly 3 characters');
expect(() => {
mujoco.mju_euler2Quat(quat, euler, 'xyzy');
})
.toThrowError(
'MuJoCo Error: mju_euler2Quat: seq must contain exactly 3 characters');
expect(() => {
mujoco.mju_euler2Quat(quat, euler, 'xYp');
})
.toThrowError(
`MuJoCo Error: mju_euler2Quat: seq[2] is 'p', should be one of x, y, z, X, Y, Z`);
} finally {
quat.delete();
}
});
// Corresponds to bindings_test.py:test_texture_size
it('should load a texture from a model', () => {
const texFilename = 'tex.png';
writeXMLFile(texFilename, 'tex');
const tempXmlFilename = '/tmp/with_texture.xml';
const TEST_XML_TEXTURE = `
<mujoco>
<asset>
<texture name="tex" type="2d" builtin="checker" rgb1=".2 .3 .4" rgb2=".1 0.15 0.2"
width="512" height="512" mark="cross" markrgb=".8 .8 .8"/>
<material name="mat" reflectance="0.3" texture="tex" texrepeat="1 1" texuniform="true"/>
</asset>
<worldbody>
<geom type="plane" size="1 1 1" material="mat"/>
</worldbody>
</mujoco>
`;
writeXMLFile(tempXmlFilename, TEST_XML_TEXTURE);
const model = mujoco.MjModel!.mj_loadXML(tempXmlFilename);
try {
expect(model).toBeDefined();
expect(model!.tex_height).toEqual(new Int32Array([512]));
expect(model!.tex_width).toEqual(new Int32Array([512]));
} finally {
model?.delete();
unlinkXMLFile(texFilename);
unlinkXMLFile(tempXmlFilename);
}
});
it('should create distinct MjModel instances and copy correctly', () => {
const model1 = model!;
const model2 = new mujoco.MjModel(model1);
try {
assertExists(model1);
assertExists(model2);
expect(model1).not.toBe(model2);
expect(model1.opt.timestep).toEqual(model2.opt.timestep);
expect(model1.stat.meanmass).toEqual(model2.stat.meanmass);
model1.opt.timestep = 0.123;
expect(model1.opt.timestep).toEqual(0.123);
expect(model2.opt.timestep).not.toEqual(model1.opt.timestep);
expect(model2.opt.timestep).toEqual(0.002);
} finally {
model2.delete();
}
});
it('should create distinct MjData instances and copy correctly', () => {
const data1 = new mujoco.MjData(model!);
const data2 = new mujoco.MjData(model!, data1);
try {
assertExists(data1);
assertExists(data2);
expect(data1).not.toBe(data2);
expectArraysEqual(data1.qpos, data2.qpos);
expectArraysEqual(data1.qvel, data2.qvel);
data1.qpos[0] = 1.0;
expect(data1.qpos[0]).toEqual(1.0);
expect(data2.qpos[0]).not.toEqual(data1.qpos[0]);
} finally {
data1.delete();
data2.delete();
}
});
// Corresponds to specs_test.py:test_address
it('should create distinct MjSpec instances and copy correctly', () => {
const spec1 = mujoco.parseXMLString(TEST_XML);
const spec2 = new mujoco.MjSpec(spec1);
try {
assertExists(spec1);
assertExists(spec2);
expect(spec1).not.toBe(spec2);
expect(spec1.modelname).toEqual(spec2.modelname);
expect(spec1.option.timestep).toEqual(spec2.option.timestep);
expect(spec1.visual.quality.shadowsize)
.toEqual(spec2.visual.quality.shadowsize);
expect(spec1.stat.meanmass).toEqual(spec2.stat.meanmass);
spec1.modelname = 'modified';
expect(spec2.modelname).not.toEqual(spec1.modelname);
expect(spec2.modelname).toEqual('test');
} finally {
spec1?.delete();
spec2?.delete();
}
});
// Corresponds to partial of user_api_test.cc:TEST_F(PluginTest, AttachPlugin)
it('should correctly copy MjSpec instances when attaching plugins', () => {
const xmlPlugin1 = `
<mujoco model="MuJoCo Model">
<worldbody>
<body name="body"/>
</worldbody>
</mujoco>`;
const spec1 = mujoco.parseXMLString(xmlPlugin1);
const spec2 = new mujoco.MjSpec(spec1);
const spec3 = new mujoco.MjSpec(spec1);
try {
assertExists(spec1);
assertExists(spec2);
assertExists(spec3);
spec2.modelname = 'first_copy';
spec3.modelname = 'second_copy';
expect(spec1.modelname).toEqual('MuJoCo Model');
expect(spec2.modelname).toEqual('first_copy');
expect(spec3.modelname).toEqual('second_copy');
} finally {
spec1?.delete();
spec2?.delete();
spec3?.delete();
}
});
it('should save the model to an XML file', () => {
const tempXmlFilename = '/tmp/saved_model.xml';
const xml = `<mujoco model="MuJoCo Model">
<compiler angle="radian"/>
<asset>
<texture type="2d" colorspace="auto" name="tex" builtin="checker" mark="cross" rgb1="0.2 0.3 0.4" rgb2="0.1 0.15 0.2" markrgb="0.8 0.8 0.8" width="512" height="512"/>
<material name="mat" texture="tex" texuniform="true" reflectance="0.3"/>
</asset>
<worldbody>
<geom size="1 1 1" type="plane" material="mat"/>
</worldbody>
</mujoco>`;
writeXMLFile(tempXmlFilename, xml);
const model = mujoco.MjModel!.mj_loadXML(tempXmlFilename);
try {
mujoco.mj_saveLastXML(tempXmlFilename, model!);
const savedXmlContent =
(mujoco as any).FS.readFile(tempXmlFilename, {encoding: 'utf8'});
// Remove whitespaces from the saved XML content to avoid flakiness.
expect(savedXmlContent.replace(/\s/g, ''))
.toEqual(xml.replace(/\s/g, ''));
} finally {
unlinkXMLFile(tempXmlFilename);
}
});
it('can call mj_setLengthRange with actuators', () => {
const tempXmlFilename = '/tmp/actuator_model.xml';
const actuatorXml = `<mujoco model="actuator_test">
<worldbody>
<body>
<inertial pos="0 0 0" mass="1" diaginertia="1 1 1"/>
<joint name="hinge1" type="hinge" axis="0 1 0"/>
</body>
</worldbody>
<actuator>
<position name="myactuator" joint="hinge1"/>
</actuator>
</mujoco>`;
writeXMLFile(tempXmlFilename, actuatorXml);
const model = mujoco.MjModel.mj_loadXML(tempXmlFilename);
assertExists(model);
const data = new mujoco.MjData(model);
assertExists(data);
const opt = new mujoco.MjLROpt();
try {
const result = mujoco.mj_setLengthRange(
model,
data,
/* index= */ 0,
opt,
);
expect(result).toBe(1);
} finally {
opt.delete();
model.delete();
data.delete();
unlinkXMLFile(tempXmlFilename);
}
});
it('should compile a spec from XML string with no assets', () => {
let spec = null;
let model = null;
try {
spec = mujoco.parseXMLString(TEST_XML);
expect(spec).not.toBeNull();
model = mujoco.mj_compile(spec);
expect(model).not.toBeNull();
expect(model.nq).toBeGreaterThan(0);
const jointId =
mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT.value, 'myhinge');
expect(jointId).toBeGreaterThanOrEqual(0);
} finally {
if (spec) {
spec.delete();
}
if (model) {
model.delete();
}
}
});
it('should compile a spec from XML with .obj asset', () => {
const xml = `
<mujoco>
<asset>
<mesh file="cube.obj"/>
</asset>
<worldbody>
<geom type="mesh" mesh="cube"/>
</worldbody>
</mujoco>`;
const cube1 = `
v -1 -1 1
v 1 -1 1
v -1 1 1
v 1 1 1
v -1 1 -1
v 1 1 -1
v -1 -1 -1
v 1 -1 -1`;
let spec: MjSpec|null = null;
let model: MjModel|null = null;
let vfs: MjVFS|null = null;
try {
spec = mujoco.parseXMLString(xml);
assertExists(spec);
vfs = new mujoco.MjVFS();
vfs.addBuffer('cube.obj', new TextEncoder().encode(cube1));
assertExists(vfs);
model = mujoco.mj_compile(spec, vfs);
assertExists(model);
expect(model.nmesh).toBe(1);
const meshId =
mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_MESH.value, 'cube');
expect(meshId).toBeGreaterThanOrEqual(0);
} finally {
spec?.delete();
vfs?.delete();
model?.delete();
}
});
it('should compile a spec from XML with 10 meshes and 10 textures', () => {
const assetsCount = 10;
let xml = `<mujoco>
<asset>`;
const cube1 = `
v -1 -1 1
v 1 -1 1
v -1 1 1
v 1 1 1
v -1 1 -1
v 1 1 -1
v -1 -1 -1
v 1 -1 -1`;
const dummyTex = 'dummy texture data';
for (let i = 0; i < assetsCount; i++) {
xml += `<mesh name="cube${i}" file="cube${i}.obj"/>`;
xml += `<texture name="tex${
i}" type="2d" builtin="checker" width="16" height="16"/>`;
}
xml += `</asset><worldbody>`;
for (let i = 0; i < assetsCount; i++) {
xml += `<geom type="mesh" mesh="cube${i}"/>`;
}
xml += `</worldbody></mujoco>`;
let spec: MjSpec|null = null;
let model: MjModel|null = null;
let vfs: MjVFS|null = null;
try {
spec = mujoco.parseXMLString(xml);
assertExists(spec);
vfs = new mujoco.MjVFS();
assertExists(vfs);
for (let i = 0; i < assetsCount; i++) {
vfs.addBuffer(`cube${i}.obj`, new TextEncoder().encode(cube1));
vfs.addBuffer(`tex${i}.txt`, new TextEncoder().encode(dummyTex));
}
model = mujoco.mj_compile(spec, vfs);
assertExists(model);
expect(model.nmesh).toBe(assetsCount);
expect(model.ntex).toBe(assetsCount);
for (let i = 0; i < assetsCount; i++) {
const meshId = mujoco.mj_name2id(
model, mujoco.mjtObj.mjOBJ_MESH.value, `cube${i}`);
expect(meshId).toBe(i);
const tex = model.tex(`tex${i}`);
expect(tex).toBeDefined();
expect(tex!.name).toBe(`tex${i}`);
}
} finally {
spec?.delete();
vfs?.delete();
model?.delete();
}
});
describe('MjModel named access', () => {
// Corresponds to
// bindings_test.py:test_named_indexing_invalid_names_in_model
it('should throw an error for invalid geom names in model', () => {
expect(() => {
model!.geom('badgeom');
})
.toThrowError(
`MuJoCo Error: Invalid name 'badgeom' for geom. Valid names: ['mocap-sphere', 'mybox', 'myplane']`);
});
// Corresponds to
// bindings_test.py:test_named_indexing_invalid_index_in_model
it('should throw an error for invalid geom indices in model', () => {
const numGeoms = model!.ngeom;
expect(() => {
model!.geom(numGeoms);
})
.toThrowError(
`MuJoCo Error: Invalid index 3 for geom. Valid indices from 0 to 2`);
expect(() => {
model!.geom(-1);
})
.toThrowError(
`MuJoCo Error: Invalid index -1 for geom. Valid indices from 0 to 2`);
});
// Corresponds to bindings_test.py:test_named_indexing_geom_size
it('should correctly access geom size using named indexing', () => {
const boxId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_GEOM.value, 'mybox');
assertExists(boxId);
// Check that named indexing returns the same object as id indexing.
expect(model!.geom('mybox')).toEqual(model!.geom(boxId));
expect(model!.geom('mybox')!.size).toEqual(model!.geom(boxId)!.size);
expect(model!.geom('mybox')!.size.length).toEqual(3);
// Test that the indexer is returning a view into the underlying struct.
const sizeFromIndexer = model!.geom('mybox')!.size;
const originalGeomSize = new Float64Array(model!.geom_size);
model!.geom_size.set([7, 11, 13], boxId * 3);
expectArraysEqual(sizeFromIndexer, new Float64Array([7, 11, 13]));
model!.geom('mybox')!.size.set([5, 3, 2]);
expectArraysEqual(
model!.geom_size.slice(boxId * 3, boxId * 3 + 3),
new Float64Array([5, 3, 2]));
});
// Corresponds to bindings_test.py:test_named_indexing_geom_quat
it('should correctly access geom quat using named indexing', () => {
const boxId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_GEOM.value, 'mybox');
assertExists(boxId);
// Check that named indexing returns the same object as id indexing.
expect(model!.geom('mybox')).toEqual(model!.geom(boxId));
expect(model!.geom('mybox')!.quat).toEqual(model!.geom(boxId)!.quat);
expect(model!.geom('mybox')!.quat.length).toEqual(4);
// Test that the indexer is returning a view into the underlying struct.
const quatFromIndexer = model!.geom('mybox')!.quat;
model!.geom_quat.set([5, 10, 15, 20], boxId * 4);
expectArraysEqual(quatFromIndexer, new Float64Array([5, 10, 15, 20]));
model!.geom('mybox')!.quat.set([12, 9, 6, 3]);
expectArraysEqual(
model!.geom_quat.slice(boxId * 4, boxId * 4 + 4),
new Float64Array([12, 9, 6, 3]));
});
it('should support named access for Joints', () => {
const xml = `<mujoco model="Box falling">
<option viscosity="1"/>
<worldbody>
<light diffuse=".5 .5 .5" pos="0 0 3" dir="0 0 -1"/>
<geom name="MyFloor" type="plane" size="1 1 0.1" rgba=".9 0 0 1" user="5 4 3 2 1"/>
<geom name="MyWall" type="plane" size="0.1 1 0.1" rgba="0 1 0 1" user="5 4 3"/>
<body pos="0 0 1" name="MyBox">
<joint type="free" name="root"/>
<geom name="MyBoxGeom" type="box" size=".1 .2 .3" rgba="0 .9 0 1"/>
<body pos=".1 0 0" name="MyHingeBody">
<joint frictionloss="0.4" type="hinge" name="hinge" axis="0 1 0"/>
<geom type="capsule" size=".05 .05" rgba="0 0 .9 1"/>
</body>
</body>
</worldbody>
</mujoco>`;
const tempXmlFilename = '/tmp/jnt.xml';
writeXMLFile(tempXmlFilename, xml);
const model = mujoco.MjModel.mj_loadXML(tempXmlFilename);
try {
assertExists(model);
const j0 = model.jnt('root');
expect(j0.id).toBe(0);
expect(j0.name).toBe('root');
expect(j0.type).toBe(mujoco.mjtJoint.mjJNT_FREE.value);
expect(j0.qposadr).toBe(0);
expect(j0.dofadr).toBe(0);
expect(j0.axis).toEqual(new Float64Array([0, 0, 1]));
expect(j0.type).toEqual(mujoco.mjtJoint.mjJNT_FREE.value);
const j0qposStart = model.jnt_qposadr[j0.id];
const j0qposEnd =
(j0.id < model.njnt - 1) ? model.jnt_qposadr[j0.id + 1] : model.nq;
const expectedJ0qpos = model.qpos0.slice(j0qposStart, j0qposEnd);
expectArraysEqual(
expectedJ0qpos, new Float64Array([0, 0, 1, 1, 0, 0, 0]));
expectArraysEqual(j0.qpos0, expectedJ0qpos);
const j0dofStart = model.jnt_dofadr[j0.id];
const j0dofEnd =
(j0.id < model.njnt - 1) ? model.jnt_dofadr[j0.id + 1] : model.nv;
const expectedJ0dof = model.dof_bodyid.slice(j0dofStart, j0dofEnd);
expectArraysEqual(expectedJ0dof, new Int32Array([1, 1, 1, 1, 1, 1]));
expectArraysEqual(j0.bodyid, expectedJ0dof);
const j1 = model.jnt('hinge');
expect(j1.id).toBe(1);
expect(j1.name).toBe('hinge');
expect(j1.type).toBe(mujoco.mjtJoint.mjJNT_HINGE.value);
expect(j1.qposadr).toBe(7);
expect(j1.dofadr).toBe(6);
expect(j1.axis).toEqual(new Float64Array([0, 1, 0]));
expect(j1.frictionloss).toEqual(new Float64Array([0.4]));
const j1qposStart = model.jnt_qposadr[j1.id];
const j1qposEnd =
(j1.id < model.njnt - 1) ? model.jnt_qposadr[j1.id + 1] : model.nq;
const expectedJ1qpos = model.qpos0.slice(j1qposStart, j1qposEnd);
expectArraysEqual(expectedJ1qpos, new Float64Array([0]));
expectArraysEqual(j1.qpos0, expectedJ1qpos);
const j1dofStart = model.jnt_dofadr[j1.id];
const j1dofEnd =
(j1.id < model.njnt - 1) ? model.jnt_dofadr[j1.id + 1] : model.nv;
const expectedJ1dof = model.dof_bodyid.slice(j1dofStart, j1dofEnd);
expectArraysEqual(expectedJ1dof, new Int32Array([2]));
expectArraysEqual(j1.bodyid, new Int32Array([2]));
} finally {
model?.delete();
unlinkXMLFile(tempXmlFilename);
}
});
it('should support named access for HFields', () => {
const xml = `<mujoco model="test_hfield">
<asset>
<hfield name="hf" nrow="2" ncol="3" size="1 2 0.1 0.2"/>
</asset>
<worldbody>
<geom type="hfield" hfield="hf"/>
</worldbody>
</mujoco>`;
const tempXmlFilename = '/tmp/hf.xml';
writeXMLFile(tempXmlFilename, xml);
const model = mujoco.MjModel.mj_loadXML(tempXmlFilename);
try {
assertExists(model);
const hf = model.hfield('hf');
expect(hf.id).toBe(0);
expect(hf.name).toBe('hf');
expect(hf.nrow).toBe(2);
expect(hf.ncol).toBe(3);
expectArraysClose(hf.size, new Float64Array([1, 2, 0.1, 0.2]));
const expectedDataLength =
model.hfield_nrow[hf.id] * model.hfield_ncol[hf.id];
expect(expectedDataLength).toBe(6);
expect(hf.data.length).toBe(expectedDataLength);
} finally {
model?.delete();
unlinkXMLFile(tempXmlFilename);
}
});
it('should support named access for Numeric', () => {
const xml = `<mujoco>
<custom>
<numeric name="n1" data="1 2 3"/>
<numeric name="n2" data="4 5 6 7"/>
</custom>
</mujoco>`;
const tempXmlFilename = '/tmp/num.xml';
writeXMLFile(tempXmlFilename, xml);
const model = mujoco.MjModel.mj_loadXML(tempXmlFilename);
try {
assertExists(model);
const n1 = model.numeric('n1');
const expectedN1data = model.numeric_data.slice(
model.numeric_adr[n1.id],
model.numeric_adr[n1.id] + model.numeric_size[n1.id]);
expect(n1.id).toBe(0);
expect(n1.name).toBe('n1');
expect(n1.size).toBe(3);
expect(expectedN1data).toEqual(new Float64Array([1, 2, 3]));
expectArraysClose(n1.data, expectedN1data);
const n2 = model.numeric('n2');
const expectedN2data = model.numeric_data.slice(
model.numeric_adr[n2.id],
model.numeric_adr[n2.id] + model.numeric_size[n2.id]);
expect(n2.id).toBe(1);
expect(n2.name).toBe('n2');
expect(n2.size).toBe(4);
expect(expectedN2data).toEqual(new Float64Array([4, 5, 6, 7]));
expectArraysClose(n2.data, expectedN2data);
} finally {
model?.delete();
unlinkXMLFile(tempXmlFilename);
}
});
it('should support named access for Texture', () => {
const xml = `<mujoco>
<asset>
<texture name="t1" type="2d" builtin="checker" width="10"
height="12"/>
</asset>
</mujoco>`;
const tempXmlFilename = '/tmp/tex.xml';
writeXMLFile(tempXmlFilename, xml);
const model = mujoco.MjModel.mj_loadXML(tempXmlFilename);
try {
assertExists(model);
const t1 = model.tex('t1');
const texAdr = Number(model.tex_adr[t1.id]);
const expectedData = model.tex_data.slice(
texAdr,
texAdr +
model.tex_height[t1.id] * model.tex_width[t1.id] *
model.tex_nchannel[t1.id]);
expect(t1.id).toBe(0);
expect(t1.name).toBe('t1');
expect(t1.type).toBe(mujoco.mjtTexture.mjTEXTURE_2D.value);
expect(t1.width).toBe(10);
expect(t1.height).toBe(12);
expect(t1.nchannel).toBe(3);
expect(t1.data.length).toBe(t1.width * t1.height * t1.nchannel);
expectArraysClose(t1.data, expectedData);
} finally {
model?.delete();
unlinkXMLFile(tempXmlFilename);
}
});
it('should support named access for Tuple', () => {
const xml = `<mujoco>
<worldbody>
<geom name="g1" type="sphere" size=".1"/>
<geom name="g2" type="box" size=".1 .1 .1"/>
</worldbody>
<custom>
<tuple name="tup1">
<element objtype="geom" objname="g1" prm="0.1"/>
<element objtype="geom" objname="g2" prm="0.2"/>
</tuple>
</custom>
</mujoco>`;
const tempXmlFilename = '/tmp/tup.xml';
writeXMLFile(tempXmlFilename, xml);
const model = mujoco.MjModel.mj_loadXML(tempXmlFilename);
try {
assertExists(model);
const t1 = model.tuple('tup1');
const expectedObjtype = model.tuple_objtype.slice(
model.tuple_adr[t1.id],
model.tuple_adr[t1.id] + model.tuple_size[t1.id]);
expect(t1.id).toBe(0);
expect(t1.name).toBe('tup1');
expect(t1.size).toBe(2);
expectArraysEqual(
expectedObjtype, new Int32Array([
mujoco.mjtObj.mjOBJ_GEOM.value, mujoco.mjtObj.mjOBJ_GEOM.value
]));
expectArraysEqual(t1.objtype, expectedObjtype);
} finally {
model?.delete();
unlinkXMLFile(tempXmlFilename);
}
});
});
describe('MjData named access', () => {
it('should support named access for MjData', () => {
mujoco.mj_forward(model!, data!);
// Actuator
expect(model!.nu).toBe(1);
const actuator = data!.actuator(0);
expect(actuator.name).toBe('myactuator');
expect(actuator.velocity).toBe(0);
expect(actuator.force).toBe(0);
expectArraysClose(
actuator.moment, new Float64Array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
// Body
expect(model!.nbody).toBe(5);
expect(data!.body('mybox').name).toBe('mybox');
expectArraysClose(
data!.body('mybox').xpos, new Float64Array([0, 0, 0.1]));
expect(data!.body('myhinge-body').name).toBe('myhinge-body');
expectArraysClose(
data!.body('myhinge-body').xpos, new Float64Array([0, 0, 1]));
expect(data!.body('myball-body').name).toBe('myball-body');
expectArraysClose(
data!.body('myball-body').xpos, new Float64Array([2, 0, 1]));
expect(data!.body('mocap-body').name).toBe('mocap-body');
expectArraysClose(
data!.body('mocap-body').xpos, new Float64Array([42, 0, 42]));
// Geom
expect(model!.ngeom).toBe(3);
expect(data!.geom('myplane').name).toBe('myplane');
expectArraysClose(
data!.geom('myplane').xpos, new Float64Array([0, 0, 0]));
expectArraysClose(
data!.geom('myplane').xmat,
new Float64Array([1, 0, 0, 0, 1, 0, 0, 0, 1]));
expect(data!.geom('mybox').name).toBe('mybox');
expectArraysClose(
data!.geom('mybox').xpos, new Float64Array([0, 0, 0.1]));
expectArraysClose(
data!.geom('mybox').xmat,
new Float64Array([1, 0, 0, 0, 1, 0, 0, 0, 1]));
expect(data!.geom('mocap-sphere').name).toBe('mocap-sphere');
expectArraysClose(
data!.geom('mocap-sphere').xpos, new Float64Array([42, 0, 42]));
expectArraysClose(
data!.geom('mocap-sphere').xmat,
new Float64Array([1, 0, 0, 0, 1, 0, 0, 0, 1]));
// Joint
expect(model!.njnt).toBe(3);
const freeJnt = data!.jnt('myfree');
expect(freeJnt.name).toBe('myfree');
expectArraysClose(freeJnt.xanchor, new Float64Array([0, 0, 0.1]));
expectArraysClose(freeJnt.xaxis, new Float64Array([0, 0, 1]));
const hingeJnt = data!.jnt('myhinge');
expect(hingeJnt.name).toBe('myhinge');
expectArraysClose(hingeJnt.xanchor, new Float64Array([0, 0, 1]));
expectArraysClose(hingeJnt.xaxis, new Float64Array([0, 1, 0]));
const ballJnt = data!.jnt('myball');
expect(ballJnt.name).toBe('myball');
expectArraysClose(ballJnt.xanchor, new Float64Array([2, 0, 1]));
expectArraysClose(ballJnt.xaxis, new Float64Array([0, 0, 1]));
// Sensor
expect(model!.nsensor).toBe(2);
const jointvelSensor = data!.sensor('myjointvel');
expect(jointvelSensor.name).toBe('myjointvel');
expectArraysClose(jointvelSensor.data, new Float64Array([0]));
const accelSensor = data!.sensor('myaccelerometer');
expect(accelSensor.name).toBe('myaccelerometer');
expectArraysClose(accelSensor.data, new Float64Array([0, 0, 9.81]));
// Site
expect(model!.nsite).toBe(1);
const site = data!.site('mysite');
expect(site.name).toBe('mysite');
expectArraysClose(site.xpos, new Float64Array([0, 0, 0]));
expectArraysClose(
site.xmat, new Float64Array([1, 0, 0, 0, 1, 0, 0, 0, 1]));
});
// Corresponds to bindings_test.py:test_indexer_name_id
it('should support named and indexed access for MjData geoms', () => {
const xml = `
<mujoco>
<worldbody>
<geom name="mygeom" size="1" pos="0 0 1"/>
<geom size="2" pos="0 0 2"/>
<geom size="3" pos="0 0 3"/>
<geom name="myothergeom" size="4" pos="0 0 4"/>
<geom size="5" pos="0 0 5"/>
</worldbody>
</mujoco>
`;
const tempXmlFilename = '/tmp/geom_idx.xml';
writeXMLFile(tempXmlFilename, xml);
const model = mujoco.MjModel.mj_loadXML(tempXmlFilename);
const data = new mujoco.MjData(model);
try {
assertExists(model);
assertExists(data);
mujoco.mj_forward(model, data);
expect(data.geom('mygeom').id).toBe(0);
expect(data.geom('myothergeom').id).toBe(3);
expect(data.geom(0).name).toBe('mygeom');
expect(data.geom(1).name).toBe('');
expect(data.geom(2).name).toBe('');
expect(data.geom(3).name).toBe('myothergeom');
expect(data.geom(4).name).toBe('');
expect(data.geom(0).xpos[2]).toBeCloseTo(1);
expect(data.geom(1).xpos[2]).toBeCloseTo(2);
expect(data.geom(2).xpos[2]).toBeCloseTo(3);
expect(data.geom(3).xpos[2]).toBeCloseTo(4);
expect(data.geom(4).xpos[2]).toBeCloseTo(5);
} finally {
model?.delete();
data?.delete();
unlinkXMLFile(tempXmlFilename);
}
});
// Corresponds to bindings_test.py:test_named_indexing_invalid_names_in_data
it('should throw an error for invalid geom names in data', () => {
expect(() => {
data!.geom('badgeom');
})
.toThrowError(
`MuJoCo Error: Invalid name 'badgeom' for geom. Valid names: ['mocap-sphere', 'mybox', 'myplane']`);
});
// Corresponds to bindings_test.py:test_named_indexing_invalid_index_in_data
it('should throw an error for invalid geom indices in data', () => {
expect(() => {
data!.geom(3);
})
.toThrowError(
'MuJoCo Error: Invalid index 3 for geom. Valid indices from 0 to 2');
expect(() => {
data!.geom(-1);
})
.toThrowError(
'MuJoCo Error: Invalid index -1 for geom. Valid indices from 0 to 2');
});
// Corresponds to bindings_test.py:test_named_indexing_actuator_ctrl
it('should support named access for Actuator ctrl', () => {
const actuatorId = mujoco.mj_name2id(
model!, mujoco.mjtObj.mjOBJ_ACTUATOR.value, 'myactuator');
assertExists(actuatorId);
const actuator = data!.actuator('myactuator');
expect(actuator).toEqual(data!.actuator(actuatorId));
expect(actuator.ctrl).toEqual(data!.actuator(actuatorId)!.ctrl);
// Test that the indexer is returning a view into the underlying
data!.ctrl[actuatorId] = 5;
expect(actuator.ctrl).toBe(5);
actuator.ctrl = 7;
expect(data!.ctrl[actuatorId]).toBe(7);
});
// Corresponds to bindings_test.py:test_named_indexing_ragged_qpos
it('should support named access for Joint qpos', () => {
const balljointId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_JOINT.value, 'myball');
assertExists(balljointId);
const ballJnt = data!.jnt('myball');
expect(ballJnt).toEqual(data!.jnt(balljointId));
expect(ballJnt.qpos).toEqual(data!.jnt(balljointId)!.qpos);
// Test that the indexer is returning a view into the underlying struct.
const qposFromIndexer = ballJnt.qpos;
const qposIdx = model!.jnt_qposadr[balljointId];
data!.qpos.set([4, 5, 6, 7], qposIdx);
expectArraysEqual(qposFromIndexer, new Float64Array([4, 5, 6, 7]));
ballJnt.qpos.set(new Float64Array([9, 8, 7, 6]))
expectArraysEqual(
data!.qpos.slice(qposIdx, qposIdx + 4),
new Float64Array([9, 8, 7, 6]));
});
// Corresponds to bindings_test.py:test_named_indexing_ragged2d_cdof
it('should support named access for Joint cdof', () => {
const freejointId =
mujoco.mj_name2id(model!, mujoco.mjtObj.mjOBJ_JOINT.value, 'myfree');
assertExists(freejointId);
mujoco.mj_forward(model!, data!);
const freeJnt = data!.jnt('myfree');
expect(freeJnt).toEqual(data!.jnt(freejointId));
expect(freeJnt.cdof).toEqual(data!.jnt(freejointId)!.cdof);
expect(freeJnt.cdof.length).toBe(36); // 6x6
// Test that the indexer is returning a view into the underlying
const cdofFromIndexer = freeJnt.cdof;
const dofIdx = model!.jnt_dofadr[freejointId];
const testArray = new Float64Array(36);
for (let i = 0; i < 36; i++) {
testArray[i] = i;
}
data!.cdof.set(testArray);
expectArraysEqual(freeJnt.cdof, testArray);
const expectedCdof = new Float64Array(36).fill(42);
freeJnt.cdof.set(expectedCdof);
expectArraysEqual(data!.cdof.slice(dofIdx, dofIdx + 36), expectedCdof);
});
});
it('should save model to buffer', () => {
assertExists(model);
const modelSize = mujoco.mj_sizeModel(model);
const buffer = new mujoco.Uint8Buffer(Number(modelSize));
try {
mujoco.mj_saveModel(model, null, buffer);
// Test the first 3 byte values of the buffer.
expect(buffer.GetView()[0]).toBe(49);
expect(buffer.GetView()[1]).toBe(212);
expect(buffer.GetView()[2]).toBe(0);
} finally {
buffer.delete();
}
});
it('should save model to file', () => {
assertExists(model);
const filename = '/tmp/test.mjb';
const modelSize = mujoco.mj_sizeModel(model);
const buffer = new mujoco.Uint8Buffer(Number(modelSize));
try {
mujoco.mj_saveModel(model, filename, null);
const fileContent =
(mujoco as any).FS.readFile(filename, {encoding: 'binary'});
mujoco.mj_saveModel(model, null, buffer);
const bufferContent = buffer.GetView();
expect(fileContent.length).toBe(bufferContent.length);
expect(fileContent).toEqual(bufferContent);
} finally {
buffer.delete();
unlinkXMLFile(filename);
}
});
it('should load and save a model with assets to binary', () => {
const xmlContent = `
<mujoco model="test_binary_save">
<asset>
<mesh file="cube.obj"/>
</asset>
<worldbody>
<geom type="mesh" mesh="cube"/>
</worldbody>
</mujoco>`;
const cube1 = `
v -1 -1 1
v 1 -1 1
v -1 1 1
v 1 1 1
v -1 1 -1
v 1 1 -1
v -1 -1 -1
v 1 -1 -1`;
const xmlFilename = '/tmp/binary_test.xml';
const objFilename = '/tmp/cube.obj';
const mjbFilename = '/tmp/binary_test.mjb';
writeXMLFile(xmlFilename, xmlContent);
writeXMLFile(objFilename, cube1);
let model: MjModel|null = null;
let binaryModel: MjModel|null = null;
let vfs: MjVFS|null = null;
try {
model = mujoco.MjModel.mj_loadXML(xmlFilename);
assertExists(model);
mujoco.mj_saveModel(model, mjbFilename, null);
vfs = new mujoco.MjVFS();
vfs.addBuffer(objFilename, new TextEncoder().encode(cube1));
binaryModel = mujoco.MjModel.mj_loadBinary(mjbFilename, vfs);
assertExists(binaryModel);
expect(mujoco.mj_sizeModel(binaryModel))
.toEqual(mujoco.mj_sizeModel(model));
expect(binaryModel.nbody).toEqual(model!.nbody);
expect(binaryModel.nq).toEqual(model!.nq);
expect(binaryModel.nv).toEqual(model!.nv);
expect(binaryModel.njnt).toEqual(model!.njnt);
expect(binaryModel.nmesh).toEqual(model!.nmesh);
} finally {
model?.delete();
binaryModel?.delete();
vfs?.delete();
unlinkXMLFile(xmlFilename);
unlinkXMLFile(objFilename);
unlinkXMLFile(mjbFilename);
}
});
// Corresponds to bindings_test.py:test_mj_saveModel
it('should save and load a model from binary', () => {
const mjbFilename = '/tmp/saved_model.mjb';
let binaryModel: MjModel|null = null;
let vfs: MjVFS|null = null;
try {
mujoco.mj_saveModel(model!, mjbFilename, null);
const bufSize = mujoco.mj_sizeModel(model!);
vfs = new mujoco.MjVFS();
binaryModel = mujoco.MjModel.mj_loadBinary(mjbFilename, vfs);
assertExists(binaryModel);
expect(mujoco.mj_sizeModel(binaryModel)).toEqual(bufSize);
expect(binaryModel.nbody).toEqual(model!.nbody);
expect(binaryModel.nq).toEqual(model!.nq);
expect(binaryModel.nv).toEqual(model!.nv);
expect(binaryModel.njnt).toEqual(model!.njnt);
expect(binaryModel.nmesh).toEqual(model!.nmesh);
} finally {
binaryModel?.delete();
vfs?.delete();
unlinkXMLFile(mjbFilename);
}
});
});