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

MOTION_CORRECTION.md

102 lines · skills/neuropixels-analysis/references/MOTION_CORRECTION.md at 5f2bc03

Open on GitHub
1# Motion and drift correction
2
3Reviewed against SpikeInterface 0.105.0 on 2026-10-01. Examples with real
4recordings are illustrative; tiny synthetic checks validate API/data contracts,
5not the accuracy of a drift estimate.
6

Motion and drift correction

Reviewed against SpikeInterface 0.105.0 on 2026-10-01. Examples with real recordings are illustrative; tiny synthetic checks validate API/data contracts, not the accuracy of a drift estimate.

Raw lines 1 to 6
7## Inspect before choosing correction
8
9Use filtered, referenced, **unwhitened** traces with verified contact positions in
10micrometers. Select one AP stream and one continuous segment. A cloud of peaks
11spanning the probe is population depth, not motion: look for changes over time
12in coherent bands. Changing firing rates can confound registration. Separate
13shanks before estimating motion when they do not share a displacement field.
15```python
16import spikeinterface.full as si
17from spikeinterface.sortingcomponents.peak_detection import detect_peaks
18from spikeinterface.sortingcomponents.peak_localization import localize_peaks
20noise = si.get_noise_levels(rec, return_in_uV=False)
21peaks = detect_peaks(rec, method='locally_exclusive', method_kwargs={'noise_levels': noise, 'detect_threshold': 5, 'radius_um': 50}, job_kwargs={'n_jobs': 1})
22locations = localize_peaks(rec, peaks, method='center_of_mass', job_kwargs={'n_jobs': 1})
23si.plot_drift_raster_map(peaks=peaks, peak_locations=locations, recording=rec)
24```
26Peak fields are `sample_index`, `channel_index`, `amplitude`, `segment_index`.
27Sample indices are local to each segment; convert to seconds with that segment's
28sampling rate/time origin, rather than overlaying segments at zero.

Inspect before choosing correction

Use filtered, referenced, unwhitened traces with verified contact positions in micrometers. Select one AP stream and one continuous segment. A cloud of peaks spanning the probe is population depth, not motion: look for changes over time in coherent bands. Changing firing rates can confound registration. Separate shanks before estimating motion when they do not share a displacement field.

import spikeinterface.full as si
from spikeinterface.sortingcomponents.peak_detection import detect_peaks
from spikeinterface.sortingcomponents.peak_localization import localize_peaks

noise = si.get_noise_levels(rec, return_in_uV=False)
peaks = detect_peaks(rec, method='locally_exclusive', method_kwargs={'noise_levels': noise, 'detect_threshold': 5, 'radius_um': 50}, job_kwargs={'n_jobs': 1})
locations = localize_peaks(rec, peaks, method='center_of_mass', job_kwargs={'n_jobs': 1})
si.plot_drift_raster_map(peaks=peaks, peak_locations=locations, recording=rec)

Peak fields are sample_index, channel_index, amplitude, segment_index. Sample indices are local to each segment; convert to seconds with that segment's sampling rate/time origin, rather than overlaying segments at zero.

Raw lines 7 to 29
30## Estimate, inspect, then interpolate
32`estimate_motion` returns a `Motion` object, not three arrays. `win_scale_um`
33controls window size; the older `win_sigma_um` examples are unsupported.
35```python
36from spikeinterface.sortingcomponents.motion import estimate_motion, interpolate_motion
38motion = estimate_motion(rec, peaks, locations, method="decentralized",
39 direction="y", rigid=True, bin_s=2.0,
40 progress_bar=True)
41si.plot_motion(motion)
42motion.save("motion_estimate/")
43motion = si.load("motion_estimate/")
44rec_corrected = interpolate_motion(rec, motion, border_mode="remove_channels")
45```
47The temporal samples and displacements are `motion.temporal_bins_s[segment]`
48and `motion.displacement[segment]`; spatial bins are `motion.spatial_bins_um`.
49`remove_channels` can change the contact set. Recheck channel IDs, geometry and
50probe mapping after interpolation. `force_extrapolate` retains edge contacts but
51can extrapolate outside measured support; inspect those channels separately.

Estimate, inspect, then interpolate

estimate_motion returns a Motion object, not three arrays. win_scale_um controls window size; the older win_sigma_um examples are unsupported.

from spikeinterface.sortingcomponents.motion import estimate_motion, interpolate_motion

motion = estimate_motion(rec, peaks, locations, method="decentralized",
                         direction="y", rigid=True, bin_s=2.0,
                         progress_bar=True)
si.plot_motion(motion)
motion.save("motion_estimate/")
motion = si.load("motion_estimate/")
rec_corrected = interpolate_motion(rec, motion, border_mode="remove_channels")

The temporal samples and displacements are motion.temporal_bins_s[segment] and motion.displacement[segment]; spatial bins are motion.spatial_bins_um. remove_channels can change the contact set. Recheck channel IDs, geometry and probe mapping after interpolation. force_extrapolate retains edge contacts but can extrapolate outside measured support; inspect those channels separately.

Raw lines 30 to 52
53## Preset workflow
55```python
56rec_corrected, motion_info = si.correct_motion(
57 rec, preset="nonrigid_fast_and_accurate", folder="motion/",
58 output_motion_info=True, n_jobs=1, chunk_duration="1s",
60si.plot_motion_info(motion_info, recording=rec)
61```
63In 0.105.0, supported presets are `dredge`, `dredge_fast` (upstream default),
64`medicine`, `nonrigid_accurate`, `nonrigid_fast_and_accurate`, `rigid_fast`, and
65`kilosort_like`. Names are not guarantees of accuracy. Evaluate residual drift,
66waveform stability, edge effects, and unit yield on the experiment. Some methods
67need optional dependencies. The bundled pipeline deliberately retains its
68explicit `nonrigid_fast_and_accurate` preset for reproducibility.
70For estimation only:
72```python
73motion, motion_info = si.compute_motion(
74 rec, preset="nonrigid_fast_and_accurate", output_motion_info=True,
75 folder="motion_only/", n_jobs=1,
77```
79There is no `dredge_lfp` preset in this API. LFP registration is a distinct DREDge
80workflow: consult its upstream examples, validate filtering, electrode positions
81and alignment between AP and LF clocks, then verify the sign/units/time domain
82before transferring motion. Do not substitute LF data into an AP peak pipeline.
83There is also no public `estimate_motion_from_sorting` helper in this release.

Preset workflow

rec_corrected, motion_info = si.correct_motion(
    rec, preset="nonrigid_fast_and_accurate", folder="motion/",
    output_motion_info=True, n_jobs=1, chunk_duration="1s",
)
si.plot_motion_info(motion_info, recording=rec)

In 0.105.0, supported presets are dredge, dredge_fast (upstream default), medicine, nonrigid_accurate, nonrigid_fast_and_accurate, rigid_fast, and kilosort_like. Names are not guarantees of accuracy. Evaluate residual drift, waveform stability, edge effects, and unit yield on the experiment. Some methods need optional dependencies. The bundled pipeline deliberately retains its explicit nonrigid_fast_and_accurate preset for reproducibility.

For estimation only:

motion, motion_info = si.compute_motion(
    rec, preset="nonrigid_fast_and_accurate", output_motion_info=True,
    folder="motion_only/", n_jobs=1,
)

There is no dredge_lfp preset in this API. LFP registration is a distinct DREDge workflow: consult its upstream examples, validate filtering, electrode positions and alignment between AP and LF clocks, then verify the sign/units/time domain before transferring motion. Do not substitute LF data into an AP peak pipeline. There is also no public estimate_motion_from_sorting helper in this release.

Raw lines 53 to 84
85## Avoid applying two motion corrections
87```python
88sorting = si.run_sorter("kilosort4", rec_corrected, folder="ks4/",
89 do_correction=False)
90sorting_cpu = si.run_sorter("spykingcircus2", rec_corrected, folder="sc2/",
91 apply_motion_correction=False)
92```
94Kilosort 2.5/3 use `do_correction=False` too. Spykingcircus2's
95`apply_preprocessing=False` does not disable its separate motion stage. If using
96uncorrected input, choose the sorter's own correction instead. Inspect before and
97after plots using identical limits; residual activity bands and registration
98uncertainty matter more than a universal micrometer cutoff.
100Sources: [motion guide](https://spikeinterface.readthedocs.io/en/stable/modules/motion_correction.html),
101[API](https://spikeinterface.readthedocs.io/en/stable/api.html),
102[DREDge](https://github.com/evarol/DREDge).

Avoid applying two motion corrections

sorting = si.run_sorter("kilosort4", rec_corrected, folder="ks4/",
                         do_correction=False)
sorting_cpu = si.run_sorter("spykingcircus2", rec_corrected, folder="sc2/",
                             apply_motion_correction=False)

Kilosort 2.5/3 use do_correction=False too. Spykingcircus2's apply_preprocessing=False does not disable its separate motion stage. If using uncorrected input, choose the sorter's own correction instead. Inspect before and after plots using identical limits; residual activity bands and registration uncertainty matter more than a universal micrometer cutoff.

Sources: motion guide, API, DREDge.

Raw lines 85 to 102