ION-Sim: A Novel Open-Source Simulation Framework for Intraoperative Neurophysiological Monitoring.
The 29 matches
- [1] § Appendix A › Appendix A.5. CAP Module ↔ capIOM.py, lines 224–282 · score 0.79 · 10–1500 Hz, 5–3000 Hz, Spline Interpolation, Bandpass filters, Smooth, CAP
- [2] § 2. Materials and Methods › 2.2. Modules and Educational Scenario › 2.2.1. Physiological Modules ↔ Virtual_patient/baepIOM_EN.py, lines 1–36 · score 0.69 · Evoked Potential, acoustic, Condensation, Rarefaction, contralateral, bilateral
- [3] § 2. Materials and Methods › 2.2. Modules and Educational Scenario › 2.2.1. Physiological Modules ↔ baepIOM_EN.py, lines 1–35 · score 0.69 · Evoked Potential, acoustic, Condensation, Rarefaction, contralateral, bilateral
- [4] § Appendix A › Appendix A.8. ECoG Module ↔ utils_funcIOM.py, lines 60–109 · score 0.68 · Fast Fourier Transform, fast_normalized_cross_correlation, FFT
- [5] § Appendix A › Appendix A.6. CMAP Module ↔ cmapIOM.py, lines 199–263 · score 0.67 · tccm_spread_ms, Central Jitter, corticospinal, MUAP, clinical, temporal
- [6] § Appendix A › Appendix A.7. EEG Module ↔ Virtual_patient/eegAuxIOM4.py, lines 143–287 · score 0.66 · slow drifts, bandpass filter, 1.6 Hz, 35 Hz, ECG, 25 Hz
- [7] § Appendix A › Appendix A.7. EEG Module ↔ eegAuxIOM4.py, lines 143–287 · score 0.66 · slow drifts, bandpass filter, 1.6 Hz, 35 Hz, ECG, 25 Hz
- [8] § Appendix A › Appendix A.1. MEP Module ↔ Virtual_patient/client_student_monitorIOM.py, lines 69–176 · score 0.65 · cranial nerves, lower limbs, upper limbs, monitoring, training, MEP
- [9] § Appendix A › Appendix A.6. CMAP Module ↔ simulaMepIOM2.py, lines 138–218 · score 0.65 · Compound Muscle Action, muscle response, motor pathways, Summation, summing, MEP
- [10] § Appendix A › Appendix A.5. CAP Module ↔ N20-P25model.py, lines 35–74 · score 0.62 · Ricker Wavelet, Mexican Hat, Model
- [11] § Appendix A › Appendix A.2. BAEP Module ↔ Virtual_patient/baepIOM_EN.py, lines 784–872 · score 0.60 · 5–6.5 ms, scipy.signal.find_peaks, III, BAEP, windows, latencies
- [12] § 2. Materials and Methods › 2.2. Modules and Educational Scenario › 2.2.1. Physiological Modules ↔ anesthesia_IOM_EN.py, lines 97–139 · score 0.59 · SpO2, RPM, NIBP, HR, vital, height
- [13] § Appendix A › Appendix A.7. EEG Module ↔ Virtual_patient/eegAuxIOM4.py, lines 939–983 · score 0.58 · Power Spectral Density, theta, delta, bands, beta, alpha
- [14] § Appendix A › Appendix A.7. EEG Module ↔ eegAuxIOM4.py, lines 936–980 · score 0.58 · Power Spectral Density, theta, delta, bands, beta, alpha
- [15] § Appendix A › Appendix A.5. CAP Module ↔ capIOM.py, lines 308–431 · score 0.58 · CAPWorker, Compound Action Potential, IOM, graphical, thread, GUI
- [16] § Appendix A › Appendix A.1. MEP Module ↔ dwaveIOM2.py, lines 220–279 · score 0.57 · Train Pulses, Inter Stimulus, ISI, trace, Interval, Artifacts
- [17] § Appendix A › Appendix A.7. EEG Module ↔ Virtual_patient/eegAuxIOM4.py, lines 143–287 · score 0.54 · EOG Blinks, EOG template, artifacts, channels, EEG, simulator
- [18] § Appendix A › Appendix A.7. EEG Module ↔ eegAuxIOM4.py, lines 143–287 · score 0.54 · EOG Blinks, EOG template, artifacts, channels, EEG, simulator
- [19] § Appendix A › Appendix A.8. ECoG Module ↔ Arx_modelQtEn8.py, lines 1828–1977 · score 0.53 · Circular Buffer, run_simulation_step, Logic
- [20] § Appendix A › Appendix A.8. ECoG Module ↔ utilities_extra/Arx_modelQtEn8.py, lines 1828–1977 · score 0.53 · Circular Buffer, run_simulation_step, Logic
- [21] § Appendix A › Appendix A.1. MEP Module ↔ sepIOMmeg.py, lines 642–780 · score 0.53 · AI Tutor, AI Agent, feedback, logged, database, simulation
- [22] § Appendix A › Appendix A.1. MEP Module ↔ vp_tutorIOM2.py, lines 23–140 · score 0.52 · AI Tutor, AI Agent, logged, database, simulation, Module
- [23] § Appendix B ↔ Virtual_patient/eegAuxIOM4.py, lines 939–983 · score 0.52 · frequency bands, theta, delta, beta, power, alpha
- [24] § Appendix A › Appendix A.1. MEP Module ↔ Virtual_patient/mepIOM_EN2.py, lines 788–933 · score 0.51 · facilitation_map, amplitude scale, Pulses, Train, stimulator, MEP
- [25] § Appendix A › Appendix A.1. MEP Module ↔ mepIOM_EN2.py, lines 788–933 · score 0.51 · facilitation_map, amplitude scale, Pulses, Train, stimulator, MEP
- [26] § Appendix B ↔ eegAuxIOM4.py, lines 936–980 · score 0.50 · frequency bands, theta, delta, beta, power, alpha
- [27] § Appendix A › Appendix A.1. MEP Module ↔ simulaMepIOM2.py, lines 138–218 · score 0.50 · Compound Muscle Action, muscle response, waveforms, stimulation, MEP, simulation
- [28] § Appendix A › Appendix A.7. EEG Module ↔ Virtual_patient/eegAuxIOM4.py, lines 45–122 · score 0.50 · mne.io.read_raw_cnt, Neuroscan, EEG
- [29] § Appendix A › Appendix A.7. EEG Module ↔ eegAuxIOM4.py, lines 45–122 · score 0.50 · mne.io.read_raw_cnt, Neuroscan, EEG
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 997 lines · 40 KB · no license · 5 matches
- import datetime
- import sys
- import json
- import logging
- import time
- from pathlib import Path
- import webbrowser as wb
- import numpy as np
- import pandas as pd
- import pyqtgraph as pg
- import mne
- from mne.time_frequency import RawTFR
- import scipy.integrate
- from PySide6 import QtCore, QtWidgets
- from PySide6.QtCore import Qt, Signal, Slot
- from PySide6.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout,
- QPushButton, QCheckBox, QLabel, QGroupBox,
- QComboBox, QSlider, QGridLayout, QSpinBox, QLCDNumber, QSplitter, QTableWidget,
- QHeaderView, QTableWidgetItem
- )
- from utils_funcIOM import apply_bandpass_filter, apply_simple_smooth, apply_spectral_whitening, analyze_r_peaks
- # --- EEG CHANNELS ---
- EEG_CHANNELS = ['FP2', 'F4', 'C4', 'P4', 'O2', 'F8', 'T4', 'T6', 'FP1', 'F3', 'C3', 'P3', 'O1', 'F7', 'T3', 'T5']
- # --- AUXILIAR CHANNELS ---
- AUX_EOG = ['VEOG', 'HEOG']
- AUX_ECG = ['EKG']
- AUX_EMG = ['MILO', 'MASST']
- N_EEG_CHANNELS = len(EEG_CHANNELS)
- AUX_CHANNELS = ['EOG', 'ECG']
- ALL_PLOT_CHANNELS = EEG_CHANNELS + AUX_CHANNELS
- N_ALL_CHANNELS = len(ALL_PLOT_CHANNELS)
- FS_EEG = 512
- TIME_DURATION_S = 10.0
- TIME_POINTS_EEG = int(FS_EEG * TIME_DURATION_S)
- TIME_VECTOR_EEG = np.linspace(0, TIME_DURATION_S, TIME_POINTS_EEG, endpoint=False)
- def load_eeg_data_mne_cnt(cnt_filepath: str, target_time_vector: np.ndarray, target_fs: float):
- """ Carica un file .cnt usando MNE """
- print(f"Caricamento da file CNT (Neuroscan) con MNE: {cnt_filepath}")
- try:
- raw = mne.io.read_raw_cnt(cnt_filepath, preload=True, verbose=False)
- events_original, event_id_map = mne.events_from_annotations(raw, verbose=False)
- original_sfreq = raw.info['sfreq']
- if raw.info['sfreq'] != target_fs:
- print(f"Ricampionamento da {raw.info['sfreq']} Hz a {target_fs} Hz...")
- raw.resample(target_fs, npad="auto")
- n_target_samples = len(target_time_vector)
- n_raw_samples = raw.n_times
- all_data, times = raw.get_data(return_times=True)
- all_data = all_data * -1e6 # Conversione a microvolt
- if n_raw_samples >= n_target_samples:
- all_data = all_data[:, :n_target_samples]
- if original_sfreq != target_fs:
- events, event_id_map = mne.events_from_annotations(raw, verbose=False)
- else:
- events = events_original
- events = events[events[:, 0] < n_target_samples]
- else:
- padding_len = n_target_samples - n_raw_samples
- all_data = np.pad(all_data, ((0, 0), (0, padding_len)), 'constant')
- events, event_id_map = mne.events_from_annotations(raw, verbose=False)
- raw_ch_names = raw.ch_names
- data_map = {name.upper(): all_data[i] for i, name in enumerate(raw_ch_names)}
- eeg_list = []
- for ch_name in EEG_CHANNELS:
- key = ch_name.upper()
- if key in data_map:
- eeg_list.append(data_map[key])
- else:
- eeg_list.append(np.zeros(n_target_samples))
- eeg_data_2d = np.array(eeg_list)
- eog_keys = ['EOG', 'VEOG', 'VEO', 'HEO']
- ecg_keys = ['ECG', 'EKG']
- eog_data = np.zeros(n_target_samples)
- for key in eog_keys:
- if key in data_map:
- eog_data = data_map[key]
- break
- ecg_data = np.zeros(n_target_samples)
- for key in ecg_keys:
- if key in data_map:
- ecg_data = data_map[key]
- break
- return {
- 'eeg_data_2d': eeg_data_2d,
- 'eog_v': eog_data,
- 'ecg': ecg_data,
- 'template_len': n_target_samples,
- 'events': events,
- 'event_id_map': event_id_map
- }
- except Exception as e:
- print(f"ERRORE caricamento MNE: {e}")
- len_dummy = len(target_time_vector)
- return {
- 'eeg_data_2d': np.random.normal(0, 1, (len(EEG_CHANNELS), len_dummy)),
- 'eog_v': np.random.normal(0, 1, len_dummy),
- 'ecg': np.random.normal(0, 1, len_dummy),
- 'template_len': len_dummy,
- 'events': np.array([]),
- 'event_id_map': {}
- }
- def add_eeg_artifacts_dynamic(eeg_data, time_vector, fs, artifact_state, eog_template=None, ecg_template=None):
- n_channels, time_points = eeg_data.shape
- out_data = eeg_data.copy()
- state_eog = artifact_state.get('EOG_Blink', {'enabled': False, 'amplitude': 0})
- if state_eog['enabled'] and eog_template is not None:
- factor = state_eog['amplitude'] / 100.0
- out_data[0, :] += eog_template * factor
- out_data[1, :] += eog_template * factor
- state_emg = artifact_state.get('EMG_Noise', {'enabled': False, 'amplitude': 0})
- if state_emg['enabled']:
- noise = np.random.normal(0, state_emg['amplitude'], (n_channels, time_points))
- out_data += noise
- return out_data
- # --- WORKER CLASS #####################################################################################################
- class EEGWorker(QtCore.QObject):
- data_ready = Signal(dict)
- spectra_ready = Signal(np.ndarray)
- def __init__(self, simulator_params, parent=None):
- super().__init__(parent)
- self.FS = simulator_params['FS']
- self.EEG_CHANNELS = simulator_params['eeg_channels']
- self.N_EEG_CHANNELS = len(self.EEG_CHANNELS)
- self.time = simulator_params['time_vector']
- # --- Flag Analisi ECG ---
- self.ecg_enabled = False
- # --- BUFFER ECG ---
- self.ecg_analysis_window_s = 10.0
- self.ecg_buffer_size = int(self.ecg_analysis_window_s * self.FS)
- self.ecg_buffer = np.zeros(self.ecg_buffer_size)
- # --- BUFFER EEG PER SPETTRI ---
- self.eeg_buffer = np.zeros((self.N_EEG_CHANNELS, self.ecg_buffer_size))
- self.samples_since_last_spectra = 0
- self.spectra_interval_samples = int(2.0 * self.FS)
- loaded = load_eeg_data_mne_cnt(simulator_params['cnt_filepath'], self.time, self.FS)
- self.template_eeg_data = loaded['eeg_data_2d']
- self.template_eog_v = loaded['eog_v']
- self.template_ecg = loaded['ecg']
- self.is_running = False
- self.chunk_duration_s = simulator_params['timer_interval_ms'] / 1000.0
- self.chunk_samples = int(self.chunk_duration_s * self.FS)
- self.current_index = 0
- self.current_display_index = 0
- self.artifact_state = {
- 'EOG_Blink': {'enabled': False, 'amplitude': 50.0},
- 'EMG_Noise': {'enabled': False, 'amplitude': 10.0},
- 'Slow_Drift': {'enabled': False, 'amplitude': 0.0}
- }
- @Slot()
- def _init_timer(self):
- self.timer = QtCore.QTimer()
- self.timer.timeout.connect(self._run_simulation_step)
- @Slot(int)
- def start_simulation(self, interval_ms):
- self.is_running = True
- self.timer.start(interval_ms)
- @Slot()
- def stop_simulation(self):
- self.is_running = False
- self.timer.stop()
- @Slot(bool)
- def set_ecg_enabled(self, enabled):
- """ Attiva o disattiva il calcolo pesante dell'ECG """
- self.ecg_enabled = enabled
- @Slot(str, bool, float)
- def update_artifact_state(self, artifact_key: str, enabled: bool, amplitude: float):
- if artifact_key in self.artifact_state:
- self.artifact_state[artifact_key]['enabled'] = enabled
- self.artifact_state[artifact_key]['amplitude'] = amplitude
- @Slot()
- def _run_simulation_step(self):
- if not self.is_running: return
- total_samples = self.template_eeg_data.shape[1]
- end_index = self.current_index + self.chunk_samples
- if end_index > total_samples:
- remaining = end_index - total_samples
- chunk_eeg = np.hstack(
- (self.template_eeg_data[:, self.current_index:], self.template_eeg_data[:, :remaining]))
- chunk_eog = np.hstack((self.template_eog_v[self.current_index:], self.template_eog_v[:remaining]))
- chunk_ecg = np.hstack((self.template_ecg[self.current_index:], self.template_ecg[:remaining]))
- self.current_index = remaining
- else:
- chunk_eeg = self.template_eeg_data[:, self.current_index:end_index]
- chunk_eog = self.template_eog_v[self.current_index:end_index]
- chunk_ecg = self.template_ecg[self.current_index:end_index]
- self.current_index = end_index
- # --- PROCESSING ---
- chunk_eog = chunk_eog * 1e-1
- chunk_eeg = apply_bandpass_filter(chunk_eeg, 1.6, 35, self.FS)
- chunk_eog = apply_bandpass_filter(chunk_eog, 0.1, 20, self.FS)
- average_reference = np.mean(chunk_eeg, axis=0)
- chunk_eeg = chunk_eeg - average_reference
- chunk_ecg_filt = apply_bandpass_filter(chunk_ecg, 1.0, 25, self.FS) * 0.5
- chunk_eeg = add_eeg_artifacts_dynamic(
- chunk_eeg, None, self.FS, self.artifact_state,
- eog_template=chunk_eog, ecg_template=chunk_ecg_filt
- )
- # --- GESTIONE BUFFER ---
- num_new_samples = chunk_eeg.shape[1]
- self.ecg_buffer = np.roll(self.ecg_buffer, -num_new_samples)
- self.ecg_buffer[-num_new_samples:] = chunk_ecg
- self.eeg_buffer = np.roll(self.eeg_buffer, -num_new_samples, axis=1)
- self.eeg_buffer[:, -num_new_samples:] = chunk_eeg
- # --- ANALISI ECG CONDIZIONALE ---
- if self.ecg_enabled:
- qrs_stats = analyze_r_peaks(self.ecg_buffer, self.FS)
- else:
- qrs_stats = {'peaks': [], 'bpm': 0, 'rr_intervals_ms': [], 'sdnn': 0, 'rmssd': 0}
- # --- TRIGGER SPETTRI ---
- self.samples_since_last_spectra += num_new_samples
- if self.samples_since_last_spectra >= self.spectra_interval_samples:
- self.samples_since_last_spectra = 0
- self.spectra_ready.emit(self.eeg_buffer.copy())
- # --- PREPARAZIONE DATI GUI ---
- eeg_multichannel_data = {ch: chunk_eeg[i, :] for i, ch in enumerate(self.EEG_CHANNELS)}
- aux_data = {'EOG': chunk_eog, 'ECG': chunk_ecg_filt}
- start_disp = self.current_display_index
- self.current_display_index += self.chunk_samples
- buffer_len = len(self.ecg_buffer)
- chunk_start_in_buffer = buffer_len - num_new_samples
- peaks_in_current_chunk = []
- if self.ecg_enabled:
- for p_idx in qrs_stats['peaks']:
- if p_idx >= chunk_start_in_buffer:
- local_idx = p_idx - chunk_start_in_buffer
- peaks_in_current_chunk.append(local_idx)
- qrs_stats['peaks'] = peaks_in_current_chunk
- self.data_ready.emit({
- 'new_data_chunk': eeg_multichannel_data,
- 'aux_data_chunk': aux_data,
- 'ecg_analysis': qrs_stats,
- 'start_display_index': start_disp
- })
- # --- MAIN WINDOW ---
- class EEGControlWindow(QWidget):
- simulation_finished = Signal()
- def __init__(self, json_anomaly_path=None, learning_manager=None, parent=None):
- super().__init__(parent)
- self.setWindowFlag(Qt.WindowType.Window)
- self.logger = logging.getLogger(__name__)
- self.json_anomaly_path = json_anomaly_path
- self.learning_manager = learning_manager
- self.setWindowTitle(time.strftime('%d %b %Y') + ' EEG - Real Time Processing')
- self.resize(1200, 800)
- # --- INIZIALIZZAZIONE ---
- self.timer_interval_ms = 100
- self.is_simulating = False
- self.display_mode = "Sweep"
- self.current_display_duration_s = 10.0
- self.display_samples = int(FS_EEG * self.current_display_duration_s)
- self.eeg_display_buffer = np.zeros((N_ALL_CHANNELS, self.display_samples))
- self.time_vector_plot = np.linspace(0, self.current_display_duration_s, self.display_samples, endpoint=False)
- # Lista per memorizzare i picchi da disegnare
- self.ecg_peaks_points = []
- self.worker_params = {
- 'FS': FS_EEG,
- 'time_vector': TIME_VECTOR_EEG,
- 'eeg_channels': EEG_CHANNELS,
- 'aux_channels': AUX_CHANNELS,
- 'cnt_filepath': 'eeg_data/ALBAEGIT.CNT',
- 'timer_interval_ms': self.timer_interval_ms
- }
- self.spectra_window = None
- self._init_ui()
- self._init_plotting()
- self.thread = QtCore.QThread()
- self.worker = EEGWorker(self.worker_params)
- self.worker.moveToThread(self.thread)
- self.worker.data_ready.connect(self.update_eeg_plot)
- self.worker.spectra_ready.connect(self.handle_spectra_update)
- self.thread.start()
- QtCore.QMetaObject.invokeMethod(self.worker, "_init_timer", QtCore.Qt.ConnectionType.QueuedConnection)
- self.load_json_anomaly()
- def _init_ui(self):
- main_layout = QVBoxLayout(self)
- top_layout = QHBoxLayout()
- sim_group = QGroupBox("Simulazione")
- sim_layout = QHBoxLayout()
- self.btn_start_stop = QPushButton("Start EEG")
- self.btn_start_stop.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 5px;")
- self.btn_start_stop.clicked.connect(self.start_stop_simulation)
- self.combo_spectra = QComboBox()
- self.combo_spectra.addItem('Time frequency')
- self.combo_spectra.addItem('Channels PSD')
- self.combo_spectra.addItem('Spectra OFF')
- self.combo_spectra.currentIndexChanged.connect(self.run_spectra)
- self.check_filter = QCheckBox("Filtro Attivo (1.6-45Hz)")
- self.check_filter.setChecked(True)
- sim_layout.addWidget(self.btn_start_stop)
- sim_layout.addWidget(self.combo_spectra)
- sim_layout.addWidget(self.check_filter)
- sim_group.setLayout(sim_layout)
- view_group = QGroupBox("Visualizzazione")
- view_layout = QHBoxLayout()
- self.combo_mode = QComboBox()
- self.combo_mode.addItems(["Sweep (Continuous)", "Page (Paging)"])
- self.combo_mode.currentIndexChanged.connect(self.change_display_mode)
- lbl_dur = QLabel("Sec/Pagina:")
- self.spin_duration = QSpinBox()
- self.spin_duration.setRange(2, 60)
- self.spin_duration.setValue(int(self.current_display_duration_s))
- self.spin_duration.setSuffix(" s")
- self.spin_duration.valueChanged.connect(self.update_display_duration)
- view_layout.addWidget(QLabel("Modo:"))
- view_layout.addWidget(self.combo_mode)
- view_layout.addWidget(lbl_dur)
- view_layout.addWidget(self.spin_duration)
- view_group.setLayout(view_layout)
- self.btn_close = QPushButton("Close")
- self.btn_close.setStyleSheet("background-color: #f44336; color: blue;")
- self.btn_close.clicked.connect(self.close)
- self.btn_help = QPushButton("Help")
- self.btn_help.setStyleSheet("background-color: #f44336; color: blue;")
- self.btn_help.clicked.connect(self.help_context)
- top_layout.addWidget(sim_group)
- top_layout.addWidget(view_group)
- top_layout.addStretch()
- top_layout.addWidget(self.btn_help)
- top_layout.addWidget(self.btn_close)
- main_layout.addLayout(top_layout)
- self.gl_widget = pg.GraphicsLayoutWidget()
- main_layout.addWidget(self.gl_widget, stretch=2)
- art_group = QGroupBox("Generatore Artefatti (Controlli Dinamici)")
- art_layout = QHBoxLayout()
- self.artifact_controls = {}
- artifacts_def = [("EOG_Blink", "EOG Blink"), ("EMG_Noise", "EMG Noise")]
- for key, label_text in artifacts_def:
- col_layout = QVBoxLayout()
- lbl_title = QLabel(label_text)
- lbl_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
- chk_active = QCheckBox("Attivo")
- chk_active.stateChanged.connect(lambda state, k=key: self.handle_artifact_toggle(k, state))
- slider = QSlider(Qt.Orientation.Horizontal)
- slider.setRange(0, 200)
- slider.setValue(50 if key == 'EOG_Blink' else 10)
- slider.valueChanged.connect(lambda val, k=key: self.handle_artifact_slider(k, val))
- lbl_val = QLabel(f"{slider.value()} uV")
- lbl_val.setAlignment(Qt.AlignmentFlag.AlignCenter)
- col_layout.addWidget(lbl_title)
- col_layout.addWidget(chk_active)
- col_layout.addWidget(slider)
- col_layout.addWidget(lbl_val)
- self.artifact_controls[key] = {'check': chk_active, 'slider': slider, 'label': lbl_val}
- container = QWidget()
- container.setLayout(col_layout)
- container.setStyleSheet("border: 1px solid #ccc; border-radius: 5px; margin: 2px;")
- art_layout.addWidget(container)
- art_group.setLayout(art_layout)
- self.ecg_group = QGroupBox("ECG Analysis (Real-time)")
- self.ecg_group.setCheckable(True)
- self.ecg_group.setChecked(False)
- self.ecg_group.toggled.connect(self.toggle_ecg_analysis)
- ecg_layout = QHBoxLayout()
- self.lbl_bpm = QLabel("BPM: --")
- self.lbl_bpm.setStyleSheet("font-size: 14pt; font-weight: bold; color: #d32f2f;")
- self.lbl_sdnn = QLabel("SDNN: -- ms")
- self.lbl_rmssd = QLabel("RMSSD: -- ms")
- ecg_layout.addWidget(self.lbl_bpm)
- ecg_layout.addWidget(self.lbl_sdnn)
- ecg_layout.addWidget(self.lbl_rmssd)
- ecg_layout.addStretch()
- self.ecg_group.setLayout(ecg_layout)
- main_layout.addWidget(self.ecg_group)
- def _init_plotting(self):
- self.gl_widget.setBackground(background='#EEE5D1')
- self.eeg_plt = self.gl_widget.addPlot()
- self.eeg_plt.setLabel('bottom', "Tempo (s)")
- self.eeg_plt.showGrid(x=True, y=False)
- # update axis and text colors
- pen = pg.mkPen(color=(100, 100, 100))
- self.eeg_plt.getAxis('bottom').setPen(pen)
- self.eeg_plt.getAxis('bottom').setTextPen(pen)
- self.eeg_plt.hideAxis('left')
- self.eeg_plt.enableAutoRange(axis='y', enable=False)
- self.offset_step = 50.0
- total_height = N_ALL_CHANNELS * self.offset_step
- self.eeg_plt.setYRange(-self.offset_step, total_height + self.offset_step)
- self.eeg_plt.setXRange(0, self.current_display_duration_s)
- self.eeg_plots = {}
- current_y_pos = (N_ALL_CHANNELS - 1) * self.offset_step
- colors = ['#101010' for _ in range(N_ALL_CHANNELS)]
- for i, ch_name in enumerate(ALL_PLOT_CHANNELS):
- color = colors[i]
- pen = pg.mkPen(color='#0000AA', width=1.5) if ch_name in AUX_CHANNELS else pg.mkPen(color=color, width=1)
- curve = self.eeg_plt.plot(
- self.time_vector_plot,
- np.zeros_like(self.time_vector_plot) + current_y_pos,
- pen=pen, name=ch_name
- )
- self.eeg_plots[ch_name] = curve
- text_item = pg.TextItem(text=ch_name, color='#333333', anchor=(1, 0.5))
- self.eeg_plt.addItem(text_item)
- text_item.setPos(0, current_y_pos)
- current_y_pos -= self.offset_step
- # --- SCATTER PLOT PER I DOT SPOT ---
- self.peak_scatter = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 0, 0, 255))
- self.eeg_plt.addItem(self.peak_scatter)
- def toggle_ecg_analysis(self, state):
- """ Attiva/Disattiva l'analisi ECG nel Worker """
- QtCore.QMetaObject.invokeMethod(self.worker, "set_ecg_enabled",
- QtCore.Qt.ConnectionType.QueuedConnection,
- QtCore.Q_ARG(bool, state))
- if not state:
- self.lbl_bpm.setText("BPM: -- (OFF)")
- self.lbl_sdnn.setText("SDNN: --")
- self.lbl_rmssd.setText("RMSSD: --")
- self.peak_scatter.setData([], []) # Pulisce i punti se spento
- self.ecg_peaks_points = []
- def change_display_mode(self, index):
- if index == 0:
- self.display_mode = "Sweep"
- else:
- self.display_mode = "Page"
- self.eeg_display_buffer[:] = 0
- self.ecg_peaks_points = [] # Reset punti
- self.peak_scatter.setData([], [])
- self._refresh_full_plot()
- def help_context(self):
- help_path = Path('help_files/Tut_spectraECoG.pdf')
- if help_path.exists():
- wb.open_new(str(help_path))
- else:
- print(f"Help file not found: {help_path}")
- @Slot(np.ndarray)
- def handle_spectra_update(self, eeg_buffer_data):
- if self.spectra_window and self.spectra_window.isVisible():
- self.spectra_window.update_tfr_data(eeg_buffer_data)
- def update_display_duration(self, value_seconds):
- print(f"Cambio durata visualizzazione a: {value_seconds} s")
- self.current_display_duration_s = float(value_seconds)
- new_samples = int(FS_EEG * self.current_display_duration_s)
- self.display_samples = new_samples
- self.eeg_display_buffer = np.zeros((N_ALL_CHANNELS, self.display_samples))
- self.time_vector_plot = np.linspace(0, self.current_display_duration_s, self.display_samples, endpoint=False)
- self.eeg_plt.setXRange(0, self.current_display_duration_s)
- self.ecg_peaks_points = [] # Reset punti al resize
- self._refresh_full_plot()
- def start_stop_simulation(self):
- if not self.is_simulating:
- self.is_simulating = True
- self.btn_start_stop.setText('Stop EEG')
- self.btn_start_stop.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;")
- QtCore.QMetaObject.invokeMethod(self.worker, "start_simulation",
- QtCore.Qt.ConnectionType.QueuedConnection,
- QtCore.Q_ARG(int, self.timer_interval_ms))
- else:
- self.is_simulating = False
- self.btn_start_stop.setText('Start EEG')
- self.btn_start_stop.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
- QtCore.QMetaObject.invokeMethod(self.worker, "stop_simulation", QtCore.Qt.ConnectionType.QueuedConnection)
- def handle_artifact_toggle(self, key, state):
- enabled = (state == Qt.CheckState.Checked.value)
- ctrl = self.artifact_controls[key]
- val = ctrl['slider'].value()
- QtCore.QMetaObject.invokeMethod(self.worker, "update_artifact_state",
- QtCore.Qt.ConnectionType.QueuedConnection,
- QtCore.Q_ARG(str, key),
- QtCore.Q_ARG(bool, enabled),
- QtCore.Q_ARG(float, val))
- def handle_artifact_slider(self, key, value):
- ctrl = self.artifact_controls[key]
- ctrl['label'].setText(f"{value} uV")
- enabled = ctrl['check'].isChecked()
- QtCore.QMetaObject.invokeMethod(self.worker, "update_artifact_state",
- QtCore.Qt.ConnectionType.QueuedConnection,
- QtCore.Q_ARG(str, key),
- QtCore.Q_ARG(bool, enabled),
- QtCore.Q_ARG(float, value))
- # --- METODO RUN_SPECTRA REINSERITO ---
- def run_spectra(self):
- selected_mode = self.combo_spectra.currentText()
- if selected_mode == 'Spectra OFF':
- if self.spectra_window:
- self.spectra_window.close()
- self.spectra_window = None
- return
- if self.spectra_window is not None:
- self.spectra_window.close()
- dummy_data = np.zeros((len(EEG_CHANNELS), int(FS_EEG * 10)))
- self.spectra_window = SpectraWindow(
- eeg_data_buffer=dummy_data,
- channels=EEG_CHANNELS,
- fs=FS_EEG,
- mode=selected_mode
- )
- self.spectra_window.show()
- print(f"Finestra Spettri aperta in modalità: {selected_mode}")
- @Slot(dict)
- def update_eeg_plot(self, data_packet):
- if 'new_data_chunk' not in data_packet: return
- eeg_data = data_packet['new_data_chunk']
- aux_data = data_packet.get('aux_data_chunk', {})
- all_new_data = {**eeg_data, **aux_data}
- raw_start_idx = data_packet.get('start_display_index', 0)
- chunk_len = list(eeg_data.values())[0].size
- start_idx = raw_start_idx % self.display_samples
- end_idx = start_idx + chunk_len
- # Reset per Page mode o riavvio Sweep
- reset_required = False
- if start_idx == 0 or (raw_start_idx // self.display_samples) > (
- (raw_start_idx - chunk_len) // self.display_samples):
- reset_required = True
- if reset_required:
- if self.display_mode == "Page":
- self.eeg_display_buffer[:] = 0
- self.ecg_peaks_points = [] # Reset punti all'inizio del ciclo
- # Scrittura Buffer
- for i, ch_name in enumerate(ALL_PLOT_CHANNELS):
- if ch_name not in all_new_data: continue
- new_chunk = all_new_data[ch_name]
- if end_idx <= self.display_samples:
- self.eeg_display_buffer[i, start_idx:end_idx] = new_chunk
- else:
- part1_len = self.display_samples - start_idx
- self.eeg_display_buffer[i, start_idx:] = new_chunk[:part1_len]
- part2_len = chunk_len - part1_len
- self.eeg_display_buffer[i, :part2_len] = new_chunk[part1_len:]
- self._refresh_full_plot()
- # --- GESTIONE PUNTI ROSSI (ECG DOT SPOT) ---
- if 'ecg_analysis' in data_packet:
- stats = data_packet['ecg_analysis']
- if self.ecg_group.isChecked():
- self.lbl_bpm.setText(f"BPM: {stats['bpm']}")
- self.lbl_sdnn.setText(f"SDNN: {stats['sdnn']} ms")
- self.lbl_rmssd.setText(f"RMSSD: {stats['rmssd']} ms")
- if stats['peaks'] and 'ECG' in ALL_PLOT_CHANNELS:
- ecg_ch_idx = ALL_PLOT_CHANNELS.index('ECG')
- y_offset = (N_ALL_CHANNELS - 1 - ecg_ch_idx) * self.offset_step
- for local_p_idx in stats['peaks']:
- # Calcola posizione globale nel buffer circolare
- buffer_idx = (start_idx + local_p_idx) % self.display_samples
- # Tempo X
- x_pos = self.time_vector_plot[buffer_idx]
- # Voltaggio Y (esatto dal buffer + offset grafico)
- y_val = self.eeg_display_buffer[ecg_ch_idx, buffer_idx]
- y_pos = y_val + y_offset
- self.ecg_peaks_points.append({'pos': (x_pos, y_pos), 'brush': pg.mkBrush('r')})
- # Disegna i punti
- self.peak_scatter.setData(self.ecg_peaks_points)
- def _refresh_full_plot(self):
- current_offset = (N_ALL_CHANNELS - 1) * self.offset_step
- for i, ch_name in enumerate(ALL_PLOT_CHANNELS):
- offset_data = self.eeg_display_buffer[i, :] + current_offset
- self.eeg_plots[ch_name].setData(self.time_vector_plot, offset_data)
- current_offset -= self.offset_step
- def load_json_anomaly(self):
- if self.json_anomaly_path:
- self.logger.info(f"Caricamento anomalia: {self.json_anomaly_path}")
- def closeEvent(self, event):
- if self.is_simulating:
- QtCore.QMetaObject.invokeMethod(self.worker, "stop_simulation", QtCore.Qt.ConnectionType.QueuedConnection)
- if self.spectra_window:
- self.spectra_window.close()
- self.thread.quit()
- self.thread.wait()
- self.worker.deleteLater()
- self.simulation_finished.emit()
- event.accept()
- # --- SPECTRA WINDOW ---
- from collections import OrderedDict
- import datetime
- CHANNEL_R = 'O2'
- CHANNEL_L = 'O1'
- class SpectraWindow(QWidget):
- close_signal = Signal()
- def __init__(self, eeg_data_buffer, channels, fs, mode='Time frequency', parent=None):
- super().__init__(parent)
- self.mode = mode
- self.setWindowTitle(f"EEG Analysis - {self.mode}")
- self.resize(1400, 500)
- self.setWindowFlag(Qt.WindowType.Window)
- self.eeg_data_V = eeg_data_buffer * 1e-6
- self.channels = channels
- self.fs = fs
- self.epoch_counter = 0
- # Variabili per memorizzare gli ultimi dati PSD calcolati (per il cursore)
- self.last_psd_data = None
- self.last_freqs = None
- # Configurazione Time Frequency
- self.tfr_channels = [CHANNEL_R, CHANNEL_L]
- self.tfr_indices = {ch: self.channels.index(ch) for ch in self.tfr_channels if ch in self.channels}
- if len(self.tfr_indices) < 2 and len(self.channels) >= 2:
- self.tfr_channels = [self.channels[0], self.channels[1]]
- self.tfr_indices = {self.channels[0]: 0, self.channels[1]: 1}
- self.offset_step = 20.0
- self.n_time_steps_to_plot_tfr = 5
- self.eeg_plots = OrderedDict()
- # --- LAYOUT PRINCIPALE ---
- main_layout = QVBoxLayout(self)
- # 1. TOOLBAR
- toolbar_layout = QHBoxLayout()
- self.chk_whitening = QCheckBox("Applica Whitening (1/f)")
- self.chk_whitening.setChecked(False)
- self.chk_whitening.toggled.connect(self.refresh_view)
- toolbar_layout.addWidget(self.chk_whitening)
- toolbar_layout.addSpacing(20)
- self.lbl_cursor = QLabel("Cursore: Sposta la linea verticale per info")
- self.lbl_cursor.setStyleSheet("font-weight: bold; color: yellow;")
- toolbar_layout.addWidget(self.lbl_cursor)
- toolbar_layout.addStretch()
- lbl_lcd = QLabel("Epoche:")
- lbl_lcd.setStyleSheet("color: #AAA;")
- self.lcd_epochs = QLCDNumber()
- self.lcd_epochs.setDigitCount(4)
- self.lcd_epochs.setSegmentStyle(QLCDNumber.SegmentStyle.Flat)
- self.lcd_epochs.setStyleSheet("background-color: #000; color: #00FF00; border: 1px solid #555;")
- self.lcd_epochs.display(0)
- toolbar_layout.addWidget(lbl_lcd)
- toolbar_layout.addWidget(self.lcd_epochs)
- main_layout.addLayout(toolbar_layout)
- # 2. SPLITTER
- self.splitter = QSplitter(Qt.Orientation.Horizontal)
- main_layout.addWidget(self.splitter)
- # --- SINISTRA: GRAFICO ---
- self.plot_widget = pg.PlotWidget()
- self.splitter.addWidget(self.plot_widget)
- # --- DESTRA: TAB WIDGET ---
- self.tabs_analysis = QtWidgets.QTabWidget()
- self.tabs_analysis.setFixedWidth(480)
- self.splitter.addWidget(self.tabs_analysis)
- # Setup Tab 1: Picchi + Cursore
- self.tab_peaks = QWidget()
- self._setup_tab_peaks()
- self.tabs_analysis.addTab(self.tab_peaks, "Cursore & Picchi")
- # Setup Tab 2: Bande
- self.tab_bands = QWidget()
- self._setup_tab_bands()
- self.tabs_analysis.addTab(self.tab_bands, "Aree Bande")
- # Inizializzazione Grafico
- if self.mode == 'Channels PSD':
- self._init_psd_raster_plotting()
- self._init_cursor() # Inizializza il cursore solo in modalità PSD
- else:
- self._init_tfr_plotting()
- self.splitter.setStretchFactor(0, 3)
- self.splitter.setStretchFactor(1, 1)
- def _setup_tab_peaks(self):
- layout = QVBoxLayout(self.tab_peaks)
- self.table_peaks = QTableWidget()
- # Aggiunta colonna "Cursor dB"
- self.table_peaks.setColumnCount(4)
- self.table_peaks.setHorizontalHeaderLabels(["Ch", "Max Freq", "Max dB", "Cursor dB"])
- header = self.table_peaks.horizontalHeader()
- header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
- header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
- header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
- header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
- layout.addWidget(self.table_peaks)
- def _setup_tab_bands(self):
- layout = QVBoxLayout(self.tab_bands)
- self.table_bands = QTableWidget()
- self.table_bands.setColumnCount(5)
- self.table_bands.setHorizontalHeaderLabels(["Ch", "Delta", "Theta", "Alpha", "Beta"])
- header = self.table_bands.horizontalHeader()
- header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
- for i in range(1, 5):
- header.setSectionResizeMode(i, QHeaderView.ResizeMode.Stretch)
- layout.addWidget(self.table_bands)
- def _init_cursor(self):
- # Linea verticale infinita
- self.v_line = pg.InfiniteLine(angle=90, movable=True, pen=pg.mkPen('y', width=2, style=Qt.PenStyle.DashLine))
- self.plot_widget.addItem(self.v_line)
- # Posiziona inizialmente a 10Hz
- self.v_line.setPos(10)
- # Collega il segnale di movimento
- self.v_line.sigPositionChanged.connect(self.on_cursor_moved)
- def on_cursor_moved(self):
- """ Chiamato quando l'utente sposta la linea verticale """
- if self.last_psd_data is None or self.last_freqs is None:
- return
- # Ottieni la posizione X (Frequenza) corrente della linea
- cursor_freq = self.v_line.value()
- # Aggiorna label in alto
- self.lbl_cursor.setText(f"Cursore a: {cursor_freq:.2f} Hz")
- # Calcolo indice o interpolazione
- # Per semplicità usiamo interpolazione lineare sui dati dB
- psd_db_matrix = 10 * np.log10(self.last_psd_data + 1e-15)
- # Aggiorna la colonna "Cursor dB" nella tabella
- # Disabilito sorting temporaneo per evitare glitch grafici durante update rapido
- self.table_peaks.setSortingEnabled(False)
- for i, ch_name in enumerate(self.channels):
- # Interpola valore dB alla frequenza del cursore
- val_at_cursor = np.interp(cursor_freq, self.last_freqs, psd_db_matrix[i, :])
- # Aggiorna la cella (colonna 3)
- item = self.table_peaks.item(i, 3)
- if item is None:
- item = QTableWidgetItem()
- self.table_peaks.setItem(i, 3, item)
- item.setText(f"{val_at_cursor:.1f}")
- # Opzionale: Evidenzia se vicino al picco?
- # if abs(val_at_cursor - max_val) < 2: ...
- self.table_peaks.setSortingEnabled(True)
- def refresh_view(self):
- if self.mode == 'Channels PSD':
- self._compute_and_plot_psd()
- def _init_tfr_plotting(self):
- self.plot_widget.clear()
- self.eeg_plots.clear()
- self.plot_widget.setBackground('black')
- self.plot_widget.setLabel('bottom', "Frequency", units='Hz')
- self.plot_widget.setLabel('left', "Time Steps", units='')
- self.plot_widget.showGrid(x=True, y=False)
- # ... (codice TFR esistente)
- def _init_psd_raster_plotting(self):
- self.plot_widget.clear()
- self.eeg_plots.clear()
- self.plot_widget.setBackground('#202020')
- self.plot_widget.setLabel('bottom', "Frequency", units='Hz')
- self.plot_widget.setLabel('left', "Channels", units='')
- self.plot_widget.showGrid(x=True, y=True, alpha=0.3)
- self.plot_widget.hideAxis('left')
- n_channels = len(self.channels)
- self.plot_widget.setYRange(-self.offset_step, n_channels * self.offset_step + self.offset_step)
- current_y_pos = (n_channels - 1) * self.offset_step
- for i, ch_name in enumerate(self.channels):
- color_val = int(255 * (i / n_channels))
- pen = pg.mkPen(color=(200, 255 - color_val, 255), width=1.5)
- curve = self.plot_widget.plot([], [], pen=pen)
- text = pg.TextItem(ch_name, anchor=(1, 0.5), color='w')
- self.plot_widget.addItem(text)
- text.setPos(0, current_y_pos)
- self.eeg_plots[ch_name] = {'curve': curve, 'y_offset': current_y_pos, 'text': text}
- current_y_pos -= self.offset_step
- @Slot(np.ndarray)
- def update_tfr_data(self, new_eeg_data_buffer):
- self.eeg_data_V = new_eeg_data_buffer * 1e-6
- self.epoch_counter += 1
- self.lcd_epochs.display(self.epoch_counter)
- if self.mode == 'Channels PSD':
- self._compute_and_plot_psd()
- else:
- self._compute_and_plot_tfr()
- def _compute_and_plot_psd(self):
- try:
- ch_types = ['eeg'] * len(self.channels)
- info = mne.create_info(ch_names=self.channels, sfreq=self.fs, ch_types=ch_types)
- raw_eeg = mne.io.RawArray(self.eeg_data_V, info, verbose=False)
- n_fft = min(int(self.fs * 2), raw_eeg.n_times)
- spectrum = raw_eeg.compute_psd(method='welch', fmin=1, fmax=45, n_fft=n_fft, verbose=False)
- psd_data, freqs = spectrum.get_data(return_freqs=True)
- # Memorizza dati per il cursore
- self.last_psd_data = psd_data
- self.last_freqs = freqs
- # --- PLOTTING ---
- psd_db = 10 * np.log10(psd_data + 1e-15)
- if self.chk_whitening.isChecked():
- plot_data = apply_spectral_whitening(psd_db, freqs)
- scale_factor = 2.0
- else:
- plot_data = psd_db
- scale_factor = 0.8
- db_min = np.percentile(plot_data, 5)
- db_max = np.percentile(plot_data, 95)
- db_range = db_max - db_min
- if db_range == 0: db_range = 1
- for i, ch_name in enumerate(self.channels):
- if ch_name not in self.eeg_plots: continue
- y_vals = plot_data[i, :]
- y_norm = (y_vals - db_min) / db_range
- y_plot = (y_norm * (self.offset_step * scale_factor)) + self.eeg_plots[ch_name]['y_offset']
- y_plot = apply_simple_smooth(y_plot, window_len=7)
- self.eeg_plots[ch_name]['curve'].setData(freqs, y_plot)
- self.eeg_plots[ch_name]['text'].setPos(freqs[0], self.eeg_plots[ch_name]['y_offset'])
- self.plot_widget.setXRange(freqs.min(), freqs.max())
- # --- UPDATE TABLES ---
- # Aggiorna tabelle con i nuovi dati (Nota: on_cursor_moved aggiornerà la colonna cursore se necessario)
- self._update_analysis_tables(psd_data, freqs)
- # Se il cursore esiste, forza l'aggiornamento dei valori del cursore sui nuovi dati
- if hasattr(self, 'v_line'):
- self.on_cursor_moved()
- except Exception as e:
- print(f"Errore PSD e Analisi: {e}")
- def _update_analysis_tables(self, psd_data, freqs):
- current_time = datetime.datetime.now().strftime("%H:%M:%S")
- self.table_peaks.setRowCount(len(self.channels))
- self.table_bands.setRowCount(len(self.channels))
- bands_def = {
- 'Delta': (1, 4),
- 'Theta': (4, 8),
- 'Alpha': (8, 13),
- 'Beta': (13, 30)
- }
- # Calcola dB matrix per i picchi
- psd_db = 10 * np.log10(psd_data + 1e-15)
- for i, ch_name in enumerate(self.channels):
- # --- TAB 1: PICCHI ---
- # Trova Max Freq
- idx_max = np.argmax(psd_db[i, :])
- peak_freq = freqs[idx_max]
- max_val_db = psd_db[i, idx_max]
- self.table_peaks.setItem(i, 0, QTableWidgetItem(ch_name))
- self.table_peaks.setItem(i, 1, QTableWidgetItem(f"{peak_freq:.2f}"))
- self.table_peaks.setItem(i, 2, QTableWidgetItem(f"{max_val_db:.1f}"))
- # La colonna 3 (Cursor) viene gestita da on_cursor_moved, ma la inizializziamo vuota se serve
- if self.table_peaks.item(i, 3) is None:
- self.table_peaks.setItem(i, 3, QTableWidgetItem("--"))
- # --- TAB 2: AREE BANDE ---
- self.table_bands.setItem(i, 0, QTableWidgetItem(ch_name))
- band_col = 1
- for band_name, (low, high) in bands_def.items():
- idx_band = np.logical_and(freqs >= low, freqs <= high)
- if np.any(idx_band):
- freqs_band = freqs[idx_band]
- psd_band_linear = psd_data[i, idx_band] # Integriamo la potenza lineare (uV^2/Hz)
- band_power = np.trapezoid(psd_band_linear, freqs_band)
- else:
- band_power = 0.0
- self.table_bands.setItem(i, band_col, QTableWidgetItem(f"{band_power:.2e}"))
- band_col += 1
- def _compute_and_plot_tfr(self):
- pass
- def closeEvent(self, event):
- self.close_signal.emit()
- event.accept()
- if __name__ == "__main__":
- app = QApplication(sys.argv)
- window = EEGControlWindow()
- window.show()
- sys.exit(app.exec())
eegAuxIOM4.py at commit c44abb6, no license · at the source
Overview
- Sano Centre for Computational Medicine, 30-054 Krakow, Poland
- Amsterdam UMC Location University of Amsterdam, 1012 WP Amsterdam, The Netherlands
- University Hospital S.M. della Misericordia, 33100 Udine, Italy
Abstract
Highlights: What are the main findings?
This study presents ION-Sim, a novel, open-source framework designed to simulate intraoperative neurophysiological monitoring (IONM) signals and complex clinical scenarios for educational purposes.
The framework integrates multiple physiological modules (including EEG, EMG, MEP, and SEP) with an advanced Learning Manager capable of dynamically injecting targeted clinical anomalies.
ION-Sim is featured with a Tutor modality to interact with surgical scenarios by modifying their characteristics or creating new ones tailored to specific learning goals.
What are the implications of the main findings?
ION-Sim facilitates the supervised learning of intraoperative neurophysiology through a completely hardware-agnostic approach.
By releasing the software under an open-source license (GPLv3), the project democratizes access to high-quality neurophysiological training and facilitates the standardized assessment of skill acquisition across the global clinical community.
Abstract: The educational pathway for expertise in intraoperative neurophysiological monitoring (IONM) is complex and lengthy, requiring a solid foundation in neuroscience, neurophysiology, and neuroanatomy. It also demands direct familiarity with a broad range of neurosurgical scenarios, including supratentorial, infratentorial, and spinal procedures, gained through exposure to at least ten distinct surgical approaches. Intraoperative neurophysiology must be tailored to each patient’s preoperative assessments. It relies on a variety of methods to collect, analyze, and report neurophysiological signals that are relevant to the surgical procedure. Despite its importance, there remains a substantial shortage of training tools designed to support realistic practice and skill development. To address this gap, we developed a comprehensive framework (ION-Sim) that integrates all laboratory testing modalities and adapts them to the operating room environment. ION_sim supports the simulation and analysis of spontaneous EEG and EMG activity, a wide range of evoked potentials, and intraoperative stimulus–response testing protocols. The framework provides a unified environment for practicing, testing, and validating the core neurophysiological procedures employed during neurosurgical interventions. In addition, it incorporates a robust data-management architecture, maintaining a database with system setups, user profiles, educational performance metrics, and automatically generating reports. This structure enables the longitudinal tracking of objective skill acquisition and facilitates standardized assessments of trainee progress. ION_Sim is distributed both as a ready-to-use application, suitable for direct integration into teaching and training programs, and as a modular scientific library. Through its dedicated APIs, users can design customized configurations, create novel simulation scenarios, and extend the platform to support additional research or educational objectives. It is available upon request for educational purposes and is open-source and released under the GNU General Public License, ensuring transparency, reproducibility, and long-term accessibility for the scientific and clinical communities.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 29 matches between paragraphs and lines of code.
riccardo-budai/ION_Simula
c44abb6d4351cd4905c166aa61ae7a62df6e8e56, 21 April 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
100 files
- Arx_modelQtEn8.py, Python, 2,080 lines, 1 match
- BEM_8sources/
DesikanKilliany_Roi.py , Python, 142 lines - BEM_8sources/
bem_8sources_model.py , Python, 145 lines - BEM_8sources/
bem_8sources_sLoreta.py , Python, 596 lines - BEM_8sources/
etc/ , Python, 232 lines8soiurces_3Dmaps3.py - BEM_8sources/
etc/ , Python, 126 lines8sources_2Dmaps.py - BEM_8sources/
etc/ , Python, 146 lines8sources_3Dmaps2.py - BEM_8sources/
read_h5eeg.py , Python, 22 lines - CapModel.py, Python, 125 lines
- CapModel2.py, Python, 99 lines
- ECoGIOM_EN2.py, Python, 1,494 lines
- FiberModel2.py, Python, 117 lines
- FiberModel3D.py, Python, 261 lines
- FiberModelHR.py, Python, 204 lines
- FiberModelMulti.py, Python, 71 lines
- Muse_Stim_test.py, Python, 149 lines
- N20-P25model.py, Python, 282 lines, 1 match
- Virtual_patient/
ECoGIOM3.py , Python, 1,390 lines - Virtual_patient/
anesthesia_IOM_EN.py , Python, 555 lines - Virtual_patient/
anomaly_manager2.py , Python, 188 lines - Virtual_patient/
artifact_genIOM.py , Python, 225 lines - Virtual_patient/
baepIOM_EN.py , Python, 1,148 lines, 2 matches - Virtual_patient/
client_lsl_monitorIOM.py , Python, 369 lines - Virtual_patient/
client_student_monitorIO , Python, 372 lines, 1 matchM.py - Virtual_patient/
eegAuxIOM4.py , Python, 997 lines, 5 matches - Virtual_patient/
emgIOM.py , Python, 676 lines - Virtual_patient/
mepIOM_EN2.py , Python, 978 lines, 1 match - Virtual_patient/
simula_localIOM.py , Python, 85 lines - Virtual_patient/
vepIOM.py , Python, 334 lines - Virtual_patient/
virtual_generatorIOM3.py , Python, 339 lines - ai_tutorIOM_EN.py, Python, 174 lines
- anesthesia_IOM_EN.py, Python, 521 lines, 1 match
- anomaly_manager2.py, Python, 188 lines
- app_helpIOM.py, Python, 76 lines
- artifact_genIOM.py, Python, 225 lines
- baepIOM_EN.py, Python, 1,148 lines, 1 match
- capIOM.py, Python, 881 lines, 2 matches
- ccepIOM.py, Python, 215 lines
- childText.py, Python, 96 lines
- cmapIOM.py, Python, 308 lines, 1 match
- convertWord_Pdf_TOC.py, Python, 140 lines
- data_modelsIOM_sqlt3.py, Python, 90 lines
- database_managerIOM_sqlt
3.py , Python, 466 lines - dbaseIOM_EN_sqlt3.py, Python, 697 lines
- dbase_dialogs_sqlt3.py, Python, 1,025 lines
- dwaveIOM2.py, Python, 1,257 lines, 1 match
- eegAuxIOM4.py, Python, 994 lines, 5 matches
- emgIOM.py, Python, 676 lines
- geminiIOM2.py, Python, 307 lines
- gemini_models.py, Python, 116 lines
- gen_signalAnomalyIOM.py, Python, 260 lines
- generate_signalsIOM.py, Python, 204 lines
- headstage_IOM.py, Python, 190 lines
- helpPdfRB.py, Python, 87 lines
- lauchHelp_IOM.py, Python, 72 lines
- learning_manager.py, Python, 200 lines
- learning_manager2.py, Python, 113 lines
- mainIOM_EN2.py, Python, 720 lines
- mdbCurry/
load_mdb_Curry2.py , Python, 106 lines - mdbCurry/
mdb_CurrySbj.py , Python, 471 lines - mdbCurry/
mesh_viewerIOM.py , Python, 265 lines - mdbCurry/
mri_viewerIOM.py , Python, 133 lines - mepIOM_EN2.py, Python, 978 lines, 1 match
- mep_femIOM2.py, Python, 289 lines
- migrazione_dati.py, Python, 143 lines
- noisyOscIOM.py, Python, 91 lines
- opm_data.py, Python, 154 lines
- paragraph_test.py, Python, 21 lines
- raw_marker_test.py, Python, 151 lines
- sepAiIOMmeg.py, Python, 974 lines
- sepIOMmeg.py, Python, 1,065 lines, 1 match
- sepReportWin.py, Python, 87 lines
- setup_managerIOM5.py, Python, 595 lines
- signalTrakerIOM_EN.py, Python, 215 lines
- simulaJson.py, Python, 55 lines
- simulaMepIOM.py, Python, 245 lines
- simulaMepIOM2.py, Python, 263 lines, 2 matches
- simula_Dipolo.py, Python, 79 lines
- simula_DipoloMonopol.py, Python, 143 lines
- simula_FPcervicale.py, Python, 115 lines
- simula_LOGO.py, Python, 93 lines
- simula_LOGO2.py, Python, 133 lines
- simula_MNE_raw.py, Python, 143 lines
- simula_TextItem.py, Python, 100 lines
- simula_TextItem2.py, Python, 108 lines
- simula_bimodalIOM.py, Python, 259 lines
- simula_capIOM.py, Python, 958 lines
- simula_cortex.py, Python, 191 lines
- simula_managerIOM.py, Python, 85 lines
- simulate_evoked_data.py, Python, 84 lines
- synSigGan_IOM.py, Python, 239 lines
- tutor_GenAI.py, Python, 120 lines
- tutor_ListModels.py, Python, 17 lines
- utilities_extra/
3D_vedo_Anatomy3.py , Python, 427 lines - utilities_extra/
Arx_modelQtEn8.py , Python, 2,080 lines, 1 match - utilities_extra/
signalTrakerIOM_EN.py , Python, 215 lines - utilities_extra/
siibraIFG44.py , Python, 34 lines - utils_funcIOM.py, Python, 195 lines, 1 match
- vepIOM.py, Python, 359 lines
- vp_tutorIOM2.py, Python, 494 lines, 1 match
3. Software Implementation and Availability
The simulation framework is implemented in Python 3.12 and was chosen for its extensive ecosystem of scientific computing libraries and its robust cross-platform compatibility, released as an open-source scientific library hosted on GitHub.com. It exposes a robust set of Application Programming Interfaces (APIs) designed for seamless integration within Python or C++ development environments. The ION-Sim system home screen is shown in Figure A1.
Technology Stack: The architecture leverages a modular stack of specialized libraries.
Computational Backend: NumPy and SciPy constitute the core engine for signal processing, matrix operations, and the mathematical modeling of physiological responses.
Graphical User Interface (GUI): The frontend is developed using PySide6 (the official Python binding for the Qt framework), ensuring a responsive and native look-and-feel across different operating systems.
Real-Time Visualization: High-performance signal rendering—essential for simulating continuous EEG/
User Interface Design: The UI is engineered to streamline user interactions by abstracting the underlying database complexities. By automating session management and pre-loading scenario configurations, the system minimizes the setup burden, allowing the user to focus on the educational task.
Licensing and Availability: This is to foster collaboration and accessibility within the scientific community; the source code is released under the GNU General Public License v3 (GPLv3). The complete repository, including documentation and installation instructions, is publicly hosted on GitHub [https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 100 scripts, each with its path and the digest of its content;
- 29 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data Availability Statement
No new data were created or analyzed in this study. The original contributions presented in this study are included in the article. Further inquiries can be directed to the corresponding author(s).
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 27 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 2 authors, 9 keywords, 6 references.
Cite
This paper
Blanco, R., & Budai, R. (2026). ION-Sim: A Novel Open-Source Simulation Framework for Intraoperative Neurophysiological Monitoring. Brain sciences, 16(7), 680. https://
BibTeX
@article{blanco2026ion,
author = {Blanco, Rosmary and Budai, Riccardo},
title = {{ION-Sim: A Novel Open-Source Simulation Framework for Intraoperative Neurophysiological Monitoring}},
journal = {Brain sciences},
year = {2026},
month = jun,
volume = {16},
number = {7},
pages = {680},
publisher = {Multidisciplinary Digital Publishing Institute (MDPI)},
issn = {2076-3425},
doi = {10.3390/
url = {https://
pmid = {42512455},
pmcid = {PMC13406592}
}
RIS
TY - JOUR
AU - Blanco, Rosmary
AU - Budai, Riccardo
TI - ION-Sim: A Novel Open-Source Simulation Framework for Intraoperative Neurophysiological Monitoring
T2 - Brain sciences
J2 - Brain Sci
PY - 2026
DA - 2026/
VL - 16
IS - 7
SP - 680
SN - 2076-3425
PB - Multidisciplinary Digital Publishing Institute (MDPI)
DO - 10.3390/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.3390/
"type": "article-journal",
"title": "ION-Sim: A Novel Open-Source Simulation Framework for Intraoperative Neurophysiological Monitoring",
"container-title": "Brain sciences",
"author": [
{
"family": "Blanco",
"given": "Rosmary"
},
{
"family": "Budai",
"given": "Riccardo"
}
],
"container-title-short":
"volume": "16",
"issue": "7",
"page": "680",
"DOI": "10.3390/
"PMID": "42512455",
"PMCID": "PMC13406592",
"ISSN": "2076-3425",
"publisher": "Multidisciplinary Digital Publishing Institute (MDPI)",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
28
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1007/s00234-026-04103-8 [code]
- Enhanced detection of subtle cortical abnormalities in focal epilepsy using 7 T MRI surface-based models and graph neural networks.Journal: NeuroradiologyIn common: Nilearn, h5py, statsmodels, 7 other tools, EEG
- [2] doi:10.64898/2026.08.18.26360725 [code]
- Temporal pole blurring in hippocampal sclerosis reflects seizure-disrupted myelinationJournal: medRxiv (preprint)In common: Nilearn, h5py, statsmodels, 7 other tools
- [3] doi:10.1038/s41467-026-76452-0 [code]
- Music evokes shared neural representations of imagined narratives across sensory modalities.Journal: Nature communicationsIn common: Nilearn, h5py, statsmodels, 7 other tools
- [4] doi:10.1016/j.isci.2026.117180 [code]
- Developmental changes in similarity between neural representations of mental arithmetic and artificial neural networks.Journal: iScienceIn common: Nilearn, h5py, statsmodels, 7 other tools
- [5] doi:10.1162/imag.a.1286 [code]
- Behavioral imitation with artificial neural networks leads to personalized models of brain dynamics during videogame play.Journal: Imaging neuroscience (Cambridge, Mass.)In common: Nilearn, h5py, statsmodels, 7 other tools
- [6] doi:10.1038/s41467-026-73996-z [code]
- Genetic architecture of white matter microstructure captured by unsupervised deep representation learning of fractional anisotropy maps.Journal: Nature communicationsIn common: Nilearn, h5py, statsmodels, 7 other tools
- [7] doi:10.1167/jov.26.5.7 [code]
- Representations in vision and language converge in a shared, multidimensional space of perceived similarities.Journal: Journal of visionIn common: Nilearn, h5py, statsmodels, 7 other tools
- [8] doi:10.1002/agm2.70073 [code]
- Foreign Language Learning in Older Adults Modifies Resting-State Functional Connectivity Between the Subcortical Structures and the Cortex.Journal: Aging medicine (Milton (N.S.W))In common: Nilearn, h5py, statsmodels, 7 other tools
- [9] doi:10.7554/elife.107933 [code]
- Modality-agnostic decoding of vision and language from fMRI.Journal: eLifeIn common: Nilearn, h5py, statsmodels, 7 other tools
- [10] doi:10.1162/imag.a.1269 [code]
- From early to contemporary normative modeling: Mapping individual differences in neurophysiological signals.Journal: Imaging neuroscience (Cambridge, Mass.)In common: MNE-Python, Nilearn, statsmodels, 6 other tools, EEG
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 100 scripts, and 29 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:a111f0987573f57d…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
