Electrodes, montages and the 10-20 system: standard montages, channel names and types, and where the reference is

nb-0-2-montages Level 0 · Read the Raw Signal ~2 min Used in L0.2 · Electrodes, montages and the 10-20 system

Downloads from ds-eegbci, ds-iowapd when you run it.

Download the notebook (.ipynb) Outputs below are the ones stored when it was executed — you do not need to run anything to read it.

nb-0-2-montages · Electrodes, montages and the 10-20 system (L0.2)

Lesson L0.2 Electrodes, montages and the 10-20 system · Level 0 · Status draft — drafted for expert review; every scientific statement below is a draft and uncertain points carry TODO(confirm).

What you will do

  1. Read the channel labels of a real EDF file (Sharbrough-style labels with trailing dots), map them to 10-10 names, and attach a standard montage in MNE.
  2. Plot the sensors (a static figure) and check the naming convention — odd = left, even = right, z = midline — against the electrode positions.
  3. Repair a set of mismatched channel names (a vendor-style export: prefixes, suffixes, upper case, legacy names) so that a montage can be attached, and set the channel types so that a non-EEG line is not treated as EEG.
  4. Exercise data: open a BIDS-organised EEGLAB recording from ds-iowapd and identify its recording reference from the data.

Data

  • ds-eegbci — EEG Motor Movement/Imagery Dataset (EEGMMIDB), Schalk et al. (2004), PhysioNet v1.0.0, DOI 10.13026/C28G6P, license ODC-By 1.0. From the catalog: 64 channels placed by the international 10-10 system (Sharbrough-style labels in the files), 160 Hz, no hardware filters, 60 Hz mains. Subject S001, run R01 (eyes open, ~1 min) — one 1.2 MB EDF file.
  • ds-iowapd — Iowa Parkinson's disease resting EEG, "Rest eyes open", Anjum et al. (2024), OpenNeuro ds004584, DOI 10.18112/openneuro.ds004584.v1.0.0, license CC0. From the catalog: 64-channel actiCAP on a Brain Vision system, 500 Hz, Pz as the online reference, 0.1 Hz online high-pass, 60 Hz mains, eyes-open rest of about 3 min, BIDS layout with EEGLAB .set/.fdt files. Subject sub-001 only — about 36 MB from OpenNeuro's public S3 bucket, nothing else of the dataset.

The catalog does not document the recording reference of ds-eegbci (TODO(confirm)); for ds-iowapd it does, which is what makes the second file a good exercise: the file has to agree with the documentation, and you should be able to tell from the data alone.

In [1]:
# Setup: dependencies, the shared helpers, non-interactive plotting.
import importlib.util
import subprocess
import sys
import warnings
from pathlib import Path

# 1. Dependencies are pinned in notebooks/requirements.txt.  Nothing is installed
#    when the pinned stack is already present (local runs, CI); a fresh Colab or
#    Binder kernel installs it once.  On Colab, run from a clone of the repository
#    so that notebooks/_shared/ is available (repository URL: TODO(confirm), spec
#    section 13 item 3).
_needed = ("mne", "scipy", "matplotlib", "pooch")
_missing = [p for p in _needed if importlib.util.find_spec(p) is None]
if _missing:
    _req = next((d / "requirements.txt" for d in (Path.cwd(), *Path.cwd().parents)
                 if (d / "requirements.txt").exists()), None)
    _cmd = [sys.executable, "-m", "pip", "install", "-q"]
    _cmd += ["-r", str(_req)] if _req else ["mne==1.10.2", "moabb==1.7.2", "pooch>=1.8", "edfio>=0.4"]
    subprocess.check_call(_cmd)

# 2. Shared helpers (notebooks/_shared/helpers.py), located relative to the working
#    directory -- notebooks/<level>/ or notebooks/ -- never through an absolute path.
_shared = next((d / "_shared" for d in (Path.cwd(), *Path.cwd().parents)
                if (d / "_shared" / "helpers.py").exists()), None)
if _shared is None:
    raise FileNotFoundError("start the kernel in notebooks/L0/ (or notebooks/) so that _shared/helpers.py is found")
sys.path.insert(0, str(_shared))
import helpers

# 3. Plotting: Jupyter's default inline backend renders static PNGs through Agg
#    (no windows, nothing blocks); outside Jupyter the helpers select Agg.  Every
#    MNE figure is requested with show=False, and plt.show() renders each cell's
#    figures in place.
import matplotlib.pyplot as plt
import numpy as np
import mne

mne.viz.set_browser_backend("matplotlib", verbose=False)
mne.set_log_level("WARNING")
plt.rcParams["figure.dpi"] = 72
print(f"MNE {mne.__version__}; helpers imported from notebooks/_shared; "
      "downloads go to MNE's default data directory unless EEG_COURSE_DATA is set")
MNE 1.10.2; helpers imported from notebooks/_shared; downloads go to MNE's default data directory unless EEG_COURSE_DATA is set

1. From the labels in the file to 10-10 names

The EDF header of ds-eegbci stores Sharbrough-style labels padded with dots to a fixed width (Fc5., Fp1., T9..). MNE's montages know the 10-10 names in their canonical case (FC5, Fp1, T9), so set_montage refuses the raw labels — which is the right behaviour: a silent partial match would leave channels without positions. mne.datasets.eegbci.standardize applies the dataset-specific mapping; the table below shows what it did, then the montage attaches without a single missing name.

In [2]:
from mne.datasets import eegbci

DATASET, SUBJECT, RUN = "ds-eegbci", "S001", "R01"
root = helpers.data_dir()                                              # None = MNE's default data directory
edf_path = eegbci.load_data(1, [1], update_path=False, verbose=False, **({"path": str(root)} if root else {}))[0]
raw = mne.io.read_raw_edf(edf_path, preload=True, verbose=False)      # the file as stored: names untouched
print("file:", Path(edf_path).name, "|", raw)
names_in_file = list(raw.ch_names)

montage = mne.channels.make_standard_montage("standard_1005")           # a superset of the 10-10 positions
try:
    raw.copy().set_montage(montage, on_missing="raise", verbose=False)
except ValueError as e:
    print("set_montage with the raw labels ->", str(e).split("\n")[0][:110], "...")

eegbci.standardize(raw)                                                # dataset-specific: strip the dots, fix the case
mapping = dict(zip(names_in_file, raw.ch_names))
changed = {k: v for k, v in mapping.items() if k != v}
print(f"{len(changed)} of {len(mapping)} labels changed; examples:")
for k in ["Fc5.", "Fcz.", "Fp1.", "Afz.", "T9..", "Iz..", "Po7.", "Cp3."]:
    print(f"   {k!r:8s} -> {mapping[k]!r}")
raw.set_montage(montage, on_missing="raise", verbose=False)           # no ValueError now: every name is known
n_pos = sum(bool(np.any(ch["loc"][:3] != 0)) for ch in raw.info["chs"])
print(f"montage attached: {n_pos} of {len(raw.ch_names)} channels have a position")
file: S001R01.edf | <RawEDF | S001R01.edf, 64 x 9760 (61.0 s), ~4.8 MiB, data loaded>
set_montage with the raw labels -> DigMontage is only a subset of info. There are 64 channel positions not present in the DigMontage. The channel ...
64 of 64 labels changed; examples:
   'Fc5.'   -> 'FC5'
   'Fcz.'   -> 'FCz'
   'Fp1.'   -> 'Fp1'
   'Afz.'   -> 'AFz'
   'T9..'   -> 'T9'
   'Iz..'   -> 'Iz'
   'Po7.'   -> 'PO7'
   'Cp3.'   -> 'CP3'
montage attached: 64 of 64 channels have a position

2. Plot the sensors, and check the convention against the positions

Two static views of the same montage: the flat top view with names, and a 3-D view. The convention says odd numbers are on the left, even on the right, z on the midline. MNE's head coordinate frame has +x pointing to the right ear, so the convention can be checked against the template positions rather than taken on trust — a useful habit when a cap has been put on backwards or a vendor numbers its electrodes differently.

In [3]:
fig = plt.figure(figsize=(12, 5.5))
ax2d = fig.add_subplot(1, 2, 1)
ax3d = fig.add_subplot(1, 2, 2, projection="3d")
raw.plot_sensors(show_names=True, axes=ax2d, show=False)
raw.plot_sensors(kind="3d", axes=ax3d, show=False)
ax2d.set_title("ds-eegbci S001: 64 electrodes, template 10-10 positions (standard_1005), top view")
ax3d.set_title("the same positions in 3-D (head coordinates, m)")
fig.tight_layout()
plt.show()   # render the static figure(s) of this cell inline

# The odd/even/z convention, checked against the x coordinate (+x = right ear).
agree, disagree = 0, []
for ch in raw.info["chs"]:
    name, x_mm = ch["ch_name"], ch["loc"][0] * 1000
    digit = next((c for c in reversed(name) if c.isdigit()), None)
    if name.endswith("z"):
        ok = abs(x_mm) < 5
    elif digit is not None:
        ok = (x_mm < 0) if int(digit) % 2 else (x_mm > 0)
    else:
        ok = True
    agree += ok
    if not ok:
        disagree.append((name, round(x_mm)))
print(f"odd = left, even = right, z = midline: {agree} of {len(raw.ch_names)} labels agree with their template position"
      + (f"; exceptions: {disagree}" if disagree else ""))
Figure 1 of notebook nb-0-2-montages, an output plot. The text around it states what it shows and the units of every axis.
odd = left, even = right, z = midline: 64 of 64 labels agree with their template position

3. Mismatched channel names, and channel types

Files exported from acquisition software rarely carry clean 10-10 names. Common variants: a modality prefix and a reference suffix (EEG Fp1-Ref), upper case (FP1), the legacy 10-20 names T3/T4/T5/T6 for what the 10-10 system calls T7/T8/P7/P8, and a non-EEG line (a trigger or status channel) that the exporter typed as EEG.

To practise the repair on real data, the cell below relabels a copy of the same recording in that style and appends an all-zero Status line typed as EEG (synthetic labels on real data — the only thing that is invented here is the naming). A small normalisation function then recovers the montage names; set_channel_types fixes the type. Neither step touches the data; both are recorded in info, which is where MNE keeps them for every later step.

In [4]:
LEGACY_1020 = {"T3": "T7", "T4": "T8", "T5": "P7", "T6": "P8"}      # 10-20 legacy names -> 10-10 names

# --- a vendor-style copy of the recording (synthetic names; the data are S001 R01 unchanged)
messy = raw.copy()
vendor_names = {}
for name in messy.ch_names:
    legacy = {v: k for k, v in LEGACY_1020.items()}.get(name, name)  # T7 -> T3 etc.
    vendor_names[name] = f"EEG {legacy.upper()}-REF"
messy.rename_channels(vendor_names)
status = mne.io.RawArray(np.zeros((1, messy.n_times)), mne.create_info(["Status"], messy.info["sfreq"], "eeg"), verbose=False)
messy.add_channels([status], force_update_info=True)
print("vendor-style labels:", messy.ch_names[:4], "...", messy.ch_names[-3:])
try:
    messy.copy().set_montage(montage, on_missing="raise", verbose=False)
except ValueError as e:
    print("set_montage ->", str(e).split("\n")[0][:100], "...")


def normalise_name(label, known):
    """Map an exported label onto a montage name: strip prefix/suffix, ignore case, translate legacy 10-20 names."""
    core = label.strip()
    for prefix in ("EEG ", "EEG:", "EEG_"):
        if core.upper().startswith(prefix):
            core = core[len(prefix):]
    core = core.split("-")[0].strip()                    # 'FP1-REF' -> 'FP1'
    core = LEGACY_1020.get(core.upper(), core)
    by_upper = {k.upper(): k for k in known}             # case-insensitive lookup into the montage names
    return by_upper.get(core.upper(), label)             # unknown labels are left alone (and reported below)


known = set(montage.ch_names)
fixed = {ch: normalise_name(ch, known) for ch in messy.ch_names}
unresolved = [ch for ch, new in fixed.items() if new not in known]
print(f"renamed {sum(k != v for k, v in fixed.items())} labels; unresolved: {unresolved}")
messy.rename_channels(fixed)
messy.set_channel_types({"Status": "stim"})               # not EEG: excluded from EEG picks, references, topographies
messy.set_montage(montage, on_missing="raise", verbose=False)   # the stim channel needs no position
n_eeg, n_stim = len(mne.pick_types(messy.info, eeg=True)), len(mne.pick_types(messy.info, stim=True))
print(f"after the repair: {n_eeg} EEG channels with positions + {n_stim} stim channel; "
      f"identical data to the original: {np.array_equal(messy.get_data(picks='eeg'), raw.get_data())}")
vendor-style labels: ['EEG FC5-REF', 'EEG FC3-REF', 'EEG FC1-REF', 'EEG FCZ-REF'] ... ['EEG O2-REF', 'EEG IZ-REF', 'Status']
set_montage -> DigMontage is only a subset of info. There are 65 channel positions not present in the DigMontage. T ...
renamed 64 labels; unresolved: ['Status']
after the repair: 64 EEG channels with positions + 1 stim channel; identical data to the original: True

4. Exercise data: ds-iowapd sub-001 — where is the recording reference?

A referential recording stores every electrode minus one reference electrode. If the reference electrode is itself stored as a channel, that channel is flat (the reference minus itself is zero); if the acquisition software omits it, it is simply absent from the file. Either way the data carry the information, and it should agree with the documentation: the catalog says the reference is Pz, and BIDS puts the same fact in the sidecar (EEGReference).

helpers.load_iowapd downloads only this subject's files from the OpenNeuro bucket (the .set header, the .fdt data and the small BIDS sidecars), reads them with MNE's EEGLAB reader and attaches template 10-10 positions by channel name.

In [5]:
import json

setf = helpers.fetch_iowapd("sub-001")                     # prints what it fetches or reuses; None if unreachable
IOWA_OK = setf is not None
if not IOWA_OK:
    print("ds-iowapd could not be fetched; the rest of this section is skipped")
else:
    sidecar = json.loads((setf.parent / setf.name.replace("_eeg.set", "_eeg.json")).read_text())
    channels_tsv = (setf.parent / setf.name.replace("_eeg.set", "_channels.tsv")).read_text().splitlines()
    print("BIDS sidecar (selected keys):")
    for k in ("EEGReference", "EEGGround", "EEGChannelCount", "SamplingFrequency", "PowerLineFrequency",
              "RecordingDuration", "SoftwareFilters"):
        print(f"   {k:20s} {sidecar.get(k)}")
    print(f"channels.tsv: {len(channels_tsv) - 1} rows (header: {channels_tsv[0]!r})")

    iowa = helpers.load_iowapd("sub-001", quiet=True)
    rep = helpers.first_look_report(iowa, "ds-iowapd")
    ann_counts = {str(k): v for k, v in rep["annotation_counts"].items()}
    print(f"\nfile: {setf.name} | {rep['sfreq_hz']:g} Hz | {rep['n_channels']} channels ({rep['channel_types']}) | "
          f"{rep['duration_s']:.1f} s | annotations {ann_counts}")
    print("header filters:", rep["highpass_hz_in_header"], "Hz high-pass,", rep["lowpass_hz_in_header"], "Hz low-pass "
          "(the header records no filter; the catalog documents a 0.1 Hz online high-pass -- the file does not carry it)")
    print("is the documented reference 'Pz' among the stored channels?", "Pz" in iowa.ch_names)
ds-iowapd sub-001: using the cached files sub-001_task-Rest_eeg.set, sub-001_task-Rest_eeg.fdt
BIDS sidecar (selected keys):
   EEGReference         Pz
   EEGGround            AFz
   EEGChannelCount      63
   SamplingFrequency    500
   PowerLineFrequency   60
   RecordingDuration    281.66
   SoftwareFilters      n/a
channels.tsv: 63 rows (header: 'name\ttype\tunits')
file: sub-001_task-Rest_eeg.set | 500 Hz | 63 channels ({'eeg': 63}) | 281.7 s | annotations {'boundary': 1}
header filters: 0.0 Hz high-pass, 250.0 Hz low-pass (the header records no filter; the catalog documents a 0.1 Hz online high-pass -- the file does not carry it)
is the documented reference 'Pz' among the stored channels? False

4a. The hole in the montage

Attach the template positions by name and draw the sensors. A 64-electrode cap with 63 stored channels leaves one position empty; adding the reference back as an (all-zero) channel with mne.add_reference_channels puts a marker at that position — drawn in red here by marking it bad for the plot only.

In [6]:
if IOWA_OK:
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")                                 # MNE reminds us to set the montage again; we do
        with_ref = mne.add_reference_channels(iowa, "Pz", copy=True)   # the reference as an explicit, all-zero channel
    with_ref.set_montage("standard_1005", on_missing="ignore", verbose=False)
    with_ref.info["bads"] = ["Pz"]                                  # for the plot: bads are drawn in red
    fig = plt.figure(figsize=(12, 5.5))
    ax_a = fig.add_subplot(1, 2, 1)
    ax_b = fig.add_subplot(1, 2, 2)
    iowa.plot_sensors(show_names=True, axes=ax_a, show=False)
    with_ref.plot_sensors(show_names=True, axes=ax_b, show=False)
    ax_a.set_title("ds-iowapd sub-001: the 63 stored channels (template 10-10 positions)")
    ax_b.set_title("the same, with the documented reference Pz added back (red)")
    fig.tight_layout()
    plt.show()   # render the static figure(s) of this cell inline
    with_ref.info["bads"] = []
Figure 2 of notebook nb-0-2-montages, an output plot. The text around it states what it shows and the units of every axis.

4b. What the amplitudes say

Every stored channel is electrode minus Pz. Electrodes right next to Pz see almost the same potential as Pz, so their difference is small: the channels with the smallest amplitude should cluster around the reference site. The topography of each channel's standard deviation makes that visible; the bar chart names the two smallest. (Whether those two are additionally bridged to the reference by gel cannot be decided from the file — TODO(confirm); the label here is algorithmic, as in nb-0-5.)

In [7]:
if IOWA_OK:
    eeg_names = [iowa.ch_names[i] for i in mne.pick_types(iowa.info, eeg=True)]
    sd = iowa.get_data(picks=eeg_names).std(axis=1) * 1e6
    order = np.argsort(sd)
    fig, axes = plt.subplots(1, 2, figsize=(13, 4.6), gridspec_kw=dict(width_ratios=[1, 1.8]))
    helpers.plot_topomap_values(iowa, sd, unit="uV", title="Standard deviation per channel", ax=axes[0], vlim=(0, float(np.percentile(sd, 95))))
    axes[1].bar(range(len(sd)), sd[order], color=["tab:red" if i < 2 else "tab:blue" for i in range(len(sd))])
    axes[1].set_xticks(range(len(sd)))
    axes[1].set_xticklabels([eeg_names[i] for i in order], rotation=90, fontsize=7)
    axes[1].set(ylabel="std (uV)", title="Channel standard deviation, sorted (uV); the two smallest in red")
    axes[1].grid(alpha=0.3, axis="y")
    fig.tight_layout()
    plt.show()   # render the static figure(s) of this cell inline

    lowest = [(eeg_names[i], float(sd[i])) for i in order[:2]]
    pos = {ch["ch_name"]: ch["loc"][:3] for ch in with_ref.info["chs"]}
    for name, s in lowest:
        d_cm = np.linalg.norm(pos[name] - pos["Pz"]) * 100
        print(f"{name}: std {s:.1f} uV (median over channels {np.median(sd):.1f} uV), {d_cm:.1f} cm from the Pz position")
Figure 3 of notebook nb-0-2-montages, an output plot. The text around it states what it shows and the units of every axis.
P1: std 4.3 uV (median over channels 41.6 uV), 3.0 cm from the Pz position
P3: std 4.8 uV (median over channels 41.6 uV), 6.0 cm from the Pz position

4c. The flat-channel test, before and after adding the reference back

helpers.flat_channels lists EEG channels whose standard deviation is below a threshold (0.5 µV by default). On the file as stored it finds nothing — the reference is absent, not flat. On the copy with Pz added back it finds exactly one flat channel, which is what a recording that does store its reference electrode looks like. Re-referencing shows that flatness is a property of the reference choice, not of the electrode: after an average reference (L2.3) Pz carries signal again.

In [8]:
if IOWA_OK:
    print("flat channels in the file as stored     :", helpers.flat_channels(iowa) or "none")
    print("flat channels with Pz added back        :", helpers.flat_channels(with_ref))
    avg = with_ref.copy().set_eeg_reference("average", projection=False, verbose=False)
    print(f"Pz after an average reference           : std {float(avg.get_data(picks='Pz').std() * 1e6):.1f} uV (no longer flat)")
    eeg_before = len(mne.pick_types(iowa.info, eeg=True))
    print(f"EEG channels: {eeg_before} stored -> {len(mne.pick_types(with_ref.info, eeg=True))} with the reference added back")
flat channels in the file as stored     : none
flat channels with Pz added back        : [('Pz', 0.0)]
Pz after an average reference           : std 27.6 uV (no longer flat)
EEG channels: 63 stored -> 64 with the reference added back

What the data say, independently of the documentation: 63 stored channels for a 64-electrode cap; no stored channel is flat, so the reference was not stored; and the two lowest-amplitude channels sit beside the parietal midline, where the sensor plot shows an empty position. The data locate the reference region; the documentation — the sidecar's EEGReference and the catalog — names the electrode, Pz, and adding it back turns the empty position into the flat channel. When the two sources do not agree, the file (or its documentation) has a problem you must sort out before anything else (L0.6).

5. The numbers

In [9]:
print(f"nb-0-2-montages -- {DATASET} (EEGMMIDB; PhysioNet DOI 10.13026/C28G6P; ODC-By 1.0), subject {SUBJECT} run {RUN}")
print(f"labels mapped from the EDF header to 10-10 names: {len(changed)} of {len(mapping)} changed; "
      f"{n_pos} of {len(raw.ch_names)} channels positioned by standard_1005; "
      f"odd/even/z convention agrees with the template positions for {agree} of {len(raw.ch_names)} labels")
print(f"vendor-style repair: {sum(k != v for k, v in fixed.items())} labels renamed, 1 channel retyped (Status: eeg -> stim), "
      f"{n_eeg} EEG channels positioned")
print()
if IOWA_OK:
    ref_name = sidecar.get("EEGReference")
    print("ds-iowapd (OpenNeuro ds004584; CC0; DOI 10.18112/openneuro.ds004584.v1.0.0), sub-001 task-Rest:")
    print(f"  {rep['sfreq_hz']:g} Hz | {rep['n_channels']} stored channels (catalog: 64-channel cap) | {rep['duration_s']:.1f} s")
    flat_after = helpers.flat_channels(with_ref)
    print(f"  recording reference: {ref_name}  <-- the answer this notebook reports (L0.2 exercise, 'identify the recording reference')")
    print(f"     named by the documentation (sidecar EEGReference = {ref_name!r}; catalog: Pz online reference) and confirmed by the data:")
    print(f"     (1) {rep['n_channels']} stored channels for a 64-electrode cap, and the empty position in the montage is the parietal midline;")
    print(f"     (2) no stored channel is flat (smallest std {sd[order[0]]:.1f} uV on {eeg_names[order[0]]}), so the reference electrode was not "
          f"stored -- added back it is the flat channel ({flat_after[0][0]}, {flat_after[0][1]:.2f} uV);")
    print(f"     (3) the two lowest-amplitude channels, {lowest[0][0]} ({lowest[0][1]:.1f} uV) and {lowest[1][0]} ({lowest[1][1]:.1f} uV), "
          f"lie beside that position (median channel std {np.median(sd):.1f} uV)")
else:
    print("ds-iowapd: not fetched in this run (see the note above); the reference answer is TODO(confirm) until the file is checked")
print()
print("Note: in this file the reference electrode is absent rather than stored flat; the flat channel appears only once the")
print("reference is added back. TODO(confirm): draft answer until the author reviews it.")
nb-0-2-montages -- ds-eegbci (EEGMMIDB; PhysioNet DOI 10.13026/C28G6P; ODC-By 1.0), subject S001 run R01
labels mapped from the EDF header to 10-10 names: 64 of 64 changed; 64 of 64 channels positioned by standard_1005; odd/even/z convention agrees with the template positions for 64 of 64 labels
vendor-style repair: 64 labels renamed, 1 channel retyped (Status: eeg -> stim), 64 EEG channels positioned

ds-iowapd (OpenNeuro ds004584; CC0; DOI 10.18112/openneuro.ds004584.v1.0.0), sub-001 task-Rest:
  500 Hz | 63 stored channels (catalog: 64-channel cap) | 281.7 s
  recording reference: Pz  <-- the answer this notebook reports (L0.2 exercise, 'identify the recording reference')
     named by the documentation (sidecar EEGReference = 'Pz'; catalog: Pz online reference) and confirmed by the data:
     (1) 63 stored channels for a 64-electrode cap, and the empty position in the montage is the parietal midline;
     (2) no stored channel is flat (smallest std 4.3 uV on P1), so the reference electrode was not stored -- added back it is the flat channel (Pz, 0.00 uV);
     (3) the two lowest-amplitude channels, P1 (4.3 uV) and P3 (4.8 uV), lie beside that position (median channel std 41.6 uV)

Note: in this file the reference electrode is absent rather than stored flat; the flat channel appears only once the
reference is added back. TODO(confirm): draft answer until the author reviews it.