Description
Raw.interpolate_to(..., method="spline") produces badly wrong reconstructions at electrodes low on the head — up to exact sign inversion (r = −1 against ground truth) — where interpolate_bads reconstructs the same channels from the same data correctly. Observed in 1.12.1; the code on current main is unchanged.
Root cause
In mne/channels/interpolation.py::_interpolate_to_eeg (spline branch), the fitted sphere origin is subtracted from the source positions but not from the target positions:
pos_from = inst.info._get_channel_positions(picks_good_eeg) - origin_val
pos_to = np.stack(list(ch_pos.values()), axis=0) # origin NOT subtracted
...
mapping = _make_interpolation_matrix(pos_from, pos_to, alpha=reg)
_make_interpolation_matrix then unit-normalises both sets, so source and target directions are measured from different centres. With origin="auto" the fitted origin sits well above the head-frame origin (~36 mm for the GSN-HydroCel templates), so target unit vectors are systematically wrong relative to the sources — worst for low-lying electrodes, where the spline effectively evaluates the field at a displaced scalp location. If that displaced location crosses a sign boundary of the field (e.g. after linked-mastoid referencing), the reconstruction is a negation of the truth.
_interpolate_bads_eeg subtracts the origin from both sets (pos_good = pos[goods_idx_pos] - origin; pos_bad = pos[bads_idx_pos] - origin), which is why the two entry points disagree.
Reproduction
Hold a channel out, reconstruct it at its own position, compare with what it recorded:
import numpy as np
import mne
montage = mne.channels.make_standard_montage("GSN-HydroCel-129")
positions = montage.get_positions()
ch_pos = positions["ch_pos"]
names = montage.ch_names
# Smooth synthetic field: amplitude graded with electrode height. 20 uV, 10 Hz.
t = np.arange(1000) / 250.0
wave = 20e-6 * np.sin(2 * np.pi * 10.0 * t)
z = np.array([float(ch_pos[n][2]) for n in names])
weights = 0.5 + (z - z.min()) / np.ptp(z) # 0.5 .. 1.5, smooth in space
data = weights[:, None] * wave
raw = mne.io.RawArray(data, mne.create_info(names, 250.0, "eeg"), verbose=False)
raw.set_montage(montage, verbose=False)
raw.set_eeg_reference(ref_channels=["E57", "E100"], verbose=False) # linked mastoids
for chan in ("E11", "E62", "E38", "E121", "E56", "E107"):
truth = raw.get_data(picks=[chan])[0]
single = mne.channels.make_dig_montage(
ch_pos={chan: ch_pos[chan]}, nasion=positions["nasion"],
lpa=positions["lpa"], rpa=positions["rpa"], coord_frame="head")
est_to = (raw.copy().drop_channels([chan])
.interpolate_to(single, method="spline").get_data()[0])
raw_bads = raw.copy()
raw_bads.info["bads"] = [chan]
est_bads = raw_bads.interpolate_bads(reset_bads=False).get_data(picks=[chan])[0]
print(f"{chan:>5} interpolate_to r={np.corrcoef(truth, est_to)[0,1]:+.3f} "
f"interpolate_bads r={np.corrcoef(truth, est_bads)[0,1]:+.3f}")
Output (MNE 1.12.1):
E11 interpolate_to r=+1.000 interpolate_bads r=+1.000
E62 interpolate_to r=+1.000 interpolate_bads r=+1.000
E38 interpolate_to r=-1.000 interpolate_bads r=+1.000
E121 interpolate_to r=-1.000 interpolate_bads r=+1.000
E56 interpolate_to r=+1.000 interpolate_bads r=+1.000
E107 interpolate_to r=+1.000 interpolate_bads r=+1.000
E38/E121 (lower frontal ring) reconstruct as the exact negation of a spatially smooth field. On real EEG we first noticed this as r ≈ −0.99 at temporal electrodes when validating montage reduction — physically implausible values that vanish once the frame is fixed.
Expected behaviour
interpolate_to at a held-out channel's own position should agree with interpolate_bads (and with the recorded data): r = +1.000 for all channels above.
Suggested fix
pos_to = np.stack(list(ch_pos.values()), axis=0) - origin_val
We validated this correction externally: with both position sets centred on the same fitted origin, held-out reconstruction on all three GSN nets returns r > 0.999 at every 10-20 target for smooth synthetic fields, and the anomalous negative correlations on real recordings disappear.
Secondary note: interpolate_to defaults to reg=0.0 while interpolate_bads uses alpha=1e-5; aligning the defaults would make the two entry points consistent once the frame is fixed.
Environment
MNE 1.12.1, Python 3.x, Windows 11 (also inspected main — _interpolate_to_eeg unchanged).
Description
Raw.interpolate_to(..., method="spline")produces badly wrong reconstructions at electrodes low on the head — up to exact sign inversion (r = −1 against ground truth) — whereinterpolate_badsreconstructs the same channels from the same data correctly. Observed in 1.12.1; the code on currentmainis unchanged.Root cause
In
mne/channels/interpolation.py::_interpolate_to_eeg(spline branch), the fitted sphere origin is subtracted from the source positions but not from the target positions:_make_interpolation_matrixthen unit-normalises both sets, so source and target directions are measured from different centres. Withorigin="auto"the fitted origin sits well above the head-frame origin (~36 mm for the GSN-HydroCel templates), so target unit vectors are systematically wrong relative to the sources — worst for low-lying electrodes, where the spline effectively evaluates the field at a displaced scalp location. If that displaced location crosses a sign boundary of the field (e.g. after linked-mastoid referencing), the reconstruction is a negation of the truth._interpolate_bads_eegsubtracts the origin from both sets (pos_good = pos[goods_idx_pos] - origin; pos_bad = pos[bads_idx_pos] - origin), which is why the two entry points disagree.Reproduction
Hold a channel out, reconstruct it at its own position, compare with what it recorded:
Output (MNE 1.12.1):
E38/E121 (lower frontal ring) reconstruct as the exact negation of a spatially smooth field. On real EEG we first noticed this as r ≈ −0.99 at temporal electrodes when validating montage reduction — physically implausible values that vanish once the frame is fixed.
Expected behaviour
interpolate_toat a held-out channel's own position should agree withinterpolate_bads(and with the recorded data): r = +1.000 for all channels above.Suggested fix
We validated this correction externally: with both position sets centred on the same fitted origin, held-out reconstruction on all three GSN nets returns r > 0.999 at every 10-20 target for smooth synthetic fields, and the anomalous negative correlations on real recordings disappear.
Secondary note:
interpolate_todefaults toreg=0.0whileinterpolate_badsusesalpha=1e-5; aligning the defaults would make the two entry points consistent once the frame is fixed.Environment
MNE 1.12.1, Python 3.x, Windows 11 (also inspected
main—_interpolate_to_eegunchanged).