Fruit Fly
README.md
brain
A whole connectome running as a leaky integrate-and-fire network on the GPU.
Brain.load binds any connectome asset to the package's kernels and returns
a network; the FruitFlyBrain component is the usual owner, and anything
holding the network can step, drive, cut, read out and draw it.
Exports
M.load(connectomeRef, params?) -> Brain: allocate the network at rest.paramsoverrides any key ofParams:dtMs,vRest,vThresh,vReset,tauM,tauSyn,refracMs,delayMs,weightMv,gain,minSynapses,traceMs,maxSpikes,seed,timeScale,maxStepsPerFrame. A key that is not one of those raises, assetParamsdoes, and so does a value that is not a number.brain:update(dt): once a frame: queue the stepsdtcovers, issue and drain the counter readback, and fold the activity snapshot when something has asked for one.brain:step(n): queuensteps outright.brain:select(sel) -> { index }: the neurons a selection names, ascending. The array is the network's own and memoized on the selection, so read it rather than writing into it.brain:stimulate(sel, hz, source?): Poisson drive. The rate a neuron sees is the largest any source asked for, so the fly's senses and the bench's experiments can drive the same neurons without taking each other's drive away.brain:drives()lists{ source, count, hz }per source andbrain:clearStimulation(source?)clears one source or all of them.brain:silence(sel, on): ablate or restore.brain:watch(name, sel)/brain:unwatch(name)/brain:rates(): read groups out as Hz per neuron. A neuron carries at most three groups at once and a watch that would need a fourth on any neuron is refused, naming the neuron and the groups it holds. Watching a name again with the same selection is a no-op;unwatchfrees the slots and the group's id.brain:classRates(limit?): the cell classes with the most activity right now, from the 60 ms spike trace.brain:groupActivity(name): that trace per neuron for a watched group of at most 32, which is what a raster draws a row from. Both arm a per-neuron readback that is taken at most every twelve frames and only while something is asking, because it reads the whole activity lane.brain:activity(): network mean rate, total spikes, dropped spikes, simulated time.brain:info(i): what one neuron is.brain:buildCloud(opts, onReady?)/brain:setCloudDrawn(on)/brain:cloudDrawn()/brain:cloudBounds()/brain:setCloudOrigin(xyz)/brain:showCloud(opts)/brain:hideCloud()/brain:worldPosition(i)/brain:neuronAt(screenX, screenY, radiusPx?): the instanced neuron cloud.brain:setParams(patch),brain:setRunning(on),brain:setTimeScale(x),brain:setMaxStepsPerFrame(n),brain:reset(),brain:ready(),brain:destroy(),brain:describe().
Usage
local Brain = require("~.brain") -- from inside the package
local brain = Brain.load(asset.resolve("fruitFly.maleCNS", "connectome"), { gain = 0.6 })
brain:watch("MN9", { types = { "MN9" } })
brain:stimulate({ types = { "claw_tpGRN" } }, 100, "bench")
-- every frame:
brain:update(dt)
print(brain:rates().MN9)
A selection is { types?, typePrefix?, pattern?, classes?, superclasses?, sides?, excitatory?, indices?, limit? }; every given key must hold.
The cloud, and why it is built in two halves
A registered instanced population has its whole transform run recomposed on
the CPU every frame, whether or not the cull draws any of it, so 139,741
neurons cost about 4.7 ms of frame whether or not anyone is looking at the
brain. buildCloud fills the buffers and setCloudDrawn registers or drops
the draw without touching them, which is what lets FruitFlyBrain put the
registration down while the camera is pointed somewhere else and put it back
the frame the cloud enters view. Taking it down leaves worldPosition,
neuronAt and cloudBounds answering as before.
Only neurons that have a soma get a slot, which is 139,741 of 164,587. Slot
and neuron index are therefore different numbers, so the cloud_gather kernel
copies each slot's activity out of the neuron it draws once a frame, and
slotToNeuron maps back for picking.
The fill is spread over frames from a task, about forty scan slices and eighteen upload slices, so building the cloud costs about a millisecond a frame for a second rather than one frame of a third of a second.
Notes
- The network is silent until something is driven; a spike always traces back to a stimulus.
- Every GPU buffer is named
brain.<id>.<name>;compute.observe()lists them.destroyreleases them all. - The kernels'
paramsare per shader, so two networks on one engine share the model constants the lastapplyParamswrote. - Editing this file hot-reloads its methods onto networks that are already
running. A network records the
SHAPEit was made with andFruitFlyBrainloads a fresh one when the module's shape has moved past it, so raiseSHAPEwhenever a field is added to or taken off a network or its cloud.