| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| const isView = (vec) => ArrayBuffer.isView(vec); |
|
|
| |
| export function at(vec, i) { |
| return isView(vec) ? vec[i] : vec.get(i); |
| } |
|
|
| |
| export function put(vec, i, value) { |
| if (isView(vec)) vec[i] = value; |
| else vec.set(i, value); |
| } |
|
|
| export function readBlock(vec, adr, dim) { |
| const out = new Float64Array(dim); |
| for (let i = 0; i < dim; i++) out[i] = at(vec, adr + i); |
| return out; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function makeRng(seed) { |
| let state = seed >>> 0; |
| return () => { |
| |
| state ^= state << 13; |
| state ^= state >>> 17; |
| state ^= state << 5; |
| state >>>= 0; |
| return state / 4294967296; |
| }; |
| } |
|
|
| export class Sim { |
| |
| |
| |
| |
| |
| constructor(mujoco, scene, label) { |
| this.mujoco = mujoco; |
| this.scene = scene; |
| this.label = label; |
| this.iface = scene.policy_interface; |
|
|
| this.model = mujoco.MjModel.loadFromXML(`/working/${scene.top_xml}`); |
| this.#assertShape(); |
| this.#applyOverrides(); |
| this.data = new mujoco.MjData(this.model); |
|
|
| this.nu = scene.nu; |
| this.lastAction = new Float32Array(this.nu); |
| this.prevAction = new Float32Array(this.nu); |
| this.obs = new Float32Array(48); |
| this.command = new Float32Array([1.0, 0.0, 0.0]); |
|
|
| this.noiseLevel = this.iface.noise.level; |
| this.rng = makeRng(0x9e3779b9); |
|
|
| |
| |
| this.actuatorDelay = 0; |
| this.delayQueue = []; |
| this.obsNoiseSigma = 0; |
|
|
| this.reset(); |
| } |
|
|
| #assertShape() { |
| const m = this.model; |
| const expected = [this.scene.nq, this.scene.nv, this.scene.nu]; |
| const actual = [m.nq, m.nv, m.nu]; |
| for (let i = 0; i < 3; i++) { |
| if (expected[i] !== actual[i]) { |
| throw new Error( |
| `${this.label}: loaded model has nq/nv/nu ${actual.join('/')}, ` + |
| `scene.json says ${expected.join('/')} — wrong scene or wrong assets` |
| ); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| #applyOverrides() { |
| const m = this.model; |
| const s = this.scene; |
| m.opt.timestep = s.timestep; |
| m.opt.ccd_iterations = s.ccd_iterations; |
| for (let i = 0; i < s.dof_damping.length; i++) put(m.dof_damping, i, s.dof_damping[i]); |
| |
| |
| |
| const gainStride = s.actuator_gainprm_stride; |
| const biasStride = s.actuator_biasprm_stride; |
| for (let i = 0; i < s.actuator_gainprm_col0.length; i++) { |
| put(m.actuator_gainprm, i * gainStride + 0, s.actuator_gainprm_col0[i]); |
| put(m.actuator_biasprm, i * biasStride + 1, s.actuator_biasprm_col1[i]); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| dispose() { |
| for (const handle of [this.data, this.model]) { |
| if (handle && typeof handle.delete === 'function' && !handle.isDeleted?.()) { |
| handle.delete(); |
| } |
| } |
| this.data = null; |
| this.model = null; |
| } |
|
|
| |
| setMassScale(scale) { |
| const bodyId = 1; |
| if (this._baseMass === undefined) this._baseMass = at(this.model.body_mass, bodyId); |
| put(this.model.body_mass, bodyId, this._baseMass * scale); |
| } |
|
|
| |
| setFrictionScale(scale) { |
| const n = this.model.ngeom; |
| if (this._baseFriction === undefined) { |
| this._baseFriction = new Float64Array(n); |
| for (let i = 0; i < n; i++) this._baseFriction[i] = at(this.model.geom_friction, i * 3); |
| } |
| for (let i = 0; i < n; i++) put(this.model.geom_friction, i * 3, this._baseFriction[i] * scale); |
| } |
|
|
| setActuatorDelay(steps) { |
| this.actuatorDelay = Math.max(0, Math.round(steps)); |
| this.delayQueue = []; |
| } |
|
|
| setObsNoise(sigma) { |
| this.obsNoiseSigma = sigma; |
| } |
|
|
| |
| push(impulseNs) { |
| const mass = at(this.model.body_mass, 1); |
| const dv = impulseNs / Math.max(mass, 1e-9); |
| |
| let v = [this.rng() * 2 - 1, this.rng() * 2 - 1, this.rng() * 2 - 1]; |
| const norm = Math.hypot(v[0], v[1], v[2]) || 1; |
| for (let i = 0; i < 3; i++) { |
| put(this.data.qvel, i, at(this.data.qvel, i) + (v[i] / norm) * dv); |
| } |
| } |
|
|
| reset() { |
| this.mujoco.mj_resetData(this.model, this.data); |
| |
| if (this.model.nkey > 0) { |
| for (let i = 0; i < this.scene.nq; i++) put(this.data.qpos, i, at(this.model.key_qpos, i)); |
| } |
| this.lastAction.fill(0); |
| this.prevAction.fill(0); |
| this.delayQueue = []; |
| this.totalReturn = 0; |
| this.steps = 0; |
| this.jitterSum = 0; |
| this.jitterCount = 0; |
| this.fallen = false; |
| this.mujoco.mj_forward(this.model, this.data); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| gravity() { |
| const base = this.iface.imu_site_id * 9 + 6; |
| return [ |
| -at(this.data.site_xmat, base + 0), |
| -at(this.data.site_xmat, base + 1), |
| -at(this.data.site_xmat, base + 2), |
| ]; |
| } |
|
|
| #noise(scaleName) { |
| const scale = this.iface.noise.scales[scaleName] ?? 0; |
| return (2 * this.rng() - 1) * this.noiseLevel * scale; |
| } |
|
|
| |
| observe() { |
| const d = this.data; |
| const s = this.iface.sensors; |
| const o = this.obs; |
| let k = 0; |
|
|
| const linvel = readBlock(d.sensordata, s.local_linvel.adr, 3); |
| for (let i = 0; i < 3; i++) o[k++] = linvel[i] + this.#noise('linvel'); |
|
|
| const gyro = readBlock(d.sensordata, s.gyro.adr, 3); |
| for (let i = 0; i < 3; i++) o[k++] = gyro[i] + this.#noise('gyro'); |
|
|
| const g = this.gravity(); |
| for (let i = 0; i < 3; i++) o[k++] = g[i] + this.#noise('gravity'); |
|
|
| const q0 = this.iface.qpos_joint_start; |
| for (let i = 0; i < 12; i++) { |
| o[k++] = at(d.qpos, q0 + i) - this.iface.default_pose[i] + this.#noise('joint_pos'); |
| } |
|
|
| const v0 = this.iface.qvel_joint_start; |
| for (let i = 0; i < 12; i++) o[k++] = at(d.qvel, v0 + i) + this.#noise('joint_vel'); |
|
|
| for (let i = 0; i < 12; i++) o[k++] = this.lastAction[i]; |
| for (let i = 0; i < 3; i++) o[k++] = this.command[i]; |
|
|
| |
| |
| if (this.obsNoiseSigma > 0) { |
| for (let i = 0; i < o.length; i++) o[i] += this.obsNoiseSigma * gaussian(this.rng); |
| } |
| return o; |
| } |
|
|
| |
| |
| |
| |
| step(action) { |
| let applied = action; |
| if (this.actuatorDelay > 0) { |
| this.delayQueue.push(Float32Array.from(action)); |
| applied = |
| this.delayQueue.length > this.actuatorDelay |
| ? this.delayQueue.shift() |
| : new Float32Array(this.nu); |
| } |
|
|
| if (this.steps > 0) { |
| let delta = 0; |
| for (let i = 0; i < this.nu; i++) delta += (applied[i] - this.prevAction[i]) ** 2; |
| this.jitterSum += Math.sqrt(delta); |
| this.jitterCount += 1; |
| } |
| this.prevAction = Float32Array.from(applied); |
| this.lastAction = Float32Array.from(applied); |
|
|
| const pose = this.iface.default_pose; |
| const scale = this.iface.action_scale; |
| for (let i = 0; i < this.nu; i++) put(this.data.ctrl, i, pose[i] + applied[i] * scale); |
|
|
| const t0 = performance.now(); |
| for (let i = 0; i < this.scene.n_substeps; i++) { |
| this.mujoco.mj_step(this.model, this.data); |
| } |
| const physicsMs = performance.now() - t0; |
|
|
| this.steps += 1; |
| |
| |
| if (this.gravity()[2] > 0.0) this.fallen = true; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const linvel = readBlock(this.data.sensordata, this.iface.sensors.local_linvel.adr, 3); |
| const gyro = readBlock(this.data.sensordata, this.iface.sensors.gyro.adr, 3); |
| this.trackingError = Math.hypot( |
| linvel[0] - this.command[0], |
| linvel[1] - this.command[1], |
| gyro[2] - this.command[2] |
| ); |
| this.trackingSum = (this.trackingSum ?? 0) + this.trackingError; |
|
|
| return physicsMs; |
| } |
|
|
| get meanTrackingError() { |
| return this.steps > 0 ? (this.trackingSum ?? 0) / this.steps : 0; |
| } |
|
|
| get meanJitter() { |
| return this.jitterCount > 0 ? this.jitterSum / this.jitterCount : 0; |
| } |
| } |
|
|
| |
| export function gaussian(rng) { |
| let u = 0; |
| while (u === 0) u = rng(); |
| return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * rng()); |
| } |
|
|