{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n\n# Identify EEG Electrodes Bridged by too much Gel\n\nResearch-grade EEG often uses a gel based system, and when too much gel is\napplied the gel conducting signal from the scalp to the electrode for one\nelectrode connects with the gel conducting signal from another electrode\n\"bridging\" the two signals. This is undesirable because the signals from the\ntwo (or more) electrodes are not as independent as they would otherwise be;\nthey are very similar to each other introducing additional\nspatial smearing. An algorithm has been developed to detect electrode\nbridging :footcite:`TenkeKayser2001`, which has been implemented in EEGLAB\n:footcite:`DelormeMakeig2004`. Unfortunately, there is not a lot to be\ndone about electrode brigding once the data has been collected as far as\npreprocessing other than interpolating bridged channels. Therefore, our\nrecommendation is to check for electrode bridging early in data collection\nand address the problem. Or, if the data has already been collected, quantify\nthe extent of the bridging so as not to introduce bias into the data from this\neffect and exclude subjects with bridging that might effect the outcome of a\nstudy. Preventing electrode bridging is ideal but awareness of the problem at\nleast will mitigate its potential as a confound to a study. This tutorial\nfollows the eBridge tutorial from https://psychophysiology.cpmc.columbia.edu.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# Authors: Alex Rockhill \n#\n# License: BSD-3-Clause\n# Copyright the MNE-Python contributors." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.colors import LinearSegmentedColormap\n\nimport mne\n\nprint(__doc__)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Compute Electrical Distance Metric\nFirst, let's compute electrical distance metrics for a group of example\nsubjects from the EEGBCI dataset in order to estimate electrode bridging.\nThe electrical distance is just the variance of signals subtracted\npairwise. Channels with activity that mirror another channel nearly\nexactly will have very low electrical distance. By inspecting the\ndistribution of electrical distances, we can look for pairwise distances\nthat are consistently near zero which are indicative of bridging.\n\n

Note

It is likely to be sufficient to run this algorithm on a\n small portion (~3 minutes is probably plenty) of the data but\n that gel might settle over the course of a study causing more\n bridging so using the last segment of the data will\n give the most conservative estimate.

\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "montage = mne.channels.make_standard_montage(\"standard_1005\")\ned_data = dict() # electrical distance/bridging data\nraw_data = dict() # store infos for electrode positions\nfor sub in range(1, 11):\n print(f\"Computing electrode bridges for subject {sub}\")\n raw_fname = mne.datasets.eegbci.load_data(subjects=sub, runs=(1,))[0]\n raw = mne.io.read_raw(raw_fname, preload=True, verbose=False)\n mne.datasets.eegbci.standardize(raw) # set channel names\n raw.set_montage(montage, verbose=False)\n raw_data[sub] = raw\n ed_data[sub] = mne.preprocessing.compute_bridged_electrodes(raw)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Examine an Electrical Distance Matrix\nBefore we look at the electrical distance distributions across subjects,\nlet's look at the distance matrix for one subject and try and understand\nhow the algorithm works. We'll use subject 6 as it is a good example of\nbridging. In the zoomed out color scale version on the right, we can see\nthat there is a distribution of electrical distances that are specific to\nthat subject's head physiology/geometry and brain activity during the\nrecording. On the right, when we clip the color range to zoom in, we can\nsee several electrical distance outliers that are near zero;\nthese indicate bridging.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "bridged_idx, ed_matrix = ed_data[6]\n\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4), layout=\"constrained\")\nfig.suptitle(\"Subject 6 Electrical Distance Matrix\")\n\n# take median across epochs, only use upper triangular, lower is NaNs\ned_plot = np.zeros(ed_matrix.shape[1:]) * np.nan\ntriu_idx = np.triu_indices(ed_plot.shape[0], 1)\nfor idx0, idx1 in np.array(triu_idx).T:\n ed_plot[idx0, idx1] = np.nanmedian(ed_matrix[:, idx0, idx1])\n\n# plot full distribution color range\nim1 = ax1.imshow(ed_plot, aspect=\"auto\")\ncax1 = fig.colorbar(im1, ax=ax1)\ncax1.set_label(r\"Electrical Distance ($\\mu$$V^2$)\")\n\n# plot zoomed in colors\nim2 = ax2.imshow(ed_plot, aspect=\"auto\", vmax=5)\ncax2 = fig.colorbar(im2, ax=ax2)\ncax2.set_label(r\"Electrical Distance ($\\mu$$V^2$)\")\nfor ax in (ax1, ax2):\n ax.set_xlabel(\"Channel Index\")\n ax.set_ylabel(\"Channel Index\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Examine the Distribution of Electrical Distances\nNow let's plot a histogram of the electrical distance matrix. Note that the\nelectrical distance matrix from the previous plot is upper triangular but\ndoes not include the diagonal. This means that the pairwise electrical\ndistances are not computed between the same channel (which makes sense as\nthe differences between a channel and itself would just be zero). The initial\npeak near zero therefore represents pairs of different channels that\nare nearly identical which is indicative of bridging. EEG recordings\nwithout bridged electrodes do not have a peak near zero.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(5, 5))\nfig.suptitle(\"Subject 6 Electrical Distance Matrix Distribution\")\nax.hist(ed_matrix[~np.isnan(ed_matrix)], bins=np.linspace(0, 500, 51))\nax.set_xlabel(r\"Electrical Distance ($\\mu$$V^2$)\")\nax.set_ylabel(\"Count (channel pairs for all epochs)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Plot Electrical Distances on a Topomap\nNow, let's look at the topography of the electrical distance matrix and\nsee where our bridged channels are and check that their spatial\narrangement makes sense. Here, we are looking at the minimum electrical\ndistance for each channel and taking the median across all epochs\n(the raw data is epoched into 2 second non-overlapping intervals).\nThis example is of the subject from the EEGBCI dataset with the most\nbridged channels so there are many light areas and red lines. They are\ngenerally grouped together and are biased toward horizontal connections\n(this may be because the EEG experimenter usually stands to the side and\nmay have inserted the gel syringe tip in too far).\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "mne.viz.plot_bridged_electrodes(\n raw_data[6].info,\n bridged_idx,\n ed_matrix,\n title=\"Subject 6 Bridged Electrodes\",\n topomap_args=dict(vlim=(None, 5)),\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Plot the Raw Voltage Time Series for Bridged Electrodes\nFinally, let's do a sanity check and make sure that the bridged electrodes\nare indeed implausibly similar. We'll plot two bridged electrode pairs:\nF2-F4 and FC2-FC4, for subject 6 where they are bridged and subject 1\nwhere they are not. As we can see, the pairs are nearly identical for\nsubject 6 confirming that they are likely bridged. Interestingly, even\nthough the two pairs are adjacent to each other, there are two distinctive\npairs, meaning that it is unlikely that all four of these electrodes are\nbridged.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "raw = raw_data[6].copy().pick([\"FC2\", \"FC4\", \"F2\", \"F4\"])\nraw.add_channels(\n [\n mne.io.RawArray(\n raw.get_data(ch1) - raw.get_data(ch2),\n mne.create_info([f\"{ch1}-{ch2}\"], raw.info[\"sfreq\"], \"eeg\"),\n raw.first_samp,\n )\n for ch1, ch2 in [(\"F2\", \"F4\"), (\"FC2\", \"FC4\")]\n ]\n)\nraw.plot(duration=20, scalings=dict(eeg=2e-4))\n\nraw = raw_data[1].copy().pick([\"FC2\", \"FC4\", \"F2\", \"F4\"])\nraw.add_channels(\n [\n mne.io.RawArray(\n raw.get_data(ch1) - raw.get_data(ch2),\n mne.create_info([f\"{ch1}-{ch2}\"], raw.info[\"sfreq\"], \"eeg\"),\n raw.first_samp,\n )\n for ch1, ch2 in [(\"F2\", \"F4\"), (\"FC2\", \"FC4\")]\n ]\n)\nraw.plot(duration=20, scalings=dict(eeg=2e-4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Compare Bridging Across Subjects in the EEGBCI Dataset\nNow, let's look at the histograms of electrical distances for the whole\nEEGBCI dataset. As we can see in the zoomed in insert on the right,\nfor subjects 6, 7 and 8 (and to a lesser extent 2 and 4), there is a\ndifferent shape of the distribution of electrical distances around\n0 ${\\mu}V^2$ than for the other subjects. These subjects'\ndistributions have a peak around 0 ${\\mu}V^2$ distance\nand a trough around 5 ${\\mu}V^2$ which is indicative of\nelectrode bridging. The rest of the subjects' distributions increase\nmonotonically, indicating normal spatial separation of sources. The\nlarge discrepancy in shapes of distributions is likely driven primarily by\nartifacts such as blinks which are an order of magnitude larger than\nneural data since this data has not been preprocessed but likely\nreflect neural or at least anatomical differences as well (i.e. the\ndistance from the sensors to the brain).\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8, 4), layout=\"constrained\")\nfig.suptitle(\"Electrical Distance Distribution for EEGBCI Subjects\")\nfor ax in (ax1, ax2):\n ax.set_ylabel(\"Count\")\n ax.set_xlabel(r\"Electrical Distance ($\\mu$$V^2$)\")\n\nfor sub, (bridged_idx, ed_matrix) in ed_data.items():\n # ed_matrix is upper triangular so exclude bottom half of NaNs\n hist, edges = np.histogram(\n ed_matrix[~np.isnan(ed_matrix)].flatten(), bins=np.linspace(0, 1000, 101)\n )\n centers = (edges[1:] + edges[:-1]) / 2\n ax1.plot(centers, hist)\n hist, edges = np.histogram(\n ed_matrix[~np.isnan(ed_matrix)].flatten(), bins=np.linspace(0, 30, 21)\n )\n centers = (edges[1:] + edges[:-1]) / 2\n ax2.plot(centers, hist, label=f\"Sub {sub} #={len(bridged_idx)}\")\n\nax1.axvspan(0, 30, color=\"r\", alpha=0.5)\nax2.legend(loc=(1.04, 0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For the group of subjects, let's look at their electrical distances\nand bridging. Especially since this is the same task, the lack of\nlow electrical distances in many of the subjects is compelling\nevidence that the low electrical distance is caused by bridging\nand that it is avoidable given more judicious application of gel or\nother conductive electrolyte solution.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "for sub, (bridged_idx, ed_matrix) in ed_data.items():\n mne.viz.plot_bridged_electrodes(\n raw_data[sub].info,\n bridged_idx,\n ed_matrix,\n title=f\"Subject {sub} Bridged Electrodes\",\n topomap_args=dict(vlim=(None, 5)),\n )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For subjects with many bridged channels like Subject 6 shown in the example\nabove, it is advisable to exclude the subject. This because EEG recording\nmontage will not be comparable with the other subjects. And, if we tried to\ninterpole, the interpolation would depend on other channels which are also\nbridged in that case. However, for subjects with only a few bridged channels,\nthose channels can be interpolated. Since the bridged data is still\nbiological (i.e. it is recording the subject's brain), it's just spatially\nsmeared, we can use :func:`mne.preprocessing.interpolate_bridged_electrodes`\nto make a virtual channel midway between the two bridged channels\nto aid in interpolation.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# use subject 2, only one bridged electrode pair\nbridged_idx = ed_data[2][0]\nraw = mne.preprocessing.interpolate_bridged_electrodes(\n raw_data[2].copy(), bridged_idx=bridged_idx\n)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's make sure that our virtual channel aided the interpolation. We can do\nthis by simulating a bridge to make sure that we recover the original data\nbetter with the virtual channel method. If we make two channels nearly the\nsame to simulate a bridged electrode but save the original data, we can\ncompare the two interpolation methods. As we can see, the virtual channel\nrecovers the original data more slightly closely. However, as shown in the\nplots, there is still residual signal for both methods implying that it is\nthere is still a loss of data compared to unbridged channels.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "raw = raw_data[2].copy()\n\n# pick two channels to simulate a bridge\nidx0, idx1 = 9, 10\nch0, ch1 = raw.ch_names[idx0], raw.ch_names[idx1]\nbridged_idx_simulated = [(idx0, idx1)]\n# get the original data to compare to\ndata_orig = raw_data[2].get_data(picks=(idx0, idx1))\n\n# simulate a bridge between the two channels by taking their mean and adding\n# some noise\nrng = np.random.default_rng(11) # seed for reproducibility\nraw_sim = raw.copy() # raw with simulated electrode bridge\n# remove channels with original data\nraw_sim = raw_sim.drop_channels([ch0, ch1])\nbridged_data = np.tile(np.mean(data_orig, axis=0), (2, 1)) # copy mean\n# add separate noise for each channel\nbridged_data[0] += 1e-7 * rng.normal(size=raw.times.size)\nbridged_data[1] += 1e-7 * rng.normal(size=raw.times.size)\n# add back simulated data\nraw_sim = raw_sim.add_channels(\n [\n mne.io.RawArray(\n bridged_data,\n mne.create_info([ch0, ch1], raw.info[\"sfreq\"], \"eeg\"),\n raw.first_samp,\n )\n ]\n)\nraw_sim.set_montage(montage) # add back channel positions\n\n# use virtual channel method\nraw_virtual = mne.preprocessing.interpolate_bridged_electrodes(\n raw_sim.copy(), bridged_idx=bridged_idx_simulated\n)\ndata_virtual = raw_virtual.get_data(picks=(idx0, idx1))\n\n# set bads to be bridged electrodes to interpolate without a virtual channel\nraw_comp = raw_sim.copy()\nraw_comp.info[\"bads\"] = [raw_sim.ch_names[idx0], raw_sim.ch_names[idx1]]\nraw_comp.interpolate_bads()\ndata_comp = raw_comp.get_data(picks=(idx0, idx1))\n\n# compute variance of residuals\nprint(\n \"Variance of residual (interpolated data - original data)\\n\\n\"\n f\"With adding virtual channel: {np.mean(np.var(data_virtual - data_orig, axis=1))}\\n\"\n f\"Compared to interpolation only using other channels: {np.mean(np.var(data_comp - data_orig, axis=1))}\"\n \"\"\n)\n\n# plot results\nraw = raw.pick([ch0, ch1])\nraw = raw.add_channels(\n [\n mne.io.RawArray(\n np.concatenate([data_virtual, data_virtual - data_orig]),\n mne.create_info(\n [\n f\"{ch0} virtual\",\n f\"{ch1} virtual\",\n f\"{ch0} virtual diff\",\n f\"{ch1} virtual diff\",\n ],\n raw.info[\"sfreq\"],\n \"eeg\",\n ),\n raw.first_samp,\n )\n ]\n)\nraw = raw.add_channels(\n [\n mne.io.RawArray(\n np.concatenate([data_comp, data_comp - data_orig]),\n mne.create_info(\n [f\"{ch0} comp\", f\"{ch1} comp\", f\"{ch0} comp diff\", f\"{ch1} comp diff\"],\n raw.info[\"sfreq\"],\n \"eeg\",\n ),\n raw.first_samp,\n )\n ]\n)\nraw.plot(scalings=dict(eeg=7e-5))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The Relationship Between Bridging and Impedances\nElectrode bridging is often brought about by inserting more gel in order\nto bring impendances down. Thus it can be helpful to compare bridging\nto impedances in the quest to be an ideal EEG technician! Low\nimpedances lead to less noisy data and EEG without bridging is more\nspatially precise. Brain Imaging Data Structure (BIDS) recommends that\nimpedances be stored in an EEG dataset in the `electrodes.tsv`_ file.\nSince the impedances are not stored for this dataset, we will fake\nthem to demonstrate how they would be plotted. Here, the impedances\nare plotted as is typical at the end of a setup; most channels are\ngood but there are a few that need to have their impedance lowered.\nThe impedances should ideally all be less than 25 KOhm before\nstarting an experiment when using active systems and less than 5 KOhm\nwhen using a passive system.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "rng = np.random.default_rng(11) # seed for reproducibility\nraw = raw_data[1]\n# typically impedances < 25 kOhm are acceptable for active systems and\n# impedances < 5 kOhm are desirable for a passive system\nimpedances = rng.random(len(raw.ch_names)) * 30\nimpedances[10] = 80 # set a few bad impendances\nimpedances[25] = 99\ncmap = LinearSegmentedColormap.from_list(\n name=\"impedance_cmap\", colors=[\"g\", \"y\", \"r\"], N=256\n)\nfig, ax = plt.subplots(figsize=(5, 5))\nim, cn = mne.viz.plot_topomap(impedances, raw.info, axes=ax, cmap=cmap, vlim=(25, 75))\nax.set_title(\"Electrode Impendances\")\ncax = fig.colorbar(im, ax=ax)\ncax.set_label(r\"Impedance (k$\\Omega$)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\nIn this example, we have shown a dataset where electrical bridging occurred\nduring the EEG setup for several subjects. Hopefully this is convincing as to\nthe importance of proper technique as well as checking your work to\nlearn and improve as an EEG experimenter, and hopefully this tool will help\nus all collect better EEG data in the future.\n\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## References\n.. footbibliography::\n\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.2" } }, "nbformat": 4, "nbformat_minor": 0 }