Simulating EEG using toy_data function. Is it accurate?

I’m trying to simulate EEG signals using MNE library. I used this code and I’m able to generate EEG data for different channels. But I have a few doubts about this method.

from mne import create_info 
from mne.io import RawArray 
from numpy.random import default_rng


def create_toy_data(n_channels=35, duration=25, sfreq=250, seed=None): 
    rng = default_rng(seed)
    data = rng.standard_normal(size=(n_channels, duration * sfreq)) * 5e-6
    ch_names = [f'EEG {i+1:03d}' for i in range(n_channels)]  # Create channel names like 'EEG 001', 'EEG 002', etc.
    info = create_info(ch_names, sfreq, "eeg")
    return RawArray(data, info)

# Create the toy data
raw = create_toy_data()

# Plot the data with channel names visible
raw.plot(show=True, title='EEG Signal with Channel Names', show_options=True, n_channels=128)

Though this generates the output I need, I want to know if this is accurate? As the documentation suggest other ways like forward model. Some of the questions I have are:

  1. Why are we scaling the value by 5e-6?
  2. Is there are a better way to do this?

This is the output I got for 35 channels.

This is generating random data following a normal distribution. It’s useful to debug a program, but does not mimic any form of brain signals. The method you tried in the post Has anyone tried generating EEG signals using MNE library? where @cbrnr suggested this tutorial Simulate raw data using subject anatomy — MNE 1.8.0 documentation will create fake sensor data by taking into acount:

  • the position of the sources of neural activity
  • the head anatomy
    This information is contained in the forward model (specifically in the source space attached).

This method is more complex, but it does generate a simulated sensor signal, EEG or MEG.

Mathieu

2 Likes

Oh alright. I’ll give that method another try and see if I can generate EEG signals. My issue in that method was that I can’t change the number of electrodes. If you have any idea on that, pls let me know.
But thanks for the help :blush:

The forward model defines the signal transformation from a fix number of sensors to a fix number of sources, taking into account the head geometry. You should be able to define the number of EEG electrodes through this. I haven’t tried myself, but it’s likely something along those lines:

  • Define an info with mne.create_info which contains your EEG channels
  • Set a montage which match your EEG channels positions with info.set_montage()
  • Define a source space within your head geometry
  • Compute the forward model which goes from your sensor to your sources
  • Use the simulation functions

Mathieu

1 Like

Thanks for this method. Using your method, I was able to get some output but there’s no signal. I’m trying to find where the mistake is, still no luck though.

I’ll add the code here with the output. Let me know if you can spot the mistake.

import numpy as np
import mne
from mne.simulation import simulate_raw

# Step 1: Define an info object with specific EEG channels
# Use the standard 10-20 montage channel names
montage = mne.channels.make_standard_montage('standard_1020')
ch_names = montage.ch_names[:32]  # Use the first 32 channels from the montage

sfreq = 250  # Sampling frequency in Hz
ch_types = ['eeg'] * len(ch_names)

# Create the info object
info = mne.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)

# Step 2: Set the montage to the info object
info.set_montage(montage)

# Step 3: Define a source space within the head geometry
# For simplicity, we'll use the 'sample' subject data from MNE
data_path = mne.datasets.sample.data_path()
subjects_dir = data_path / 'subjects'
subject = 'sample'

# Create source space
src = mne.setup_source_space(subject, spacing='oct6', subjects_dir=subjects_dir, add_dist=False)

# Step 4: Compute the forward model
conductivity = (0.3, 0.006, 0.3)  # Conductivities for the head layers
model = mne.make_bem_model(subject=subject, ico=4, conductivity=conductivity, subjects_dir=subjects_dir)
bem = mne.make_bem_solution(model)

# Compute the forward solution (lead field matrix)
fwd = mne.make_forward_solution(info, trans='fsaverage', src=src, bem=bem, eeg=True, meg=False)

# Load or create a label for a specific cortical region
label_name = 'cuneus-lh'  # Example label name
labels = mne.read_labels_from_annot(subject, parc='aparc', subjects_dir=subjects_dir)
label = next((l for l in labels if label_name in l.name), None)

if label is None:
    raise RuntimeError(f"No label found with name '{label_name}'")

# Step 5: Use simulation functions to generate the simulated EEG data

# Set the sampling frequency to match the info object
sfreq = info['sfreq'] # Get the sampling frequency from the info object

# Define an event and create a SourceSimulator
times = np.arange(0, 2, 1.0 / sfreq)  # Simulate 2 seconds of data

# Define source activation function (simple oscillation)
def data_fun(times):
    return np.sin(10 * 2 * np.pi * times)  # 10 Hz sine wave

# Create dummy events (start time 0, id 1, duration 1)
events = np.array([[0, 0, 1]])

# Create a SourceSimulator
source_simulator = mne.simulation.SourceSimulator(src, tstep=1/sfreq)

# Add simulated data to the SourceSimulator
source_simulator.add_data(label, data_fun(times), events)

# Simulate raw data
raw_sim = simulate_raw(info, source_simulator, forward=fwd, verbose=True)
raw_sim.set_eeg_reference(projection=True)

# Step 6: Plot the simulated data
raw_sim.plot(title='Simulated EEG data')

Your raw signals are way too big in amplitude for the viewer at the moment. That is why you see these vertical lines as well. You can check the signal amplitudes in raw_sim and rescale your sinusoid approximately using that information. Note the red bar in the left upper corner of the picture indicating the scale of 40 microvolts.

Hi @SBeumer

I checked the amplitude value for raw_sim and this is what I got.

print("Signal amplitude range:", raw_sim.get_data().max(), raw_sim.get_data().min())
Signal amplitude range: 10276.246976121038 -10276.246976121038

Do you know which part of the code has to be adjusted to keep the scale to 40 microvolts?

Since the forward operation is linear you could change your data_fun to return A*np.sin(…) and the new output will also be scaled by A. MNE expects the unit of EEG signals to be V so you have to rescale quite a lot.

That’s actually not working, I tried several values. But it’s still the same.

def data_fun(times):
    amplitude = 10000  # Amplitude
    return amplitude * np.sin(10 * 2 * np.pi * times)  # 10 Hz sine wave

There’s no change in the output.

-update-
For values like like this is the output:

def data_fun(times):
    amplitude = 0.0000001 
    return amplitude * np.sin(10 * 2 * np.pi * times)  # 10 Hz sine wave

This is only for this value, if I adjust the zero, it goes back to something similar to before. I’m actually confused on what’s happening. It’s nothing compared to an EEG signal.

Well, the output now makes a lot more sense! Remember that the forward model only consists of gains. The EEG is modeled as a linear combination of the source activations. (Approximately : EEG = Gainmatrix * source activations)

Your source activation is currently a sinusoidal wave, which is what you see back on the simulated EEG. You can make the source activity more complex by adding noise or multiple frequencies as well as adding multiple sources.

Oh alright. Thanks for the help @SBeumer and @mscheltienne

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.