Spaces:
Running
Running
Download src/brain.js from lordhet/fruit-fly-simulation: direct link, hf CLI and curl.
- Browser
- Download file 4.44 kB
-
https://huggingface.co/spaces/lordhet/fruit-fly-simulation/resolve/main/src/brain.js
- Command line
-
hf download hf://spaces/lordhet/fruit-fly-simulation/src/brain.js
-
curl -L -o brain.js https://huggingface.co/spaces/lordhet/fruit-fly-simulation/resolve/main/src/brain.js
4.44 kB
| /** Connectome LIF reference. Units: mV, ms, Hz. No fitted weights. */ | |
| export const PARAMETERS = Object.freeze({ | |
| dt: 0.1, | |
| rest: -52, | |
| threshold: -45, | |
| tauM: 20, | |
| tauS: 5, | |
| refractory: 22, | |
| delay: 18, | |
| synapse: 0.275, | |
| poissonWeight: 68.75, | |
| }); | |
| export function randomWord(i, t, seed = 1) { | |
| let x = (Math.imul(i + 1, 747796405) ^ Math.imul(t + 1, 2891336453) ^ seed) >>> 0; | |
| x = Math.imul(x ^ (x >>> 16), 2246822519) >>> 0; | |
| x = Math.imul(x ^ (x >>> 13), 3266489917) >>> 0; | |
| return (x ^ (x >>> 16)) >>> 0; | |
| } | |
| export function outgoingGraph(g) { | |
| const n = g.n, | |
| offsets = new Uint32Array(n + 1); | |
| for (const i of g.sources) offsets[i + 1]++; | |
| for (let i = 0; i < n; i++) offsets[i + 1] += offsets[i]; | |
| const cursor = offsets.slice(), | |
| targets = new Uint32Array(g.sources.length), | |
| counts = new Uint32Array(g.sources.length); | |
| for (let j = 0; j < n; j++) | |
| for (let e = g.offsets[j]; e < g.offsets[j + 1]; e++) { | |
| const k = cursor[g.sources[e]]++; | |
| targets[k] = j; | |
| counts[k] = g.counts[e]; | |
| } | |
| return { offsets, targets, counts }; | |
| } | |
| const EM = Math.exp(-PARAMETERS.dt / PARAMETERS.tauM); | |
| const ES = Math.exp(-PARAMETERS.dt / PARAMETERS.tauS); | |
| const COUPLING = (PARAMETERS.tauS / (PARAMETERS.tauM - PARAMETERS.tauS)) * (EM - ES); | |
| /** Event-driven scheduling; exactly resting neurons need no state update. */ | |
| export class BrainCPU { | |
| constructor(graph, { seed = 1 } = {}) { | |
| this.graph = graph; | |
| this.n = graph.n; | |
| this.seed = seed; | |
| this.out = outgoingGraph(graph); | |
| this.reset(); | |
| } | |
| reset() { | |
| const n = this.n; | |
| this.v = new Float32Array(n).fill(-52); | |
| this.g = new Float32Array(n); | |
| this.until = new Uint32Array(n); | |
| this.counts = new Uint32Array(n); | |
| this.history = Array.from({ length: 19 }, () => []); | |
| this.tick = 0; | |
| this.active = new Uint32Array(n); | |
| this.present = new Uint8Array(n); | |
| this.activeCount = 0; | |
| } | |
| activate(i) { | |
| if (!this.present[i]) { | |
| this.present[i] = 1; | |
| this.active[this.activeCount++] = i; | |
| } | |
| } | |
| step(rates, externalEvents = null, silenced = false) { | |
| const driven = []; | |
| for (let i = 0; i < this.n; i++) | |
| if (externalEvents ? externalEvents[i] : rates[i] > 0) driven.push(i); | |
| return this.advance(rates, driven, externalEvents, silenced); | |
| } | |
| advance(rates, driven, externalEvents, silenced) { | |
| const { v, g, until, counts, active, present } = this, | |
| t = this.tick, | |
| p = PARAMETERS; | |
| const fired = []; | |
| let kept = 0; | |
| for (let k = 0; k < this.activeCount; k++) { | |
| const i = active[k]; | |
| // No epsilon cutoff: skip only the exact stationary state. Refractory | |
| // deadlines remain stored and incoming events still check them. | |
| if (v[i] === p.rest && g[i] === 0) { | |
| present[i] = 0; | |
| continue; | |
| } | |
| active[kept++] = i; | |
| if (t >= until[i]) { | |
| v[i] = p.rest + (v[i] - p.rest) * EM + g[i] * COUPLING; | |
| g[i] *= ES; | |
| if (v[i] > p.threshold) fired.push(i); | |
| } | |
| } | |
| this.activeCount = kept; | |
| // Preserve the dense reference's source order and Float32 accumulation. | |
| fired.sort((a, b) => a - b); | |
| const due = this.history[t % 19], | |
| out = this.out; | |
| if (!silenced) | |
| for (const i of due) { | |
| const sign = this.graph.sign[i] * p.synapse; | |
| if (!sign) continue; | |
| for (let e = out.offsets[i]; e < out.offsets[i + 1]; e++) { | |
| const j = out.targets[e]; | |
| if (t >= until[j]) { | |
| g[j] += out.counts[e] * sign; | |
| this.activate(j); | |
| } | |
| } | |
| } | |
| for (const i of driven) { | |
| if ( | |
| t >= until[i] && | |
| (externalEvents || randomWord(i, t, this.seed) / 4294967296 < (rates[i] * p.dt) / 1000) | |
| ) { | |
| v[i] += p.poissonWeight; | |
| this.activate(i); | |
| } | |
| } | |
| for (const i of fired) { | |
| v[i] = p.rest; | |
| g[i] = 0; | |
| until[i] = t + (rates[i] > 0 ? 0 : p.refractory); | |
| counts[i]++; | |
| } | |
| this.history[t % 19] = []; | |
| this.history[(t + p.delay) % 19] = fired; | |
| this.tick++; | |
| return fired; | |
| } | |
| batch(steps, rates, silenced = false) { | |
| const driven = []; | |
| for (let i = 0; i < this.n; i++) if (rates[i] > 0) driven.push(i); | |
| this.counts.fill(0); | |
| let total = 0; | |
| for (let k = 0; k < steps; k++) total += this.advance(rates, driven, null, silenced).length; | |
| return { counts: this.counts.slice(), tick: this.tick, total }; | |
| } | |
| } | |