A spiking simulation of a whole connectome on the GPU. `Brain.load`
binds any `connectome` asset to the kernels of this package and returns a
network you step, stimulate, silence, read out and draw: leaky
integrate-and-fire neurons, synapse-count weights signed by the
presynaptic transmitter, a fixed-point input ring for the synaptic delay,
a Poisson external drive per neuron keyed by the source that asked for
it, per-group spike counters read back every frame, and an instanced
cloud of every neuron with a soma, each lit by its own activity.
neuronMesh( ) → void
The octahedron every neuron is drawn as: six vertices, eight faces, unit
radius. Flat normals come from the position, which is what a point needs.
load(connectome: any, opts: { [string]: any }?) →
Load a connectome onto the GPU as a spiking network at rest.
value of a component's `Field.assetRef("connectome", ...)`, or
`asset.resolve("maleCNS", "connectome")` written as a literal.
which is `dtMs`, `vRest`, `vThresh`, `vReset`, `tauM`, `tauSyn`,
`refracMs`, `delayMs`, `weightMv`, `gain`, `minSynapses`, `traceMs`,
`maxSpikes`, `seed`, `timeScale` and `maxStepsPerFrame`. A key that is
not one of those raises, the way `setParams` does.
| arg | type | description |
|---|
| connectome | any | The connectome asset, an `AssetRef<connectome>`: the |
| opts | { [string]: any }? | Optional overrides of the model parameters: any key of `Params`, |
examples
local brain = Brain.load(asset.resolve("maleCNS", "connectome"), { gain = 0.5 })put(t: ?, k: ?, i: ?) → void
| arg | type | description |
|---|
| t | ? | |
| k | ? | |
| i | ? | |
applyParams( ) →
Push the model parameters to the kernels. Called by `load`, and again
by `setParams` after a change.
examples
brain:applyParams()
setParams(patch: { [string]: number }) →
Change model parameters live: any key `load` accepts. A change to
`dtMs`, `tauM`, `tauSyn`, `delayMs`, `weightMv` or `gain` takes effect on
the next step without a reset.
| arg | type | description |
|---|
| patch | { [string]: number } | The keys to change. |
examples
brain:setParams({ gain = 0.3 })reset( ) →
Put the network back at rest: every membrane at `vRest`, the input
ring empty, the counters at zero, the clock at step zero. Stimulation,
silencing and readout groups are kept.
step(count: number) →
Queue `count` simulation steps of `dtMs` each. Every step is three
dispatches: integrate, propagate, clear. When the cloud is on screen one
further dispatch follows the last step, gathering each drawn slot's
activity from the neuron it draws.
| arg | type | description |
|---|
| count | number | How many steps. |
gatherCloud( ) → void
Copy the activity of every drawn neuron into the slot the cloud draws it
in. Only worth doing while the population is registered.
ready( ) → boolean
Whether all four simulation kernels are compiled and resident. A step
asked for before then is skipped rather than queued behind the compile,
so the network's clock only runs on kernels that exist.
examples
if brain:ready() then brain:step(1) end
drainReadbacks(self: ?) → void
wantActivity(self: ?) → void
The integrate kernel writes 16 floats per neuron and the whole lane is a
10.5 MB read, so it is taken only while `classRates` or `groupActivity`
have been asked for recently, and at most once every ACTIVITY_MIN_FRAMES.
startClassSweep(self: ?) → void
stepClassSweep(self: ?) → void
pumpActivity(self: ?) → void
update(dt: number) → number
Advance the network by real time: queues as many steps as `dt`
seconds times `timeScale` covers (at most `maxStepsPerFrame`), issues the
counter readback for this frame, and folds in every readback that has
arrived. Call once per frame.
| arg | type | description |
|---|
| dt | number | Seconds since the previous call. |
examples
function update(dt) brain:update(dt) end
setRunning(on: boolean) →
Whether the network advances on `update`. `false` freezes it while
keeping every state.
| arg | type | description |
|---|
| on | boolean | Run or hold. |
examples
brain:setRunning(false)
setTimeScale(scale: number) →
Simulated milliseconds per real second, as a multiple of real time;
bounded by `maxStepsPerFrame` steps a frame.
| arg | type | description |
|---|
| scale | number | 1 is real time. |
examples
brain:setTimeScale(0.25)
setMaxStepsPerFrame(steps: number) →
The most simulation steps one frame queues, which is what caps the
network's GPU time per frame.
| arg | type | description |
|---|
| steps | number | The ceiling. |
examples
brain:setMaxStepsPerFrame(32)
simulatedMs( ) → number
How many milliseconds of network time have been simulated.
examples
print(brain:simulatedMs())
count( ) → number
How many neurons the network holds.
info(i: number) →
What one neuron is: its soma position (micrometres, centred), its
type, class, superclass, side, transmitter, out-degree, source body id,
its sexual dimorphism and fruitless/doublesex labels, and whether it
excites its targets.
| arg | type | description |
|---|
| i | number | The neuron index, from 0. |
examples
local info = brain:info(0)
unionOf(buckets: { [number]: { number } }, keys: { [number]: boolean }) → void
A selection is answered from the index buckets `load` built, one bucket per
type, class, superclass and side, rather than by walking every neuron. The
answer itself is memoized on a canonical key of the selection, so the bench
pressing stimulate on the same term twice pays for the walk once.
The indices every named bucket holds, ascending. One bucket is already
ascending; a union of several is sorted so a `limit` takes the same
neurons a full scan would have taken.
| arg | type | description |
|---|
| buckets | { [number]: { number } } | |
| keys | { [number]: boolean } | |
selectionKey(sel: Selection) → string
| arg | type | description |
|---|
| sel | Selection | |
remember(self: ?, key: string, answer: { number }) → void
| arg | type | description |
|---|
| self | ? | |
| key | string | |
| answer | { number } | |
select(sel: Selection) →
The neurons a selection names, as an array of indices (from 0). A
selection combines any of: `types` (exact type names), `typePrefix`,
`pattern` (a Lua pattern over the type name), `classes`, `superclasses`,
`sides`, `excitatory`, `indices`; every given key must hold. `limit`
caps the count. The array is the network's own, memoized on the
selection, so read it rather than writing into it.
| arg | type | description |
|---|
| sel | Selection | The selection. |
examples
local mn9 = brain:select({ types = { "MN9" } })consider(axis: string, keys: { [number]: boolean }?) → void
| arg | type | description |
|---|
| axis | string | |
| keys | { [number]: boolean }? | |
test(i: number) → boolean
| arg | type | description |
|---|
| i | number | |
resolveSelection(self: ?, sel: any) → void
| arg | type | description |
|---|
| self | ? | |
| sel | any | |
effectiveDrive(self: ?, i: number) → number
| arg | type | description |
|---|
| self | ? | |
| i | number | |
stimulate(sel: any, rateHz: number, source: string?) → number
Drive a set of neurons with external spikes at a Poisson rate. A rate
of 0 stops this source driving them. The rate a neuron actually sees is
the largest any source asked for, so the fly's own senses and the bench's
experiments can drive the same neurons without taking each other's drive
away. Stimulation persists across `reset`.
passes `"fly"`, the bench `"bench"`, a protocol `"protocol"`; a call that
names none is recorded as `"code"`.
| arg | type | description |
|---|
| sel | any | A selection, or an array of indices. |
| rateHz | number | Spikes per second per neuron. |
| source | string? | Who is driving, for `drives` and `clearStimulation`. The fly |
examples
brain:stimulate({ types = { "claw_tpGRN" } }, 100, "bench")clearStimulation(source: string?) → number
Stop one source's external drive, or every source's.
| arg | type | description |
|---|
| source | string? | The source to clear. Omitted, every drive goes. |
examples
brain:clearStimulation("bench")drives( ) →
Who is driving the network right now, one row per source.
neurons that source drives and `hz` the highest rate it asked for.
examples
for _, d in ipairs(brain:drives()) do print(d.source, d.count, d.hz) end
silence(sel: any, silenced: boolean) → number
Silence a set of neurons (an ablation: they never spike) or restore them.
| arg | type | description |
|---|
| sel | any | A selection, or an array of indices. |
| silenced | boolean | True to silence, false to restore. |
examples
brain:silence({ types = { "DNp01" } }, true)slotOf(w: number, s: number) → number
| arg | type | description |
|---|
| w | number | |
| s | number | |
withSlot(w: number, s: number, id: number) → number
| arg | type | description |
|---|
| w | number | |
| s | number | |
| id | number | |
clearGroupIndices(self: ?, id: number, indices: { number }) → void
Take a group off the neurons it was placed on. The group's own index list
is the record of where that is, so a re-watch costs the size of the group
rather than the size of the network.
| arg | type | description |
|---|
| self | ? | |
| id | number | |
| indices | { number } | |
watch(name: string, sel: any) → number
Read a set of neurons out under a name: from then on `rates()`
reports their mean firing rate. A neuron reads out under at most three
groups at once, and a watch that would need a fourth slot on any neuron
is refused, naming the neuron and the groups it already carries, so a
new readout can never take one something else depends on. Watching a name
again with the same selection is a no-op; with a different one it
replaces the selection. Up to 255 groups.
| arg | type | description |
|---|
| name | string | The group's name. |
| sel | any | A selection, or an array of indices. |
examples
brain:watch("MN9", { types = { "MN9" } })unwatch(name: string) → boolean
Stop reading a group out. Its name, its id and the readout slots it
held on every neuron are released.
| arg | type | description |
|---|
| name | string | The group's name. |
examples
brain:unwatch("MN9")rates( ) →
The mean firing rate of every watched group, in spikes per second
per neuron, over the steps between the last two counter readbacks.
examples
for name, hz in pairs(brain:rates()) do print(name, hz) end
activity( ) →
The mean firing rate over the whole network, Hz per neuron, and the
spikes the list could not hold (which lose their propagation).
examples
print(brain:activity().hz)
group(name: string) →
The indices a watched group holds.
| arg | type | description |
|---|
| name | string | The group's name. |
examples
brain:group("MN9")classRates(limit: number?) →
The cell classes with the most activity right now, by the mean of the
60 ms spike trace over every neuron of the class. The trace is set to 1
at a spike and decays, so this is activity, not a rate in Hz, and a class
firing faster than the trace decays reads above 1. Asking for it arms the per-neuron
readback that answers it, so the first call after a quiet period returns
the previous sweep and the next one is current.
| arg | type | description |
|---|
| limit | number? | How many classes to return, most active first. Default 10. |
examples
for _, c in ipairs(brain:classRates(5)) do print(c.class, c.activity) end
groupActivity(name: string) →
The activity of each neuron of a watched group, in the order
`group(name)` lists them, from the same 60 ms trace `classRates` reads.
A group of more than 32 neurons answers nil: this is what a raster draws
a row from, and a raster of more rows than that is a bar.
| arg | type | description |
|---|
| name | string | The group's name. |
examples
local rows = brain:groupActivity("MN9")readActivity( ) →
Start an asynchronous read of every neuron's activity lane: the
trace, the normalised membrane and the spike flag. Poll the handle's
`:state()`; `:result()` is 16 floats per neuron, the first three being
those values.
examples
local rb = brain:readActivity()
sliceOf(src: buffer, byteOffset: number, byteCount: number) → buffer
One instanced draw of every neuron that has a soma. The population is
built in two halves. `buildCloud` fills the transform and colour buffers,
a slice of neurons per frame so no frame carries the whole pass, and
`setCloudDrawn` registers or drops the draw without touching those
buffers. That split is what lets the component put the registration down
while nothing is looking at the brain: a registration costs its whole
transform run on the CPU every frame whether or not the cull draws any of
it, and 164,587 of those is most of the frame.
| arg | type | description |
|---|
| src | buffer | |
| byteOffset | number | |
| byteCount | number | |
writeSlot(xf: buffer, slot: number, x: number, y: number, z: number, r: number) → void
Write the transform of one drawn slot. The dataset's y grows ventrally
(image rows), the world's y grows up.
| arg | type | description |
|---|
| xf | buffer | |
| slot | number | |
| x | number | |
| y | number | |
| z | number | |
| r | number | |
scanCloud(self: ?, c: ?) → void
| arg | type | description |
|---|
| self | ? | |
| c | ? | |
uploadCloud(self: ?, c: ?) → void
| arg | type | description |
|---|
| self | ? | |
| c | ? | |
buildCloud(opts: { [string]: any }?, onReady: ((any) →
Build the neuron cloud's buffers: one instance per neuron that has a
soma, placed by the connectome's own coordinates. `origin` places the
brain's centre in the world, `scale` is world units per micrometre,
`radius` the octahedron's radius in world units. The fill is spread over
frames from a task, so no frame carries the whole pass; `built` on the
returned record says when it is done. Nothing is drawn until
`setCloudDrawn(true)`.
| arg | type | description |
|---|
| opts | { [string]: any }? | `{ origin?, scale?, radius?, material? }`. |
| onReady | ((any | Called with the cloud record once the buffers are filled. |
examples
brain:buildCloud({ origin = { 0, 5.5, -9 }, scale = 0.01, radius = 0.012 })setCloudDrawn(on: boolean) → boolean
Put the cloud's draw up or take it down, keeping every buffer. This
is the call that costs or saves the frame: a registered population has
its whole transform run recomposed on the CPU every frame, whether or not
the cull draws any of it, so a cloud nothing is looking at is worth
dropping and putting straight back.
| arg | type | description |
|---|
| on | boolean | Register the draw, or drop it. |
examples
brain:setCloudDrawn(false)
cloudDrawn( ) → boolean
Whether the cloud's draw is registered right now.
examples
print(brain:cloudDrawn())
cloudBounds( ) →
The cloud's world-space bounding sphere, which is what a visibility
test needs.
examples
local x, y, z, r = brain:cloudBounds()
showCloud(opts: { [string]: any }?) →
Build the cloud and draw it. The buffers fill over the next frames,
and the draw goes up when they are ready.
| arg | type | description |
|---|
| opts | { [string]: any }? | `{ origin?, scale?, radius?, material? }`. |
examples
brain:showCloud({ origin = { 0, 5.5, -9 }, scale = 0.01, radius = 0.012 })setCloudOrigin(origin: { number }) →
Move the cloud to a new origin without rebuilding it: each slot's
transform is rewritten from the neuron record it already draws, a slice a
frame, and pushed to the buffer the draw is already reading. Every slice
writes the position the CURRENT origin puts that neuron at, so a move
that arrives while another is still running replaces it and the cloud
still lands whole at the last origin asked for.
| arg | type | description |
|---|
| origin | { number } | `{ x, y, z }` in world units. |
examples
brain:setCloudOrigin({ 0, 7.2, 9 })worldPosition(i: number) →
Where a neuron sits in the world while a cloud exists, whether or not
its draw is registered.
| arg | type | description |
|---|
| i | number | The neuron index. |
examples
local x, y, z = brain:worldPosition(i)
neuronAt(screenX: number, screenY: number, radiusPx: number?) → number
The drawn neuron nearest a point on screen, for a click in the cloud.
Every drawn slot is projected with the active camera's own
view-projection and the nearest one inside `radiusPx` wins, the nearer of
two overlapping neurons taking it.
| arg | type | description |
|---|
| screenX | number | Pixels from the left of the viewport. |
| screenY | number | Pixels from the top. |
| radiusPx | number? | How far from the point to look. Default 12. |
examples
local i = brain:neuronAt(405, 540)
hideCloud( ) →
Take the neuron cloud down and release everything it holds.
examples
brain:hideCloud()
destroy( ) →
Release every GPU buffer the network holds. The handle is dead after this.
describe( ) →
A one-table summary for logs and inspectors.
examples
print(brain:describe().hz)