Skill Tracer
Skills › Scientific Agent Skills › neuropixels-analysis › AI_CURATION.md

AI_CURATION.md

166 lines · skills/neuropixels-analysis/references/AI_CURATION.md at 5f2bc03

Open on GitHub
1# AI-Assisted Curation Reference
2
3Use vision-language models to analyze spike-sorting visualizations for borderline units,
4complementing quantitative quality metrics. Reviewed 2026-10-01; HTTP payloads
5are documentation-verified only. No paid API calls or scientific classification
6validation were performed. Visual suggestions must not auto-accept units.
7
8```
9Traditional: Metrics → Threshold → Labels
10AI-Assisted: Metrics → Render plots → Vision suggestion → Human review → Labels
11```
13> **Credential safety:** never hardcode API keys in analysis scripts — they end up in
14> version control and logs. Read them from environment variables that you set in your shell
15> (e.g. `export ANTHROPIC_API_KEY=...`). All examples below follow this pattern.

AI-Assisted Curation Reference

Use vision-language models to analyze spike-sorting visualizations for borderline units, complementing quantitative quality metrics. Reviewed 2026-10-01; HTTP payloads are documentation-verified only. No paid API calls or scientific classification validation were performed. Visual suggestions must not auto-accept units.

Traditional:  Metrics → Threshold → Labels
AI-Assisted:  Metrics → Render plots → Vision suggestion → Human review → Labels
Credential safety: never hardcode API keys in analysis scripts — they end up in version control and logs. Read them from environment variables that you set in your shell (e.g. export ANTHROPIC_API_KEY=...). All examples below follow this pattern.
Raw lines 1 to 16
17## Agent integration (no API key needed)
19When you run this skill inside an agent (Cursor, Claude Code, etc.), the agent can inspect
20images directly. Generate a unit summary figure and ask the agent to assess it:
22```python
23import spikeinterface.widgets as sw
24import matplotlib.pyplot as plt
26sw.plot_unit_summary(analyzer, unit_id=0)
27plt.savefig("unit_0_summary.png", dpi=150, bbox_inches="tight")
28# Then ask the agent: "Is unit 0 a well-isolated single unit, MUA, or noise? Consider
29# waveform consistency, the refractory gap in the autocorrelogram, and amplitude stability."
30```
32The agent can assess waveform shape/consistency, refractory-period violations, amplitude
33stability over time, and overall isolation quality.

Agent integration (no API key needed)

When you run this skill inside an agent (Cursor, Claude Code, etc.), the agent can inspect images directly. Generate a unit summary figure and ask the agent to assess it:

import spikeinterface.widgets as sw
import matplotlib.pyplot as plt

sw.plot_unit_summary(analyzer, unit_id=0)
plt.savefig("unit_0_summary.png", dpi=150, bbox_inches="tight")
# Then ask the agent: "Is unit 0 a well-isolated single unit, MUA, or noise? Consider
# waveform consistency, the refractory gap in the autocorrelogram, and amplitude stability."

The agent can assess waveform shape/consistency, refractory-period violations, amplitude stability over time, and overall isolation quality.

Raw lines 17 to 34
35## Programmatic API access

Programmatic API access

Raw lines 35 to 36
37### Render a unit summary image
39```python
40import io, base64
41import matplotlib.pyplot as plt
42import spikeinterface.widgets as sw
44def render_unit_image(analyzer, unit_id) -> str:
45 """Return a base64-encoded PNG summary for one unit."""
46 fig = plt.figure(figsize=(12, 8))
47 sw.plot_unit_summary(analyzer, unit_id=unit_id, figure=fig)
48 buf = io.BytesIO()
49 fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
50 plt.close(fig)
51 return base64.b64encode(buf.getvalue()).decode("utf-8")
52```

Render a unit summary image

import io, base64
import matplotlib.pyplot as plt
import spikeinterface.widgets as sw

def render_unit_image(analyzer, unit_id) -> str:
    """Return a base64-encoded PNG summary for one unit."""
    fig = plt.figure(figsize=(12, 8))
    sw.plot_unit_summary(analyzer, unit_id=unit_id, figure=fig)
    buf = io.BytesIO()
    fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
    plt.close(fig)
    return base64.b64encode(buf.getvalue()).decode("utf-8")
Raw lines 37 to 53
54### Anthropic (Claude) example
56```python
57import os
58from anthropic import Anthropic
60client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"]) # set in shell, not in code
62PROMPT = (
63 "You are an expert electrophysiologist curating a spike-sorted unit. "
64 "Based on the waveform, template, autocorrelogram, amplitude-over-time, and ISI "
65 "histogram, classify this unit as exactly one of: good (well-isolated single unit), "
66 "mua (multi-unit), or noise. Reply with the label and a one-sentence justification."
69def analyze_unit_visually(analyzer, unit_id, model=None):
70 model = model or os.environ["ANTHROPIC_VISION_MODEL"]
71 img_b64 = render_unit_image(analyzer, unit_id)
72 msg = client.messages.create(
73 model=model,
74 max_tokens=300,
75 messages=[{
76 "role": "user",
77 "content": [
78 {"type": "image",
79 "source": {"type": "base64", "media_type": "image/png", "data": img_b64}},
80 {"type": "text", "text": PROMPT},
81 ],
82 }],
83 )
84 return "\n".join(block.text for block in msg.content if block.type == "text")
86print(analyze_unit_visually(analyzer, unit_id=0))
87```

Anthropic (Claude) example

import os
from anthropic import Anthropic

client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])  # set in shell, not in code

PROMPT = (
    "You are an expert electrophysiologist curating a spike-sorted unit. "
    "Based on the waveform, template, autocorrelogram, amplitude-over-time, and ISI "
    "histogram, classify this unit as exactly one of: good (well-isolated single unit), "
    "mua (multi-unit), or noise. Reply with the label and a one-sentence justification."
)

def analyze_unit_visually(analyzer, unit_id, model=None):
    model = model or os.environ["ANTHROPIC_VISION_MODEL"]
    img_b64 = render_unit_image(analyzer, unit_id)
    msg = client.messages.create(
        model=model,
        max_tokens=300,
        messages=[{
            "role": "user",
            "content": [
                {"type": "image",
                 "source": {"type": "base64", "media_type": "image/png", "data": img_b64}},
                {"type": "text", "text": PROMPT},
            ],
        }],
    )
    return "\n".join(block.text for block in msg.content if block.type == "text")

print(analyze_unit_visually(analyzer, unit_id=0))
Raw lines 54 to 88
89### OpenAI example
91```python
92import os
93from openai import OpenAI
95client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
97def analyze_unit_visually_openai(analyzer, unit_id, model=None):
98 model = model or os.environ["OPENAI_VISION_MODEL"]
99 img_b64 = render_unit_image(analyzer, unit_id)
100 resp = client.responses.create(
101 model=model,
102 input=[{
103 "role": "user",
104 "content": [
105 {"type": "input_text", "text": PROMPT},
106 {"type": "input_image", "image_url": f"data:image/png;base64,{img_b64}"},
107 ],
108 }],
109 )
110 return resp.output_text
111```
113> Model names change frequently. Use your provider's current vision-capable model
114> (e.g. a current Claude or GPT multimodal model) rather than an old preview ID.

OpenAI example

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

def analyze_unit_visually_openai(analyzer, unit_id, model=None):
    model = model or os.environ["OPENAI_VISION_MODEL"]
    img_b64 = render_unit_image(analyzer, unit_id)
    resp = client.responses.create(
        model=model,
        input=[{
            "role": "user",
            "content": [
                {"type": "input_text", "text": PROMPT},
                {"type": "input_image", "image_url": f"data:image/png;base64,{img_b64}"},
            ],
        }],
    )
    return resp.output_text
Model names change frequently. Use your provider's current vision-capable model (e.g. a current Claude or GPT multimodal model) rather than an old preview ID.
Raw lines 89 to 115
116## Cost optimization: only call the model on uncertain units
118```python
119uncertain = metrics.query(
120 "snr > 2 and snr < 8 and isi_violations_ratio > 0.001 and isi_violations_ratio < 0.1"
121).index.tolist()
123ai_labels = {}
124for uid in uncertain:
125 ai_labels[uid] = analyze_unit_visually(analyzer, uid)
126```

Cost optimization: only call the model on uncertain units

uncertain = metrics.query(
    "snr > 2 and snr < 8 and isi_violations_ratio > 0.001 and isi_violations_ratio < 0.1"
).index.tolist()

ai_labels = {}
for uid in uncertain:
    ai_labels[uid] = analyze_unit_visually(analyzer, uid)
Raw lines 116 to 127
128## Keep advisory responses separate
130Store unit ID, image hash, model ID, prompt, returned text and reviewer decision.
131Do not use arbitrary model prose as a curation label or interpret its stated
132confidence as a calibrated isolation probability. AI cannot establish ground truth
133from a summary image; inspect omitted channels, epochs and neighboring units.

Keep advisory responses separate

Store unit ID, image hash, model ID, prompt, returned text and reviewer decision. Do not use arbitrary model prose as a curation label or interpret its stated confidence as a calibrated isolation probability. AI cannot establish ground truth from a summary image; inspect omitted channels, epochs and neighboring units.

Raw lines 128 to 134
135## What each panel tells you
137| Panel | Content | What to look for |
138|-------|---------|------------------|
139| Waveforms | Individual spike waveforms | Consistency, shape |
140| Template | Mean ± std | Clean negative peak, physiological shape |
141| Autocorrelogram | Spike timing | Gap at 0 ms (refractory period) |
142| Amplitudes | Amplitude over time | Stability, no drift |
143| ISI histogram | Inter-spike intervals | Refractory gap < ~1.5 ms |

What each panel tells you

PanelContentWhat to look for
WaveformsIndividual spike waveformsConsistency, shape
TemplateMean ± stdClean negative peak, physiological shape
AutocorrelogramSpike timingGap at 0 ms (refractory period)
AmplitudesAmplitude over timeStability, no drift
ISI histogramInter-spike intervalsRefractory gap < ~1.5 ms
Raw lines 135 to 144
145## Best Practices
1471. **Use AI for uncertain cases** — don't spend API calls on obvious good/noise units.
1482. **Combine with metrics and model-based curation** — AI supplements, not replaces,
149 quantitative measures (see [AUTOMATED_CURATION.md](AUTOMATED_CURATION.md)).
1503. **Keep a human in the loop** for important analyses.
1514. **Record reasoning** for each decision for reproducibility.
1525. **Never commit credentials** — keep keys in environment variables.

Best Practices

  1. Use AI for uncertain cases — don't spend API calls on obvious good/noise units.
  2. Combine with metrics and model-based curation — AI supplements, not replaces, quantitative measures (see AUTOMATED_CURATION.md).
  3. Keep a human in the loop for important analyses.
  4. Record reasoning for each decision for reproducibility.
  5. Never commit credentials — keep keys in environment variables.
Raw lines 145 to 153
154## References
156- [Anthropic Vision API](https://platform.claude.com/docs/en/build-with-claude/vision)
157- [OpenAI Vision/Images](https://developers.openai.com/api/docs/guides/images-vision)
158- [SpikeInterface model-based curation](https://spikeinterface.readthedocs.io/en/stable/tutorials/curation/plot_1_automated_curation.html)
159- [SpikeAgent](https://github.com/SpikeAgent/SpikeAgent) — AI-powered spike-sorting assistant
161Anthropic uses authenticated `POST /v1/messages` with image source `{type: base64,
162media_type: image/png, data: ...}` plus text content; the SDK handles its API key
163and version header. OpenAI uses bearer-authenticated `POST /v1/responses` with
164`input_image.image_url` containing a PNG data URL; read `output_text`. These are
165single-response calls without pagination. Select a provider-supported vision model
166explicitly through the environment; model access and limits depend on the account.

References

Anthropic uses authenticated POST /v1/messages with image source {type: base64, media_type: image/png, data: ...} plus text content; the SDK handles its API key and version header. OpenAI uses bearer-authenticated POST /v1/responses with input_image.image_url containing a PNG data URL; read output_text. These are single-response calls without pagination. Select a provider-supported vision model explicitly through the environment; model access and limits depend on the account.

Raw lines 154 to 166