mujoco-react 8.4.2 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -100,6 +100,58 @@ function ResetButton() {
100
100
  }
101
101
  ```
102
102
 
103
+ ## Map Controls to Joints
104
+
105
+ Use control groups when a robot's actuator order does not match a simple `qpos[0..n]` layout:
106
+
107
+ ```tsx
108
+ import { useRef } from "react";
109
+ import { resolveControlGroup, useBeforePhysicsStep } from "mujoco-react";
110
+ import type { ControlGroupInfo } from "mujoco-react";
111
+
112
+ function HoldTcpPose() {
113
+ const armRef = useRef<ControlGroupInfo | null>(null);
114
+
115
+ useBeforePhysicsStep((model, data) => {
116
+ armRef.current ??= resolveControlGroup(model, { siteName: "tcp" });
117
+ if (!armRef.current) return;
118
+
119
+ armRef.current.writeCtrl(data, armRef.current.readQpos(data));
120
+ });
121
+
122
+ return null;
123
+ }
124
+ ```
125
+
126
+ `resolveControlGroup()` accepts `{ siteName }`, `{ bodyName }`, `{ joints }`, or `{ actuators }`. Selectors can be a name, ordered name array, regex, or predicate.
127
+
128
+ ## Build Observations
129
+
130
+ Build policy-ready observation vectors from common MuJoCo state without hard-coding offsets:
131
+
132
+ ```tsx
133
+ import { buildObservation, useBeforePhysicsStep } from "mujoco-react";
134
+
135
+ function PolicyDriver() {
136
+ useBeforePhysicsStep((model, data) => {
137
+ const obs = buildObservation(model, data, {
138
+ qpos: true,
139
+ qvel: true,
140
+ ctrl: true,
141
+ sensors: ["imu_gyro", "imu_accel"],
142
+ sites: ["tcp"],
143
+ projectedGravity: "torso",
144
+ });
145
+
146
+ runPolicy(obs.values, obs.layout);
147
+ });
148
+
149
+ return null;
150
+ }
151
+ ```
152
+
153
+ Use `output: "float64"` when a downstream model expects double precision. Named resources are skipped when absent, so `obs.layout` is the source of truth for the current model.
154
+
103
155
  ## WebSocket Control
104
156
 
105
157
  Stream actuator commands over a WebSocket and send simulation state back. Use a schema validator such as Zod at this boundary because socket messages are untrusted app input (`npm install zod` for this example):
@@ -655,13 +707,19 @@ useGamepad({
655
707
  Framework-agnostic decimation loop for RL policies:
656
708
 
657
709
  ```tsx
658
- const { step, isRunning } = usePolicy({
710
+ const obs = useObservation({ qpos: true, qvel: true, projectedGravity: "torso" });
711
+
712
+ const policy = usePolicy({
659
713
  frequency: 50,
660
- onObservation: (model, data) => buildObs(model, data),
714
+ onObservation: () => obs.readValues(),
661
715
  onAction: (action, model, data) => applyAction(action, data),
662
716
  });
663
717
  ```
664
718
 
719
+ ### `buildObservation(model, data, config)` / `useObservation(config)`
720
+
721
+ Build a flat `Float32Array` or `Float64Array` plus a layout map from qpos, qvel, ctrl, actuator activations, sensordata, named sensors, named site positions, and projected gravity.
722
+
665
723
  ### `useTrajectoryRecorder(config)` / `useTrajectoryPlayer(trajectory, config)`
666
724
 
667
725
  Record and play back simulation trajectories:
@@ -766,7 +824,11 @@ The full API object available via `ref` or `useMujoco()` (when `isReady`):
766
824
  | `setQpos(values)` / `getQpos()` | Direct qpos access |
767
825
  | `setQvel(values)` / `getQvel()` | Direct qvel access |
768
826
  | `setCtrl(nameOrValues, value?)` | Set control by name or batch |
769
- | `getCtrl(name?)` | Get control values |
827
+ | `getCtrl()` | Get all control values |
828
+ | `getControlMap()` | Map directly actuated scalar joints to qpos/dof/ctrl addresses |
829
+ | `getActuatedJoints()` | List scalar hinge/slide joints with matching actuators |
830
+ | `resolveControlGroup(selector)` | Resolve qpos/ctrl mapping from a site, body, joint selector, or actuator selector |
831
+ | `buildObservation(model, data, config)` | Build policy-ready observation vectors with layout metadata |
770
832
  | `applyKeyframe(nameOrIndex)` | Apply a keyframe |
771
833
  | `getKeyframeNames()` / `getKeyframeCount()` | Keyframe introspection |
772
834
 
@@ -857,7 +919,6 @@ Features planned but not yet implemented:
857
919
  | **User-uploaded model loading** | P2 | `loadFromFiles(FileList)` -- detect meshdir, write to VFS |
858
920
  | **URDF loading** | P2 | Load URDF models via MuJoCo's built-in URDF compiler |
859
921
  | **XML mutation / recompile** | P1 | `addBody()`, `removeBody()`, `recompile()` for runtime XML editing |
860
- | **Observation builder utilities** | P2 | Helpers for projected gravity, joint positions/velocities for RL |
861
922
  | **Physics interpolation** | P1 | Smooth rendering between physics ticks for very high refresh displays |
862
923
  | **Instanced geom rendering** | P2 | `<InstancedGeomRenderer />` for particle/granular sims |
863
924
  | **Web Worker physics** | P2 | Run `mj_step` off main thread via SharedArrayBuffer |
package/dist/index.d.ts CHANGED
@@ -477,6 +477,44 @@ interface PolicyConfig {
477
477
  onObservation: (model: MujocoModel, data: MujocoData) => Float32Array | Float64Array | number[];
478
478
  onAction: (action: Float32Array | Float64Array | number[], model: MujocoModel, data: MujocoData) => void;
479
479
  }
480
+ type ObservationOutput = 'float32' | 'float64';
481
+ interface ObservationConfig {
482
+ /** Include scalar simulation time. */
483
+ time?: boolean;
484
+ /** Include all qpos values. */
485
+ qpos?: boolean;
486
+ /** Include all qvel values. */
487
+ qvel?: boolean;
488
+ /** Include all ctrl values. */
489
+ ctrl?: boolean;
490
+ /** Include all actuator activation values. */
491
+ act?: boolean;
492
+ /** Include all raw sensordata values. */
493
+ sensordata?: boolean;
494
+ /** Include named sensor values in the configured order. */
495
+ sensors?: readonly Sensors[];
496
+ /** Include named site world positions in the configured order. */
497
+ sites?: readonly Sites[];
498
+ /** Include world gravity projected into each named body's local frame. */
499
+ projectedGravity?: Bodies | readonly Bodies[];
500
+ /** Output array type. Defaults to Float32Array. */
501
+ output?: ObservationOutput;
502
+ }
503
+ interface ObservationLayoutItem {
504
+ name: string;
505
+ start: number;
506
+ size: number;
507
+ }
508
+ interface ObservationResult {
509
+ values: Float32Array | Float64Array;
510
+ layout: ObservationLayoutItem[];
511
+ }
512
+ interface ObservationHandle {
513
+ /** Read a fresh observation from the current live MuJoCo model/data refs. */
514
+ read(): ObservationResult;
515
+ /** Read just the vector values for policy inference. */
516
+ readValues(): Float32Array | Float64Array;
517
+ }
480
518
  interface DebugProps {
481
519
  showGeoms?: boolean;
482
520
  showSites?: boolean;
@@ -831,6 +869,19 @@ interface LoadResult {
831
869
  */
832
870
  declare function loadScene(mujoco: MujocoModule, config: SceneConfig, onProgress?: (msg: string) => void): Promise<LoadResult>;
833
871
 
872
+ /**
873
+ * @license
874
+ * SPDX-License-Identifier: Apache-2.0
875
+ */
876
+
877
+ /**
878
+ * Build a flat observation vector plus a layout map from live MuJoCo state.
879
+ *
880
+ * Missing named resources are skipped. The returned layout is the source of
881
+ * truth for the vector produced from the current model.
882
+ */
883
+ declare function buildObservation(model: MujocoModel, data: MujocoData, config: ObservationConfig): ObservationResult;
884
+
834
885
  /**
835
886
  * @license
836
887
  * SPDX-License-Identifier: Apache-2.0
@@ -1182,6 +1233,19 @@ declare function usePolicy(config: PolicyConfig): {
1182
1233
  readonly lastObservation: Float64Array<ArrayBufferLike> | Float32Array<ArrayBufferLike> | number[] | null;
1183
1234
  };
1184
1235
 
1236
+ /**
1237
+ * @license
1238
+ * SPDX-License-Identifier: Apache-2.0
1239
+ */
1240
+
1241
+ /**
1242
+ * Live observation reader for policy loops and telemetry.
1243
+ *
1244
+ * The handle is stable; call `read()` inside callbacks to sample the latest
1245
+ * MuJoCo model/data state without forcing React renders.
1246
+ */
1247
+ declare function useObservation(config: ObservationConfig): ObservationHandle;
1248
+
1185
1249
  /**
1186
1250
  * @license
1187
1251
  * SPDX-License-Identifier: Apache-2.0
@@ -1386,4 +1450,4 @@ interface CameraAnimationAPI {
1386
1450
  */
1387
1451
  declare function useCameraAnimation(): CameraAnimationAPI;
1388
1452
 
1389
- export { type ActuatedJointInfo, type ActuatorInfo, type Actuators, type Bodies, Body, type BodyInfo, type BodyProps, type BodyStateResult, type CameraAnimationAPI, type ContactInfo, ContactListener, type ContactListenerProps, ContactMarkers, type ControlGroupInfo, type ControlGroupSelector, type ControlJointInfo, type ControllerComponent, type ControllerOptions, type CtrlHandle, Debug, type DebugProps, DragInteraction, type DragInteractionProps, FlexRenderer, type GeomInfo, type Geoms, type IKSolveFn, type IkConfig, type IkContextValue, IkGizmo, type IkGizmoProps, type JointInfo, type JointStateResult, type Joints, type KeyBinding, type KeyboardTeleopConfig, type Keyframes, type ModelOptions, MujocoCanvas, type MujocoCanvasProps, type MujocoContact, type MujocoContactArray, type MujocoContextValue, type MujocoData, type MujocoLoader, type MujocoLoaderOptions, type MujocoModel, type MujocoModule, MujocoPhysics, type MujocoPhysicsProps, MujocoProvider, type MujocoProviderProps, type MujocoSimAPI, MujocoSimProvider, type MujocoWasmVariant, type PhysicsConfig, type PhysicsStepCallback, type PlaybackState, type PolicyConfig, type RayHit, type Register, type ResourceSelector, type SceneConfig, SceneLights, type SceneLightsProps, type SceneMarker, type SceneObject, type SensorHandle, type SensorInfo, type SensorResult, type Sensors, type SiteInfo, type SitePositionResult, type Sites, type StateSnapshot, TendonRenderer, type TrajectoryData, type TrajectoryFrame, type TrajectoryInput, TrajectoryPlayer, type TrajectoryPlayerProps, type XmlPatch, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
1453
+ export { type ActuatedJointInfo, type ActuatorInfo, type Actuators, type Bodies, Body, type BodyInfo, type BodyProps, type BodyStateResult, type CameraAnimationAPI, type ContactInfo, ContactListener, type ContactListenerProps, ContactMarkers, type ControlGroupInfo, type ControlGroupSelector, type ControlJointInfo, type ControllerComponent, type ControllerOptions, type CtrlHandle, Debug, type DebugProps, DragInteraction, type DragInteractionProps, FlexRenderer, type GeomInfo, type Geoms, type IKSolveFn, type IkConfig, type IkContextValue, IkGizmo, type IkGizmoProps, type JointInfo, type JointStateResult, type Joints, type KeyBinding, type KeyboardTeleopConfig, type Keyframes, type ModelOptions, MujocoCanvas, type MujocoCanvasProps, type MujocoContact, type MujocoContactArray, type MujocoContextValue, type MujocoData, type MujocoLoader, type MujocoLoaderOptions, type MujocoModel, type MujocoModule, MujocoPhysics, type MujocoPhysicsProps, MujocoProvider, type MujocoProviderProps, type MujocoSimAPI, MujocoSimProvider, type MujocoWasmVariant, type ObservationConfig, type ObservationHandle, type ObservationLayoutItem, type ObservationOutput, type ObservationResult, type PhysicsConfig, type PhysicsStepCallback, type PlaybackState, type PolicyConfig, type RayHit, type Register, type ResourceSelector, type SceneConfig, SceneLights, type SceneLightsProps, type SceneMarker, type SceneObject, type SensorHandle, type SensorInfo, type SensorResult, type Sensors, type SiteInfo, type SitePositionResult, type Sites, type StateSnapshot, TendonRenderer, type TrajectoryData, type TrajectoryFrame, type TrajectoryInput, TrajectoryPlayer, type TrajectoryPlayerProps, type XmlPatch, buildObservation, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, useObservation, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
package/dist/index.js CHANGED
@@ -1953,6 +1953,81 @@ var MujocoPhysics = forwardRef(
1953
1953
  );
1954
1954
  }
1955
1955
  );
1956
+
1957
+ // src/core/ObservationBuilder.ts
1958
+ function append(values, layout, name, chunk) {
1959
+ const start = values.length;
1960
+ for (let i = 0; i < chunk.length; i++) values.push(chunk[i] ?? 0);
1961
+ layout.push({ name, start, size: chunk.length });
1962
+ }
1963
+ function appendScalar(values, layout, name, value) {
1964
+ const start = values.length;
1965
+ values.push(value);
1966
+ layout.push({ name, start, size: 1 });
1967
+ }
1968
+ function namedBodyList(names) {
1969
+ if (!names) return [];
1970
+ return typeof names === "string" ? [names] : names;
1971
+ }
1972
+ function normalizedGravity(model) {
1973
+ const gx = model.opt.gravity[0] ?? 0;
1974
+ const gy = model.opt.gravity[1] ?? 0;
1975
+ const gz = model.opt.gravity[2] ?? -9.81;
1976
+ const length = Math.hypot(gx, gy, gz);
1977
+ if (length === 0) return [0, 0, 0];
1978
+ return [gx / length, gy / length, gz / length];
1979
+ }
1980
+ function rotateWorldVectorToBody(data, bodyId, world) {
1981
+ const adr = bodyId * 4;
1982
+ const w = data.xquat[adr] ?? 1;
1983
+ const x = -(data.xquat[adr + 1] ?? 0);
1984
+ const y = -(data.xquat[adr + 2] ?? 0);
1985
+ const z = -(data.xquat[adr + 3] ?? 0);
1986
+ const vx = world[0];
1987
+ const vy = world[1];
1988
+ const vz = world[2];
1989
+ const tx = 2 * (y * vz - z * vy);
1990
+ const ty = 2 * (z * vx - x * vz);
1991
+ const tz = 2 * (x * vy - y * vx);
1992
+ return [
1993
+ vx + w * tx + y * tz - z * ty,
1994
+ vy + w * ty + z * tx - x * tz,
1995
+ vz + w * tz + x * ty - y * tx
1996
+ ];
1997
+ }
1998
+ function buildObservation(model, data, config) {
1999
+ const values = [];
2000
+ const layout = [];
2001
+ if (config.time) appendScalar(values, layout, "time", data.time);
2002
+ if (config.qpos) append(values, layout, "qpos", data.qpos);
2003
+ if (config.qvel) append(values, layout, "qvel", data.qvel);
2004
+ if (config.ctrl) append(values, layout, "ctrl", data.ctrl);
2005
+ if (config.act) append(values, layout, "act", data.act);
2006
+ if (config.sensordata) append(values, layout, "sensordata", data.sensordata);
2007
+ for (const name of config.sensors ?? []) {
2008
+ const sensorId = findSensorByName(model, name);
2009
+ if (sensorId < 0) continue;
2010
+ const start = model.sensor_adr[sensorId] ?? 0;
2011
+ const dim = model.sensor_dim[sensorId] ?? 0;
2012
+ append(values, layout, `sensor:${name}`, data.sensordata.subarray(start, start + dim));
2013
+ }
2014
+ for (const name of config.sites ?? []) {
2015
+ const siteId = findSiteByName(model, name);
2016
+ if (siteId < 0) continue;
2017
+ const start = siteId * 3;
2018
+ append(values, layout, `site:${name}:xpos`, data.site_xpos.subarray(start, start + 3));
2019
+ }
2020
+ const gravity = normalizedGravity(model);
2021
+ for (const name of namedBodyList(config.projectedGravity)) {
2022
+ const bodyId = findBodyByName(model, name);
2023
+ if (bodyId < 0) continue;
2024
+ append(values, layout, `projectedGravity:${name}`, rotateWorldVectorToBody(data, bodyId, gravity));
2025
+ }
2026
+ return {
2027
+ values: config.output === "float64" ? new Float64Array(values) : new Float32Array(values),
2028
+ layout
2029
+ };
2030
+ }
1956
2031
  function shallowEqual(a, b) {
1957
2032
  const keysA = Object.keys(a);
1958
2033
  const keysB = Object.keys(b);
@@ -4086,6 +4161,26 @@ function usePolicy(config) {
4086
4161
  }
4087
4162
  };
4088
4163
  }
4164
+ var EMPTY_OBSERVATION = {
4165
+ values: new Float32Array(0),
4166
+ layout: []
4167
+ };
4168
+ function useObservation(config) {
4169
+ const { mjModelRef, mjDataRef } = useMujocoContext();
4170
+ const configRef = useRef(config);
4171
+ configRef.current = config;
4172
+ return useMemo(() => ({
4173
+ read() {
4174
+ const model = mjModelRef.current;
4175
+ const data = mjDataRef.current;
4176
+ if (!model || !data) return EMPTY_OBSERVATION;
4177
+ return buildObservation(model, data, configRef.current);
4178
+ },
4179
+ readValues() {
4180
+ return this.read().values;
4181
+ }
4182
+ }), [mjDataRef, mjModelRef]);
4183
+ }
4089
4184
  function useTrajectoryRecorder(options = {}) {
4090
4185
  const { mjModelRef } = useMujocoContext();
4091
4186
  const recordingRef = useRef(false);
@@ -4573,6 +4668,6 @@ function useCameraAnimation() {
4573
4668
  * useCameraAnimation — composable camera animation hook.
4574
4669
  */
4575
4670
 
4576
- export { Body, ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, MujocoCanvas, MujocoPhysics, MujocoProvider, MujocoSimProvider, SceneLights, TendonRenderer, TrajectoryPlayer, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
4671
+ export { Body, ContactListener, ContactMarkers, Debug, DragInteraction, FlexRenderer, IkGizmo, MujocoCanvas, MujocoPhysics, MujocoProvider, MujocoSimProvider, SceneLights, TendonRenderer, TrajectoryPlayer, buildObservation, createContiguousControlGroup, createController, createControllerHook, findActuatorByName, findBodyByName, findGeomByName, findJointByName, findKeyframeByName, findSensorByName, findSiteByName, findTendonByName, getActuatedJoints, getContact, getControlMap, getName, loadScene, resolveControlGroup, useActuators, useAfterPhysicsStep, useBeforePhysicsStep, useBodyMeshes, useBodyState, useCameraAnimation, useContactEvents, useContacts, useCtrl, useCtrlNoise, useGamepad, useGravityCompensation, useIkController, useJointState, useKeyboardTeleop, useMujoco, useMujocoWasm, useObservation, usePolicy, useSceneLights, useSelectionHighlight, useSensor, useSensors, useSitePosition, useTrajectoryPlayer, useTrajectoryRecorder, useVideoRecorder };
4577
4672
  //# sourceMappingURL=index.js.map
4578
4673
  //# sourceMappingURL=index.js.map