fruit-fly-simulation / src /stimulus.js
Xenova's picture
Xenova HF Staff
Upload 227 files
cc38b25 verified
Raw History Blame Contribute Delete
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;
});
}