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.

Reader support in 0.7.2. «Channels tested» is the largest count exercised by the test suite, not a hard limit.
LayoutSuffixReadWriteMetadata fromChannels testedExtra needed
Interleaved integer.dat .binyesyessidecar or arguments128—
Interleaved float.bin .f32yesyessidecar or arguments64—
HDF5 dataset.h5 .hdf5yesnofile attributes64hdf5
NumPy array.npyyesyesarguments only64—
axonforge container.afzyesyesembedded128afz
Spike times only.csvyesyesheader rown/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)
What CSV input cannot do

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#