flybrain/packages/brain/examples/node-random-frames.ts
acamilo 660c3cf00d
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
flybrain v0.4.0: public tree (history retained privately)
2026-09-21 15:09:46 +00:00

100 lines
4.4 KiB
TypeScript

/**
* Run the agent loop on the real connectome, off a synthetic environment.
*
* ```sh
* npx tsx examples/node-random-frames.ts [frames] # from packages/brain
* ```
*
* There is no emulator here: the "environment" is a deterministic noise frame that shifts a little
* every frame, plus a reward every 40th frame. That is enough to see the whole loop work and to
* measure how many frames per second the network sustains on this machine — a Game Boy needs
* 59.73, so anything above that runs in real time.
*/
import { loadBrainDatasetFromDir } from '../src/dataset/load-node';
import { GAMEBOY_MS_PER_FRAME, NeuralAgent } from '../src/agent/agent';
import { gameboyDecoderConfig } from '../src/readout/presets/gameboy';
const FRAME_WIDTH = 160;
const FRAME_HEIGHT = 144;
const REWARD_EVERY = 40;
const DATA_DIR = new URL('../../../data/fafb-v783', import.meta.url).pathname;
/** Deterministic xorshift32, so two runs of this example print the same numbers. */
function xorshift(seed: number): () => number {
let state = seed | 0 || 1;
return () => {
state ^= state << 13; state ^= state >>> 17; state ^= state << 5;
return (state >>> 0) / 0x1_0000_0000;
};
}
/** A drifting noise frame: fresh gradient offset per frame, cheap enough not to skew the timing. */
function noiseFrame(random: () => number, frame: number): Uint8Array {
const rgba = new Uint8Array(FRAME_WIDTH * FRAME_HEIGHT * 4);
const phase = frame * 7;
for (let y = 0; y < FRAME_HEIGHT; y++) {
for (let x = 0; x < FRAME_WIDTH; x++) {
const offset = (y * FRAME_WIDTH + x) * 4;
const value = (x + y + phase + Math.floor(random() * 64)) & 0xff;
rgba[offset] = value;
rgba[offset + 1] = 255 - value;
rgba[offset + 2] = (value * 3) & 0xff;
rgba[offset + 3] = 255;
}
}
return rgba;
}
function pad(text: string, width: number): string {
return text.length >= width ? text : ' '.repeat(width - text.length) + text;
}
async function main(): Promise<void> {
const frames = Number(process.argv[2] ?? 120);
if (!Number.isInteger(frames) || frames <= 0) throw new Error(`Frame count must be a positive integer, got ${process.argv[2]}`);
const loadStarted = performance.now();
const dataset = await loadBrainDatasetFromDir(DATA_DIR);
const loadMs = performance.now() - loadStarted;
console.log(`${dataset.meta.dataset}: ${dataset.meta.neurons} neurons, ${dataset.meta.edges} edges, ${dataset.meta.visual.count} retina columns (loaded in ${loadMs.toFixed(0)} ms)`);
const random = xorshift(20260915);
const agent = new NeuralAgent(dataset, {
decoder: gameboyDecoderConfig(),
frame: { width: FRAME_WIDTH, height: FRAME_HEIGHT },
});
console.log(`compatibility: ${agent.compatibility()}`);
console.log(`plastic synapses: ${agent.plasticity.statistics().synapses}, ${GAMEBOY_MS_PER_FRAME.toFixed(4)} ms per frame`);
const warmupStarted = performance.now();
agent.warmup(noiseFrame(random, 0));
console.log(`warm-up: ${agent.warmupMs} ms of network time in ${(performance.now() - warmupStarted).toFixed(0)} ms wall clock\n`);
const columns = ['frame', 'ms', 'pop rate', 'active', 'updates', 'changed', 'mean |dg|', 'signal'];
console.log(columns.map((name, index) => pad(name, [6, 8, 9, 18, 8, 8, 10, 7][index]!)).join(' '));
const clockStarted = agent.network.ms;
const tickStarted = performance.now();
for (let frame = 1; frame <= frames; frame++) {
const rewards = frame % REWARD_EVERY === 0 ? [{ value: 1 }] : [];
const result = agent.tick(noiseFrame(random, frame), { rewards, boot: frame <= frames / 2 });
if (frame % 10 === 0 || frame === frames) {
const { learning, ms, populationRate } = agent.snapshot();
console.log([
pad(String(frame), 6),
pad(ms.toFixed(0), 8),
pad(populationRate.toFixed(3), 9),
pad(result.active.join(',') || '-', 18),
pad(String(learning.updates), 8),
pad(String(learning.changed), 8),
pad(learning.meanChange.toExponential(2), 10),
pad(learning.signal.toFixed(3), 7),
].join(' '));
}
}
const elapsed = performance.now() - tickStarted;
console.log(`\n${frames} frames in ${elapsed.toFixed(0)} ms = ${(frames * 1000 / elapsed).toFixed(1)} frames/sec (Game Boy needs ${(1000 / GAMEBOY_MS_PER_FRAME).toFixed(2)})`);
console.log(`${((agent.network.ms - clockStarted) / elapsed).toFixed(2)}x real time; network clock at ${agent.network.ms} ms`);
}
await main();