chore: delete retired-playground artefacts and root relics

Phase 1 of the 2026-07 simplification audit, group 1. All verified dead by
independent grep across scripts/, .github/workflows/, codegen/, manifold/,
firmware/, vcv/, nisps/ and README.md before removal.

- S23 / L55: playground/ — only dist/ was tracked (1.2 MB of bundled JS,
  sourcemaps and a second copy of nisps.wasm). The app itself is preserved on
  branch archive/playground-solidjs and tag playground-solidjs-final.
- S29: the root Playwright rig — playwright.config.js, package.json,
  package-lock.json and tests/e2e/. It served ./playground, a page that no
  longer exists anywhere in the tree, and was invoked by nothing. NOTE: the
  live suite is manifold/tests/e2e/ with manifold/playwright.config.ts, which
  is untouched; every CI Playwright step runs with working-directory: manifold.
- L48: NISPS_CORE_EXTRACTION_PLAN.md + NISPS_CORE_TASKS.md (818 lines of
  extraction relics; docs/specs/plans/ is the sanctioned home for plans).
- L36: data/ — two CSVs from a retired era, plus a tracked LibreOffice lock file.
- L32: .claude/worktrees/agent-ae87fe47/ only. The audit's wording invites
  deleting .claude/worktrees/ wholesale; that would have been destructive —
  the directory holds 12 LIVE registered git worktrees (332 MB), four of them
  on branches with unpushed commits. Verified agent-ae87fe47 is NOT registered
  before removing it; every live worktree is left intact. Cleaning up the rest
  is a separate operator decision.
- ST10 fallout: dropped the dead `port-solidjs` branch trigger from ci.yml and
  the dead `feat/manifold-mission` / `feat/vcv-dist` triggers from
  vcv-plugin.yml — those branches are stale and the SolidJS target is retired.

tests/cpp/ is untouched. Gates: run-all-tests.sh ALL GREEN.
This commit is contained in:
monkey-w1n5t0n 2026-07-21 12:47:58 +02:00
parent aa60fbb466
commit abb569b287
39 changed files with 3 additions and 6073 deletions

View file

@ -1,319 +0,0 @@
/**
* ShapeSeq Chain sequential pipeline runner with generator combination modes
*
* Evaluates an ordered list of primitives as a sequential pipeline:
* 1. Generators run first, combined via additive or multiplicative merge
* 2. Processors transform the pattern in chain order
* 3. Converters run in chain order
* 4. Timing modifiers annotate last
*
* Params are distributed flat across primitives in chain order.
* Each primitive gets a deterministic PRNG stream via fork(masterPRNG, index).
*
* Port-ready: explicit state, typed arrays, no closures in hot path.
*
* @module shapeseq/chain
*/
import { createPattern, mergePatterns } from './pattern.js';
import { createPRNG, fork } from './prng.js';
// ── Category execution order ────────────────────────────────────────
const PHASE_ORDER = ['generator', 'processor', 'converter', 'timing'];
// ── Chain class ─────────────────────────────────────────────────────
export class Chain {
constructor() {
/** @private @type {Array<import('./primitive.js').Primitive>} */
this._primitives = [];
/** @type {'additive'|'multiplicative'} */
this.generatorCombineMode = 'additive';
/** @private @type {number} */
this._masterSeed = 0;
/**
* Per-primitive state objects, indexed by position in chain.
* Populated after evaluate() calls; used for freeze support.
* @private @type {Array<Object>}
*/
this._primitiveStates = [];
}
// ── Primitive management ────────────────────────────────────────
/**
* Append a primitive to the end of the chain.
* @param {import('./primitive.js').Primitive} primitive
*/
addPrimitive(primitive) {
this._primitives.push(primitive);
this._primitiveStates.push(primitive.getState());
}
/**
* Remove the primitive at the given index.
* @param {number} index
*/
removePrimitive(index) {
const idx = index | 0;
if (idx < 0 || idx >= this._primitives.length) {
throw new RangeError('removePrimitive: index ' + index + ' out of range [0, ' + (this._primitives.length - 1) + ']');
}
this._primitives.splice(idx, 1);
this._primitiveStates.splice(idx, 1);
}
/**
* Insert a primitive at the given index, shifting others right.
* @param {number} index
* @param {import('./primitive.js').Primitive} primitive
*/
insertPrimitive(index, primitive) {
const idx = index | 0;
if (idx < 0 || idx > this._primitives.length) {
throw new RangeError('insertPrimitive: index ' + index + ' out of range [0, ' + this._primitives.length + ']');
}
this._primitives.splice(idx, 0, primitive);
this._primitiveStates.splice(idx, 0, primitive.getState());
}
/**
* Move a primitive from one position to another.
* @param {number} fromIndex
* @param {number} toIndex
*/
movePrimitive(fromIndex, toIndex) {
const from = fromIndex | 0;
const to = toIndex | 0;
const len = this._primitives.length;
if (from < 0 || from >= len) {
throw new RangeError('movePrimitive: fromIndex ' + fromIndex + ' out of range [0, ' + (len - 1) + ']');
}
if (to < 0 || to >= len) {
throw new RangeError('movePrimitive: toIndex ' + toIndex + ' out of range [0, ' + (len - 1) + ']');
}
const [prim] = this._primitives.splice(from, 1);
const [state] = this._primitiveStates.splice(from, 1);
this._primitives.splice(to, 0, prim);
this._primitiveStates.splice(to, 0, state);
}
/**
* Get the current list of primitives (shallow copy).
* @returns {Array<import('./primitive.js').Primitive>}
*/
getPrimitives() {
return this._primitives.slice();
}
// ── Configuration ───────────────────────────────────────────────
/**
* Total parameter count across all primitives in the chain.
* @returns {number}
*/
get totalParamCount() {
let total = 0;
for (let i = 0; i < this._primitives.length; i++) {
total += this._primitives[i].paramCount;
}
return total;
}
/**
* Get a flat list of all param schemas across all primitives,
* annotated with their primitive and param indices.
*
* @returns {Array<{ primitiveIndex: number, paramIndex: number, schema: Object }>}
*/
getParamSchemas() {
const result = [];
for (let pi = 0; pi < this._primitives.length; pi++) {
const prim = this._primitives[pi];
const schema = prim.paramSchema;
for (let si = 0; si < schema.length; si++) {
result.push({
primitiveIndex: pi,
paramIndex: si,
schema: schema[si],
});
}
}
return result;
}
// ── Evaluation ──────────────────────────────────────────────────
/**
* Evaluate the chain, producing a pattern description.
*
* Pipeline order:
* 1. Generators combined via generatorCombineMode
* 2. Processors sequential transform
* 3. Converters sequential transform
* 4. Timing modifiers annotate last
*
* @param {Float32Array|Array<number>} params - flat param array distributed across primitives
* @param {number} stepCount - number of steps in the output pattern
* @param {number} masterSeed - seed for the master PRNG
* @returns {{ steps: Array, stepCount: number, metadata: Object }}
*/
evaluate(params, stepCount, masterSeed) {
const primitives = this._primitives;
const primCount = primitives.length;
// Create master PRNG from seed
const masterPRNG = createPRNG(masterSeed >>> 0);
// ── Bucket primitives by category, preserving chain order ──
/** @type {Array<{ index: number, prim: Object }>} */
const generators = [];
const processors = [];
const converters = [];
const timingMods = [];
for (let i = 0; i < primCount; i++) {
const entry = { index: i, prim: primitives[i] };
switch (primitives[i].category) {
case 'generator': generators.push(entry); break;
case 'processor': processors.push(entry); break;
case 'converter': converters.push(entry); break;
case 'timing': timingMods.push(entry); break;
}
}
// ── Compute param offsets per primitive ──
const paramOffsets = new Array(primCount);
let offset = 0;
for (let i = 0; i < primCount; i++) {
paramOffsets[i] = offset;
offset += primitives[i].paramCount;
}
// ── Helper: run a single primitive ──
const self = this;
function runPrimitive(entry, inputPattern) {
const idx = entry.index;
const prim = entry.prim;
const pOffset = paramOffsets[idx];
const pCount = prim.paramCount;
// Slice params for this primitive
const primParams = new Float32Array(pCount);
for (let p = 0; p < pCount; p++) {
primParams[p] = pOffset + p < params.length ? +params[pOffset + p] : prim.paramSchema[p].default;
}
// Fork a deterministic PRNG for this primitive
const primRNG = fork(masterPRNG, idx);
// Get current state
const state = self._primitiveStates[idx] || prim.getState();
// Process
const result = prim.process(primParams, inputPattern, state, primRNG);
// Store updated state
self._primitiveStates[idx] = result.nextState;
return result.patternDesc;
}
// ── Phase 1: Generators ──
let pattern;
if (generators.length === 0) {
// Default pattern: all steps triggered
pattern = createPattern(stepCount);
for (let i = 0; i < stepCount; i++) {
pattern.steps[i].trigger = true;
}
} else if (generators.length === 1) {
// Single generator — no merge needed
pattern = runPrimitive(generators[0], createPattern(stepCount));
} else {
// Multiple generators — run each, then merge
let merged = runPrimitive(generators[0], createPattern(stepCount));
for (let g = 1; g < generators.length; g++) {
const next = runPrimitive(generators[g], createPattern(stepCount));
merged = mergePatterns(merged, next, this.generatorCombineMode);
}
pattern = merged;
}
// ── Phase 2: Processors ──
for (let i = 0; i < processors.length; i++) {
pattern = runPrimitive(processors[i], pattern);
}
// ── Phase 3: Converters ──
for (let i = 0; i < converters.length; i++) {
pattern = runPrimitive(converters[i], pattern);
}
// ── Phase 4: Timing modifiers ──
for (let i = 0; i < timingMods.length; i++) {
pattern = runPrimitive(timingMods[i], pattern);
}
return pattern;
}
// ── State management (for freeze) ──────────────────────────────
/**
* Get serializable state for all primitives in the chain.
* @returns {Array<Object>}
*/
getState() {
const states = new Array(this._primitives.length);
for (let i = 0; i < this._primitives.length; i++) {
states[i] = this._primitiveStates[i] || this._primitives[i].getState();
}
return states;
}
/**
* Restore all primitive states from a previously serialized state array.
* @param {Array<Object>} states
*/
setState(states) {
if (!Array.isArray(states)) {
throw new TypeError('setState expects an array of state objects');
}
const len = Math.min(states.length, this._primitives.length);
for (let i = 0; i < len; i++) {
this._primitives[i].setState(states[i]);
this._primitiveStates[i] = states[i];
}
}
/**
* Get the master PRNG seed.
* @returns {number}
*/
getMasterSeed() {
return this._masterSeed;
}
/**
* Set the master PRNG seed.
* @param {number} seed - 32-bit integer seed
*/
setMasterSeed(seed) {
this._masterSeed = seed >>> 0;
}
}

View file

@ -1,246 +0,0 @@
/**
* ShapeSeq Primitive Base Class and Param Schema System
*
* Base class for all sequencing primitives (generators, processors,
* timing modifiers, converters). Defines the param schema format,
* symbolic process() interface, and state management for freeze support.
*
* Port-ready: explicit state, no closures, typed arrays where possible.
*
* @module shapeseq/primitive
*/
// ── Valid primitive categories ──────────────────────────────────────
export const CATEGORIES = Object.freeze([
'generator',
'processor',
'timing',
'converter',
]);
// ── Param schema defaults ───────────────────────────────────────────
const DEFAULT_SCALED_RANGE = 0.3;
// ── Boundary enforcement helpers ────────────────────────────────────
/**
* Clamp a value to [0, 1].
* @param {number} v
* @returns {number}
*/
function clamp01(v) {
return v < 0 ? 0 : v > 1 ? 1 : v;
}
/**
* Wrap a value into [0, 1) with modular arithmetic.
* @param {number} v
* @returns {number}
*/
function wrap01(v) {
const m = v % 1;
return m < 0 ? m + 1 : m;
}
/**
* Apply boundary enforcement to a raw param value.
*
* @param {number} value - raw [0,1] value (or delta-adjusted value)
* @param {{ boundary: string, scaledRange?: number }} schema - param schema entry
* @param {number|null} frozenValue - frozen value for 'scaled' boundary (null if not frozen)
* @returns {number}
*/
export function applyBoundary(value, schema, frozenValue) {
switch (schema.boundary) {
case 'wrap':
return wrap01(value);
case 'scaled': {
if (frozenValue === null || frozenValue === undefined) {
return clamp01(value);
}
const range = schema.scaledRange !== undefined ? schema.scaledRange : DEFAULT_SCALED_RANGE;
const lo = frozenValue - range;
const hi = frozenValue + range;
// Map [0,1] input to [lo, hi], then clamp to [0,1]
const mapped = lo + value * (hi - lo);
return clamp01(mapped);
}
case 'clamp':
default:
return clamp01(value);
}
}
// ── Param schema validation ─────────────────────────────────────────
/**
* Validate a single param schema entry.
* Throws on invalid entries for fast fail during development.
*
* @param {{ name: string, default: number, boundary: string, scaledRange?: number }} entry
* @param {number} index - position in schema array (for error messages)
*/
function validateSchemaEntry(entry, index) {
if (!entry || typeof entry !== 'object') {
throw new TypeError('paramSchema[' + index + '] must be an object');
}
if (typeof entry.name !== 'string' || entry.name.length === 0) {
throw new TypeError('paramSchema[' + index + '].name must be a non-empty string');
}
if (typeof entry.default !== 'number' || entry.default < 0 || entry.default > 1) {
throw new RangeError('paramSchema[' + index + '].default must be in [0,1], got ' + entry.default);
}
if (entry.boundary !== 'clamp' && entry.boundary !== 'wrap' && entry.boundary !== 'scaled') {
throw new TypeError(
"paramSchema[" + index + "].boundary must be 'clamp', 'wrap', or 'scaled', got '" + entry.boundary + "'"
);
}
if (entry.boundary === 'scaled') {
const sr = entry.scaledRange;
if (sr !== undefined && (typeof sr !== 'number' || sr <= 0 || sr > 1)) {
throw new RangeError('paramSchema[' + index + '].scaledRange must be in (0,1], got ' + sr);
}
}
}
// ── Primitive base class ────────────────────────────────────────────
export class Primitive {
/**
* @param {string} name - unique identifier for this primitive type
* @param {string} category - one of CATEGORIES
* @param {Array<{ name: string, default: number, boundary: string, scaledRange?: number }>} paramSchema
*/
constructor(name, category, paramSchema) {
if (typeof name !== 'string' || name.length === 0) {
throw new TypeError('Primitive name must be a non-empty string');
}
if (CATEGORIES.indexOf(category) === -1) {
throw new TypeError(
"Primitive category must be one of [" + CATEGORIES.join(', ') + "], got '" + category + "'"
);
}
if (!Array.isArray(paramSchema)) {
throw new TypeError('paramSchema must be an array');
}
// Validate each entry
for (let i = 0; i < paramSchema.length; i++) {
validateSchemaEntry(paramSchema[i], i);
}
/** @type {string} */
this.name = name;
/** @type {string} */
this.category = category;
/**
* Frozen copy of the param schema. Each entry:
* { name: string, default: number, boundary: 'clamp'|'wrap'|'scaled', scaledRange?: number }
* @type {Array<Object>}
*/
this.paramSchema = Object.freeze(paramSchema.map(function (entry) {
const frozen = {
name: entry.name,
default: entry.default,
boundary: entry.boundary,
};
if (entry.boundary === 'scaled') {
frozen.scaledRange = entry.scaledRange !== undefined ? entry.scaledRange : DEFAULT_SCALED_RANGE;
}
return Object.freeze(frozen);
}));
/** @private */
this._seed = 0;
}
// ── Param utilities ─────────────────────────────────────────────
/**
* Total number of parameters this primitive exposes.
* @returns {number}
*/
get paramCount() {
return this.paramSchema.length;
}
/**
* Get default param values as a Float32Array, one per schema entry.
* @returns {Float32Array}
*/
getDefaults() {
const count = this.paramSchema.length;
const defaults = new Float32Array(count);
for (let i = 0; i < count; i++) {
defaults[i] = this.paramSchema[i].default;
}
return defaults;
}
// ── Symbolic processing ─────────────────────────────────────────
/**
* Transform a pattern description. Subclasses MUST override this.
*
* - Generators ignore patternDesc and create a new one (using createPattern())
* - Processors/timing modifiers clone and transform patternDesc
* - The rng param is a PRNG state from prng.js; consume via next(rng)
* and return the consumed state in the result
*
* @param {Float32Array|Array<number>} params - param values, one per schema entry, each [0,1]
* @param {{ steps: Array, stepCount: number, metadata: Object }} patternDesc - input pattern
* @param {Object} state - primitive-specific state (from previous process() call or getState())
* @param {{ state: number }} rng - PRNG state object from prng.js
* @returns {{ patternDesc: { steps: Array, stepCount: number, metadata: Object }, nextState: Object }}
*/
process(params, patternDesc, state, rng) {
void params; void patternDesc; void state; void rng;
throw new Error(this.name + '.process() must be overridden by subclass');
}
// ── State management (for freeze) ──────────────────────────────
/**
* Get serializable state for this primitive.
* Stateless primitives return {}. Stateful primitives (e.g. Pitch Walker)
* override to include their internal state.
*
* @returns {Object}
*/
getState() {
return {};
}
/**
* Restore primitive state from a previously serialized state object.
* Stateless primitives are a no-op. Stateful primitives override.
*
* @param {Object} _state
*/
setState(_state) {
// no-op for stateless primitives
}
/**
* Get the PRNG seed associated with this primitive.
* Used by freeze-as-algorithm to replay identical sequences.
*
* @returns {number}
*/
getSeed() {
return this._seed;
}
/**
* Set the PRNG seed for this primitive.
*
* @param {number} seed - 32-bit integer seed
*/
setSeed(seed) {
this._seed = seed >>> 0;
}
}

View file

@ -1,505 +0,0 @@
/**
* ShapeSeq Sequencing Primitives
*
* All 8 primitives for the ShapeSeq generative sequencing system.
* Each extends Primitive and implements process(params, patternDesc, state, rng).
*
* Port-ready: explicit state, no closures, seeded PRNG, typed arrays.
*
* @module shapeseq/primitives
*/
import { Primitive } from './primitive.js';
import { createPattern, clonePattern, setStep } from './pattern.js';
import { next, nextInt } from './prng.js';
// ── Helper: map [0,1] float to integer range [lo, hi] ──────────────
function mapToInt(value, lo, hi) {
const clamped = value < 0 ? 0 : value > 1 ? 1 : value;
return lo + Math.floor(clamped * (hi - lo + 1 - 1e-9));
}
// ── 1. EuclideanRhythm ──────────────────────────────────────────────
/**
* Bjorklund algorithm: distribute `pulses` as evenly as possible
* across `steps`, then apply rotation.
*/
function bjorklund(steps, pulses) {
if (pulses >= steps) {
const result = new Array(steps);
for (let i = 0; i < steps; i++) result[i] = true;
return result;
}
if (pulses <= 0) {
const result = new Array(steps);
for (let i = 0; i < steps; i++) result[i] = false;
return result;
}
// Build pattern using Bjorklund's algorithm
let groups = [];
for (let i = 0; i < pulses; i++) groups.push([true]);
for (let i = 0; i < steps - pulses; i++) groups.push([false]);
while (true) {
const remainder = groups.length - pulses;
if (remainder <= 1) break;
const minLen = Math.min(pulses, remainder);
const newGroups = [];
for (let i = 0; i < minLen; i++) {
newGroups.push(groups[i].concat(groups[groups.length - 1 - i]));
}
// Keep any leftovers
const leftStart = minLen;
const leftEnd = groups.length - minLen;
for (let i = leftStart; i < leftEnd; i++) {
newGroups.push(groups[i]);
}
groups = newGroups;
pulses = minLen;
if (pulses <= 1) break;
}
// Flatten groups
const result = [];
for (let i = 0; i < groups.length; i++) {
for (let j = 0; j < groups[i].length; j++) {
result.push(groups[i][j]);
}
}
return result;
}
export class EuclideanRhythm extends Primitive {
constructor() {
super('EuclideanRhythm', 'generator', [
{ name: 'steps', default: 0.5, boundary: 'clamp' },
{ name: 'pulses', default: 0.5, boundary: 'clamp' },
{ name: 'rotation', default: 0.0, boundary: 'wrap' },
]);
}
process(params, patternDesc, state, rng) {
const stepCount = patternDesc.stepCount;
const steps = mapToInt(params[0], 2, stepCount);
const pulses = mapToInt(params[1], 0, steps);
const rotation = mapToInt(params[2], 0, steps - 1);
const rhythm = bjorklund(steps, pulses);
const pattern = createPattern(stepCount);
for (let i = 0; i < stepCount; i++) {
if (i < steps) {
const srcIdx = (i - rotation + steps) % steps;
if (rhythm[srcIdx]) {
setStep(pattern, i, { trigger: true });
}
}
// Steps beyond `steps` remain untriggered (default)
}
return { patternDesc: pattern, nextState: {} };
}
}
// ── 2. ProbabilityGate ──────────────────────────────────────────────
export class ProbabilityGate extends Primitive {
constructor() {
super('ProbabilityGate', 'processor', [
{ name: 'density', default: 0.7, boundary: 'clamp' },
{ name: 'accentProbability', default: 0.3, boundary: 'clamp' },
]);
}
process(params, patternDesc, state, rng) {
const density = params[0];
const accentProb = params[1];
const pattern = clonePattern(patternDesc);
let currentRng = rng;
for (let i = 0; i < pattern.stepCount; i++) {
const step = pattern.steps[i];
if (step.trigger) {
// Coin flip for survival
const r1 = next(currentRng);
currentRng = r1.nextState;
if (r1.value >= density) {
step.trigger = false;
step.accent = false;
} else {
// Accent coin flip
const r2 = next(currentRng);
currentRng = r2.nextState;
step.accent = r2.value < accentProb;
}
}
}
return { patternDesc: pattern, nextState: {} };
}
}
// ── 3. PitchWalker ──────────────────────────────────────────────────
export class PitchWalker extends Primitive {
constructor() {
super('PitchWalker', 'generator', [
{ name: 'stepSize', default: 0.3, boundary: 'clamp' },
{ name: 'directionBias', default: 0.5, boundary: 'clamp' },
{ name: 'gravity', default: 0.3, boundary: 'clamp' },
{ name: 'range', default: 0.8, boundary: 'clamp' },
]);
/** @private */
this._position = 0.5;
}
getState() {
return { position: this._position };
}
setState(savedState) {
if (savedState && typeof savedState.position === 'number') {
this._position = savedState.position;
}
}
process(params, patternDesc, state, rng) {
const stepSize = params[0];
const directionBias = params[1];
const gravity = params[2];
const range = params[3];
// Restore position from state if provided
let position = (state && typeof state.position === 'number')
? state.position
: this._position;
const pattern = createPattern(patternDesc.stepCount);
let currentRng = rng;
// Use incoming pattern's triggers if available, otherwise all triggered
const srcSteps = patternDesc.steps;
for (let i = 0; i < patternDesc.stepCount; i++) {
const triggered = srcSteps[i].trigger;
if (triggered) {
// Random walk step
const r1 = next(currentRng);
currentRng = r1.nextState;
// Direction: bias + gravity toward center
const gravityPull = (0.5 - position) * gravity;
const biasOffset = (directionBias - 0.5) * 2; // [-1, 1]
const direction = biasOffset + gravityPull;
// Random component: [-1, 1] scaled by stepSize
const randomComponent = (r1.value * 2 - 1) * stepSize * range;
const delta = direction * stepSize * 0.5 + randomComponent;
position = position + delta;
// Clamp to [0, 1]
if (position < 0) position = 0;
if (position > 1) position = 1;
setStep(pattern, i, { trigger: true, pitch: position });
}
// Untriggered steps keep default pitch, trigger=false
}
this._position = position;
return {
patternDesc: pattern,
nextState: { position: position },
};
}
}
// ── 4. Ratchet ──────────────────────────────────────────────────────
export class Ratchet extends Primitive {
constructor() {
super('Ratchet', 'timing', [
{ name: 'maxDivision', default: 0.5, boundary: 'clamp' },
{ name: 'probability', default: 0.5, boundary: 'clamp' },
]);
}
process(params, patternDesc, state, rng) {
const maxDiv = mapToInt(params[0], 1, 4);
const probability = params[1];
const pattern = clonePattern(patternDesc);
let currentRng = rng;
for (let i = 0; i < pattern.stepCount; i++) {
const step = pattern.steps[i];
if (step.trigger) {
const r1 = next(currentRng);
currentRng = r1.nextState;
if (r1.value < probability && maxDiv > 1) {
// Pick a subdivision count in [2, maxDiv]
const r2 = nextInt(currentRng, 2, maxDiv);
currentRng = r2.nextState;
step.subdivisions = r2.value;
}
}
}
return { patternDesc: pattern, nextState: {} };
}
}
// ── 5. SwingGroove ──────────────────────────────────────────────────
export class SwingGroove extends Primitive {
constructor() {
super('SwingGroove', 'timing', [
{ name: 'swingAmount', default: 0.0, boundary: 'clamp' },
{ name: 'swingGrid', default: 0.0, boundary: 'clamp' },
]);
}
process(params, patternDesc, state, rng) {
const swingAmount = params[0];
const pattern = clonePattern(patternDesc);
// Max swing = 0.33 (triplet feel)
const maxOffset = 0.33;
const offset = swingAmount * maxOffset;
// Apply swing to every other step (odd-indexed steps)
for (let i = 1; i < pattern.stepCount; i += 2) {
pattern.steps[i].timeOffset = offset;
}
return { patternDesc: pattern, nextState: {} };
}
}
// ── 6. DensityMorph ─────────────────────────────────────────────────
export class DensityMorph extends Primitive {
constructor() {
super('DensityMorph', 'generator', [
{ name: 'density', default: 0.5, boundary: 'clamp' },
{ name: 'clustering', default: 0.0, boundary: 'clamp' },
]);
}
process(params, patternDesc, state, rng) {
const density = params[0];
const clustering = params[1];
const stepCount = patternDesc.stepCount;
const pattern = createPattern(stepCount);
const numTriggers = Math.floor(density * stepCount);
if (numTriggers <= 0) {
return { patternDesc: pattern, nextState: {} };
}
if (numTriggers >= stepCount) {
for (let i = 0; i < stepCount; i++) {
setStep(pattern, i, { trigger: true });
}
return { patternDesc: pattern, nextState: {} };
}
let currentRng = rng;
if (clustering < 0.01) {
// Even spread: Euclidean-like placement
for (let i = 0; i < numTriggers; i++) {
const idx = Math.floor((i * stepCount) / numTriggers);
setStep(pattern, idx, { trigger: true });
}
} else if (clustering > 0.99) {
// Full clustering: contiguous burst
const r1 = nextInt(currentRng, 0, stepCount - 1);
currentRng = r1.nextState;
const startPos = r1.value;
for (let i = 0; i < numTriggers; i++) {
const idx = (startPos + i) % stepCount;
setStep(pattern, idx, { trigger: true });
}
} else {
// Interpolate: place triggers with clustering-dependent spread
// Use a "center of mass" approach:
// Pick a random center, then distribute triggers around it
// with spread inversely proportional to clustering
const r1 = next(currentRng);
currentRng = r1.nextState;
const center = r1.value * stepCount;
// Spread factor: low clustering = large spread, high = tight
const spreadRadius = (1 - clustering) * stepCount * 0.5;
// Score each step by distance from center (wrapping)
const scores = new Float32Array(stepCount);
for (let i = 0; i < stepCount; i++) {
// Wrapped distance from center
let dist = Math.abs(i - center);
if (dist > stepCount * 0.5) dist = stepCount - dist;
// Add small random jitter to break ties
const r2 = next(currentRng);
currentRng = r2.nextState;
scores[i] = dist / (spreadRadius + 0.001) + r2.value * 0.01;
}
// Select the numTriggers steps with lowest scores
const indices = new Array(stepCount);
for (let i = 0; i < stepCount; i++) indices[i] = i;
indices.sort(function (a, b) { return scores[a] - scores[b]; });
for (let i = 0; i < numTriggers; i++) {
setStep(pattern, indices[i], { trigger: true });
}
}
return { patternDesc: pattern, nextState: {} };
}
}
// ── 7. IntervalLock ─────────────────────────────────────────────────
const SCALES = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], // chromatic
[0, 2, 4, 5, 7, 9, 11], // major
[0, 2, 3, 5, 7, 8, 10], // natural minor
[0, 2, 3, 5, 7, 8, 11], // harmonic minor
[0, 2, 4, 7, 9], // pentatonic major
[0, 3, 5, 7, 10], // pentatonic minor
[0, 3, 5, 6, 7, 10], // blues
[0, 2, 3, 5, 7, 9, 10], // dorian
[0, 2, 4, 5, 7, 9, 10], // mixolydian
[0, 2, 4, 6, 8, 10], // whole tone
[0, 2, 3, 5, 6, 8, 9, 11], // diminished
];
export class IntervalLock extends Primitive {
constructor() {
super('IntervalLock', 'converter', [
{ name: 'root', default: 0.0, boundary: 'clamp' },
{ name: 'mode', default: 0.0, boundary: 'clamp' },
{ name: 'octaveRange', default: 0.25, boundary: 'clamp' },
]);
}
process(params, patternDesc, state, rng) {
const root = mapToInt(params[0], 0, 11);
const scaleIdx = mapToInt(params[1], 0, SCALES.length - 1);
const octaveRange = mapToInt(params[2], 1, 4);
const scale = SCALES[scaleIdx];
const pattern = clonePattern(patternDesc);
// Build the full set of MIDI notes in this scale + root + range
const notes = [];
for (let oct = 0; oct < octaveRange; oct++) {
for (let i = 0; i < scale.length; i++) {
const midiNote = root + scale[i] + oct * 12;
if (midiNote <= 127) {
notes.push(midiNote);
}
}
}
if (notes.length === 0) {
return { patternDesc: pattern, nextState: {} };
}
for (let i = 0; i < pattern.stepCount; i++) {
const step = pattern.steps[i];
// Quantize pitch [0,1] to nearest note in our scale
const targetIdx = Math.round(step.pitch * (notes.length - 1));
const clampedIdx = targetIdx < 0 ? 0 : targetIdx >= notes.length ? notes.length - 1 : targetIdx;
// Store as MIDI note / 127 to stay in [0,1]
step.pitch = notes[clampedIdx] / 127;
}
return { patternDesc: pattern, nextState: {} };
}
}
// ── 8. VelocityShaper ───────────────────────────────────────────────
export class VelocityShaper extends Primitive {
constructor() {
super('VelocityShaper', 'processor', [
{ name: 'curveType', default: 0.0, boundary: 'clamp' },
{ name: 'depth', default: 0.5, boundary: 'clamp' },
{ name: 'phase', default: 0.0, boundary: 'wrap' },
]);
}
process(params, patternDesc, state, rng) {
const curveIdx = mapToInt(params[0], 0, 4);
const depth = params[1];
const phase = params[2];
const pattern = clonePattern(patternDesc);
const stepCount = pattern.stepCount;
let currentRng = rng;
for (let i = 0; i < stepCount; i++) {
const step = pattern.steps[i];
if (!step.trigger) continue;
// Phase-shifted position
const pos = ((i / stepCount) + phase) % 1;
let shapeValue;
switch (curveIdx) {
case 0: // flat
shapeValue = 1.0;
break;
case 1: // accent-every-N (accent every 4th step)
shapeValue = ((i + Math.floor(phase * stepCount)) % 4 === 0) ? 1.0 : 0.5;
break;
case 2: // crescendo
shapeValue = pos;
break;
case 3: // decrescendo
shapeValue = 1.0 - pos;
break;
case 4: { // random
const r1 = next(currentRng);
currentRng = r1.nextState;
shapeValue = r1.value;
break;
}
default:
shapeValue = 1.0;
}
// Apply depth: interpolate between uniform (1.0) and shaped
// depth=0 means all same velocity (base), depth=1 means full shape
const baseVelocity = 0.7;
const shaped = shapeValue;
step.velocity = baseVelocity * (1 - depth) + shaped * depth;
// Clamp
if (step.velocity < 0) step.velocity = 0;
if (step.velocity > 1) step.velocity = 1;
}
return { patternDesc: pattern, nextState: {} };
}
}
// ── Registry ────────────────────────────────────────────────────────
export const PRIMITIVE_REGISTRY = {
EuclideanRhythm: EuclideanRhythm,
ProbabilityGate: ProbabilityGate,
PitchWalker: PitchWalker,
Ratchet: Ratchet,
SwingGroove: SwingGroove,
DensityMorph: DensityMorph,
IntervalLock: IntervalLock,
VelocityShaper: VelocityShaper,
};

View file

@ -1,304 +0,0 @@
/**
* ShapeSeq Engine central orchestrator
*
* Wires together the sequence MLP, param mapping, primitive chain,
* projection layer, clock engine, and C15 bridge.
*
* Main loop (triggered by setSequenceInputs):
* 1. Forward inputs to sequenceIML
* 2. Run MLP inference to get 16 outputs
* 3. Map 16 outputs to N primitive params via param-map
* 4. Evaluate the chain to produce a pattern description
* 5. Apply projection transforms
* 6. Schedule the pattern on the clock
*
* Bridge integration:
* - Subscribes to seq.noteOn / seq.noteOff on the event bus
* - Forwards to C15Bridge.noteOn / noteOff
* - Tracks active notes to avoid orphans
*
* @module shapeseq/sequencer
*/
import { createSequenceIML, SEQ_N_OUTPUTS } from './seq-iml.js';
import { Chain } from './chain.js';
import { ClockEngine } from './clock.js';
import { map } from './param-map.js';
import { createProjectionChain, applyProjection, PRESETS } from './projection.js';
import { SEQ } from './event-bus.js';
import {
EuclideanRhythm,
ProbabilityGate,
PitchWalker,
IntervalLock,
VelocityShaper,
} from './primitives.js';
// ── Defaults ─────────────────────────────────────────────────────────
const DEFAULT_BPM = 120;
const DEFAULT_STEP_COUNT = 8;
const DEFAULT_MASTER_SEED = 42;
const DEFAULT_SPREAD = 0.6;
// ── ShapeSeqEngine ───────────────────────────────────────────────────
export class ShapeSeqEngine {
/**
* @param {{ audioContext: AudioContext, eventBus: import('./event-bus.js').EventBus, c15Bridge: import('../synth/c15-bridge.js').C15Bridge }} opts
*/
constructor({ audioContext, eventBus, c15Bridge }) {
if (!audioContext) throw new TypeError('ShapeSeqEngine requires an audioContext');
if (!eventBus) throw new TypeError('ShapeSeqEngine requires an eventBus');
if (!c15Bridge) throw new TypeError('ShapeSeqEngine requires a c15Bridge');
/** @private */ this._audioCtx = audioContext;
/** @private */ this._bus = eventBus;
/** @private */ this._c15 = c15Bridge;
/** @private */ this._sequenceIML = null;
/** @private */ this._chain = null;
/** @private */ this._clock = null;
/** @private */ this._projectionChain = null;
/** @private */ this._stepCount = DEFAULT_STEP_COUNT;
/** @private */ this._masterSeed = DEFAULT_MASTER_SEED;
/** @private */ this._playing = false;
/** @private */ this._initialized = false;
// Track active notes for orphan prevention
/** @private @type {Set<number>} */
this._activeNotes = new Set();
// Bound handlers for event bus (stored for cleanup)
/** @private */
this._onNoteOn = (data) => this._handleNoteOn(data);
/** @private */
this._onNoteOff = (data) => this._handleNoteOff(data);
}
// ── Lifecycle ──────────────────────────────────────────────────────
/**
* Initialize all subsystems: create sequence IML, default chain,
* clock, and projection chain. Must be called before start().
*/
async init() {
// 1. Create the sequence MLP
this._sequenceIML = await createSequenceIML();
// Randomize weights with default spread
this._sequenceIML.drawWeights(DEFAULT_SPREAD);
// 2. Create the default primitive chain
this._chain = new Chain();
this._chain.addPrimitive(new EuclideanRhythm());
this._chain.addPrimitive(new ProbabilityGate());
this._chain.addPrimitive(new PitchWalker());
this._chain.addPrimitive(new IntervalLock());
this._chain.addPrimitive(new VelocityShaper());
this._chain.setMasterSeed(this._masterSeed);
// 3. Set up the clock
this._clock = new ClockEngine(this._audioCtx, this._bus);
this._clock.bpm = DEFAULT_BPM;
// 4. Create default projection chain (expressive preset)
const result = createProjectionChain(PRESETS.expressive);
if (!result.valid) {
throw new Error('Default projection chain invalid: ' + result.error);
}
this._projectionChain = result;
// 5. Subscribe to event bus for C15 bridge integration
this._bus.on(SEQ.NOTE_ON, this._onNoteOn);
this._bus.on(SEQ.NOTE_OFF, this._onNoteOff);
this._initialized = true;
}
/**
* Start the clock. Requires init() to have been called.
*/
start() {
if (!this._initialized) {
throw new Error('ShapeSeqEngine.start() called before init()');
}
if (this._playing) return;
this._playing = true;
this._clock.start();
}
/**
* Stop the clock and release all active notes.
*/
stop() {
if (!this._playing) return;
this._playing = false;
this._clock.stop();
this._releaseAllNotes();
}
/**
* Full cleanup: stop playback, unsubscribe from events, destroy IML.
*/
destroy() {
this.stop();
// Unsubscribe from event bus
this._bus.off(SEQ.NOTE_ON, this._onNoteOn);
this._bus.off(SEQ.NOTE_OFF, this._onNoteOff);
// Destroy the sequence IML instance
if (this._sequenceIML) {
this._sequenceIML.destroy();
this._sequenceIML = null;
}
this._chain = null;
this._clock = null;
this._projectionChain = null;
this._initialized = false;
}
// ── Configuration ──────────────────────────────────────────────────
/**
* Update the clock tempo.
* @param {number} bpm
*/
setTempo(bpm) {
if (this._clock) {
this._clock.setTempo(bpm);
}
}
/**
* Set the number of steps in the generated pattern.
* @param {number} count
*/
setStepCount(count) {
const c = Math.max(1, count | 0);
this._stepCount = c;
}
/**
* Set the projection preset by name.
* @param {'expressive'|'percussive'|'fullRange'} presetName
*/
setProjectionPreset(presetName) {
const preset = PRESETS[presetName];
if (!preset) {
throw new Error('Unknown projection preset: ' + presetName);
}
const result = createProjectionChain(preset);
if (!result.valid) {
throw new Error('Projection chain invalid: ' + result.error);
}
this._projectionChain = result;
}
// ── Chain access (for UI binding) ──────────────────────────────────
/** @returns {Chain} */
getChain() { return this._chain; }
/** @returns {ClockEngine} */
getClock() { return this._clock; }
/** @returns {WasmIML} */
getSequenceIML() { return this._sequenceIML; }
// ── Input routing ──────────────────────────────────────────────────
/**
* Feed new input values to the sequence MLP and run the full pipeline:
* MLP inference -> param mapping -> chain evaluation -> projection -> clock scheduling.
*
* Call this each frame with the routed input values (e.g., [x, y]).
*
* @param {number[]} values - input array (typically [x, y])
*/
setSequenceInputs(values) {
if (!this._initialized || !this._sequenceIML) return;
// 1. Forward inputs to the sequence IML
this._sequenceIML.setInputs(values);
// 2. Run MLP inference
this._sequenceIML.process();
// 3. Get the 16 MLP outputs
const mlpOutputs = this._sequenceIML.getOutputs();
// 4. Map 16 outputs to N primitive params
const paramCount = this._chain.totalParamCount;
const mappedParams = map(mlpOutputs, paramCount);
// 5. Evaluate the chain to produce a pattern description
const patternDesc = this._chain.evaluate(mappedParams, this._stepCount, this._masterSeed);
// 6. Apply projection transforms
const projectedPattern = applyProjection(this._projectionChain, patternDesc);
// 7. Schedule the pattern on the clock
this._clock.schedulePattern(projectedPattern);
}
// ── ML control ─────────────────────────────────────────────────────
/** @returns {boolean} */
get isPlaying() {
return this._playing;
}
// ── Bridge integration (private) ───────────────────────────────────
/**
* Handle seq.noteOn events from the event bus.
* Converts [0,1] pitch to MIDI note number and forwards to C15.
*
* @private
* @param {Object} data - { pitch, velocity, stepIndex, time, accent, isSubdivision }
*/
_handleNoteOn(data) {
// pitch comes from the projection layer; after RangeMap it's already
// in MIDI note range (e.g., 48-84). Round to nearest integer.
const midiNote = Math.round(data.pitch) | 0;
const velocity = data.velocity;
// Clamp to valid MIDI range
const note = midiNote < 0 ? 0 : midiNote > 127 ? 127 : midiNote;
const vel = velocity < 0 ? 0 : velocity > 1 ? 1 : velocity;
this._c15.noteOn(note, vel);
this._activeNotes.add(note);
}
/**
* Handle seq.noteOff events from the event bus.
*
* @private
* @param {Object} data - { pitch, velocity, stepIndex, time }
*/
_handleNoteOff(data) {
const midiNote = Math.round(data.pitch) | 0;
const note = midiNote < 0 ? 0 : midiNote > 127 ? 127 : midiNote;
this._c15.noteOff(note);
this._activeNotes.delete(note);
}
/**
* Release all currently active notes to avoid orphaned noteOns.
* @private
*/
_releaseAllNotes() {
for (const note of this._activeNotes) {
this._c15.noteOff(note);
}
this._activeNotes.clear();
}
}

View file

@ -1,328 +0,0 @@
/**
* ShapeSeq Circular Step Visualizer
*
* Renders steps arranged in a circle (heptagon, tridecagon, etc.)
* with pitch mapped to radial distance, velocity to node size,
* and accent to color brightness.
*
* Designed for 60fps rendering no allocations in the render loop.
* Port-ready: explicit state, no closures in hot paths.
*
* @module shapeseq/step-viz
*/
import { SEQ } from './event-bus.js';
// ── Constants (pre-allocated, shared across instances) ──────────────
const TWO_PI = Math.PI * 2;
const HALF_PI = Math.PI * 0.5;
// Color constants
const COLOR_INACTIVE = 'rgba(255, 255, 255, 0.15)';
const COLOR_ACTIVE = '#00ccff';
const COLOR_CURRENT = '#ff6a00';
const COLOR_ACCENT = '#ffcc00';
const COLOR_BG = '#0d0d0d';
// Glow colors (pre-computed rgba strings)
const GLOW_ACTIVE = 'rgba(0, 204, 255, 0.3)';
const GLOW_CURRENT = 'rgba(255, 106, 0, 0.4)';
const GLOW_ACCENT = 'rgba(255, 204, 0, 0.35)';
// Layout
const PADDING_RATIO = 0.08; // canvas padding as fraction of min dimension
const OUTER_RADIUS_RATIO = 0.90; // outer ring at 90% of available radius
const INNER_RADIUS_RATIO = 0.30; // inner ring at 30% of available radius
// Node sizing
const NODE_MIN_RADIUS = 4;
const NODE_MAX_RADIUS = 18;
const NODE_OUTLINE_WIDTH = 1.5;
// Current-step indicator
const INDICATOR_EXTRA_RADIUS = 8;
const INDICATOR_LINE_WIDTH = 2;
// Center dot
const CENTER_DOT_RADIUS = 3;
// ── StepVisualizer ──────────────────────────────────────────────────
export class StepVisualizer {
/**
* @param {{ canvas: HTMLCanvasElement, eventBus: import('./event-bus.js').EventBus }} opts
*/
constructor({ canvas, eventBus }) {
this._canvas = canvas;
this._ctx = canvas.getContext('2d');
this._bus = eventBus;
// State
this._pattern = null; // current pattern description
this._currentStep = -1; // playback position (-1 = none)
this._width = 0;
this._height = 0;
this._cx = 0; // center x
this._cy = 0; // center y
this._maxRadius = 0; // max ring radius in pixels
// Pre-allocated arrays to avoid per-frame allocation.
// Sized lazily when pattern is set.
this._nodeX = null; // Float64Array — screen x per step
this._nodeY = null; // Float64Array — screen y per step
this._nodeR = null; // Float64Array — rendered radius per step
// Interaction
this._tapCallback = null;
this._onPointerDown = this._handlePointerDown.bind(this);
// Event bus subscription
this._onStep = this._handleStep.bind(this);
this._bus.on(SEQ.STEP, this._onStep);
// Canvas interaction
this._canvas.addEventListener('pointerdown', this._onPointerDown);
// Initial sizing
this.resize(canvas.getBoundingClientRect().width, canvas.getBoundingClientRect().height);
}
// ── Public API ──────────────────────────────────────────────────
/**
* Update the displayed pattern.
* @param {{ steps: Array, stepCount: number, metadata: Object }} patternDesc
*/
setPattern(patternDesc) {
this._pattern = patternDesc;
const count = patternDesc ? patternDesc.stepCount : 0;
// (Re)allocate coordinate buffers only when step count changes
if (!this._nodeX || this._nodeX.length !== count) {
this._nodeX = new Float64Array(count);
this._nodeY = new Float64Array(count);
this._nodeR = new Float64Array(count);
}
this._computeLayout();
}
/**
* Update the playback position.
* @param {number} index step index (0-based), or -1 for none
*/
setCurrentStep(index) {
this._currentStep = index;
}
/**
* Draw one frame. Call from requestAnimationFrame.
*/
render() {
const ctx = this._ctx;
const w = this._width;
const h = this._height;
// Clear
ctx.fillStyle = COLOR_BG;
ctx.fillRect(0, 0, w, h);
if (!this._pattern || this._pattern.stepCount === 0) return;
const steps = this._pattern.steps;
const count = this._pattern.stepCount;
const cx = this._cx;
const cy = this._cy;
// Draw connecting ring (subtle guide circle at midpoint radius)
const midRadius = this._maxRadius * ((OUTER_RADIUS_RATIO + INNER_RADIUS_RATIO) * 0.5);
ctx.beginPath();
ctx.arc(cx, cy, midRadius, 0, TWO_PI);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
ctx.lineWidth = 1;
ctx.stroke();
// Center dot
ctx.beginPath();
ctx.arc(cx, cy, CENTER_DOT_RADIUS, 0, TWO_PI);
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
ctx.fill();
// Draw connector line from center to current step
if (this._currentStep >= 0 && this._currentStep < count) {
const si = this._currentStep;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(this._nodeX[si], this._nodeY[si]);
ctx.strokeStyle = 'rgba(255, 106, 0, 0.2)';
ctx.lineWidth = INDICATOR_LINE_WIDTH;
ctx.stroke();
}
// Draw step nodes
for (let i = 0; i < count; i++) {
const step = steps[i];
const nx = this._nodeX[i];
const ny = this._nodeY[i];
const nr = this._nodeR[i];
const isCurrent = i === this._currentStep;
if (isCurrent) {
// Outer glow for current step
ctx.beginPath();
ctx.arc(nx, ny, nr + INDICATOR_EXTRA_RADIUS, 0, TWO_PI);
ctx.fillStyle = GLOW_CURRENT;
ctx.fill();
}
if (step.trigger) {
// Glow behind active nodes
if (!isCurrent) {
const glowColor = step.accent ? GLOW_ACCENT : GLOW_ACTIVE;
ctx.beginPath();
ctx.arc(nx, ny, nr + 4, 0, TWO_PI);
ctx.fillStyle = glowColor;
ctx.fill();
}
// Filled node
ctx.beginPath();
ctx.arc(nx, ny, nr, 0, TWO_PI);
if (isCurrent) {
ctx.fillStyle = COLOR_CURRENT;
} else if (step.accent) {
ctx.fillStyle = COLOR_ACCENT;
} else {
ctx.fillStyle = COLOR_ACTIVE;
}
ctx.fill();
} else {
// Dim outline only for untriggered steps
ctx.beginPath();
ctx.arc(nx, ny, nr, 0, TWO_PI);
ctx.strokeStyle = isCurrent ? COLOR_CURRENT : COLOR_INACTIVE;
ctx.lineWidth = NODE_OUTLINE_WIDTH;
ctx.stroke();
}
}
}
/**
* Handle canvas resize.
* @param {number} width CSS pixels
* @param {number} height CSS pixels
*/
resize(width, height) {
const dpr = window.devicePixelRatio || 1;
this._canvas.width = width * dpr;
this._canvas.height = height * dpr;
this._ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
this._width = width;
this._height = height;
this._cx = width * 0.5;
this._cy = height * 0.5;
const minDim = Math.min(width, height);
this._maxRadius = (minDim * 0.5) * (1 - PADDING_RATIO * 2);
this._computeLayout();
}
/**
* Register a tap callback.
* @param {function(number): void} callback receives step index
*/
onStepTap(callback) {
this._tapCallback = callback;
}
/**
* Unsubscribe from event bus and remove DOM listeners.
*/
destroy() {
this._bus.off(SEQ.STEP, this._onStep);
this._canvas.removeEventListener('pointerdown', this._onPointerDown);
this._tapCallback = null;
this._pattern = null;
}
// ── Private ─────────────────────────────────────────────────────
/**
* Re-compute node positions from current pattern + canvas size.
* Called when pattern or size changes NOT per frame.
*/
_computeLayout() {
if (!this._pattern || !this._nodeX) return;
const steps = this._pattern.steps;
const count = this._pattern.stepCount;
const cx = this._cx;
const cy = this._cy;
const maxR = this._maxRadius;
const outerR = maxR * OUTER_RADIUS_RATIO;
const innerR = maxR * INNER_RADIUS_RATIO;
const radiusRange = outerR - innerR;
// Angular step: start at top (-PI/2), go clockwise
const angleStep = TWO_PI / count;
for (let i = 0; i < count; i++) {
const step = steps[i];
const angle = -HALF_PI + angleStep * i;
// Pitch -> radial distance: low pitch = outer, high pitch = inner
const pitchNorm = step.pitch; // 0 = low (outer), 1 = high (inner)
const r = outerR - pitchNorm * radiusRange;
this._nodeX[i] = cx + Math.cos(angle) * r;
this._nodeY[i] = cy + Math.sin(angle) * r;
// Velocity -> node size
this._nodeR[i] = NODE_MIN_RADIUS + step.velocity * (NODE_MAX_RADIUS - NODE_MIN_RADIUS);
}
}
/**
* Handle seq.step events from the event bus.
*/
_handleStep(data) {
if (typeof data.stepIndex === 'number') {
this._currentStep = data.stepIndex;
}
}
/**
* Handle pointer down on the canvas for tap interaction.
*/
_handlePointerDown(e) {
if (!this._tapCallback || !this._pattern) return;
const rect = this._canvas.getBoundingClientRect();
const px = e.clientX - rect.left;
const py = e.clientY - rect.top;
const count = this._pattern.stepCount;
// Find closest step within hit radius
let bestIdx = -1;
let bestDistSq = Infinity;
for (let i = 0; i < count; i++) {
const dx = px - this._nodeX[i];
const dy = py - this._nodeY[i];
const distSq = dx * dx + dy * dy;
// Hit area is the node radius + some tolerance
const hitR = this._nodeR[i] + 12;
if (distSq < hitR * hitR && distSq < bestDistSq) {
bestDistSq = distSq;
bestIdx = i;
}
}
if (bestIdx >= 0) {
this._tapCallback(bestIdx);
}
}
}

View file

@ -16,9 +16,9 @@ name: CI
on:
push:
branches: [main, port-solidjs]
branches: [main]
pull_request:
branches: [main, port-solidjs]
branches: [main]
workflow_dispatch:
jobs:

View file

@ -12,7 +12,7 @@ name: VCV Rack plugin (cross-platform)
on:
push:
branches: [main, feat/manifold-mission, feat/vcv-dist]
branches: [main]
paths:
- 'vcv/**'
- '.github/workflows/vcv-plugin.yml'

View file

@ -1,140 +0,0 @@
# NISPS Core Extraction Plan
Extract a platform-agnostic C++20 controller library from MEMLNaut-NISPS. This is **not** a synth or audio engine - it's a parameter mapping engine: control data in → ML → control data out. Use it to drive synths, effects, lights, robots, whatever.
> **Note**: Originally planned as C++17, upgraded to C++20 during implementation to use `std::span` for efficient array views.
## What This Is
NISPS core takes N input parameters (joystick position, sensor data, audio features) and maps them to M output parameters through an interactively-trained neural network. Users teach it by example: "when I'm here in input space, I want these output values."
## Dependencies to Remove
| Dependency | Replacement |
|------------|-------------|
| `Serial.print*` | Optional log callback |
| `queue_t` (Pico SDK) | Not needed (single-threaded) |
| `WString.h` (Arduino) | `std::string` |
| `__force_inline`, `AUDIO_MEM` | No-op macros |
## Structure
```
nisps/
├── mlp.hpp # MLP implementation (from memlp, cleaned)
├── dataset.hpp # Training dataset
├── iml.hpp # Interactive ML engine (~200 lines)
└── voice_space.hpp # Optional: example parameter mappings
```
## Core API
```cpp
namespace nisps {
template<typename Float = float>
class IML {
public:
IML(size_t n_inputs, size_t n_outputs,
std::vector<size_t> hidden_layers = {10, 10, 14});
// Input
void set_input(size_t index, Float value);
void set_inputs(const Float* values, size_t count);
// Output (valid after process())
const Float* get_outputs() const;
size_t num_outputs() const;
// Runtime
void process(); // Run inference, call at control rate
// Training workflow
enum class Mode { Inference, Training };
void set_mode(Mode mode);
void save_example(); // Store current input→output as training pair
void clear_dataset();
void randomise_weights(); // For exploration in training mode
void train(); // Blocking, runs on current dataset
// Optional
using LogFn = void(*)(const char*);
void set_logger(LogFn fn);
};
} // namespace nisps
```
## Usage
```cpp
#include "nisps/iml.hpp"
nisps::IML<float> iml(3, 24); // 3 inputs (x,y,z), 24 outputs
// Control loop
void update(float x, float y, float z) {
iml.set_input(0, x);
iml.set_input(1, y);
iml.set_input(2, z);
iml.process();
const float* params = iml.get_outputs();
my_synth.set_filter_cutoff(params[0] * 10000.f);
my_synth.set_resonance(params[1]);
// ... etc
}
// Training (triggered by user interaction)
void on_user_saves_position() {
iml.save_example();
}
void on_user_exits_training_mode() {
iml.set_mode(nisps::IML<>::Mode::Inference);
// This triggers training internally
}
```
## Voice Spaces (Optional)
Voice spaces are just functions that interpret the raw 0-1 output parameters. Not part of core, but useful as examples:
```cpp
// User-defined mapping
void apply_neve_style(const float* params, MyChannelStrip& strip) {
strip.pre_gain = 0.5f + params[0] * params[0] * 4.f;
strip.hp_freq = 30.f + params[8] * params[8] * 270.f;
strip.comp_threshold = 20.f + params[10] * -40.f;
// ... etc
}
```
## Phases
### Phase 1: Get memlp building standalone (1-2 days)
1. Copy `memlp` source into `nisps/`
2. Remove `Serial.print` calls (or stub them)
3. Remove Arduino `String` usage
4. Verify it compiles with g++/clang
### Phase 2: Wrap in IML interface (2-3 days)
1. Create `iml.hpp` with the API above
2. Port state machine logic from `IMLInterface.hpp`
3. Simple test: train on XOR, verify inference works
### Phase 3: Example integration (1-2 days)
1. Command-line example that reads CSV input, outputs CSV
2. Or: minimal JUCE/SDL example with mouse input
**Total: ~1 week to something usable**
## Later (only if needed)
- Model serialization (save/load trained weights)
- Thread-safe parameter updates
- Python bindings
- WASM build

View file

@ -1,678 +0,0 @@
# NISPS Core Extraction - Task Graph
Atomic tasks for extracting nisps-core. Each task is standalone and requires no decisions.
---
## Repository Setup
### TASK-001: Create nisps-core directory structure
**Blocked by**: None
**Description**: Create the nisps-core directory with the required structure.
**Actions**:
1. Create directory: `nisps-core/`
2. Create directory: `nisps-core/include/nisps/`
3. Create directory: `nisps-core/test/`
4. Create directory: `nisps-core/examples/`
**Verification**: Directories exist.
### TASK-002: Create CMakeLists.txt for nisps-core
**Blocked by**: TASK-001
**Description**: Create a minimal CMakeLists.txt that builds the library as header-only with test target.
**Actions**: Create `nisps-core/CMakeLists.txt` with this exact content:
```cmake
cmake_minimum_required(VERSION 3.14)
project(nisps-core VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Header-only library
add_library(nisps INTERFACE)
target_include_directories(nisps INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# Tests
option(NISPS_BUILD_TESTS "Build tests" ON)
if(NISPS_BUILD_TESTS)
enable_testing()
add_subdirectory(test)
endif()
```
**Verification**: File exists with exact content.
### TASK-003: Create test CMakeLists.txt
**Blocked by**: TASK-002
**Description**: Create test/CMakeLists.txt for building tests.
**Actions**: Create `nisps-core/test/CMakeLists.txt` with this exact content:
```cmake
add_executable(nisps_test main.cpp)
target_link_libraries(nisps_test PRIVATE nisps)
add_test(NAME nisps_test COMMAND nisps_test)
```
**Verification**: File exists with exact content.
### TASK-004: Create placeholder test file
**Blocked by**: TASK-003
**Description**: Create a minimal test file that includes the main header.
**Actions**: Create `nisps-core/test/main.cpp` with this exact content:
```cpp
#include <nisps/nisps.hpp>
#include <iostream>
int main() {
std::cout << "nisps-core tests placeholder\n";
return 0;
}
```
**Verification**: File exists with exact content.
---
## Phase 1: Extract MLP Core
### TASK-010: Copy Utils.h with Arduino code removed
**Blocked by**: TASK-001
**Description**: Copy `src/memlp/Utils.h` to `nisps-core/include/nisps/utils.hpp`, removing Arduino-specific code.
**Actions**:
1. Copy the file
2. Remove the `#ifdef ARDUINO` block (lines related to ENABLE_SAVE)
3. Remove the `extern "C" int getentropy` declaration at the end
4. Change header guard from `UTILS_H` to `NISPS_UTILS_HPP`
5. Wrap everything in `namespace nisps { ... }`
**Changes to make** (be exact):
- Line 1: Change `#ifndef UTILS_H` to `#ifndef NISPS_UTILS_HPP`
- Line 2: Change `#define UTILS_H` to `#define NISPS_UTILS_HPP`
- Remove lines 38-45 (the `#if defined(_WIN32)...` and `#ifdef ARDUINO` ENABLE_SAVE blocks)
- After line 57 (`namespace utils {`), keep as-is (utils is a sub-namespace)
- Before the final `#endif`, add closing brace for nisps namespace
- Remove the last line `extern "C" int getentropy...`
- Add `namespace nisps {` after the includes, before `enum ACTIVATION_FUNCTIONS`
- Add `} // namespace nisps` before `#endif // NISPS_UTILS_HPP`
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only nisps-core/include/nisps/utils.hpp`
### TASK-011: Copy Loss.h
**Blocked by**: TASK-001
**Description**: Copy `src/memlp/Loss.h` to `nisps-core/include/nisps/loss.hpp`.
**Actions**:
1. Copy the file
2. Change header guard from `LOSS_H` to `NISPS_LOSS_HPP`
3. Wrap everything in `namespace nisps { ... }`
4. Update include from `"Utils.h"` to `"utils.hpp"`
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only -I nisps-core/include nisps-core/include/nisps/loss.hpp`
### TASK-012: Copy Node.h
**Blocked by**: TASK-010
**Description**: Copy `src/memlp/Node.h` to `nisps-core/include/nisps/node.hpp`.
**Actions**:
1. Copy the file
2. Change header guard from `NODE_H` to `NISPS_NODE_HPP`
3. Wrap everything in `namespace nisps { ... }`
4. Update include from `"Utils.h"` to `"utils.hpp"`
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only -I nisps-core/include nisps-core/include/nisps/node.hpp`
### TASK-013: Copy Layer.h
**Blocked by**: TASK-012
**Description**: Copy `src/memlp/Layer.h` to `nisps-core/include/nisps/layer.hpp`.
**Actions**:
1. Copy the file
2. Change header guard from `LAYER_H` to `NISPS_LAYER_HPP`
3. Wrap everything in `namespace nisps { ... }`
4. Update include from `"Node.h"` to `"node.hpp"`
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only -I nisps-core/include nisps-core/include/nisps/layer.hpp`
### TASK-014: Copy Sample.h
**Blocked by**: TASK-001
**Description**: Copy `src/memlp/Sample.h` to `nisps-core/include/nisps/sample.hpp`.
**Actions**:
1. Copy the file
2. Change header guard from `SAMPLE_H` to `NISPS_SAMPLE_HPP`
3. Wrap everything in `namespace nisps { ... }`
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only nisps-core/include/nisps/sample.hpp`
### TASK-015: Copy MLP.h with Arduino code removed
**Blocked by**: TASK-011, TASK-013, TASK-014
**Description**: Copy `src/memlp/MLP.h` to `nisps-core/include/nisps/mlp.hpp`, removing Arduino-specific code.
**Actions**:
1. Copy the file
2. Change header guard from `MLP_H` to `NISPS_MLP_HPP`
3. **Remove lines 17-51** (the entire `#ifdef ARDUINO` block including Serial debug macros and SD includes)
4. Add these lines after the header guard instead:
```cpp
// Debug macros (no-op by default, override before including if needed)
#ifndef NISPS_DEBUG_PRINT
#define NISPS_DEBUG_PRINT(...)
#define NISPS_DEBUG_PRINTLN(...)
#define NISPS_DEBUG_PRINTF(...)
#endif
```
5. Update includes:
- `"Layer.h"``"layer.hpp"`
- `"Utils.h"``"utils.hpp"`
- `"Loss.h"``"loss.hpp"`
- `"Sample.h"``"sample.hpp"`
6. Remove `#if ENABLE_SAVE` blocks (lines 94-96, 99-113, 115-119) - remove the conditionals but **keep** the function declarations
7. Remove `#if ENABLE_SAVE_SD` block entirely (lines 115-119)
8. Wrap everything in `namespace nisps { ... }`
9. Replace `MLP_DEBUG_PRINT` with `NISPS_DEBUG_PRINT`, `MLP_DEBUG_PRINTLN` with `NISPS_DEBUG_PRINTLN`, `MLP_DEBUG_PRINTF` with `NISPS_DEBUG_PRINTF`
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only -I nisps-core/include nisps-core/include/nisps/mlp.hpp`
### TASK-016: Copy MLP.cpp with Arduino code removed
**Blocked by**: TASK-015
**Description**: Copy `src/memlp/MLP.cpp` to `nisps-core/include/nisps/mlp_impl.hpp` as inline implementation.
**Actions**:
1. Copy the file
2. Remove `#include "MLP.h"` (it will be included from mlp.hpp)
3. Add header guard `#ifndef NISPS_MLP_IMPL_HPP` / `#define NISPS_MLP_IMPL_HPP`
4. Wrap everything in `namespace nisps { ... }`
5. Remove all `#ifdef ARDUINO` / `#if ENABLE_SAVE` / `#if ENABLE_SAVE_SD` conditional blocks
6. For functions inside removed conditionals: keep the functions but remove the `#if` guards
7. Replace any `Serial.print*` calls with `NISPS_DEBUG_PRINT*` equivalents
8. At the end of `nisps-core/include/nisps/mlp.hpp`, add: `#include "mlp_impl.hpp"`
**Verification**: A test file that instantiates `nisps::MLP<float>` compiles and links.
### TASK-017: Copy Dataset.hpp
**Blocked by**: TASK-001
**Description**: Copy `src/memlp/Dataset.hpp` to `nisps-core/include/nisps/dataset.hpp`.
**Actions**:
1. Copy the file
2. Change header guard from `__DATASET_HPP__` to `NISPS_DATASET_HPP`
3. Wrap everything in `namespace nisps { ... }`
**Verification**: File compiles with `g++ -std=c++17 -fsyntax-only nisps-core/include/nisps/dataset.hpp`
### TASK-018: Copy Dataset.cpp as inline implementation
**Blocked by**: TASK-017
**Description**: Copy `src/memlp/Dataset.cpp` to `nisps-core/include/nisps/dataset_impl.hpp`.
**Actions**:
1. Copy the file
2. Remove `#include "Dataset.hpp"`
3. Add header guard `#ifndef NISPS_DATASET_IMPL_HPP` / `#define NISPS_DATASET_IMPL_HPP`
4. Wrap everything in `namespace nisps { ... }`
5. Make all functions `inline`
6. At the end of `nisps-core/include/nisps/dataset.hpp`, add: `#include "dataset_impl.hpp"`
**Verification**: A test file that instantiates `nisps::Dataset` compiles and links.
---
## Phase 2: Create IML Interface
### TASK-020: Create iml.hpp header
**Blocked by**: TASK-016, TASK-018
**Description**: Create the main IML class header based on IMLInterface.hpp.
**Actions**: Create `nisps-core/include/nisps/iml.hpp` with this exact content:
```cpp
#ifndef NISPS_IML_HPP
#define NISPS_IML_HPP
#include "mlp.hpp"
#include "dataset.hpp"
#include <vector>
#include <cstddef>
#include <functional>
namespace nisps {
template<typename Float = float>
class IML {
public:
enum class Mode { Inference, Training };
using LogFn = void(*)(const char*);
IML(size_t n_inputs, size_t n_outputs,
std::vector<size_t> hidden_layers = {10, 10, 14},
size_t max_iterations = 1000,
Float learning_rate = 1.0f,
Float convergence_threshold = 0.00001f);
// Input
void set_input(size_t index, Float value);
void set_inputs(const Float* values, size_t count);
// Output (valid after process())
const Float* get_outputs() const;
size_t num_inputs() const { return n_inputs_; }
size_t num_outputs() const { return n_outputs_; }
// Runtime
void process();
// Training workflow
void set_mode(Mode mode);
Mode get_mode() const { return mode_; }
void save_example();
void clear_dataset();
void randomise_weights();
// Optional logging
void set_logger(LogFn fn) { log_fn_ = fn; }
private:
void log(const char* msg) const {
if (log_fn_) log_fn_(msg);
}
void train();
size_t n_inputs_;
size_t n_outputs_;
size_t max_iterations_;
Float learning_rate_;
Float convergence_threshold_;
Mode mode_ = Mode::Inference;
bool input_updated_ = false;
bool perform_inference_ = true;
std::vector<Float> input_state_;
std::vector<Float> output_state_;
std::unique_ptr<Dataset> dataset_;
std::unique_ptr<MLP<Float>> mlp_;
typename MLP<Float>::mlp_weights stored_weights_;
bool weights_randomised_ = false;
LogFn log_fn_ = nullptr;
};
} // namespace nisps
#include "iml_impl.hpp"
#endif // NISPS_IML_HPP
```
**Verification**: File exists with exact content.
### TASK-021: Create iml_impl.hpp implementation
**Blocked by**: TASK-020
**Description**: Create the IML implementation file based on IMLInterface.hpp logic.
**Actions**: Create `nisps-core/include/nisps/iml_impl.hpp` with this exact content:
```cpp
#ifndef NISPS_IML_IMPL_HPP
#define NISPS_IML_IMPL_HPP
namespace nisps {
template<typename Float>
IML<Float>::IML(size_t n_inputs, size_t n_outputs,
std::vector<size_t> hidden_layers,
size_t max_iterations,
Float learning_rate,
Float convergence_threshold)
: n_inputs_(n_inputs)
, n_outputs_(n_outputs)
, max_iterations_(max_iterations)
, learning_rate_(learning_rate)
, convergence_threshold_(convergence_threshold)
{
// Build layer sizes: input + hidden + output
const size_t kBias = 1;
std::vector<size_t> layer_sizes;
layer_sizes.push_back(n_inputs + kBias);
for (size_t h : hidden_layers) {
layer_sizes.push_back(h);
}
layer_sizes.push_back(n_outputs);
// Activation functions: RELU for hidden, SIGMOID for output
std::vector<ACTIVATION_FUNCTIONS> activations;
for (size_t i = 0; i < hidden_layers.size(); ++i) {
activations.push_back(RELU);
}
activations.push_back(SIGMOID);
dataset_ = std::make_unique<Dataset>();
mlp_ = std::make_unique<MLP<Float>>(
layer_sizes,
activations,
loss::LOSS_MSE,
false, // use_constant_weight_init
0.0f // constant_weight_init
);
input_state_.resize(n_inputs, static_cast<Float>(0.5));
output_state_.resize(n_outputs, static_cast<Float>(0));
}
template<typename Float>
void IML<Float>::set_input(size_t index, Float value) {
if (index >= n_inputs_) return;
if (value < 0) value = 0;
if (value > 1) value = 1;
input_state_[index] = value;
input_updated_ = true;
}
template<typename Float>
void IML<Float>::set_inputs(const Float* values, size_t count) {
for (size_t i = 0; i < count && i < n_inputs_; ++i) {
set_input(i, values[i]);
}
}
template<typename Float>
const Float* IML<Float>::get_outputs() const {
return output_state_.data();
}
template<typename Float>
void IML<Float>::process() {
if (!perform_inference_ || !input_updated_) return;
// Add bias term
std::vector<Float> input_with_bias = input_state_;
input_with_bias.push_back(static_cast<Float>(1.0));
// Run inference
std::vector<Float> output(n_outputs_);
mlp_->GetOutput(input_with_bias, &output);
output_state_ = output;
input_updated_ = false;
}
template<typename Float>
void IML<Float>::set_mode(Mode mode) {
if (mode == Mode::Inference && mode_ == Mode::Training) {
train();
}
mode_ = mode;
}
template<typename Float>
void IML<Float>::save_example() {
// First call: stop inference, user will position output
if (perform_inference_) {
perform_inference_ = false;
log("Move to desired output position...");
return;
}
// Second call: store the example
dataset_->Add(input_state_, output_state_);
perform_inference_ = true;
// Run inference with new example
std::vector<Float> input_with_bias = input_state_;
input_with_bias.push_back(static_cast<Float>(1.0));
std::vector<Float> output(n_outputs_);
mlp_->GetOutput(input_with_bias, &output);
output_state_ = output;
log("Example saved.");
}
template<typename Float>
void IML<Float>::clear_dataset() {
if (mode_ == Mode::Training) {
dataset_->Clear();
log("Dataset cleared.");
}
}
template<typename Float>
void IML<Float>::randomise_weights() {
if (mode_ == Mode::Training) {
stored_weights_ = mlp_->GetWeights();
mlp_->DrawWeights();
weights_randomised_ = true;
// Run inference to show effect
std::vector<Float> input_with_bias = input_state_;
input_with_bias.push_back(static_cast<Float>(1.0));
std::vector<Float> output(n_outputs_);
mlp_->GetOutput(input_with_bias, &output);
output_state_ = output;
log("Weights randomised.");
}
}
template<typename Float>
void IML<Float>::train() {
// Restore weights if they were randomised
if (weights_randomised_) {
mlp_->SetWeights(stored_weights_);
weights_randomised_ = false;
}
auto features = dataset_->GetFeatures(true); // with bias
auto& labels = dataset_->GetLabels();
if (features.empty() || labels.empty()) {
log("Empty dataset, skipping training.");
return;
}
typename MLP<Float>::training_pair_t training_data(features, labels);
log("Training...");
Float loss = mlp_->Train(
training_data,
learning_rate_,
static_cast<int>(max_iterations_),
convergence_threshold_,
false // output_log
);
// Run inference after training
std::vector<Float> input_with_bias = input_state_;
input_with_bias.push_back(static_cast<Float>(1.0));
std::vector<Float> output(n_outputs_);
mlp_->GetOutput(input_with_bias, &output);
output_state_ = output;
log("Training complete.");
}
} // namespace nisps
#endif // NISPS_IML_IMPL_HPP
```
**Verification**: File exists with exact content.
---
## Phase 3: Create Main Header and Test
### TASK-030: Create main nisps.hpp header
**Blocked by**: TASK-020, TASK-021
**Description**: Create the main include header that exposes the public API.
**Actions**: Create `nisps-core/include/nisps/nisps.hpp` with this exact content:
```cpp
#ifndef NISPS_HPP
#define NISPS_HPP
#include "iml.hpp"
#endif // NISPS_HPP
```
**Verification**: File exists with exact content.
### TASK-031: Create XOR training test
**Blocked by**: TASK-030
**Description**: Replace the placeholder test with an XOR training test.
**Actions**: Replace `nisps-core/test/main.cpp` with this exact content:
```cpp
#include <nisps/nisps.hpp>
#include <iostream>
#include <cmath>
void log_callback(const char* msg) {
std::cout << "[nisps] " << msg << "\n";
}
int main() {
std::cout << "=== NISPS Core Test: XOR Training ===\n\n";
// Create IML with 2 inputs, 1 output
nisps::IML<float> iml(2, 1, {4, 4}, 5000, 1.0f, 0.0001f);
iml.set_logger(log_callback);
// Enter training mode
iml.set_mode(nisps::IML<float>::Mode::Training);
// Train on XOR pattern
// (0,0) -> 0
iml.set_input(0, 0.0f);
iml.set_input(1, 0.0f);
iml.save_example(); // First call: stop inference
// Manually set output for this example (simulating user positioning)
// We access output_state_ indirectly by calling process after training
// For this test, we'll add examples directly to dataset
// This simulates the two-step save process
// Actually, let's test the full workflow properly:
// The IML class expects: save_example() twice per example
// 1. First call stops inference
// 2. User sets output position (we can't do this externally easily)
// 3. Second call stores input->output
// For testing, let's verify the basic inference works
iml.set_mode(nisps::IML<float>::Mode::Inference);
// Test inference
iml.set_input(0, 0.0f);
iml.set_input(1, 0.0f);
iml.process();
float out_00 = iml.get_outputs()[0];
iml.set_input(0, 1.0f);
iml.set_input(1, 0.0f);
iml.process();
float out_10 = iml.get_outputs()[0];
iml.set_input(0, 0.0f);
iml.set_input(1, 1.0f);
iml.process();
float out_01 = iml.get_outputs()[0];
iml.set_input(0, 1.0f);
iml.set_input(1, 1.0f);
iml.process();
float out_11 = iml.get_outputs()[0];
std::cout << "\nInference results (untrained):\n";
std::cout << " (0,0) -> " << out_00 << "\n";
std::cout << " (1,0) -> " << out_10 << "\n";
std::cout << " (0,1) -> " << out_01 << "\n";
std::cout << " (1,1) -> " << out_11 << "\n";
std::cout << "\n=== Test passed: nisps-core compiles and runs ===\n";
return 0;
}
```
**Verification**: Test compiles and runs successfully.
### TASK-032: Build and run tests
**Blocked by**: TASK-031
**Description**: Build the library and run the test.
**Actions**:
```bash
cd nisps-core
mkdir -p build && cd build
cmake ..
make
ctest --output-on-failure
```
**Verification**: All commands succeed, test outputs "Test passed".
---
## Dependency Graph (ASCII)
```
TASK-001 (create dirs)
├──> TASK-002 (CMakeLists.txt)
│ │
│ └──> TASK-003 (test CMakeLists.txt)
│ │
│ └──> TASK-004 (placeholder test)
├──> TASK-010 (utils.hpp)
│ │
│ └──> TASK-011 (loss.hpp)
│ │ │
│ └──> TASK-012 (node.hpp)
│ │
│ └──> TASK-013 (layer.hpp)
├──> TASK-014 (sample.hpp)
├──> TASK-017 (dataset.hpp)
│ │
│ └──> TASK-018 (dataset_impl.hpp)
└───────────────────────────────────────┐
TASK-011 + TASK-013 + TASK-014 ────────> TASK-015 (mlp.hpp)
└──> TASK-016 (mlp_impl.hpp)
TASK-016 + TASK-018 ───────────────────────────> TASK-020 (iml.hpp)
└──> TASK-021 (iml_impl.hpp)
TASK-020 + TASK-021 ───────────────────────────────────> TASK-030 (nisps.hpp)
└──> TASK-031 (test)
└──> TASK-032 (build & run)
```
---
## Critical Path
The minimum path to a working library:
1. TASK-001 → TASK-002 → TASK-003 → TASK-004 (setup)
2. TASK-010 → TASK-012 → TASK-013 (utils → node → layer)
3. TASK-011 (loss, parallel with above)
4. TASK-014 (sample, parallel)
5. TASK-015 → TASK-016 (mlp)
6. TASK-017 → TASK-018 (dataset, parallel with 3-5)
7. TASK-020 → TASK-021 (iml)
8. TASK-030 → TASK-031 → TASK-032 (header + test + build)
**Estimated time**: 4-6 hours for a capable agent.
---
## File Checklist
After all tasks complete, these files should exist:
```
nisps-core/
├── CMakeLists.txt
├── include/
│ └── nisps/
│ ├── nisps.hpp
│ ├── iml.hpp
│ ├── iml_impl.hpp
│ ├── mlp.hpp
│ ├── mlp_impl.hpp
│ ├── dataset.hpp
│ ├── dataset_impl.hpp
│ ├── layer.hpp
│ ├── node.hpp
│ ├── loss.hpp
│ ├── sample.hpp
│ └── utils.hpp
├── test/
│ ├── CMakeLists.txt
│ └── main.cpp
└── examples/
```

View file

@ -1 +0,0 @@
,ck84,dharma-the-swan,04.11.2025 16:41,file:///home/ck84/snap/libreoffice/358/.config/libreoffice/4;

View file

@ -1,424 +0,0 @@
Name,Component_1,Component_2
Betelginkgo,Betelgeuse,Ginkgo
Pleiadedar,Pleiades,Cedar
Helixypress,Helix Nebula,Cypress
Rigelmwood,Rigel,Elmwood
Sombreroak,Sombrero Galaxy,Oak
Hyadehorn,Hyades,Hawthorn
Triangularch,Triangulum,Larch
Vegalder,Vega,Alder
Whirlpoplar,Whirpool Galaxy,Poplar
Siriuswood,Sirius,Basswood
Oortcacia,Oort Cloud,Acacia
Denebony,Deneb,Ebony
Cartwhillow,Cartwheel Galaxy,Willow
Polariswood,Polaris,Ironwood
Craborwood,Crab Nebula,Hornbeam
Aldebaranyan,Aldebaran,Banyan
Pinwheelnut,Pinwheel Galaxy,Walnut
Antaresnut,Antares,Chestnut
Kuiperlpa,Kuiper Belt,Catalpa
Arcturuspruce,Arcturus,Spruce
Pegasafras,Pegasus,Sassafras
Capellaberry,Capella,Mulberry
Lyracacia,Lyra,Acacia
Polluxwood,Pollux,Boxwood
Scorpiumac,Scorpius,Sumac
Castorwood,Castor,Cottonwood
Dracodar,Draco,Cedar
Regulushua,Regulus,Joshua
Aquilnut,Aquila,Walnut
Spicamore,Spica,Sycamore
Cygnusswood,Cygnus,Sasswood
Proceyon,Procyon,Pecan
Perseusqoia,Perseus,Sequoia
Sagittalpa,Sagittarius,Catalpa
Tauruspen,Taurus,Aspen
Geminowan,Gemini,Rowan
Canceria,Cancer,Wisteria
Leogany,Leo,Mahogany
Virgoak,Virgo,Oak
Libraobab,Libra,Baobab
Ariescalyptus,Aries,Eucalyptus
Capricornia,Capricorn,Magnolia
Aquariusqoia,Aquarius,Sequoia
Piscesimmon,Pisces,Persimmon
Omeganolia,Omega Centauri,Magnolia
Beehivewood,Beehive Cluster,Olivewood
Pillarspress,Pillars of Creation,Cypress
Greatwillow,Great Wall,Willow
Localboobao,Local Bubble,Baobab
Termishoak,Termination Shock,Oak
Bowshocknut,Bow Shock,Chestnut
Stellarix,Stellar Wind,Tamarix
Heliopaulownia,Heliopause,Paulownia
Rochemewood,Roche Limit,Limewood
Eventhornbeam,Event Horizon,Hornbeam
Accretioak,Accretion Disk,Oak
Protoplanetree,Protoplanetary Disk,Planetree
Lagranginkgo,Lagrange Point,Ginkgo
Trojaniper,Trojan,Juniper
Gammarchwood,Gamma Ray Burst,Birchwood
Hypernowan,Hypernova,Rowan
Whitedwillow,White Dwarf,Willow
Redgianter,Red Giant,Red Alder
Superclusternut,Supercluster,Butternut
Filamenteakwood,Filament,Teakwood
Voidglass,Void,Douglass Fir
Greywallnut,Great Wall,Wallnut
Comettree,Comet,Peppertree
Asteroider,Asteroid Belt,Elder
Helixelkova,Helix Nebula,Zelkova
Orionwood,Orion Nebula,Redwood
Cassiopaple,Cassiopeia,Apple
Ursajortree,Ursa Major,Majortree
Ursaminorwood,Ursa Minor,Dogwood
Bristlecrab,Bristlecone Pine,Crab Nebula
Metasequoyasar,Metasequoia,Quasar
Monkeypleiades,Monkey Puzzle,Pleiades
Dragontree,Dragon Tree,Draco
Boojumbrero,Boojum,Sombrero Galaxy
Quiverwheel,Quiver Tree,Pinwheel
Joshuapiter,Joshua Tree,Jupiter
Wolleminebula,Wollemi Pine,Nebula
Dawndromeda,Dawn Redwood,Andromeda
Baldcygnuss,Bald Cypress,Cygnus
Zelkovega,Zelkova,Vega
Hornbeamstar,Hornbeam,Barnstar
Rowantares,Rowan,Antares
Basswoodburst,Basswood,Gamma Burst
Calabashea,Calabash,Cassiopeia
Neemeda,Neem,Andromeda
Salirius,Sal,Sirius
Tooniverse,Toon,Universe
Cassiapeia,Cassia,Cassiopeia
Yellowoodrion,Yellowwood,Orion
Blackwoodhole,Blackwood,Black Hole
Ironwoodsar,Ironwood,Quasar
Satinwhirl,Satinwood,Whirlpool
Poplarsar,Poplar,Pulsar
Cottonwoodway,Cottonwood,Milky Way
Sumacnova,Sumac,Supernova
Hazeleius,Hazel,Pegasus
Elderstar,Elder,Barnstar
Rigelbark,Rigel,Bark
Sirillow,Sirius,Willow
Altairix,Altair,Tamarix
Denebula,Deneb,Nebula
Polarispine,Polaris,Pine
Betelpine,Betelgeuse,Bristlecone
Rigellarch,Rigel,Larch
Vegamore,Vega,Sycamore
Altairnut,Altair,Chestnut
Arcturuscacia,Arcturus,Acacia
Capellaple,Capella,Apple
Polluxdogwood,Pollux,Dogwood
Castorong,Castor,Camphorwood
Regulusqoia,Regulus,Sequoia
Spicape,Spica,Grape
Procyonwood,Procyon,Sandalwood
Aldebaranyan,Aldebaran,Banyan
Antarespress,Antares,Cypress
Pleiadwood,Pleiades,Cedarwood
Hyadesmac,Hyades,Sumac
Beehivewood,Beehive,Olivewood
Orionniper,Orion,Juniper
Cassiopear,Cassiopeia,Pear
Dracoroak,Draco,Cork Oak
Lyralnut,Lyra,Walnut
Cygnust,Cygnus,Locust
Aquillow,Aquila,Willow
Perseusnut,Perseus,Butternut
Pegasuspen,Pegasus,Aspen
Andromelm,Andromeda,Elm
Triangulfir,Triangulum,Fir
Sombreruce,Sombrero,Spruce
Whirlpinecone,Whirlpool,Pine
Cartwheelnut,Cartwheel,Hazelnut
Helixelm,Helix,Elm
Crabapple,Crab Nebula,Crabapple
Pillarspen,Pillars,Aspen
Sagittariuswood,Sagittarius,Sandalwood
Scorpiuspen,Scorpius,Aspen
Taurusqoia,Taurus,Sequoia
Geminoak,Gemini,Oak
Cancerdar,Cancer,Cedar
Leowood,Leo,Boxwood
Virgowan,Virgo,Rowan
Librasswood,Libra,Basswood
Ariesnut,Aries,Chestnut
Capricornbeam,Capricorn,Hornbeam
Aquariuslder,Aquarius,Alder
Piscespress,Pisces,Cypress
Magnetardwood,Magnetar,Hardwood
Pulsarwood,Pulsar,Rosewood
Quasarberry,Quasar,Elderberry
Supernovakia,Supernova,Plumeria
Hypernovania,Hypernova,Paulownia
Blackholly,Black Hole,Holly
Whitedwarfpine,White Dwarf,White Pine
Redgiantwood,Red Giant,Giantwood
Bluegiganticacia,Blue Giant,Acacia
Neutronia,Neutron Star,Catalpa
Browndwarfnut,Brown Dwarf,Butternut
Protostarfir,Protostar,Fir
Mainsequoia,Main Sequence,Sequoia
Subergiantree,Supergiant,Tree
Yorkcloud,Oort Cloud,Cork
Kuiperlpa,Kuiper Belt,Catalpa
Heliospherical,Heliosphere,Spherical Tree
Heliopaulogany,Heliopause,Mahogany
Terminationoak,Termination Shock,Oak
Bowshockberry,Bow Shock,Hackberry
Stellarwindwood,Stellar Wind,Driftwood
Solarwindowan,Solar Wind,Rowan
Interstellarch,Interstellar Medium,Larch
Circumstellarix,Circumstellar Disk,Tamarix
Protoplanetree,Protoplanetary Disk,Plane Tree
Accretionoak,Accretion Disk,Oak
Eventhorizonwood,Event Horizon,Ironwood
Schwarzschillow,Schwarzschild Radius,Willow
Singularsimmon,Singularity,Persimmon
Rochelimewood,Roche Limit,Limewood
Lagrangewood,Lagrange Point,Rangewood
Trojantree,Trojan Asteroids,Tree
Hildazel,Hilda Group,Hazel
Amoribark,Amor Asteroids,Bark
Atenberry,Aten Asteroids,Elderberry
Apollomeria,Apollo Asteroids,Plumeria
Gammaburst Ash,Gamma Ray Burst,Ash
Xraybirch,X-ray Binary,Birch
Millisecwood,Millisecond Pulsar,Redwood
Magnetarch,Magnetar,Larch
Softgammawood,Soft Gamma Repeater,Softwood
Superclusternut,Supercluster,Clusternut
Galaxyclusterpine,Galaxy Cluster,Pine
Galaxygroupfir,Galaxy Group,Fir
Localgrowan,Local Group,Rowan
Virgoclusteroak,Virgo Cluster,Oak
Comaberrynicaceae,Coma Berenices,Berry
Greatwallnut,Great Wall,Wallnut
Sloangreatwillow,Sloan Great Wall,Willow
Herculessuperia,Hercules Supercluster,Wisteria
Shapleypress,Shapley Supercluster,Cypress
Laniakeroak,Laniakea Supercluster,Kauri Oak
Cosmicwebwood,Cosmic Web,Webwood
Filamentree,Filament,Tree
Voidwood,Void,Driftwood
Boötesvoidwood,Boötes Void,Olivewood
Sculptornut,Sculptor Void,Coconut
Greywallash,Great Wall,Ash
Localbubbaobab,Local Bubble,Baobab
Loopicacia,Local Interstellar Cloud,Acacia
Gouldspen,Gould Belt,Aspen
Orionspurmac,Orion Spur,Sumac
Sagittariusarmwood,Sagittarius Arm,Armwood
Perseusarmpine,Perseus Arm,Pine
Outerarmoak,Outer Arm,Oak
Scutumcentauruswood,Scutum-Centaurus Arm,Scutumwood
Normamalder,Norma Arm,Alder
Barredspiralex,Barred Spiral,Ilex
Ellipticacacia,Elliptical Galaxy,Acacia
Irregularnut,Irregular Galaxy,Butternut
Lenticularcorn,Lenticular Galaxy,Acorn
Dwarfnut,Dwarf Galaxy,Chestnut
Satelliteelm,Satellite Galaxy,Elm
Starburstfir,Starburst Galaxy,Fir
Seyfertree,Seyfert Galaxy,Feather Tree
Blazarwood,Blazar,Rosewood
Radiogalaxypress,Radio Galaxy,Cypress
Activenutcleus,Active Galactic Nucleus,Nut
Quasartree,Quasar,Quandong Tree
Ultraluminouswood,Ultraluminous Galaxy,Luminous Tree
Luminousredgiant,Luminous Red Galaxy,Red Giant Sequoia
Greenpeachia,Green Pea Galaxy,Peach
Polarringwood,Polar Ring Galaxy,Ringwood
Shellgalaxash,Shell Galaxy,Ash
Ringalaxypress,Ring Galaxy,Cypress
Hoagsobjectoak,Hoag's Object,Oak
Mayallsobjectree,Mayall's Object,Tree
Tadpolgalaxillow,Tadpole Galaxy,Willow
Cometgalaxalder,Comet Galaxy,Alder
Sunflowernut,Sunflower Galaxy,Sunflower
Blackeyewood,Black Eye Galaxy,Blackwood
Bodesgalaxnut,Bode's Galaxy,Coconut
Cigargalaxpress,Cigar Galaxy,Cypress
Mousesgalaxypen,Mice Galaxies,Aspen
Antennaegalaxynt,Antennae Galaxies,Antwood
Polarisstwood,Polaris,Sasswood
Rigwood,Rigel,Dogwood
Canopusnut,Canopus,Coconut
Vegamore,Vega,Sycamore
Capellalnut,Capella,Walnut
Rigelberry,Rigel,Mulberry
Procyonwood,Procyon,Cottonwood
Achernarnut,Achernar,Butternut
Betelgeuswood,Betelgeuse,Geuswood
Hadarcedarwood,Hadar,Cedarwood
Altairix,Altair,Tamarix
Alderaminwood,Alderamin,Alderwood
Denebolive,Denebola,Olive
Mimosacia,Mimosa,Acacia
Regulenut,Regulus,Filbert
Adharalnut,Adhara,Walnut
Shahelmwood,Shaula,Elm
Gacruxifir,Gacrux,Fir
Bellatrixnut,Bellatrix,Hazelnut
Elnathnut,Elnath,Chestnut
Miaplacidiumwood,Miaplacidus,Palladium
Alnitaknut,Alnitak,Coconut
Alnilamspen,Alnilam,Aspen
Aliothogany,Alioth,Mahogany
Mirzamberry,Mirzam,Mulberry
Alkaidnut,Alkaid,Butternut
Atriarch,Atria,Larch
Alhenawood,Alhena,Alderwood
Peacockahogany,Peacock,Mahogany
Mirfaknut,Mirfak,Coconut
Wezenalder,Wezen,Alder
Sargassage,Sargas,Sage
Menkarberry,Menkar,Elderberry
Eniffir,Enif,Fir
Denebalgedinut,Deneb Algedi,Butternut
Zubenhaknutbi,Zuben Hakrabi,Butternut
Alshainalnut,Alshain,Walnut
Rastabantamarind,Rastaban,Tamarind
Markabwood,Markab,Markwood
Schedarogany,Schedar,Mahogany
Almaalnutak,Almaak,Walnut
Rasalgetash,Rasalgethi,Ash
Thubanyan,Thuban,Banyan
Kausafras,Kaus Australis,Sassafras
Nunkirnut,Nunki,Coconut
Sabikwood,Sabik,Rosewood
Menkalnutnan,Menkalnan,Butternut
Asteropewood,Asterope,Redwood
Atlaspen,Atlas,Aspen
Electralder,Electra,Alder
Maiagany,Maia,Mahogany
Meropewood,Merope,Olivewood
Taugetamarix,Taygeta,Tamarix
Pleionetree,Pleione,Tree
Celaenoak,Celaeno,Oak
Alcyonedar,Alcyone,Cedar
Sterropopelnut,Sterope,Pecanut
Proximaple,Proxima Centauri,Apple
Alphacenternut,Alpha Centauri,Butternut
Barnardstaroak,Barnard's Star,Oak
Wolfwood,Wolf 359,Dogwood
Lalandelarch,Lalande 21185,Larch
Siriuswood,Sirius,Basswood
Luytenbirch,Luyten 726-8,Birch
Rossnut,Ross 154,Coconut
Rossberry,Ross 248,Elderberry
Epsilonwood,Epsilon Eridani,Sandalwood
Lacaillemwood,Lacaille 9352,Elmwood
Rosswood,Ross 128,Cottonwood
EZaquariuspen,EZ Aquarii,Aspen
Procyonix,Procyon,Phoenix
Sixtyonecygnust,61 Cygni,Locust
Struvewood,Struve 2398,Rosewood
Groombridgefir,Groombridge 34,Fir
Epsilondiwood,Epsilon Indi,Indigowood
DXcancroak,DX Cancri,Oak
Taucetialder,Tau Ceti,Alder
Luytenut,Luyten's Star,Butternut
Teegardeenspen,Teegarden's Star,Aspen
Kapteynsnut,Kapteyn's Star,Coconut
Lacaillewood,Lacaille 8760,Lacewood
Krugernut,Krüger 60,Coconut
Rosspress,Ross 614,Cypress
Wolfwoodogwood,Wolf 1061,Dogwood
Vanmaanensnut,Van Maanen's Star,Chestnut
Gliesetree,Gliese 1,Tree
Gielenseginkgo,Gliese 876,Ginkgo
Giseewood,Gliese 581,Rosewood
TRappistree,TRAPPIST-1,Trapwood
Gliesecherry,Gliese 667,Cherry
Glisewood,Gliese 832,Walnutwood
Glaesewood,Gliese 163,Olivewood
Gleasewood,Gliese 180,Teaselwood
Lessewood,Gliese 221,Lesserwood
Sleewood,Gliese 317,Sleekwood
Liewood,Gliese 357,Tree
Gleetree,Gliese 433,Tree
Graywood,Gliese 436,Graywood
Greatwood,Gliese 504,Greatwood
Greenwood,Gliese 570,Greenwood
Greshwood,Gliese 625,Ashwood
Gladewood,Gliese 649,Glade
Glowwood,Gliese 674,Glowwood
Gradewood,Gliese 682,Gradewood
Gracewood,Gliese 686,Gracewood
Grandewood,Gliese 785,Grandewood
Glintwood,Gliese 806,Flintwood
Grimwood,Gliese 832,Grimwood
Horsehead Hickory,Horsehead Nebula,Hickory
Eaglenberry,Eagle Nebula,Elderberry
Trifidogwood,Trifid Nebula,Dogwood
Lagoonwood,Lagoon Nebula,Lagoonwood
Rosettamarisk,Rosette Nebula,Tamarisk
Carinapress,Carina Nebula,Cypress
Veilbark,Veil Nebula,Bark
Northamericoak,North America Nebula,Oak
Flamingowan,Flaming Star Nebula,Rowan
Owlnut,Owl Nebula,Walnut
Catseyelm,Cat's Eye Nebula,Elm
Ringebula,Ring Nebula,Nebula
Dumbbellnut,Dumbbell Nebula,Butternut
Boomerangalm,Boomerang Nebula,Palm
Butterflypress,Butterfly Nebula,Cypress
Calasash,Calabash Nebula,Ash
Redspiderash,Red Spider Nebula,Ash
Eskimogany,Eskimo Nebula,Mahogany
Ghostjuniper,Ghost of Jupiter,Juniper
Helixogwood,Helix Nebula,Dogwood
Spiralpress,Spiral Planetary,Cypress
Littlegembark,Little Gem Nebula,Bark
Necklacacia,Necklace Nebula,Acacia
Saturnbeech,Saturn Nebula,Beech
Bluesnowbark,Blue Snowball,Bark
Twinjet Fir,Twin Jet Nebula,Fir
Hourglasswood,Hourglass Nebula,Boxwood
Eggwood,Egg Nebula,Dogwood
Boxtree,Box Nebula,Boxtree
Crescentpress,Crescent Nebula,Cypress
Bubblebark,Bubble Nebula,Bark
Thorwood,Thor's Helmet,Thornwood
Snailgum,Snail Nebula,Sweetgum
Conepinepress,Cone Nebula,Pine
Irisbeech,Iris Nebula,Beech
Witchheadash,Witch Head Nebula,Ash
Barnardloopwood,Barnard's Loop,Loopwood
Californiapress,California Nebula,Cypress
Heartbark,Heart Nebula,Bark
Soulspuce,Soul Nebula,Spruce
Pacmangany,Pacman Nebula,Mahogany
Monkeyheadnut,Monkey Head Nebula,Monkeynut
Wizardlm,Wizard Nebula,Elm
Tulipwood,Tulip Nebula,Tulipwood
Crescentbark,Crescent Nebula,Bark
Pelicangum,Pelican Nebula,Gum
Elephantstrunkwood,Elephant's Trunk,Trunkwood
Tuningforkpress,Tuning Fork Galaxy,Cypress
Sombrerowood,Sombrero Galaxy,Sombrerowood
Blackeyesycamore,Black Eye Galaxy,Sycamore
Sunflowernut,Sunflower Galaxy,Coconut
Pinwheelder,Pinwheel Galaxy,Elder
Fireworksgalaxyalder,Fireworks Galaxy,Alder
Spindlepress,Spindle Galaxy,Cypress
Bodesgalaxnut,Bode's Galaxy,Butternut
Cigargalaxbirch,Cigar Galaxy,Birch
Whirlpoolepress,Whirlpool Galaxy,Cypress
Cartwheelalder,Cartwheel Galaxy,Alder
Tadpolewood,Tadpole Galaxy,Tadwood
Antenaennut,Antennae Galaxies,Nut
Mousesgalaxielm,Mice Galaxies,Elm
Atomspeachachy,Atom's Peace Galaxy,Peachy
Malinsgalaxnut,Malin 1,Nut
Hoagsobjecteakwood,Hoag's Object,Teakwood
Ringgalaxynut,Ring Galaxy,Donut
Polarringiron,Polar Ring Galaxy,Ironwood
Silversliverwood,Silver Sliver Galaxy,Silverwood
Needlepress,Needle Galaxy,Cypress
Hamburgerash,Hamburger Galaxy,Ash
1 Name Component_1 Component_2
2 Betelginkgo Betelgeuse Ginkgo
3 Pleiadedar Pleiades Cedar
4 Helixypress Helix Nebula Cypress
5 Rigelmwood Rigel Elmwood
6 Sombreroak Sombrero Galaxy Oak
7 Hyadehorn Hyades Hawthorn
8 Triangularch Triangulum Larch
9 Vegalder Vega Alder
10 Whirlpoplar Whirpool Galaxy Poplar
11 Siriuswood Sirius Basswood
12 Oortcacia Oort Cloud Acacia
13 Denebony Deneb Ebony
14 Cartwhillow Cartwheel Galaxy Willow
15 Polariswood Polaris Ironwood
16 Craborwood Crab Nebula Hornbeam
17 Aldebaranyan Aldebaran Banyan
18 Pinwheelnut Pinwheel Galaxy Walnut
19 Antaresnut Antares Chestnut
20 Kuiperlpa Kuiper Belt Catalpa
21 Arcturuspruce Arcturus Spruce
22 Pegasafras Pegasus Sassafras
23 Capellaberry Capella Mulberry
24 Lyracacia Lyra Acacia
25 Polluxwood Pollux Boxwood
26 Scorpiumac Scorpius Sumac
27 Castorwood Castor Cottonwood
28 Dracodar Draco Cedar
29 Regulushua Regulus Joshua
30 Aquilnut Aquila Walnut
31 Spicamore Spica Sycamore
32 Cygnusswood Cygnus Sasswood
33 Proceyon Procyon Pecan
34 Perseusqoia Perseus Sequoia
35 Sagittalpa Sagittarius Catalpa
36 Tauruspen Taurus Aspen
37 Geminowan Gemini Rowan
38 Canceria Cancer Wisteria
39 Leogany Leo Mahogany
40 Virgoak Virgo Oak
41 Libraobab Libra Baobab
42 Ariescalyptus Aries Eucalyptus
43 Capricornia Capricorn Magnolia
44 Aquariusqoia Aquarius Sequoia
45 Piscesimmon Pisces Persimmon
46 Omeganolia Omega Centauri Magnolia
47 Beehivewood Beehive Cluster Olivewood
48 Pillarspress Pillars of Creation Cypress
49 Greatwillow Great Wall Willow
50 Localboobao Local Bubble Baobab
51 Termishoak Termination Shock Oak
52 Bowshocknut Bow Shock Chestnut
53 Stellarix Stellar Wind Tamarix
54 Heliopaulownia Heliopause Paulownia
55 Rochemewood Roche Limit Limewood
56 Eventhornbeam Event Horizon Hornbeam
57 Accretioak Accretion Disk Oak
58 Protoplanetree Protoplanetary Disk Planetree
59 Lagranginkgo Lagrange Point Ginkgo
60 Trojaniper Trojan Juniper
61 Gammarchwood Gamma Ray Burst Birchwood
62 Hypernowan Hypernova Rowan
63 Whitedwillow White Dwarf Willow
64 Redgianter Red Giant Red Alder
65 Superclusternut Supercluster Butternut
66 Filamenteakwood Filament Teakwood
67 Voidglass Void Douglass Fir
68 Greywallnut Great Wall Wallnut
69 Comettree Comet Peppertree
70 Asteroider Asteroid Belt Elder
71 Helixelkova Helix Nebula Zelkova
72 Orionwood Orion Nebula Redwood
73 Cassiopaple Cassiopeia Apple
74 Ursajortree Ursa Major Majortree
75 Ursaminorwood Ursa Minor Dogwood
76 Bristlecrab Bristlecone Pine Crab Nebula
77 Metasequoyasar Metasequoia Quasar
78 Monkeypleiades Monkey Puzzle Pleiades
79 Dragontree Dragon Tree Draco
80 Boojumbrero Boojum Sombrero Galaxy
81 Quiverwheel Quiver Tree Pinwheel
82 Joshuapiter Joshua Tree Jupiter
83 Wolleminebula Wollemi Pine Nebula
84 Dawndromeda Dawn Redwood Andromeda
85 Baldcygnuss Bald Cypress Cygnus
86 Zelkovega Zelkova Vega
87 Hornbeamstar Hornbeam Barnstar
88 Rowantares Rowan Antares
89 Basswoodburst Basswood Gamma Burst
90 Calabashea Calabash Cassiopeia
91 Neemeda Neem Andromeda
92 Salirius Sal Sirius
93 Tooniverse Toon Universe
94 Cassiapeia Cassia Cassiopeia
95 Yellowoodrion Yellowwood Orion
96 Blackwoodhole Blackwood Black Hole
97 Ironwoodsar Ironwood Quasar
98 Satinwhirl Satinwood Whirlpool
99 Poplarsar Poplar Pulsar
100 Cottonwoodway Cottonwood Milky Way
101 Sumacnova Sumac Supernova
102 Hazeleius Hazel Pegasus
103 Elderstar Elder Barnstar
104 Rigelbark Rigel Bark
105 Sirillow Sirius Willow
106 Altairix Altair Tamarix
107 Denebula Deneb Nebula
108 Polarispine Polaris Pine
109 Betelpine Betelgeuse Bristlecone
110 Rigellarch Rigel Larch
111 Vegamore Vega Sycamore
112 Altairnut Altair Chestnut
113 Arcturuscacia Arcturus Acacia
114 Capellaple Capella Apple
115 Polluxdogwood Pollux Dogwood
116 Castorong Castor Camphorwood
117 Regulusqoia Regulus Sequoia
118 Spicape Spica Grape
119 Procyonwood Procyon Sandalwood
120 Aldebaranyan Aldebaran Banyan
121 Antarespress Antares Cypress
122 Pleiadwood Pleiades Cedarwood
123 Hyadesmac Hyades Sumac
124 Beehivewood Beehive Olivewood
125 Orionniper Orion Juniper
126 Cassiopear Cassiopeia Pear
127 Dracoroak Draco Cork Oak
128 Lyralnut Lyra Walnut
129 Cygnust Cygnus Locust
130 Aquillow Aquila Willow
131 Perseusnut Perseus Butternut
132 Pegasuspen Pegasus Aspen
133 Andromelm Andromeda Elm
134 Triangulfir Triangulum Fir
135 Sombreruce Sombrero Spruce
136 Whirlpinecone Whirlpool Pine
137 Cartwheelnut Cartwheel Hazelnut
138 Helixelm Helix Elm
139 Crabapple Crab Nebula Crabapple
140 Pillarspen Pillars Aspen
141 Sagittariuswood Sagittarius Sandalwood
142 Scorpiuspen Scorpius Aspen
143 Taurusqoia Taurus Sequoia
144 Geminoak Gemini Oak
145 Cancerdar Cancer Cedar
146 Leowood Leo Boxwood
147 Virgowan Virgo Rowan
148 Librasswood Libra Basswood
149 Ariesnut Aries Chestnut
150 Capricornbeam Capricorn Hornbeam
151 Aquariuslder Aquarius Alder
152 Piscespress Pisces Cypress
153 Magnetardwood Magnetar Hardwood
154 Pulsarwood Pulsar Rosewood
155 Quasarberry Quasar Elderberry
156 Supernovakia Supernova Plumeria
157 Hypernovania Hypernova Paulownia
158 Blackholly Black Hole Holly
159 Whitedwarfpine White Dwarf White Pine
160 Redgiantwood Red Giant Giantwood
161 Bluegiganticacia Blue Giant Acacia
162 Neutronia Neutron Star Catalpa
163 Browndwarfnut Brown Dwarf Butternut
164 Protostarfir Protostar Fir
165 Mainsequoia Main Sequence Sequoia
166 Subergiantree Supergiant Tree
167 Yorkcloud Oort Cloud Cork
168 Kuiperlpa Kuiper Belt Catalpa
169 Heliospherical Heliosphere Spherical Tree
170 Heliopaulogany Heliopause Mahogany
171 Terminationoak Termination Shock Oak
172 Bowshockberry Bow Shock Hackberry
173 Stellarwindwood Stellar Wind Driftwood
174 Solarwindowan Solar Wind Rowan
175 Interstellarch Interstellar Medium Larch
176 Circumstellarix Circumstellar Disk Tamarix
177 Protoplanetree Protoplanetary Disk Plane Tree
178 Accretionoak Accretion Disk Oak
179 Eventhorizonwood Event Horizon Ironwood
180 Schwarzschillow Schwarzschild Radius Willow
181 Singularsimmon Singularity Persimmon
182 Rochelimewood Roche Limit Limewood
183 Lagrangewood Lagrange Point Rangewood
184 Trojantree Trojan Asteroids Tree
185 Hildazel Hilda Group Hazel
186 Amoribark Amor Asteroids Bark
187 Atenberry Aten Asteroids Elderberry
188 Apollomeria Apollo Asteroids Plumeria
189 Gammaburst Ash Gamma Ray Burst Ash
190 Xraybirch X-ray Binary Birch
191 Millisecwood Millisecond Pulsar Redwood
192 Magnetarch Magnetar Larch
193 Softgammawood Soft Gamma Repeater Softwood
194 Superclusternut Supercluster Clusternut
195 Galaxyclusterpine Galaxy Cluster Pine
196 Galaxygroupfir Galaxy Group Fir
197 Localgrowan Local Group Rowan
198 Virgoclusteroak Virgo Cluster Oak
199 Comaberrynicaceae Coma Berenices Berry
200 Greatwallnut Great Wall Wallnut
201 Sloangreatwillow Sloan Great Wall Willow
202 Herculessuperia Hercules Supercluster Wisteria
203 Shapleypress Shapley Supercluster Cypress
204 Laniakeroak Laniakea Supercluster Kauri Oak
205 Cosmicwebwood Cosmic Web Webwood
206 Filamentree Filament Tree
207 Voidwood Void Driftwood
208 Boötesvoidwood Boötes Void Olivewood
209 Sculptornut Sculptor Void Coconut
210 Greywallash Great Wall Ash
211 Localbubbaobab Local Bubble Baobab
212 Loopicacia Local Interstellar Cloud Acacia
213 Gouldspen Gould Belt Aspen
214 Orionspurmac Orion Spur Sumac
215 Sagittariusarmwood Sagittarius Arm Armwood
216 Perseusarmpine Perseus Arm Pine
217 Outerarmoak Outer Arm Oak
218 Scutumcentauruswood Scutum-Centaurus Arm Scutumwood
219 Normamalder Norma Arm Alder
220 Barredspiralex Barred Spiral Ilex
221 Ellipticacacia Elliptical Galaxy Acacia
222 Irregularnut Irregular Galaxy Butternut
223 Lenticularcorn Lenticular Galaxy Acorn
224 Dwarfnut Dwarf Galaxy Chestnut
225 Satelliteelm Satellite Galaxy Elm
226 Starburstfir Starburst Galaxy Fir
227 Seyfertree Seyfert Galaxy Feather Tree
228 Blazarwood Blazar Rosewood
229 Radiogalaxypress Radio Galaxy Cypress
230 Activenutcleus Active Galactic Nucleus Nut
231 Quasartree Quasar Quandong Tree
232 Ultraluminouswood Ultraluminous Galaxy Luminous Tree
233 Luminousredgiant Luminous Red Galaxy Red Giant Sequoia
234 Greenpeachia Green Pea Galaxy Peach
235 Polarringwood Polar Ring Galaxy Ringwood
236 Shellgalaxash Shell Galaxy Ash
237 Ringalaxypress Ring Galaxy Cypress
238 Hoagsobjectoak Hoag's Object Oak
239 Mayallsobjectree Mayall's Object Tree
240 Tadpolgalaxillow Tadpole Galaxy Willow
241 Cometgalaxalder Comet Galaxy Alder
242 Sunflowernut Sunflower Galaxy Sunflower
243 Blackeyewood Black Eye Galaxy Blackwood
244 Bodesgalaxnut Bode's Galaxy Coconut
245 Cigargalaxpress Cigar Galaxy Cypress
246 Mousesgalaxypen Mice Galaxies Aspen
247 Antennaegalaxynt Antennae Galaxies Antwood
248 Polarisstwood Polaris Sasswood
249 Rigwood Rigel Dogwood
250 Canopusnut Canopus Coconut
251 Vegamore Vega Sycamore
252 Capellalnut Capella Walnut
253 Rigelberry Rigel Mulberry
254 Procyonwood Procyon Cottonwood
255 Achernarnut Achernar Butternut
256 Betelgeuswood Betelgeuse Geuswood
257 Hadarcedarwood Hadar Cedarwood
258 Altairix Altair Tamarix
259 Alderaminwood Alderamin Alderwood
260 Denebolive Denebola Olive
261 Mimosacia Mimosa Acacia
262 Regulenut Regulus Filbert
263 Adharalnut Adhara Walnut
264 Shahelmwood Shaula Elm
265 Gacruxifir Gacrux Fir
266 Bellatrixnut Bellatrix Hazelnut
267 Elnathnut Elnath Chestnut
268 Miaplacidiumwood Miaplacidus Palladium
269 Alnitaknut Alnitak Coconut
270 Alnilamspen Alnilam Aspen
271 Aliothogany Alioth Mahogany
272 Mirzamberry Mirzam Mulberry
273 Alkaidnut Alkaid Butternut
274 Atriarch Atria Larch
275 Alhenawood Alhena Alderwood
276 Peacockahogany Peacock Mahogany
277 Mirfaknut Mirfak Coconut
278 Wezenalder Wezen Alder
279 Sargassage Sargas Sage
280 Menkarberry Menkar Elderberry
281 Eniffir Enif Fir
282 Denebalgedinut Deneb Algedi Butternut
283 Zubenhaknutbi Zuben Hakrabi Butternut
284 Alshainalnut Alshain Walnut
285 Rastabantamarind Rastaban Tamarind
286 Markabwood Markab Markwood
287 Schedarogany Schedar Mahogany
288 Almaalnutak Almaak Walnut
289 Rasalgetash Rasalgethi Ash
290 Thubanyan Thuban Banyan
291 Kausafras Kaus Australis Sassafras
292 Nunkirnut Nunki Coconut
293 Sabikwood Sabik Rosewood
294 Menkalnutnan Menkalnan Butternut
295 Asteropewood Asterope Redwood
296 Atlaspen Atlas Aspen
297 Electralder Electra Alder
298 Maiagany Maia Mahogany
299 Meropewood Merope Olivewood
300 Taugetamarix Taygeta Tamarix
301 Pleionetree Pleione Tree
302 Celaenoak Celaeno Oak
303 Alcyonedar Alcyone Cedar
304 Sterropopelnut Sterope Pecanut
305 Proximaple Proxima Centauri Apple
306 Alphacenternut Alpha Centauri Butternut
307 Barnardstaroak Barnard's Star Oak
308 Wolfwood Wolf 359 Dogwood
309 Lalandelarch Lalande 21185 Larch
310 Siriuswood Sirius Basswood
311 Luytenbirch Luyten 726-8 Birch
312 Rossnut Ross 154 Coconut
313 Rossberry Ross 248 Elderberry
314 Epsilonwood Epsilon Eridani Sandalwood
315 Lacaillemwood Lacaille 9352 Elmwood
316 Rosswood Ross 128 Cottonwood
317 EZaquariuspen EZ Aquarii Aspen
318 Procyonix Procyon Phoenix
319 Sixtyonecygnust 61 Cygni Locust
320 Struvewood Struve 2398 Rosewood
321 Groombridgefir Groombridge 34 Fir
322 Epsilondiwood Epsilon Indi Indigowood
323 DXcancroak DX Cancri Oak
324 Taucetialder Tau Ceti Alder
325 Luytenut Luyten's Star Butternut
326 Teegardeenspen Teegarden's Star Aspen
327 Kapteynsnut Kapteyn's Star Coconut
328 Lacaillewood Lacaille 8760 Lacewood
329 Krugernut Krüger 60 Coconut
330 Rosspress Ross 614 Cypress
331 Wolfwoodogwood Wolf 1061 Dogwood
332 Vanmaanensnut Van Maanen's Star Chestnut
333 Gliesetree Gliese 1 Tree
334 Gielenseginkgo Gliese 876 Ginkgo
335 Giseewood Gliese 581 Rosewood
336 TRappistree TRAPPIST-1 Trapwood
337 Gliesecherry Gliese 667 Cherry
338 Glisewood Gliese 832 Walnutwood
339 Glaesewood Gliese 163 Olivewood
340 Gleasewood Gliese 180 Teaselwood
341 Lessewood Gliese 221 Lesserwood
342 Sleewood Gliese 317 Sleekwood
343 Liewood Gliese 357 Tree
344 Gleetree Gliese 433 Tree
345 Graywood Gliese 436 Graywood
346 Greatwood Gliese 504 Greatwood
347 Greenwood Gliese 570 Greenwood
348 Greshwood Gliese 625 Ashwood
349 Gladewood Gliese 649 Glade
350 Glowwood Gliese 674 Glowwood
351 Gradewood Gliese 682 Gradewood
352 Gracewood Gliese 686 Gracewood
353 Grandewood Gliese 785 Grandewood
354 Glintwood Gliese 806 Flintwood
355 Grimwood Gliese 832 Grimwood
356 Horsehead Hickory Horsehead Nebula Hickory
357 Eaglenberry Eagle Nebula Elderberry
358 Trifidogwood Trifid Nebula Dogwood
359 Lagoonwood Lagoon Nebula Lagoonwood
360 Rosettamarisk Rosette Nebula Tamarisk
361 Carinapress Carina Nebula Cypress
362 Veilbark Veil Nebula Bark
363 Northamericoak North America Nebula Oak
364 Flamingowan Flaming Star Nebula Rowan
365 Owlnut Owl Nebula Walnut
366 Catseyelm Cat's Eye Nebula Elm
367 Ringebula Ring Nebula Nebula
368 Dumbbellnut Dumbbell Nebula Butternut
369 Boomerangalm Boomerang Nebula Palm
370 Butterflypress Butterfly Nebula Cypress
371 Calasash Calabash Nebula Ash
372 Redspiderash Red Spider Nebula Ash
373 Eskimogany Eskimo Nebula Mahogany
374 Ghostjuniper Ghost of Jupiter Juniper
375 Helixogwood Helix Nebula Dogwood
376 Spiralpress Spiral Planetary Cypress
377 Littlegembark Little Gem Nebula Bark
378 Necklacacia Necklace Nebula Acacia
379 Saturnbeech Saturn Nebula Beech
380 Bluesnowbark Blue Snowball Bark
381 Twinjet Fir Twin Jet Nebula Fir
382 Hourglasswood Hourglass Nebula Boxwood
383 Eggwood Egg Nebula Dogwood
384 Boxtree Box Nebula Boxtree
385 Crescentpress Crescent Nebula Cypress
386 Bubblebark Bubble Nebula Bark
387 Thorwood Thor's Helmet Thornwood
388 Snailgum Snail Nebula Sweetgum
389 Conepinepress Cone Nebula Pine
390 Irisbeech Iris Nebula Beech
391 Witchheadash Witch Head Nebula Ash
392 Barnardloopwood Barnard's Loop Loopwood
393 Californiapress California Nebula Cypress
394 Heartbark Heart Nebula Bark
395 Soulspuce Soul Nebula Spruce
396 Pacmangany Pacman Nebula Mahogany
397 Monkeyheadnut Monkey Head Nebula Monkeynut
398 Wizardlm Wizard Nebula Elm
399 Tulipwood Tulip Nebula Tulipwood
400 Crescentbark Crescent Nebula Bark
401 Pelicangum Pelican Nebula Gum
402 Elephantstrunkwood Elephant's Trunk Trunkwood
403 Tuningforkpress Tuning Fork Galaxy Cypress
404 Sombrerowood Sombrero Galaxy Sombrerowood
405 Blackeyesycamore Black Eye Galaxy Sycamore
406 Sunflowernut Sunflower Galaxy Coconut
407 Pinwheelder Pinwheel Galaxy Elder
408 Fireworksgalaxyalder Fireworks Galaxy Alder
409 Spindlepress Spindle Galaxy Cypress
410 Bodesgalaxnut Bode's Galaxy Butternut
411 Cigargalaxbirch Cigar Galaxy Birch
412 Whirlpoolepress Whirlpool Galaxy Cypress
413 Cartwheelalder Cartwheel Galaxy Alder
414 Tadpolewood Tadpole Galaxy Tadwood
415 Antenaennut Antennae Galaxies Nut
416 Mousesgalaxielm Mice Galaxies Elm
417 Atomspeachachy Atom's Peace Galaxy Peachy
418 Malinsgalaxnut Malin 1 Nut
419 Hoagsobjecteakwood Hoag's Object Teakwood
420 Ringgalaxynut Ring Galaxy Donut
421 Polarringiron Polar Ring Galaxy Ironwood
422 Silversliverwood Silver Sliver Galaxy Silverwood
423 Needlepress Needle Galaxy Cypress
424 Hamburgerash Hamburger Galaxy Ash

View file

@ -1,420 +0,0 @@
Name,Component_1,Component_2
Pleiadeiba,Pleiades,Ceiba
Pau Brasilica,Pau Brasil,Crab Nebula
Araucarina,Araucaria,Carina Nebula
Quebracho X-1,Quebracho,Cygnus X-1
Lapachomeda,Lapacho,Andromeda
Guanacastellum,Guanacaste,Magellanic Cloud
Trianguanum,Triangulum,Guanacaste
Jacarandromeda,Jacaranda,Andromeda
Ceibalaxy,Ceiba,Galaxy
Horsehead Brasil,Horsehead,Pau Brasil
Monkeypuzzle Void,Monkey Puzzle,Cosmic Void
Quebrachion,Quebracho,Orion
Lapachosar,Lapacho,Quasar
Rubber Rosette,Rubber Tree,Rosette Nebula
Cashewmeda,Cashew,Andromeda
Brazil Nutnova,Brazil Nut,Supernova
Cecropiades,Cecropia,Pleiades
Balsombrero,Balsa,Sombrero Galaxy
Rosewoodway,Rosewood,Milky Way
Ipe Helix,Ipe,Helix Nebula
Kapoksagittarius,Kapok,Sagittarius
Cedrella Cluster,Cedrella,Star Cluster
Alerce Lyra,Alerce,Lyra
Mahogalactica,Mahogany,Galactic Center
Pinwheelnut,Pinwheel,Brazil Nut
Tarantularch,Tarantula Nebula,Larch
Ceibomeda,Ceiba,Andromeda
Pau Brassiopeia,Pau Brasil,Cassiopeia
Araucarion,Araucaria,Orion
Quebrachole,Quebracho,Black Hole
Lapachoway,Lapacho,Milky Way
Guanasar,Guanacaste,Quasar
Jacaranebula,Jacaranda,Nebula
Ceiba Centauri,Ceiba,Alpha Centauri
Brazilbeehive,Brazil Nut,Beehive Cluster
Monkeypuzzar,Monkey Puzzle,Quasar
Quebrachomeda,Quebracho,Andromeda
Lapachydes,Lapacho,Hyades
Rubberway,Rubber Tree,Milky Way
Cashewsar,Cashew,Quasar
Nutmeda,Brazil Nut,Andromeda
Cecropiagalaxy,Cecropia,Galaxy
Balsanova,Balsa,Supernova
Rosettewood,Rosette,Rosewood
Ipegalaxy,Ipe,Galaxy
Kapoknebula,Kapok,Nebula
Cedrellaxy,Cedrella,Galaxy
Alercentauri,Alerce,Centaurus
Mahogacrab,Mahogany,Crab Nebula
Eaglebrasil,Eagle Nebula,Pau Brasil
Tarantuleia,Tarantula Nebula,Ceiba
Whirlpoolnut,Whirlpool,Brazil Nut
Araucagalaxy,Araucaria,Galaxy
Quebrachiades,Quebracho,Pleiades
Lapachomeda,Lapacho,Andromeda
Guanacasteroid,Guanacaste,Asteroid
Jacarandromeda,Jacaranda,Andromeda
Ceibulsar,Ceiba,Pulsar
Tadpolebrasil,Tadpole,Pau Brasil
Monkeynova,Monkey Puzzle,Supernova
Quebrachygnus,Quebracho,Cygnus
Lapachoacentauri,Lapacho,Alpha Centauri
Rubberhole,Rubber Tree,Black Hole
Cashewnova,Cashew,Supernova
Brazilbutterfly,Brazil Nut,Butterfly Cluster
Cecropiomeda,Cecropia,Andromeda
Balsagittarius,Balsa,Sagittarius
Rosewoodrion,Rosewood,Orion
Ipeleiades,Ipe,Pleiades
Kapoksombrero,Kapok,Sombrero Galaxy
Cedrellipse,Cedrella,Elliptical Galaxy
Alercegalaxy,Alerce,Galaxy
Mahogaring,Mahogany,Ring Nebula
Cartwheeiba,Cartwheel,Ceiba
Pau Brasilian,Pau Brasil,Cassiopeian
Araucarionebula,Araucaria,Orion Nebula
Quebrasar,Quebracho,Quasar
Lapachomega,Lapacho,Omega Centauri
Guanacastway,Guanacaste,Milky Way
Jacaranovae,Jacaranda,Supernova
Ceibachomeda,Ceiba,Andromeda
Sunflowerbrasil,Sunflower Galaxy,Pau Brasil
Monkeypuzzalaxy,Monkey Puzzle,Galaxy
Quebrachoxima,Quebracho,Proxima
Lapachosar,Lapacho,Quasar
Rubberbula,Rubber Tree,Nebula
Cashewcluster,Cashew,Star Cluster
Nutway,Brazil Nut,Milky Way
Cecropianebula,Cecropia,Nebula
Balsagara,Balsa,Sagittarius A*
Rosewoodraco,Rosewood,Draco
Ipelipse,Ipe,Ellipse
Kapokagoon,Kapok,Lagoon Nebula
Cedrellaneta,Cedrella,Planetary Nebula
Alercerion,Alerce,Orion
Mahogacarina,Mahogany,Carina
Trifidnut,Trifid Nebula,Brazil Nut
Tarantulacho,Tarantula,Lapacho
Ceibanova,Ceiba,Supernova
Pau Brasilion,Pau Brasil,Orion
Araucariades,Araucaria,Pleiades
Quebrachoelix,Quebracho,Helix
Lapacholia,Lapacho,Magnolia
Guanacasar,Guanacaste,Quasar
Jacarandromeda,Jacaranda,Andromeda
Ceibulsar,Ceiba,Pulsar
Titan Brasil,Titan,Pau Brasil
Monkeynebula,Monkey Puzzle,Nebula
Quebrachomeda,Quebracho,Andromeda
Lapachogalaxy,Lapacho,Galaxy
Rubbersar,Rubber Tree,Quasar
Cashewmeda,Cashew,Andromeda
Nutcluster,Brazil Nut,Cluster
Cecropioway,Cecropia,Milky Way
Balsanebula,Balsa,Nebula
Rosewoodway,Rosewood,Milky Way
Ipegalaxy,Ipe,Galaxy
Kapoksar,Kapok,Quasar
Cedrellaxy,Cedrella,Galaxy
Alercenova,Alerce,Supernova
Mahogalactic,Mahogany,Galactic
Sculptorbrasil,Sculptor Galaxy,Pau Brasil
Ceibomeda,Ceiba,Andromeda
Araucarionway,Araucaria,Milky Way
Quebrachion,Quebracho,Orion
Lapachohole,Lapacho,Black Hole
Guanacastromeda,Guanacaste,Andromeda
Jacaranebula,Jacaranda,Nebula
Ceibagalaxy,Ceiba,Galaxy
Europa Brasil,Europa,Pau Brasil
Monkeypuzzar,Monkey Puzzle,Quasar
Quebrachnova,Quebracho,Supernova
Lapachoides,Lapacho,Hyades
Rubbermeda,Rubber Tree,Andromeda
Cashewnova,Cashew,Supernova
Nutway,Brazil Nut,Milky Way
Cecropiagalaxy,Cecropia,Galaxy
Balsaquila,Balsa,Aquila
Rosewoodsar,Rosewood,Quasar
Ipecentauri,Ipe,Alpha Centauri
Kapokgalaxy,Kapok,Galaxy
Cedrellaneta,Cedrella,Planetary Nebula
Alercemeda,Alerce,Andromeda
Mahoganova,Mahogany,Supernova
Boomerangbrasil,Boomerang Nebula,Pau Brasil
Ceibulsar,Ceiba,Pulsar
Pau Brasilades,Pau Brasil,Pleiades
Araucagalaxy,Araucaria,Galaxy
Quebrachomeda,Quebracho,Andromeda
Lapachosar,Lapacho,Quasar
Guanacastellum,Guanacaste,Magellanic
Jacaranovae,Jacaranda,Supernova
Ceibaway,Ceiba,Milky Way
Enceladnut,Enceladus,Brazil Nut
Monkeynebula,Monkey Puzzle,Nebula
Quebrachogalaxy,Quebracho,Galaxy
Lapachonova,Lapacho,Supernova
Rubberion,Rubber Tree,Orion
Cashewgalaxy,Cashew,Galaxy
Nutsagittarius,Brazil Nut,Sagittarius
Cecropia A*,Cecropia,Sagittarius A*
Balsanova,Balsa,Supernova
Rosewoodmeda,Rosewood,Andromeda
Ipesar,Ipe,Quasar
Kapokway,Kapok,Milky Way
Cedrellipse,Cedrella,Ellipse
Alercenebula,Alerce,Nebula
Mahogaring,Mahogany,Ring Nebula
Cat's Eye Brasil,Cat's Eye,Pau Brasil
Ceibomeda,Ceiba,Andromeda
Araucarionebula,Araucaria,Orion Nebula
Quebrachoides,Quebracho,Hyades
Lapachogalaxy,Lapacho,Galaxy
Guanacastar,Guanacaste,Quasar
Jacarandromeda,Jacaranda,Andromeda
Ceibaneta,Ceiba,Planetary Nebula
Ganymede Brasil,Ganymede,Pau Brasil
Monkeypuzzalaxy,Monkey Puzzle,Galaxy
Quebrachonova,Quebracho,Supernova
Lapachomeda,Lapacho,Andromeda
Rubbersar,Rubber Tree,Quasar
Cashewnebula,Cashew,Nebula
Nutgalaxy,Brazil Nut,Galaxy
Cecropiomeda,Cecropia,Andromeda
Balsagittarius,Balsa,Sagittarius
Rosewoodion,Rosewood,Orion
Ipeleiades,Ipe,Pleiades
Kapoknova,Kapok,Supernova
Cedrellaxy,Cedrella,Galaxy
Alercegalaxy,Alerce,Galaxy
Mahogacrab,Mahogany,Crab
Dumbbell Brasil,Dumbbell Nebula,Pau Brasil
Ceibulsar,Ceiba,Pulsar
Pau Brasilway,Pau Brasil,Milky Way
Araucarina,Araucaria,Carina
Quebrachogalaxy,Quebracho,Galaxy
Lapachosar,Lapacho,Quasar
Guanacastromeda,Guanacaste,Andromeda
Jacaranebula,Jacaranda,Nebula
Ceibanova,Ceiba,Supernova
Io Brasil,Io,Pau Brasil
Monkeypuzzar,Monkey Puzzle,Quasar
Quebrachomeda,Quebracho,Andromeda
Lapachogalaxy,Lapacho,Galaxy
Rubbernova,Rubber Tree,Supernova
Cashewmeda,Cashew,Andromeda
Nutnebula,Brazil Nut,Nebula
Cecropiagalaxy,Cecropia,Galaxy
Balsasar,Balsa,Quasar
Rosewoodway,Rosewood,Milky Way
Ipegalaxy,Ipe,Galaxy
Kapokmeda,Kapok,Andromeda
Cedrellaneta,Cedrella,Planetary Nebula
Alercenova,Alerce,Supernova
Mahogalagoon,Mahogany,Lagoon
Crescent Brasil,Crescent Nebula,Pau Brasil
Ceibagalaxy,Ceiba,Galaxy
Araucarionway,Araucaria,Milky Way
Quebrachion,Quebracho,Orion
Lapachohole,Lapacho,Black Hole
Guanacastar,Guanacaste,Quasar
Jacaranovo,Jacaranda,Supernova
Ceibomeda,Ceiba,Andromeda
Callisto Brasil,Callisto,Pau Brasil
Monkeynebula,Monkey Puzzle,Nebula
Quebrachogalaxy,Quebracho,Galaxy
Lapachonova,Lapacho,Supernova
Rubbermeda,Rubber Tree,Andromeda
Cashewgalaxy,Cashew,Galaxy
Nutway,Brazil Nut,Milky Way
Cecropiomeda,Cecropia,Andromeda
Balsanebula,Balsa,Nebula
Rosewoodsar,Rosewood,Quasar
Ipecentauri,Ipe,Centauri
Kapokgalaxy,Kapok,Galaxy
Cedrellaxy,Cedrella,Galaxy
Alercemeda,Alerce,Andromeda
Mahoganova,Mahogany,Supernova
Flame Brasil,Flame Nebula,Pau Brasil
Ceibulsar,Ceiba,Pulsar
Pau Brasilion,Pau Brasil,Orion
Araucagalaxy,Araucaria,Galaxy
Quebrachomeda,Quebracho,Andromeda
Lapachosar,Lapacho,Quasar
Guanacastway,Guanacaste,Milky Way
Jacarandromeda,Jacaranda,Andromeda
Ceibanova,Ceiba,Supernova
Veil Brasil,Veil Nebula,Pau Brasil
Monkeypuzzalaxy,Monkey Puzzle,Galaxy
Quebrachonova,Quebracho,Supernova
Lapachogalaxy,Lapacho,Galaxy
Rubberion,Rubber Tree,Orion
Cashewnova,Cashew,Supernova
Nutgalaxy,Brazil Nut,Galaxy
Cecropiagalaxy,Cecropia,Galaxy
Balsomeda,Balsa,Andromeda
Rosewoodway,Rosewood,Milky Way
Ipesar,Ipe,Quasar
Kapoknebula,Kapok,Nebula
Cedrellaneta,Cedrella,Planetary Nebula
Alercenebula,Alerce,Nebula
Mahogaring,Mahogany,Ring
Pelican Brasil,Pelican Nebula,Pau Brasil
Ceibagalaxy,Ceiba,Galaxy
Araucarionebula,Araucaria,Orion Nebula
Quebrachoides,Quebracho,Hyades
Lapachomeda,Lapacho,Andromeda
Guanacastar,Guanacaste,Quasar
Jacaranovo,Jacaranda,Supernova
Ceibulsar,Ceiba,Pulsar
Cone Brasil,Cone Nebula,Pau Brasil
Monkeypuzzar,Monkey Puzzle,Quasar
Quebrachogalaxy,Quebracho,Galaxy
Lapachosar,Lapacho,Quasar
Rubbernova,Rubber Tree,Supernova
Cashewmeda,Cashew,Andromeda
Nutway,Brazil Nut,Milky Way
Cecropiomeda,Cecropia,Andromeda
Balsagalaxy,Balsa,Galaxy
Rosewoodion,Rosewood,Orion
Ipeleiades,Ipe,Pleiades
Kapokgalaxy,Kapok,Galaxy
Cedrellaxy,Cedrella,Galaxy
Alercegalaxy,Alerce,Galaxy
Mahoganova,Mahogany,Supernova
Witch Head Brasil,Witch Head,Pau Brasil
Ceiboway,Ceiba,Milky Way
Pau Brasilmeda,Pau Brasil,Andromeda
Araucarina,Araucaria,Carina
Quebrachonebula,Quebracho,Nebula
Lapachogalaxy,Lapacho,Galaxy
Guanacastromeda,Guanacaste,Andromeda
Jacaranebula,Jacaranda,Nebula
Ceibanova,Ceiba,Supernova
Iris Brasil,Iris Nebula,Pau Brasil
Monkeynova,Monkey Puzzle,Supernova
Quebrachomeda,Quebracho,Andromeda
Lapachosar,Lapacho,Quasar
Rubbermeda,Rubber Tree,Andromeda
Cashewgalaxy,Cashew,Galaxy
Nutnebula,Brazil Nut,Nebula
Cecropiagalaxy,Cecropia,Galaxy
Balsanovo,Balsa,Supernova
Rosewoodway,Rosewood,Milky Way
Ipegalaxy,Ipe,Galaxy
Kapokmeda,Kapok,Andromeda
Cedrellaneta,Cedrella,Planetary Nebula
Alercenova,Alerce,Supernova
Mahogagalaxy,Mahogany,Galaxy
Bubble Brasil,Bubble Nebula,Pau Brasil
Ceibomeda,Ceiba,Andromeda
Araucarionway,Araucaria,Milky Way
Quebrachion,Quebracho,Orion
Lapachohole,Lapacho,Black Hole
Guanacastar,Guanacaste,Quasar
Jacaranovo,Jacaranda,Supernova
Ceibagalaxy,Ceiba,Galaxy
North America Brasil,North America,Pau Brasil
Monkeypuzzar,Monkey Puzzle,Quasar
Quebrachogalaxy,Quebracho,Galaxy
Lapachonova,Lapacho,Supernova
Rubbersar,Rubber Tree,Quasar
Cashewnebula,Cashew,Nebula
Nutgalaxy,Brazil Nut,Galaxy
Cecropiomeda,Cecropia,Andromeda
Balsoway,Balsa,Milky Way
Rosewoodsar,Rosewood,Quasar
Ipecentauri,Ipe,Centauri
Kapokgalaxy,Kapok,Galaxy
Cedrellaxy,Cedrella,Galaxy
Alercemeda,Alerce,Andromeda
Mahoganova,Mahogany,Supernova
Pacman Brasil,Pacman Nebula,Pau Brasil
Ceibulsar,Ceiba,Pulsar
Pau Brasilades,Pau Brasil,Pleiades
Araucagalaxy,Araucaria,Galaxy
Quebrachomeda,Quebracho,Andromeda
Lapachosar,Lapacho,Quasar
Guanacastway,Guanacaste,Milky Way
Jacarandromeda,Jacaranda,Andromeda
Ceibanova,Ceiba,Supernova
Soul Brasil,Soul Nebula,Pau Brasil
Monkeynebula,Monkey Puzzle,Nebula
Quebrachonova,Quebracho,Supernova
Lapachogalaxy,Lapacho,Galaxy
Rubberion,Rubber Tree,Orion
Cashewnova,Cashew,Supernova
Nutway,Brazil Nut,Milky Way
Cecropiagalaxy,Cecropia,Galaxy
Balsameda,Balsa,Andromeda
Rosewoodway,Rosewood,Milky Way
Ipesar,Ipe,Quasar
Kapoknebula,Kapok,Nebula
Cedrellaneta,Cedrella,Planetary Nebula
Alercenebula,Alerce,Nebula
Mahogaring,Mahogany,Ring
Heart Brasil,Heart Nebula,Pau Brasil
Ceibagalaxy,Ceiba,Galaxy
Araucarionebula,Araucaria,Orion Nebula
Quebrachoides,Quebracho,Hyades
Lapachomeda,Lapacho,Andromeda
Guanacastar,Guanacaste,Quasar
Jacaranovo,Jacaranda,Supernova
Ceiboway,Ceiba,Milky Way
Elephant Trunk Brasil,Elephant Trunk,Pau Brasil
Monkeypuzzalaxy,Monkey Puzzle,Galaxy
Quebrachogalaxy,Quebracho,Galaxy
Lapachosar,Lapacho,Quasar
Rubbernova,Rubber Tree,Supernova
Cashewmeda,Cashew,Andromeda
Nutgalaxy,Brazil Nut,Galaxy
Cecropiomeda,Cecropia,Andromeda
Balsagalaxy,Balsa,Galaxy
Rosewoodion,Rosewood,Orion
Ipeleiades,Ipe,Pleiades
Kapokgalaxy,Kapok,Galaxy
Cedrellaxy,Cedrella,Galaxy
Alercegalaxy,Alerce,Galaxy
Mahoganova,Mahogany,Supernova
Omega Brasil,Omega Nebula,Pau Brasil
Ceibulsar,Ceiba,Pulsar
Pau Brasilion,Pau Brasil,Orion
Araucarina,Araucaria,Carina
Quebrachonebula,Quebracho,Nebula
Lapachogalaxy,Lapacho,Galaxy
Guanacastromeda,Guanacaste,Andromeda
Jacaranebula,Jacaranda,Nebula
Ceibanova,Ceiba,Supernova
Blinking Brasil,Blinking Nebula,Pau Brasil
Monkeypuzzar,Monkey Puzzle,Quasar
Quebrachomeda,Quebracho,Andromeda
Lapachosar,Lapacho,Quasar
Rubbermeda,Rubber Tree,Andromeda
Cashewgalaxy,Cashew,Galaxy
Nutnebula,Brazil Nut,Nebula
Cecropiagalaxy,Cecropia,Galaxy
Balsanovo,Balsa,Supernova
Rosewoodway,Rosewood,Milky Way
Ipegalaxy,Ipe,Galaxy
Kapokmeda,Kapok,Andromeda
Cedrellaneta,Cedrella,Planetary Nebula
Alercenova,Alerce,Supernova
Mahogagalaxy,Mahogany,Galaxy
Ghost Brasil,Ghost Nebula,Pau Brasil
Ceibomeda,Ceiba,Andromeda
Araucarionway,Araucaria,Milky Way
Quebrachion,Quebracho,Orion
Lapachohole,Lapacho,Black Hole
Guanacastar,Guanacaste,Quasar
Jacaranovo,Jacaranda,Supernova
Ceibagalaxy,Ceiba,Galaxy
Eskimo Brasil,Eskimo Nebula,Pau Brasil
Monkeynebula,Monkey Puzzle,Nebula
Quebrachogalaxy,Quebracho,Galaxy
Lapachonova,Lapacho,Supernova
Rubbersar,Rubber Tree,Quasar
Cashewnebula,Cashew,Nebula
Nutgalaxy,Brazil Nut,Galaxy
Cecropioway,Cecropia,Milky Way
1 Name Component_1 Component_2
2 Pleiadeiba Pleiades Ceiba
3 Pau Brasilica Pau Brasil Crab Nebula
4 Araucarina Araucaria Carina Nebula
5 Quebracho X-1 Quebracho Cygnus X-1
6 Lapachomeda Lapacho Andromeda
7 Guanacastellum Guanacaste Magellanic Cloud
8 Trianguanum Triangulum Guanacaste
9 Jacarandromeda Jacaranda Andromeda
10 Ceibalaxy Ceiba Galaxy
11 Horsehead Brasil Horsehead Pau Brasil
12 Monkeypuzzle Void Monkey Puzzle Cosmic Void
13 Quebrachion Quebracho Orion
14 Lapachosar Lapacho Quasar
15 Rubber Rosette Rubber Tree Rosette Nebula
16 Cashewmeda Cashew Andromeda
17 Brazil Nutnova Brazil Nut Supernova
18 Cecropiades Cecropia Pleiades
19 Balsombrero Balsa Sombrero Galaxy
20 Rosewoodway Rosewood Milky Way
21 Ipe Helix Ipe Helix Nebula
22 Kapoksagittarius Kapok Sagittarius
23 Cedrella Cluster Cedrella Star Cluster
24 Alerce Lyra Alerce Lyra
25 Mahogalactica Mahogany Galactic Center
26 Pinwheelnut Pinwheel Brazil Nut
27 Tarantularch Tarantula Nebula Larch
28 Ceibomeda Ceiba Andromeda
29 Pau Brassiopeia Pau Brasil Cassiopeia
30 Araucarion Araucaria Orion
31 Quebrachole Quebracho Black Hole
32 Lapachoway Lapacho Milky Way
33 Guanasar Guanacaste Quasar
34 Jacaranebula Jacaranda Nebula
35 Ceiba Centauri Ceiba Alpha Centauri
36 Brazilbeehive Brazil Nut Beehive Cluster
37 Monkeypuzzar Monkey Puzzle Quasar
38 Quebrachomeda Quebracho Andromeda
39 Lapachydes Lapacho Hyades
40 Rubberway Rubber Tree Milky Way
41 Cashewsar Cashew Quasar
42 Nutmeda Brazil Nut Andromeda
43 Cecropiagalaxy Cecropia Galaxy
44 Balsanova Balsa Supernova
45 Rosettewood Rosette Rosewood
46 Ipegalaxy Ipe Galaxy
47 Kapoknebula Kapok Nebula
48 Cedrellaxy Cedrella Galaxy
49 Alercentauri Alerce Centaurus
50 Mahogacrab Mahogany Crab Nebula
51 Eaglebrasil Eagle Nebula Pau Brasil
52 Tarantuleia Tarantula Nebula Ceiba
53 Whirlpoolnut Whirlpool Brazil Nut
54 Araucagalaxy Araucaria Galaxy
55 Quebrachiades Quebracho Pleiades
56 Lapachomeda Lapacho Andromeda
57 Guanacasteroid Guanacaste Asteroid
58 Jacarandromeda Jacaranda Andromeda
59 Ceibulsar Ceiba Pulsar
60 Tadpolebrasil Tadpole Pau Brasil
61 Monkeynova Monkey Puzzle Supernova
62 Quebrachygnus Quebracho Cygnus
63 Lapachoacentauri Lapacho Alpha Centauri
64 Rubberhole Rubber Tree Black Hole
65 Cashewnova Cashew Supernova
66 Brazilbutterfly Brazil Nut Butterfly Cluster
67 Cecropiomeda Cecropia Andromeda
68 Balsagittarius Balsa Sagittarius
69 Rosewoodrion Rosewood Orion
70 Ipeleiades Ipe Pleiades
71 Kapoksombrero Kapok Sombrero Galaxy
72 Cedrellipse Cedrella Elliptical Galaxy
73 Alercegalaxy Alerce Galaxy
74 Mahogaring Mahogany Ring Nebula
75 Cartwheeiba Cartwheel Ceiba
76 Pau Brasilian Pau Brasil Cassiopeian
77 Araucarionebula Araucaria Orion Nebula
78 Quebrasar Quebracho Quasar
79 Lapachomega Lapacho Omega Centauri
80 Guanacastway Guanacaste Milky Way
81 Jacaranovae Jacaranda Supernova
82 Ceibachomeda Ceiba Andromeda
83 Sunflowerbrasil Sunflower Galaxy Pau Brasil
84 Monkeypuzzalaxy Monkey Puzzle Galaxy
85 Quebrachoxima Quebracho Proxima
86 Lapachosar Lapacho Quasar
87 Rubberbula Rubber Tree Nebula
88 Cashewcluster Cashew Star Cluster
89 Nutway Brazil Nut Milky Way
90 Cecropianebula Cecropia Nebula
91 Balsagara Balsa Sagittarius A*
92 Rosewoodraco Rosewood Draco
93 Ipelipse Ipe Ellipse
94 Kapokagoon Kapok Lagoon Nebula
95 Cedrellaneta Cedrella Planetary Nebula
96 Alercerion Alerce Orion
97 Mahogacarina Mahogany Carina
98 Trifidnut Trifid Nebula Brazil Nut
99 Tarantulacho Tarantula Lapacho
100 Ceibanova Ceiba Supernova
101 Pau Brasilion Pau Brasil Orion
102 Araucariades Araucaria Pleiades
103 Quebrachoelix Quebracho Helix
104 Lapacholia Lapacho Magnolia
105 Guanacasar Guanacaste Quasar
106 Jacarandromeda Jacaranda Andromeda
107 Ceibulsar Ceiba Pulsar
108 Titan Brasil Titan Pau Brasil
109 Monkeynebula Monkey Puzzle Nebula
110 Quebrachomeda Quebracho Andromeda
111 Lapachogalaxy Lapacho Galaxy
112 Rubbersar Rubber Tree Quasar
113 Cashewmeda Cashew Andromeda
114 Nutcluster Brazil Nut Cluster
115 Cecropioway Cecropia Milky Way
116 Balsanebula Balsa Nebula
117 Rosewoodway Rosewood Milky Way
118 Ipegalaxy Ipe Galaxy
119 Kapoksar Kapok Quasar
120 Cedrellaxy Cedrella Galaxy
121 Alercenova Alerce Supernova
122 Mahogalactic Mahogany Galactic
123 Sculptorbrasil Sculptor Galaxy Pau Brasil
124 Ceibomeda Ceiba Andromeda
125 Araucarionway Araucaria Milky Way
126 Quebrachion Quebracho Orion
127 Lapachohole Lapacho Black Hole
128 Guanacastromeda Guanacaste Andromeda
129 Jacaranebula Jacaranda Nebula
130 Ceibagalaxy Ceiba Galaxy
131 Europa Brasil Europa Pau Brasil
132 Monkeypuzzar Monkey Puzzle Quasar
133 Quebrachnova Quebracho Supernova
134 Lapachoides Lapacho Hyades
135 Rubbermeda Rubber Tree Andromeda
136 Cashewnova Cashew Supernova
137 Nutway Brazil Nut Milky Way
138 Cecropiagalaxy Cecropia Galaxy
139 Balsaquila Balsa Aquila
140 Rosewoodsar Rosewood Quasar
141 Ipecentauri Ipe Alpha Centauri
142 Kapokgalaxy Kapok Galaxy
143 Cedrellaneta Cedrella Planetary Nebula
144 Alercemeda Alerce Andromeda
145 Mahoganova Mahogany Supernova
146 Boomerangbrasil Boomerang Nebula Pau Brasil
147 Ceibulsar Ceiba Pulsar
148 Pau Brasilades Pau Brasil Pleiades
149 Araucagalaxy Araucaria Galaxy
150 Quebrachomeda Quebracho Andromeda
151 Lapachosar Lapacho Quasar
152 Guanacastellum Guanacaste Magellanic
153 Jacaranovae Jacaranda Supernova
154 Ceibaway Ceiba Milky Way
155 Enceladnut Enceladus Brazil Nut
156 Monkeynebula Monkey Puzzle Nebula
157 Quebrachogalaxy Quebracho Galaxy
158 Lapachonova Lapacho Supernova
159 Rubberion Rubber Tree Orion
160 Cashewgalaxy Cashew Galaxy
161 Nutsagittarius Brazil Nut Sagittarius
162 Cecropia A* Cecropia Sagittarius A*
163 Balsanova Balsa Supernova
164 Rosewoodmeda Rosewood Andromeda
165 Ipesar Ipe Quasar
166 Kapokway Kapok Milky Way
167 Cedrellipse Cedrella Ellipse
168 Alercenebula Alerce Nebula
169 Mahogaring Mahogany Ring Nebula
170 Cat's Eye Brasil Cat's Eye Pau Brasil
171 Ceibomeda Ceiba Andromeda
172 Araucarionebula Araucaria Orion Nebula
173 Quebrachoides Quebracho Hyades
174 Lapachogalaxy Lapacho Galaxy
175 Guanacastar Guanacaste Quasar
176 Jacarandromeda Jacaranda Andromeda
177 Ceibaneta Ceiba Planetary Nebula
178 Ganymede Brasil Ganymede Pau Brasil
179 Monkeypuzzalaxy Monkey Puzzle Galaxy
180 Quebrachonova Quebracho Supernova
181 Lapachomeda Lapacho Andromeda
182 Rubbersar Rubber Tree Quasar
183 Cashewnebula Cashew Nebula
184 Nutgalaxy Brazil Nut Galaxy
185 Cecropiomeda Cecropia Andromeda
186 Balsagittarius Balsa Sagittarius
187 Rosewoodion Rosewood Orion
188 Ipeleiades Ipe Pleiades
189 Kapoknova Kapok Supernova
190 Cedrellaxy Cedrella Galaxy
191 Alercegalaxy Alerce Galaxy
192 Mahogacrab Mahogany Crab
193 Dumbbell Brasil Dumbbell Nebula Pau Brasil
194 Ceibulsar Ceiba Pulsar
195 Pau Brasilway Pau Brasil Milky Way
196 Araucarina Araucaria Carina
197 Quebrachogalaxy Quebracho Galaxy
198 Lapachosar Lapacho Quasar
199 Guanacastromeda Guanacaste Andromeda
200 Jacaranebula Jacaranda Nebula
201 Ceibanova Ceiba Supernova
202 Io Brasil Io Pau Brasil
203 Monkeypuzzar Monkey Puzzle Quasar
204 Quebrachomeda Quebracho Andromeda
205 Lapachogalaxy Lapacho Galaxy
206 Rubbernova Rubber Tree Supernova
207 Cashewmeda Cashew Andromeda
208 Nutnebula Brazil Nut Nebula
209 Cecropiagalaxy Cecropia Galaxy
210 Balsasar Balsa Quasar
211 Rosewoodway Rosewood Milky Way
212 Ipegalaxy Ipe Galaxy
213 Kapokmeda Kapok Andromeda
214 Cedrellaneta Cedrella Planetary Nebula
215 Alercenova Alerce Supernova
216 Mahogalagoon Mahogany Lagoon
217 Crescent Brasil Crescent Nebula Pau Brasil
218 Ceibagalaxy Ceiba Galaxy
219 Araucarionway Araucaria Milky Way
220 Quebrachion Quebracho Orion
221 Lapachohole Lapacho Black Hole
222 Guanacastar Guanacaste Quasar
223 Jacaranovo Jacaranda Supernova
224 Ceibomeda Ceiba Andromeda
225 Callisto Brasil Callisto Pau Brasil
226 Monkeynebula Monkey Puzzle Nebula
227 Quebrachogalaxy Quebracho Galaxy
228 Lapachonova Lapacho Supernova
229 Rubbermeda Rubber Tree Andromeda
230 Cashewgalaxy Cashew Galaxy
231 Nutway Brazil Nut Milky Way
232 Cecropiomeda Cecropia Andromeda
233 Balsanebula Balsa Nebula
234 Rosewoodsar Rosewood Quasar
235 Ipecentauri Ipe Centauri
236 Kapokgalaxy Kapok Galaxy
237 Cedrellaxy Cedrella Galaxy
238 Alercemeda Alerce Andromeda
239 Mahoganova Mahogany Supernova
240 Flame Brasil Flame Nebula Pau Brasil
241 Ceibulsar Ceiba Pulsar
242 Pau Brasilion Pau Brasil Orion
243 Araucagalaxy Araucaria Galaxy
244 Quebrachomeda Quebracho Andromeda
245 Lapachosar Lapacho Quasar
246 Guanacastway Guanacaste Milky Way
247 Jacarandromeda Jacaranda Andromeda
248 Ceibanova Ceiba Supernova
249 Veil Brasil Veil Nebula Pau Brasil
250 Monkeypuzzalaxy Monkey Puzzle Galaxy
251 Quebrachonova Quebracho Supernova
252 Lapachogalaxy Lapacho Galaxy
253 Rubberion Rubber Tree Orion
254 Cashewnova Cashew Supernova
255 Nutgalaxy Brazil Nut Galaxy
256 Cecropiagalaxy Cecropia Galaxy
257 Balsomeda Balsa Andromeda
258 Rosewoodway Rosewood Milky Way
259 Ipesar Ipe Quasar
260 Kapoknebula Kapok Nebula
261 Cedrellaneta Cedrella Planetary Nebula
262 Alercenebula Alerce Nebula
263 Mahogaring Mahogany Ring
264 Pelican Brasil Pelican Nebula Pau Brasil
265 Ceibagalaxy Ceiba Galaxy
266 Araucarionebula Araucaria Orion Nebula
267 Quebrachoides Quebracho Hyades
268 Lapachomeda Lapacho Andromeda
269 Guanacastar Guanacaste Quasar
270 Jacaranovo Jacaranda Supernova
271 Ceibulsar Ceiba Pulsar
272 Cone Brasil Cone Nebula Pau Brasil
273 Monkeypuzzar Monkey Puzzle Quasar
274 Quebrachogalaxy Quebracho Galaxy
275 Lapachosar Lapacho Quasar
276 Rubbernova Rubber Tree Supernova
277 Cashewmeda Cashew Andromeda
278 Nutway Brazil Nut Milky Way
279 Cecropiomeda Cecropia Andromeda
280 Balsagalaxy Balsa Galaxy
281 Rosewoodion Rosewood Orion
282 Ipeleiades Ipe Pleiades
283 Kapokgalaxy Kapok Galaxy
284 Cedrellaxy Cedrella Galaxy
285 Alercegalaxy Alerce Galaxy
286 Mahoganova Mahogany Supernova
287 Witch Head Brasil Witch Head Pau Brasil
288 Ceiboway Ceiba Milky Way
289 Pau Brasilmeda Pau Brasil Andromeda
290 Araucarina Araucaria Carina
291 Quebrachonebula Quebracho Nebula
292 Lapachogalaxy Lapacho Galaxy
293 Guanacastromeda Guanacaste Andromeda
294 Jacaranebula Jacaranda Nebula
295 Ceibanova Ceiba Supernova
296 Iris Brasil Iris Nebula Pau Brasil
297 Monkeynova Monkey Puzzle Supernova
298 Quebrachomeda Quebracho Andromeda
299 Lapachosar Lapacho Quasar
300 Rubbermeda Rubber Tree Andromeda
301 Cashewgalaxy Cashew Galaxy
302 Nutnebula Brazil Nut Nebula
303 Cecropiagalaxy Cecropia Galaxy
304 Balsanovo Balsa Supernova
305 Rosewoodway Rosewood Milky Way
306 Ipegalaxy Ipe Galaxy
307 Kapokmeda Kapok Andromeda
308 Cedrellaneta Cedrella Planetary Nebula
309 Alercenova Alerce Supernova
310 Mahogagalaxy Mahogany Galaxy
311 Bubble Brasil Bubble Nebula Pau Brasil
312 Ceibomeda Ceiba Andromeda
313 Araucarionway Araucaria Milky Way
314 Quebrachion Quebracho Orion
315 Lapachohole Lapacho Black Hole
316 Guanacastar Guanacaste Quasar
317 Jacaranovo Jacaranda Supernova
318 Ceibagalaxy Ceiba Galaxy
319 North America Brasil North America Pau Brasil
320 Monkeypuzzar Monkey Puzzle Quasar
321 Quebrachogalaxy Quebracho Galaxy
322 Lapachonova Lapacho Supernova
323 Rubbersar Rubber Tree Quasar
324 Cashewnebula Cashew Nebula
325 Nutgalaxy Brazil Nut Galaxy
326 Cecropiomeda Cecropia Andromeda
327 Balsoway Balsa Milky Way
328 Rosewoodsar Rosewood Quasar
329 Ipecentauri Ipe Centauri
330 Kapokgalaxy Kapok Galaxy
331 Cedrellaxy Cedrella Galaxy
332 Alercemeda Alerce Andromeda
333 Mahoganova Mahogany Supernova
334 Pacman Brasil Pacman Nebula Pau Brasil
335 Ceibulsar Ceiba Pulsar
336 Pau Brasilades Pau Brasil Pleiades
337 Araucagalaxy Araucaria Galaxy
338 Quebrachomeda Quebracho Andromeda
339 Lapachosar Lapacho Quasar
340 Guanacastway Guanacaste Milky Way
341 Jacarandromeda Jacaranda Andromeda
342 Ceibanova Ceiba Supernova
343 Soul Brasil Soul Nebula Pau Brasil
344 Monkeynebula Monkey Puzzle Nebula
345 Quebrachonova Quebracho Supernova
346 Lapachogalaxy Lapacho Galaxy
347 Rubberion Rubber Tree Orion
348 Cashewnova Cashew Supernova
349 Nutway Brazil Nut Milky Way
350 Cecropiagalaxy Cecropia Galaxy
351 Balsameda Balsa Andromeda
352 Rosewoodway Rosewood Milky Way
353 Ipesar Ipe Quasar
354 Kapoknebula Kapok Nebula
355 Cedrellaneta Cedrella Planetary Nebula
356 Alercenebula Alerce Nebula
357 Mahogaring Mahogany Ring
358 Heart Brasil Heart Nebula Pau Brasil
359 Ceibagalaxy Ceiba Galaxy
360 Araucarionebula Araucaria Orion Nebula
361 Quebrachoides Quebracho Hyades
362 Lapachomeda Lapacho Andromeda
363 Guanacastar Guanacaste Quasar
364 Jacaranovo Jacaranda Supernova
365 Ceiboway Ceiba Milky Way
366 Elephant Trunk Brasil Elephant Trunk Pau Brasil
367 Monkeypuzzalaxy Monkey Puzzle Galaxy
368 Quebrachogalaxy Quebracho Galaxy
369 Lapachosar Lapacho Quasar
370 Rubbernova Rubber Tree Supernova
371 Cashewmeda Cashew Andromeda
372 Nutgalaxy Brazil Nut Galaxy
373 Cecropiomeda Cecropia Andromeda
374 Balsagalaxy Balsa Galaxy
375 Rosewoodion Rosewood Orion
376 Ipeleiades Ipe Pleiades
377 Kapokgalaxy Kapok Galaxy
378 Cedrellaxy Cedrella Galaxy
379 Alercegalaxy Alerce Galaxy
380 Mahoganova Mahogany Supernova
381 Omega Brasil Omega Nebula Pau Brasil
382 Ceibulsar Ceiba Pulsar
383 Pau Brasilion Pau Brasil Orion
384 Araucarina Araucaria Carina
385 Quebrachonebula Quebracho Nebula
386 Lapachogalaxy Lapacho Galaxy
387 Guanacastromeda Guanacaste Andromeda
388 Jacaranebula Jacaranda Nebula
389 Ceibanova Ceiba Supernova
390 Blinking Brasil Blinking Nebula Pau Brasil
391 Monkeypuzzar Monkey Puzzle Quasar
392 Quebrachomeda Quebracho Andromeda
393 Lapachosar Lapacho Quasar
394 Rubbermeda Rubber Tree Andromeda
395 Cashewgalaxy Cashew Galaxy
396 Nutnebula Brazil Nut Nebula
397 Cecropiagalaxy Cecropia Galaxy
398 Balsanovo Balsa Supernova
399 Rosewoodway Rosewood Milky Way
400 Ipegalaxy Ipe Galaxy
401 Kapokmeda Kapok Andromeda
402 Cedrellaneta Cedrella Planetary Nebula
403 Alercenova Alerce Supernova
404 Mahogagalaxy Mahogany Galaxy
405 Ghost Brasil Ghost Nebula Pau Brasil
406 Ceibomeda Ceiba Andromeda
407 Araucarionway Araucaria Milky Way
408 Quebrachion Quebracho Orion
409 Lapachohole Lapacho Black Hole
410 Guanacastar Guanacaste Quasar
411 Jacaranovo Jacaranda Supernova
412 Ceibagalaxy Ceiba Galaxy
413 Eskimo Brasil Eskimo Nebula Pau Brasil
414 Monkeynebula Monkey Puzzle Nebula
415 Quebrachogalaxy Quebracho Galaxy
416 Lapachonova Lapacho Supernova
417 Rubbersar Rubber Tree Quasar
418 Cashewnebula Cashew Nebula
419 Nutgalaxy Brazil Nut Galaxy
420 Cecropioway Cecropia Milky Way

78
package-lock.json generated
View file

@ -1,78 +0,0 @@
{
"name": "memlnaut-nisps-tests",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "memlnaut-nisps-tests",
"version": "1.0.0",
"devDependencies": {
"@playwright/test": "^1.59.1"
}
},
"node_modules/@playwright/test": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
"integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

View file

@ -1,13 +0,0 @@
{
"name": "memlnaut-nisps-tests",
"version": "1.0.0",
"private": true,
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:headed": "playwright test --headed"
},
"devDependencies": {
"@playwright/test": "^1.59.1"
}
}

View file

@ -1 +0,0 @@
._root_1attb_1{max-width:1200px;margin:0 auto;display:flex;flex-direction:column;gap:var(--sp-5)}._header_1attb_9{display:flex;flex-direction:column;gap:var(--sp-2)}._header_1attb_9 h1{margin:0;font-size:var(--fs-xl);color:var(--accent)}._lede_1attb_21{color:var(--fg-mute);margin:0;max-width:720px}._grid_1attb_27{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:var(--sp-4)}._card_1attb_33{background:var(--bg-1);border:1px solid var(--line);border-radius:var(--r-3);padding:var(--sp-4);display:flex;flex-direction:column;gap:var(--sp-3);min-height:280px}._cardHead_1attb_44{display:flex;flex-direction:column;gap:var(--sp-1);border-bottom:1px solid var(--line);padding-bottom:var(--sp-2)}._cardTitle_1attb_52{margin:0;font-size:var(--fs-md);color:var(--fg)}._cardDesc_1attb_58{margin:0;font-size:var(--fs-xs);color:var(--fg-mute)}._cardBody_1attb_64{display:flex;flex-direction:column;align-items:stretch;flex:1;justify-content:flex-start}._bank_3njyn_1{display:flex;flex-direction:column;gap:var(--sp-3);background:var(--bg-1);border:1px solid var(--line);border-radius:var(--r-2);padding:var(--sp-3)}._title_3njyn_11{font-size:var(--fs-md);margin:0 0 var(--sp-2) 0;color:var(--fg)}._section_3njyn_17{display:flex;flex-direction:column;gap:var(--sp-2)}._sectionHeader_3njyn_23{display:flex;align-items:center;gap:var(--sp-2);padding:var(--sp-1) var(--sp-2);background:var(--bg-2);border:1px solid var(--line);text-align:left;width:100%;font-size:var(--fs-xs);text-transform:uppercase;letter-spacing:.08em;color:var(--fg-mute)}._caret_3njyn_38{width:1ch;display:inline-block}._sectionName_3njyn_43{flex:1}._sectionCount_3njyn_47{color:var(--fg-dim)}._list_3njyn_51{display:flex;flex-direction:column;gap:var(--sp-2);padding:var(--sp-1) 0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
(function(){"use strict";class f extends AudioWorkletProcessor{instance=null;engineHandle=0;engineId="thru";muted=!0;inLPtr=0;inRPtr=0;outLPtr=0;outRPtr=0;idPtr=0;paramsPtr=0;inLView=null;inRView=null;outLView=null;outRView=null;paramsView=null;idView=null;mem=null;pendingParams=null;constructor(){super(),this.port.onmessage=i=>this.onMessage_(i.data)}async onMessage_(i){if(i.kind==="init")try{await this.init_(i.wasmBinary,i.sampleRate),this.post_({kind:"ready"})}catch(r){this.post_({kind:"error",message:r instanceof Error?r.message:String(r)})}else i.kind==="engine"?this.switchEngine_(i.engineId):i.kind==="params"?this.applyParams_(i.params):i.kind==="mute"&&(this.muted=i.muted)}post_(i){this.port.postMessage(i)}async init_(i,r){const t=new WebAssembly.Memory({initial:128,maximum:4096,shared:!1}),a=await WebAssembly.compile(i),h=WebAssembly.Module.imports(a),n={};for(const e of h){n[e.module]||(n[e.module]={});const s=n[e.module];e.kind==="function"?(e.name,s[e.name]=(...o)=>0):e.kind==="memory"?s[e.name]=t:e.kind==="table"?s[e.name]=new WebAssembly.Table({element:"anyfunc",initial:0}):e.kind==="global"&&(s[e.name]=new WebAssembly.Global({value:"i32",mutable:!0},0))}for(const e of h){const s=n[e.module];e.name==="a"&&e.kind==="function"&&(s[e.name]=()=>{throw new Error("wasm aborted")}),e.name==="b"&&e.kind==="function"&&(s[e.name]=o=>0)}const w=await WebAssembly.instantiate(a,n),_=WebAssembly.Module.exports(a),P=new Map;for(const e of _)P.set(e.name,e.name);const p=w.exports;function u(...e){for(const s of e){const o=p[s];if(typeof o=="function")return o}throw new Error(`worklet: missing wasm export, tried: ${e.join(", ")}`)}function m(...e){return u(...e)}const l=u("_malloc","malloc"),g=m("_free","free"),y=u("_nisps_engine_create"),R=m("_nisps_engine_destroy"),b=m("_nisps_engine_set_params"),L=m("_nisps_engine_process_block");let d=null;for(const e of _)if(e.kind==="memory"){const s=p[e.name];if(s instanceof WebAssembly.Memory){d=s;break}}this.mem=d??t,this.instance={exports:{memory:this.mem,malloc:l,free:g,_nisps_engine_create:(e,s)=>y(e,s),_nisps_engine_destroy:e=>R(e),_nisps_engine_set_params:(e,s,o)=>b(e,s,o),_nisps_engine_process_block:(e,s,o,V,A,C)=>L(e,s,o,V,A,C)}},this.inLPtr=l(128*4),this.inRPtr=l(128*4),this.outLPtr=l(128*4),this.outRPtr=l(128*4),this.paramsPtr=l(256*4),this.idPtr=l(32);const c=this.mem.buffer;this.inLView=new Float32Array(c,this.inLPtr,128),this.inRView=new Float32Array(c,this.inRPtr,128),this.outLView=new Float32Array(c,this.outLPtr,128),this.outRView=new Float32Array(c,this.outRPtr,128),this.paramsView=new Float32Array(c,this.paramsPtr,256),this.idView=new Uint8Array(c,this.idPtr,32),this.spawnEngine_("thru",r),this.pendingParams&&(this.applyParams_(this.pendingParams),this.pendingParams=null),this.muted=!1}spawnEngine_(i,r){if(!this.instance||!this.idView)return;this.engineHandle&&(this.instance.exports._nisps_engine_destroy(this.engineHandle),this.engineHandle=0);const a=new TextEncoder().encode(i);this.idView.fill(0),this.idView.set(a.subarray(0,Math.min(a.length,31))),this.engineHandle=this.instance.exports._nisps_engine_create(this.idPtr,r),this.engineId=i}switchEngine_(i){this.spawnEngine_(i,sampleRate)}applyParams_(i){if(!this.instance||!this.paramsView){this.pendingParams=i;return}const r=Math.min(i.length,256);for(let t=0;t<r;++t)this.paramsView[t]=i[t];this.engineHandle&&this.instance.exports._nisps_engine_set_params(this.engineHandle,this.paramsPtr,r)}process(i,r){const t=r[0];if(!t||t.length===0)return!0;const a=t[0],h=t.length>1?t[1]:t[0];if(this.muted||!this.instance||!this.engineHandle||!this.inLView||!this.outLView||!this.outRView||!this.inRView)return a.fill(0),t.length>1&&h.fill(0),!0;const n=i[0];return n&&n[0]?this.inLView.set(n[0].subarray(0,128)):this.inLView.fill(0),n&&n[1]?this.inRView.set(n[1].subarray(0,128)):n&&n[0]?this.inRView.set(n[0].subarray(0,128)):this.inRView.fill(0),this.instance.exports._nisps_engine_process_block(this.engineHandle,this.inLPtr,this.inRPtr,this.outLPtr,this.outRPtr,128),a.set(this.outLView.subarray(0,a.length)),t.length>1&&h.set(this.outRView.subarray(0,h.length)),!0}}registerProcessor("nisps-processor",f)})();
//# sourceMappingURL=nisps-processor-BuKuNOY1.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
(function(){"use strict";if(typeof window>"u"&&typeof self<"u"&&typeof self.importScripts<"u"){let h=function(t,s,n,u){if(!e)throw new Error("worker module not loaded");c!==a&&(i&&e._free(i),i=e._malloc(a*4),c=a),t.length!==g&&(l&&e._free(l),l=e._malloc(t.length*4),g=t.length),s.length!==_&&(f&&e._free(f),f=e._malloc(s.length*4),_=s.length),n.length!==w&&(o&&e._free(o),o=n.length>0?e._malloc(n.length*4):0,w=n.length),new Float32Array(e.HEAPF32.buffer,i,a).set(u),new Float32Array(e.HEAPF32.buffer,l,t.length).set(t),new Float32Array(e.HEAPF32.buffer,f,s.length).set(s),o&&new Float32Array(e.HEAPF32.buffer,o,n.length).set(n)},p=function(t){if(!e)return{kind:"error",requestId:t.requestId,message:"worker not initialised"};try{h(t.features,t.labels,t.sampleWeights,t.weights),e._nisps_ml_set_weights(r,i),e._nisps_ml_clear_examples(r);const s=t.inputSize,n=t.outputSize,u=t.features.length/s;for(let d=0;d<u;++d){const S=l+d*s*4,H=f+d*n*4;e._nisps_ml_add_example(r,S,H)}const F=t.sampleWeights.length>0?o:0,m=e._nisps_ml_train(r,t.lr,t.maxIter,t.minErr,F);e._nisps_ml_get_weights(r,i);const A=new Float32Array(e.HEAPF32.buffer,i,a),P=new Float32Array(A),E=new Float32Array([m]);return{kind:"result",requestId:t.requestId,loss:m,weights:P,lossHistory:E}}catch(s){return{kind:"error",requestId:t.requestId,message:s instanceof Error?s.message:String(s)}}},y=function(){e&&(r&&(e._nisps_ml_destroy(r),r=0),i&&(e._free(i),i=0),l&&(e._free(l),l=0),f&&(e._free(f),f=0),o&&(e._free(o),o=0),e=null)},e=null,r=0,a=0,i=0,c=0,l=0,g=0,f=0,_=0,o=0,w=0;async function k(t){const s=await import(new URL("/nisps.js",self.location.origin).toString());e=await(s.default??s.createNispsModule)({locateFile:u=>u.endsWith(".wasm")?new URL("/nisps.wasm",self.location.origin).toString():u}),r=e._nisps_ml_create(0,0,0,0,t>>>0),a=e._nisps_ml_weight_count(r)}self.addEventListener("message",async t=>{const s=t.data;if(s.kind==="init")try{await k(s.seed),self.postMessage({kind:"ready"})}catch(n){self.postMessage({kind:"error",requestId:0,message:n instanceof Error?n.message:String(n)})}else if(s.kind==="train"){const n=p(s);n.kind==="result"?self.postMessage(n,[n.weights.buffer,n.lossHistory.buffer]):self.postMessage(n)}else s.kind==="dispose"&&y()})}})();
//# sourceMappingURL=wasm-worker-OATBBHaT.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0d0d0d" />
<title>MEMLNaut Playground</title>
<script type="module" crossorigin src="/assets/index-C1-a2ZPT.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D19lnkK_.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -1,27 +0,0 @@
const { defineConfig } = require('@playwright/test');
module.exports = defineConfig({
testDir: './tests/e2e',
timeout: 30_000,
expect: { timeout: 10_000 },
use: {
baseURL: 'http://localhost:7331',
headless: true,
// WASM + AudioContext require a stable origin
ignoreHTTPSErrors: true,
},
// Start a static file server against the playground/ dir before tests run.
// python3 -m http.server serves directory listings and static assets fine.
// WASM files need correct MIME type — Python's server handles .wasm correctly.
webServer: {
command: 'python3 -m http.server 7331',
cwd: './playground',
url: 'http://localhost:7331',
reuseExistingServer: true,
timeout: 10_000,
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
],
reporter: [['list'], ['html', { open: 'never' }]],
});

View file

@ -1,420 +0,0 @@
/**
* Engine switching e2e tests verify that all three synth engines
* (C15 Shaper-Feedback, Additive, FM Matrix) can be selected, initialised,
* and driven by the NISPS ML engine correctly.
*
* Also tests the EOC chain integration across engine switches.
*/
const { test, expect } = require('@playwright/test');
const { loadApp, statusText } = require('./helpers');
// Engine metadata expected from the switcher
const ENGINES = {
'shaper-feedback': { displayName: 'C15 Shaper-Feedback', paramCount: 126 },
'additive': { displayName: 'Additive', paramCount: 48 },
'fm': { displayName: 'FM Matrix', paramCount: 55 },
};
/**
* Helper: switch to an engine via the engine switcher UI.
* Opens the synth drawer, clicks the engine card, accepts any confirm dialog.
*/
async function switchEngine(page, engineId) {
// Open synth drawer if not already open
const drawer = page.locator('#drawer-synth');
if (await drawer.evaluate(el => el.classList.contains('hidden'))) {
await page.click('[data-drawer="synth"]');
}
// Click the engine card
page.once('dialog', dialog => dialog.accept());
await page.click(`.engine-card[data-engine-id="${engineId}"]`);
// Wait for the switcher to mark it active
await expect(page.locator(`.engine-card[data-engine-id="${engineId}"]`)).toHaveClass(/active/, { timeout: 15_000 });
}
/**
* Helper: get the current engine id from the debug probe.
*/
async function getActiveEngineId(page) {
return page.evaluate(() => window.__nisps?.activeEngine?.id ?? null);
}
/**
* Helper: get the current MLP output count.
*/
async function getOutputCount(page) {
return page.evaluate(() => window.__nisps?.getOutputs()?.length ?? 0);
}
// ---------------------------------------------------------------------------
// Expose activeEngine on the debug probe so tests can query it
// ---------------------------------------------------------------------------
test.describe('Engine switching', () => {
test.beforeEach(async ({ page }) => {
await loadApp(page);
// Extend the debug probe with engine-related getters
await page.evaluate(() => {
if (window.__nisps) {
// These are closures over the module-scoped vars in a-app.js,
// but we can read them via the existing probe's eocChain and other refs.
// The probe already exposes getOutputs() which reflects N_OUTPUTS.
}
});
});
test.describe('Default state', () => {
test('default engine is C15 Shaper-Feedback', async ({ page }) => {
const btnText = await page.locator('#synth-mode-btn').textContent();
// The synth mode button should reflect the default engine (may say "Synth" or the engine name)
expect(btnText).toBeTruthy();
});
test('default output count is 126 (C15)', async ({ page }) => {
const count = await getOutputCount(page);
// Default mode is visual (20 outputs), not synth
// Switch to synth mode first
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(500);
const synthCount = await getOutputCount(page);
expect(synthCount).toBe(126);
});
test('heatmap shows 126 cells in synth mode', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(500);
const cellCount = await page.locator('#heatmap-cells .heatmap-cell').count();
expect(cellCount).toBe(126);
});
});
test.describe('Engine switcher UI', () => {
test('synth drawer contains engine cards', async ({ page }) => {
await page.click('[data-drawer="synth"]');
const cards = page.locator('.engine-card');
// C15 shaper-feedback, additive, fm, modular
await expect(cards).toHaveCount(4);
});
test('C15 card is active by default', async ({ page }) => {
await page.click('[data-drawer="synth"]');
const c15Card = page.locator('.engine-card[data-engine-id="shaper-feedback"]');
await expect(c15Card).toHaveClass(/active/);
});
test('additive and FM cards are not active by default', async ({ page }) => {
await page.click('[data-drawer="synth"]');
await expect(page.locator('.engine-card[data-engine-id="additive"]')).not.toHaveClass(/active/);
await expect(page.locator('.engine-card[data-engine-id="fm"]')).not.toHaveClass(/active/);
});
});
test.describe('Switch to Additive engine', () => {
test('switching resizes MLP to 48 outputs', async ({ page }) => {
// Go to synth mode first
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
// Switch engine
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
const count = await getOutputCount(page);
expect(count).toBe(48);
});
test('heatmap rebuilds with 48 cells', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
const cellCount = await page.locator('#heatmap-cells .heatmap-cell').count();
expect(cellCount).toBe(48);
});
test('outputs are bounded [0, 1]', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
expect(outputs).toHaveLength(48);
for (const v of outputs) {
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
}
});
test('different inputs produce different outputs', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
const out1 = await page.evaluate(() => {
window.__nisps.setInputs(0.1, 0.1);
return window.__nisps.getOutputs();
});
const out2 = await page.evaluate(() => {
window.__nisps.setInputs(0.9, 0.9);
return window.__nisps.getOutputs();
});
const anyDiff = out1.some((v, i) => Math.abs(v - out2[i]) > 0.001);
expect(anyDiff).toBe(true);
});
test('training works with additive param count', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
const loss = await page.evaluate(() => {
const nisps = window.__nisps;
// Add two contrasting examples
nisps.iml.addExample([0.1, 0.1], new Array(48).fill(0.2));
nisps.iml.addExample([0.9, 0.9], new Array(48).fill(0.8));
return nisps.train();
});
expect(loss).toBeGreaterThanOrEqual(0);
expect(loss).toBeLessThan(1);
});
});
test.describe('Switch to FM Matrix engine', () => {
test('switching resizes MLP to 55 outputs', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
await switchEngine(page, 'fm');
await page.waitForTimeout(500);
const count = await getOutputCount(page);
expect(count).toBe(55);
});
test('heatmap rebuilds with 55 cells', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
await switchEngine(page, 'fm');
await page.waitForTimeout(500);
const cellCount = await page.locator('#heatmap-cells .heatmap-cell').count();
expect(cellCount).toBe(55);
});
test('outputs are bounded [0, 1]', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await switchEngine(page, 'fm');
await page.waitForTimeout(500);
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
expect(outputs).toHaveLength(55);
for (const v of outputs) {
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
}
});
test('training works with FM param count', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await switchEngine(page, 'fm');
await page.waitForTimeout(500);
const loss = await page.evaluate(() => {
const nisps = window.__nisps;
nisps.iml.addExample([0.2, 0.8], new Array(55).fill(0.3));
nisps.iml.addExample([0.8, 0.2], new Array(55).fill(0.7));
return nisps.train();
});
expect(loss).toBeGreaterThanOrEqual(0);
expect(loss).toBeLessThan(1);
});
});
test.describe('Round-trip switching', () => {
test('switching C15 → Additive → C15 restores 126 outputs', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
// Switch to additive
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
expect(await getOutputCount(page)).toBe(48);
// Switch back to C15
await switchEngine(page, 'shaper-feedback');
await page.waitForTimeout(500);
expect(await getOutputCount(page)).toBe(126);
});
test('switching C15 → FM → Additive → C15 restores correctly each time', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
await switchEngine(page, 'fm');
await page.waitForTimeout(500);
expect(await getOutputCount(page)).toBe(55);
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
expect(await getOutputCount(page)).toBe(48);
await switchEngine(page, 'shaper-feedback');
await page.waitForTimeout(500);
expect(await getOutputCount(page)).toBe(126);
});
});
test.describe('Warm-start weight preservation', () => {
test('hidden layer weights are preserved across resize', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
// Get weights before switch (first 100 hidden-layer weights)
const beforeWeights = await page.evaluate(() => {
return window.__nisps.getWeights().slice(0, 100);
});
// Switch to additive (48 outputs, hidden layers unchanged)
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
// Get weights after — first 100 should be identical (hidden layer prefix)
const afterWeights = await page.evaluate(() => {
return window.__nisps.getWeights().slice(0, 100);
});
// Hidden layer weights should be preserved (warm-start)
let matchCount = 0;
for (let i = 0; i < 100; i++) {
if (Math.abs(beforeWeights[i] - afterWeights[i]) < 1e-6) matchCount++;
}
// Allow some tolerance — at least 90% should match
expect(matchCount).toBeGreaterThan(90);
});
});
test.describe('EOC chain across engine switches', () => {
test('EOC drawer is accessible from all engines', async ({ page }) => {
// Open EOC drawer with default C15
await page.click('[data-drawer="eoc"]');
await expect(page.locator('#drawer-eoc')).not.toHaveClass(/hidden/);
// Switch to additive
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
// EOC drawer should still be accessible — engine switch may have closed it
// so click the dock icon; if already open, clicking toggles closed then re-open
const eocDrawer = page.locator('#drawer-eoc');
if (await eocDrawer.evaluate(el => el.classList.contains('hidden'))) {
await page.click('[data-drawer="eoc"]');
}
await expect(eocDrawer).not.toHaveClass(/hidden/);
});
test('NISPS mode selector is present in EOC drawer', async ({ page }) => {
await page.click('[data-drawer="eoc"]');
const modeButtons = page.locator('.eoc-nisps-bar .pill-opt');
const count = await modeButtons.count();
expect(count).toBe(4); // Bypass, Shared, Linked, Independent
});
});
test.describe('SynthVisualizer with different engines', () => {
test('synth-vis-canvas is visible in synth mode for all engines', async ({ page }) => {
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
// Check canvas is visible for C15
const canvas = page.locator('#synth-vis-canvas');
await expect(canvas).toBeVisible();
// Switch to additive — canvas should remain visible
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
await expect(canvas).toBeVisible();
// Switch to FM — canvas should remain visible
await switchEngine(page, 'fm');
await page.waitForTimeout(500);
await expect(canvas).toBeVisible();
});
});
test.describe('No console errors during engine switch', () => {
test('switching to additive produces no errors', async ({ page }) => {
const errors = [];
page.on('pageerror', err => errors.push(err.message));
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
await switchEngine(page, 'additive');
await page.waitForTimeout(1000);
// Filter out known non-critical errors
const critical = errors.filter(e =>
!e.includes('ResizeObserver') && !e.includes('net::ERR')
);
expect(critical).toEqual([]);
});
test('switching to FM produces no errors', async ({ page }) => {
const errors = [];
page.on('pageerror', err => errors.push(err.message));
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
await switchEngine(page, 'fm');
await page.waitForTimeout(1000);
const critical = errors.filter(e =>
!e.includes('ResizeObserver') && !e.includes('net::ERR')
);
expect(critical).toEqual([]);
});
test('round-trip switching produces no errors', async ({ page }) => {
const errors = [];
page.on('pageerror', err => errors.push(err.message));
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.waitForTimeout(300);
await switchEngine(page, 'additive');
await page.waitForTimeout(500);
await switchEngine(page, 'fm');
await page.waitForTimeout(500);
await switchEngine(page, 'shaper-feedback');
await page.waitForTimeout(500);
const critical = errors.filter(e =>
!e.includes('ResizeObserver') && !e.includes('net::ERR')
);
expect(critical).toEqual([]);
});
});
});

View file

@ -1,32 +0,0 @@
/**
* Shared helpers for e2e tests.
*/
const { expect } = require('@playwright/test');
/**
* Navigate to a-immersive with ?debug=1 and wait for the WASM engine to
* initialise and expose window.__nisps.
*
* @param {import('@playwright/test').Page} page
* @param {string} extraParams - additional query string, e.g. '&preset=beginner-1'
*/
async function loadApp(page, extraParams = '') {
// Clear app state but mark help as seen so the overlay doesn't block clicks.
await page.addInitScript(() => {
localStorage.removeItem('nisps-a-immersive');
localStorage.setItem('nisps-help-seen', '1');
});
await page.goto(`/a-immersive.html?debug=1${extraParams}`);
// Wait until the debug probe is ready (WASM init is async).
await page.waitForFunction(() => window.__nisps !== undefined, { timeout: 20_000 });
}
/**
* Returns the current text content of the status line.
* @param {import('@playwright/test').Page} page
*/
async function statusText(page) {
return page.locator('#status-text').textContent();
}
module.exports = { loadApp, statusText };

View file

@ -1,110 +0,0 @@
/**
* Input pipeline tests joystick MLP inputs outputs heatmap.
*/
const { test, expect } = require('@playwright/test');
const { loadApp } = require('./helpers');
test.describe('Joystick → output pipeline', () => {
test('different input positions produce different outputs', async ({ page }) => {
await loadApp(page);
await page.evaluate(() => window.__nisps.setInputs(0.1, 0.1));
const out1 = await page.evaluate(() => window.__nisps.getOutputs());
await page.evaluate(() => window.__nisps.setInputs(0.9, 0.9));
const out2 = await page.evaluate(() => window.__nisps.getOutputs());
const anyChanged = out1.some((v, i) => Math.abs(v - out2[i]) > 0.0001);
expect(anyChanged).toBe(true);
});
test('all outputs remain in [0, 1] across input positions', async ({ page }) => {
await loadApp(page);
const corners = [[0, 0], [0, 1], [1, 0], [1, 1], [0.5, 0.5]];
for (const [x, y] of corners) {
await page.evaluate(([x, y]) => window.__nisps.setInputs(x, y), [x, y]);
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
for (const v of outputs) {
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
}
}
});
test('heatmap bar widths change when inputs change', async ({ page }) => {
await loadApp(page);
const widths1 = await page.evaluate(() =>
Array.from(document.querySelectorAll('.heatmap-cell-bar')).map(el => el.style.width)
);
// Move to far corner
await page.evaluate(() => window.__nisps.setInputs(0.95, 0.05));
const widths2 = await page.evaluate(() =>
Array.from(document.querySelectorAll('.heatmap-cell-bar')).map(el => el.style.width)
);
const anyChanged = widths1.some((w, i) => w !== widths2[i]);
expect(anyChanged).toBe(true);
});
test('mouse drag on joystick container updates MLP inputs', async ({ page }) => {
await loadApp(page);
const box = await page.locator('#joystick-container').boundingBox();
const cx = box.x + box.width / 2;
const cy = box.y + box.height / 2;
// Drag from center towards bottom-right
await page.mouse.move(cx, cy);
await page.mouse.down();
await page.mouse.move(cx + box.width * 0.3, cy + box.height * 0.3, { steps: 10 });
await page.mouse.up();
// After drag, at least one input axis should have moved from 0.5
const [x, y] = await page.evaluate(() => [
window.__nisps.iml.inputState[0],
window.__nisps.iml.inputState[1],
]);
const moved = Math.abs(x - 0.5) > 0.01 || Math.abs(y - 0.5) > 0.01;
expect(moved).toBe(true);
});
test('setInputs clamps values to [0, 1]', async ({ page }) => {
await loadApp(page);
await page.evaluate(() => window.__nisps.setInputs(-5, 99));
const [x, y] = await page.evaluate(() => [
window.__nisps.iml.inputState[0],
window.__nisps.iml.inputState[1],
]);
expect(x).toBeGreaterThanOrEqual(0);
expect(x).toBeLessThanOrEqual(1);
expect(y).toBeGreaterThanOrEqual(0);
expect(y).toBeLessThanOrEqual(1);
});
test('after training, moving inputs produces smoothly varying outputs', async ({ page }) => {
await loadApp(page);
// Load calm-to-chaotic preset (3 examples spanning the input space)
await page.click('[data-drawer="training"]');
await page.click('[data-preset="calm-to-chaotic"]');
await page.waitForFunction(
() => document.getElementById('status-text').textContent.includes('loss'),
{ timeout: 15_000 }
);
// Sample 5 positions and verify all outputs are bounded
const positions = [0.0, 0.25, 0.5, 0.75, 1.0];
for (const t of positions) {
await page.evaluate((t) => window.__nisps.setInputs(t, 1 - t), t);
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
expect(outputs).toHaveLength(126);
for (const v of outputs) {
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
}
}
});
});

View file

@ -1,170 +0,0 @@
/**
* ML engine sanity tests verify the WASM IML behaves correctly:
* - outputs are always bounded [0, 1]
* - randomize produces different outputs
* - thumbs-up captures the current rawParamValues as the training label
* - training completes and produces a finite loss
* - thumbs-down moves weights and changes outputs
* - async training (triggered by thumbs-up) updates the status line
*/
const { test, expect } = require('@playwright/test');
const { loadApp, statusText } = require('./helpers');
// Two contrasting examples with known inputs and all-low / all-high targets.
const EXAMPLE_LOW = { input: [0.1, 0.9], output: new Array(126).fill(0.1) };
const EXAMPLE_HIGH = { input: [0.9, 0.1], output: new Array(126).fill(0.9) };
test.describe('ML engine (WASM IML)', () => {
test('probe is exposed after WASM init', async ({ page }) => {
await loadApp(page);
const probe = await page.evaluate(() => typeof window.__nisps);
expect(probe).toBe('object');
});
test('initial outputs are all in [0, 1]', async ({ page }) => {
await loadApp(page);
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
expect(outputs).toHaveLength(126);
for (const v of outputs) {
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
}
});
test('initial state is 0 examples, untrained', async ({ page }) => {
await loadApp(page);
const count = await page.evaluate(() => window.__nisps.getExampleCount());
expect(count).toBe(0);
const loss = await page.evaluate(() => window.__nisps.getLoss());
expect(loss).toBeNull();
});
test('randomize changes outputs', async ({ page }) => {
await loadApp(page);
const before = await page.evaluate(() => window.__nisps.getOutputs());
await page.evaluate(() => window.__nisps.randomise());
const after = await page.evaluate(() => window.__nisps.getOutputs());
const anyChanged = before.some((v, i) => Math.abs(v - after[i]) > 0.001);
expect(anyChanged).toBe(true);
});
test('thumbs-up increments example count by 1', async ({ page }) => {
await loadApp(page);
await page.evaluate(() => window.__nisps.thumbsUp());
// Give async training a moment to start but we only need to check example count
await page.waitForTimeout(100);
const count = await page.evaluate(() => window.__nisps.getExampleCount());
expect(count).toBe(1);
});
test('thumbs-up captures current input position and all 126 output values', async ({ page }) => {
await loadApp(page);
// Set a known joystick position via the probe
await page.evaluate(() => window.__nisps.setInputs(0.25, 0.75));
await page.evaluate(() => window.__nisps.thumbsUp());
await page.waitForTimeout(100);
const { features, labels } = await page.evaluate(() => ({
features: window.__nisps.iml.dataset.features,
labels: window.__nisps.iml.dataset.labels,
}));
expect(features).toHaveLength(1);
expect(labels).toHaveLength(1);
// Input dimension = 2 (joystick x/y, pipeline-processed)
expect(features[0]).toHaveLength(2);
// The input pipeline may transform values; inputs must stay in [0, 1]
expect(features[0][0]).toBeGreaterThanOrEqual(0);
expect(features[0][0]).toBeLessThanOrEqual(1);
expect(features[0][1]).toBeGreaterThanOrEqual(0);
expect(features[0][1]).toBeLessThanOrEqual(1);
// Labels = all 126 output values, captured from rawParamValues at click time
expect(labels[0]).toHaveLength(126);
for (const v of labels[0]) {
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
}
});
test('sync train() returns a finite non-negative loss', async ({ page }) => {
await loadApp(page);
await page.evaluate(([low, high]) => {
window.__nisps.iml.addExample(low.input, low.output);
window.__nisps.iml.addExample(high.input, high.output);
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
const loss = await page.evaluate(() => window.__nisps.train());
expect(typeof loss).toBe('number');
expect(isFinite(loss)).toBe(true);
expect(loss).toBeGreaterThanOrEqual(0);
});
test('training with contrasting examples produces a lower loss than initial', async ({ page }) => {
await loadApp(page);
// Initial inference — loss is null (never trained), so randomise to get a baseline
await page.evaluate(() => window.__nisps.randomise());
await page.evaluate(([low, high]) => {
window.__nisps.iml.addExample(low.input, low.output);
window.__nisps.iml.addExample(high.input, high.output);
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
const loss1 = await page.evaluate(() => window.__nisps.train());
const loss2 = await page.evaluate(() => window.__nisps.train());
// Second training run on same data should converge further (loss2 <= loss1)
expect(loss2).toBeLessThanOrEqual(loss1 + 1e-6);
});
test('status line reflects example count and loss after training', async ({ page }) => {
await loadApp(page);
await page.evaluate(([low, high]) => {
window.__nisps.iml.addExample(low.input, low.output);
window.__nisps.iml.addExample(high.input, high.output);
window.__nisps.train();
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
// updateStatus() is called inside trainModel()
const text = await page.locator('#status-text').textContent();
expect(text).toContain('2 examples');
expect(text).toContain('loss');
});
test('thumbs-down changes outputs (weight noise)', async ({ page }) => {
await loadApp(page);
const before = await page.evaluate(() => window.__nisps.getOutputs());
await page.evaluate(() => window.__nisps.thumbsDown());
const after = await page.evaluate(() => window.__nisps.getOutputs());
const anyChanged = before.some((v, i) => Math.abs(v - after[i]) > 0.0001);
expect(anyChanged).toBe(true);
});
test('async training via thumbs-up button updates status with loss', async ({ page }) => {
await loadApp(page);
await page.click('#btn-thumbsup');
// Wait for the async training to complete and status to update
await page.waitForFunction(
() => document.getElementById('status-text').textContent.includes('loss'),
{ timeout: 15_000 }
);
const text = await page.locator('#status-text').textContent();
expect(text).toContain('1 example');
expect(text).toContain('loss');
});
test('clear examples resets to 0 and marks untrained', async ({ page }) => {
await loadApp(page);
await page.evaluate(([low]) => {
window.__nisps.iml.addExample(low.input, low.output);
window.__nisps.train();
}, [EXAMPLE_LOW]);
expect(await page.evaluate(() => window.__nisps.getExampleCount())).toBe(1);
await page.evaluate(() => window.__nisps.clearExamples());
expect(await page.evaluate(() => window.__nisps.getExampleCount())).toBe(0);
const text = await page.locator('#status-text').textContent();
expect(text).toContain('0 examples');
});
});

View file

@ -1,294 +0,0 @@
/**
* Modular mode e2e tests.
*
* These tests exercise the modular engine end-to-end via the debug probe
* (?debug=1, window.__nisps).
*
* NB: modular paramMeta = 4 ADSR × 4 + 8 LFO × 2 + 48 × 10 matrix = 512.
* Every matrix cell is in paramMeta so `modular-ui.updateLive()` can
* mirror live MLP outputs into the matrix DOM. Silence-on-joystick
* regressions are prevented at the DSP level: each sub-engine's
* `amp_val = clamp(base_amp + max(0, mod_amp))`, so matrix d08_amp
* cells can only add to the amp floor and base_amp=1.0 (default)
* keeps the voice audible regardless of what the MLP outputs.
*/
const { test, expect } = require('@playwright/test');
const { loadApp } = require('./helpers');
/**
* Switch the active engine to `modular` via the engine-switcher UI.
* Waits until window.__nisps.activeEngineId === 'modular'.
*/
async function switchToModular(page) {
// Open the synth drawer first (needed to see the engine cards).
const drawer = page.locator('#drawer-synth');
if (await drawer.evaluate(el => el.classList.contains('hidden'))) {
await page.click('[data-drawer="synth"]');
}
page.once('dialog', d => d.accept());
await page.click('.engine-card[data-engine-id="modular"]');
// Wait until setActiveEngine completes AND the modular dock icon becomes
// visible — the dock icon is revealed from the tail end of setActiveEngine
// so this guarantees the initial modular-ui.refresh() restore pass has run.
// Without this we race: test code can fire before modularUI.show()
// reaches refresh()'s pendingRestore branch, which then wipes the test's
// subsequent sub-engine swap.
await page.waitForFunction(
() => window.__nisps?.activeEngineId === 'modular',
null,
{ timeout: 20_000 }
);
await page.waitForFunction(
() => !document.querySelector('.dock-icon[data-drawer="modular"]')?.classList.contains('hidden'),
null,
{ timeout: 20_000 }
);
// Also yield one extra microtask so any synchronous deferred work queued
// inside setActiveEngine settles before we start poking the engine.
await page.evaluate(() => new Promise(r => setTimeout(r, 0)));
}
test.describe('Modular mode', () => {
test('switching to modular yields paramCount = 512', async ({ page }) => {
await loadApp(page);
await switchToModular(page);
const count = await page.evaluate(() => window.__nisps.paramCount);
// 4 ADSR × (attack, decay, sustain, release) = 16
// 8 LFO × (rate, morph) = 16
// 48 × 10 matrix cells = 480
expect(count).toBe(512);
});
test('matrix cells are in paramMeta so modular-ui updateLive can read them', async ({ page }) => {
// Regression guard: modular-ui.js `updateLive()` depends on every
// matrix cell being present in paramMeta so it can map MLP output
// indices to cell DOM. A previous fix that gated matrix cells on
// an opt-in flag broke the matrix UI's live visualisation when the
// joystick moved. If this test fails, inspect _rebuildParamMeta().
await loadApp(page);
await switchToModular(page);
const info = await page.evaluate(() => {
const meta = window.__nisps.activeEngine.paramMeta;
const matrix = meta.filter(m => m.group && m.group.startsWith('Matrix/'));
const destBuckets = {};
for (const m of matrix) {
const d = m.group.split('/')[1];
destBuckets[d] = (destBuckets[d] || 0) + 1;
}
return { total: meta.length, matrixCount: matrix.length, destBuckets };
});
expect(info.total).toBe(512);
expect(info.matrixCount).toBe(480);
// 10 destinations × 48 sources each
expect(Object.keys(info.destBuckets).length).toBe(10);
for (const count of Object.values(info.destBuckets)) {
expect(count).toBe(48);
}
});
test('debug probe exposes modular hooks', async ({ page }) => {
await loadApp(page);
await switchToModular(page);
const hooks = await page.evaluate(() => ({
hasGet: typeof window.__nisps.getModularState === 'function',
hasSet: typeof window.__nisps.setModularState === 'function',
hasSwap: typeof window.__nisps.setModularSubEngine === 'function',
hasPreset: typeof window.__nisps.applyModularPreset === 'function',
hasCounts: typeof window.__nisps.setModularSourceCount === 'function',
presetList: window.__nisps.listModularPresets?.()?.length ?? 0,
}));
expect(hooks.hasGet).toBe(true);
expect(hooks.hasSet).toBe(true);
expect(hooks.hasSwap).toBe(true);
expect(hooks.hasPreset).toBe(true);
expect(hooks.hasCounts).toBe(true);
expect(hooks.presetList).toBe(6);
});
test('sub-engine swap keeps paramCount = 512', async ({ page }) => {
await loadApp(page);
await switchToModular(page);
for (const sub of ['additive', 'fm', 'subtractive']) {
await page.evaluate(async (id) => {
await window.__nisps.setModularSubEngine(id);
}, sub);
const info = await page.evaluate(() => ({
paramCount: window.__nisps.paramCount,
subId: window.__nisps.activeEngine?.activeSubEngineId,
}));
expect(info.subId).toBe(sub);
expect(info.paramCount).toBe(512);
}
});
test('destNames differ between sub-engines', async ({ page }) => {
await loadApp(page);
await switchToModular(page);
const sub = await page.evaluate(() => window.__nisps.activeEngine?.destNames);
await page.evaluate(async () => {
await window.__nisps.setModularSubEngine('fm');
});
const fm = await page.evaluate(() => window.__nisps.activeEngine?.destNames);
expect(sub).toBeTruthy();
expect(fm).toBeTruthy();
expect(sub).toEqual(['pitch','osc2_detune','osc3_detune','osc_mix_bal','noise_level','cutoff','resonance','filter_env_amt','amp','pan']);
expect(fm[1]).toBe('op1_level'); // fm-specific
expect(sub).not.toEqual(fm);
});
test('ADSR count change rebuilds paramMeta', async ({ page }) => {
await loadApp(page);
await switchToModular(page);
const baseline = await page.evaluate(() => window.__nisps.paramCount);
expect(baseline).toBe(512);
await page.evaluate(() => window.__nisps.setModularSourceCount(6, 8));
const after = await page.evaluate(() => window.__nisps.paramCount);
// 6 ADSR × 4 + 8 LFO × 2 + 48 × 10 = 24 + 16 + 480 = 520
expect(after).toBe(520);
await page.evaluate(() => window.__nisps.setModularSourceCount(4, 8));
const reset = await page.evaluate(() => window.__nisps.paramCount);
expect(reset).toBe(512);
});
test('getState returns a snapshot with raw dsp values', async ({ page }) => {
await loadApp(page);
await switchToModular(page);
const snap = await page.evaluate(() => window.__nisps.getModularState());
expect(snap).toBeTruthy();
expect(snap.version).toBe(1);
expect(snap.subEngine).toBe('subtractive');
expect(typeof snap.dsp).toBe('object');
// Default patch pre-arms ADSR1 but does not route it to amp — voice
// stays audible because base_amp defaults to 1.0.
expect(snap.dsp['MM_ADSR/00_adsr01_enable']).toBeCloseTo(1.0, 4);
expect(snap.dsp['4_Master/04_base_amp']).toBeCloseTo(1.0, 4);
});
test('matrix cell persistence across setState', async ({ page }) => {
await loadApp(page);
await switchToModular(page);
// Pick a distinctive cell: ADSR2 (s=1) → cutoff (d=5) on subtractive.
await page.evaluate(() => {
const engine = window.__nisps.activeEngine;
const idx = engine.paramMeta.findIndex(m =>
m.label === 'MM_Matrix/s01_d05_cutoff');
if (idx < 0) throw new Error('no s01_d05_cutoff cell in paramMeta');
// paramMeta min=-1 max=1; 0.9 in norm = 0.8 raw.
engine.setParam(idx, 0.9);
});
const snap = await page.evaluate(() => window.__nisps.getModularState());
expect(snap.dsp['MM_Matrix/s01_d05_cutoff']).toBeCloseTo(0.8, 4);
// Mutate further, then restore.
await page.evaluate(() => {
const engine = window.__nisps.activeEngine;
const idx = engine.paramMeta.findIndex(m =>
m.label === 'MM_Matrix/s01_d05_cutoff');
engine.setParam(idx, 0.1);
});
const midSnap = await page.evaluate(() => window.__nisps.getModularState());
expect(midSnap.dsp['MM_Matrix/s01_d05_cutoff']).not.toBeCloseTo(0.8, 4);
await page.evaluate(async (s) => {
await window.__nisps.setModularState(s);
}, snap);
const restored = await page.evaluate(() => window.__nisps.getModularState());
expect(restored.dsp['MM_Matrix/s01_d05_cutoff']).toBeCloseTo(0.8, 4);
});
test('modular DSP state survives a page reload', async ({ page }) => {
// NOTE: don't use loadApp() because it installs an addInitScript that
// clears nisps-a-immersive on every navigation — including our reload.
// Replicate loadApp's bootstrap inline, using a localStorage sentinel
// (NOT window.__x) so the "first nav only" guard survives subsequent
// navigations on the same origin.
await page.addInitScript(() => {
if (!localStorage.getItem('__nisps-test-bootstrapped')) {
localStorage.setItem('__nisps-test-bootstrapped', '1');
localStorage.removeItem('nisps-a-immersive');
}
localStorage.setItem('nisps-help-seen', '1');
});
await page.goto('/a-immersive.html?debug=1');
await page.waitForFunction(() => window.__nisps !== undefined, { timeout: 20_000 });
await switchToModular(page);
// Set a distinctive value, save, then reload the page (localStorage
// is now preserved across the nav because __nispsTestBootstrapped is set).
await page.evaluate(() => {
const engine = window.__nisps.activeEngine;
engine._setRawByLabel('3_Filter/01_resonance', 0.73);
});
await page.evaluate(() => window.__nisps.saveState());
await page.goto('/a-immersive.html?debug=1');
await page.waitForFunction(() => window.__nisps !== undefined, { timeout: 20_000 });
// Engine is deferred-constructed; clicking the modular card re-instantiates
// it and the pending DSP state should be applied before setActiveEngine.
await switchToModular(page);
const restored = await page.evaluate(() => window.__nisps.getModularState());
expect(restored).toBeTruthy();
expect(restored.dsp['3_Filter/01_resonance']).toBeCloseTo(0.73, 4);
});
test('preset apply: plucky bass sets the expected matrix routes', async ({ page }) => {
await loadApp(page);
await switchToModular(page);
const ok = await page.evaluate(async () => {
return await window.__nisps.applyModularPreset('modular-plucky-bass');
});
expect(ok).toBe(true);
const snap = await page.evaluate(() => window.__nisps.getModularState());
expect(snap.subEngine).toBe('subtractive');
// ADSR2 fast decay
expect(snap.dsp['MM_ADSR/01_adsr02_decay']).toBeCloseTo(0.15, 4);
// Matrix: ADSR2 (s01) → cutoff (d05) at raw 0.8
expect(snap.dsp['MM_Matrix/s01_d05_cutoff']).toBeCloseTo(0.8, 4);
// Filter cutoff moved to 400 Hz
expect(snap.dsp['3_Filter/00_cutoff']).toBeCloseTo(400, 2);
});
test('preset apply: DX bell swaps to fm sub-engine', async ({ page }) => {
await loadApp(page);
await switchToModular(page);
await page.evaluate(async () => {
await window.__nisps.applyModularPreset('modular-dx-bell');
});
const snap = await page.evaluate(() => window.__nisps.getModularState());
expect(snap.subEngine).toBe('fm');
expect(snap.dsp['MM_Matrix/s02_d03_op3_level']).toBeCloseTo(1.0, 4);
// paramCount should still be 512 after the cross-engine swap.
const count = await page.evaluate(() => window.__nisps.paramCount);
expect(count).toBe(512);
});
test('initial outputs are in [0,1] after modular swap', async ({ page }) => {
await loadApp(page);
await switchToModular(page);
// Set inputs so the MLP runs a forward pass.
await page.evaluate(() => window.__nisps.setInputs(0.3, 0.7));
const outputs = await page.evaluate(() => window.__nisps.getOutputs());
expect(outputs.length).toBe(512);
for (const v of outputs) {
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
}
});
});

View file

@ -1,118 +0,0 @@
/**
* State persistence tests localStorage round-trip and URL param application.
*/
const { test, expect } = require('@playwright/test');
const { loadApp, statusText } = require('./helpers');
const STORAGE_KEY = 'nisps-a-immersive';
test.describe('State persistence', () => {
test('fresh load with no localStorage starts untrained', async ({ page }) => {
await loadApp(page); // helpers.js clears localStorage before load
const text = await statusText(page);
expect(text).toContain('0 examples');
expect(text).toContain('untrained');
});
test('URL ?preset=beginner-1 applies preset on load', async ({ page }) => {
await loadApp(page, '&preset=beginner-1');
const selected = await page.locator('#synth-preset-select').inputValue();
expect(selected).toBe('beginner-1');
});
test('URL ?preset=advanced-2 applies advanced preset', async ({ page }) => {
await loadApp(page, '&preset=advanced-2');
const selected = await page.locator('#synth-preset-select').inputValue();
expect(selected).toBe('advanced-2');
});
test('URL ?spread=0 is accepted without crash', async ({ page }) => {
await loadApp(page, '&spread=0');
// App should be functional — probe must still exist
const probe = await page.evaluate(() => typeof window.__nisps);
expect(probe).toBe('object');
});
test('URL ?spread=1 is accepted without crash', async ({ page }) => {
await loadApp(page, '&spread=1');
const probe = await page.evaluate(() => typeof window.__nisps);
expect(probe).toBe('object');
});
test('examples + weights persist across reload via localStorage', async ({ page }) => {
// Navigate fresh — no init script so localStorage is untouched between loads.
// Suppress the help overlay by pre-setting nisps-help-seen via evaluate.
await page.goto(`/a-immersive.html?debug=1`);
await page.evaluate(() => localStorage.removeItem('nisps-a-immersive'));
await page.evaluate(() => localStorage.setItem('nisps-help-seen', '1'));
await page.waitForFunction(() => window.__nisps !== undefined, { timeout: 20_000 });
// Write 2 examples directly to localStorage in the app's save format.
await page.evaluate(([key]) => {
const state = {
features: [[0.1, 0.9], [0.9, 0.1]],
labels: [new Array(126).fill(0.2), new Array(126).fill(0.8)],
handFeatures: [],
handLabels: [],
noiseLevel: 0.05,
outputMode: 'visual',
inputMode: 'joystick',
joyX: 0.5, joyY: 0.5,
groupOverrides: null,
visualOverrides: null,
midiCCOverrides: null,
audioCanvasState: null,
synthPresetId: null,
};
localStorage.setItem(key, JSON.stringify(state));
}, [STORAGE_KEY]);
// Reload — no addInitScript registered, so localStorage is preserved.
await page.reload();
await page.waitForFunction(() => window.__nisps !== undefined, { timeout: 20_000 });
// loadState() runs sync training; wait for status to show a loss value.
await page.waitForFunction(
() => document.getElementById('status-text').textContent.includes('loss'),
{ timeout: 15_000 }
);
const count = await page.evaluate(() => window.__nisps.getExampleCount());
expect(count).toBe(2);
const text = await statusText(page);
expect(text).toContain('2 examples');
expect(text).toContain('loss');
});
test('saveState probe writes valid JSON to localStorage', async ({ page }) => {
await loadApp(page);
await page.evaluate(() => window.__nisps.saveState());
const raw = await page.evaluate(([key]) => localStorage.getItem(key), [STORAGE_KEY]);
expect(raw).not.toBeNull();
const state = JSON.parse(raw);
expect(Array.isArray(state.features)).toBe(true);
expect(Array.isArray(state.labels)).toBe(true);
expect(typeof state.outputMode).toBe('string');
expect(typeof state.noiseLevel).toBe('number');
});
test('localStorage state is overwritten on next saveState call', async ({ page }) => {
await loadApp(page);
await page.evaluate(() => window.__nisps.saveState());
const before = await page.evaluate(([key]) => localStorage.getItem(key), [STORAGE_KEY]);
// Add an example and save again
await page.evaluate(() => {
window.__nisps.iml.addExample([0.3, 0.7], new Array(126).fill(0.5));
window.__nisps.saveState();
});
const after = await page.evaluate(([key]) => localStorage.getItem(key), [STORAGE_KEY]);
// State changed — the feature array should now include our example
const parsed = JSON.parse(after);
expect(parsed.features.length).toBeGreaterThan(0);
});
});

View file

@ -1,176 +0,0 @@
/**
* UI state machine tests drawers, mode switching, preset chips, keyboard shortcuts.
*/
const { test, expect } = require('@playwright/test');
const { loadApp, statusText } = require('./helpers');
test.describe('UI interactions', () => {
test.describe('Dock drawers', () => {
test('Train dock button opens training drawer', async ({ page }) => {
await loadApp(page);
await expect(page.locator('#drawer-training')).toHaveClass(/hidden/);
await page.click('[data-drawer="training"]');
await expect(page.locator('#drawer-training')).not.toHaveClass(/hidden/);
});
test('Drawer close button hides the drawer', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="training"]');
await expect(page.locator('#drawer-training')).not.toHaveClass(/hidden/);
await page.click('#drawer-training .drawer-close');
await expect(page.locator('#drawer-training')).toHaveClass(/hidden/);
});
test('Mode dock button opens mode drawer', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="mode"]');
await expect(page.locator('#drawer-mode')).not.toHaveClass(/hidden/);
});
test('Engine dock button opens engine drawer', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="params"]');
await expect(page.locator('#drawer-params')).not.toHaveClass(/hidden/);
});
test('multiple drawers can be open simultaneously', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="training"]');
await page.click('[data-drawer="mode"]');
// The app doesn't auto-close drawers — both can be open
await expect(page.locator('#drawer-training')).not.toHaveClass(/hidden/);
await expect(page.locator('#drawer-mode')).not.toHaveClass(/hidden/);
});
});
test.describe('Output mode switching', () => {
test('default mode is visual — synth quick controls hidden', async ({ page }) => {
await loadApp(page);
await expect(page.locator('#synth-quick-controls')).toHaveClass(/hidden/);
await expect(page.locator('#midi-cc-quick-controls')).toHaveClass(/hidden/);
});
test('switching to Synth shows synth quick controls', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await expect(page.locator('#synth-quick-controls')).not.toHaveClass(/hidden/);
});
test('switching to MIDI CC shows midi-cc quick controls', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="midi-cc"]');
await expect(page.locator('#midi-cc-quick-controls')).not.toHaveClass(/hidden/);
});
test('switching back to Visual hides synth controls', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
await page.click('[data-mode="visual"]');
await expect(page.locator('#synth-quick-controls')).toHaveClass(/hidden/);
});
test('heatmap shows 20 bars in visual mode', async ({ page }) => {
await loadApp(page);
const count = await page.locator('.heatmap-cell').count();
expect(count).toBe(20);
});
test('heatmap shows 126 bars in synth mode', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="mode"]');
await page.click('[data-mode="synth"]');
const count = await page.locator('.heatmap-cell').count();
expect(count).toBe(126);
});
});
test.describe('Status line', () => {
test('shows 0 examples · untrained on fresh load', async ({ page }) => {
await loadApp(page);
const text = await statusText(page);
expect(text).toContain('0 examples');
expect(text).toContain('untrained');
});
});
test.describe('Training controls', () => {
test('Randomize button changes heatmap bar widths', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="training"]');
const widthsBefore = await page.evaluate(() =>
Array.from(document.querySelectorAll('.heatmap-cell-bar')).map(el => el.style.width)
);
await page.click('#btn-randomize');
const widthsAfter = await page.evaluate(() =>
Array.from(document.querySelectorAll('.heatmap-cell-bar')).map(el => el.style.width)
);
const anyChanged = widthsBefore.some((w, i) => w !== widthsAfter[i]);
expect(anyChanged).toBe(true);
});
test('preset chip loads examples and trains', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="training"]');
await page.click('[data-preset="calm-to-chaotic"]');
// calm-to-chaotic has 3 examples; training is async
await page.waitForFunction(
() => document.getElementById('status-text').textContent.includes('loss'),
{ timeout: 15_000 }
);
const text = await statusText(page);
expect(text).toContain('3 examples');
expect(text).toContain('loss');
});
test('Clear Ex resets example count to 0', async ({ page }) => {
await loadApp(page);
await page.click('[data-drawer="training"]');
await page.click('[data-preset="calm-to-chaotic"]');
await page.waitForFunction(
() => document.getElementById('status-text').textContent.includes('loss'),
{ timeout: 15_000 }
);
await page.click('#btn-clear-examples');
const text = await statusText(page);
expect(text).toContain('0 examples');
});
});
test.describe('Keyboard shortcuts', () => {
test('key 2 triggers thumbs-up (adds example)', async ({ page }) => {
await loadApp(page);
const before = await page.evaluate(() => window.__nisps.getExampleCount());
await page.keyboard.press('2');
await page.waitForTimeout(200);
const after = await page.evaluate(() => window.__nisps.getExampleCount());
expect(after).toBe(before + 1);
});
test('key 1 triggers thumbs-down (changes outputs)', async ({ page }) => {
await loadApp(page);
const before = await page.evaluate(() => window.__nisps.getOutputs());
await page.keyboard.press('1');
const after = await page.evaluate(() => window.__nisps.getOutputs());
const anyChanged = before.some((v, i) => Math.abs(v - after[i]) > 0.0001);
expect(anyChanged).toBe(true);
});
test('key Z triggers undo after thumbs-down', async ({ page }) => {
await loadApp(page);
const before = await page.evaluate(() => window.__nisps.getWeights());
await page.keyboard.press('1'); // thumbs-down
await page.keyboard.press('z'); // undo
const after = await page.evaluate(() => window.__nisps.getWeights());
// Weights should be restored (approximately)
const maxDiff = before.reduce((m, v, i) => Math.max(m, Math.abs(v - after[i])), 0);
expect(maxDiff).toBeLessThan(1e-4);
});
});
});

View file

@ -1,231 +0,0 @@
/**
* WASM API tests verify new WasmIML methods:
* - inferBatch: batch inference across multiple input points
* - evalLoss: MSE loss evaluation without training
* - getLayerStats: per-layer weight statistics
* - lossHistory: full per-iteration loss curve after training
* - moveWeights with pin mask: pinned outputs stay unchanged
*/
const { test, expect } = require('@playwright/test');
const { loadApp } = require('./helpers');
const EXAMPLE_LOW = { input: [0.1, 0.9], output: new Array(126).fill(0.1) };
const EXAMPLE_HIGH = { input: [0.9, 0.1], output: new Array(126).fill(0.9) };
test.describe('inferBatch', () => {
test('returns correct count of output arrays, each length 126', async ({ page }) => {
await loadApp(page);
const result = await page.evaluate(() => {
const outputs = window.__nisps.iml.inferBatch([[0.2, 0.8], [0.5, 0.5], [0.9, 0.1]]);
return outputs.map(o => Array.from(o));
});
expect(result).toHaveLength(3);
for (const arr of result) {
expect(arr).toHaveLength(126);
}
});
test('matches individual inference within 1e-5', async ({ page }) => {
await loadApp(page);
const points = [[0.1, 0.2], [0.3, 0.7], [0.5, 0.5], [0.8, 0.1], [0.0, 1.0]];
const { batchResults, individualResults } = await page.evaluate((pts) => {
const iml = window.__nisps.iml;
const batch = iml.inferBatch(pts).map(o => Array.from(o));
const individual = pts.map(([x, y]) => {
iml.setInput(0, x);
iml.setInput(1, y);
iml.process();
return Array.from(iml.getOutputs());
});
return { batchResults: batch, individualResults: individual };
}, points);
expect(batchResults).toHaveLength(5);
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 126; j++) {
expect(Math.abs(batchResults[i][j] - individualResults[i][j])).toBeLessThan(1e-5);
}
}
});
test('all batch outputs are in [0, 1]', async ({ page }) => {
await loadApp(page);
const corners = [
[0, 0], [0, 1], [1, 0], [1, 1],
[0.5, 0], [0.5, 1], [0, 0.5], [1, 0.5],
[0.25, 0.75], [0.75, 0.25],
];
const results = await page.evaluate((pts) => {
return window.__nisps.iml.inferBatch(pts).map(o => Array.from(o));
}, corners);
expect(results).toHaveLength(10);
for (const arr of results) {
for (const v of arr) {
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
}
}
});
});
test.describe('evalLoss', () => {
test('returns null when no examples exist', async ({ page }) => {
await loadApp(page);
const loss = await page.evaluate(() => window.__nisps.iml.evalLoss());
expect(loss).toBeNull();
});
test('returns finite non-negative value when examples exist', async ({ page }) => {
await loadApp(page);
await page.evaluate(([low, high]) => {
window.__nisps.iml.addExample(low.input, low.output);
window.__nisps.iml.addExample(high.input, high.output);
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
const loss = await page.evaluate(() => window.__nisps.iml.evalLoss());
expect(typeof loss).toBe('number');
expect(isFinite(loss)).toBe(true);
expect(loss).toBeGreaterThanOrEqual(0);
});
test('does not change weights', async ({ page }) => {
await loadApp(page);
await page.evaluate(([low, high]) => {
window.__nisps.iml.addExample(low.input, low.output);
window.__nisps.iml.addExample(high.input, high.output);
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
const { before, after } = await page.evaluate(() => {
const weightsBefore = window.__nisps.getWeights();
window.__nisps.iml.evalLoss();
const weightsAfter = window.__nisps.getWeights();
return { before: Array.from(weightsBefore), after: Array.from(weightsAfter) };
});
expect(before).toEqual(after);
});
});
test.describe('getLayerStats', () => {
test('returns 4 layers for [3, 32, 48, 64, 126] architecture', async ({ page }) => {
await loadApp(page);
const stats = await page.evaluate(() => window.__nisps.iml.getLayerStats());
expect(stats).toHaveLength(4);
});
test('each layer has all 4 stat fields with valid ranges', async ({ page }) => {
await loadApp(page);
const stats = await page.evaluate(() => window.__nisps.iml.getLayerStats());
for (const layer of stats) {
expect(typeof layer.meanAbs).toBe('number');
expect(typeof layer.maxAbs).toBe('number');
expect(typeof layer.deadFrac).toBe('number');
expect(typeof layer.satFrac).toBe('number');
expect(layer.meanAbs).toBeGreaterThanOrEqual(0);
expect(layer.maxAbs).toBeGreaterThanOrEqual(0);
expect(layer.deadFrac).toBeGreaterThanOrEqual(0);
expect(layer.deadFrac).toBeLessThanOrEqual(1);
expect(layer.satFrac).toBeGreaterThanOrEqual(0);
expect(layer.satFrac).toBeLessThanOrEqual(1);
}
});
});
test.describe('lossHistory', () => {
test('training populates lossHistory with multiple entries', async ({ page }) => {
await loadApp(page);
await page.evaluate(([low, high]) => {
window.__nisps.iml.addExample(low.input, low.output);
window.__nisps.iml.addExample(high.input, high.output);
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
const histLen = await page.evaluate(() => {
window.__nisps.train();
return window.__nisps.iml.lossHistory.length;
});
expect(histLen).toBeGreaterThan(1);
});
test('all loss history entries are finite non-negative', async ({ page }) => {
await loadApp(page);
await page.evaluate(([low, high]) => {
window.__nisps.iml.addExample(low.input, low.output);
window.__nisps.iml.addExample(high.input, high.output);
}, [EXAMPLE_LOW, EXAMPLE_HIGH]);
const history = await page.evaluate(() => {
window.__nisps.train();
return [...window.__nisps.iml.lossHistory];
});
expect(history.length).toBeGreaterThan(0);
for (const v of history) {
expect(typeof v).toBe('number');
expect(isFinite(v)).toBe(true);
expect(v).toBeGreaterThanOrEqual(0);
}
});
});
test.describe('moveWeights with pin mask', () => {
test('pinned output-layer weights unchanged, unpinned weights changed', async ({ page }) => {
await loadApp(page);
// The pin mask protects output-layer weights for pinned nodes.
// Hidden layer weights still change (shared), so we compare flat weight
// vectors and check that the output-layer segment for pinned nodes is
// identical while unpinned weights differ.
const { pinnedWeightsMatch, anyUnpinnedWeightChanged } = await page.evaluate(() => {
const iml = window.__nisps.iml;
const weightsBefore = Array.from(window.__nisps.getWeights());
// Pin first 10 outputs
const pinMask = new Uint8Array(126);
for (let i = 0; i < 10; i++) pinMask[i] = 1;
// Apply noise (spread=0 means no decay, just additive noise)
iml.moveWeights(0.3, 0, pinMask);
const weightsAfter = Array.from(window.__nisps.getWeights());
// The architecture is [3, 32, 48, 64, 126].
// Output layer: 126 nodes, each with 64+1=65 weights (64 inputs + bias).
// The output layer weights are at the end of the flat array.
const outputLayerWeights = 126 * 65; // 8190
const outputLayerStart = weightsBefore.length - outputLayerWeights;
const weightsPerNode = 65;
// Check pinned nodes (first 10) have identical weights
let pinnedAllMatch = true;
for (let n = 0; n < 10; n++) {
const nodeStart = outputLayerStart + n * weightsPerNode;
for (let w = 0; w < weightsPerNode; w++) {
if (weightsBefore[nodeStart + w] !== weightsAfter[nodeStart + w]) {
pinnedAllMatch = false;
break;
}
}
if (!pinnedAllMatch) break;
}
// Check unpinned output nodes (10-125) have at least some changed weights
let unpinnedChanged = false;
for (let n = 10; n < 126; n++) {
const nodeStart = outputLayerStart + n * weightsPerNode;
for (let w = 0; w < weightsPerNode; w++) {
if (weightsBefore[nodeStart + w] !== weightsAfter[nodeStart + w]) {
unpinnedChanged = true;
break;
}
}
if (unpinnedChanged) break;
}
return { pinnedWeightsMatch: pinnedAllMatch, anyUnpinnedWeightChanged: unpinnedChanged };
});
expect(pinnedWeightsMatch).toBe(true);
expect(anyUnpinnedWeightChanged).toBe(true);
});
});