memlnaut-nisps/playground/js/shapeseq/tests/delta.test.js
w1n5t0n 9b08918b1e feat(shapeseq): freeze-as-algorithm/pattern + delta controller
Two freeze modes for capturing and preserving musical state:

- Algorithm freeze: snapshot all primitive params + seeds. Individual
  params can be selectively unfrozen for ML-driven exploration via
  DeltaController (applies MLP deltas to frozen base values with
  per-param boundary enforcement).

- Pattern freeze: snapshot the realized note pattern. The sequencer
  bypasses the primitive chain entirely and loops the frozen pattern.

Extracts _runPipeline() from setSequenceInputs() for reuse.
2026-04-06 23:55:23 +01:00

193 lines
7.1 KiB
JavaScript

import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { DeltaController } from '../delta.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const EPS = 1e-6;
function approxEqual(actual, expected, msg) {
assert.ok(
Math.abs(actual - expected) < EPS,
`${msg || ''} expected ${expected}, got ${actual}`
);
}
function makeSchemas(count, boundary = 'clamp', scaledRange) {
const schemas = [];
for (let i = 0; i < count; i++) {
const entry = { boundary };
if (boundary === 'scaled' && scaledRange !== undefined) {
entry.scaledRange = scaledRange;
}
schemas.push({ schema: entry });
}
return schemas;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('DeltaController.computeEffective', () => {
it('all frozen (liveFlags all 0): output === frozenParams', () => {
const frozen = new Float32Array([0.2, 0.5, 0.8]);
const live = new Uint8Array([0, 0, 0]);
const mlp = new Float32Array([0.0, 1.0, 0.5]);
const schemas = makeSchemas(3);
const result = DeltaController.computeEffective(frozen, live, mlp, schemas);
approxEqual(result[0], 0.2, 'param 0');
approxEqual(result[1], 0.5, 'param 1');
approxEqual(result[2], 0.8, 'param 2');
});
it('all live with clamp boundary: delta applied, result clamped to [0,1]', () => {
const frozen = new Float32Array([0.9, 0.1]);
const live = new Uint8Array([1, 1]);
const mlp = new Float32Array([1.0, 0.0]); // max positive, max negative
const schemas = makeSchemas(2, 'clamp');
const deltaScale = 0.3;
const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale);
// param 0: 0.9 + (1.0 - 0.5)*2*0.3 = 0.9 + 0.3 = 1.2 -> clamped to 1.0
approxEqual(result[0], 1.0, 'param 0 clamped high');
// param 1: 0.1 + (0.0 - 0.5)*2*0.3 = 0.1 - 0.3 = -0.2 -> clamped to 0.0
approxEqual(result[1], 0.0, 'param 1 clamped low');
});
it('live with wrap boundary: values wrap around', () => {
const frozen = new Float32Array([0.9, 0.1]);
const live = new Uint8Array([1, 1]);
const mlp = new Float32Array([1.0, 0.0]);
const schemas = makeSchemas(2, 'wrap');
const deltaScale = 0.3;
const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale);
// param 0: 0.9 + 0.3 = 1.2 -> wrap -> 0.2
approxEqual(result[0], 0.2, 'param 0 wrapped high');
// param 1: 0.1 - 0.3 = -0.2 -> wrap -> 0.8
approxEqual(result[1], 0.8, 'param 1 wrapped low');
});
it('live with scaled boundary: delta operates within scaledRange of frozen value', () => {
const frozen = new Float32Array([0.5]);
const live = new Uint8Array([1]);
// With scaled boundary, applyBoundary maps [0,1] input to [frozen-range, frozen+range]
// The raw value passed to applyBoundary is frozen + delta
// For scaled: lo = 0.5 - 0.3 = 0.2, hi = 0.5 + 0.3 = 0.8
// mapped = 0.2 + raw * 0.6
const mlp = new Float32Array([0.75]); // delta = (0.75 - 0.5)*2*0.3 = 0.15, raw = 0.65
const schemas = makeSchemas(1, 'scaled', 0.3);
const deltaScale = 0.3;
const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale);
// raw = 0.5 + 0.15 = 0.65
// scaled: lo = 0.5 - 0.3 = 0.2, hi = 0.5 + 0.3 = 0.8
// mapped = 0.2 + 0.65 * (0.8 - 0.2) = 0.2 + 0.39 = 0.59
approxEqual(result[0], 0.59, 'scaled boundary');
});
it('MLP value 0.5 produces zero delta (frozen value unchanged)', () => {
const frozen = new Float32Array([0.3, 0.7]);
const live = new Uint8Array([1, 1]);
const mlp = new Float32Array([0.5, 0.5]);
const schemas = makeSchemas(2, 'clamp');
const result = DeltaController.computeEffective(frozen, live, mlp, schemas);
approxEqual(result[0], 0.3, 'param 0 unchanged');
approxEqual(result[1], 0.7, 'param 1 unchanged');
});
it('MLP value 0.0 produces max negative delta', () => {
const frozen = new Float32Array([0.5]);
const live = new Uint8Array([1]);
const mlp = new Float32Array([0.0]);
const schemas = makeSchemas(1, 'clamp');
const deltaScale = 0.3;
const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale);
// 0.5 + (0.0 - 0.5)*2*0.3 = 0.5 - 0.3 = 0.2
approxEqual(result[0], 0.2, 'max negative delta');
});
it('MLP value 1.0 produces max positive delta', () => {
const frozen = new Float32Array([0.5]);
const live = new Uint8Array([1]);
const mlp = new Float32Array([1.0]);
const schemas = makeSchemas(1, 'clamp');
const deltaScale = 0.3;
const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale);
// 0.5 + (1.0 - 0.5)*2*0.3 = 0.5 + 0.3 = 0.8
approxEqual(result[0], 0.8, 'max positive delta');
});
it('mixed frozen/live params', () => {
const frozen = new Float32Array([0.2, 0.5, 0.8, 0.4]);
const live = new Uint8Array([0, 1, 0, 1]);
const mlp = new Float32Array([0.9, 0.5, 0.1, 0.75]);
const schemas = makeSchemas(4, 'clamp');
const deltaScale = 0.3;
const result = DeltaController.computeEffective(frozen, live, mlp, schemas, deltaScale);
// param 0: frozen -> 0.2
approxEqual(result[0], 0.2, 'frozen param 0');
// param 1: live, mlp=0.5 -> zero delta -> 0.5
approxEqual(result[1], 0.5, 'live param 1 zero delta');
// param 2: frozen -> 0.8
approxEqual(result[2], 0.8, 'frozen param 2');
// param 3: live, 0.4 + (0.75 - 0.5)*2*0.3 = 0.4 + 0.15 = 0.55
approxEqual(result[3], 0.55, 'live param 3');
});
it('deltaScale affects magnitude', () => {
const frozen = new Float32Array([0.5]);
const live = new Uint8Array([1]);
const mlp = new Float32Array([1.0]); // max positive
const schemas = makeSchemas(1, 'clamp');
const r1 = DeltaController.computeEffective(frozen, live, mlp, schemas, 0.1);
const r2 = DeltaController.computeEffective(frozen, live, mlp, schemas, 0.5);
// scale 0.1: 0.5 + 0.1 = 0.6
approxEqual(r1[0], 0.6, 'small deltaScale');
// scale 0.5: 0.5 + 0.5 = 1.0
approxEqual(r2[0], 1.0, 'large deltaScale');
});
it('defaults deltaScale to 0.3 when not provided', () => {
const frozen = new Float32Array([0.5]);
const live = new Uint8Array([1]);
const mlp = new Float32Array([1.0]);
const schemas = makeSchemas(1, 'clamp');
const result = DeltaController.computeEffective(frozen, live, mlp, schemas);
// 0.5 + (1.0 - 0.5)*2*0.3 = 0.8
approxEqual(result[0], 0.8, 'default deltaScale 0.3');
});
it('falls back to clamp when schema is missing', () => {
const frozen = new Float32Array([0.9]);
const live = new Uint8Array([1]);
const mlp = new Float32Array([1.0]);
const schemas = []; // empty — no schema for this index
const result = DeltaController.computeEffective(frozen, live, mlp, schemas, 0.3);
// 0.9 + 0.3 = 1.2 -> clamped to 1.0
approxEqual(result[0], 1.0, 'missing schema falls back to clamp');
});
});