df0bc58578
Introduces `from_xml_path` and `from_binary_path` as more idiomatic aliases for `mj_loadXML` and `mj_loadModel` respectively. Refactors the binding of `mj*STRING` constants. Instead of exposing `get_mj*STRING` functions, these constants are now available as properties on the `Module` object. Runtime getters are added via EM_ASM to lazily call the underlying C++ functions. PiperOrigin-RevId: 882566011 Change-Id: Id3bd5c23704fd122af30a50aed089bd3143fbc96
2607 lines
86 KiB
TypeScript
2607 lines
86 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.mjDISABLESTRING).toEqual([
|
|
'Constraint', 'Equality', 'Frictionloss', 'Limit', 'Contact', 'Spring',
|
|
'Damper', 'Gravity', 'Clampctrl', 'Warmstart', 'Filterparent',
|
|
'Actuation', 'Refsafe', 'Sensor', 'Midphase', 'Eulerdamp', 'AutoReset',
|
|
'NativeCCD', 'Island'
|
|
]);
|
|
expect(mujoco.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.mjFRAMESTRING.length).toEqual(mujoco.mjtFrame.mjNFRAME.value);
|
|
expect(mujoco.mjVISSTRING.length)
|
|
.toEqual(mujoco.mjtVisFlag.mjNVISFLAG.value);
|
|
expect(mujoco.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_loadModel(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_loadModel(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);
|
|
}
|
|
});
|
|
|
|
it('should load XML with assets from VFS', () => {
|
|
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`;
|
|
|
|
const xmlFilename = '/tmp/with_vfs.xml';
|
|
writeXMLFile(xmlFilename, xml);
|
|
|
|
let model: MjModel|null = null;
|
|
let vfs: MjVFS|null = null;
|
|
try {
|
|
vfs = new mujoco.MjVFS();
|
|
vfs.addBuffer('cube.obj', new TextEncoder().encode(cube1));
|
|
assertExists(vfs);
|
|
|
|
model = mujoco.MjModel.mj_loadXML(xmlFilename, 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 {
|
|
model?.delete();
|
|
vfs?.delete();
|
|
unlinkXMLFile(xmlFilename);
|
|
}
|
|
});
|
|
|
|
});
|