mjswan 0.8.1 → 0.8.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mjswan",
3
3
  "type": "module",
4
- "version": "0.8.1",
4
+ "version": "0.8.2",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/ttktjmt/mjswan.git"
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Direct unit tests for the DSL primitives added for composition-graph
3
+ * observations (ADR 0003): arithmetic (Div/Sqrt/Sum), Slice, and the
4
+ * motion-reference sources (TrackingRefField/TrackingIsReady). Each primitive
5
+ * is exercised on its own, not through any task's observation graph.
6
+ */
7
+ import { describe, expect, it } from 'vitest';
8
+
9
+ import type { EvalContext } from '../interpreter';
10
+ import { Primitives } from '../primitives';
11
+
12
+ function expectClose(a: Float32Array, b: ArrayLike<number>, tol = 1e-6): void {
13
+ expect(a.length).toBe(b.length);
14
+ for (let i = 0; i < a.length; i++) expect(Math.abs(a[i] - b[i])).toBeLessThan(tol);
15
+ }
16
+
17
+ // A minimal motion command exposing the reference-trajectory buffers the
18
+ // sources read (the generic MotionRefSource shape — not tied to any task).
19
+ interface RefTerm {
20
+ isReady(): boolean;
21
+ refRootPos: Float32Array[];
22
+ refRootQuat: Float32Array[];
23
+ refJointPos: Float32Array[];
24
+ refIdx: number;
25
+ refLen: number;
26
+ }
27
+
28
+ function ctx(term: RefTerm | null, nJoints = 3): EvalContext {
29
+ const runner = {
30
+ getContext: () => ({
31
+ commandManager: { getTerm: (n: string) => (n === 'motion' ? term : null) },
32
+ }),
33
+ getNumActions: () => nJoints,
34
+ getLastActions: () => new Float32Array(nJoints),
35
+ };
36
+ return { runner, state: {}, params: {} } as unknown as EvalContext;
37
+ }
38
+
39
+ function refTerm(refIdx: number, refLen: number, ready = true): RefTerm {
40
+ const ramp = (base: number, w: number) =>
41
+ new Float32Array(Array.from({ length: w }, (_v, i) => base + i));
42
+ return {
43
+ isReady: () => ready,
44
+ refIdx,
45
+ refLen,
46
+ refRootPos: Array.from({ length: refLen }, (_v, t) => ramp(t * 10, 3)),
47
+ refRootQuat: Array.from({ length: refLen }, () => new Float32Array([1, 0, 0, 0])),
48
+ refJointPos: Array.from({ length: refLen }, (_v, t) => ramp(t * 100, 3)),
49
+ };
50
+ }
51
+
52
+ describe('DSL arithmetic primitives', () => {
53
+ it('Div broadcasts a scalar divisor over a vector', () => {
54
+ expectClose(Primitives.Div([new Float32Array([3, 0, 4]), 2], {}, ctx(null), {}) as Float32Array, [1.5, 0, 2]);
55
+ });
56
+ it('Sqrt is elementwise', () => {
57
+ expectClose(Primitives.Sqrt([new Float32Array([9, 16])], {}, ctx(null), {}) as Float32Array, [3, 4]);
58
+ });
59
+ it('Sum reduces a vector to its scalar sum', () => {
60
+ expect(Primitives.Sum([new Float32Array([3, 0, 4])], {}, ctx(null), {})).toBe(7);
61
+ });
62
+ it('Div/Sqrt/Sum compose to L2-normalize (3,0,4 → 0.6,0,0.8)', () => {
63
+ const v = new Float32Array([3, 0, 4]);
64
+ const norm = Primitives.Sqrt([Primitives.Sum([Primitives.Mul([v, v], {}, ctx(null), {})], {}, ctx(null), {})], {}, ctx(null), {});
65
+ expectClose(Primitives.Div([v, norm], {}, ctx(null), {}) as Float32Array, [0.6, 0, 0.8]);
66
+ });
67
+ });
68
+
69
+ describe('Slice primitive', () => {
70
+ it('extracts the contiguous sub-range [start, start+len)', () => {
71
+ const v = new Float32Array([10, 11, 12, 13, 14, 15]);
72
+ expectClose(Primitives.Slice([v], { start: 2, len: 3 }, ctx(null), {}) as Float32Array, [12, 13, 14]);
73
+ });
74
+ it('zero-pads past the end', () => {
75
+ expectClose(Primitives.Slice([new Float32Array([1, 2])], { start: 1, len: 3 }, ctx(null), {}) as Float32Array, [2, 0, 0]);
76
+ });
77
+ });
78
+
79
+ describe('motion-reference primitives', () => {
80
+ it('TrackingRefField reads the field at refIdx+step, clamped to the clip', () => {
81
+ const t = refTerm(2, 5);
82
+ expectClose(Primitives.TrackingRefField([], { field: 'root_pos', step: 1 }, ctx(t), {}) as Float32Array, t.refRootPos[3]);
83
+ expectClose(Primitives.TrackingRefField([], { field: 'joint_pos', step: -5 }, ctx(t), {}) as Float32Array, t.refJointPos[0]);
84
+ expectClose(Primitives.TrackingRefField([], { field: 'root_pos', step: 99 }, ctx(t), {}) as Float32Array, t.refRootPos[4]);
85
+ });
86
+ it('TrackingRefField falls back to a finite value when not ready / absent', () => {
87
+ const nr = ctx(refTerm(0, 4, false));
88
+ expect(Array.from(Primitives.TrackingRefField([], { field: 'root_quat', step: 0 }, nr, {}) as Float32Array)).toEqual([1, 0, 0, 0]);
89
+ expect((Primitives.TrackingRefField([], { field: 'root_pos', step: 0 }, nr, {}) as Float32Array).length).toBe(3);
90
+ // joint width falls back to the policy action count when no clip is loaded.
91
+ expect((Primitives.TrackingRefField([], { field: 'joint_pos', step: 0 }, ctx(null, 5), {}) as Float32Array).length).toBe(5);
92
+ });
93
+ it('TrackingIsReady reflects readiness / presence', () => {
94
+ expect(Primitives.TrackingIsReady([], {}, ctx(refTerm(0, 4, true)), {})).toBe(1);
95
+ expect(Primitives.TrackingIsReady([], {}, ctx(refTerm(0, 4, false)), {})).toBe(0);
96
+ expect(Primitives.TrackingIsReady([], {}, ctx(null), {})).toBe(0);
97
+ });
98
+ });
@@ -71,6 +71,41 @@ function trackingTerm(ctx: EvalContext): TrackingSource | null {
71
71
  return isTrackingSource(term) && term.isReady() ? (term as TrackingSource) : null;
72
72
  }
73
73
 
74
+ /** A motion command exposing reference-trajectory buffers — distinct from the
75
+ * anchor-based {@link TrackingSource}; built-in and bespoke players declare them. */
76
+ type MotionRefSource = {
77
+ isReady(): boolean;
78
+ refRootPos: Float32Array[];
79
+ refRootQuat: Float32Array[];
80
+ refJointPos: Float32Array[];
81
+ refIdx: number;
82
+ refLen: number;
83
+ };
84
+
85
+ /** The active motion command carrying reference buffers, or null. */
86
+ function motionRefTerm(ctx: EvalContext): MotionRefSource | null {
87
+ const term = ctx.runner.getContext()?.commandManager?.getTerm('motion');
88
+ if (
89
+ typeof term === 'object' &&
90
+ term !== null &&
91
+ 'refRootPos' in term &&
92
+ 'refRootQuat' in term &&
93
+ 'refJointPos' in term &&
94
+ typeof (term as { isReady?: unknown }).isReady === 'function'
95
+ ) {
96
+ return term as unknown as MotionRefSource;
97
+ }
98
+ return null;
99
+ }
100
+
101
+ /** Add `step` to `base`, clamped to `[0, len-1]` (matches clampFutureIndices). */
102
+ function clampRefIdx(base: number, step: number, len: number): number {
103
+ const i = base + step;
104
+ if (i < 0) return 0;
105
+ if (i >= len) return Math.max(0, len - 1);
106
+ return i;
107
+ }
108
+
74
109
  // --- Model name-table decoders + address resolution --------------------------
75
110
 
76
111
  function decodeNames(
@@ -197,10 +232,20 @@ export const Primitives: Record<string, Primitive> = {
197
232
  Add: (inputs) => elementwise(inputs[0], inputs[1], (a, b) => a + b),
198
233
  Sub: (inputs) => elementwise(inputs[0], inputs[1], (a, b) => a - b),
199
234
  Mul: (inputs) => elementwise(inputs[0], inputs[1], (a, b) => a * b),
235
+ Div: (inputs) => elementwise(inputs[0], inputs[1], (a, b) => a / b),
200
236
  Neg: (inputs) => unary(inputs[0], (a) => -a),
201
237
  Abs: (inputs) => unary(inputs[0], Math.abs),
238
+ Sqrt: (inputs) => unary(inputs[0], Math.sqrt),
202
239
  Acos: (inputs) => unary(inputs[0], (a) => Math.acos(Math.max(-1, Math.min(1, a)))),
203
240
 
241
+ // Reduce a vector to the scalar sum of its elements.
242
+ Sum: (inputs) => {
243
+ const v = asVec(inputs[0]);
244
+ let s = 0;
245
+ for (let i = 0; i < v.length; i++) s += v[i];
246
+ return s;
247
+ },
248
+
204
249
  // -- Indexing ------------------------------------------------------------
205
250
  Index: (inputs, attrs) => {
206
251
  const vec = asVec(inputs[0]);
@@ -208,6 +253,16 @@ export const Primitives: Record<string, Primitive> = {
208
253
  return vec[idx] ?? 0;
209
254
  },
210
255
 
256
+ // Extract the contiguous sub-range [start, start+len) of a vector.
257
+ Slice: (inputs, attrs) => {
258
+ const vec = asVec(inputs[0]);
259
+ const start = Number(attrs.start ?? 0);
260
+ const len = Number(attrs.len ?? 0);
261
+ const out = new Float32Array(len);
262
+ for (let i = 0; i < len; i++) out[i] = vec[start + i] ?? 0;
263
+ return out;
264
+ },
265
+
211
266
  // -- Comparisons (produce Uint8Array for vectors, boolean for scalars) --
212
267
  Gt: (inputs) => compare(inputs[0], inputs[1], (a, b) => a > b),
213
268
  Lt: (inputs) => compare(inputs[0], inputs[1], (a, b) => a < b),
@@ -437,6 +492,37 @@ export const Primitives: Record<string, Primitive> = {
437
492
  return new Float32Array(xquat.slice(id * 4, id * 4 + 4));
438
493
  },
439
494
 
495
+ // -- Motion reference trajectory, sampled at a lookahead offset ----------
496
+ // `field`: 'root_pos' (3) | 'root_quat' (4) | 'joint_pos' (nJoints).
497
+ // `step`: offset added to refIdx, clamped to [0, refLen-1].
498
+ // Not ready → finite fallback (identity quat / zeros); pair with
499
+ // `TrackingIsReady` to zero the term out.
500
+ TrackingRefField: (_in, attrs, ctx) => {
501
+ const field = String(attrs.field ?? '');
502
+ const fallback = () =>
503
+ field === 'root_quat'
504
+ ? new Float32Array([1, 0, 0, 0])
505
+ : field === 'root_pos'
506
+ ? new Float32Array(3)
507
+ : new Float32Array(ctx.runner.getNumActions());
508
+ const term = motionRefTerm(ctx);
509
+ if (!term || !term.isReady()) return fallback();
510
+ const buf =
511
+ field === 'root_pos'
512
+ ? term.refRootPos
513
+ : field === 'root_quat'
514
+ ? term.refRootQuat
515
+ : term.refJointPos;
516
+ const frame = buf[clampRefIdx(term.refIdx, Number(attrs.step ?? 0), term.refLen)];
517
+ return frame ? new Float32Array(frame) : fallback();
518
+ },
519
+
520
+ // 1.0 when a motion reference is loaded and ready, else 0.0.
521
+ TrackingIsReady: (_in, _attrs, ctx) => {
522
+ const term = motionRefTerm(ctx);
523
+ return term && term.isReady() ? 1.0 : 0.0;
524
+ },
525
+
440
526
  // -- Tracking reference, per body (world frame) --------------------------
441
527
  TrackingRefBodyPos: (_in, attrs, ctx) => {
442
528
  const term = trackingTerm(ctx);