Skip to content

radiens-qc

PyPI Python Version

radiens-qc turns a Radiens recording into an answerable question: is this data good enough to use?

It computes standardized signal quality metrics, grades electrode health, records where the detector fired, and exports self-contained report packages for instant visualization.

Installation

pip install radiens-qc

or with uv:

uv add radiens-qc

Requires Python 3.12+ and access to a Radiens server to compute reports. Saved reports can be reloaded, analyzed, and plotted completely offline.

Quick start

Compute & save a report

from pathlib import Path

from radiens_core import VidereClient
from radiens_qc import compute_report, save_report

vc = VidereClient()
report = compute_report(vc, Path("/data/rec01.xdat"), source="exp/day3")

print(report.median("snr"))         # 6.2
print(report.integrity_counts())    # {"ok": 30, "noisy": 1, "dead": 1}
print(report.event_counts())        # {"present": 24, "absent": 8}

save_report(report, Path("reports"))  # -> reports/rec01/

Note: compute_report accepts a local Path or an existing DatasetMetadata object.

Generate plots

FIGURES names every figure the package draws. To render them all, say so once -- only what the selected figures need is fetched:

from pathlib import Path
from radiens_qc import FIGURES, fetch_figure_sources, render, save_figures

sources = fetch_figure_sources(
    vc, dataset, FIGURES, report=report, crossings_spikes_id=crossings_id, units_spikes_id=units_id
)
save_figures(render(FIGURES, sources), Path("figures"))   # figures/<level>/<name>.png

The events and units levels read spike sorts you run, so you decide which sort and whether an earlier one will do. Crossings need a detect-only scrape that detects every site independently, run after the report set the threshold; units need a clustered sort:

from radiens_core.models import SpikeSorterLaunchParams, SpikeSorterSubCommand
from radiens_qc import PER_CHANNEL_NEIGHBORS

crossings_id = vc.spike_sort(
    SpikeSorterLaunchParams(
        dsource_id=dataset.id,
        sub_cmd=SpikeSorterSubCommand.DETECT_ONLY,
        max_num_site_neighbors=PER_CHANNEL_NEIGHBORS,
    )
)
units_id = vc.spike_sort(SpikeSorterLaunchParams(dsource_id=dataset.id, neighbor_radius_um=150.0))

Each reader checks the fileset it is given and raises if it cannot support the figure: crossings detected with neighbors or at another threshold, or units from a detect-only scrape. Leave an ID out and that level is skipped.

select narrows that by level or by name, and render returns {spec: Figure} if you would rather place the figures yourself:

from radiens_qc import FigureSources, select

for spec, fig in render(select(levels=["channel"]), FigureSources(report=report)).items():
    print(spec.name, spec.caption)

A report read off disk supports the channel and recording levels; the raw-signal, events and units levels describe the recording rather than the measurement, so they need a live dataset. render skips a figure whose source it was not given, which is what makes a saved report directory a valid thing to draw from.

Each figure is also a plain function returning one matplotlib Figure that it owns entirely, so you choose the size and where it goes:

from radiens_qc import plot_channel_profiles, plot_site_maps, plot_stability

plot_site_maps(report)          # SNR, noise, event rate, amplitude on the probe
plot_channel_profiles(report)   # the same four, one bar row per site
plot_stability(report)          # each metric over time against its own start

Every one takes the object it draws: plot_raw_traces and plot_raw_spectra take what fetch_raw_signal returns, plot_event_raster what fetch_events returns, and plot_unit_footprints, plot_unit_yield, plot_unit_raster and plot_unit_inventory what fetch_units returns.

Unit amplitude is measured from spike waveforms, which are the largest payload here and so are fetched only when you ask for them:

from radiens_qc import fetch_unit_waveforms, fetch_units, plot_unit_yield

units = fetch_units(vc, dataset, units_id)
units = units.with_waveforms(fetch_unit_waveforms(vc, dataset, units))
plot_unit_yield(units)

Given units without them, the figures that report amplitude say so on that panel instead of drawing it; the rest are unaffected. fetch_figure_sources handles this for you, fetching waveforms when a selected figure reports amplitude.

Saved report structure

save_report(report, out_dir) creates <out_dir>/<base_name>/. It writes data only -- figures are drawn from a report, not stored beside one:

File Description
meta.json Provenance, thresholds, channel counts, and whole-recording readings
channels.csv Channel geometry, metric values, integrity tier, and event state
matrices.npz Full per-metric matrices, enough to redraw every figure without a server

License

See LICENSE.