Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Pipeline for Neuropixels extracellular electrophysiology: probe geometry (ProbeInterface), Kilosort sorting via SpikeInterface, quality metrics, unit curation (ISI, firing rate, SNR), post-sort analysis (PSTH, tuning curves, population decoding). Supports Neuropixels 1.0/2.0/Ultra in rodent/primate experiments.
.claude/skills/jaechang-hits-neuropixels-analysis/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 317% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 387% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 456% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 183% | 0% |
| case-15 | ✓→✓ | = Same ✓ | 179% | 0% |
Neuropixels probes record extracellular voltage from 384 (NP 1.0) or 192 (NP 2.0) simultaneously recorded channels at 30 kHz. Analysis follows a canonical pipeline: raw data → spike sorting (Kilosort) → quality curation → unit analysis. SpikeInterface (Python) provides a unified API across 10+ spike sorters, handles data loading from multiple formats (SpikeGLX, OpenEphys, NWB), computes quality metrics, and exports sorted results. ProbeInterface manages probe geometry and channel maps. Post-sort analysis (PSTHs, firing rate, decoding) uses standard Python scientific stack.
.bin) or OpenEphys (.dat) to extract single-unit activityspikeinterface[full], probeinterface, numpy, pandas, matplotlib, scipypip install kilosort); or MATLAB-based Kilosort2/3 (requires MATLAB license).bin (SpikeGLX) or .dat (OpenEphys) recording; probe channel map file (.prb or from ProbeInterface)bashpip install spikeinterface[full] probeinterface kilosort # Optional: Phy for manual curation pip install phy
pythonimport spikeinterface.full as si from pathlib import Path # SpikeGLX recording (most common Neuropixels format) data_dir = Path("/data/recording/session_001") recording = si.read_spikeglx(data_dir, stream_name="imec0.ap") print(f"Probe type: {recording.get_probe().name}") print(f"Channels: {recording.get_num_channels()}") print(f"Sampling rate: {recording.get_sampling_frequency()} Hz") print(f"Duration: {recording.get_total_duration():.1f} s") print(f"Total samples: {recording.get_total_samples()}")
pythonimport spikeinterface.preprocessing as spre # Common median reference (removes common noise across all channels) recording_cmr = spre.common_reference(recording, reference="global", operator="median") # Bandpass filter: 300–6000 Hz for spikes recording_filt = spre.bandpass_filter(recording_cmr, freq_min=300, freq_max=6000) # Remove bad channels automatically recording_clean, removed_ids = spre.remove_bad_channels(recording_filt) print(f"Removed {len(removed_ids)} bad channels: {removed_ids}") print(f"Clean recording: {recording_clean.get_num_channels()} channels")
pythonimport spikeinterface.sorters as ss from pathlib import Path output_dir = Path("./sorting_output") # Kilosort4 (recommended for Neuropixels; requires GPU) sorting = ss.run_sorter( "kilosort4", recording_clean, output_folder=output_dir / "kilosort4", remove_existing_folder=True, verbose=True, # Kilosort4 parameters nblocks=5, # number of drift correction blocks Th_learned=8, # threshold for learned templates do_correction=True, # drift correction ) print(f"Units found: {len(sorting.get_unit_ids())}") print(f"Spike counts (first 5): {[len(sorting.get_unit_spike_train(u, segment_index=0)) for u in sorting.unit_ids[:5]]}")
pythonimport spikeinterface.full as si import spikeinterface.qualitymetrics as sqm # Extract waveforms (snippets around each spike) waveforms = si.extract_waveforms( recording_clean, sorting, folder="./waveforms", ms_before=1.0, # ms before spike peak ms_after=2.0, # ms after spike peak max_spikes_per_unit=1000, overwrite=True, n_jobs=4, ) print(f"Waveform shape per unit: {waveforms.get_waveforms(waveforms.unit_ids[0]).shape}") # (n_spikes, n_samples, n_channels) # Compute quality metrics metrics = sqm.compute_quality_metrics( waveforms, metric_names=["snr", "isi_violation", "firing_rate", "presence_ratio", "amplitude_cutoff", "nearest_neighbor"], ) print(f"\nQuality metrics summary:") print(metrics.describe())
pythonimport pandas as pd # Curation thresholds (Allen Brain Institute defaults) thresholds = { "snr": (5.0, None), # SNR ≥ 5 "isi_violations_ratio": (None, 0.1), # ISI violation ratio ≤ 10% "firing_rate": (0.1, None), # firing rate ≥ 0.1 Hz "presence_ratio": (0.9, None), # present ≥ 90% of recording "amplitude_cutoff": (None, 0.1), # amplitude cutoff ≤ 10% } def apply_thresholds(metrics_df, thresholds): mask = pd.Series(True, index=metrics_df.index) for metric, (low, high) in thresholds.items(): if metric not in metrics_df.columns: continue if low is not None: mask &= metrics_df[metric] >= low if high is not None: mask &= metrics_df[metric] <= high return mask good_units_mask = apply_thresholds(metrics, thresholds) good_unit_ids = metrics[good_units_mask].index.tolist() print(f"Total units: {len(metrics)}") print(f"Good units: {len(good_unit_ids)} ({100*len(good_unit_ids)/len(metrics):.0f}%)") # Filter sorting to good units only sorting_curated = sorting.select_units(good_unit_ids)
pythonimport numpy as np import matplotlib.pyplot as plt def compute_psth(spike_times, event_times, window=(-0.5, 1.0), bin_size=0.01, fs=30000): """Compute peri-stimulus time histogram for one unit.""" bins = np.arange(window[0], window[1] + bin_size, bin_size) spike_times_s = spike_times / fs # samples → seconds counts = np.zeros(len(bins) - 1) for t_event in event_times: rel_times = spike_times_s - t_event in_window = rel_times[(rel_times >= window[0]) & (rel_times < window[1])] counts += np.histogram(in_window, bins=bins)[0] rate = counts / (len(event_times) * bin_size) # convert to Hz return bins[:-1] + bin_size / 2, rate # bin centers, firing rate # Example: visual stimulus events at 1.0, 2.5, 4.0, 5.5 s fs = 30000 event_times_s = np.array([1.0, 2.5, 4.0, 5.5, 7.0, 8.5]) # Plot PSTH for first 4 good units fig, axes = plt.subplots(2, 2, figsize=(10, 6)) for ax, unit_id in zip(axes.flat, good_unit_ids[:4]): spikes = sorting_curated.get_unit_spike_train(unit_id, segment_index=0) times, rate = compute_psth(spikes, event_times_s, fs=fs) ax.bar(times, rate, width=0.01, color="steelblue", alpha=0.8) ax.axvline(0, color="red", lw=1.5, linestyle="--", label="Stimulus") ax.set_xlabel("Time from stimulus (s)") ax.set_ylabel("Firing rate (Hz)") ax.set_title(f"Unit {unit_id}") plt.tight_layout() plt.savefig("psth_grid.pdf", bbox_inches="tight") print("Saved psth_grid.pdf")
pythonimport spikeinterface.exporters as sexp # Export sorted data to Neurodata Without Borders format nwb_path = "./recording_sorted.nwb" sexp.export_to_nwb( sorting_curated, nwb_file_path=nwb_path, overwrite=True, ) print(f"Exported to NWB: {nwb_path}") # Also export waveforms for Phy manual curation phy_dir = "./phy_export" sexp.export_to_phy( waveforms, output_folder=phy_dir, compute_pc_features=True, copy_binary=True, ) print(f"Phy export ready at: {phy_dir}") print("Launch with: phy template-gui phy_export/params.py")
| Parameter | Module/Function | Default | Range / Options | Effect | |-----------|----------------|---------|-----------------|--------| | freq_min / freq_max | bandpass_filter | 300/6000 Hz | 150–500 / 3000–10000 | Spike band; NP 1.0 standard is 300–6000 Hz | | nblocks | Kilosort4 | 5 | 0–10 | Number of drift correction blocks; 0 disables drift correction | | Th_learned | Kilosort4 | 8 | 6–12 | Detection threshold (× noise level); lower = more spikes, more noise | | ms_before / ms_after | extract_waveforms | 1.0/2.0 | 0.5–2.0/1.0–3.0 ms | Waveform snippet window around spike peak | | max_spikes_per_unit | extract_waveforms | 500 | 100–5000 | Maximum spikes per unit for waveform extraction | | snr threshold | quality metrics | — | 5–10 | Signal-to-noise threshold for unit acceptance | | isi_violations_ratio | quality metrics | — | 0.05–0.2 | Refractory period violation rate; <0.1 is well-isolated | | presence_ratio | quality metrics | — | 0.9 | Fraction of recording where unit is active | | bin_size | PSTH | 0.01 s | 0.001–0.05 s | PSTH temporal resolution; smaller = more detail, noisier |
Raw voltage → preprocessing (common reference, bandpass) → detection (threshold crossing or learned templates) → clustering (PCA + k-means or Gaussian mixture) → template matching → quality metrics → curation. Kilosort4 adds drift correction, which is critical for chronic NP recordings (tip drift of 5–50 µm over hours degrades unit isolation).
pythonimport spikeinterface.sorters as ss import spikeinterface.comparison as sc # Run two sorters sorting_ks4 = ss.run_sorter("kilosort4", recording_clean, output_folder="./ks4") sorting_sc = ss.run_sorter("spykingcircus2", recording_clean, output_folder="./sc2") # Compare outputs comparison = sc.compare_two_sorters(sorting_ks4, sorting_sc, sorting1_name="Kilosort4", sorting2_name="SpykingCircus2") print(comparison.get_performance()) # Shows: agreement fraction, recall, precision per matched unit pair
pythonimport numpy as np import matplotlib.pyplot as plt fs = 30000 duration_s = sorting_curated.get_total_duration() bin_edges = np.arange(0, duration_s, 0.05) # 50 ms bins # Build firing rate matrix: (n_units, n_bins) fr_matrix = np.zeros((len(good_unit_ids), len(bin_edges)-1)) for i, uid in enumerate(good_unit_ids[:50]): spikes_s = sorting_curated.get_unit_spike_train(uid, segment_index=0) / fs counts, _ = np.histogram(spikes_s, bins=bin_edges) fr_matrix[i] = counts / 0.05 # Hz # Normalize each unit fr_norm = (fr_matrix - fr_matrix.mean(1, keepdims=True)) / (fr_matrix.std(1, keepdims=True) + 1e-6) fig, ax = plt.subplots(figsize=(12, 5)) im = ax.imshow(fr_norm, aspect="auto", cmap="RdBu_r", extent=[0, duration_s, len(good_unit_ids[:50]), 0], vmin=-2, vmax=2) ax.set_xlabel("Time (s)") ax.set_ylabel("Unit #") ax.set_title("Population Activity Heatmap (z-scored FR)") plt.colorbar(im, ax=ax, label="z-score") plt.tight_layout() plt.savefig("population_heatmap.pdf", bbox_inches="tight")
| Output | Format | Typical Content | |--------|--------|-----------------| | sorting/ | SpikeInterface folder | Spike times per unit; template waveforms | | waveforms/ | SpikeInterface folder | Waveform snippets (n_spikes, n_samples, n_channels) | | quality_metrics.csv | CSV | SNR, ISI violations, firing rate, presence ratio per unit | | psth_grid.pdf | PDF | PSTH plots for curated units | | recording_sorted.nwb | NWB/HDF5 | Portable spike-sorted data for sharing | | phy_export/ | Phy format | .npy + params.py for manual curation GUI |
| Problem | Cause | Solution | |---------|-------|----------| | Kilosort GPU memory error | Recording too long or too many channels for VRAM | Process recording in time chunks using si.SubRecording; use NP 2.0 (192 ch) settings | | Zero units found | Threshold too high or preprocessing removed all signal | Lower Th_learned to 6; verify recording_clean has non-zero channel data | | Excessive ISI violations (>50%) | Multi-unit activity merged as single unit | Manual curation with Phy; increase Th_learned to be more conservative | | Waveform extraction OOM | Too many spikes × channels × samples | Reduce max_spikes_per_unit; increase n_jobs for parallel extraction | | Drift correction fails | Too few spikes or very short recording | Set nblocks=1 (minimal correction) or nblocks=0 (disable) | | NWB export fails | Missing session metadata (subject, date) | Provide NWBFile metadata; or use sexp.export_to_nwb(..., metadata={...}) | | SpikeGLX file not found | Wrong stream name | List streams: si.get_neo_streams("spikeglx", data_dir) |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 18,119 | 24,738 | +37% | 1 | 1 | 0% | 3,381 | 8,041 | +138% | 0 | 0 | — |
case-15 | pass→pass | 10,907 | 5,951 | -45% | 1 | 1 | 0% | 1,940 | 5,416 | +179% | 0 | 0 | — |
case-02 | pass→pass | 6,229 | 4,294 | -31% | 1 | 1 | 0% | 1,239 | 5,201 | +320% | 0 | 0 | — |
case-03 | pass→pass | 5,754 | 5,702 | -1% | 1 | 1 | 0% | 1,057 | 5,361 | +407% | 0 | 0 | — |
case-04 | pass→pass | 12,224 | 8,630 | -29% | 1 | 1 | 0% | 2,139 | 5,780 | +170% | 0 | 0 | — |
case-05 | fail→pass | 7,437 | 7,190 | -3% | 1 | 1 | 0% | 1,357 | 5,664 | +317% | 0 | 0 | — |
case-21 | pass→pass | 20,650 | 18,055 | -13% | 1 | 1 | 0% | 3,798 | 7,761 | +104% | 0 | 0 | — |
case-06 | pass→pass | 9,536 | 4,986 | -48% | 1 | 1 | 0% | 1,633 | 5,188 | +218% | 0 | 0 | — |
case-07 | pass→pass | 15,020 | 12,948 | -14% | 1 | 1 | 0% | 2,690 | 6,688 | +149% | 0 | 0 | — |
case-08 | fail→fail | 13,761 | 10,753 | -22% | 1 | 1 | 0% | 2,467 | 6,299 | +155% | 0 | 0 | — |
case-09 | pass→pass | 15,031 | 11,625 | -23% | 1 | 1 | 0% | 2,646 | 6,446 | +144% | 0 | 0 | — |
case-10 | pass→pass | 8,866 | 10,035 | +13% | 1 | 1 | 0% | 1,709 | 6,280 | +267% | 0 | 0 | — |
case-11 | fail→pass | 6,674 | 4,554 | -32% | 1 | 1 | 0% | 1,040 | 5,068 | +387% | 0 | 0 | — |
case-12 | pass→pass | 9,555 | 5,659 | -41% | 1 | 1 | 0% | 1,552 | 5,326 | +243% | 0 | 0 | — |
case-13 | pass→pass | 9,386 | 7,777 | -17% | 1 | 1 | 0% | 1,537 | 5,595 | +264% | 0 | 0 | — |
case-14 | pass→pass | 12,271 | 7,856 | -36% | 1 | 1 | 0% | 2,308 | 5,822 | +152% | 0 | 0 | — |
case-16 | fail→fail | 9,953 | 9,002 | -10% | 1 | 1 | 0% | 1,757 | 5,880 | +235% | 0 | 0 | — |
case-17 | fail→pass | 15,660 | 15,888 | +1% | 1 | 1 | 0% | 927 | 5,158 | +456% | 0 | 0 | — |
case-18 | fail→pass | 26,242 | 10,113 | -61% | 1 | 1 | 0% | 2,128 | 6,019 | +183% | 0 | 0 | — |
case-19 | pass→pass | 3,109 | 10,348 | +233% | 1 | 1 | 0% | 521 | 4,655 | +793% | 0 | 0 | — |
case-20 | pass→pass | 22,346 | 21,712 | -3% | 1 | 1 | 0% | 4,358 | 8,549 | +96% | 0 | 0 | — |
case-22 | pass→pass | 19,163 | 16,508 | -14% | 1 | 1 | 0% | 3,851 | 7,843 | +104% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +18 percentage points is the difference between those two pass rates over the 21 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.