memlnaut-nisps/playground/js/shapeseq/tests/freeze.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

293 lines
9.4 KiB
JavaScript

import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { FreezeManager } from '../freeze.js';
import { Chain } from '../chain.js';
import { EuclideanRhythm, ProbabilityGate, PitchWalker } from '../primitives.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Create a chain with known primitives for testing.
* Returns { chain, paramCount }.
*/
function makeTestChain() {
const chain = new Chain();
chain.addPrimitive(new EuclideanRhythm());
chain.addPrimitive(new ProbabilityGate());
chain.addPrimitive(new PitchWalker());
return { chain, paramCount: chain.totalParamCount };
}
/**
* Create a Float32Array of params with sequential values for easy identification.
*/
function makeParams(count) {
const params = new Float32Array(count);
for (let i = 0; i < count; i++) {
params[i] = (i + 1) / (count + 1); // spread across (0, 1)
}
return params;
}
// ---------------------------------------------------------------------------
// freeze() captures params, seeds, states correctly
// ---------------------------------------------------------------------------
describe('FreezeManager.freeze()', () => {
it('captures params as a Float32Array copy', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
const params = makeParams(paramCount);
const masterSeed = 42;
fm.freeze(chain, params, masterSeed);
const frozen = fm.getFrozenParams();
assert.ok(frozen instanceof Float32Array, 'should be Float32Array');
assert.strictEqual(frozen.length, paramCount);
// Values should match
for (let i = 0; i < paramCount; i++) {
assert.strictEqual(frozen[i], params[i], 'param ' + i + ' should match');
}
// Should be a copy, not the same reference
assert.notStrictEqual(frozen, params);
});
it('captures per-primitive seeds', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
const params = makeParams(paramCount);
// Set known seeds
const prims = chain.getPrimitives();
prims[0].setSeed(100);
prims[1].setSeed(200);
prims[2].setSeed(300);
fm.freeze(chain, params, 42);
const seeds = fm.getFrozenSeeds();
assert.ok(Array.isArray(seeds));
assert.strictEqual(seeds.length, 3);
assert.strictEqual(seeds[0], 100);
assert.strictEqual(seeds[1], 200);
assert.strictEqual(seeds[2], 300);
});
it('captures per-primitive states', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
const params = makeParams(paramCount);
fm.freeze(chain, params, 42);
const states = fm.getFrozenStates();
assert.ok(Array.isArray(states));
assert.strictEqual(states.length, 3);
});
it('captures master seed', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
const params = makeParams(paramCount);
fm.freeze(chain, params, 12345);
assert.strictEqual(fm.getMasterSeed(), 12345);
});
it('initializes all live flags to 0 (frozen)', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
const params = makeParams(paramCount);
fm.freeze(chain, params, 42);
const flags = fm.getLiveFlags();
assert.ok(flags instanceof Uint8Array);
assert.strictEqual(flags.length, paramCount);
for (let i = 0; i < paramCount; i++) {
assert.strictEqual(flags[i], 0, 'param ' + i + ' should be frozen');
}
});
});
// ---------------------------------------------------------------------------
// isFrozen
// ---------------------------------------------------------------------------
describe('FreezeManager.isFrozen', () => {
it('returns false initially', () => {
const fm = new FreezeManager();
assert.strictEqual(fm.isFrozen, false);
});
it('returns true after freeze()', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
fm.freeze(chain, makeParams(paramCount), 42);
assert.strictEqual(fm.isFrozen, true);
});
});
// ---------------------------------------------------------------------------
// toggleParam
// ---------------------------------------------------------------------------
describe('FreezeManager.toggleParam()', () => {
it('flips a param from frozen to live and back', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
fm.freeze(chain, makeParams(paramCount), 42);
assert.strictEqual(fm.getLiveFlags()[0], 0);
fm.toggleParam(0);
assert.strictEqual(fm.getLiveFlags()[0], 1);
fm.toggleParam(0);
assert.strictEqual(fm.getLiveFlags()[0], 0);
});
it('only affects the targeted param', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
fm.freeze(chain, makeParams(paramCount), 42);
fm.toggleParam(2);
const flags = fm.getLiveFlags();
assert.strictEqual(flags[0], 0);
assert.strictEqual(flags[1], 0);
assert.strictEqual(flags[2], 1);
assert.strictEqual(flags[3], 0);
});
it('throws when not frozen', () => {
const fm = new FreezeManager();
assert.throws(() => fm.toggleParam(0), /cannot toggle when not frozen/);
});
it('throws on out-of-range index', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
fm.freeze(chain, makeParams(paramCount), 42);
assert.throws(() => fm.toggleParam(paramCount + 10), /out of range/);
});
});
// ---------------------------------------------------------------------------
// setParamLive
// ---------------------------------------------------------------------------
describe('FreezeManager.setParamLive()', () => {
it('explicitly sets live state', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
fm.freeze(chain, makeParams(paramCount), 42);
fm.setParamLive(1, true);
assert.strictEqual(fm.getLiveFlags()[1], 1);
fm.setParamLive(1, false);
assert.strictEqual(fm.getLiveFlags()[1], 0);
});
it('throws when not frozen', () => {
const fm = new FreezeManager();
assert.throws(() => fm.setParamLive(0, true), /cannot set when not frozen/);
});
});
// ---------------------------------------------------------------------------
// getEffectiveParams
// ---------------------------------------------------------------------------
describe('FreezeManager.getEffectiveParams()', () => {
it('returns frozen values for frozen params, current for live', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
const frozenParams = makeParams(paramCount);
fm.freeze(chain, frozenParams, 42);
// Make param 2 live
fm.toggleParam(2);
// Create different "current" ML params
const currentParams = new Float32Array(paramCount);
for (let i = 0; i < paramCount; i++) {
currentParams[i] = 0.99;
}
const effective = fm.getEffectiveParams(currentParams);
assert.ok(effective instanceof Float32Array);
assert.strictEqual(effective.length, paramCount);
// Param 0: frozen -> uses frozen value
assert.strictEqual(effective[0], frozenParams[0]);
// Param 1: frozen -> uses frozen value
assert.strictEqual(effective[1], frozenParams[1]);
// Param 2: live -> uses current ML value (Float32 precision)
assert.ok(Math.abs(effective[2] - 0.99) < 1e-5, 'live param should use current ML value');
// Param 3: frozen -> uses frozen value
assert.strictEqual(effective[3], frozenParams[3]);
});
it('returns null when not frozen', () => {
const fm = new FreezeManager();
const result = fm.getEffectiveParams(new Float32Array(4));
assert.strictEqual(result, null);
});
});
// ---------------------------------------------------------------------------
// unfreeze
// ---------------------------------------------------------------------------
describe('FreezeManager.unfreeze()', () => {
it('clears all captured state', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
fm.freeze(chain, makeParams(paramCount), 42);
assert.strictEqual(fm.isFrozen, true);
fm.unfreeze();
assert.strictEqual(fm.isFrozen, false);
assert.strictEqual(fm.getFrozenParams(), null);
assert.strictEqual(fm.getLiveFlags(), null);
assert.strictEqual(fm.getFrozenSeeds(), null);
assert.strictEqual(fm.getFrozenStates(), null);
assert.strictEqual(fm.getMasterSeed(), null);
});
});
// ---------------------------------------------------------------------------
// shouldSuppressReEval
// ---------------------------------------------------------------------------
describe('FreezeManager.shouldSuppressReEval()', () => {
it('returns true when frozen', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
fm.freeze(chain, makeParams(paramCount), 42);
assert.strictEqual(fm.shouldSuppressReEval(), true);
});
it('returns false when not frozen', () => {
const fm = new FreezeManager();
assert.strictEqual(fm.shouldSuppressReEval(), false);
});
it('returns false after unfreeze', () => {
const fm = new FreezeManager();
const { chain, paramCount } = makeTestChain();
fm.freeze(chain, makeParams(paramCount), 42);
fm.unfreeze();
assert.strictEqual(fm.shouldSuppressReEval(), false);
});
});