Supported recording formats
axonforge reads generic layouts, not acquisition-system native files. The trade is deliberate: six readers that are fully specified here, instead of thirty that half-work and drift out of date.
| Layout | Suffix | Read | Write | Metadata from | Channels tested | Extra needed |
|---|---|---|---|---|---|---|
| Interleaved integer | .dat .bin | yes | yes | sidecar or arguments | 128 | — |
| Interleaved float | .bin .f32 | yes | yes | sidecar or arguments | 64 | — |
| HDF5 dataset | .h5 .hdf5 | yes | no | file attributes | 64 | hdf5 |
| NumPy array | .npy | yes | yes | arguments only | 64 | — |
| axonforge container | .afz | yes | yes | embedded | 128 | afz |
| Spike times only | .csv | yes | yes | header row | n/a | — |
Interleaved binary#
The workhorse. Samples are stored channel-by-channel within each time step: c0 t0, c1 t0, … c31 t0, c0 t1, and so on. Nothing in the file says how many channels there are, so either a sidecar or three keyword arguments must supply it.
rec = af.io.read("raw.dat", fs=30_000, n_channels=32, dtype="int16",
gain_uv_per_bit=0.195, offset=0)
Getting n_channels wrong is the single most common mistake with this layout,
and it does not raise: the data is reinterpreted as a different number of channels and still
looks like a recording. The reader guards against the obvious case — if the file length is
not an exact multiple of n_channels × itemsize, it refuses — but 32 against 16
divides evenly and will pass. The symptom is a spectrum with a comb of peaks, and
af.io.sanity(rec) flags it.
The sidecar#
A JSON file with the same stem as the recording. If present it is read automatically and its values are used as defaults; anything you pass explicitly wins over it.
{
"fs": 30000,
"n_channels": 32,
"dtype": "int16",
"gain_uv_per_bit": 0.195,
"offset": 0,
"channel_ids": ["sh0-0", "sh0-1", "sh0-2", "sh0-3"],
"channel_groups": [[0,1,2,3], [4,5,6,7], [8,9,10,11]],
"geometry_um": [[0,0], [0,25], [0,50], [0,75]],
"bad_channels": [11, 26],
"notes": "shank 0, 850 um depth"
}
Only fs, n_channels and dtype are required. The
geometry is used by nothing in the core at present — grouping is what drives referencing and
sorting — but it is carried through into the container so that it survives the analysis.
HDF5#
rec = af.io.read("session.h5", dataset="/acquisition/raw", fs_attr="sample_rate")
The dataset path is explicit because no two HDF5 conventions agree on it. Both orientations
are accepted and detected by shape, with a tie broken in favour of channels-first; if your
recording genuinely has more channels than samples, pass
orientation="samples_first". Reading is lazy and chunked, so a dataset larger
than memory is fine as long as the chunking in the file is not pathological.
The .afz container#
What units.save() writes. It is an ordinary zip-like archive with a
zstd-compressed payload and a documented manifest, so it can be opened without axonforge if
it ever comes to that. It holds the spike table, the aligned waveforms, the features, the
cluster assignments and — the part that matters six months later — every parameter used to
produce them.
units = af.io.read("session12.afz")
print(units.provenance["detect"])
# {'k': 4.5, 'polarity': 'neg', 'dead_time_ms': 1.0, 'sigma': 'median/0.6745'}
print(units.provenance["axonforge_version"]) # 0.7.2
It does not store the raw signal. A ten-minute, 32-channel session produces an 18 MB container against 1.15 GB of raw data, and keeping the two together is your archiving policy, not ours.
Spike times as CSV#
For data that was detected elsewhere. Two columns are required,
time_s and unit; channel and amplitude_uv
are read if present and ignored if not.
st = af.io.read_spikes("sorted_elsewhere.csv")
print(af.stats.cv2(st)) # interval statistics work as usual
af.plot.raster(st, t0=0, t1=5)
Without waveforms there is nothing to align, no features to compute and no cluster shape
to measure, so sorting and three of the four quality metrics are unavailable. Interval
violations can still be computed, because that one needs only times. Calling
af.sort.features on a table read from CSV raises rather than inventing
something.
Not supported#
- Native formats of commercial acquisition systems, in any version. Export or convert to interleaved binary first; most systems do this in one step, and the conversion is where the gain and sample rate get recorded into a sidecar.
- Compressed audio-style containers, 24-bit packed integers, and files with per-block headers interleaved in the data.
- Multi-file sessions split by time. Concatenate first and keep the boundary times, or
process each file separately and merge the spike tables with
af.io.concat_spikes(tables, offsets_s=...). - Writing HDF5. We read it and will not add a writer; the container format exists precisely so there is one blessed way to write results.