memlnaut-nisps/playground/js/shapeseq/param-map.js

95 lines
3.4 KiB
JavaScript
Raw Normal View History

// ShapeSeq param mapping layer
// Maps fixed-size MLP outputs (16 values [0,1]) to variable-count primitive params
//
// The sequence MLP always outputs a fixed number of values (default 16).
// The primitive chain has a variable number of params depending on which
// primitives are active. This module bridges the two via automatic distribution:
// - N <= mlpOutputCount: each param gets one dedicated output
// - N > mlpOutputCount: outputs distributed via linear interpolation
//
// Port-ready: Float32Array, no closures, pure functions.
/**
* Create a param mapper instance for a given MLP output count.
* @param {number} mlpOutputCount - number of MLP outputs (e.g. 16)
* @returns {{ map: Function, mapWithSchema: Function, mlpOutputCount: number }}
*/
export function createParamMap(mlpOutputCount) {
if (!Number.isInteger(mlpOutputCount) || mlpOutputCount < 1) {
throw new Error('mlpOutputCount must be a positive integer');
}
return {
mlpOutputCount,
map,
mapWithSchema,
};
}
/**
* Map MLP outputs to N primitive params via automatic distribution.
*
* If paramCount <= mlpOutputCount, each param gets one dedicated output
* (first paramCount outputs used, rest ignored).
*
* If paramCount > mlpOutputCount, outputs are distributed via linear
* interpolation so that the first param maps to the first output and the
* last param maps to the last output, with intermediate params interpolated.
*
* @param {Float32Array|number[]} mlpOutputs - MLP output values [0,1]
* @param {number} paramCount - number of primitive params to produce
* @returns {Float32Array} mapped values [0,1], length = paramCount
*/
export function map(mlpOutputs, paramCount) {
const mlpCount = mlpOutputs.length;
const result = new Float32Array(paramCount);
if (paramCount === 0) return result;
if (paramCount <= mlpCount) {
// Direct mapping: each param gets one dedicated output
for (let i = 0; i < paramCount; i++) {
result[i] = mlpOutputs[i];
}
} else {
// Interpolated mapping: spread mlpCount outputs across paramCount params
// param[i] maps to a fractional position in the output array
// param[0] -> output[0], param[paramCount-1] -> output[mlpCount-1]
const scale = paramCount > 1 ? (mlpCount - 1) / (paramCount - 1) : 0;
for (let i = 0; i < paramCount; i++) {
const pos = i * scale;
const lo = pos | 0; // floor
const hi = lo + 1 < mlpCount ? lo + 1 : lo;
const frac = pos - lo;
result[i] = mlpOutputs[lo] + (mlpOutputs[hi] - mlpOutputs[lo]) * frac;
}
}
return result;
}
/**
* Map MLP outputs to primitive params and apply per-param min/max scaling.
*
* Each param schema defines { min, max } (both [0,1]). The mapped [0,1]
* value is scaled into [min, max] for each param.
*
* @param {Float32Array|number[]} mlpOutputs - MLP output values [0,1]
* @param {Array<{ min: number, max: number }>} paramSchemas - per-param range definitions
* @returns {Float32Array} scaled values, length = paramSchemas.length
*/
export function mapWithSchema(mlpOutputs, paramSchemas) {
const paramCount = paramSchemas.length;
const mapped = map(mlpOutputs, paramCount);
for (let i = 0; i < paramCount; i++) {
const schema = paramSchemas[i];
const min = schema.min !== undefined ? schema.min : 0;
const max = schema.max !== undefined ? schema.max : 1;
mapped[i] = min + mapped[i] * (max - min);
}
return mapped;
}