Add object-based accessors for mjData in WASM bindings.

PiperOrigin-RevId: 862736608
Change-Id: I0618e9d2af37e377bad1aead1b3fd1a12334df35
This commit is contained in:
Google DeepMind
2026-01-29 07:54:50 -08:00
committed by Copybara-Service
parent 59c5017073
commit d2efc0e154
5 changed files with 727 additions and 125 deletions
+224 -6
View File
@@ -45,17 +45,17 @@ const TEST_XML = `
<geom name="mybox" type="box" size="0.1 0.1 0.1" mass="0.25"/>
<freejoint name="myfree"/>
</body>
<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>
<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 mocap="true" pos="42 0 42">
<geom type="sphere" size="0.1"/>
<body name="mocap-body" mocap="true" pos="42 0 42">
<geom name="mocap-sphere" type="sphere" size="0.1"/>
</body>
</worldbody>
<actuator>
@@ -900,7 +900,7 @@ describe('MuJoCo WASM Bindings', () => {
// 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, 0, 0, 0, 0, 42, 0, 42]);
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);
});
@@ -1881,7 +1881,7 @@ describe('MuJoCo WASM Bindings', () => {
model!.geom('badgeom');
})
.toThrowError(
`MuJoCo Error: Invalid name 'badgeom' for geom. Valid names: \['mybox', 'myplane'\]`);
`MuJoCo Error: Invalid name 'badgeom' for geom. Valid names: ['mocap-sphere', 'mybox', 'myplane']`);
});
// Corresponds to
@@ -2160,6 +2160,223 @@ describe('MuJoCo WASM Bindings', () => {
});
});
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);
@@ -2279,4 +2496,5 @@ describe('MuJoCo WASM Bindings', () => {
unlinkXMLFile(mjbFilename);
}
});
});