Spaces:
Running
Running
Download src/stimulus.js from lordhet/fruit-fly-simulation: direct link, hf CLI and curl.
- Browser
- Download file 3.46 kB
-
https://huggingface.co/spaces/lordhet/fruit-fly-simulation/resolve/main/src/stimulus.js
- Command line
-
hf download hf://spaces/lordhet/fruit-fly-simulation/src/stimulus.js
-
curl -L -o stimulus.js https://huggingface.co/spaces/lordhet/fruit-fly-simulation/resolve/main/src/stimulus.js
3.46 kB
| /** Input envelope is defined in neural milliseconds, independent of render FPS. */ | |
| export const PULSE_ENVELOPES = Object.freeze({ | |
| paint: Object.freeze({ holdMs: 80, decayMs: 180, durationMs: 1000 }), | |
| turn: Object.freeze({ holdMs: 650, decayMs: 220, durationMs: 2500 }), | |
| }); | |
| export class PulseBank { | |
| constructor(n) { | |
| this.n = n; | |
| this.pulses = []; | |
| this.rates = new Float32Array(n); | |
| } | |
| add(indices, tick, strength = 180, profile = 'paint') { | |
| if (!Object.hasOwn(PULSE_ENVELOPES, profile)) throw Error('Unknown pulse profile'); | |
| const unique = [...new Set(indices)].filter((i) => Number.isInteger(i) && i >= 0 && i < this.n); | |
| if (!unique.length) return; | |
| // Coalesce rapid strokes in the same simulation tick and bound retained pulses. | |
| const last = this.pulses.at(-1); | |
| if (last?.tick === tick && last.strength === strength && last.profile === profile) | |
| last.indices = [...new Set([...last.indices, ...unique])]; | |
| else this.pulses.push({ indices: unique, tick, strength, profile }); | |
| if (this.pulses.length > 32) this.pulses.splice(0, this.pulses.length - 32); | |
| } | |
| sample(tick) { | |
| this.rates.fill(0); | |
| this.pulses = this.pulses.filter( | |
| (p) => (tick - p.tick) * 0.1 < PULSE_ENVELOPES[p.profile].durationMs, | |
| ); | |
| for (const p of this.pulses) { | |
| const ms = (tick - p.tick) * 0.1, | |
| envelope = PULSE_ENVELOPES[p.profile]; | |
| const rate = p.strength * Math.exp(-Math.max(0, ms - envelope.holdMs) / envelope.decayMs); | |
| if (rate < 1) continue; | |
| for (const i of p.indices) this.rates[i] = Math.max(this.rates[i], rate); | |
| } | |
| return this.rates; | |
| } | |
| reset() { | |
| this.pulses = []; | |
| this.rates.fill(0); | |
| } | |
| } | |
| export function populations(neurons) { | |
| const g = { | |
| walkLeft: [], | |
| walkRight: [], | |
| turnLeft: [], | |
| turnRight: [], | |
| reverse: [], | |
| escape: [], | |
| walk: [], | |
| left: [], | |
| right: [], | |
| escapeInput: [], | |
| }; | |
| neurons.forEach((r, i) => { | |
| const t = r[1], | |
| s = r[3], | |
| side = s === 'L' ? 'Left' : s === 'R' ? 'Right' : null; | |
| if (side && ['DNp09', 'DNg100', 'DNg97'].includes(t)) g['walk' + side].push(i); | |
| if (side && ['DNa02', 'DNa11', 'DNg13'].includes(t)) g['turn' + side].push(i); | |
| if (t === 'MDN') g.reverse.push(i); | |
| if (t === 'DNp01') g.escape.push(i); | |
| if (t === 'LC9') g.walk.push(i); | |
| // DNa02 supplies a direct turn drive alongside the LC9 input. | |
| if (side && (t === 'LC9' || t === 'DNa02')) g[s === 'L' ? 'left' : 'right'].push(i); | |
| if (t === 'LC4') g.escapeInput.push(i); | |
| }); | |
| return g; | |
| } | |
| export const CHANNELS = ['walkLeft', 'walkRight', 'turnLeft', 'turnRight', 'reverse', 'escape']; | |
| // Readout columns contain only the contributing descending neurons. | |
| export function compactReadout(groups) { | |
| const indices = Uint32Array.from(new Set(CHANNELS.flatMap((key) => groups[key]))); | |
| const width = Math.max(1, indices.length), | |
| columns = new Map([...indices].map((id, i) => [id, i])); | |
| const weights = new Float32Array(CHANNELS.length * width); | |
| CHANNELS.forEach((key, c) => { | |
| for (const id of groups[key]) weights[c * width + columns.get(id)] = 1 / groups[key].length; | |
| }); | |
| return { indices, width, weights }; | |
| } | |
| export function decodeCounts(counts, groups, steps) { | |
| return Float32Array.from(CHANNELS, (key) => { | |
| let sum = 0; | |
| for (const i of groups[key]) sum += counts[i]; | |
| return groups[key].length ? ((sum / groups[key].length) * 10000) / steps : 0; | |
| }); | |
| } | |