Metadata-Version: 2.5
Name: radiens-qc
Version: 0.0.3
Summary: radiens-qc turns a Radiens recording into an answerable question: is this data good enough to use?
Author-email: Aaron Kelley <akelley@neuronexus.com>
License-File: LICENSE
Keywords: electrophysiology,neuroscience,quality-control,radiens,signal-quality
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.12
Requires-Dist: matplotlib>=3.9
Requires-Dist: numpy>=1.26
Requires-Dist: pandas>=2.2
Requires-Dist: pydantic>=2.6
Requires-Dist: radiens-core>=0.1.0
Description-Content-Type: text/markdown

# radiens-qc

[![PyPI](https://img.shields.io/pypi/v/radiens-qc.svg)](https://pypi.org/project/radiens-qc/)
[![Python Version](https://img.shields.io/pypi/pyversions/radiens-qc.svg)](https://pypi.org/project/radiens-qc/)

> **[radiens-qc](https://pypi.org/project/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

```bash
pip install radiens-qc
```

or with [uv](https://github.com/astral-sh/uv):

```bash
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

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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](LICENSE).
