04. From MEG sensor-space data to HNN simulation

This example demonstrates how to calculate an inverse solution of the median nerve evoked response potential (ERP) in S1 from the MNE somatosensory dataset, and then simulate a biophysical model network that reproduces the observed dynamics. Note that we do not expound on how we came up with the sequence of evoked drives used in this example, rather, we only demonstrate its implementation. For those who want more background on the HNN model and the process used to articulate the proximal and distal drives needed to simulate evoked responses, see the HNN ERP tutorial. The sequence of evoked drives presented here is not part of a current publication but is motivated by prior studies [1], [2].

# Authors: Mainak Jas <mainakjas@gmail.com>
#          Ryan Thorpe <ryan_thorpe@brown.edu>

# sphinx_gallery_thumbnail_number = 2

First, we will import the packages needed for computing the inverse solution from the MNE somatosensory dataset. MNE (and its dependency NiBabel) can be installed with pip install mne nibabel The somatosensory dataset can be downloaded by importing somato from mne.datasets.

import os.path as op
import matplotlib.pyplot as plt

import mne
from mne.datasets import somato
from mne.minimum_norm import apply_inverse, make_inverse_operator

Now we set the the path of the somato dataset for subject '01'.

data_path = somato.data_path()
subject = '01'
task = 'somato'
raw_fname = op.join(data_path, 'sub-{}'.format(subject), 'meg',
                    'sub-{}_task-{}_meg.fif'.format(subject, task))
fwd_fname = op.join(data_path, 'derivatives', 'sub-{}'.format(subject),
                    'sub-{}_task-{}-fwd.fif'.format(subject, task))
subjects_dir = op.join(data_path, 'derivatives', 'freesurfer', 'subjects')
Using default location ~/mne_data for somato...
Creating ~/mne_data
Downloading file 'MNE-somato-data.tar.gz' from 'https://osf.io/tp4sg/download?version=7' to '/home/circleci/mne_data'.

  0%|                                               | 0.00/611M [00:00<?, ?B/s]
  1%|▍                                     | 7.09M/611M [00:00<00:08, 70.9MB/s]
  3%|█                                     | 16.7M/611M [00:00<00:06, 85.9MB/s]
  4%|█▌                                    | 25.3M/611M [00:00<00:08, 68.6MB/s]
  6%|██▏                                   | 34.3M/611M [00:00<00:07, 75.9MB/s]
  7%|██▋                                   | 44.0M/611M [00:00<00:06, 82.8MB/s]
  9%|███▍                                  | 54.4M/611M [00:00<00:06, 89.7MB/s]
 11%|████                                  | 64.6M/611M [00:00<00:05, 93.6MB/s]
 12%|████▌                                 | 74.2M/611M [00:00<00:08, 66.7MB/s]
 14%|█████▏                                | 83.5M/611M [00:01<00:07, 73.1MB/s]
 15%|█████▊                                | 93.2M/611M [00:01<00:06, 79.1MB/s]
 17%|██████▌                                | 102M/611M [00:01<00:06, 82.1MB/s]
 18%|███████▏                               | 112M/611M [00:01<00:05, 85.5MB/s]
 20%|███████▋                               | 121M/611M [00:01<00:05, 87.4MB/s]
 21%|████████▎                              | 130M/611M [00:01<00:05, 82.1MB/s]
 23%|████████▊                              | 138M/611M [00:01<00:05, 79.1MB/s]
 24%|█████████▎                             | 147M/611M [00:01<00:05, 78.1MB/s]
 25%|█████████▊                             | 155M/611M [00:01<00:06, 76.0MB/s]
 27%|██████████▍                            | 163M/611M [00:02<00:05, 78.9MB/s]
 28%|███████████                            | 172M/611M [00:02<00:05, 82.7MB/s]
 30%|███████████▌                           | 181M/611M [00:02<00:05, 83.9MB/s]
 31%|████████████▏                          | 190M/611M [00:02<00:04, 86.2MB/s]
 33%|████████████▋                          | 199M/611M [00:02<00:04, 87.9MB/s]
 34%|█████████████▎                         | 209M/611M [00:02<00:04, 89.2MB/s]
 36%|█████████████▉                         | 218M/611M [00:02<00:04, 90.4MB/s]
 37%|██████████████▌                        | 227M/611M [00:02<00:04, 90.9MB/s]
 39%|███████████████                        | 236M/611M [00:02<00:04, 90.6MB/s]
 40%|███████████████▋                       | 246M/611M [00:02<00:04, 91.1MB/s]
 42%|████████████████▎                      | 255M/611M [00:03<00:03, 90.5MB/s]
 43%|████████████████▊                      | 264M/611M [00:03<00:03, 91.2MB/s]
 45%|█████████████████▍                     | 273M/611M [00:03<00:03, 91.8MB/s]
 46%|██████████████████                     | 283M/611M [00:03<00:03, 92.6MB/s]
 48%|██████████████████▋                    | 292M/611M [00:03<00:03, 92.9MB/s]
 49%|███████████████████▎                   | 302M/611M [00:03<00:03, 93.3MB/s]
 51%|███████████████████▊                   | 311M/611M [00:03<00:03, 93.6MB/s]
 52%|████████████████████▍                  | 320M/611M [00:03<00:03, 93.9MB/s]
 54%|█████████████████████                  | 330M/611M [00:03<00:03, 72.3MB/s]
 55%|█████████████████████▌                 | 338M/611M [00:04<00:03, 69.6MB/s]
 57%|██████████████████████                 | 345M/611M [00:04<00:03, 70.1MB/s]
 58%|██████████████████████▌                | 353M/611M [00:04<00:03, 65.6MB/s]
 59%|██████████████████████▉                | 360M/611M [00:04<00:04, 61.7MB/s]
 60%|███████████████████████▍               | 367M/611M [00:04<00:03, 64.2MB/s]
 61%|███████████████████████▉               | 375M/611M [00:04<00:03, 69.8MB/s]
 63%|████████████████████████▍              | 382M/611M [00:04<00:03, 69.7MB/s]
 64%|████████████████████████▉              | 390M/611M [00:04<00:03, 71.5MB/s]
 65%|█████████████████████████▍             | 397M/611M [00:04<00:03, 65.9MB/s]
 66%|█████████████████████████▊             | 404M/611M [00:05<00:03, 60.6MB/s]
 67%|██████████████████████████▎            | 411M/611M [00:05<00:03, 63.2MB/s]
 68%|██████████████████████████▋            | 418M/611M [00:05<00:02, 64.6MB/s]
 70%|███████████████████████████▏           | 426M/611M [00:05<00:02, 70.5MB/s]
 71%|███████████████████████████▊           | 435M/611M [00:05<00:02, 74.6MB/s]
 73%|████████████████████████████▎          | 444M/611M [00:05<00:02, 78.7MB/s]
 74%|████████████████████████████▊          | 452M/611M [00:05<00:02, 78.3MB/s]
 75%|█████████████████████████████▎         | 460M/611M [00:05<00:02, 74.0MB/s]
 77%|█████████████████████████████▊         | 467M/611M [00:05<00:02, 71.5MB/s]
 78%|██████████████████████████████▎        | 474M/611M [00:06<00:01, 70.3MB/s]
 79%|██████████████████████████████▊        | 481M/611M [00:06<00:01, 65.3MB/s]
 80%|███████████████████████████████▏       | 488M/611M [00:06<00:01, 63.7MB/s]
 81%|███████████████████████████████▋       | 495M/611M [00:06<00:01, 65.6MB/s]
 82%|████████████████████████████████       | 502M/611M [00:06<00:01, 65.0MB/s]
 84%|████████████████████████████████▌      | 510M/611M [00:06<00:01, 69.6MB/s]
 85%|█████████████████████████████████      | 517M/611M [00:06<00:01, 58.2MB/s]
 86%|█████████████████████████████████▍     | 523M/611M [00:06<00:01, 60.2MB/s]
 87%|█████████████████████████████████▊     | 530M/611M [00:06<00:01, 57.3MB/s]
 88%|██████████████████████████████████▍    | 538M/611M [00:07<00:01, 64.6MB/s]
 90%|██████████████████████████████████▉    | 547M/611M [00:07<00:00, 70.7MB/s]
 91%|███████████████████████████████████▌   | 556M/611M [00:07<00:00, 75.6MB/s]
 92%|████████████████████████████████████   | 565M/611M [00:07<00:00, 79.4MB/s]
 94%|████████████████████████████████████▌  | 573M/611M [00:07<00:00, 78.5MB/s]
 95%|█████████████████████████████████████  | 581M/611M [00:07<00:00, 68.6MB/s]
 96%|█████████████████████████████████████▌ | 588M/611M [00:07<00:00, 61.8MB/s]
 98%|██████████████████████████████████████ | 596M/611M [00:07<00:00, 66.4MB/s]
 99%|██████████████████████████████████████▌| 604M/611M [00:07<00:00, 71.7MB/s]
  0%|                                               | 0.00/611M [00:00<?, ?B/s]
100%|███████████████████████████████████████| 611M/611M [00:00<00:00, 1.73TB/s]
Untarring contents of '/home/circleci/mne_data/MNE-somato-data.tar.gz' to '/home/circleci/mne_data'
Attempting to create new mne-python configuration file:
/home/circleci/.mne/mne-python.json
Download complete in 14s (582.2 MB)

Then, we load the raw data and estimate the inverse operator.

# Read and band-pass filter the raw data
raw = mne.io.read_raw_fif(raw_fname, preload=True)
l_freq, h_freq = 1, 40
raw.filter(l_freq, h_freq)

# Identify stimulus events associated with MEG time series in the dataset
events = mne.find_events(raw, stim_channel='STI 014')

# Define epochs within the time series
event_id, tmin, tmax = 1, -.2, .17
baseline = None
epochs = mne.Epochs(raw, events, event_id, tmin, tmax, baseline=baseline,
                    reject=dict(grad=4000e-13, eog=350e-6), preload=True)

# Compute the inverse operator
fwd = mne.read_forward_solution(fwd_fname)
cov = mne.compute_covariance(epochs)
inv = make_inverse_operator(epochs.info, fwd, cov)
Opening raw data file /home/circleci/mne_data/MNE-somato-data/sub-01/meg/sub-01_task-somato_meg.fif...
    Range : 237600 ... 506999 =    791.189 ...  1688.266 secs
Ready.
Reading 0 ... 269399  =      0.000 ...   897.077 secs...
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 40 Hz

FIR filter parameters
---------------------
Designing a one-pass, zero-phase, non-causal bandpass filter:
- Windowed time-domain design (firwin) method
- Hamming window with 0.0194 passband ripple and 53 dB stopband attenuation
- Lower passband edge: 1.00
- Lower transition bandwidth: 1.00 Hz (-6 dB cutoff frequency: 0.50 Hz)
- Upper passband edge: 40.00 Hz
- Upper transition bandwidth: 10.00 Hz (-6 dB cutoff frequency: 45.00 Hz)
- Filter length: 993 samples (3.307 s)

[Parallel(n_jobs=1)]: Done  17 tasks      | elapsed:    0.1s
[Parallel(n_jobs=1)]: Done  71 tasks      | elapsed:    0.4s
[Parallel(n_jobs=1)]: Done 161 tasks      | elapsed:    0.9s
[Parallel(n_jobs=1)]: Done 287 tasks      | elapsed:    1.7s
111 events found on stim channel STI 014
Event IDs: [1]
Not setting metadata
111 matching events found
No baseline correction applied
0 projection items activated
Using data from preloaded Raw for 111 events and 112 original time points ...
0 bad epochs dropped
Reading forward solution from /home/circleci/mne_data/MNE-somato-data/derivatives/sub-01/sub-01_task-somato-fwd.fif...
    Reading a source space...
    [done]
    Reading a source space...
    [done]
    2 source spaces read
    Desired named matrix (kind = 3523) not available
    Read MEG forward solution (8155 sources, 306 channels, free orientations)
    Source spaces transformed to the forward solution coordinate frame
Computing rank from data with rank=None
    Using tolerance 1.5e-08 (2.2e-16 eps * 306 dim * 2.2e+05  max singular value)
    Estimated rank (mag + grad): 306
    MEG: rank 306 computed from 306 data channels with 0 projectors
/home/circleci/project/examples/workflows/plot_simulate_somato.py:66: RuntimeWarning: Something went wrong in the data-driven estimation of the data rank as it exceeds the theoretical rank from the info (306 > 64). Consider setting rank to "auto" or setting it explicitly as an integer.
  cov = mne.compute_covariance(epochs)
Reducing data rank from 306 -> 306
Estimating covariance using EMPIRICAL
Done.
Number of samples used : 12432
[done]
Converting forward solution to surface orientation
    No patch info available. The standard source space normals will be employed in the rotation to the local surface coordinates....
    Converting to surface-based source orientations...
    [done]
Computing inverse operator with 306 channels.
    306 out of 306 channels remain after picking
Selected 306 channels
Creating the depth weighting matrix...
    204 planar channels
    limit = 7615/8155 = 10.004172
    scale = 5.17919e-08 exp = 0.8
Applying loose dipole orientations to surface source spaces: 0.2
Whitening the forward solution.
Computing rank from covariance with rank=None
    Using tolerance 2e-12 (2.2e-16 eps * 306 dim * 29  max singular value)
    Estimated rank (mag + grad): 64
    MEG: rank 64 computed from 306 data channels with 0 projectors
    Setting small MEG eigenvalues to zero (without PCA)
Creating the source covariance matrix
Adjusting source covariance matrix.
Computing SVD of whitened and weighted lead field matrix.
    largest singular value = 2.42284
    scaling factor to adjust the trace = 3.86104e+18 (nchan = 306 nzero = 242)

There are several methods to do source reconstruction. Some of the methods such as MNE are distributed source methods whereas dipole fitting will estimate the location and amplitude of a single current dipole. At the moment, we do not offer explicit recommendations on which source reconstruction technique is best for HNN. However, we do want our users to note that the dipole currents simulated with HNN are assumed to be normal to the cortical surface. Hence, using the option pick_ori='normal' is appropriate.

snr = 3.
lambda2 = 1. / snr ** 2
evoked = epochs.average()
stc = apply_inverse(evoked, inv, lambda2, method='MNE',
                    pick_ori="normal", return_residual=False,
                    verbose=True)
Preparing the inverse operator for use...
    Scaled noise and source covariance from nave = 1 to nave = 111
    Created the regularized inverter
    The projection vectors do not apply to these channels.
    Created the whitener using a noise covariance matrix with rank 64 (242 small eigenvalues omitted)
Applying inverse operator to "1"...
    Picked 306 channels from the data
    Computing inverse...
    Eigenleads need to be weighted ...
    Computing residual...
    Explained  86.1% variance
[done]

To extract the primary response in primary somatosensory cortex (S1), we create a label for the postcentral gyrus (S1) in source-space

hemi = 'rh'
label_tag = 'G_postcentral'
label_s1 = mne.read_labels_from_annot(subject, parc='aparc.a2009s', hemi=hemi,
                                      regexp=label_tag,
                                      subjects_dir=subjects_dir)[0]
Reading labels from parcellation...
   read 1 labels from /home/circleci/mne_data/MNE-somato-data/derivatives/freesurfer/subjects/01/label/rh.aparc.a2009s.annot

Visualizing the distributed S1 activation in reference to the geometric structure of the cortex (i.e., plotted on a structural MRI) can help us figure out how to orient the dipole. Note that in the HNN framework, positive and negative deflections of a current dipole source correspond to upwards (from deep to superficial) and downwards (from superficial to deep) current flow, respectively. Uncomment the following code to open an interactive 3D render of the brain and its surface activation (requires the pyvista python library). You should get 2 plots, the first showing the post-central gyrus label from which the dipole time course was extracted and the second showing MNE activation at 0.040 sec that resemble the following images.

'''
Brain = mne.viz.get_brain_class()
brain_label = Brain(subject, hemi, 'white', subjects_dir=subjects_dir)
brain_label.add_label(label_s1, color='green', alpha=0.9)
stc_label = stc.in_label(label_s1)
brain = stc_label.plot(subjects_dir=subjects_dir, hemi=hemi, surface='white',
                       view_layout='horizontal', initial_time=0.04,
                       backend='pyvista')
'''
"\nBrain = mne.viz.get_brain_class()\nbrain_label = Brain(subject, hemi, 'white', subjects_dir=subjects_dir)\nbrain_label.add_label(label_s1, color='green', alpha=0.9)\nstc_label = stc.in_label(label_s1)\nbrain = stc_label.plot(subjects_dir=subjects_dir, hemi=hemi, surface='white',\n                       view_layout='horizontal', initial_time=0.04,\n                       backend='pyvista')\n"

mne_label_fig

mne_activity_fig

Now we extract the representative time course of dipole activation in our labeled brain region using mode='pca_flip' (see this MNE-python example for more details). Note that the most prominent component of the median nerve response occurs in the posterior wall of the central sulcus at ~0.040 sec. Since the dipolar activity here is negative, we orient the extracted waveform so that the deflection at ~0.040 sec is pointed downwards. Thus, the ~0.040 sec deflection corresponds to current flow traveling from superficial to deep layers of cortex.

flip_data = stc.extract_label_time_course(label_s1, inv['src'],
                                          mode='pca_flip')
dipole_tc = -flip_data[0] * 1e9

plt.figure()
plt.plot(1e3 * stc.times, dipole_tc, 'ro--')
plt.xlabel('Time (ms)')
plt.ylabel('Current Dipole (nAm)')
plt.xlim((0, 170))
plt.axhline(0, c='k', ls=':')
plt.show()
plot simulate somato
Extracting time courses for 1 labels (mode: pca_flip)

Now, let us try to simulate the same with hnn-core. We read in the network parameters from N20.json and instantiate the network.

import hnn_core
from hnn_core import simulate_dipole, jones_2009_model
from hnn_core import average_dipoles, JoblibBackend

hnn_core_root = op.dirname(hnn_core.__file__)
params_fname = op.join(hnn_core_root, 'param', 'N20.json')
net = jones_2009_model(params_fname)

To simulate the source of the median nerve evoked response, we add a sequence of synchronous evoked drives: 1 proximal, 2 distal, and 1 final proximal drive. In order to understand the physiological implications of proximal and distal drive as well as the general process used to articulate a sequence of exogenous drive for simulating evoked responses, see the HNN ERP tutorial. Note that setting n_drive_cells=1 and cell_specific=False creates a drive with synchronous input across cells in the network.

# Early proximal drive
weights_ampa_p = {'L2_basket': 0.0036, 'L2_pyramidal': 0.0039,
                  'L5_basket': 0.0019, 'L5_pyramidal': 0.0020}
weights_nmda_p = {'L2_basket': 0.0029, 'L2_pyramidal': 0.0005,
                  'L5_basket': 0.0030, 'L5_pyramidal': 0.0019}
synaptic_delays_p = {'L2_basket': 0.1, 'L2_pyramidal': 0.1,
                     'L5_basket': 1.0, 'L5_pyramidal': 1.0}

net.add_evoked_drive(
    'evprox1', mu=21., sigma=4., numspikes=1, location='proximal',
    n_drive_cells=1, cell_specific=False, weights_ampa=weights_ampa_p,
    weights_nmda=weights_nmda_p, synaptic_delays=synaptic_delays_p,
    event_seed=276)

# Late proximal drive
weights_ampa_p = {'L2_basket': 0.003, 'L2_pyramidal': 0.0039,
                  'L5_basket': 0.004, 'L5_pyramidal': 0.0020}
weights_nmda_p = {'L2_basket': 0.001, 'L2_pyramidal': 0.0005,
                  'L5_basket': 0.002, 'L5_pyramidal': 0.0020}
synaptic_delays_p = {'L2_basket': 0.1, 'L2_pyramidal': 0.1,
                     'L5_basket': 1.0, 'L5_pyramidal': 1.0}

net.add_evoked_drive(
    'evprox2', mu=134., sigma=4.5, numspikes=1, location='proximal',
    n_drive_cells=1, cell_specific=False, weights_ampa=weights_ampa_p,
    weights_nmda=weights_nmda_p, synaptic_delays=synaptic_delays_p,
    event_seed=276)

# Early distal drive
weights_ampa_d = {'L2_basket': 0.0043, 'L2_pyramidal': 0.0032,
                  'L5_pyramidal': 0.0009}
weights_nmda_d = {'L2_basket': 0.0029, 'L2_pyramidal': 0.0051,
                  'L5_pyramidal': 0.0010}
synaptic_delays_d = {'L2_basket': 0.1, 'L2_pyramidal': 0.1,
                     'L5_pyramidal': 0.1}

net.add_evoked_drive(
    'evdist1', mu=32., sigma=2.5, numspikes=1, location='distal',
    n_drive_cells=1, cell_specific=False, weights_ampa=weights_ampa_d,
    weights_nmda=weights_nmda_d, synaptic_delays=synaptic_delays_d,
    event_seed=277)

# Late distal drive
weights_ampa_d = {'L2_basket': 0.0041, 'L2_pyramidal': 0.0019,
                  'L5_pyramidal': 0.0018}
weights_nmda_d = {'L2_basket': 0.0032, 'L2_pyramidal': 0.0018,
                  'L5_pyramidal': 0.0017}
synaptic_delays_d = {'L2_basket': 0.1, 'L2_pyramidal': 0.1,
                     'L5_pyramidal': 0.1}

net.add_evoked_drive(
    'evdist2', mu=84., sigma=4.5, numspikes=1, location='distal',
    n_drive_cells=1, cell_specific=False, weights_ampa=weights_ampa_d,
    weights_nmda=weights_nmda_d, synaptic_delays=synaptic_delays_d,
    event_seed=275)

Now we run the simulation over 2 trials so that we can plot the average aggregate dipole. For a better match to the empirical waveform, set n_trials to be >=25.

n_trials = 2
# n_trials = 25
with JoblibBackend(n_jobs=2):
    dpls = simulate_dipole(net, tstop=170., n_trials=n_trials)
Joblib will run 2 trial(s) in parallel by distributing trials over 2 jobs.

Since the model is a reduced representation of the larger network contributing to the response, the model response is noisier than it would be in the net activity from a larger network where these effects are averaged out, and the dipole amplitude is smaller than the recorded data. The post-processing steps of smoothing and scaling the simulated dipole response allow us to more accurately approximate the true signal responsible for the recorded macroscopic evoked response [1], [2].

dpl_smooth_win = 20
dpl_scalefctr = 12
for dpl in dpls:
    dpl.smooth(dpl_smooth_win)
    dpl.scale(dpl_scalefctr)

Finally, we plot the driving spike histogram, empirical and simulated median nerve evoked response waveforms, and output spike histogram.

fig, axes = plt.subplots(3, 1, sharex=True, figsize=(6, 6),
                         constrained_layout=True)
net.cell_response.plot_spikes_hist(ax=axes[0],
                                   spike_types=['evprox', 'evdist'],
                                   show=False)
axes[1].axhline(0, c='k', ls=':', label='_nolegend_')
axes[1].plot(1e3 * stc.times, dipole_tc, 'r--')
average_dipoles(dpls).plot(ax=axes[1], show=False)
axes[1].legend(['MNE label average', 'HNN simulation'])
axes[1].set_ylabel('Current Dipole (nAm)')
net.cell_response.plot_spikes_raster(ax=axes[2])
Aggregate (L2/3 + L5)
<Figure size 600x600 with 3 Axes>

References

Total running time of the script: (0 minutes 47.033 seconds)

Gallery generated by Sphinx-Gallery