Tutorial, part 3: many sessions
Three parts: 1 · File to raster · 2 · Interval statistics · 3 · Many sessions
One session is a tutorial; a study is a folder of them. This part runs the same pipeline over several recordings without touching the parameters, merges the results onto one clock, and makes the whole thing reproducible so the numbers survive a reviewer re-running them.
The command line, in a loop#
The library and the command do the same work; for a folder, the command is less to get wrong. The flags are exactly the keyword arguments from the CLI reference, so a batch script is self-documenting:
for f in sessions/*.dat; do
axonforge detect "$f" --k 4.5 --bandpass 300:6000 \
--dead-time 1.0 --jobs 4 --out "${f%.dat}.afz"
axonforge sort "${f%.dat}.afz" --max-k 8 --components 3 --criterion bic
done
Keeping --k, the band and the model order identical across sessions is the whole
point: a difference in units between two recordings should mean a difference in the tissue, not
a difference in the threshold. --jobs 4 fans the channel groups across four workers;
on the 32-channel session that cut the detection step from 38 s to about 12, and it is the only
flag that changes the timing rather than the result.
| Session | Duration | Events | Units | Wall time |
|---|---|---|---|---|
session12 | 600 s | 41 882 | 6 | 1 m 46 s |
session13 | 600 s | 38 410 | 5 | 1 m 39 s |
session14 | 900 s | 71 205 | 7 | 2 m 58 s |
Merging onto one clock#
When sessions are consecutive recordings of the same probe, you often want one spike table on
a shared timeline. io.concat_spikes shifts each table by its start offset and
concatenates. What it deliberately does not do is decide that u03 in one file is the
same cell as u03 in the next: matching units across recordings is a sorting judgement
about drift and waveform identity, not a bookkeeping step, and pretending otherwise is how a
drifting unit gets counted as two.
import axonforge as af
tables = [af.io.read("session12.afz").spikes,
af.io.read("session13.afz").spikes,
af.io.read("session14.afz").spikes]
offsets = [0.0, 600.0, 1200.0] # seconds, start of each
merged = af.io.concat_spikes(tables, offsets)
print(merged.n, merged.span_s) # 151497 2100.0
Reproducibility#
Two things make a run repeatable. The clustering takes a seed, and with it fixed
the Gaussian-mixture restarts start from the same initialisations every time, so the unit labels
and counts do not move between runs on the same machine. And the container records the parameters
and the curation operations in its provenance, so a colleague opening your .afz can
read exactly which threshold and which merges produced it.
units = af.io.read("session12.afz")
print(units.provenance["detect"]["k"]) # 4.5
print(units.provenance["sort"]["seed"]) # 0
print(units.provenance["curation"]) # [('drop', 'u07')]
The seed pins the clustering, not the hardware: a different BLAS or a different NumPy build can change the filtered samples in the last bit, and on a borderline spike that can move one event across the threshold. The counts are stable; do not quote a single-event difference between two machines as a finding.