OSCR

ION-Sim: A Novel Open-Source Simulation Framework for Intraoperative Neurophysiological Monitoring.

Code ↔ Paper

29 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 29 matches
  1. [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] § 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. [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. [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. [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. [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. [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. [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. [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. [10] § Appendix A › Appendix A.5. CAP Module ↔ N20-P25model.py, lines 35–74 · score 0.62 · Ricker Wavelet, Mexican Hat, Model
  11. [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. [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. [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. [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. [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. [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. [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. [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. [19] § Appendix A › Appendix A.8. ECoG Module ↔ Arx_modelQtEn8.py, lines 1828–1977 · score 0.53 · Circular Buffer, run_simulation_step, Logic
  20. [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. [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. [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. [23] § Appendix B ↔ Virtual_patient/eegAuxIOM4.py, lines 939–983 · score 0.52 · frequency bands, theta, delta, beta, power, alpha
  24. [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. [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. [26] § Appendix B ↔ eegAuxIOM4.py, lines 936–980 · score 0.50 · frequency bands, theta, delta, beta, power, alpha
  27. [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. [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. [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

  1. import datetime
  2. import sys
  3. import json
  4. import logging
  5. import time
  6. from pathlib import Path
  7. import webbrowser as wb
  8. import numpy as np
  9. import pandas as pd
  10. import pyqtgraph as pg
  11. import mne
  12. from mne.time_frequency import RawTFR
  13. import scipy.integrate
  14. from PySide6 import QtCore, QtWidgets
  15. from PySide6.QtCore import Qt, Signal, Slot
  16. from PySide6.QtWidgets import (QApplication, QWidget, QHBoxLayout, QVBoxLayout,
  17. QPushButton, QCheckBox, QLabel, QGroupBox,
  18. QComboBox, QSlider, QGridLayout, QSpinBox, QLCDNumber, QSplitter, QTableWidget,
  19. QHeaderView, QTableWidgetItem
  20. )
  21. from utils_funcIOM import apply_bandpass_filter, apply_simple_smooth, apply_spectral_whitening, analyze_r_peaks
  22. # --- EEG CHANNELS ---
  23. EEG_CHANNELS = ['FP2', 'F4', 'C4', 'P4', 'O2', 'F8', 'T4', 'T6', 'FP1', 'F3', 'C3', 'P3', 'O1', 'F7', 'T3', 'T5']
  24. # --- AUXILIAR CHANNELS ---
  25. AUX_EOG = ['VEOG', 'HEOG']
  26. AUX_ECG = ['EKG']
  27. AUX_EMG = ['MILO', 'MASST']
  28. N_EEG_CHANNELS = len(EEG_CHANNELS)
  29. AUX_CHANNELS = ['EOG', 'ECG']
  30. ALL_PLOT_CHANNELS = EEG_CHANNELS + AUX_CHANNELS
  31. N_ALL_CHANNELS = len(ALL_PLOT_CHANNELS)
  32. FS_EEG = 512
  33. TIME_DURATION_S = 10.0
  34. TIME_POINTS_EEG = int(FS_EEG * TIME_DURATION_S)
  35. TIME_VECTOR_EEG = np.linspace(0, TIME_DURATION_S, TIME_POINTS_EEG, endpoint=False)
  36. def load_eeg_data_mne_cnt(cnt_filepath: str, target_time_vector: np.ndarray, target_fs: float):
  37. """ Carica un file .cnt usando MNE """
  38. print(f"Caricamento da file CNT (Neuroscan) con MNE: {cnt_filepath}")
  39. try:
  40. raw = mne.io.read_raw_cnt(cnt_filepath, preload=True, verbose=False)
  41. events_original, event_id_map = mne.events_from_annotations(raw, verbose=False)
  42. original_sfreq = raw.info['sfreq']
  43. if raw.info['sfreq'] != target_fs:
  44. print(f"Ricampionamento da {raw.info['sfreq']} Hz a {target_fs} Hz...")
  45. raw.resample(target_fs, npad="auto")
  46. n_target_samples = len(target_time_vector)
  47. n_raw_samples = raw.n_times
  48. all_data, times = raw.get_data(return_times=True)
  49. all_data = all_data * -1e6 # Conversione a microvolt
  50. if n_raw_samples >= n_target_samples:
  51. all_data = all_data[:, :n_target_samples]
  52. if original_sfreq != target_fs:
  53. events, event_id_map = mne.events_from_annotations(raw, verbose=False)
  54. else:
  55. events = events_original
  56. events = events[events[:, 0] < n_target_samples]
  57. else:
  58. padding_len = n_target_samples - n_raw_samples
  59. all_data = np.pad(all_data, ((0, 0), (0, padding_len)), 'constant')
  60. events, event_id_map = mne.events_from_annotations(raw, verbose=False)
  61. raw_ch_names = raw.ch_names
  62. data_map = {name.upper(): all_data[i] for i, name in enumerate(raw_ch_names)}
  63. eeg_list = []
  64. for ch_name in EEG_CHANNELS:
  65. key = ch_name.upper()
  66. if key in data_map:
  67. eeg_list.append(data_map[key])
  68. else:
  69. eeg_list.append(np.zeros(n_target_samples))
  70. eeg_data_2d = np.array(eeg_list)
  71. eog_keys = ['EOG', 'VEOG', 'VEO', 'HEO']
  72. ecg_keys = ['ECG', 'EKG']
  73. eog_data = np.zeros(n_target_samples)
  74. for key in eog_keys:
  75. if key in data_map:
  76. eog_data = data_map[key]
  77. break
  78. ecg_data = np.zeros(n_target_samples)
  79. for key in ecg_keys:
  80. if key in data_map:
  81. ecg_data = data_map[key]
  82. break
  83. return {
  84. 'eeg_data_2d': eeg_data_2d,
  85. 'eog_v': eog_data,
  86. 'ecg': ecg_data,
  87. 'template_len': n_target_samples,
  88. 'events': events,
  89. 'event_id_map': event_id_map
  90. }
  91. except Exception as e:
  92. print(f"ERRORE caricamento MNE: {e}")
  93. len_dummy = len(target_time_vector)
  94. return {
  95. 'eeg_data_2d': np.random.normal(0, 1, (len(EEG_CHANNELS), len_dummy)),
  96. 'eog_v': np.random.normal(0, 1, len_dummy),
  97. 'ecg': np.random.normal(0, 1, len_dummy),
  98. 'template_len': len_dummy,
  99. 'events': np.array([]),
  100. 'event_id_map': {}
  101. }
  102. def add_eeg_artifacts_dynamic(eeg_data, time_vector, fs, artifact_state, eog_template=None, ecg_template=None):
  103. n_channels, time_points = eeg_data.shape
  104. out_data = eeg_data.copy()
  105. state_eog = artifact_state.get('EOG_Blink', {'enabled': False, 'amplitude': 0})
  106. if state_eog['enabled'] and eog_template is not None:
  107. factor = state_eog['amplitude'] / 100.0
  108. out_data[0, :] += eog_template * factor
  109. out_data[1, :] += eog_template * factor
  110. state_emg = artifact_state.get('EMG_Noise', {'enabled': False, 'amplitude': 0})
  111. if state_emg['enabled']:
  112. noise = np.random.normal(0, state_emg['amplitude'], (n_channels, time_points))
  113. out_data += noise
  114. return out_data
  115. # --- WORKER CLASS #####################################################################################################
  116. class EEGWorker(QtCore.QObject):
  117. data_ready = Signal(dict)
  118. spectra_ready = Signal(np.ndarray)
  119. def __init__(self, simulator_params, parent=None):
  120. super().__init__(parent)
  121. self.FS = simulator_params['FS']
  122. self.EEG_CHANNELS = simulator_params['eeg_channels']
  123. self.N_EEG_CHANNELS = len(self.EEG_CHANNELS)
  124. self.time = simulator_params['time_vector']
  125. # --- Flag Analisi ECG ---
  126. self.ecg_enabled = False
  127. # --- BUFFER ECG ---
  128. self.ecg_analysis_window_s = 10.0
  129. self.ecg_buffer_size = int(self.ecg_analysis_window_s * self.FS)
  130. self.ecg_buffer = np.zeros(self.ecg_buffer_size)
  131. # --- BUFFER EEG PER SPETTRI ---
  132. self.eeg_buffer = np.zeros((self.N_EEG_CHANNELS, self.ecg_buffer_size))
  133. self.samples_since_last_spectra = 0
  134. self.spectra_interval_samples = int(2.0 * self.FS)
  135. loaded = load_eeg_data_mne_cnt(simulator_params['cnt_filepath'], self.time, self.FS)
  136. self.template_eeg_data = loaded['eeg_data_2d']
  137. self.template_eog_v = loaded['eog_v']
  138. self.template_ecg = loaded['ecg']
  139. self.is_running = False
  140. self.chunk_duration_s = simulator_params['timer_interval_ms'] / 1000.0
  141. self.chunk_samples = int(self.chunk_duration_s * self.FS)
  142. self.current_index = 0
  143. self.current_display_index = 0
  144. self.artifact_state = {
  145. 'EOG_Blink': {'enabled': False, 'amplitude': 50.0},
  146. 'EMG_Noise': {'enabled': False, 'amplitude': 10.0},
  147. 'Slow_Drift': {'enabled': False, 'amplitude': 0.0}
  148. }
  149. @Slot()
  150. def _init_timer(self):
  151. self.timer = QtCore.QTimer()
  152. self.timer.timeout.connect(self._run_simulation_step)
  153. @Slot(int)
  154. def start_simulation(self, interval_ms):
  155. self.is_running = True
  156. self.timer.start(interval_ms)
  157. @Slot()
  158. def stop_simulation(self):
  159. self.is_running = False
  160. self.timer.stop()
  161. @Slot(bool)
  162. def set_ecg_enabled(self, enabled):
  163. """ Attiva o disattiva il calcolo pesante dell'ECG """
  164. self.ecg_enabled = enabled
  165. @Slot(str, bool, float)
  166. def update_artifact_state(self, artifact_key: str, enabled: bool, amplitude: float):
  167. if artifact_key in self.artifact_state:
  168. self.artifact_state[artifact_key]['enabled'] = enabled
  169. self.artifact_state[artifact_key]['amplitude'] = amplitude
  170. @Slot()
  171. def _run_simulation_step(self):
  172. if not self.is_running: return
  173. total_samples = self.template_eeg_data.shape[1]
  174. end_index = self.current_index + self.chunk_samples
  175. if end_index > total_samples:
  176. remaining = end_index - total_samples
  177. chunk_eeg = np.hstack(
  178. (self.template_eeg_data[:, self.current_index:], self.template_eeg_data[:, :remaining]))
  179. chunk_eog = np.hstack((self.template_eog_v[self.current_index:], self.template_eog_v[:remaining]))
  180. chunk_ecg = np.hstack((self.template_ecg[self.current_index:], self.template_ecg[:remaining]))
  181. self.current_index = remaining
  182. else:
  183. chunk_eeg = self.template_eeg_data[:, self.current_index:end_index]
  184. chunk_eog = self.template_eog_v[self.current_index:end_index]
  185. chunk_ecg = self.template_ecg[self.current_index:end_index]
  186. self.current_index = end_index
  187. # --- PROCESSING ---
  188. chunk_eog = chunk_eog * 1e-1
  189. chunk_eeg = apply_bandpass_filter(chunk_eeg, 1.6, 35, self.FS)
  190. chunk_eog = apply_bandpass_filter(chunk_eog, 0.1, 20, self.FS)
  191. average_reference = np.mean(chunk_eeg, axis=0)
  192. chunk_eeg = chunk_eeg - average_reference
  193. chunk_ecg_filt = apply_bandpass_filter(chunk_ecg, 1.0, 25, self.FS) * 0.5
  194. chunk_eeg = add_eeg_artifacts_dynamic(
  195. chunk_eeg, None, self.FS, self.artifact_state,
  196. eog_template=chunk_eog, ecg_template=chunk_ecg_filt
  197. )
  198. # --- GESTIONE BUFFER ---
  199. num_new_samples = chunk_eeg.shape[1]
  200. self.ecg_buffer = np.roll(self.ecg_buffer, -num_new_samples)
  201. self.ecg_buffer[-num_new_samples:] = chunk_ecg
  202. self.eeg_buffer = np.roll(self.eeg_buffer, -num_new_samples, axis=1)
  203. self.eeg_buffer[:, -num_new_samples:] = chunk_eeg
  204. # --- ANALISI ECG CONDIZIONALE ---
  205. if self.ecg_enabled:
  206. qrs_stats = analyze_r_peaks(self.ecg_buffer, self.FS)
  207. else:
  208. qrs_stats = {'peaks': [], 'bpm': 0, 'rr_intervals_ms': [], 'sdnn': 0, 'rmssd': 0}
  209. # --- TRIGGER SPETTRI ---
  210. self.samples_since_last_spectra += num_new_samples
  211. if self.samples_since_last_spectra >= self.spectra_interval_samples:
  212. self.samples_since_last_spectra = 0
  213. self.spectra_ready.emit(self.eeg_buffer.copy())
  214. # --- PREPARAZIONE DATI GUI ---
  215. eeg_multichannel_data = {ch: chunk_eeg[i, :] for i, ch in enumerate(self.EEG_CHANNELS)}
  216. aux_data = {'EOG': chunk_eog, 'ECG': chunk_ecg_filt}
  217. start_disp = self.current_display_index
  218. self.current_display_index += self.chunk_samples
  219. buffer_len = len(self.ecg_buffer)
  220. chunk_start_in_buffer = buffer_len - num_new_samples
  221. peaks_in_current_chunk = []
  222. if self.ecg_enabled:
  223. for p_idx in qrs_stats['peaks']:
  224. if p_idx >= chunk_start_in_buffer:
  225. local_idx = p_idx - chunk_start_in_buffer
  226. peaks_in_current_chunk.append(local_idx)
  227. qrs_stats['peaks'] = peaks_in_current_chunk
  228. self.data_ready.emit({
  229. 'new_data_chunk': eeg_multichannel_data,
  230. 'aux_data_chunk': aux_data,
  231. 'ecg_analysis': qrs_stats,
  232. 'start_display_index': start_disp
  233. })
  234. # --- MAIN WINDOW ---
  235. class EEGControlWindow(QWidget):
  236. simulation_finished = Signal()
  237. def __init__(self, json_anomaly_path=None, learning_manager=None, parent=None):
  238. super().__init__(parent)
  239. self.setWindowFlag(Qt.WindowType.Window)
  240. self.logger = logging.getLogger(__name__)
  241. self.json_anomaly_path = json_anomaly_path
  242. self.learning_manager = learning_manager
  243. self.setWindowTitle(time.strftime('%d %b %Y') + ' EEG - Real Time Processing')
  244. self.resize(1200, 800)
  245. # --- INIZIALIZZAZIONE ---
  246. self.timer_interval_ms = 100
  247. self.is_simulating = False
  248. self.display_mode = "Sweep"
  249. self.current_display_duration_s = 10.0
  250. self.display_samples = int(FS_EEG * self.current_display_duration_s)
  251. self.eeg_display_buffer = np.zeros((N_ALL_CHANNELS, self.display_samples))
  252. self.time_vector_plot = np.linspace(0, self.current_display_duration_s, self.display_samples, endpoint=False)
  253. # Lista per memorizzare i picchi da disegnare
  254. self.ecg_peaks_points = []
  255. self.worker_params = {
  256. 'FS': FS_EEG,
  257. 'time_vector': TIME_VECTOR_EEG,
  258. 'eeg_channels': EEG_CHANNELS,
  259. 'aux_channels': AUX_CHANNELS,
  260. 'cnt_filepath': 'eeg_data/ALBAEGIT.CNT',
  261. 'timer_interval_ms': self.timer_interval_ms
  262. }
  263. self.spectra_window = None
  264. self._init_ui()
  265. self._init_plotting()
  266. self.thread = QtCore.QThread()
  267. self.worker = EEGWorker(self.worker_params)
  268. self.worker.moveToThread(self.thread)
  269. self.worker.data_ready.connect(self.update_eeg_plot)
  270. self.worker.spectra_ready.connect(self.handle_spectra_update)
  271. self.thread.start()
  272. QtCore.QMetaObject.invokeMethod(self.worker, "_init_timer", QtCore.Qt.ConnectionType.QueuedConnection)
  273. self.load_json_anomaly()
  274. def _init_ui(self):
  275. main_layout = QVBoxLayout(self)
  276. top_layout = QHBoxLayout()
  277. sim_group = QGroupBox("Simulazione")
  278. sim_layout = QHBoxLayout()
  279. self.btn_start_stop = QPushButton("Start EEG")
  280. self.btn_start_stop.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 5px;")
  281. self.btn_start_stop.clicked.connect(self.start_stop_simulation)
  282. self.combo_spectra = QComboBox()
  283. self.combo_spectra.addItem('Time frequency')
  284. self.combo_spectra.addItem('Channels PSD')
  285. self.combo_spectra.addItem('Spectra OFF')
  286. self.combo_spectra.currentIndexChanged.connect(self.run_spectra)
  287. self.check_filter = QCheckBox("Filtro Attivo (1.6-45Hz)")
  288. self.check_filter.setChecked(True)
  289. sim_layout.addWidget(self.btn_start_stop)
  290. sim_layout.addWidget(self.combo_spectra)
  291. sim_layout.addWidget(self.check_filter)
  292. sim_group.setLayout(sim_layout)
  293. view_group = QGroupBox("Visualizzazione")
  294. view_layout = QHBoxLayout()
  295. self.combo_mode = QComboBox()
  296. self.combo_mode.addItems(["Sweep (Continuous)", "Page (Paging)"])
  297. self.combo_mode.currentIndexChanged.connect(self.change_display_mode)
  298. lbl_dur = QLabel("Sec/Pagina:")
  299. self.spin_duration = QSpinBox()
  300. self.spin_duration.setRange(2, 60)
  301. self.spin_duration.setValue(int(self.current_display_duration_s))
  302. self.spin_duration.setSuffix(" s")
  303. self.spin_duration.valueChanged.connect(self.update_display_duration)
  304. view_layout.addWidget(QLabel("Modo:"))
  305. view_layout.addWidget(self.combo_mode)
  306. view_layout.addWidget(lbl_dur)
  307. view_layout.addWidget(self.spin_duration)
  308. view_group.setLayout(view_layout)
  309. self.btn_close = QPushButton("Close")
  310. self.btn_close.setStyleSheet("background-color: #f44336; color: blue;")
  311. self.btn_close.clicked.connect(self.close)
  312. self.btn_help = QPushButton("Help")
  313. self.btn_help.setStyleSheet("background-color: #f44336; color: blue;")
  314. self.btn_help.clicked.connect(self.help_context)
  315. top_layout.addWidget(sim_group)
  316. top_layout.addWidget(view_group)
  317. top_layout.addStretch()
  318. top_layout.addWidget(self.btn_help)
  319. top_layout.addWidget(self.btn_close)
  320. main_layout.addLayout(top_layout)
  321. self.gl_widget = pg.GraphicsLayoutWidget()
  322. main_layout.addWidget(self.gl_widget, stretch=2)
  323. art_group = QGroupBox("Generatore Artefatti (Controlli Dinamici)")
  324. art_layout = QHBoxLayout()
  325. self.artifact_controls = {}
  326. artifacts_def = [("EOG_Blink", "EOG Blink"), ("EMG_Noise", "EMG Noise")]
  327. for key, label_text in artifacts_def:
  328. col_layout = QVBoxLayout()
  329. lbl_title = QLabel(label_text)
  330. lbl_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
  331. chk_active = QCheckBox("Attivo")
  332. chk_active.stateChanged.connect(lambda state, k=key: self.handle_artifact_toggle(k, state))
  333. slider = QSlider(Qt.Orientation.Horizontal)
  334. slider.setRange(0, 200)
  335. slider.setValue(50 if key == 'EOG_Blink' else 10)
  336. slider.valueChanged.connect(lambda val, k=key: self.handle_artifact_slider(k, val))
  337. lbl_val = QLabel(f"{slider.value()} uV")
  338. lbl_val.setAlignment(Qt.AlignmentFlag.AlignCenter)
  339. col_layout.addWidget(lbl_title)
  340. col_layout.addWidget(chk_active)
  341. col_layout.addWidget(slider)
  342. col_layout.addWidget(lbl_val)
  343. self.artifact_controls[key] = {'check': chk_active, 'slider': slider, 'label': lbl_val}
  344. container = QWidget()
  345. container.setLayout(col_layout)
  346. container.setStyleSheet("border: 1px solid #ccc; border-radius: 5px; margin: 2px;")
  347. art_layout.addWidget(container)
  348. art_group.setLayout(art_layout)
  349. self.ecg_group = QGroupBox("ECG Analysis (Real-time)")
  350. self.ecg_group.setCheckable(True)
  351. self.ecg_group.setChecked(False)
  352. self.ecg_group.toggled.connect(self.toggle_ecg_analysis)
  353. ecg_layout = QHBoxLayout()
  354. self.lbl_bpm = QLabel("BPM: --")
  355. self.lbl_bpm.setStyleSheet("font-size: 14pt; font-weight: bold; color: #d32f2f;")
  356. self.lbl_sdnn = QLabel("SDNN: -- ms")
  357. self.lbl_rmssd = QLabel("RMSSD: -- ms")
  358. ecg_layout.addWidget(self.lbl_bpm)
  359. ecg_layout.addWidget(self.lbl_sdnn)
  360. ecg_layout.addWidget(self.lbl_rmssd)
  361. ecg_layout.addStretch()
  362. self.ecg_group.setLayout(ecg_layout)
  363. main_layout.addWidget(self.ecg_group)
  364. def _init_plotting(self):
  365. self.gl_widget.setBackground(background='#EEE5D1')
  366. self.eeg_plt = self.gl_widget.addPlot()
  367. self.eeg_plt.setLabel('bottom', "Tempo (s)")
  368. self.eeg_plt.showGrid(x=True, y=False)
  369. # update axis and text colors
  370. pen = pg.mkPen(color=(100, 100, 100))
  371. self.eeg_plt.getAxis('bottom').setPen(pen)
  372. self.eeg_plt.getAxis('bottom').setTextPen(pen)
  373. self.eeg_plt.hideAxis('left')
  374. self.eeg_plt.enableAutoRange(axis='y', enable=False)
  375. self.offset_step = 50.0
  376. total_height = N_ALL_CHANNELS * self.offset_step
  377. self.eeg_plt.setYRange(-self.offset_step, total_height + self.offset_step)
  378. self.eeg_plt.setXRange(0, self.current_display_duration_s)
  379. self.eeg_plots = {}
  380. current_y_pos = (N_ALL_CHANNELS - 1) * self.offset_step
  381. colors = ['#101010' for _ in range(N_ALL_CHANNELS)]
  382. for i, ch_name in enumerate(ALL_PLOT_CHANNELS):
  383. color = colors[i]
  384. pen = pg.mkPen(color='#0000AA', width=1.5) if ch_name in AUX_CHANNELS else pg.mkPen(color=color, width=1)
  385. curve = self.eeg_plt.plot(
  386. self.time_vector_plot,
  387. np.zeros_like(self.time_vector_plot) + current_y_pos,
  388. pen=pen, name=ch_name
  389. )
  390. self.eeg_plots[ch_name] = curve
  391. text_item = pg.TextItem(text=ch_name, color='#333333', anchor=(1, 0.5))
  392. self.eeg_plt.addItem(text_item)
  393. text_item.setPos(0, current_y_pos)
  394. current_y_pos -= self.offset_step
  395. # --- SCATTER PLOT PER I DOT SPOT ---
  396. self.peak_scatter = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 0, 0, 255))
  397. self.eeg_plt.addItem(self.peak_scatter)
  398. def toggle_ecg_analysis(self, state):
  399. """ Attiva/Disattiva l'analisi ECG nel Worker """
  400. QtCore.QMetaObject.invokeMethod(self.worker, "set_ecg_enabled",
  401. QtCore.Qt.ConnectionType.QueuedConnection,
  402. QtCore.Q_ARG(bool, state))
  403. if not state:
  404. self.lbl_bpm.setText("BPM: -- (OFF)")
  405. self.lbl_sdnn.setText("SDNN: --")
  406. self.lbl_rmssd.setText("RMSSD: --")
  407. self.peak_scatter.setData([], []) # Pulisce i punti se spento
  408. self.ecg_peaks_points = []
  409. def change_display_mode(self, index):
  410. if index == 0:
  411. self.display_mode = "Sweep"
  412. else:
  413. self.display_mode = "Page"
  414. self.eeg_display_buffer[:] = 0
  415. self.ecg_peaks_points = [] # Reset punti
  416. self.peak_scatter.setData([], [])
  417. self._refresh_full_plot()
  418. def help_context(self):
  419. help_path = Path('help_files/Tut_spectraECoG.pdf')
  420. if help_path.exists():
  421. wb.open_new(str(help_path))
  422. else:
  423. print(f"Help file not found: {help_path}")
  424. @Slot(np.ndarray)
  425. def handle_spectra_update(self, eeg_buffer_data):
  426. if self.spectra_window and self.spectra_window.isVisible():
  427. self.spectra_window.update_tfr_data(eeg_buffer_data)
  428. def update_display_duration(self, value_seconds):
  429. print(f"Cambio durata visualizzazione a: {value_seconds} s")
  430. self.current_display_duration_s = float(value_seconds)
  431. new_samples = int(FS_EEG * self.current_display_duration_s)
  432. self.display_samples = new_samples
  433. self.eeg_display_buffer = np.zeros((N_ALL_CHANNELS, self.display_samples))
  434. self.time_vector_plot = np.linspace(0, self.current_display_duration_s, self.display_samples, endpoint=False)
  435. self.eeg_plt.setXRange(0, self.current_display_duration_s)
  436. self.ecg_peaks_points = [] # Reset punti al resize
  437. self._refresh_full_plot()
  438. def start_stop_simulation(self):
  439. if not self.is_simulating:
  440. self.is_simulating = True
  441. self.btn_start_stop.setText('Stop EEG')
  442. self.btn_start_stop.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;")
  443. QtCore.QMetaObject.invokeMethod(self.worker, "start_simulation",
  444. QtCore.Qt.ConnectionType.QueuedConnection,
  445. QtCore.Q_ARG(int, self.timer_interval_ms))
  446. else:
  447. self.is_simulating = False
  448. self.btn_start_stop.setText('Start EEG')
  449. self.btn_start_stop.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
  450. QtCore.QMetaObject.invokeMethod(self.worker, "stop_simulation", QtCore.Qt.ConnectionType.QueuedConnection)
  451. def handle_artifact_toggle(self, key, state):
  452. enabled = (state == Qt.CheckState.Checked.value)
  453. ctrl = self.artifact_controls[key]
  454. val = ctrl['slider'].value()
  455. QtCore.QMetaObject.invokeMethod(self.worker, "update_artifact_state",
  456. QtCore.Qt.ConnectionType.QueuedConnection,
  457. QtCore.Q_ARG(str, key),
  458. QtCore.Q_ARG(bool, enabled),
  459. QtCore.Q_ARG(float, val))
  460. def handle_artifact_slider(self, key, value):
  461. ctrl = self.artifact_controls[key]
  462. ctrl['label'].setText(f"{value} uV")
  463. enabled = ctrl['check'].isChecked()
  464. QtCore.QMetaObject.invokeMethod(self.worker, "update_artifact_state",
  465. QtCore.Qt.ConnectionType.QueuedConnection,
  466. QtCore.Q_ARG(str, key),
  467. QtCore.Q_ARG(bool, enabled),
  468. QtCore.Q_ARG(float, value))
  469. # --- METODO RUN_SPECTRA REINSERITO ---
  470. def run_spectra(self):
  471. selected_mode = self.combo_spectra.currentText()
  472. if selected_mode == 'Spectra OFF':
  473. if self.spectra_window:
  474. self.spectra_window.close()
  475. self.spectra_window = None
  476. return
  477. if self.spectra_window is not None:
  478. self.spectra_window.close()
  479. dummy_data = np.zeros((len(EEG_CHANNELS), int(FS_EEG * 10)))
  480. self.spectra_window = SpectraWindow(
  481. eeg_data_buffer=dummy_data,
  482. channels=EEG_CHANNELS,
  483. fs=FS_EEG,
  484. mode=selected_mode
  485. )
  486. self.spectra_window.show()
  487. print(f"Finestra Spettri aperta in modalità: {selected_mode}")
  488. @Slot(dict)
  489. def update_eeg_plot(self, data_packet):
  490. if 'new_data_chunk' not in data_packet: return
  491. eeg_data = data_packet['new_data_chunk']
  492. aux_data = data_packet.get('aux_data_chunk', {})
  493. all_new_data = {**eeg_data, **aux_data}
  494. raw_start_idx = data_packet.get('start_display_index', 0)
  495. chunk_len = list(eeg_data.values())[0].size
  496. start_idx = raw_start_idx % self.display_samples
  497. end_idx = start_idx + chunk_len
  498. # Reset per Page mode o riavvio Sweep
  499. reset_required = False
  500. if start_idx == 0 or (raw_start_idx // self.display_samples) > (
  501. (raw_start_idx - chunk_len) // self.display_samples):
  502. reset_required = True
  503. if reset_required:
  504. if self.display_mode == "Page":
  505. self.eeg_display_buffer[:] = 0
  506. self.ecg_peaks_points = [] # Reset punti all'inizio del ciclo
  507. # Scrittura Buffer
  508. for i, ch_name in enumerate(ALL_PLOT_CHANNELS):
  509. if ch_name not in all_new_data: continue
  510. new_chunk = all_new_data[ch_name]
  511. if end_idx <= self.display_samples:
  512. self.eeg_display_buffer[i, start_idx:end_idx] = new_chunk
  513. else:
  514. part1_len = self.display_samples - start_idx
  515. self.eeg_display_buffer[i, start_idx:] = new_chunk[:part1_len]
  516. part2_len = chunk_len - part1_len
  517. self.eeg_display_buffer[i, :part2_len] = new_chunk[part1_len:]
  518. self._refresh_full_plot()
  519. # --- GESTIONE PUNTI ROSSI (ECG DOT SPOT) ---
  520. if 'ecg_analysis' in data_packet:
  521. stats = data_packet['ecg_analysis']
  522. if self.ecg_group.isChecked():
  523. self.lbl_bpm.setText(f"BPM: {stats['bpm']}")
  524. self.lbl_sdnn.setText(f"SDNN: {stats['sdnn']} ms")
  525. self.lbl_rmssd.setText(f"RMSSD: {stats['rmssd']} ms")
  526. if stats['peaks'] and 'ECG' in ALL_PLOT_CHANNELS:
  527. ecg_ch_idx = ALL_PLOT_CHANNELS.index('ECG')
  528. y_offset = (N_ALL_CHANNELS - 1 - ecg_ch_idx) * self.offset_step
  529. for local_p_idx in stats['peaks']:
  530. # Calcola posizione globale nel buffer circolare
  531. buffer_idx = (start_idx + local_p_idx) % self.display_samples
  532. # Tempo X
  533. x_pos = self.time_vector_plot[buffer_idx]
  534. # Voltaggio Y (esatto dal buffer + offset grafico)
  535. y_val = self.eeg_display_buffer[ecg_ch_idx, buffer_idx]
  536. y_pos = y_val + y_offset
  537. self.ecg_peaks_points.append({'pos': (x_pos, y_pos), 'brush': pg.mkBrush('r')})
  538. # Disegna i punti
  539. self.peak_scatter.setData(self.ecg_peaks_points)
  540. def _refresh_full_plot(self):
  541. current_offset = (N_ALL_CHANNELS - 1) * self.offset_step
  542. for i, ch_name in enumerate(ALL_PLOT_CHANNELS):
  543. offset_data = self.eeg_display_buffer[i, :] + current_offset
  544. self.eeg_plots[ch_name].setData(self.time_vector_plot, offset_data)
  545. current_offset -= self.offset_step
  546. def load_json_anomaly(self):
  547. if self.json_anomaly_path:
  548. self.logger.info(f"Caricamento anomalia: {self.json_anomaly_path}")
  549. def closeEvent(self, event):
  550. if self.is_simulating:
  551. QtCore.QMetaObject.invokeMethod(self.worker, "stop_simulation", QtCore.Qt.ConnectionType.QueuedConnection)
  552. if self.spectra_window:
  553. self.spectra_window.close()
  554. self.thread.quit()
  555. self.thread.wait()
  556. self.worker.deleteLater()
  557. self.simulation_finished.emit()
  558. event.accept()
  559. # --- SPECTRA WINDOW ---
  560. from collections import OrderedDict
  561. import datetime
  562. CHANNEL_R = 'O2'
  563. CHANNEL_L = 'O1'
  564. class SpectraWindow(QWidget):
  565. close_signal = Signal()
  566. def __init__(self, eeg_data_buffer, channels, fs, mode='Time frequency', parent=None):
  567. super().__init__(parent)
  568. self.mode = mode
  569. self.setWindowTitle(f"EEG Analysis - {self.mode}")
  570. self.resize(1400, 500)
  571. self.setWindowFlag(Qt.WindowType.Window)
  572. self.eeg_data_V = eeg_data_buffer * 1e-6
  573. self.channels = channels
  574. self.fs = fs
  575. self.epoch_counter = 0
  576. # Variabili per memorizzare gli ultimi dati PSD calcolati (per il cursore)
  577. self.last_psd_data = None
  578. self.last_freqs = None
  579. # Configurazione Time Frequency
  580. self.tfr_channels = [CHANNEL_R, CHANNEL_L]
  581. self.tfr_indices = {ch: self.channels.index(ch) for ch in self.tfr_channels if ch in self.channels}
  582. if len(self.tfr_indices) < 2 and len(self.channels) >= 2:
  583. self.tfr_channels = [self.channels[0], self.channels[1]]
  584. self.tfr_indices = {self.channels[0]: 0, self.channels[1]: 1}
  585. self.offset_step = 20.0
  586. self.n_time_steps_to_plot_tfr = 5
  587. self.eeg_plots = OrderedDict()
  588. # --- LAYOUT PRINCIPALE ---
  589. main_layout = QVBoxLayout(self)
  590. # 1. TOOLBAR
  591. toolbar_layout = QHBoxLayout()
  592. self.chk_whitening = QCheckBox("Applica Whitening (1/f)")
  593. self.chk_whitening.setChecked(False)
  594. self.chk_whitening.toggled.connect(self.refresh_view)
  595. toolbar_layout.addWidget(self.chk_whitening)
  596. toolbar_layout.addSpacing(20)
  597. self.lbl_cursor = QLabel("Cursore: Sposta la linea verticale per info")
  598. self.lbl_cursor.setStyleSheet("font-weight: bold; color: yellow;")
  599. toolbar_layout.addWidget(self.lbl_cursor)
  600. toolbar_layout.addStretch()
  601. lbl_lcd = QLabel("Epoche:")
  602. lbl_lcd.setStyleSheet("color: #AAA;")
  603. self.lcd_epochs = QLCDNumber()
  604. self.lcd_epochs.setDigitCount(4)
  605. self.lcd_epochs.setSegmentStyle(QLCDNumber.SegmentStyle.Flat)
  606. self.lcd_epochs.setStyleSheet("background-color: #000; color: #00FF00; border: 1px solid #555;")
  607. self.lcd_epochs.display(0)
  608. toolbar_layout.addWidget(lbl_lcd)
  609. toolbar_layout.addWidget(self.lcd_epochs)
  610. main_layout.addLayout(toolbar_layout)
  611. # 2. SPLITTER
  612. self.splitter = QSplitter(Qt.Orientation.Horizontal)
  613. main_layout.addWidget(self.splitter)
  614. # --- SINISTRA: GRAFICO ---
  615. self.plot_widget = pg.PlotWidget()
  616. self.splitter.addWidget(self.plot_widget)
  617. # --- DESTRA: TAB WIDGET ---
  618. self.tabs_analysis = QtWidgets.QTabWidget()
  619. self.tabs_analysis.setFixedWidth(480)
  620. self.splitter.addWidget(self.tabs_analysis)
  621. # Setup Tab 1: Picchi + Cursore
  622. self.tab_peaks = QWidget()
  623. self._setup_tab_peaks()
  624. self.tabs_analysis.addTab(self.tab_peaks, "Cursore & Picchi")
  625. # Setup Tab 2: Bande
  626. self.tab_bands = QWidget()
  627. self._setup_tab_bands()
  628. self.tabs_analysis.addTab(self.tab_bands, "Aree Bande")
  629. # Inizializzazione Grafico
  630. if self.mode == 'Channels PSD':
  631. self._init_psd_raster_plotting()
  632. self._init_cursor() # Inizializza il cursore solo in modalità PSD
  633. else:
  634. self._init_tfr_plotting()
  635. self.splitter.setStretchFactor(0, 3)
  636. self.splitter.setStretchFactor(1, 1)
  637. def _setup_tab_peaks(self):
  638. layout = QVBoxLayout(self.tab_peaks)
  639. self.table_peaks = QTableWidget()
  640. # Aggiunta colonna "Cursor dB"
  641. self.table_peaks.setColumnCount(4)
  642. self.table_peaks.setHorizontalHeaderLabels(["Ch", "Max Freq", "Max dB", "Cursor dB"])
  643. header = self.table_peaks.horizontalHeader()
  644. header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
  645. header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
  646. header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
  647. header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
  648. layout.addWidget(self.table_peaks)
  649. def _setup_tab_bands(self):
  650. layout = QVBoxLayout(self.tab_bands)
  651. self.table_bands = QTableWidget()
  652. self.table_bands.setColumnCount(5)
  653. self.table_bands.setHorizontalHeaderLabels(["Ch", "Delta", "Theta", "Alpha", "Beta"])
  654. header = self.table_bands.horizontalHeader()
  655. header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
  656. for i in range(1, 5):
  657. header.setSectionResizeMode(i, QHeaderView.ResizeMode.Stretch)
  658. layout.addWidget(self.table_bands)
  659. def _init_cursor(self):
  660. # Linea verticale infinita
  661. self.v_line = pg.InfiniteLine(angle=90, movable=True, pen=pg.mkPen('y', width=2, style=Qt.PenStyle.DashLine))
  662. self.plot_widget.addItem(self.v_line)
  663. # Posiziona inizialmente a 10Hz
  664. self.v_line.setPos(10)
  665. # Collega il segnale di movimento
  666. self.v_line.sigPositionChanged.connect(self.on_cursor_moved)
  667. def on_cursor_moved(self):
  668. """ Chiamato quando l'utente sposta la linea verticale """
  669. if self.last_psd_data is None or self.last_freqs is None:
  670. return
  671. # Ottieni la posizione X (Frequenza) corrente della linea
  672. cursor_freq = self.v_line.value()
  673. # Aggiorna label in alto
  674. self.lbl_cursor.setText(f"Cursore a: {cursor_freq:.2f} Hz")
  675. # Calcolo indice o interpolazione
  676. # Per semplicità usiamo interpolazione lineare sui dati dB
  677. psd_db_matrix = 10 * np.log10(self.last_psd_data + 1e-15)
  678. # Aggiorna la colonna "Cursor dB" nella tabella
  679. # Disabilito sorting temporaneo per evitare glitch grafici durante update rapido
  680. self.table_peaks.setSortingEnabled(False)
  681. for i, ch_name in enumerate(self.channels):
  682. # Interpola valore dB alla frequenza del cursore
  683. val_at_cursor = np.interp(cursor_freq, self.last_freqs, psd_db_matrix[i, :])
  684. # Aggiorna la cella (colonna 3)
  685. item = self.table_peaks.item(i, 3)
  686. if item is None:
  687. item = QTableWidgetItem()
  688. self.table_peaks.setItem(i, 3, item)
  689. item.setText(f"{val_at_cursor:.1f}")
  690. # Opzionale: Evidenzia se vicino al picco?
  691. # if abs(val_at_cursor - max_val) < 2: ...
  692. self.table_peaks.setSortingEnabled(True)
  693. def refresh_view(self):
  694. if self.mode == 'Channels PSD':
  695. self._compute_and_plot_psd()
  696. def _init_tfr_plotting(self):
  697. self.plot_widget.clear()
  698. self.eeg_plots.clear()
  699. self.plot_widget.setBackground('black')
  700. self.plot_widget.setLabel('bottom', "Frequency", units='Hz')
  701. self.plot_widget.setLabel('left', "Time Steps", units='')
  702. self.plot_widget.showGrid(x=True, y=False)
  703. # ... (codice TFR esistente)
  704. def _init_psd_raster_plotting(self):
  705. self.plot_widget.clear()
  706. self.eeg_plots.clear()
  707. self.plot_widget.setBackground('#202020')
  708. self.plot_widget.setLabel('bottom', "Frequency", units='Hz')
  709. self.plot_widget.setLabel('left', "Channels", units='')
  710. self.plot_widget.showGrid(x=True, y=True, alpha=0.3)
  711. self.plot_widget.hideAxis('left')
  712. n_channels = len(self.channels)
  713. self.plot_widget.setYRange(-self.offset_step, n_channels * self.offset_step + self.offset_step)
  714. current_y_pos = (n_channels - 1) * self.offset_step
  715. for i, ch_name in enumerate(self.channels):
  716. color_val = int(255 * (i / n_channels))
  717. pen = pg.mkPen(color=(200, 255 - color_val, 255), width=1.5)
  718. curve = self.plot_widget.plot([], [], pen=pen)
  719. text = pg.TextItem(ch_name, anchor=(1, 0.5), color='w')
  720. self.plot_widget.addItem(text)
  721. text.setPos(0, current_y_pos)
  722. self.eeg_plots[ch_name] = {'curve': curve, 'y_offset': current_y_pos, 'text': text}
  723. current_y_pos -= self.offset_step
  724. @Slot(np.ndarray)
  725. def update_tfr_data(self, new_eeg_data_buffer):
  726. self.eeg_data_V = new_eeg_data_buffer * 1e-6
  727. self.epoch_counter += 1
  728. self.lcd_epochs.display(self.epoch_counter)
  729. if self.mode == 'Channels PSD':
  730. self._compute_and_plot_psd()
  731. else:
  732. self._compute_and_plot_tfr()
  733. def _compute_and_plot_psd(self):
  734. try:
  735. ch_types = ['eeg'] * len(self.channels)
  736. info = mne.create_info(ch_names=self.channels, sfreq=self.fs, ch_types=ch_types)
  737. raw_eeg = mne.io.RawArray(self.eeg_data_V, info, verbose=False)
  738. n_fft = min(int(self.fs * 2), raw_eeg.n_times)
  739. spectrum = raw_eeg.compute_psd(method='welch', fmin=1, fmax=45, n_fft=n_fft, verbose=False)
  740. psd_data, freqs = spectrum.get_data(return_freqs=True)
  741. # Memorizza dati per il cursore
  742. self.last_psd_data = psd_data
  743. self.last_freqs = freqs
  744. # --- PLOTTING ---
  745. psd_db = 10 * np.log10(psd_data + 1e-15)
  746. if self.chk_whitening.isChecked():
  747. plot_data = apply_spectral_whitening(psd_db, freqs)
  748. scale_factor = 2.0
  749. else:
  750. plot_data = psd_db
  751. scale_factor = 0.8
  752. db_min = np.percentile(plot_data, 5)
  753. db_max = np.percentile(plot_data, 95)
  754. db_range = db_max - db_min
  755. if db_range == 0: db_range = 1
  756. for i, ch_name in enumerate(self.channels):
  757. if ch_name not in self.eeg_plots: continue
  758. y_vals = plot_data[i, :]
  759. y_norm = (y_vals - db_min) / db_range
  760. y_plot = (y_norm * (self.offset_step * scale_factor)) + self.eeg_plots[ch_name]['y_offset']
  761. y_plot = apply_simple_smooth(y_plot, window_len=7)
  762. self.eeg_plots[ch_name]['curve'].setData(freqs, y_plot)
  763. self.eeg_plots[ch_name]['text'].setPos(freqs[0], self.eeg_plots[ch_name]['y_offset'])
  764. self.plot_widget.setXRange(freqs.min(), freqs.max())
  765. # --- UPDATE TABLES ---
  766. # Aggiorna tabelle con i nuovi dati (Nota: on_cursor_moved aggiornerà la colonna cursore se necessario)
  767. self._update_analysis_tables(psd_data, freqs)
  768. # Se il cursore esiste, forza l'aggiornamento dei valori del cursore sui nuovi dati
  769. if hasattr(self, 'v_line'):
  770. self.on_cursor_moved()
  771. except Exception as e:
  772. print(f"Errore PSD e Analisi: {e}")
  773. def _update_analysis_tables(self, psd_data, freqs):
  774. current_time = datetime.datetime.now().strftime("%H:%M:%S")
  775. self.table_peaks.setRowCount(len(self.channels))
  776. self.table_bands.setRowCount(len(self.channels))
  777. bands_def = {
  778. 'Delta': (1, 4),
  779. 'Theta': (4, 8),
  780. 'Alpha': (8, 13),
  781. 'Beta': (13, 30)
  782. }
  783. # Calcola dB matrix per i picchi
  784. psd_db = 10 * np.log10(psd_data + 1e-15)
  785. for i, ch_name in enumerate(self.channels):
  786. # --- TAB 1: PICCHI ---
  787. # Trova Max Freq
  788. idx_max = np.argmax(psd_db[i, :])
  789. peak_freq = freqs[idx_max]
  790. max_val_db = psd_db[i, idx_max]
  791. self.table_peaks.setItem(i, 0, QTableWidgetItem(ch_name))
  792. self.table_peaks.setItem(i, 1, QTableWidgetItem(f"{peak_freq:.2f}"))
  793. self.table_peaks.setItem(i, 2, QTableWidgetItem(f"{max_val_db:.1f}"))
  794. # La colonna 3 (Cursor) viene gestita da on_cursor_moved, ma la inizializziamo vuota se serve
  795. if self.table_peaks.item(i, 3) is None:
  796. self.table_peaks.setItem(i, 3, QTableWidgetItem("--"))
  797. # --- TAB 2: AREE BANDE ---
  798. self.table_bands.setItem(i, 0, QTableWidgetItem(ch_name))
  799. band_col = 1
  800. for band_name, (low, high) in bands_def.items():
  801. idx_band = np.logical_and(freqs >= low, freqs <= high)
  802. if np.any(idx_band):
  803. freqs_band = freqs[idx_band]
  804. psd_band_linear = psd_data[i, idx_band] # Integriamo la potenza lineare (uV^2/Hz)
  805. band_power = np.trapezoid(psd_band_linear, freqs_band)
  806. else:
  807. band_power = 0.0
  808. self.table_bands.setItem(i, band_col, QTableWidgetItem(f"{band_power:.2e}"))
  809. band_col += 1
  810. def _compute_and_plot_tfr(self):
  811. pass
  812. def closeEvent(self, event):
  813. self.close_signal.emit()
  814. event.accept()
  815. if __name__ == "__main__":
  816. app = QApplication(sys.argv)
  817. window = EEGControlWindow()
  818. window.show()
  819. sys.exit(app.exec())

eegAuxIOM4.py at commit c44abb6, no license · at the source

Overview

Authors: Rosmary Blanco1,2, Riccardo Budai3
ORCID iDs: Riccardo Budai
  1. Sano Centre for Computational Medicine, 30-054 Krakow, Poland
  2. Amsterdam UMC Location University of Amsterdam, 1012 WP Amsterdam, The Netherlands
  3. University Hospital S.M. della Misericordia, 33100 Udine, Italy
Journal: Brain sciences, volume 16, issue 7, article 680
Dates: received 13 April 2026; accepted 24 June 2026; published online 28 June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3390/brainsci16070680 · PMID 42512455 · PMCID PMC13406592 · OpenAlex W7166692677
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), other (modality)
Methods: Evoked potentials, Smoothing, state filtering, decompositions, Physiology & signal measures
Keywords: medical education, simulation, intraoperative neurophysiological monitoring (IONM), computational neuroscience, somatosensory-evoked potentials (SSEP), motor-evoked potentials (MEP), visual-evoked potential (VEP), compound action potential (CAP), cortico-cortical-evoked potentials (CCEP)
Topic: Intraoperative Neuromonitoring and Anesthetic Effects (Surgery, Medicine), according to OpenAlex
Citations: not cited yet (Europe PMC); 40 references in the paper

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

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: c44abb6d4351cd4905c166aa61ae7a62df6e8e56, 21 April 2026
Languages: Python (100)
Size: 1,271 files, 100 scripts
Software Heritage: not archived
Found in: “3. Software Implementation and Availability”
Holds: tests, documentation
Not found: README, license file, CITATION.cff, environment file, continuous integration
Tools: NumPy (69 files), Matplotlib (25 files), MNE-Python (23 files), SciPy (23 files), pandas (8 files), h5py (3 files), PyTorch (3 files), seaborn (3 files), NEURON (2 files), Nilearn (2 files), statsmodels (2 files), NiBabel (1 file), PyWavelets (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
100 files

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/EMG streams and evoked potentials—is managed by the “pyqtgraph” library, which is optimized for fast data plotting.

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://github.com/riccardo-budai/ION_Simula Accessed on 22 May 2026].

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://doi.org/10.3390/brainsci16070680

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/brainsci16070680},
url = {https://doi.org/10.3390/brainsci16070680},
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/06/28
VL - 16
IS - 7
SP - 680
SN - 2076-3425
PB - Multidisciplinary Digital Publishing Institute (MDPI)
DO - 10.3390/brainsci16070680
UR - https://doi.org/10.3390/brainsci16070680
LA - en
ER -

CSL-JSON

{
"id": "10.3390/brainsci16070680",
"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": "Brain Sci",
"volume": "16",
"issue": "7",
"page": "680",
"DOI": "10.3390/brainsci16070680",
"PMID": "42512455",
"PMCID": "PMC13406592",
"ISSN": "2076-3425",
"publisher": "Multidisciplinary Digital Publishing Institute (MDPI)",
"URL": "https://doi.org/10.3390/brainsci16070680",
"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: Neuroradiology
In 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 myelination
Journal: 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 communications
In 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: iScience
In 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 communications
In 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 vision
In 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: eLife
In 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.

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.