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/dist/assets/{index-B7_87iWG.js → index-D2tMWOeA.js} +265 -265
- package/dist/index.html +1 -1
- package/dist/mjswan.js +1589 -1554
- package/package.json +1 -1
- package/src/core/dsl/__tests__/primitives.test.ts +98 -0
- package/src/core/dsl/primitives.ts +86 -0
package/package.json
CHANGED
|
@@ -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);
|