Library guide
Everything in DotFly.Core / DotFly.Engine, by task, with the code you would actually write.
The API reference has every member; this page is the tour.
1. Open a checkpoint
using DotFly; // Simulation, ports, RealtimeDriver, units (Ms(), Seconds())
using DotFly.Core.Graph; // Brain, NeuronSet, Side, NeuronInfo, EdgeSpan
using DotFly.Core.Models; // Neurotransmitter, NeuronModel
using Brain brain = Brain.Open(".data/malecns-v1.0/malecns-v1.0-superclass.dfb");
Console.WriteLine($"{brain.Provenance.Name}: {brain.NeuronCount:N0} neurons, {brain.EdgeCount:N0} edges");
Console.WriteLine($"w_syn {brain.Model.SynapseWeightMv} mV, sign policy {brain.Provenance.SignPolicy}");
Brain is a memory-mapped, immutable view of the .dfb file: opening is instant, nothing is
copied, and several Simulations can share one Brain. The provenance block records the source
files (SHA-256), the filter, the sign policy and the dropped-edge counts, so a result can always be
tied to the data it came from.
2. Select neurons
A NeuronSet is an immutable, sorted list of neuron indices with a name. Everything — inputs,
outputs, silencing, exploration — takes one.
NeuronSet lc4Left = brain.Query(type: "LC4", side: Side.Left, name: "LC4_L");
NeuronSet hs = brain.Query(type: "HSE") | brain.Query(type: "HSN") | brain.Query(type: "HSS"); // union
NeuronSet lb3 = brain.Query(type: "LB3*"); // trailing/leading * = glob
NeuronSet kenyon = brain.Query(cls: "Kenyon_Cell"); // by class
NeuronSet gaba = brain.Query(superclass: "descending_neuron", nt: Neurotransmitter.Gaba);
NeuronSet wind = brain.ByBodyIds("wind_L", [11960UL, 13072UL]); // exact 64-bit IDs
NeuronSet one = brain.ByBodyId(720575940660219265); // FlyWire MN9
NeuronSet notKc = hs - kenyon; // difference
NeuronSet both = lc4Left & brain.Query(nt: Neurotransmitter.Acetylcholine); // intersection
NeuronInfo info = brain[lc4Left[0]]; // type, class, superclass, instance, NT, side, soma (voxels)
Console.WriteLine($"{info.BodyId} {info.Type} {info.Side} soma {info.Soma}");
Body IDs are ulong and never pass through a floating-point type. Named sets stored in the
checkpoint (from the builder) are in brain.Sets.
Following the wiring
NeuronSet targets = brain.Downstream(lc4Left, hops: 1, minWeight: 5); // who LC4_L drives, ≥ 5 contacts
NeuronSet drivers = brain.Upstream(one, hops: 2, minWeight: 5); // who drives MN9, two hops back
EdgeSpan edges = brain.OutEdges(lc4Left[0]); // raw CSR row: targets + signed weights
for (int k = 0; k < edges.Length; k++)
{
Console.WriteLine($"→ {brain[edges.Targets[k]].Type} {edges.Weights[k]:+#;-#} contacts");
}
This is how the demo's readouts were found: stimulate a population with the CLI, see who fires,
confirm the path with Upstream.
3. Create a simulation
using Simulation sim = brain.CreateSimulation(new SimulationOptions
{
Seed = 1, // same checkpoint + options + seed ⇒ identical spikes, at any thread count
Threads = 0, // 0 = one per physical core
SynapticGain = 1.0, // the model's one free parameter; ≠ 1.0 is a *calibration* — say so
Backend = Backend.Cpu, // float32 SIMD; Backend.Reference = float64, matches Brian2 to rounding
});
The model constants come from the checkpoint (NeuronModel.Shiu2024: exact integrator, 18-step
delay, 22-step refractoriness, Poisson weight 68.75 mV) — you don't set them per run.
4. Inputs
InputPort odor = sim.Input(brain.Query(type: "ORN_DM1", side: Side.Left), InputKind.PoissonToV);
odor.Fill(60f); // 60 Hz Poisson drive on every neuron of the set
odor.Set1(position: 3, value: 120f); // one neuron of the set
odor.Write(rates); // a whole span, one value per neuron of the set
InputPort tonic = sim.Input(brain.Query(type: "EPG"), InputKind.Current);
tonic.Fill(0.05f); // +0.05 mV added to v every step (a tonic drive; not in Shiu's model)
PoissonToV is the Shiu et al. stimulation (Brian2 PoissonInput(target_var='v')): each event
adds 68.75 mV, so every accepted event fires the target, and driven neurons get no refractory
period. Port writes are thread-safe and take effect at the next Advance — a game thread can
write them while the simulation thread runs.
5. Outputs
NeuronSet mn9 = brain.Query(type: "MN9", name: "MN9");
OutputPort rate = sim.Output(mn9, OutputKind.Rate(30.Ms()), publishEvery: 20.Ms()); // Hz per neuron
OutputPort count = sim.Output(mn9, OutputKind.SpikeCount(200.Ms())); // spikes per window
OutputPort v = sim.Output(mn9, OutputKind.Membrane); // v in mV, every publish
OutputFrame f = rate.Snapshot; // lock-free: the latest published frame
float mean = f.Mean(); // over the set
float first = f[0]; // per neuron, in set order
Console.WriteLine($"frame {f.FrameIndex} at {f.NeuralTime}: MN9 {mean:F0} Hz");
rate.Published += (port, frame) => { /* called on the simulation thread, every publishEvery */ };
A Rate window of 30 ms over two neurons has a resolution of ~17 Hz per spike — for decisions,
smooth over several frames (the room demo uses τ = 0.15–0.3 s).
6. Every spike
long total = 0;
sim.OnSpikes += (long step, ReadOnlySpan<int> ids) =>
{
total += ids.Length; // ids are neuron indices; runs on the simulation thread, no allocation
};
7. Running, scheduling, injecting
sim.Run(1.Seconds()); // neural time; blocks until done
sim.Advance(180); // 180 steps of 0.1 ms
sim.ScheduleAt(250.Ms(), s => s.Inputs[0].Fill(0f)); // an action at a neural time
sim.Inject(brain.IndexOf(720575940660219265), at: 300.Ms()); // one Poisson-sized event on one neuron
sim.Inject(mn9, at: 300.Ms()); // on a whole set
Console.WriteLine($"{sim.NeuralTime} neural in {sim.Clock.WallTime} wall = {sim.Clock.RealTimeFactor:F1}×");
Neural time, simulated duration and wall time are reported separately and never conflated.
8. Trials: reset, snapshot, restore
for (int trial = 0; trial < 30; trial++)
{
sim.Reset(seed: 1000 + (ulong)trial); // same inputs, a fresh Poisson stream; ports keep their values
sim.Run(1.Seconds());
}
SimulationSnapshot before = sim.Snapshot(); // full state (v, g, refractoriness, delay ring, RNG)
sim.Run(500.Ms());
sim.Restore(before); // back to the exact same state
Use Reset for trials rather than a new Simulation: constructing one rebuilds the blocked CSR
(~0.2 s for MaleCNS).
9. Silencing and causal switches
sim.Silence(brain.Query(cls: "Kenyon_Cell") | brain.Query(type: "lLN*")); // outgoing weights → 0; they still spike
sim.Unsilence(kenyon);
sim.RecurrentTransmission = false; // no synaptic delivery at all: the network is a pass-through of its inputs
sim.ExternalInput = false; // no input events: the network runs on nothing
Silencing is Shiu et al.'s silence(). The two switches are what the demos' R / E keys flip:
they let an audience see what the wiring contributes versus what the adapters contribute.
10. Real time, in a game loop
using var driver = new RealtimeDriver(sim, new RealtimeOptions
{
Ratio = 1.0, // neural seconds per wall second
MaxLag = 250.Ms(), // beyond this the driver skips ahead and reports it
});
driver.Start(); // its own thread; the game thread never blocks
// each frame:
odor.Fill(concentration * 120f); // thread-safe write
float pn = rate.Snapshot.Mean(); // lock-free read
if (driver.BehindBy > 50.Ms()) { /* show "behind" on the HUD */ }
driver.Pause(); driver.Start(); // Space
Threading contract: Simulation methods run on one thread (the driver's); InputPort.Write/Fill,
OutputPort.Snapshot, the switches and read-only properties are safe from any thread. Inside a
host (Godot), don't pin the caller thread; the workers are pinned to their own cores.
11. Recording and replay
using (Recorder rec = sim.Record("run.dfs")) // spikes, input writes, injections, output frames
{
sim.Run(2.Seconds());
}
Recording r = Recording.Read("run.dfs");
r.WriteSpikesCsv("spikes.csv");
r.WriteFramesCsv("mn9.csv", port: 0);
using Simulation again = brain.CreateSimulation(new SimulationOptions { Seed = sim.Seed });
r.Replay(again); // reproduces the run's spikes exactly
12. Readout adapters
An adapter turns a frame's values into actions and carries its origin — Fixed, Calibrated or Trained — so a demo can label it honestly.
using DotFly.Adapters;
// Hand rules: action 0 = "feed" when feature 0 (MN9 rate) exceeds 15 Hz
var rules = new ThresholdAdapter(featureCount: 1, [new ThresholdAdapter.Rule(Feature: 0, Threshold: 15f)],
AdapterOrigin.Fixed, "MN9 > 15 Hz → feed");
// A trained linear map (weights from samples/DotFly.Sample.TrainReadout), smoothed
IReadoutAdapter linear = new SmoothingAdapter(LinearAdapter.Load("readout.json"), alpha: 0.3f);
// ONNX / ML.NET models (separate packages: DotFly.Adapters.Onnx / .MLNet)
IReadoutAdapter onnx = new OnnxAdapter("readout.onnx");
IReadoutAdapter mlnet = MLNetAdapter.Load(new MLContext(), "readout.zip", featureCount: 9);
Span<float> actions = stackalloc float[rules.ActionCount];
rules.Evaluate(rate.Snapshot, actions);
The graph never learns; only the adapter is trained. Training data come from recordings
(Recording.Outputs) or live frames.
13. Several simulations in one process
int flies = 4, cores = DotFly.Cpu.Fast.CpuBackend.DefaultThreads(), each = Math.Max(1, cores / flies);
var sims = Enumerable.Range(0, flies).Select(i => brain.CreateSimulation(new SimulationOptions
{
Seed = 1 + (ulong)i, Threads = each,
ThreadPinOffset = i * each, ThreadPinTotal = flies * each, // disjoint cores per simulation
})).ToArray();
Without the pin offsets every pool pins its workers from CPU 0 upward and they stack on the same cores.
14. Pitfalls we hit so you don't
- Runaway attractors at gain 1.0: one-sided LC4 input or any ORN input can ignite a loop that
never stops (optic lobe; Kenyon cells + antennal-lobe LNs). Check with
dotfly run --stim-secondsthat your readouts return to baseline; silence the loop or say the gain is calibrated. - Godot builds in Debug and drags referenced projects into Debug — pin the dotFly references
with
SetConfiguration="Configuration=Release"or the kernel is 25× slower. - Tiered JIT: per-step methods must carry
[MethodImpl(MethodImplOptions.AggressiveOptimization)](already the case inside dotFly; relevant if you write your ownIParallelBody). - Readout noise: rates over 30 ms from a handful of cells jump in ~17–33 Hz steps; smooth before thresholding.