OSCR

Body surface potential mapping of the cortico-muscular axis using smart textile electrode arrays.

Code ↔ Paper

19 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 19 matches
  1. [1] § Methods › Feature extraction ↔ Wireless_BSPM.ipynb, lines 1290–1370 · score 0.95 · overlapping windows, power spectral density, 0.5–4 Hz, 30–100 Hz, 8–13 Hz, 0.5–100 Hz
  2. [2] § Methods › EEG signal processing and analytical procedures › Spectral decomposition and periodic vs. aperiodic components ↔ Additional_figures.ipynb, lines 560–637 · score 0.93 · peak width limits, 1–100 Hz, 1–12 Hz, peak height, Power spectral densities, aperiodic
  3. [3] § Methods › Preprocessing of recorded electrophysiological signals ↔ Wireless_BSPM.ipynb, lines 1290–1370 · score 0.85 · power spectral density, 8–13 Hz, 13–30 Hz, band power, EEG signals, frequency bands
  4. [4] § Methods › Classification and performance evaluation ↔ Wireless_BSPM.ipynb, lines 288–344 · score 0.83 · Confusion matrices, Logistic regression, F1 score, cross validation, single channel, precision
  5. [5] § Methods › PLS regression (EEG → EMG mapping) ↔ Wireless_BSPM.ipynb, lines 3107–3136 · score 0.80 · PLSRegression, EMG maps, EEG features, RMSE, squares, components
  6. [6] § Methods › EEG signal processing and analytical procedures › Evoked vs. induced activity ↔ Additional_figures.ipynb, lines 825–954 · score 0.80 · flattened ERP, confusion matrices, logistic regression, lbfgs, multinomial, solver
  7. [7] § Results and Discussion › Spatiotemporal features for enhanced classification ↔ Wireless_BSPM.ipynb, lines 1434–1505 · score 0.77 · logistic regression, F1 score, Event related, motor activity, precision, recall
  8. [8] § Methods › Feature importance analysis using correlation circles ↔ Wireless_BSPM.ipynb, lines 399–516 · score 0.76 · correlation circle coordinates, unit circle, standardised feature matrix, PCA, variance, component
  9. [9] § Results and Discussion › Spatiotemporal features for enhanced classification ↔ Wireless_BSPM.ipynb, lines 288–344 · score 0.76 · repeated cross validation, logistic regression, F1 score, single channel, precision, recall
  10. [10] § Methods › EEG signal processing and analytical procedures › Event-Related Desynchronisation/Synchronisation (ERD/ERS) ↔ Additional_figures.ipynb, lines 1030–1148 · score 0.75 · movement onset, 8–12 Hz, 13–30 Hz, Hilbert, ERD, ERS
  11. [11] § Methods › Classification and performance evaluation ↔ Wireless_BSPM.ipynb, lines 3138–3185 · score 0.69 · Spearman rank correlation, Pearson correlation, standard deviation, cosine, predictions
  12. [12] § Methods › Machine learning model descriptions ↔ Wireless_BSPM.ipynb, lines 953–1009 · score 0.69 · repeated stratified, F1 score, cross validation, precision, recall, fold
  13. [13] § Methods › EEG signal processing and analytical procedures › Event-Related Desynchronisation/Synchronisation (ERD/ERS) ↔ Additional_figures.ipynb, lines 1030–1148 · score 0.69 · broadband envelope, ERD, ERS, Gaussian, Epochs, onset
  14. [14] § Methods › Machine learning model training ↔ Wireless_BSPM.ipynb, lines 3138–3185 · score 0.67 · Spearman rank correlation, Pearson correlation, cosine, Predictive
  15. [15] § Methods › Feature importance analysis using correlation circles ↔ Wireless_BSPM.ipynb, lines 399–516 · score 0.62 · correlation circles, feature matrices, PCA, variance, component, SNR
  16. [16] § Results and Discussion › Mapping and prediction of cortico-muscular axis dynamics ↔ Wireless_BSPM.ipynb, lines 2691–2744 · score 0.61 · Gaussian fits, hand flexion, wrist extension, Histograms, Spatio temporal, movement
  17. [17] § Methods › EEG signal processing and analytical procedures › Evoked Response Potentials (ERPs) ↔ Additional_figures.ipynb, lines 439–557 · score 0.60 · Anterior Frontal, SEM, Parietal, Occipital, ERPs, Evoked
  18. [18] § Results and Discussion › Mapping and prediction of cortico-muscular axis dynamics ↔ Wireless_BSPM.ipynb, lines 2804–2809 · score 0.53 · 15–30 Hz, beta band, cortico muscular, components, Event, 15 Hz
  19. [19] § Results and Discussion › Mapping and prediction of cortico-muscular axis dynamics ↔ Wireless_BSPM.ipynb, lines 2691–2744 · score 0.52 · hand flexion, wrist extension, Gaussian, 600 ms, movements, reaction

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

Jupyter notebook · 3,675 lines · 148 KB · no license · 14 matches

  1. # %% [markdown]
  2. # # Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology
  3. # %% [markdown]
  4. # Ruben Ruiz-Mateos Serrano <br>
  5. # Start date : 10/01/2025
  6. # %% [markdown]
  7. # ## BSPM & ML
  8. # %% [markdown]
  9. # ### Test 16 channel BSPM recordings (with INTAN)
  10. # %% [markdown]
  11. # #### Data import
  12. # %%
  13. import os
  14. import re
  15. from biolab.bspm import BSPM,image2layout
  16. from biolab.utils import apply2all
  17. # Save new recordings into .bspm files
  18. # Define parameters
  19. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings'
  20. # Convert electrode array image into layout
  21. ECG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\ECG_lyt.jpg',seq='cw')
  22. EMG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EMG_lyt.jpg',seq='cw')
  23. #EEG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EEG_lyt.jpg',seq='cw')
  24. # Define operation to be applied to each file
  25. def func(filepath):
  26. # Extract filename from filepath
  27. filename = os.path.splitext(os.path.basename(filepath))[0]
  28. # Define the regex pattern to match the required values
  29. pattern = r'^(([^\s]+)_M(\d+))_\d{6}_\d{6}$'
  30. # Search for the pattern in the given filename
  31. match = re.search(pattern,filename)
  32. if match:
  33. filename = match.group(1)
  34. mode = match.group(2) # The type of signal (i.e. ECG, EMG, EEG)
  35. measurement = match.group(3) # The number following 'M'
  36. else:
  37. raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
  38. meta = {
  39. 'mode': mode.split('_')[0],
  40. 'measurement': measurement
  41. }
  42. # Load BSPM data from .rhs file
  43. if mode == 'ECG_BSPM':
  44. MP = BSPM.from_file(filepath=filepath,layout=ECG_lyt,metadata=meta,refchs=[])
  45. elif mode == 'EMG_BSPM':
  46. MP = BSPM.from_file(filepath=filepath,layout=EMG_lyt,metadata=meta,refchs=[])
  47. #elif mode == 'EEG_BSPM':
  48. #MP = BSPM.from_file(filepath=filepath,layout=EEG_lyt,metadata=meta,refchs=[])
  49. MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\\'+filename+'.bspm')
  50. # Call DataSaver function
  51. apply2all(read_directory=readpath,extension='.rhs',func=func)
  52. # %% [markdown]
  53. # #### Electrode array impedance analysis
  54. # %% [markdown]
  55. # Average electrode impedance across all measurements and channels for each signal mode
  56. # %%
  57. import pandas as pd
  58. import matplotlib.pyplot as plt
  59. # Import .bspm files
  60. ECG = BSPM.load(loadpath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_BSPM_M2.bspm')
  61. df_ECG = ECG.channel_data.loc[:,['electrode_impedance_magnitude','custom_order']]
  62. EMG = BSPM.load(loadpath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_BSPM_M2.bspm')
  63. df_EMG = EMG.channel_data.loc[:,['electrode_impedance_magnitude','custom_order']]
  64. # Add a 'mode' column to each DataFrame
  65. df_ECG['mode'] = 'ECG'
  66. df_EMG['mode'] = 'EMG'
  67. # Concatenate the DataFrames
  68. df = pd.concat([df_ECG,df_EMG],ignore_index=True)
  69. # %% [markdown]
  70. # Violin plot of impedance distribution for all modes
  71. # %%
  72. import seaborn as sns
  73. sns.violinplot(x='mode',y='electrode_impedance_magnitude',data=df,cut=0)
  74. plt.title('')
  75. plt.xlabel('Mode')
  76. plt.ylabel('Impedance [$\Omega$]')
  77. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Impedance_distribution_by_mode.pdf')
  78. plt.show()
  79. # %% [markdown]
  80. # #### Body Surface Potential Mapping
  81. # %%
  82. from biolab.bspm import BSPM
  83. # Import .bspm files
  84. ECG = BSPM.load(loadpath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_BSPM_M1.bspm')
  85. EMG = BSPM.load(loadpath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_BSPM_M2.bspm')
  86. # %%
  87. import matplotlib.pyplot as plt
  88. from biolab.bspm import interpolate
  89. vmap = ECG.potential(time=10)
  90. frame = interpolate(valmap=vmap)
  91. plt.imshow(frame,vmin=-2000,vmax=2000)
  92. cbar = plt.colorbar()
  93. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  94. # Remove the axes
  95. plt.axis('off')
  96. # %% [markdown]
  97. # ### Shape classification from muscular BSPM
  98. # %% [markdown]
  99. # #### Data import
  100. # %%
  101. import os
  102. import re
  103. from biolab.bspm import BSPM,image2layout
  104. from biolab.utils import apply2all
  105. # Save new recordings into .bspm files
  106. # Define parameters
  107. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\EMG_classification'
  108. # Convert electrode array image into layout
  109. EMG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EMG_lyt.jpg',seq='cw')
  110. # Define operation to be applied to each file
  111. def func(filepath):
  112. # Extract filename from filepath
  113. filename = os.path.splitext(os.path.basename(filepath))[0]
  114. # Define the regex pattern to match the required values
  115. pattern = r'^([A-Za-z]+)_([A-Za-z]+)_(\d+)_\d{6}_\d{6}$'
  116. # Search for the pattern in the given filename
  117. match = re.search(pattern,filename)
  118. if match:
  119. filename = match.group(1)+'_'+match.group(2)+'_'+match.group(3)
  120. mode = match.group(1) # type of signal (i.e. ECG, EMG, EEG)
  121. shape = match.group(2) # shape of the object
  122. measurement = match.group(3) # measurement
  123. else:
  124. raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
  125. meta = {
  126. 'mode': mode,
  127. 'measurement': measurement,
  128. 'shape': {'ball':'sphere','cylinder':'cylinder','emptyhand':'no object'}.get(shape,None)
  129. }
  130. # Load BSPM data from .rhs file
  131. MP = BSPM.from_file(filepath=filepath,layout=EMG_lyt,metadata=meta,refchs=[])
  132. # Save BSPM object
  133. MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\\'+filename+'.bspm')
  134. # Call DataSaver function
  135. apply2all(read_directory=readpath,extension='.rhs',func=func)
  136. # %% [markdown]
  137. # #### SNR computation
  138. # %%
  139. import pandas as pd
  140. import numpy as np
  141. from biolab.utils import apply2all,snr
  142. from biolab.bspm import BSPM
  143. # Define read directory
  144. rdir = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification'
  145. # Define `apply2all` function
  146. def fun(filepath):
  147. # Load BSPM files
  148. MP = BSPM.load(loadpath=filepath)
  149. # Resample data
  150. newfs = 800
  151. rMP = MP.resample(newfs=newfs)
  152. # Preprocess data
  153. rMP.preprocess(bw=(5,399))
  154. # Read DataFrame with existing data
  155. excelpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\SNR.xlsx'
  156. with pd.ExcelWriter(excelpath,engine='openpyxl',mode='a',if_sheet_exists='overlay') as writer:
  157. existing_data = pd.read_excel(excelpath)
  158. updated_data = existing_data
  159. # Compute SNR for each channel (5s baseline + 5s contraction)
  160. for col in rMP.preprocessed_data.columns:
  161. acc = 0
  162. # Get the slices as lists of absolute values
  163. arrays = [
  164. np.abs(rMP.preprocessed_data[col][0.5:4.5].values),
  165. np.abs(rMP.preprocessed_data[col][10.5:14.5].values),
  166. np.abs(rMP.preprocessed_data[col][20.5:24.5].values),
  167. np.abs(rMP.preprocessed_data[col][30.5:34.5].values),
  168. np.abs(rMP.preprocessed_data[col][40.5:44.5].values),
  169. np.abs(rMP.preprocessed_data[col][50.5:54.5].values),
  170. np.abs(rMP.preprocessed_data[col][60.5:64.5].values),
  171. np.abs(rMP.preprocessed_data[col][70.5:74.5].values),
  172. np.abs(rMP.preprocessed_data[col][80.5:84.5].values),
  173. np.abs(rMP.preprocessed_data[col][90.5:94.5].values)
  174. ]
  175. # Find the minimum length of all arrays
  176. min_length = min(len(arr) for arr in arrays)
  177. # Trim each array to the minimum length
  178. arrays_trimmed = [arr[:min_length] for arr in arrays]
  179. # Compute the mean
  180. baseline = np.mean(arrays_trimmed,axis=0)
  181. while (acc+10)*newfs<len(rMP.preprocessed_data[col].values):
  182. data = np.abs(rMP.preprocessed_data[col][5+acc:10+acc].values)
  183. snrval = snr(s=data,n=baseline)
  184. new_data = {'SNR':snrval,'shape':rMP.metadata['shape'],'measurement':rMP.metadata['measurement'],'envelope':acc/10+1,'channel':col}
  185. updated_data = updated_data.append(new_data,ignore_index=True)
  186. acc = acc+10
  187. # Write the updated DataFrame back to the Excel file, overwriting the old data
  188. updated_data.to_excel(writer,index=False)
  189. apply2all(read_directory=rdir,extension='.bspm',func=fun)
  190. # %% [markdown]
  191. # #### SNR values inspection
  192. # %%
  193. import pandas as pd
  194. import seaborn as sns
  195. import matplotlib.pyplot as plt
  196. # Import SNR data from Excel
  197. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\SNR.xlsx'
  198. df = pd.read_excel(io=readpath)
  199. # Display SNR as a function of shape, channel and measurement
  200. # Create a figure with subplots
  201. fig, axes = plt.subplots(1,3,figsize=(18,6))
  202. # Plot 1: Index by shape
  203. df = df.sort_values(by='shape')
  204. sns.violinplot(x='shape',y='SNR',data=df,order=df['shape'].unique(),ax=axes[0])
  205. axes[0].set_title('')
  206. axes[0].set_xlabel('Shape')
  207. axes[0].set_ylabel('SNR')
  208. # Plot 2: Index by channel
  209. df = df.sort_values(by='channel')
  210. df['channel'] = df['channel'].astype(int)
  211. sns.violinplot(x='channel',y='SNR',data=df,order=df['channel'].unique(),ax=axes[1])
  212. axes[1].set_title('')
  213. axes[1].set_xlabel('Channel')
  214. axes[1].set_ylabel('SNR')
  215. # Plot 3: Index by measurement
  216. df = df.sort_values(by='measurement')
  217. sns.violinplot(x='measurement',y='SNR',data=df,order=df['measurement'].unique(),ax=axes[2])
  218. axes[2].set_title('')
  219. axes[2].set_xlabel('Measurement')
  220. axes[2].set_ylabel('SNR')
  221. # Adjust layout to prevent overlap of titles and labels
  222. plt.tight_layout()
  223. # Display SNR as a function of channel for each shape
  224. # Create a figure with subplots
  225. fig, axes = plt.subplots(1,3,figsize=(18,6))
  226. # Plot 1: Index by shape
  227. df_sphere = df[df['shape']=='sphere'].sort_values(by='channel')
  228. sns.violinplot(x='channel',y='SNR',data=df_sphere,order=df_sphere['channel'].unique(),ax=axes[0])
  229. axes[0].set_title('Sphere')
  230. axes[0].set_xlabel('Channel')
  231. axes[0].set_ylabel('SNR')
  232. # Plot 2: Index by channel
  233. df_cylinder = df[df['shape']=='cylinder'].sort_values(by='channel')
  234. df_cylinder['channel'] = df_cylinder['channel'].astype(int)
  235. sns.violinplot(x='channel',y='SNR',data=df_cylinder,order=df_cylinder['channel'].unique(),ax=axes[1])
  236. axes[1].set_title('Cylinder')
  237. axes[1].set_xlabel('Channel')
  238. axes[1].set_ylabel('SNR')
  239. # Plot 3: Index by measurement
  240. df_noobject = df[df['shape']=='no object'].sort_values(by='channel')
  241. sns.violinplot(x='channel',y='SNR',data=df_noobject,order=df_noobject['channel'].unique(),ax=axes[2])
  242. axes[2].set_title('No object')
  243. axes[2].set_xlabel('Channel')
  244. axes[2].set_ylabel('SNR')
  245. # Adjust layout to prevent overlap of titles and labels
  246. plt.tight_layout()
  247. # %% [markdown]
  248. # *There doesn't seem to be a significant difference in SNR across different shapes. This probably indicates inability for a single electrode to perform accurate classification. When displaying SNR as a function of channel for different shapes, a difference in the distribution of SNR can be observed.*
  249. # %% [markdown]
  250. # #### Machine learning classification of shapes
  251. # %% [markdown]
  252. # Single electrode (control - middle electrode)
  253. # %%
  254. import pandas as pd
  255. from sklearn.model_selection import train_test_split
  256. from sklearn.linear_model import LogisticRegression
  257. from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
  258. from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, make_scorer,precision_score,recall_score,f1_score
  259. import matplotlib.pyplot as plt
  260. # Import SNR data from Excel
  261. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\EMG_classification\SNR.xlsx'
  262. df = pd.read_excel(io=readpath)
  263. # Generate feature matrix from data
  264. fmatrix = df[df['channel']==9].drop(['measurement','envelope','channel'],axis=1)
  265. # Separate features and labels
  266. X = fmatrix.drop('shape',axis=1) # feature matrix
  267. y = fmatrix['shape'] # labels
  268. # Split the data into training and testing sets
  269. X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['shape'])
  270. # Initialise and fit the logistic regression
  271. clf = LogisticRegression()
  272. clf.fit(X_train,y_train)
  273. # Predict the labels for the test set
  274. y_pred = clf.predict(X_test)
  275. # Calculate the confusion matrix
  276. cm = confusion_matrix(y_test,y_pred)
  277. # Display the confusion matrix
  278. disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=clf.classes_)
  279. fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
  280. # Set the color range limits
  281. vmin,vmax = 0,20 # Adjust these values as needed
  282. disp.plot(cmap="Reds",ax=ax,colorbar=False,values_format="d")
  283. disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
  284. # Adjust the colorbar size
  285. cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
  286. cbar.set_label("Count") # Label for better readability
  287. #plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Results\Confusion_matrices\Single_channel_EMG',dpi=1000)
  288. # Repeated Cross-Validation Accuracy
  289. rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
  290. accuracy_scores = cross_val_score(clf,X,y,cv=rkf)
  291. precision_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(precision_score, average='macro'))
  292. recall_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(recall_score, average='macro'))
  293. f1_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(f1_score, average='macro'))
  294. # Print results
  295. print(f'Repeated Cross-Validation Accuracy: {accuracy_scores.mean():.2f} ± {accuracy_scores.std():.2f}')
  296. print(f"Repeated Cross-Validation Precision: {precision_scores.mean():.2f} ± {precision_scores.std():.2f}")
  297. print(f"Repeated Cross-Validation Recall: {recall_scores.mean():.2f} ± {recall_scores.std():.2f}")
  298. print(f"Repeated Cross-Validation F1 Score: {f1_scores.mean():.2f} ± {f1_scores.std():.2f}")
  299. # %% [markdown]
  300. # Electrode array
  301. # %%
  302. import pandas as pd
  303. from sklearn.model_selection import train_test_split
  304. from sklearn.linear_model import LogisticRegression
  305. from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
  306. from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
  307. import matplotlib.pyplot as plt
  308. # Import SNR data from Excel
  309. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\EMG_classification\SNR.xlsx'
  310. df = pd.read_excel(io=readpath)
  311. # Generate feature matrix from data
  312. # Pivot the DataFrame so each channel becomes its own column
  313. fmatrix = df.pivot(index=['shape','envelope','measurement'],columns='channel',values='SNR')
  314. # Rename the columns to match the desired format
  315. fmatrix.columns = [f'snr_channel_{col}' for col in fmatrix.columns]
  316. # Reset index for a tidy DataFrame
  317. fmatrix.reset_index(inplace=True)
  318. # Remove unnecessary columns
  319. fmatrix = fmatrix.drop(columns=['envelope','measurement'])
  320. # Separate features and labels
  321. X = fmatrix.drop('shape',axis=1) # feature matrix
  322. y = fmatrix['shape'] # labels
  323. # Split the data into training and testing sets
  324. X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['shape'])
  325. # Initialise and fit the logistic regression
  326. clf = LogisticRegression()
  327. clf.fit(X_train,y_train)
  328. # Predict the labels for the test set
  329. y_pred = clf.predict(X_test)
  330. # Calculate the confusion matrix
  331. cm = confusion_matrix(y_test,y_pred)
  332. # Display the confusion matrix
  333. disp = ConfusionMatrixDisplay(confusion_matrix=cm,display_labels=clf.classes_)
  334. fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
  335. # Set the color range limits
  336. vmin,vmax = 0,20 # Adjust these values as needed
  337. disp.plot(cmap="Reds",ax=ax,colorbar=False,values_format="d")
  338. disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
  339. # Adjust the colorbar size
  340. cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
  341. cbar.set_label("Count") # Label for better readability
  342. #plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Results\Confusion_matrices\BSPM_EMG',dpi=1000)
  343. # Repeated Cross-Validation Accuracy
  344. rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
  345. scores = cross_val_score(clf,X,y,cv=rkf)
  346. print(f'Repeated Cross-Validation Accuracy: {scores.mean():.2f} ± {scores.std():.2f}')
  347. # %% [markdown]
  348. # Correlation circle for feature importance
  349. # %%
  350. import pandas as pd
  351. import numpy as np
  352. import matplotlib.pyplot as plt
  353. import matplotlib.cm as cm
  354. from sklearn.preprocessing import StandardScaler
  355. from sklearn.decomposition import PCA
  356. # ------------------------------------------------
  357. # Load data
  358. # ------------------------------------------------
  359. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\EMG_classification\SNR.xlsx'
  360. df = pd.read_excel(io=readpath)
  361. # ------------------------------------------------
  362. # Build feature matrix (same as your ML pipeline)
  363. # ------------------------------------------------
  364. fmatrix = df.pivot(
  365. index=['shape','envelope','measurement'],
  366. columns='channel',
  367. values='SNR'
  368. )
  369. # Rename columns
  370. fmatrix.columns = [f'snr_channel_{col}' for col in fmatrix.columns]
  371. # Reset index
  372. fmatrix.reset_index(inplace=True)
  373. # Remove unnecessary columns
  374. fmatrix = fmatrix.drop(columns=['envelope','measurement'])
  375. # Separate features and labels
  376. X = fmatrix.drop('shape', axis=1)
  377. y = fmatrix['shape']
  378. # ------------------------------------------------
  379. # Standardise features (IMPORTANT for PCA)
  380. # ------------------------------------------------
  381. scaler = StandardScaler()
  382. X_scaled = scaler.fit_transform(X)
  383. # ------------------------------------------------
  384. # PCA
  385. # ------------------------------------------------
  386. pca = PCA(n_components=2)
  387. X_pca = pca.fit_transform(X_scaled)
  388. # Compute loadings (correlation circle coordinates)
  389. loadings = pca.components_.T * np.sqrt(pca.explained_variance_)
  390. # ------------------------------------------------
  391. # Plot correlation circle
  392. # ------------------------------------------------
  393. fig, ax = plt.subplots(figsize=(8,8), constrained_layout=True)
  394. # Draw unit circle
  395. circle = plt.Circle((0,0),1,fill=False,linestyle='--', color='black')
  396. ax.add_artist(circle)
  397. # Create colour mapping by channel index
  398. num_features = len(X.columns)
  399. colors = cm.viridis(np.linspace(0,1,num_features))
  400. channel_numbers = [int(col.split('_')[-1]) for col in X.columns]
  401. # Plot arrows (THICKER)
  402. for i, feature in enumerate(X.columns):
  403. ax.arrow(
  404. 0, 0,
  405. loadings[i,0],
  406. loadings[i,1],
  407. color=colors[i],
  408. linewidth=2.0, # <-- thicker arrows
  409. head_width=0.03, # slightly larger arrowhead
  410. length_includes_head=True
  411. )
  412. # Add colorbar
  413. norm = plt.Normalize(min(channel_numbers), max(channel_numbers))
  414. sm = plt.cm.ScalarMappable(cmap='viridis', norm=norm)
  415. sm.set_array([])
  416. cbar = fig.colorbar(sm, ax=ax)
  417. cbar.set_label('Channel number')
  418. # Axis formatting (BLACK axes)
  419. ax.axhline(0, color='black', linewidth=1.5)
  420. ax.axvline(0, color='black', linewidth=1.5)
  421. # Make frame/spines black
  422. for spine in ax.spines.values():
  423. spine.set_color('black')
  424. spine.set_linewidth(1.5)
  425. # Tick colors
  426. ax.tick_params(colors='black')
  427. ax.set_xlim(-1.1,1.1)
  428. ax.set_ylim(-1.1,1.1)
  429. ax.set_aspect('equal')
  430. ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]*100:.1f}% variance)')
  431. ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]*100:.1f}% variance)')
  432. ax.set_title('Correlation Circle (Muscular BSPM)')
  433. plt.show()
  434. # %% [markdown]
  435. # #### Muscular BSPM
  436. # %%
  437. import matplotlib.pyplot as plt
  438. import numpy as np
  439. from biolab.bspm import interpolate,BSPM
  440. # Read .bspm data for sphere
  441. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\EMG_ball_1.bspm'
  442. EMG = BSPM.load(loadpath=readpath)
  443. rEMG_sphere = EMG.resample(newfs=800)
  444. # Display muscular BSPM
  445. vmap = rEMG_sphere.potential(time=78.70023333333333)
  446. abmap = np.abs(vmap)
  447. frame = interpolate(valmap=abmap,method='cubic')
  448. plt.imshow(frame,vmin=0,vmax=1500,cmap='viridis')
  449. cbar = plt.colorbar()
  450. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  451. # Remove the axes
  452. plt.axis('off')
  453. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_3\Sphere_viridis.pdf',dpi=1000)
  454. plt.show()
  455. # Read .bspm data for cylinder
  456. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\EMG_cylinder_1.bspm'
  457. EMG = BSPM.load(loadpath=readpath)
  458. rEMG_cylinder = EMG.resample(newfs=800)
  459. # Display muscular BSPM
  460. vmap = rEMG_cylinder.potential(time=50.11526666666666)
  461. abmap = np.abs(vmap)
  462. frame = interpolate(valmap=abmap,method='cubic')
  463. plt.imshow(frame,vmin=0,vmax=1500,cmap='viridis')
  464. cbar = plt.colorbar()
  465. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  466. # Remove the axes
  467. plt.axis('off')
  468. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_3\Cylinder_viridis.pdf',dpi=1000)
  469. plt.show()
  470. # Read .bspm data for no object
  471. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EMG_classification\EMG_emptyhand_1.bspm'
  472. EMG = BSPM.load(loadpath=readpath)
  473. rEMG_noobject = EMG.resample(newfs=800)
  474. # Display muscular BSPM
  475. vmap = rEMG_noobject.potential(time=35.5237)
  476. abmap = np.abs(vmap)
  477. frame = interpolate(valmap=abmap,method='cubic')
  478. plt.imshow(frame,vmin=0,vmax=1500,cmap='viridis')
  479. cbar = plt.colorbar()
  480. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  481. # Remove the axes
  482. plt.axis('off')
  483. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_3\No_object_viridis.pdf',dpi=1000)
  484. plt.show()
  485. # %% [markdown]
  486. # ### Position classification from cardiac BSPM
  487. # %% [markdown]
  488. # #### Data import
  489. # %%
  490. import os
  491. import re
  492. from biolab.bspm import BSPM,image2layout
  493. from biolab.utils import apply2all
  494. # Save new recordings into .bspm files
  495. # Define parameters
  496. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\ECG_classification'
  497. # Convert electrode array image into layout
  498. ECG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\ECG_lyt.jpg',seq='cw')
  499. # Define operation to be applied to each file
  500. def func(filepath):
  501. # Extract filename from filepath
  502. filename = os.path.splitext(os.path.basename(filepath))[0]
  503. # Define the regex pattern to match the required values
  504. pattern = r'^([A-Za-z]+)_([A-Za-z-]+)_M(\d+)_\d{6}_\d{6}$'
  505. # Search for the pattern in the given filename
  506. match = re.search(pattern,filename)
  507. if match:
  508. filename = match.group(1)+'_'+match.group(2)
  509. mode = match.group(1) # type of signal (i.e. ECG, EMG, EEG)
  510. position = match.group(2) # position of the subject
  511. measurement = match.group(3) # measurement
  512. else:
  513. raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
  514. meta = {
  515. 'mode': mode,
  516. 'measurement': measurement,
  517. 'position': {'standing':'standing','sitting':'sitting','lying-side':'sideways','lying-up':'lying down'}.get(position,None)
  518. }
  519. # Load BSPM data from .rhs file
  520. MP = BSPM.from_file(filepath=filepath,layout=ECG_lyt,metadata=meta,refchs=[])
  521. # Save BSPM object
  522. MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\\'+filename+'.bspm')
  523. # Call DataSaver function
  524. apply2all(read_directory=readpath,extension='.rhs',func=func)
  525. # %% [markdown]
  526. # #### Feature extraction
  527. # %% [markdown]
  528. # RR intervals from all channels
  529. # %%
  530. import wfdb.processing
  531. import numpy as np
  532. import pandas as pd
  533. from biolab.bspm import BSPM
  534. from biolab.utils import apply2all
  535. # Define read directory
  536. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification'
  537. # Define apply2all function
  538. def fun(filepath):
  539. # Define new dataframe
  540. df = pd.DataFrame()
  541. # Load .bspm data
  542. ECG = BSPM.load(loadpath=filepath)
  543. # Resample and preprocess the data
  544. rECG = ECG.resample(newfs=1000)
  545. rECG.preprocess(bw=(0.5,20))
  546. for col in rECG.preprocessed_data.columns:
  547. # Find R-R intervals
  548. rpeaks = wfdb.processing.xqrs_detect(rECG.preprocessed_data[col][0:119.75].values,fs=1000,verbose=False)
  549. rrintv = wfdb.processing.calc_rr(qrs_locs=rpeaks)
  550. # Check the current number of rows in the dataframe
  551. current_len = len(df)
  552. new_len = len(rrintv)
  553. # If the DataFrame has more rows than rrintv and the DataFrame is not empty
  554. if (current_len<new_len) & (current_len!=0):
  555. # Calculate the mean of rrintv
  556. rrintv_mean = np.mean(rrintv)
  557. # Calculate the absolute deviation from the mean
  558. deviations = np.abs(rrintv-rrintv_mean)
  559. # Get the indices of the most different (largest deviations) values
  560. indices_to_remove = np.argsort(deviations)[:(new_len-current_len)] # Select top largest deviations
  561. # Remove excess values by excluding them
  562. rrintv = np.delete(rrintv,indices_to_remove)
  563. # If rrintv is shorter than the DataFrame, truncate the DataFrame to match the length of rrintv
  564. elif current_len>new_len:
  565. # Remove rows from the dataframe to match rrintv length
  566. df = df.iloc[:new_len]
  567. df['RRinterval_ch{}'.format(col)] = rrintv
  568. df['cycle'] = np.arange(1,len(rrintv)+1)
  569. df['position'] = rECG.metadata['position']
  570. # Append the DataFrame to an existing Excel file
  571. excel_path = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\RR_interval_feature_matrix.xlsx'
  572. # Load the existing data from the Excel file
  573. try:
  574. existing_df = pd.read_excel(excel_path)
  575. # Append the new data to the existing data
  576. combined_df = pd.concat([existing_df,df],ignore_index=True)
  577. except FileNotFoundError:
  578. # If the file doesn't exist, just use the new data
  579. combined_df = df
  580. # Write the combined data back to the Excel file
  581. with pd.ExcelWriter(excel_path,mode='w',engine='openpyxl') as writer:
  582. combined_df.to_excel(writer,index=False)
  583. # Apply function to all files in read directory
  584. apply2all(read_directory=readpath,extension='.bspm',func=fun)
  585. # %% [markdown]
  586. # Delays from all channels
  587. # %%
  588. import wfdb.processing
  589. import numpy as np
  590. import pandas as pd
  591. from biolab.bspm import BSPM
  592. from biolab.utils import apply2all
  593. # Define read directory
  594. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification'
  595. # Define apply2all function
  596. def fun(filepath):
  597. # Load .bspm data
  598. ECG = BSPM.load(loadpath=filepath)
  599. # Resample and preprocess the data
  600. rECG = ECG.resample(newfs=2000)
  601. rECG.preprocess(bw=(0.5,20))
  602. # Find R peaks
  603. peaks_per_channel = {}
  604. for col in rECG.preprocessed_data.columns:
  605. peaks_per_channel[col] = wfdb.processing.xqrs_detect(rECG.preprocessed_data[col][0:119.75].values,fs=2000,verbose=False)
  606. # Get the peak indices for channel 1
  607. channel_1_peaks = peaks_per_channel[rECG.preprocessed_data.columns[0]]
  608. # Initialise list to store delays for valid cycles
  609. delays = []
  610. # Define ECG peak delay tolerance
  611. tolerance = 0.01
  612. # Calculate delays for each cycle
  613. for peak_1 in channel_1_peaks:
  614. cycle_delays = []
  615. valid_cycle = True
  616. for col in rECG.preprocessed_data.columns[1:]: # Skip channel 1, compare all others to it
  617. channel_peaks = peaks_per_channel[col]
  618. # Find the closest peak in the current channel within the tolerance window
  619. valid_peaks = np.abs(channel_peaks-peak_1)<=tolerance*rECG.metadata['amplifier_sample_rate']
  620. if np.any(valid_peaks): # If there's a valid peak
  621. closest_peak = channel_peaks[valid_peaks][0] # Get the first valid peak
  622. delay = (closest_peak-peak_1)/rECG.metadata['amplifier_sample_rate'] # Convert to seconds
  623. cycle_delays.append(delay)
  624. else:
  625. valid_cycle = False
  626. break # If any channel does not have a valid peak for this cycle, skip it
  627. # If the cycle was valid, store the delays
  628. if valid_cycle:
  629. delays.append(cycle_delays)
  630. # Convert delays to DataFrame
  631. if delays: # Only create DataFrame if there are valid cycles
  632. delay_df = pd.DataFrame(delays)
  633. delay_df.columns = [f'Delay_ch{col}' for col in rECG.preprocessed_data.columns[1:]]
  634. else:
  635. delay_df = pd.DataFrame() # Return an empty DataFrame if no valid cycles are found
  636. delay_df['position'] = rECG.metadata['position']
  637. # Append the DataFrame to an existing Excel file
  638. excel_path = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\Delay_feature_matrix.xlsx'
  639. # Load the existing data from the Excel file
  640. try:
  641. existing_df = pd.read_excel(excel_path)
  642. # Append the new data to the existing data
  643. combined_df = pd.concat([existing_df,delay_df],ignore_index=True)
  644. except FileNotFoundError:
  645. # If the file doesn't exist, just use the new data
  646. combined_df = delay_df
  647. # Write the combined data back to the Excel file
  648. with pd.ExcelWriter(excel_path,mode='w',engine='openpyxl') as writer:
  649. combined_df.to_excel(writer,index=False)
  650. # Apply function to all files in read directory
  651. apply2all(read_directory=readpath,extension='.bspm',func=fun)
  652. # %% [markdown]
  653. # #### Feature values inspection
  654. # %% [markdown]
  655. # RR intervals
  656. # %%
  657. import pandas as pd
  658. import seaborn as sns
  659. import matplotlib.pyplot as plt
  660. # Import SNR data from Excel
  661. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\RR_interval_feature_matrix.xlsx'
  662. df = pd.read_excel(io=readpath)
  663. # Display RR interval as a function of position
  664. df = df.sort_values(by='position')
  665. sns.violinplot(x='position',y='RRinterval_ch9',data=df,order=df['position'].unique())
  666. plt.title('')
  667. plt.xlabel('Position')
  668. plt.ylabel('RR Interval [$\mu$s]')
  669. # Adjust layout to prevent overlap of titles and labels
  670. plt.tight_layout()
  671. plt.show()
  672. # %% [markdown]
  673. # *Mild differences between lying and upward positions can be observed. No clear distinction can be observed between sitting and standing or lying down or sideways.*
  674. # %% [markdown]
  675. # Delays
  676. # %%
  677. import pandas as pd
  678. import seaborn as sns
  679. import numpy as np
  680. import matplotlib.pyplot as plt
  681. # Import SNR data from Excel
  682. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\Delay_feature_matrix.xlsx'
  683. df = pd.read_excel(io=readpath)
  684. # Display RR interval as a function of position
  685. df = df.sort_values(by='position')
  686. sns.violinplot(x='position',y='Delay_ch16',data=df,order=df['position'].unique())
  687. plt.title('')
  688. plt.xlabel('Position')
  689. plt.ylabel('Delay CH1 - CH9 [s]')
  690. # Adjust layout to prevent overlap of titles and labels
  691. plt.tight_layout()
  692. plt.show()
  693. # Generate delay plots
  694. # Group by position and compute average delay and standard deviation
  695. avg = df.groupby(by='position').mean()
  696. stdv = df.groupby(by='position').std()
  697. avg = avg[['Delay_ch2','Delay_ch3','Delay_ch4','Delay_ch5','Delay_ch6','Delay_ch7','Delay_ch8','Delay_ch9','Delay_ch10','Delay_ch11','Delay_ch12','Delay_ch13','Delay_ch14','Delay_ch15','Delay_ch16']]
  698. stdv = stdv[['Delay_ch2','Delay_ch3','Delay_ch4','Delay_ch5','Delay_ch6','Delay_ch7','Delay_ch8','Delay_ch9','Delay_ch10','Delay_ch11','Delay_ch12','Delay_ch13','Delay_ch14','Delay_ch15','Delay_ch16']]
  699. plt.plot(np.arange(2,17),avg.iloc[0,:].values)
  700. plt.fill_between(np.arange(2,17),avg.iloc[0,:].values-stdv.iloc[0,:].values,avg.iloc[0,:].values+stdv.iloc[0,:].values,color='blue',alpha=0.2)
  701. plt.plot(np.arange(2,17),avg.iloc[1,:].values)
  702. plt.fill_between(np.arange(2,17),avg.iloc[1,:].values-stdv.iloc[1,:].values,avg.iloc[1,:].values+stdv.iloc[1,:].values,color='red',alpha=0.2)
  703. plt.plot(np.arange(2,17),avg.iloc[2,:].values)
  704. plt.fill_between(np.arange(2,17),avg.iloc[2,:].values-stdv.iloc[2,:].values,avg.iloc[2,:].values+stdv.iloc[2,:].values,color='green',alpha=0.2)
  705. plt.plot(np.arange(2,17),avg.iloc[3,:].values)
  706. plt.fill_between(np.arange(2,17),avg.iloc[3,:].values-stdv.iloc[3,:].values,avg.iloc[3,:].values+stdv.iloc[3,:].values,color='orange',alpha=0.2)
  707. plt.show()
  708. # %% [markdown]
  709. # *Clear differences can be observed between delays for all four positions and across channels.*
  710. # %%
  711. import numpy as np
  712. import pandas as pd
  713. import matplotlib.pyplot as plt
  714. # Import SNR data from Excel
  715. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\Delay_feature_matrix.xlsx'
  716. df = pd.read_excel(io=readpath)
  717. # Set 'Position' as the index
  718. df.set_index('position',inplace=True)
  719. # Group by 'Position' and calculate mean and std for each channel
  720. grouped = df.groupby('position').agg(['mean','std'])
  721. # Channels as columns (without multi-indexing)
  722. channels = df.columns
  723. num_channels = len(channels)
  724. # Prepare the angle array for the radar chart (evenly distributed for each channel)
  725. angles = np.linspace(0,2*np.pi,num_channels,endpoint=False).tolist()
  726. # Close the loop by repeating the first angle
  727. angles += angles[:1]
  728. # Create a figure for the radar chart
  729. fig, ax = plt.subplots(figsize=(6,6),subplot_kw=dict(polar=True))
  730. # Loop through each position and plot the radar chart
  731. color = {'lying down':'blue','sideways':'red','sitting':'green','standing':'orange'}
  732. for position in grouped.index:
  733. # Calculate the mean and standard deviation for each channel
  734. mean_delays = grouped.loc[position,(channels,'mean')].values
  735. std_delays = grouped.loc[position,(channels,'std')].values
  736. # Close the loop by appending the first value of the mean and std
  737. mean_delays = np.append(mean_delays,mean_delays[0])
  738. std_delays = np.append(std_delays,std_delays[0])
  739. # Plot the shaded standard deviation area
  740. ax.fill(angles,mean_delays+std_delays,color=color[position],alpha=0.2) # Shaded area
  741. ax.fill(angles, mean_delays - std_delays, color=color[position], alpha=0.2) # Shaded area
  742. # Plot the mean delay
  743. ax.plot(angles, mean_delays, label=f'Position {position}', linewidth=2, marker='o')
  744. # Remove radial ticks
  745. ax.set_yticklabels([])
  746. # Set axis labels for the channels
  747. ax.set_xticks(angles[:-1]) # Set the angles for the channels
  748. ax.set_xticklabels(channels)
  749. # Set the title of the radar chart
  750. ax.set_title("Radar Chart: Average Delay Between ECG Peaks with Std Dev", size=16, color='black', y=1.1)
  751. # Add a legend
  752. ax.legend(title="Positions", loc='upper right')
  753. # Show the chart
  754. plt.show()
  755. # %% [markdown]
  756. # #### Machine learning classification of positions
  757. # %% [markdown]
  758. # Single electrode (middle - control electrode)
  759. # %%
  760. import pandas as pd
  761. from sklearn.model_selection import train_test_split
  762. from sklearn.linear_model import LogisticRegression
  763. from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
  764. from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay,make_scorer,precision_score,recall_score,f1_score
  765. import matplotlib.pyplot as plt
  766. # Import SNR data from Excel
  767. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\RR_interval_feature_matrix.xlsx'
  768. df = pd.read_excel(io=readpath)
  769. # Generate feature matrix from data
  770. fmatrix = df[['RRinterval_ch9', 'position']]
  771. # Separate features and labels
  772. X = fmatrix.drop('position', axis=1) # feature matrix
  773. y = fmatrix['position'] # labels
  774. # Split the data into training and testing sets
  775. X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['position'])
  776. # Initialise and fit the logistic regression
  777. clf = LogisticRegression()
  778. clf.fit(X_train,y_train)
  779. # Predict the labels for the test set
  780. y_pred = clf.predict(X_test)
  781. # Calculate the confusion matrix
  782. cm = confusion_matrix(y_test,y_pred)
  783. # Display the confusion matrix
  784. disp = ConfusionMatrixDisplay(confusion_matrix=cm,display_labels=clf.classes_)
  785. fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
  786. # Set the color range limits
  787. vmin,vmax = 0,40 # Adjust these values as needed
  788. disp.plot(cmap="Blues",ax=ax,colorbar=False,values_format="d")
  789. disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
  790. # Adjust the colorbar size
  791. cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
  792. cbar.set_label("Count") # Label for better readability
  793. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Confusion_matrices\Single_channel_ECG',dpi=1000)
  794. # Repeated Cross-Validation Accuracy
  795. rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
  796. accuracy_scores = cross_val_score(clf,X,y,cv=rkf)
  797. precision_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(precision_score, average='macro'))
  798. recall_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(recall_score, average='macro'))
  799. f1_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(f1_score, average='macro'))
  800. # Print results
  801. print(f'Repeated Cross-Validation Accuracy: {accuracy_scores.mean():.2f} ± {accuracy_scores.std():.2f}')
  802. print(f"Repeated Cross-Validation Precision: {precision_scores.mean():.2f} ± {precision_scores.std():.2f}")
  803. print(f"Repeated Cross-Validation Recall: {recall_scores.mean():.2f} ± {recall_scores.std():.2f}")
  804. print(f"Repeated Cross-Validation F1 Score: {f1_scores.mean():.2f} ± {f1_scores.std():.2f}")
  805. # %% [markdown]
  806. # Electrode array
  807. # %%
  808. import pandas as pd
  809. from sklearn.model_selection import train_test_split
  810. from sklearn.linear_model import LogisticRegression
  811. from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
  812. from sklearn.preprocessing import StandardScaler
  813. from sklearn.pipeline import Pipeline
  814. from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay,make_scorer,precision_score,recall_score,f1_score
  815. import matplotlib.pyplot as plt
  816. # Import SNR data from Excel
  817. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\Delay_feature_matrix.xlsx'
  818. df = pd.read_excel(io=readpath)
  819. # Generate feature matrix from data
  820. fmatrix = df
  821. # Separate features and labels
  822. X = fmatrix.drop('position',axis=1) # feature matrix
  823. y = fmatrix['position'] # labels
  824. # Split the data into training and testing sets
  825. X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['position'])
  826. # Define a pipeline with scaling and logistic regression
  827. pipeline = Pipeline([
  828. ('scaler',StandardScaler()), # Standardizes the data
  829. ('classifier',LogisticRegression()) # Logistic Regression model
  830. ])
  831. # Fit the model using only training data
  832. pipeline.fit(X_train,y_train)
  833. # Predict the labels for the test set
  834. y_pred = pipeline.predict(X_test)
  835. # Calculate the confusion matrix
  836. cm = confusion_matrix(y_test,y_pred)
  837. # Display the confusion matrix
  838. disp = ConfusionMatrixDisplay(confusion_matrix=cm,display_labels=pipeline.classes_)
  839. fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
  840. # Set the color range limits
  841. vmin,vmax = 0,40 # Adjust these values as needed
  842. disp.plot(cmap="Blues",ax=ax,colorbar=False,values_format="d")
  843. disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
  844. # Adjust the colorbar size
  845. cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
  846. cbar.set_label("Count") # Label for better readability
  847. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Confusion_matrices\BSPM_ECG',dpi=1000)
  848. # Repeated Cross-Validation Accuracy
  849. rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
  850. accuracy_scores = cross_val_score(pipeline,X,y,cv=rkf)
  851. precision_scores = cross_val_score(pipeline, X, y, cv=rkf, scoring=make_scorer(precision_score, average='macro'))
  852. recall_scores = cross_val_score(pipeline, X, y, cv=rkf, scoring=make_scorer(recall_score, average='macro'))
  853. f1_scores = cross_val_score(pipeline, X, y, cv=rkf, scoring=make_scorer(f1_score, average='macro'))
  854. # Print results
  855. print(f'Repeated Cross-Validation Accuracy: {accuracy_scores.mean():.2f} ± {accuracy_scores.std():.2f}')
  856. print(f"Repeated Cross-Validation Precision: {precision_scores.mean():.2f} ± {precision_scores.std():.2f}")
  857. print(f"Repeated Cross-Validation Recall: {recall_scores.mean():.2f} ± {recall_scores.std():.2f}")
  858. print(f"Repeated Cross-Validation F1 Score: {f1_scores.mean():.2f} ± {f1_scores.std():.2f}")
  859. # %% [markdown]
  860. # #### Cardiac BSPM
  861. # %%
  862. import matplotlib.pyplot as plt
  863. import numpy as np
  864. from biolab.bspm import interpolate,BSPM
  865. # Read .bspm data for sphere
  866. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\ECG_lying-side.bspm'
  867. ECG = BSPM.load(loadpath=readpath)
  868. rECG_sideways = ECG.resample(newfs=200)
  869. rECG_sideways.preprocess(bw=(0.5,99))
  870. # Display cardiac BSPM
  871. vmap = rECG_sideways.potential(time=10.385)
  872. frame = interpolate(valmap=np.where(np.isnan(-vmap),-vmap,-vmap/np.nanmax(-vmap)),method='cubic')
  873. plt.imshow(frame,vmin=0.5,vmax=1)
  874. cbar = plt.colorbar()
  875. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  876. # Remove the axes
  877. plt.axis('off')
  878. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Sideways.pdf',dpi=1000)
  879. plt.show()
  880. # Read .bspm data for sphere
  881. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\ECG_lying-up.bspm'
  882. ECG = BSPM.load(loadpath=readpath)
  883. rECG_lying_down = ECG.resample(newfs=200)
  884. rECG_lying_down.preprocess(bw=(0.5,99))
  885. # Display cardiac BSPM
  886. vmap = rECG_lying_down.potential(time=10.47)
  887. frame = interpolate(valmap=np.where(np.isnan(-vmap),-vmap,-vmap/np.nanmax(-vmap)),method='cubic')
  888. plt.imshow(frame,vmin=0.5,vmax=1)
  889. cbar = plt.colorbar()
  890. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  891. # Remove the axes
  892. plt.axis('off')
  893. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Lying down.pdf',dpi=1000)
  894. plt.show()
  895. # Read .bspm data for sphere
  896. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\ECG_sitting.bspm'
  897. ECG = BSPM.load(loadpath=readpath)
  898. rECG_sitting = ECG.resample(newfs=200)
  899. rECG_sitting.preprocess(bw=(0.5,99))
  900. # Display cardiac BSPM
  901. vmap = rECG_sitting.potential(time=10.235)
  902. frame = interpolate(valmap=np.where(np.isnan(-vmap),-vmap,-vmap/np.nanmax(-vmap)),method='cubic')
  903. plt.imshow(frame,vmin=0.5,vmax=1)
  904. cbar = plt.colorbar()
  905. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  906. # Remove the axes
  907. plt.axis('off')
  908. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Sitting.pdf',dpi=1000)
  909. plt.show()
  910. # Read .bspm data for sphere
  911. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\ECG_standing.bspm'
  912. ECG = BSPM.load(loadpath=readpath)
  913. rECG_standing = ECG.resample(newfs=200)
  914. rECG_standing.preprocess(bw=(0.5,99))
  915. # Display cardiac BSPM
  916. vmap = rECG_standing.potential(time=10.515)
  917. frame = interpolate(valmap=np.where(np.isnan(-vmap),-vmap,-vmap/np.nanmax(-vmap)),method='cubic')
  918. plt.imshow(frame,vmin=0.5,vmax=1)
  919. cbar = plt.colorbar()
  920. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  921. # Remove the axes
  922. plt.axis('off')
  923. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Standing.pdf',dpi=1000)
  924. plt.show()
  925. # %%
  926. import pandas as pd
  927. def delay(bspm,delays) -> np.ndarray:
  928. # Create copy of mapped layout
  929. dmap = np.copy(bspm.layout).astype(float)
  930. dmap[dmap == 0] = np.NaN
  931. # Convert channel_data to numpy arrays
  932. locations = bspm.channel_data['location'].values
  933. # Change value of label in layout to potential taking into account reference channels
  934. # Create a mapping from location to voltage channel
  935. location_to_voltage = dict(zip(locations,delays))
  936. # Replace values in zmap based on location_to_impedance mapping
  937. mask = np.isin(bspm.layout,locations)
  938. dmap[mask] = np.vectorize(location_to_voltage.get)(bspm.layout[mask])
  939. return dmap
  940. data = pd.read_excel(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\ECG_classification\Delay_feature_matrix.xlsx')
  941. data_avg = data.groupby('position',as_index=False).mean().drop(columns='position')
  942. # Display cardiac delay maps
  943. dmap = delay(rECG_sideways,np.insert(data_avg.iloc[0,:].values,13,0.0)*1000)
  944. frame = interpolate(valmap=dmap,method='cubic')
  945. plt.imshow(frame,vmin=0,vmax=10)
  946. cbar = plt.colorbar()
  947. cbar.set_label('Delay [ms]') # Setting the label for the colorbar
  948. # Remove the axes
  949. plt.axis('off')
  950. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Sideways_delay.pdf',dpi=1000)
  951. plt.show()
  952. dmap = delay(rECG_lying_down,np.insert(data_avg.iloc[1,:].values,13,0.0)*1000)
  953. frame = interpolate(valmap=dmap,method='cubic')
  954. plt.imshow(frame,vmin=0,vmax=10)
  955. cbar = plt.colorbar()
  956. cbar.set_label('Delay [ms]') # Setting the label for the colorbar
  957. # Remove the axes
  958. plt.axis('off')
  959. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Lying_down_delay.pdf',dpi=1000)
  960. plt.show()
  961. dmap = delay(rECG_sitting,np.insert(data_avg.iloc[2,:].values,13,0.0)*1000)
  962. frame = interpolate(valmap=dmap,method='cubic')
  963. plt.imshow(frame,vmin=0,vmax=10)
  964. cbar = plt.colorbar()
  965. cbar.set_label('Delay [ms]') # Setting the label for the colorbar
  966. # Remove the axes
  967. plt.axis('off')
  968. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Sitting_delay.pdf',dpi=1000)
  969. plt.show()
  970. dmap = delay(rECG_standing,np.insert(data_avg.iloc[3,:].values,13,0.0)*1000)
  971. frame = interpolate(valmap=dmap,method='cubic')
  972. plt.imshow(frame,vmin=0,vmax=10)
  973. cbar = plt.colorbar()
  974. cbar.set_label('Delay [ms]') # Setting the label for the colorbar
  975. # Remove the axes
  976. plt.axis('off')
  977. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Standing_delay.pdf',dpi=1000)
  978. plt.show()
  979. # %% [markdown]
  980. # ### Sensorimotor classification from cerebral BSPM
  981. # %% [markdown]
  982. # #### Data import
  983. # %%
  984. import os
  985. import re
  986. from biolab.bspm import BSPM,image2layout
  987. from biolab.utils import apply2all
  988. # Save new recordings into .bspm files
  989. # Define parameters
  990. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\EEG_classification'
  991. # Convert electrode array image into layout
  992. ECG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EEG_lyt.jpg',seq='cw')
  993. # Define operation to be applied to each file
  994. def func(filepath):
  995. # Extract filename from filepath
  996. filename = os.path.splitext(os.path.basename(filepath))[0]
  997. # Define the regex pattern to match the required values
  998. pattern = r'^([A-Za-z_]+)_\d{6}_\d{6}$'
  999. # Search for the pattern in the given filename
  1000. match = re.search(pattern,filename)
  1001. if match:
  1002. filename = match.group(1)
  1003. if filename == 'muscle activity':
  1004. mode = 'EEG/EMG'
  1005. else:
  1006. mode = 'EEG'
  1007. sensorimotor = filename
  1008. measurement = 'M1'
  1009. else:
  1010. raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
  1011. meta = {
  1012. 'mode': mode,
  1013. 'measurement': measurement,
  1014. 'sensorimotor': {'auditory_stimulus':'auditory','eyes_closed_open':'eye closing','mental_calculus':'mental calculus','muscle_activity':'motor','visual_stimulus':'visual'}.get(sensorimotor,None)
  1015. }
  1016. # Load BSPM data from .rhs file
  1017. MP = BSPM.from_file(filepath=filepath,layout=ECG_lyt,metadata=meta,refchs=[])
  1018. # Save BSPM object
  1019. MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\\'+filename+'.bspm')
  1020. # Call DataSaver function
  1021. apply2all(read_directory=readpath,extension='.rhs',func=func)
  1022. # %% [markdown]
  1023. # #### Data visualisation
  1024. # %% [markdown]
  1025. # Changes in alpha wave activity when opening and closing eyes
  1026. # %%
  1027. import pandas as pd
  1028. import plotly.express as px
  1029. from scipy.signal import butter,filtfilt
  1030. from biolab.bspm import BSPM
  1031. # Define read directory
  1032. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\Testing\eyes_closed_open.bspm'
  1033. # Load .bspm data
  1034. EEG = BSPM.load(loadpath=readpath)
  1035. # Resample and preprocess the data
  1036. rEEG = EEG.resample(newfs=1000)
  1037. rEEG.preprocess(bw=(0.5,100))
  1038. # Define bandpass filter function
  1039. def bandpass_filter(data,lowcut,highcut,sf,order=4):
  1040. return filtfilt(*butter(order,[lowcut,highcut],fs=sf,btype='band'),data)
  1041. # Define EEG bands
  1042. bands = {'Delta (0.5-4 Hz)': (1,4), 'Theta (4-8 Hz)': (4,8),
  1043. 'Alpha (8-13 Hz)': (8,13), 'Beta (13-30 Hz)': (13,30),
  1044. 'Gamma (30-100 Hz)': (30,100)}
  1045. # Select one channel (e.g., 'A-000') and apply filters
  1046. channel = 'A-000'
  1047. filtered_signals = {band: bandpass_filter(rEEG.preprocessed_data[channel],low,high,1000) for band,(low,high) in bands.items()}
  1048. # Create DataFrame for Plotly
  1049. df_plot = pd.DataFrame({'Time (s)': rEEG.preprocessed_data.index.values})
  1050. for band, signal in filtered_signals.items():
  1051. df_plot[band] = signal
  1052. # Melt DataFrame for Plotly Express
  1053. df_melted = df_plot.melt(id_vars=['Time (s)'],var_name='Band',value_name='Amplitude')
  1054. # Plot with Plotly Express
  1055. fig = px.line(df_melted,x='Time (s)',y='Amplitude',color='Band',title=f'EEG Trace of {channel} Across Frequency Bands')
  1056. fig.show()
  1057. # %% [markdown]
  1058. # Changes in theta wave activity when performing mental calculus
  1059. # %%
  1060. import pandas as pd
  1061. import plotly.express as px
  1062. from scipy.signal import butter,filtfilt
  1063. from biolab.bspm import BSPM
  1064. # Define read directory
  1065. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\Testing\mental_calculus.bspm'
  1066. # Load .bspm data
  1067. EEG = BSPM.load(loadpath=readpath)
  1068. # Resample and preprocess the data
  1069. rEEG = EEG.resample(newfs=1000)
  1070. rEEG.preprocess(bw=(0.5,100))
  1071. # Define bandpass filter function
  1072. def bandpass_filter(data,lowcut,highcut,sf,order=4):
  1073. return filtfilt(*butter(order,[lowcut,highcut],fs=sf,btype='band'),data)
  1074. # Define EEG bands
  1075. bands = {'Delta (0.5-4 Hz)': (1,4), 'Theta (4-8 Hz)': (4,8),
  1076. 'Alpha (8-13 Hz)': (8,13), 'Beta (13-30 Hz)': (13,30),
  1077. 'Gamma (30-100 Hz)': (30,100)}
  1078. # Select one channel (e.g., 'A-000') and apply filters
  1079. channel = 'A-000'
  1080. filtered_signals = {band: bandpass_filter(rEEG.preprocessed_data[channel],low,high,1000) for band,(low,high) in bands.items()}
  1081. # Create DataFrame for Plotly
  1082. df_plot = pd.DataFrame({'Time (s)': rEEG.preprocessed_data.index.values})
  1083. for band, signal in filtered_signals.items():
  1084. df_plot[band] = signal
  1085. # Melt DataFrame for Plotly Express
  1086. df_melted = df_plot.melt(id_vars=['Time (s)'],var_name='Band',value_name='Amplitude')
  1087. # Plot with Plotly Express
  1088. fig = px.line(df_melted,x='Time (s)',y='Amplitude',color='Band',title=f'EEG Trace of {channel} Across Frequency Bands')
  1089. fig.show()
  1090. # %% [markdown]
  1091. # #### Feature extraction
  1092. # %%
  1093. import numpy as np
  1094. import pandas as pd
  1095. import scipy.signal as signal
  1096. from biolab.utils import apply2all
  1097. from biolab.bspm import BSPM
  1098. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification'
  1099. def compute_band_power(eeg_signal,sf,band,method='welch'):
  1100. low,high = band
  1101. # Compute Power Spectral Density (PSD)
  1102. if method == 'welch':
  1103. freqs,psd = signal.welch(eeg_signal,sf,nperseg=sf*2) # 2-sec windows
  1104. else: # FFT method
  1105. freqs = np.fft.rfftfreq(len(eeg_signal),d=1/sf)
  1106. psd = np.abs(np.fft.rfft(eeg_signal))**2/len(eeg_signal)
  1107. # Integrate PSD within the frequency band
  1108. band_power = np.trapz(psd[(freqs>=low)&(freqs<=high)],freqs[(freqs>=low)&(freqs<=high)])
  1109. return band_power
  1110. # Define bands
  1111. bands = {'Delta':(0.5,4),'Theta':(4,8),'Alpha':(8,13),'Beta':(13,30),'Gamma':(30,100)}
  1112. def f(filepath):
  1113. EEG = BSPM.load(loadpath=filepath)
  1114. rEEG = EEG.resample(newfs=1000)
  1115. rEEG.preprocess(bw=(0.5,100))
  1116. sf = 1000 # Sampling frequency (Hz)
  1117. window_size = 2*sf # 2-second window
  1118. gap_size = 2*sf # 2-second window
  1119. start_idx = 10*sf # Start after 10 seconds
  1120. num_samples = len(rEEG.preprocessed_data)
  1121. # Create an empty list to store computed band power values
  1122. band_power_list = []
  1123. # Iterate over 2-second non-overlapping windows after the 10-second mark
  1124. for start in range(start_idx,num_samples,window_size+gap_size):
  1125. end = start+window_size
  1126. if end>num_samples: # Ensure the window does not exceed data length
  1127. break
  1128. # Extract windowed EEG data (2-sec segment)
  1129. segment = rEEG.preprocessed_data.iloc[start:end]
  1130. # Compute band power for each channel-band combination
  1131. band_power_dict = {}
  1132. for band_name,band_range in bands.items():
  1133. band_power_values = segment.apply(lambda x: compute_band_power(x.values,sf,band_range),axis=0)
  1134. # Rename columns to match channel + band (e.g., Ch1_Delta, Ch1_Theta)
  1135. for ch in band_power_values.index:
  1136. band_power_dict[f"{ch}_{band_name}"] = band_power_values[ch]
  1137. # Append result as a row in the list
  1138. band_power_list.append(band_power_dict)
  1139. # Convert list to DataFrame
  1140. df_band_power = pd.DataFrame(band_power_list)
  1141. df_band_power['action'] = EEG.metadata['sensorimotor']
  1142. excel_path = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\EEG_feature_matrix.xlsx'
  1143. # Load the existing data from the Excel file
  1144. try:
  1145. existing_df = pd.read_excel(excel_path)
  1146. # Append the new data to the existing data
  1147. combined_df = pd.concat([existing_df,df_band_power],ignore_index=True)
  1148. except FileNotFoundError:
  1149. # If the file doesn't exist, just use the new data
  1150. combined_df = df_band_power
  1151. # Write the combined data back to the Excel file
  1152. with pd.ExcelWriter(excel_path,mode='w',engine='openpyxl') as writer:
  1153. combined_df.to_excel(writer,index=False)
  1154. apply2all(read_directory=readpath,extension='.bspm',exclude=['Testing'],func=f)
  1155. # %% [markdown]
  1156. # #### Feature values inspection
  1157. # %%
  1158. import matplotlib.pyplot as plt
  1159. import pandas as pd
  1160. import seaborn as sns
  1161. # Import EEG waves data
  1162. df = pd.read_excel(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\EEG_feature_matrix.xlsx')
  1163. # Reshaping DataFrame: Convert from Wide to Long Format
  1164. df_long = df.melt(id_vars=["action"], var_name="Channel_Band", value_name="Power")
  1165. # Splitting "Channel_Band" into "Channel" and "Band"
  1166. df_long[['Channel', 'Band']] = df_long['Channel_Band'].str.extract(r"(A-\d{3})_(\w+)")
  1167. # Function to Remove Outliers using IQR
  1168. def remove_outliers(group):
  1169. Q1 = group["Power"].quantile(0.25)
  1170. Q3 = group["Power"].quantile(0.75)
  1171. IQR = Q3 - Q1
  1172. lower_bound = Q1 - 1.5 * IQR
  1173. upper_bound = Q3 + 1.5 * IQR
  1174. return group[(group["Power"] >= lower_bound) & (group["Power"] <= upper_bound)]
  1175. # Apply outlier removal
  1176. df_filtered = df_long.groupby(["Channel", "Band"]).apply(remove_outliers).reset_index(drop=True)
  1177. # Drop high-impedance channels
  1178. channels_to_remove = ["A-004", "A-025", "A-030", "A-031"]
  1179. df_filtered = df_filtered[~df_filtered["Channel"].isin(channels_to_remove)]
  1180. # Get unique EEG bands
  1181. bands = df_filtered["Band"].unique()
  1182. # Create subplots (one for each band)
  1183. fig, axes = plt.subplots(nrows=len(bands), figsize=(15, 3 * len(bands)), sharex=True)
  1184. for i, band in enumerate(bands):
  1185. ax = axes[i] if len(bands) > 1 else axes # Adjust for single-band case
  1186. band_data = df_filtered[df_filtered["Band"] == band] # Filter for current band
  1187. sns.violinplot(x="Channel", y="Power", hue="action", data=band_data, dodge=True,palette="muted", ax=ax)
  1188. ax.set_title(f"{band} Band Power Distribution Across Channels")
  1189. ax.set_ylabel("Power")
  1190. ax.set_xlabel("")
  1191. ax.tick_params(axis='x', rotation=90) # Rotate x-axis labels for readability
  1192. # Add x-axis label
  1193. plt.xlabel("Channel")
  1194. # Adjust layout & legend placement
  1195. plt.tight_layout()
  1196. plt.legend(title="Action",loc='upper left') # Move legend outside
  1197. # Show the plot
  1198. plt.show()
  1199. # %% [markdown]
  1200. # 1. Auditory Activity:
  1201. # - Theta (4-8 Hz): Involved in auditory processing, especially during tasks requiring attention or memory.
  1202. # - Gamma (30-100 Hz): Associated with auditory perception and integration, especially in response to complex sounds or language.
  1203. #
  1204. # 2. Visual Activity:
  1205. # - Alpha (8-12 Hz): Typically decreases (desynchronizes) in occipital regions during visual processing, indicating engagement.
  1206. # - Gamma (30-100 Hz): Related to visual perception, feature binding, and conscious awareness.
  1207. #
  1208. # 3. Motor Activity:
  1209. # - Beta (13-30 Hz): In sensorimotor cortex, beta activity often decreases (event-related desynchronization, ERD) during movement or motor preparation and increases (event-related synchronization, ERS) after movement.
  1210. # - Mu Rhythm (8-13 Hz): Similar to alpha but localized over motor cortex; suppressed during actual or imagined movement.
  1211. #
  1212. # Observation
  1213. # - Auditory vs. Visual: Focus on Alpha (visual engagement) and Gamma (auditory processing).
  1214. # - Auditory vs. Motor: Compare Theta/Gamma (auditory) with Beta/Mu (motor).
  1215. # - Visual vs. Motor: Contrast Alpha (visual) with Beta/Mu (motor).
  1216. # %% [markdown]
  1217. # #### Machine learning classification of brain activity
  1218. # %% [markdown]
  1219. # Single electrode (middle - control electrode)
  1220. # %%
  1221. import pandas as pd
  1222. from sklearn.model_selection import train_test_split
  1223. from sklearn.linear_model import LogisticRegression
  1224. from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
  1225. from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, make_scorer, precision_score, recall_score, f1_score
  1226. import matplotlib.pyplot as plt
  1227. # Import SNR data from Excel
  1228. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\EEG_feature_matrix.xlsx'
  1229. df = pd.read_excel(io=readpath)
  1230. # Generate feature matrix from data
  1231. fmatrix = df[['A-003_Delta','A-003_Theta','A-003_Alpha','A-003_Beta','A-003_Gamma','action']]
  1232. # Separate features and labels
  1233. X = fmatrix.drop('action', axis=1) # feature matrix
  1234. y = fmatrix['action'] # labels
  1235. # Split the data into training and testing sets
  1236. X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['action'])
  1237. # Initialise and fit the logistic regression
  1238. clf = LogisticRegression()
  1239. clf.fit(X_train,y_train)
  1240. # Predict the labels for the test set
  1241. y_pred = clf.predict(X_test)
  1242. # Calculate the confusion matrix
  1243. cm = confusion_matrix(y_test,y_pred)
  1244. # Display the confusion matrix
  1245. disp = ConfusionMatrixDisplay(confusion_matrix=cm,display_labels=clf.classes_)
  1246. fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
  1247. # Set the color range limits
  1248. vmin,vmax = 0,25 # Adjust these values as needed
  1249. disp.plot(cmap="Greens",ax=ax,colorbar=False,values_format="d")
  1250. disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
  1251. # Adjust the colorbar size
  1252. cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
  1253. cbar.set_label("Count") # Label for better readability
  1254. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Confusion_matrices\Single_channel_EEG',dpi=1000)
  1255. # Repeated Cross-Validation Accuracy
  1256. rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
  1257. accuracy_scores = cross_val_score(clf,X,y,cv=rkf)
  1258. precision_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(precision_score, average='macro'))
  1259. recall_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(recall_score, average='macro'))
  1260. f1_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(f1_score, average='macro'))
  1261. # Print results
  1262. print(f'Repeated Cross-Validation Accuracy: {accuracy_scores.mean():.2f} ± {accuracy_scores.std():.2f}')
  1263. print(f"Repeated Cross-Validation Precision: {precision_scores.mean():.2f} ± {precision_scores.std():.2f}")
  1264. print(f"Repeated Cross-Validation Recall: {recall_scores.mean():.2f} ± {recall_scores.std():.2f}")
  1265. print(f"Repeated Cross-Validation F1 Score: {f1_scores.mean():.2f} ± {f1_scores.std():.2f}")
  1266. # %% [markdown]
  1267. # Electrode array
  1268. # %%
  1269. import pandas as pd
  1270. from sklearn.model_selection import train_test_split
  1271. from sklearn.linear_model import LogisticRegression
  1272. from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score
  1273. from sklearn.preprocessing import StandardScaler
  1274. from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, make_scorer, precision_score, recall_score, f1_score
  1275. import matplotlib.pyplot as plt
  1276. # Import SNR data from Excel
  1277. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\EEG_classification\EEG_feature_matrix.xlsx'
  1278. df = pd.read_excel(io=readpath)
  1279. # Generate feature matrix from data
  1280. fmatrix = df
  1281. # Separate features and labels
  1282. X = fmatrix.drop('action',axis=1) # feature matrix
  1283. y = fmatrix['action'] # labels
  1284. # Split the data into training and testing sets
  1285. X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,stratify=fmatrix['action'])
  1286. # Initialise and fit the logistic regression
  1287. clf = LogisticRegression()
  1288. clf.fit(X_train,y_train)
  1289. # Predict the labels for the test set
  1290. y_pred = clf.predict(X_test)
  1291. # Calculate the confusion matrix
  1292. cm = confusion_matrix(y_test,y_pred)
  1293. # Display the confusion matrix
  1294. disp = ConfusionMatrixDisplay(confusion_matrix=cm,display_labels=clf.classes_)
  1295. fig,ax = plt.subplots(figsize=(6,6)) # Adjust figure size if needed
  1296. # Set the color range limits
  1297. vmin,vmax = 0,25 # Adjust these values as needed
  1298. disp.plot(cmap="Greens",ax=ax,colorbar=False,values_format="d")
  1299. disp.im_.set_clim(vmin,vmax) # Set the colorbar limits
  1300. # Adjust the colorbar size
  1301. cbar = ax.figure.colorbar(disp.im_,fraction=0.046,pad=0.04)
  1302. cbar.set_label("Count") # Label for better readability
  1303. #plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Results\Confusion_matrices\BSPM_EEG',dpi=1000)
  1304. # Repeated Cross-Validation Accuracy
  1305. rkf = RepeatedStratifiedKFold(n_splits=10,n_repeats=5)
  1306. accuracy_scores = cross_val_score(clf,X,y,cv=rkf)
  1307. precision_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(precision_score, average='macro'))
  1308. recall_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(recall_score, average='macro'))
  1309. f1_scores = cross_val_score(clf, X, y, cv=rkf, scoring=make_scorer(f1_score, average='macro'))
  1310. # Print results
  1311. print(f'Repeated Cross-Validation Accuracy: {accuracy_scores.mean():.2f} ± {accuracy_scores.std():.2f}')
  1312. print(f"Repeated Cross-Validation Precision: {precision_scores.mean():.2f} ± {precision_scores.std():.2f}")
  1313. print(f"Repeated Cross-Validation Recall: {recall_scores.mean():.2f} ± {recall_scores.std():.2f}")
  1314. print(f"Repeated Cross-Validation F1 Score: {f1_scores.mean():.2f} ± {f1_scores.std():.2f}")
  1315. # %% [markdown]
  1316. # Correlation circle for feature importance
  1317. # %%
  1318. import numpy as np
  1319. import matplotlib.pyplot as plt
  1320. from matplotlib.colors import to_rgba, Normalize
  1321. from matplotlib.cm import ScalarMappable
  1322. from sklearn.preprocessing import StandardScaler
  1323. from sklearn.decomposition import PCA
  1324. # ------------------------------------------------
  1325. # Extract frequency and channel info from feature names
  1326. # ------------------------------------------------
  1327. feature_names = X.columns
  1328. # Map canonical frequency bands to base colors
  1329. freq_colors = {
  1330. 'delta': 'blue',
  1331. 'theta': 'green',
  1332. 'alpha': 'red',
  1333. 'beta': 'orange',
  1334. 'gamma': 'purple'
  1335. }
  1336. frequencies = []
  1337. channels = []
  1338. for feat in feature_names:
  1339. channel_part, freq_part = feat.split('_')
  1340. channel_num = int(channel_part.split('-')[1])
  1341. frequencies.append(freq_part.lower())
  1342. channels.append(channel_num)
  1343. # Normalize channels for alpha mapping
  1344. chan_min = min(channels)
  1345. chan_max = max(channels)
  1346. channels_norm = [(c - chan_min)/(chan_max - chan_min) for c in channels] # 0 → 1
  1347. # ------------------------------------------------
  1348. # Standardize features and run PCA
  1349. # ------------------------------------------------
  1350. scaler = StandardScaler()
  1351. X_scaled = scaler.fit_transform(X)
  1352. pca = PCA(n_components=2)
  1353. X_pca = pca.fit_transform(X_scaled)
  1354. loadings = pca.components_.T * np.sqrt(pca.explained_variance_)
  1355. # ------------------------------------------------
  1356. # Plot correlation circle
  1357. # ------------------------------------------------
  1358. fig, ax = plt.subplots(figsize=(10,10), constrained_layout=True)
  1359. # Unit circle
  1360. circle = plt.Circle((0,0), 1, fill=False, linestyle='--', color='black')
  1361. ax.add_artist(circle)
  1362. # Plot arrows with frequency color + channel brightness
  1363. for i, feat in enumerate(feature_names):
  1364. base_color = freq_colors.get(frequencies[i], 'gray')
  1365. # Use alpha for channel: 0 = min channel → white overlay, 1 = max channel → base color
  1366. rgba = to_rgba(base_color, alpha=0.4 + 0.6*channels_norm[i])
  1367. ax.annotate(
  1368. "",
  1369. xy=(loadings[i,0], loadings[i,1]),
  1370. xytext=(0,0),
  1371. arrowprops=dict(
  1372. arrowstyle="->",
  1373. lw=2.5,
  1374. color=rgba
  1375. )
  1376. )
  1377. # ------------------------------------------------
  1378. # Legend for frequency bands
  1379. # ------------------------------------------------
  1380. for freq, color in freq_colors.items():
  1381. ax.plot([], [], color=color, lw=2, label=freq.capitalize())
  1382. ax.legend(title='Frequency band', loc='upper right')
  1383. # ------------------------------------------------
  1384. # Colorbar for channel brightness (white → black)
  1385. # ------------------------------------------------
  1386. # We'll create a grayscale colormap from white → black
  1387. from matplotlib.colors import LinearSegmentedColormap
  1388. grayscale = LinearSegmentedColormap.from_list('gray', ['white', 'black'])
  1389. norm = Normalize(vmin=chan_min, vmax=chan_max)
  1390. sm = ScalarMappable(cmap=grayscale, norm=norm)
  1391. sm.set_array([])
  1392. cbar = fig.colorbar(sm, ax=ax)
  1393. cbar.set_label('Channel number')
  1394. # ------------------------------------------------
  1395. # Axis formatting
  1396. # ------------------------------------------------
  1397. ax.axhline(0, color='black', linewidth=1.5)
  1398. ax.axvline(0, color='black', linewidth=1.5)
  1399. for spine in ax.spines.values():
  1400. spine.set_color('black')
  1401. spine.set_linewidth(1.5)
  1402. ax.tick_params(colors='black')
  1403. ax.set_xlim(-1.1,1.1)
  1404. ax.set_ylim(-1.1,1.1)
  1405. ax.set_aspect('equal')
  1406. ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]*100:.1f}% variance)')
  1407. ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]*100:.1f}% variance)')
  1408. ax.set_title('Correlation Circle (Cerebral BSPM)')
  1409. plt.show()
  1410. # %% [markdown]
  1411. # #### Cerebral BSPM
  1412. # %% [markdown]
  1413. # ##### BSPM data import
  1414. # %%
  1415. import os
  1416. import re
  1417. from biolab.bspm import BSPM,image2layout
  1418. from biolab.utils import apply2all
  1419. # Save new recordings into .bspm files
  1420. # Define parameters
  1421. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\EEG_classification\BSPM'
  1422. # Convert electrode array image into layout
  1423. ECG_lyt = image2layout(filename=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EEG_lyt.jpg',seq='cw')
  1424. # Define operation to be applied to each file
  1425. def func(filepath):
  1426. # Extract filename from filepath
  1427. filename = os.path.splitext(os.path.basename(filepath))[0]
  1428. # Define the regex pattern to match the required values
  1429. pattern = r'^([A-Za-z_]+)_\d{6}_\d{6}$'
  1430. # Search for the pattern in the given filename
  1431. match = re.search(pattern,filename)
  1432. if match:
  1433. filename = match.group(1)
  1434. if filename == 'muscle activity':
  1435. mode = 'EEG/EMG'
  1436. else:
  1437. mode = 'EEG'
  1438. sensorimotor = filename
  1439. measurement = 'M1'
  1440. else:
  1441. raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
  1442. meta = {
  1443. 'mode': mode,
  1444. 'measurement': measurement,
  1445. 'sensorimotor': {'auditory':'auditory','motor':'motor','visual':'visual'}.get(sensorimotor,None)
  1446. }
  1447. # Load BSPM data from .rhs file
  1448. MP = BSPM.from_file(filepath=filepath,layout=ECG_lyt,metadata=meta,refchs=[])
  1449. # Save BSPM object
  1450. MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\\'+filename+'.bspm')
  1451. # Call DataSaver function
  1452. apply2all(read_directory=readpath,extension='.rhs',func=func)
  1453. # %% [markdown]
  1454. # ##### Extract EEG features
  1455. # %%
  1456. import numpy as np
  1457. import pandas as pd
  1458. import scipy.signal as signal
  1459. from biolab.utils import apply2all
  1460. from biolab.bspm import BSPM
  1461. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM'
  1462. # Function to normalize EEG signals (Z-score normalization)
  1463. def normalize_signals(eeg_data):
  1464. return (eeg_data - eeg_data.mean()) / eeg_data.std()
  1465. def compute_band_power(eeg_signal, sf, band, method='welch'):
  1466. """Compute power spectral density (PSD) or average magnitude for a given EEG signal."""
  1467. low, high = band
  1468. if method == 'welch':
  1469. freqs, psd = signal.welch(eeg_signal, sf, nperseg=sf*2) # 2-sec windows
  1470. band_power = np.trapz(psd[(freqs >= low) & (freqs <= high)], freqs[(freqs >= low) & (freqs <= high)])
  1471. return band_power
  1472. elif method == 'fft':
  1473. freqs = np.fft.rfftfreq(len(eeg_signal), d=1/sf)
  1474. psd = np.abs(np.fft.rfft(eeg_signal))**2 / len(eeg_signal)
  1475. band_power = np.trapz(psd[(freqs >= low) & (freqs <= high)], freqs[(freqs >= low) & (freqs <= high)])
  1476. return band_power
  1477. elif method == 'magnitude': # Compute average magnitude using RMS
  1478. filtered_signal = bandpass_filter(eeg_signal, low, high, sf)
  1479. return np.sqrt(np.mean(filtered_signal**2)) # RMS magnitude
  1480. else:
  1481. raise ValueError("Invalid method. Choose 'welch', 'fft', or 'magnitude'.")
  1482. def bandpass_filter(data, lowcut, highcut, sf, order=4):
  1483. """Apply a bandpass filter to the EEG signal."""
  1484. b, a = signal.butter(order, [lowcut, highcut], fs=sf, btype='band')
  1485. return signal.filtfilt(b, a, data)
  1486. # Define bands
  1487. bands = {'Theta': (4, 8), 'Alpha': (8, 13), 'Beta': (13, 30), 'Gamma': (30, 100)}
  1488. def f(filepath, method='power'): # Choose 'power' or 'magnitude'
  1489. EEG = BSPM.load(loadpath=filepath)
  1490. rEEG = EEG.resample(newfs=1000)
  1491. rEEG.preprocess(bw=(0.5, 100))
  1492. sf = 1000 # Sampling frequency (Hz)
  1493. # Define start & end times based on the file type
  1494. segment_map = {
  1495. 'auditory.bspm': (8.5,9.5),
  1496. 'motor.bspm': (10.2,11.2),
  1497. 'visual.bspm': (6.5,7.5)
  1498. }
  1499. for key, (start, end) in segment_map.items():
  1500. if key in filepath:
  1501. break
  1502. # Extract the EEG segment
  1503. segment = rEEG.preprocessed_data.iloc[int(start*sf):int(end*sf)]
  1504. # Normalize EEG data
  1505. segment_normalized = segment.apply(normalize_signals, axis=0)
  1506. # Compute feature (power or magnitude) for each band & channel
  1507. band_feature_list = []
  1508. band_feature_dict = {}
  1509. for band_name, band_range in bands.items():
  1510. feature_values = segment_normalized.apply(lambda x: compute_band_power(x.values, sf, band_range, method=method), axis=0)
  1511. for ch in feature_values.index:
  1512. band_feature_dict[f"{ch}_{band_name}"] = feature_values[ch]
  1513. band_feature_list.append(band_feature_dict)
  1514. # Convert list to DataFrame
  1515. df_band_feature = pd.DataFrame(band_feature_list)
  1516. df_band_feature['action'] = EEG.metadata['sensorimotor']
  1517. # Save to Excel
  1518. excel_path = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\EEG_bands_features.xlsx'
  1519. try:
  1520. existing_df = pd.read_excel(excel_path)
  1521. combined_df = pd.concat([existing_df, df_band_feature], ignore_index=True)
  1522. except FileNotFoundError:
  1523. combined_df = df_band_feature
  1524. with pd.ExcelWriter(excel_path, mode='w', engine='openpyxl') as writer:
  1525. combined_df.to_excel(writer, index=False)
  1526. # Apply to all files
  1527. apply2all(read_directory=readpath, extension='.bspm', exclude=['Testing'], func=lambda fp: f(fp, method='fft')) # Change method here
  1528. # %% [markdown]
  1529. # ##### Generate BSPM
  1530. # %%
  1531. import matplotlib.pyplot as plt
  1532. import numpy as np
  1533. from biolab.bspm import interpolate,BSPM
  1534. limit = 50
  1535. # Read .bspm data for sphere
  1536. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\auditory.bspm'
  1537. EEG = BSPM.load(loadpath=readpath)
  1538. rEEG_auditory = EEG.resample(newfs=1000)
  1539. rEEG_auditory.preprocess(bw=(0.5,100))
  1540. # Display cardiac BSPM
  1541. vmap = rEEG_auditory.potential(time=16.4)
  1542. frame = interpolate(valmap=vmap,method='cubic')
  1543. plt.imshow(frame,vmin=-limit,vmax=limit)
  1544. cbar = plt.colorbar()
  1545. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  1546. # Remove the axes
  1547. plt.axis('off')
  1548. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Auditory.pdf',dpi=1000)
  1549. plt.show()
  1550. # Read .bspm data for sphere
  1551. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\motor.bspm'
  1552. EEG = BSPM.load(loadpath=readpath)
  1553. rEEG_motor = EEG.resample(newfs=1000)
  1554. rEEG_motor.preprocess(bw=(0.5,100))
  1555. # Display cardiac BSPM
  1556. vmap = rEEG_motor.potential(time=8.2)
  1557. frame = interpolate(valmap=vmap,method='cubic')
  1558. plt.imshow(frame,vmin=-limit,vmax=limit)
  1559. cbar = plt.colorbar()
  1560. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  1561. # Remove the axes
  1562. plt.axis('off')
  1563. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Motor.pdf',dpi=1000)
  1564. plt.show()
  1565. # Read .bspm data for sphere
  1566. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\visual.bspm'
  1567. EEG = BSPM.load(loadpath=readpath)
  1568. rEEG_visual = EEG.resample(newfs=200)
  1569. rEEG_visual.preprocess(bw=(0.5,99))
  1570. # Display cardiac BSPM
  1571. vmap = rEEG_visual.potential(time=18.2)
  1572. frame = interpolate(valmap=vmap,method='cubic')
  1573. plt.imshow(frame,vmin=-limit,vmax=limit)
  1574. cbar = plt.colorbar()
  1575. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  1576. # Remove the axes
  1577. plt.axis('off')
  1578. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Visual.pdf',dpi=1000)
  1579. plt.show()
  1580. # %%
  1581. import pandas as pd
  1582. import plotly.express as px
  1583. from scipy.signal import butter, filtfilt
  1584. from biolab.bspm import BSPM
  1585. # Define read directory
  1586. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\motor.bspm'
  1587. # Load .bspm data
  1588. EEG = BSPM.load(loadpath=readpath)
  1589. # Resample and preprocess the data
  1590. rEEG = EEG.resample(newfs=1000)
  1591. rEEG.preprocess(bw=(0.5, 100))
  1592. # Define bandpass filter function
  1593. def bandpass_filter(data, lowcut, highcut, sf, order=4):
  1594. return filtfilt(*butter(order, [lowcut, highcut], fs=sf, btype='band'), data)
  1595. # Define EEG bands
  1596. bands = {
  1597. #'Delta (0.5-4 Hz)': (0.5, 4),
  1598. 'Theta (4-8 Hz)': (4, 8),
  1599. 'Alpha (8-13 Hz)': (8, 13),
  1600. 'Beta (13-30 Hz)': (13, 30),
  1601. 'Gamma (30-100 Hz)': (30, 100)
  1602. }
  1603. # Initialize DataFrame for plotting
  1604. filtered_data = []
  1605. for channel in rEEG.preprocessed_data.columns:
  1606. for band, (low, high) in bands.items():
  1607. filtered_signal = bandpass_filter(rEEG.preprocessed_data[channel], low, high, 1000)
  1608. temp_df = pd.DataFrame({
  1609. 'Time (s)': rEEG.preprocessed_data.index.values,
  1610. 'Amplitude': filtered_signal,
  1611. 'Band': band,
  1612. 'Channel': channel
  1613. })
  1614. filtered_data.append(temp_df)
  1615. # Combine all data into a single DataFrame
  1616. df_melted = pd.concat(filtered_data, ignore_index=True)
  1617. # Plot with Plotly Express (all in one figure)
  1618. fig = px.line(
  1619. df_melted,
  1620. x='Time (s)',
  1621. y='Amplitude',
  1622. color='Band', # Different colors for bands
  1623. line_dash='Channel', # Different line styles for channels
  1624. title='EEG Traces Across Channels and Frequency Bands'
  1625. )
  1626. fig.show()
  1627. # %% [markdown]
  1628. # ##### Generate maps of anything but BSP
  1629. # %%
  1630. import pandas as pd
  1631. import numpy as np
  1632. import matplotlib.pyplot as plt
  1633. from biolab.bspm import BSPM,interpolate
  1634. def mapping(bspm,values) -> np.ndarray:
  1635. # Create copy of mapped layout
  1636. pmap = np.copy(bspm.layout).astype(float)
  1637. pmap[pmap == 0] = np.NaN
  1638. # Convert channel_data to numpy arrays
  1639. locations = bspm.channel_data['location'].values
  1640. # Change value of label in layout to potential taking into account reference channels
  1641. # Create a mapping from location to voltage channel
  1642. location_to_voltage = dict(zip(locations,values))
  1643. # Replace values in zmap based on location_to_impedance mapping
  1644. mask = np.isin(bspm.layout,locations)
  1645. pmap[mask] = np.vectorize(location_to_voltage.get)(bspm.layout[mask])
  1646. return pmap
  1647. # Load data
  1648. file_path = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\EEG_bands_features.xlsx'
  1649. data = pd.read_excel(file_path)
  1650. # Get the lowest value for each action
  1651. data_min = data.groupby('action',as_index=False).min()
  1652. # Select only Gamma band columns
  1653. band = 'Theta'
  1654. signal = 'Power_reg'
  1655. units = {'Power':'$\mu$V$^2$','RMS':'$\mu$V','Power_reg':'$\mu$V$^2$'}
  1656. unit = units[signal]
  1657. limits = {'Theta':500,'Alpha':200,'Beta':150,'Gamma':15} # RMS -> {'Theta':0.75,'Alpha':0.5,'Beta':0.5,'Gamma':0.1}
  1658. limit = limits[band]
  1659. data_min = data_min.loc[:,data_min.columns.str.contains(band,case=False,na=False)]
  1660. # Display power maps
  1661. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\auditory.bspm'
  1662. EEG = BSPM.load(loadpath=readpath)
  1663. rEEG_auditory = EEG.resample(newfs=1000)
  1664. pmap = mapping(rEEG_auditory,data_min.iloc[0,:].values)
  1665. frame = interpolate(valmap=pmap,method='cubic')
  1666. plt.imshow(frame,vmin=0,vmax=limit,cmap='viridis')
  1667. cbar = plt.colorbar()
  1668. cbar.set_label('{} [{}]'.format(signal,unit)) # Setting the label for the colorbar
  1669. # Remove the axes
  1670. plt.axis('off')
  1671. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\\'+'{}_auditory_{}'.format(band,signal)+'_viridis.pdf',dpi=1000)
  1672. plt.show()
  1673. # Display power maps
  1674. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\motor.bspm'
  1675. EEG = BSPM.load(loadpath=readpath)
  1676. rEEG_motor = EEG.resample(newfs=1000)
  1677. pmap = mapping(rEEG_motor,data_min.iloc[1,:].values)
  1678. frame = interpolate(valmap=pmap,method='cubic')
  1679. plt.imshow(frame,vmin=0,vmax=limit,cmap='viridis')
  1680. cbar = plt.colorbar()
  1681. cbar.set_label('{} [{}]'.format(signal,unit)) # Setting the label for the colorbar
  1682. # Remove the axes
  1683. plt.axis('off')
  1684. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\\'+'{}_motor_{}'.format(band,signal)+'_viridis.pdf',dpi=1000)
  1685. plt.show()
  1686. # Display power maps
  1687. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\EEG_classification\BSPM\visual.bspm'
  1688. EEG = BSPM.load(loadpath=readpath)
  1689. rEEG_visual = EEG.resample(newfs=1000)
  1690. pmap = mapping(rEEG_visual,data_min.iloc[2,:].values)
  1691. frame = interpolate(valmap=pmap,method='cubic')
  1692. plt.imshow(frame,vmin=0,vmax=limit,cmap='viridis')
  1693. cbar = plt.colorbar()
  1694. cbar.set_label('{} [{}]'.format(signal,unit)) # Setting the label for the colorbar
  1695. # Remove the axes
  1696. plt.axis('off')
  1697. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\\'+'{}_visual_{}'.format(band,signal)+'_viridis.pdf',dpi=1000)
  1698. plt.show()
  1699. # %% [markdown]
  1700. # ## Multi-modal BSPM
  1701. # %% [markdown]
  1702. # ### Data import
  1703. # %%
  1704. import os
  1705. import re
  1706. from biolab.bspm import BSPM,image2layout
  1707. from biolab.utils import apply2all
  1708. # Save new recordings into .bspm files
  1709. # Define parameters
  1710. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\Multi-modal_recording'
  1711. # Convert electrode array image into layout
  1712. EMG_EEG_lyt = image2layout(image_path=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EEG-EMG_lyt.jpg',seq='cw')
  1713. # Define operation to be applied to each file
  1714. def func(filepath):
  1715. # Extract filename from filepath
  1716. filename = os.path.splitext(os.path.basename(filepath))[0]
  1717. # Define the regex pattern to match the required values
  1718. pattern = r'^([A-Za-z_]+)_\d{6}_\d{6}$'
  1719. # Search for the pattern in the given filename
  1720. match = re.search(pattern,filename)
  1721. if match:
  1722. filename = match.group(1)
  1723. mode = 'EMG/ECG'
  1724. movement = filename
  1725. measurement = 'M1'
  1726. else:
  1727. raise ValueError("Filename \"{}\" does not match the expected pattern.".format(filename))
  1728. meta = {
  1729. 'mode': mode,
  1730. 'measurement': measurement,
  1731. 'movement': movement
  1732. }
  1733. savepath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Multi-modal\\'+filename+'.bspm'
  1734. if not os.path.isfile(savepath):
  1735. # Load BSPM data from .rhs file
  1736. MP = BSPM.from_file(filepath=filepath,layout=EMG_EEG_lyt,metadata=meta,refchs=[])
  1737. # Save BSPM object
  1738. MP.save(savepath=savepath)
  1739. # Call DataSaver function
  1740. apply2all(read_directory=readpath,extension='.rhs',exclude=['Individual_recordings'],func=func)
  1741. # %% [markdown]
  1742. # ### Joint cerebral and muscular BSPM
  1743. # %% [markdown]
  1744. # Import multimodal data
  1745. # %%
  1746. from biolab.bspm import BSPM
  1747. # Read .bspm data for fist
  1748. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\Multi-modal\fist.bspm'
  1749. FIST = BSPM.load(loadpath=readpath)
  1750. rEMGFIST = FIST.resample(newfs=1000)
  1751. rEEGFIST = FIST.resample(newfs=1000)
  1752. rEMGFIST.preprocess(bw=(5,400))
  1753. rEEGFIST.preprocess(bw=(0.5,100))
  1754. # Read .bspm data for up
  1755. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\Multi-modal\up.bspm'
  1756. UP = BSPM.load(loadpath=readpath)
  1757. rEMGUP = UP.resample(newfs=1000)
  1758. rEEGUP = UP.resample(newfs=1000)
  1759. rEMGUP.preprocess(bw=(5,400))
  1760. rEEGUP.preprocess(bw=(0.5,100))
  1761. # %% [markdown]
  1762. # Display EEG power bands
  1763. # %%
  1764. import pandas as pd
  1765. import plotly.express as px
  1766. from scipy.signal import butter, filtfilt
  1767. # Define data to be displayed
  1768. data = rEEGUP.preprocessed_data
  1769. # Define bandpass filter function
  1770. def bandpass_filter(data, lowcut, highcut, sf, order=4):
  1771. return filtfilt(*butter(order, [lowcut, highcut], fs=sf, btype='band'), data)
  1772. # Define EEG bands
  1773. bands = {
  1774. #'Delta (0.5-4 Hz)': (0.5, 4),
  1775. 'Theta (4-8 Hz)': (4, 8),
  1776. 'Alpha (8-13 Hz)': (8, 13),
  1777. 'Beta (13-30 Hz)': (13, 30),
  1778. 'Gamma (30-100 Hz)': (30, 100)
  1779. }
  1780. # Initialize DataFrame for plotting
  1781. filtered_data = []
  1782. for channel in data.filter(like="A").columns:
  1783. for band, (low, high) in bands.items():
  1784. filtered_signal = bandpass_filter(data[channel], low, high, 1000)
  1785. temp_df = pd.DataFrame({
  1786. 'Time (s)': data.index.values,
  1787. 'Amplitude': filtered_signal,
  1788. 'Band': band,
  1789. 'Channel': channel
  1790. })
  1791. filtered_data.append(temp_df)
  1792. # Combine all data into a single DataFrame
  1793. df_melted = pd.concat(filtered_data, ignore_index=True)
  1794. # Plot with Plotly Express (all in one figure)
  1795. fig = px.line(
  1796. df_melted,
  1797. x='Time (s)',
  1798. y='Amplitude',
  1799. color='Band', # Different colors for bands
  1800. line_dash='Channel', # Different line styles for channels
  1801. title='EEG Traces Across Channels and Frequency Bands'
  1802. )
  1803. fig.show()
  1804. # %% [markdown]
  1805. # Display EMG envelopes
  1806. # %%
  1807. import plotly.express as px
  1808. data = rEMGFIST.preprocessed_data
  1809. # Ensure the DataFrame is properly referenced
  1810. fig = px.line(
  1811. data.abs(),#.rolling(window=500).mean(),
  1812. x=data.index,
  1813. y=data.columns # Specify the column(s) to plot
  1814. )
  1815. fig.show()
  1816. # %% [markdown]
  1817. # Generate EMG BSPM
  1818. # %%
  1819. import copy
  1820. from biolab.bspm import interpolate,layout2map
  1821. data = copy.deepcopy(rEMGFIST.preprocessed_data.head(30000).abs().rolling(window=500).mean())
  1822. chdata = copy.deepcopy(rEMGFIST.channel_data)
  1823. # Display cardiac BSPM
  1824. chdata['custom_channel_name'] = ['A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','B','B','B','B','B','B','B','B','B','B','B','B','B','B','B','B']+chdata['custom_channel_name']
  1825. # Step 1: Create a mapping from 'locations' to 'custom_channel_names'
  1826. location_to_channel = dict(zip(chdata['location'],chdata['custom_channel_name']))
  1827. # Step 2: Rearrange the recdata columns based on the locations order
  1828. # Match the locations to the recdata columns
  1829. reordered_columns = [location_to_channel[loc] for loc in chdata['location'] if location_to_channel[loc] in rEMGFIST.preprocessed_data.columns]
  1830. # Step 3: Get the values at the specific index from recdata with the correct column order
  1831. values_at_index = data.loc[15.045,reordered_columns] # FIST = PRE (14) PERI (15.045) # UP = PRE (9.68) PERI (10.218)
  1832. vmap = layout2map(layout=rEMGFIST.layout,mapping=dict(zip(chdata['location'].values,values_at_index)))
  1833. frame = interpolate(valmap=vmap,method='cubic')
  1834. # %% [markdown]
  1835. # Generate EEG BSPM
  1836. # %%
  1837. import copy
  1838. from biolab.bspm import interpolate,layout2map
  1839. import numpy as np
  1840. import pandas as pd
  1841. import scipy.signal as signal
  1842. # Data to be used
  1843. data = copy.deepcopy(rEMGUP.preprocessed_data.head(30000))
  1844. method = 'fft'
  1845. # Function to normalize EEG signals (Z-score normalization)
  1846. def normalize_signals(eeg_data):
  1847. return (eeg_data - eeg_data.mean()) / eeg_data.std()
  1848. def compute_band_power(eeg_signal, sf, band, method='welch'):
  1849. """Compute power spectral density (PSD) or average magnitude for a given EEG signal."""
  1850. low, high = band
  1851. if method == 'welch':
  1852. freqs, psd = signal.welch(eeg_signal, sf, nperseg=sf*2) # 2-sec windows
  1853. band_power = np.trapz(psd[(freqs >= low) & (freqs <= high)], freqs[(freqs >= low) & (freqs <= high)])
  1854. return band_power
  1855. elif method == 'fft':
  1856. freqs = np.fft.rfftfreq(len(eeg_signal), d=1/sf)
  1857. psd = np.abs(np.fft.rfft(eeg_signal))**2 / len(eeg_signal)
  1858. band_power = np.trapz(psd[(freqs >= low) & (freqs <= high)], freqs[(freqs >= low) & (freqs <= high)])
  1859. return band_power
  1860. elif method == 'magnitude': # Compute average magnitude using RMS
  1861. filtered_signal = bandpass_filter(eeg_signal, low, high, sf)
  1862. return np.sqrt(np.mean(filtered_signal**2)) # RMS magnitude
  1863. else:
  1864. raise ValueError("Invalid method. Choose 'welch', 'fft', or 'magnitude'.")
  1865. def bandpass_filter(data, lowcut, highcut, sf, order=4):
  1866. """Apply a bandpass filter to the EEG signal."""
  1867. b, a = signal.butter(order, [lowcut, highcut], fs=sf, btype='band')
  1868. return signal.filtfilt(b, a, data)
  1869. # Define bands
  1870. bands = {'Theta': (4, 8), 'Alpha': (8, 13), 'Beta': (13, 30), 'Gamma': (30, 100)}
  1871. sf = 1000 # Sampling frequency (Hz)
  1872. segment_map = (14.4,15.3) # FIST = PRE (12.2,14.2) PERI (14.2,16.2) # UP = PRE (13.5,14.4) PERI (14.4,15.3)
  1873. # Extract the EEG segment
  1874. segment = data.iloc[int(segment_map[0]*sf):int(segment_map[1]*sf)]
  1875. # Normalize EEG data
  1876. segment_normalized = segment.apply(normalize_signals, axis=0)
  1877. # Compute feature (power or magnitude) for each band & channel
  1878. band_feature_list = []
  1879. band_feature_dict = {}
  1880. for band_name, band_range in bands.items():
  1881. feature_values = segment_normalized.apply(lambda x: compute_band_power(x.values, sf, band_range, method=method), axis=0)
  1882. for ch in feature_values.index:
  1883. band_feature_dict[f"{ch}_{band_name}"] = feature_values[ch]
  1884. band_feature_list.append(band_feature_dict)
  1885. # Convert list to DataFrame
  1886. df_band_feature = pd.DataFrame(band_feature_list)
  1887. # %%
  1888. chdata = copy.deepcopy(rEEGUP.channel_data)
  1889. wave = 'Beta'
  1890. # Display BSPM
  1891. chdata['custom_channel_name'] = ['A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','A','B','B','B','B','B','B','B','B','B','B','B','B','B','B','B','B']+chdata['custom_channel_name']
  1892. # Step 1: Create a mapping from 'locations' to 'custom_channel_names'
  1893. location_to_channel = dict(zip(chdata['location'],chdata['custom_channel_name']+'_{}'.format(wave)))
  1894. # Step 2: Rearrange the recdata columns based on the locations order
  1895. # Match the locations to the recdata columns
  1896. reordered_columns = [location_to_channel[loc] for loc in chdata['location'] if location_to_channel[loc] in df_band_feature.filter(like=wave)]
  1897. # Step 3: Get the values at the specific index from recdata with the correct column order
  1898. values_at_index = df_band_feature.loc[0,reordered_columns]
  1899. vmap = layout2map(layout=rEEGUP.layout,mapping=dict(zip(chdata['location'].values,values_at_index)))
  1900. frame = interpolate(valmap=vmap,method='cubic')
  1901. # %%
  1902. import matplotlib.pyplot as plt
  1903. limit = 100
  1904. plt.imshow(frame,vmin=0,vmax=limit,cmap='viridis')
  1905. cbar = plt.colorbar()
  1906. cbar.set_label('Voltage [$\mu$V]') # Setting the label for the colorbar
  1907. # Remove the axes
  1908. plt.axis('off')
  1909. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\Multimodal_EMG_peri_fist_viridis.pdf',dpi=1000)
  1910. plt.show()
  1911. # %% [markdown]
  1912. # ### Reaction time
  1913. # %% [markdown]
  1914. # Import multimodal data
  1915. # %%
  1916. from biolab.bspm import BSPM
  1917. # Read .bspm data for fist
  1918. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Multi-modal\fist.bspm'
  1919. FIST = BSPM.load(loadpath=readpath)
  1920. rEMGFIST = FIST.resample(newfs=1000)
  1921. rEEGFIST = FIST.resample(newfs=1000)
  1922. rEMGFIST.preprocess(bw=(5,400))
  1923. rEEGFIST.preprocess(bw=(0.5,100))
  1924. # Read .bspm data for up
  1925. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Multi-modal\up.bspm'
  1926. UP = BSPM.load(loadpath=readpath)
  1927. rEMGUP = UP.resample(newfs=1000)
  1928. rEEGUP = UP.resample(newfs=1000)
  1929. rEMGUP.preprocess(bw=(5,400))
  1930. rEEGUP.preprocess(bw=(0.5,100))
  1931. # %% [markdown]
  1932. # Display EEG power bands
  1933. # %%
  1934. import pandas as pd
  1935. import plotly.express as px
  1936. from scipy.signal import butter, filtfilt
  1937. import copy
  1938. # Define data to be displayed
  1939. data = copy.deepcopy(rEEGUP.preprocessed_data.head(30000))
  1940. # Define bandpass filter function
  1941. def bandpass_filter(data, lowcut, highcut, sf, order=4):
  1942. return filtfilt(*butter(order, [lowcut, highcut], fs=sf, btype='band'), data)
  1943. # Define EEG bands
  1944. bands = {
  1945. #'Delta (0.5-4 Hz)': (0.5, 4),
  1946. 'Theta (4-8 Hz)': (4, 8),
  1947. 'Alpha (8-13 Hz)': (8, 13),
  1948. 'Beta (13-30 Hz)': (13, 30),
  1949. 'Gamma (30-100 Hz)': (30, 100)
  1950. }
  1951. # Initialize DataFrame for plotting
  1952. filtered_data = []
  1953. for channel in data.filter(like="A").columns:
  1954. for band, (low, high) in bands.items():
  1955. filtered_signal = bandpass_filter(data[channel], low, high, 1000)
  1956. temp_df = pd.DataFrame({
  1957. 'Time (s)': np.lib.stride_tricks.sliding_window_view(np.abs(data.index.values),500).mean(axis=1),
  1958. 'Amplitude': np.lib.stride_tricks.sliding_window_view(np.abs(filtered_signal),500).mean(axis=1),
  1959. 'Band': band,
  1960. 'Channel': channel
  1961. })
  1962. filtered_data.append(temp_df)
  1963. # Combine all data into a single DataFrame
  1964. df_melted = pd.concat(filtered_data, ignore_index=True)
  1965. # Plot with Plotly Express (all in one figure)
  1966. fig = px.line(
  1967. df_melted,
  1968. x='Time (s)',
  1969. y='Amplitude',
  1970. color='Band', # Different colors for bands
  1971. line_dash='Channel', # Different line styles for channels
  1972. title='EEG Traces Across Channels and Frequency Bands'
  1973. )
  1974. fig.show()
  1975. # %% [markdown]
  1976. # Display EMG envelopes
  1977. # %%
  1978. import plotly.express as px
  1979. data = rEMGUP.preprocessed_data.head(30000)
  1980. # Ensure the DataFrame is properly referenced
  1981. fig = px.line(
  1982. data.abs().rolling(window=500).mean(), #.sub(data.iloc[0],axis=1),
  1983. x=data.index,
  1984. y=data.columns[data.columns.str.contains("B")] # Specify the column(s) to plot
  1985. )
  1986. fig.show()
  1987. # %% [markdown]
  1988. # Compute peaks for EMG data for display
  1989. # %%
  1990. import plotly.graph_objects as go
  1991. from scipy.signal import find_peaks
  1992. import copy
  1993. data = copy.deepcopy(rEMGUP.preprocessed_data.head(90000).abs().rolling(window=500).mean().dropna())
  1994. df_emg = data[[col for col in data.columns if "B" in col]]
  1995. # Function to find peaks
  1996. def get_peaks(df):
  1997. peaks_dict = {}
  1998. for col in df.columns:
  1999. peaks, _ = find_peaks(df[col],height=20, distance=1000) # Adjust height threshold as needed
  2000. peaks_dict[col] = peaks
  2001. return peaks_dict
  2002. # Compute the peaks
  2003. peaks_dict_emg = get_peaks(df_emg)
  2004. # Plot with Plotly
  2005. fig = go.Figure()
  2006. # Add the channel signals
  2007. for col in df_emg.columns:
  2008. fig.add_trace(go.Scatter(
  2009. x=df_emg.index,
  2010. y=df_emg[col],
  2011. mode='lines',
  2012. name=col
  2013. ))
  2014. # Add the peaks as scatter points
  2015. for col, peaks in peaks_dict_emg.items():
  2016. fig.add_trace(go.Scatter(
  2017. x=df_emg.index[peaks], # Use time for x-axis
  2018. y=df_emg[col].iloc[peaks],
  2019. mode='markers',
  2020. name=f"{col} Peaks",
  2021. marker=dict(size=8, symbol='circle', color='red')
  2022. ))
  2023. # Layout adjustments
  2024. fig.update_layout(
  2025. title="Interactive Peaks Plot with Time Axis",
  2026. xaxis_title="Time (seconds)",
  2027. yaxis_title="Amplitude",
  2028. legend=dict(title="Channels & Peaks"),
  2029. height=600,
  2030. width=1000
  2031. )
  2032. # Show the interactive plot
  2033. fig.show()
  2034. # %% [markdown]
  2035. # Compute peaks for EEG data (theta wave, motor planning) for display
  2036. # %%
  2037. import pandas as pd
  2038. import numpy as np
  2039. import plotly.graph_objects as go
  2040. from scipy.signal import butter, filtfilt, find_peaks
  2041. import copy
  2042. # Define bandpass filter function
  2043. def bandpass_filter(data, lowcut, highcut, sf, order=4):
  2044. """Applies a bandpass filter to the data."""
  2045. return filtfilt(*butter(order, [lowcut, highcut], fs=sf, btype='band'), data)
  2046. # Sampling frequency
  2047. sf = 1000
  2048. # Define EEG bands
  2049. bands = {
  2050. 'Theta (4-8 Hz)': (4, 8),
  2051. 'Alpha (8-13 Hz)': (8, 13),
  2052. 'Beta (13-30 Hz)': (13, 30),
  2053. 'Gamma (30-100 Hz)': (30, 100)
  2054. }
  2055. # Load and preprocess the data
  2056. data = copy.deepcopy(rEEGFIST.preprocessed_data.head(90000))
  2057. # Filter for theta waves across all channels
  2058. filtered_data = []
  2059. for channel in data.filter(like="A").columns:
  2060. filtered_signal = bandpass_filter(data[channel], bands['Theta (4-8 Hz)'][0], bands['Theta (4-8 Hz)'][1], sf)
  2061. # Apply rolling amplitude extraction
  2062. amplitude = np.lib.stride_tricks.sliding_window_view(np.abs(filtered_signal), 500).mean(axis=1)
  2063. # Align time index with the amplitude array
  2064. time_aligned = data.index.values[499:]
  2065. # Store filtered data
  2066. temp_df = pd.DataFrame({
  2067. 'Time (s)': time_aligned,
  2068. 'Amplitude': amplitude,
  2069. 'Channel': channel
  2070. })
  2071. filtered_data.append(temp_df)
  2072. # Combine all theta-filtered data into a single DataFrame
  2073. df_eeg = pd.concat(filtered_data, ignore_index=True)
  2074. # Function to detect peaks
  2075. def get_peaks(df, height=0.5, distance=1000):
  2076. """Detect peaks across all channels."""
  2077. peaks_dict = {}
  2078. for channel in df['Channel'].unique():
  2079. channel_data = df[df['Channel'] == channel]
  2080. peaks, _ = find_peaks(channel_data['Amplitude'], height=height, distance=distance)
  2081. peaks_dict[channel] = peaks
  2082. return peaks_dict
  2083. # Detect peaks
  2084. peaks_dict_eeg = get_peaks(df_eeg, height=5, distance=1000) # Adjust height and distance as needed
  2085. # Create interactive plot
  2086. fig = go.Figure()
  2087. # Plot the theta amplitude traces
  2088. for channel in df_eeg['Channel'].unique():
  2089. channel_data = df_eeg[df_eeg['Channel'] == channel]
  2090. fig.add_trace(go.Scatter(
  2091. x=channel_data['Time (s)'],
  2092. y=channel_data['Amplitude'],
  2093. mode='lines',
  2094. name=f"{channel} Amplitude"
  2095. ))
  2096. # Plot the peaks as scatter points
  2097. for channel, peaks in peaks_dict_eeg.items():
  2098. channel_data = df_eeg[df_eeg['Channel'] == channel]
  2099. fig.add_trace(go.Scatter(
  2100. x=channel_data['Time (s)'].iloc[peaks], # Use correct time index
  2101. y=channel_data['Amplitude'].iloc[peaks],
  2102. mode='markers',
  2103. name=f"{channel} Peaks",
  2104. marker=dict(size=8, symbol='circle', color='red')
  2105. ))
  2106. # Layout adjustments
  2107. fig.update_layout(
  2108. title="Theta Wave Peaks Across Channels",
  2109. xaxis_title="Time (seconds)",
  2110. yaxis_title="Amplitude",
  2111. legend=dict(title="Channels & Peaks"),
  2112. height=600,
  2113. width=1000
  2114. )
  2115. # Show the interactive plot
  2116. fig.show()
  2117. # %% [markdown]
  2118. # Apply to all data without visualising
  2119. # %%
  2120. import pandas as pd
  2121. import numpy as np
  2122. import copy
  2123. from scipy.signal import find_peaks, butter, filtfilt
  2124. DATA = (rEMGUP,rEEGUP)
  2125. # Load and preprocess the EMG data
  2126. data = copy.deepcopy(DATA[0].preprocessed_data.abs().rolling(window=900).mean().dropna())
  2127. df_emg = data[[col for col in data.columns if "B" in col]]
  2128. # Define bandpass filter function
  2129. def bandpass_filter(data, lowcut, highcut, sf, order=4):
  2130. """Applies a bandpass filter to the data."""
  2131. return filtfilt(*butter(order, [lowcut, highcut], fs=sf, btype='band'), data)
  2132. # Function to find peaks in EMG
  2133. def get_peaks(df):
  2134. peaks_dict = {}
  2135. for col in df.columns:
  2136. peaks, _ = find_peaks(df[col], height=20, distance=1000) # Adjust height threshold as needed
  2137. peaks_dict[col] = peaks
  2138. return peaks_dict
  2139. # Get peaks for EMG channels
  2140. peaks_dict_emg = get_peaks(df_emg)
  2141. # Load and preprocess the EEG data (filtered for theta band)
  2142. data = copy.deepcopy(DATA[1].preprocessed_data)
  2143. sf = 1000 # Sampling frequency
  2144. filtered_data = []
  2145. # Filter for theta waves across all channels (only A4 in EEG for delay calculation)
  2146. for channel in data.filter(like="A").columns:
  2147. filtered_signal = bandpass_filter(data[channel], 4, 8, sf) # Filtering for theta band (4-8 Hz)
  2148. amplitude = np.lib.stride_tricks.sliding_window_view(np.abs(filtered_signal), 500).mean(axis=1)
  2149. time_aligned = data.index.values[499:] # Align time with amplitude
  2150. temp_df = pd.DataFrame({'Time (s)': time_aligned, 'Amplitude': amplitude, 'Channel': channel})
  2151. filtered_data.append(temp_df)
  2152. df_eeg = pd.concat(filtered_data, ignore_index=True)
  2153. # Function to detect peaks in EEG
  2154. def get_peaks_eeg(df, height=0.5, distance=1000):
  2155. peaks_dict = {}
  2156. for channel in df['Channel'].unique():
  2157. channel_data = df[df['Channel'] == channel]
  2158. peaks, _ = find_peaks(channel_data['Amplitude'], height=height, distance=distance)
  2159. peaks_dict[channel] = peaks
  2160. return peaks_dict
  2161. # Get peaks for EEG (across all channels)
  2162. peaks_dict_eeg = get_peaks_eeg(df_eeg, height=5, distance=1000)
  2163. # Extract the time values for peaks from each EEG channel
  2164. eeg_peaks_times = {}
  2165. for channel in df_eeg['Channel'].unique():
  2166. eeg_peaks_times[channel] = df_eeg[df_eeg['Channel'] == channel]['Time (s)'].iloc[peaks_dict_eeg[channel]].values
  2167. # Extract the time values for peaks in channel A4 specifically
  2168. a4_peaks_times = eeg_peaks_times.get('A4', [])
  2169. # Function to calculate delay between EEG and EMG peaks with tolerance check and no reuse of EEG peaks
  2170. def calculate_delays_with_tolerance(a4_peaks_times, emg_peaks_dict, df_emg, eeg_peaks_times, min_tolerance=0.1, max_tolerance=0.6):
  2171. delays = {}
  2172. # Initialize a dictionary to track the matched peaks for each EEG channel
  2173. matched_eeg_peaks = {channel: set() for channel in eeg_peaks_times}
  2174. # Calculate the maximum number of peaks across all EMG channels
  2175. max_emg_peaks = max(len(peaks) for peaks in emg_peaks_dict.values())
  2176. for emg_channel, emg_peaks in emg_peaks_dict.items():
  2177. emg_peaks_times = df_emg[emg_channel].index[emg_peaks].values # Get time indices for EMG peaks
  2178. emg_delays = []
  2179. for emg_time in emg_peaks_times:
  2180. # For each EMG peak, try to match it with the EEG peaks of each channel
  2181. matched_delay = False
  2182. for eeg_channel, a4_peaks_times in eeg_peaks_times.items():
  2183. # Find the closest EEG peak in the current EEG channel within the tolerance range, excluding already matched peaks
  2184. time_diffs = emg_time - a4_peaks_times # Time differences
  2185. valid_peaks_times = a4_peaks_times[np.where((time_diffs >= min_tolerance) & (time_diffs <= max_tolerance))[0]]
  2186. # Exclude matched EEG peaks for the current EEG channel
  2187. valid_peaks_times = [t for t in valid_peaks_times if t not in matched_eeg_peaks[eeg_channel]]
  2188. if valid_peaks_times:
  2189. # Find the closest peak within the valid range
  2190. closest_eeg_peak_time = valid_peaks_times[np.argmin(np.abs(valid_peaks_times - emg_time))]
  2191. delay = emg_time - closest_eeg_peak_time
  2192. emg_delays.append(delay)
  2193. # Mark this EEG peak as matched (add to the set for this EEG channel)
  2194. matched_eeg_peaks[eeg_channel].add(closest_eeg_peak_time)
  2195. matched_delay = True
  2196. break
  2197. if not matched_delay:
  2198. # If no valid peaks are found within the tolerance range, append NaN to indicate no match
  2199. emg_delays.append(np.nan)
  2200. # Pad the emg_delays array with NaN if its length is less than the maximum number of EMG peaks
  2201. if len(emg_delays) < max_emg_peaks:
  2202. emg_delays.extend([np.nan] * (max_emg_peaks - len(emg_delays)))
  2203. delays[emg_channel] = emg_delays
  2204. return pd.DataFrame(delays)
  2205. # Calculate delays between EEG and all EMG channels with tolerance check and no reuse of EEG peaks for each channel
  2206. delay_df = calculate_delays_with_tolerance(a4_peaks_times, peaks_dict_emg, df_emg, eeg_peaks_times, min_tolerance=0.1, max_tolerance=0.6)
  2207. # %% [markdown]
  2208. # Display histogram of reaction times
  2209. # %%
  2210. import matplotlib.pyplot as plt
  2211. import pandas as pd
  2212. import numpy as np
  2213. from scipy.stats import norm
  2214. # Import data
  2215. delaymatrix = pd.read_excel(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\Reaction_time.xlsx')
  2216. # Flatten all numerical data into a single series
  2217. all_data_up = delaymatrix[delaymatrix['movement'] == 'UP'].select_dtypes(include='number').values.flatten()
  2218. all_data_fist = delaymatrix[delaymatrix['movement'] == 'FIST'].select_dtypes(include='number').values.flatten()
  2219. # Remove non-finite values (NaN and inf)
  2220. all_data_up = all_data_up[np.isfinite(all_data_up)]
  2221. all_data_fist = all_data_fist[np.isfinite(all_data_fist)]
  2222. # Calculate the Gaussian fitting parameters (mean and std) for both datasets
  2223. mu_up, std_up = norm.fit(all_data_up)
  2224. mu_fist, std_fist = norm.fit(all_data_fist)
  2225. # Plot the histogram
  2226. plt.figure(figsize=(6,6))
  2227. # Plot histogram for 'UP' data
  2228. count_up, bins_up, patches_up = plt.hist(all_data_up, bins=10, color='blue', edgecolor='black', alpha=0.6, label='Wrist extension')
  2229. # Plot Gaussian fit for 'UP' data
  2230. xmin_up, xmax_up = plt.xlim()
  2231. x_up = np.linspace(xmin_up, xmax_up, 100)
  2232. p_up = norm.pdf(x_up, mu_up, std_up)
  2233. plt.plot(x_up, p_up * len(all_data_up) * (bins_up[1] - bins_up[0]), 'blue')
  2234. # Plot histogram for 'FIST' data
  2235. count_fist, bins_fist, patches_fist = plt.hist(all_data_fist, bins=10, color='green', edgecolor='black', alpha=0.6, label='Hand flexion')
  2236. # Plot Gaussian fit for 'FIST' data
  2237. xmin_fist, xmax_fist = plt.xlim()
  2238. x_fist = np.linspace(xmin_fist, xmax_fist, 100)
  2239. p_fist = norm.pdf(x_fist, mu_fist, std_fist)
  2240. plt.plot(x_fist, p_fist * len(all_data_fist) * (bins_fist[1] - bins_fist[0]), 'green')
  2241. # Title and labels
  2242. plt.title('Histogram of All Numerical Data with Gaussian Fit')
  2243. plt.xlabel('Time [s]')
  2244. plt.ylabel('Frequency')
  2245. plt.legend(frameon=False)
  2246. # Show the plot
  2247. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\Reaction_time_histogram.pdf',dpi=1000)
  2248. plt.show()
  2249. # %% [markdown]
  2250. # Display spatially the difference between reaction time across channels
  2251. # %%
  2252. from biolab.bspm import image2layout
  2253. lyt = image2layout(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EMG_lyt.jpg',seq='cw')
  2254. # %%
  2255. from biolab.bspm import layout2map
  2256. delaymatrix = pd.read_excel(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\Reaction_time.xlsx')
  2257. delaymatrixFIST = delaymatrix[delaymatrix['movement']=='FIST'].drop(columns=['movement'])
  2258. delaymatrixUP = delaymatrix[delaymatrix['movement']=='UP'].drop(columns=['movement'])
  2259. column_meansFIST = delaymatrixFIST.mean()
  2260. mean_dictFIST = {int(col[1:]): column_meansFIST[col] for col in delaymatrixFIST.columns}
  2261. vmapFIST = layout2map(layout=lyt,mapping=mean_dictFIST)
  2262. column_meansUP = delaymatrixUP.mean()
  2263. mean_dictUP = {int(col[1:]): column_meansUP[col] for col in delaymatrixUP.columns}
  2264. vmapUP = layout2map(layout=lyt,mapping=mean_dictUP)
  2265. # %%
  2266. from biolab.bspm import interpolate
  2267. intmapFIST = interpolate(valmap=vmapFIST,method='cubic')
  2268. intmapUP = interpolate(valmap=vmapUP,method='cubic')
  2269. # %%
  2270. plt.imshow(intmapFIST,vmin=0.275,vmax=0.34, cmap='Greens')
  2271. plt.colorbar()
  2272. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\Reaction_time_mapping_FIST_greens.pdf')
  2273. plt.show()
  2274. # %% [markdown]
  2275. # ### CMC computation
  2276. # %% [markdown]
  2277. # Import multimodal data
  2278. # %%
  2279. # =====================================================
  2280. # COMPLETE CORTICO-MUSCULAR COHERENCE PIPELINE
  2281. # USING EVENTS.XLSX INTERVALS (FIST ONLY)
  2282. # REMOVES LOW-FREQUENCY PEAKS
  2283. # =====================================================
  2284. import numpy as np
  2285. import pandas as pd
  2286. import matplotlib.pyplot as plt
  2287. from scipy.signal import butter, filtfilt, coherence
  2288. from biolab.bspm import BSPM
  2289. # =====================================================
  2290. # PARAMETERS
  2291. # =====================================================
  2292. FS = 1000
  2293. BETA_BAND = (15,30)
  2294. LOW_FREQ_CUTOFF = 8 # remove <8 Hz slow components
  2295. FIST_PATH = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\Multi-modal\fist.bspm'
  2296. EVENTS_PATH = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Cortico-muscular axis\Experiments\INTAN\Data\Multi-modal\events.xlsx'
  2297. # =====================================================
  2298. # LOAD BSPM
  2299. # =====================================================
  2300. def load_bspm(path):
  2301. obj = BSPM.load(loadpath=path)
  2302. r = obj.resample(newfs=FS)
  2303. r.preprocess(bw=(0.5,100))
  2304. return r
  2305. # =====================================================
  2306. # SPLIT EEG / EMG
  2307. # =====================================================
  2308. def split_eeg_emg(bspm_obj):
  2309. ch_info = bspm_obj.channel_data
  2310. data = bspm_obj.preprocessed_data
  2311. eeg_cols, emg_cols = [], []
  2312. for idx, row in ch_info.iterrows():
  2313. col_name = data.columns[idx]
  2314. if row["port_prefix"] == "A":
  2315. eeg_cols.append(col_name)
  2316. elif row["port_prefix"] == "B":
  2317. emg_cols.append(col_name)
  2318. eeg = data[eeg_cols].values
  2319. emg = data[emg_cols].values
  2320. return eeg, emg
  2321. # =====================================================
  2322. # PREPROCESS EMG (BANDPASS + RECTIFY + HIGH-PASS)
  2323. # =====================================================
  2324. def preprocess_emg(emg):
  2325. # Bandpass 5–400 Hz
  2326. b,a = butter(4, [5/(FS/2),400/(FS/2)], btype='band')
  2327. emg_filt = filtfilt(b,a, emg, axis=0)
  2328. emg_rect = np.abs(emg_filt)
  2329. # High-pass to remove <LOW_FREQ_CUTOFF Hz drift
  2330. b,a = butter(4, LOW_FREQ_CUTOFF/(FS/2), btype='high')
  2331. emg_hp = filtfilt(b,a, emg_rect, axis=0)
  2332. return emg_hp
  2333. # =====================================================
  2334. # LOAD EVENTS + EXTRACT SEGMENTS
  2335. # =====================================================
  2336. def extract_event_segments(eeg, emg, events_path):
  2337. events = pd.read_excel(events_path)
  2338. events = events[events["movement"]=="FIST"]
  2339. eeg_segments = []
  2340. emg_segments = []
  2341. for _, row in events.iterrows():
  2342. start_idx = int(row["start"] * FS)
  2343. end_idx = int(row["end"] * FS)
  2344. if end_idx > len(eeg):
  2345. continue
  2346. eeg_segments.append(eeg[start_idx:end_idx])
  2347. emg_segments.append(emg[start_idx:end_idx])
  2348. eeg_segments = np.array(eeg_segments, dtype=object)
  2349. emg_segments = np.array(emg_segments, dtype=object)
  2350. print("Number of event segments:", len(eeg_segments))
  2351. return eeg_segments, emg_segments
  2352. # =====================================================
  2353. # COMPUTE CMC
  2354. # =====================================================
  2355. def compute_cmc(eeg_segments, emg_segments, fmin=5, fmax=100, fstep=1):
  2356. # Define common frequency grid
  2357. common_freqs = np.arange(fmin, fmax+fstep, fstep)
  2358. coh_trials = []
  2359. NPERSEG = 512
  2360. for eeg_seg, emg_seg in zip(eeg_segments, emg_segments):
  2361. if len(eeg_seg) < NPERSEG:
  2362. continue
  2363. emg_mean = emg_seg.mean(axis=1)
  2364. for e in range(eeg_seg.shape[1]):
  2365. eeg_trial = eeg_seg[:,e] - np.mean(eeg_seg[:,e])
  2366. emg_trial = emg_mean - np.mean(emg_mean)
  2367. f, cxy = coherence(eeg_trial, emg_trial, fs=FS, nperseg=NPERSEG)
  2368. # Keep only fmin–fmax
  2369. mask = (f >= fmin) & (f <= fmax)
  2370. f_sel = f[mask]
  2371. cxy_sel = cxy[mask]
  2372. # Interpolate onto common grid
  2373. cxy_interp = np.interp(common_freqs, f_sel, cxy_sel)
  2374. coh_trials.append(cxy_interp)
  2375. coh_trials = np.vstack(coh_trials)
  2376. return common_freqs, coh_trials
  2377. # =====================================================
  2378. # MAIN PIPELINE
  2379. # =====================================================
  2380. print("\nProcessing FIST using event intervals")
  2381. obj = load_bspm(FIST_PATH)
  2382. eeg, emg = split_eeg_emg(obj)
  2383. emg = preprocess_emg(emg)
  2384. eeg_segments, emg_segments = extract_event_segments(
  2385. eeg,
  2386. emg,
  2387. EVENTS_PATH
  2388. )
  2389. freqs, coh_all = compute_cmc(eeg_segments, emg_segments)
  2390. # =====================================================
  2391. # BETA STRENGTH
  2392. # =====================================================
  2393. idx = (freqs>=BETA_BAND[0]) & (freqs<=BETA_BAND[1])
  2394. beta_strength = coh_all[:,idx].mean(axis=1)
  2395. # =====================================================
  2396. # DEBUG PLOT
  2397. # =====================================================
  2398. plt.figure()
  2399. plt.plot(freqs, np.mean(coh_all, axis=0))
  2400. plt.axvspan(*BETA_BAND, alpha=0.2)
  2401. plt.title("Mean CMC Spectrum (FIST — Event based, low-freq removed)")
  2402. plt.xlabel("Frequency (Hz)")
  2403. plt.ylabel("Coherence")
  2404. plt.show()
  2405. # %% [markdown]
  2406. # ### Muscular BSPM prediction from cerebral BSPM
  2407. # %% [markdown]
  2408. # Import multimodal data
  2409. # %%
  2410. from biolab.bspm import BSPM
  2411. # Read .bspm data for fist
  2412. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Multi-modal\fist.bspm'
  2413. FIST = BSPM.load(loadpath=readpath)
  2414. rEMGFIST = FIST.resample(newfs=1000)
  2415. rEEGFIST = FIST.resample(newfs=1000)
  2416. rEMGFIST.preprocess(bw=(5,400))
  2417. rEEGFIST.preprocess(bw=(0.5,100))
  2418. # Read .bspm data for up
  2419. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Multi-modal\up.bspm'
  2420. UP = BSPM.load(loadpath=readpath)
  2421. rEMGUP = UP.resample(newfs=1000)
  2422. rEEGUP = UP.resample(newfs=1000)
  2423. rEMGUP.preprocess(bw=(5,400))
  2424. rEEGUP.preprocess(bw=(0.5,100))
  2425. # %% [markdown]
  2426. # Define activity locations from EMG envelopes and reaction time
  2427. # %%
  2428. import copy
  2429. from scipy.signal import find_peaks
  2430. DATA = (rEMGFIST,rEEGFIST)
  2431. window_size = 500
  2432. # Load and preprocess EMG data
  2433. emgdata = copy.deepcopy(DATA[0].preprocessed_data.abs().rolling(window=window_size).mean().dropna())
  2434. emgdata = emgdata[[col for col in emgdata.columns if "B" in col]]
  2435. # Function to find peaks in EMG
  2436. def emgpeaks(df,window_size):
  2437. peaks_dict = {}
  2438. for col in df.columns:
  2439. peaks, _ = find_peaks(df[col], height=20, distance=1000) # Adjust height threshold as needed
  2440. # Adjust peak indices by the rolling window offset
  2441. adjusted_peaks = peaks + (window_size // 2)
  2442. peaks_dict[col] = adjusted_peaks
  2443. return peaks_dict
  2444. # Get peaks for EMG channels
  2445. emg_peak_locs = emgpeaks(emgdata,window_size)
  2446. # %% [markdown]
  2447. # - DEFINE WINDOW LENGTH 1s FOR BOTH EMG AND EEG ACTIVITY
  2448. # - DEFINE DELAY BETWEEN EMG AND EEG SIGNAL TO BE 300ms
  2449. # %% [markdown]
  2450. # Compute feature matrix from EEG bands power
  2451. # %%
  2452. import numpy as np
  2453. import pandas as pd
  2454. import scipy.signal as signal
  2455. eegdata = copy.deepcopy(DATA[0].preprocessed_data.abs().rolling(window=window_size).mean().dropna())
  2456. eegdata = eegdata[[col for col in eegdata.columns if "A" in col]]
  2457. sf = 1000 # Sampling frequency
  2458. eegbands = {
  2459. 'Delta':(0.5,4),
  2460. 'Theta':(4,8),
  2461. 'Alpha':(8,13),
  2462. 'Beta':(13,30),
  2463. 'Gamma':(30,100)
  2464. }
  2465. # Function to normalize EEG signals (Z-score normalization)
  2466. def normalize_signals(eeg_data):
  2467. return (eeg_data - eeg_data.mean()) / eeg_data.std()
  2468. def compute_band_power(eeg_signal, sf, band):
  2469. """Compute power spectral density (PSD) or average magnitude for a given EEG signal."""
  2470. low, high = band
  2471. freqs = np.fft.rfftfreq(len(eeg_signal), d=1/sf)
  2472. psd = np.abs(np.fft.rfft(eeg_signal))**2 / len(eeg_signal)
  2473. band_power = np.trapz(psd[(freqs >= low) & (freqs <= high)], freqs[(freqs >= low) & (freqs <= high)])
  2474. return band_power
  2475. def bandpass_filter(data, lowcut, highcut, sf, order=4):
  2476. """Apply a bandpass filter to the EEG signal."""
  2477. b, a = signal.butter(order, [lowcut, highcut], fs=sf, btype='band')
  2478. return signal.filtfilt(b, a, data)
  2479. emg_segments = [(int(peak - 0.5*sf), int(peak + 0.5*sf)) for peak in emg_peak_locs['B1']] # 1 second
  2480. eeg_segments = [(int(peak - 0.8*sf), int(peak + 0.2*sf)) for peak in emg_peak_locs['B1']] # 1 second, with 300ms delay
  2481. # Use filter with zip and unpacking
  2482. filtered = [(t1, t2) for t1, t2 in zip(emg_segments, eeg_segments) if all(x >= 0 for x in t1 + t2)]
  2483. # Unzip the filtered pairs
  2484. emg_segments, eeg_segments = zip(*filtered) if filtered else ((), ())
  2485. band_feature_list = []
  2486. for seg in eeg_segments:
  2487. segment = eegdata.iloc[seg[0]:seg[1]]
  2488. # Normalize EEG data
  2489. segment_normalized = segment.apply(normalize_signals, axis=0)
  2490. # Compute feature (power or magnitude) for each band & channel
  2491. band_feature_dict = {}
  2492. for band_name, band_range in eegbands.items():
  2493. feature_values = segment_normalized.apply(lambda x: compute_band_power(x.values, sf, band_range), axis=0)
  2494. for ch in feature_values.index:
  2495. band_feature_dict[f"{ch}_{band_name}"] = feature_values[ch]
  2496. band_feature_list.append(band_feature_dict)
  2497. # Convert list to DataFrame
  2498. feature_matrix = pd.DataFrame(band_feature_list)
  2499. # %% [markdown]
  2500. # Compute output matrix from EMG data
  2501. # %%
  2502. # Deadjust peaks for rolling averaged EMG data
  2503. window_size = 500
  2504. emg_segments = [(int(peak - 0.5*sf), int(peak + 0.5*sf)) for peak in emg_peak_locs['B1']-(window_size // 2)] # 1 second
  2505. eeg_segments = [(int(peak - 0.8*sf), int(peak + 0.2*sf)) for peak in emg_peak_locs['B1']] # 1 second, with 300ms delay
  2506. # Use filter with zip and unpacking
  2507. filtered = [(t1, t2) for t1, t2 in zip(emg_segments, eeg_segments) if all(x >= 0 for x in t1 + t2)]
  2508. # Unzip the filtered pairs
  2509. emg_segments, eeg_segments = zip(*filtered) if filtered else ((), ())
  2510. emg_feature_list = []
  2511. for seg in emg_segments:
  2512. segment = emgdata.iloc[seg[0]:seg[1]].mean()
  2513. emg_feature_list.append(segment)
  2514. # Convert list to DataFrame
  2515. output_matrix = pd.DataFrame(emg_feature_list)
  2516. # %% [markdown]
  2517. # Perform multimodal prediction
  2518. # %%
  2519. import numpy as np
  2520. import copy
  2521. from sklearn.cross_decomposition import PLSRegression
  2522. from sklearn.model_selection import train_test_split
  2523. from sklearn.metrics import r2_score, mean_squared_error
  2524. # Example data (replace with your EEG & EMG features)
  2525. X = copy.deepcopy(feature_matrix.to_numpy()) # EEG features
  2526. y = copy.deepcopy(output_matrix.to_numpy()) # EMG maps
  2527. # Train-test split
  2528. X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
  2529. # PLSR model
  2530. n_components = 10 # Tune this hyperparameter
  2531. plsr = PLSRegression(n_components=n_components)
  2532. plsr.fit(X_train, y_train)
  2533. # Predictions
  2534. y_pred = plsr.predict(X_test)
  2535. # Evaluation
  2536. r2 = r2_score(y_test, y_pred)
  2537. rmse = np.sqrt(mean_squared_error(y_test, y_pred))
  2538. print(f'R²: {r2:.4f}, RMSE: {rmse:.4f}')
  2539. # %%
  2540. import numpy as np
  2541. from scipy.stats import pearsonr, spearmanr
  2542. from sklearn.metrics.pairwise import cosine_similarity
  2543. # Assuming y_test and y_pred are already defined
  2544. # Initialize an array to store Pearson correlation for each test sample
  2545. pearson_corr = np.zeros(y_test.shape[0])
  2546. # Compute Pearson correlation for each test sample (across 16 channels)
  2547. for i in range(y_test.shape[0]):
  2548. corr, _ = pearsonr(y_test[i], y_pred[i]) # Pearson correlation per sample
  2549. pearson_corr[i] = corr
  2550. # Average correlation across all test samples (optional)
  2551. average_pearson_corr = np.mean(pearson_corr)
  2552. print(f"Average Pearson Correlation: {average_pearson_corr}")
  2553. std_pearson_corr = np.std(pearson_corr)
  2554. print(f"Standard deviation Pearson Correlation: {std_pearson_corr}")
  2555. # Initialize an array to store Spearman correlation for each test sample
  2556. spearman_corr = np.zeros(y_test.shape[0])
  2557. # Compute Spearman rank correlation for each test sample (across 16 channels)
  2558. for i in range(y_test.shape[0]):
  2559. corr, _ = spearmanr(y_test[i], y_pred[i]) # Spearman correlation per sample
  2560. spearman_corr[i] = corr
  2561. # Average rank correlation across all test samples (optional)
  2562. average_spearman_corr = np.mean(spearman_corr)
  2563. print(f"Average Spearman Rank Correlation: {average_spearman_corr}")
  2564. std_spearman_corr = np.std(spearman_corr)
  2565. print(f"Standard deviation Spearman Rank Correlation: {std_spearman_corr}")
  2566. # Initialize an array to store Cosine similarity for each test sample
  2567. cosine_sim = np.zeros(y_test.shape[0])
  2568. # Compute Cosine similarity for each test sample (across 16 channels)
  2569. for i in range(y_test.shape[0]):
  2570. cos_sim = cosine_similarity([y_test[i]], [y_pred[i]]) # Cosine similarity per sample
  2571. cosine_sim[i] = cos_sim[0][0]
  2572. # Average Cosine similarity across all test samples (optional)
  2573. average_cosine_sim = np.mean(cosine_sim)
  2574. print(f"Average Cosine Similarity: {average_cosine_sim}")
  2575. std_cosine_sim = np.std(cosine_sim)
  2576. print(f"Standard deviation Cosine Similarity: {std_cosine_sim}")
  2577. # %% [markdown]
  2578. # Display predicted vs expected maps
  2579. # %%
  2580. from biolab.bspm import image2layout
  2581. lyt = image2layout(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EMG_lyt.jpg',seq='cw')
  2582. # %%
  2583. from biolab.bspm import layout2map
  2584. test = pd.DataFrame(y_test,columns=output_matrix.columns)
  2585. pred = pd.DataFrame(y_pred,columns=output_matrix.columns)
  2586. mapstest = {int(col[1:]): test[col][2] for col in output_matrix.columns}
  2587. mapspred = {int(col[1:]): pred[col][2] for col in output_matrix.columns}
  2588. vmaptest = layout2map(layout=lyt,mapping=mapstest)
  2589. vmappred = layout2map(layout=lyt,mapping=mapspred)
  2590. # %%
  2591. plt.imshow(intmappred,vmin=28,vmax=36,cmap='jet')
  2592. plt.colorbar()
  2593. #plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\EMG_BSPM_regression_pred_FIST.pdf')
  2594. plt.show()
  2595. # %% [markdown]
  2596. # ## Scalability
  2597. # %% [markdown]
  2598. # ### Import Matlab format data from 128CH wireless system
  2599. # %% [markdown]
  2600. # Generate BSPM
  2601. # %%
  2602. import os
  2603. import h5py
  2604. import pandas as pd
  2605. from biolab.bspm import BSPM,image2layout
  2606. from biolab.utils import apply2all
  2607. fs = 1954
  2608. # Save new recordings into .bspm files
  2609. # Define parameters
  2610. readpath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Recordings\Scalability'
  2611. # Convert electrode array image into layout
  2612. EMG_126CH_lyt = image2layout(image_path=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\Masks\Layouts\EMG_126CH_lyt.jpg',seq='cw')
  2613. # Define operation to be applied to each file
  2614. def func(filepath):
  2615. # Extract filename from filepath
  2616. filename = os.path.splitext(os.path.basename(filepath))[0]
  2617. # Split filename into sections
  2618. _,_,muscle,_ = filename.split('_',3)
  2619. meta = {
  2620. 'mode': 'EMG',
  2621. 'measurement': 'M1',
  2622. 'muscle': muscle,
  2623. 'amplifier_sample_rate': fs
  2624. }
  2625. with h5py.File(filepath, 'r') as f:
  2626. # Access the dataset inside the file
  2627. emgdata = np.delete(f['EMG_DATA'][:], [0, 64], axis=0) # remove not-connected channels
  2628. t = np.linspace(0,emgdata.shape[1]/fs,emgdata.shape[1],endpoint=False)
  2629. # Create channel names
  2630. channel_names = [f'A{i+1}' for i in range(emgdata.shape[0])]
  2631. # Create the recorded data
  2632. recdata = pd.DataFrame(emgdata.T, columns=channel_names, index=t)
  2633. # Store channel information
  2634. chns = {}
  2635. chns['custom_channel_name'] = channel_names
  2636. chns['custom_order'] = range(126)
  2637. chns['electrode_impedance_magnitude'] = np.zeros(shape=126,dtype='float')
  2638. chns['electrode_impedance_phase'] = np.zeros(shape=126,dtype='float')
  2639. chns['location'] = range(1,127)
  2640. chns['native_channel_name'] = channel_names
  2641. chns['native_order'] = range(126)
  2642. chns['port_prefix'] = np.full(126,'A')
  2643. chns['ref_channel'] = np.zeros(shape=126,dtype='float')
  2644. # Create the channel information data
  2645. chdata = pd.DataFrame(chns)
  2646. MP = BSPM(recorded_data=recdata,channel_data=chdata,layout=EMG_126CH_lyt,metadata=meta)
  2647. # Save BSPM object
  2648. MP.save(savepath=r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Scalability\\'+filename+'.bspm')
  2649. # Call DataSaver function
  2650. apply2all(read_directory=readpath,extension='.mat',func=func)
  2651. # %% [markdown]
  2652. # ### Preprocess data and obtain relevant EMG envelopes
  2653. # %% [markdown]
  2654. # Loading and data preprocessing
  2655. # %%
  2656. from biolab.bspm import BSPM
  2657. DATA = {
  2658. 'BICEPS': r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Scalability\Ruben_electrode_biceps_ordered.bspm',
  2659. 'BTD': r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Scalability\Ruben_electrode_btd_ordered.bspm',
  2660. 'FIST1': r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Scalability\Ruben_electrode_fist_ordered.bspm',
  2661. 'FIST2': r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Data\Scalability\Ruben_electrode_fist_ordered2.bspm'
  2662. }
  2663. # Load BSPM data
  2664. MP = BSPM.load(loadpath=DATA['BTD'])
  2665. # Preprocess data
  2666. rMP = MP.resample(newfs=800)
  2667. rMP.preprocess(bw=(5,399))
  2668. # %% [markdown]
  2669. # EMG envelope
  2670. # %%
  2671. from scipy.signal import find_peaks
  2672. window_size = 500
  2673. envelopes = rMP.preprocessed_data.abs().rolling(window=window_size,min_periods=1).mean().loc[70:,:]
  2674. channel = 'A126'
  2675. peaks_dict = {}
  2676. for channel in envelopes.columns:
  2677. # Find peaks in the current channel data
  2678. peaks, _ = find_peaks(envelopes[channel],distance=9*800)
  2679. peaks_dict[channel] = peaks
  2680. plt.plot(rMP.preprocessed_data[channel][70:].abs())
  2681. plt.plot(envelopes[channel])
  2682. # Mark the peaks with vertical lines (vlines) on the signal
  2683. plt.vlines(envelopes.index[peaks_dict[channel]], ymin=envelopes[channel].min(), ymax=envelopes[channel].max(),
  2684. colors='r', linestyles='--', label='Peaks')
  2685. plt.show()
  2686. # %% [markdown]
  2687. # Compute average EMG values and generate mapping
  2688. # %%
  2689. average_peaks_dict = {}
  2690. for channel, indices in peaks_dict.items():
  2691. # Get the values at the specified peak indices for the current channel
  2692. peak_values = envelopes[channel].iloc[indices]
  2693. # Compute the average of those values
  2694. average_peaks_dict[channel] = peak_values.mean()
  2695. # %% [markdown]
  2696. # Generate mapping
  2697. # %%
  2698. from biolab.bspm import layout2map
  2699. new_keys = rMP.channel_data['location'].values
  2700. # Modify dictionary to relate to locations
  2701. updated_dict = {new_keys[i]: value for i, (_,value) in enumerate(average_peaks_dict.items())}
  2702. vmap = layout2map(layout=rMP.layout,mapping=updated_dict)
  2703. # %% [markdown]
  2704. # Display BSPM
  2705. # %%
  2706. plt.imshow(vmap[:,:,0])
  2707. plt.colorbar()
  2708. # %% [markdown]
  2709. # Interpolate BSPM and save as figure
  2710. # %%
  2711. from biolab.bspm import interpolate
  2712. intmap = interpolate(valmap=vmap,method='cubic')
  2713. # %%
  2714. import matplotlib.pyplot as plt
  2715. plt.imshow(intmap,cmap='viridis')
  2716. plt.colorbar()
  2717. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_5\Fist1_viridis.pdf')
  2718. plt.show()
  2719. # %% [markdown]
  2720. # Study differences between biceps, triceps and deltoid activity
  2721. # %%
  2722. from biolab.bspm import layout2map
  2723. muscle_peaks_dict = {}
  2724. for channel, indices in peaks_dict.items():
  2725. # Get the values at the specified peak indices for the current channel
  2726. peak_values = envelopes[channel].iloc[indices]
  2727. muscle_peaks_dict[channel] = peak_values.values[0] # 0 - Biceps, 1 - Triceps, 2 - Deltoids
  2728. new_keys = rMP.channel_data['location'].values
  2729. # Modify dictionary to relate to locations
  2730. updated_dict = {new_keys[i]: value for i, (_,value) in enumerate(muscle_peaks_dict.items())}
  2731. vmap = layout2map(layout=rMP.layout,mapping=updated_dict)
  2732. # %%
  2733. plt.imshow(vmap[:,:,0])
  2734. plt.colorbar()
  2735. # %% [markdown]
  2736. # Interpolate BSPM and save as figure
  2737. # %%
  2738. from biolab.bspm import interpolate
  2739. intmap = interpolate(valmap=vmap,method='cubic')
  2740. # %%
  2741. import matplotlib.pyplot as plt
  2742. plt.imshow(intmap,vmin=0,vmax=200,cmap='viridis')
  2743. plt.colorbar()
  2744. plt.savefig(r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_5\Deltoid_BTD_viridis.pdf',dpi=1000)
  2745. plt.show()
  2746. # %%
  2747. import numpy as np
  2748. import plotly.graph_objects as go
  2749. import matplotlib
  2750. import matplotlib.pyplot as plt
  2751. # --- Example Data ---
  2752. testdata = intmap
  2753. # --- Downsampling the Array for Efficiency ---
  2754. downsample_factor = (10, 5) # (rows, cols)
  2755. data_ds = testdata[::downsample_factor[0], ::downsample_factor[1]]
  2756. # --- Circular Loop Parameters ---
  2757. rows, cols = data_ds.shape
  2758. # Limit the angle range to 300 degrees, which is approximately 5.24 radians
  2759. theta = np.linspace(0, np.deg2rad(30), cols) # 300 degrees in radians
  2760. base_radius = 10 # Base radius of the loop
  2761. # --- Create 2D Grid for Circular Loop ---
  2762. theta_grid, z_grid = np.meshgrid(theta, np.arange(rows))
  2763. # --- Apply Height Scaling ---
  2764. height_factor = 0.3 # Make the shape 30% as tall
  2765. z_scaled = z_grid * height_factor
  2766. # --- Apply Width Scaling ---
  2767. width_factor = 3 # Make the shape 50% wider
  2768. bulge_factor = 0.1 # How much wider it gets at the center
  2769. # Bulging radius with width scaling applied
  2770. radius_grid = base_radius * (1 + bulge_factor * np.sin(np.pi * z_grid / rows))
  2771. radius_scaled = radius_grid * width_factor # Scale the width
  2772. # Circular coordinates with varying radius and width scaling
  2773. x_grid = radius_scaled * np.cos(theta_grid)
  2774. y_grid = radius_scaled * np.sin(theta_grid)
  2775. # --- Preprocess Data ---
  2776. # Handle NaN values by replacing them with -1
  2777. data_with_nan_replacement = np.copy(data_ds)
  2778. data_with_nan_replacement[np.isnan(data_with_nan_replacement)] = -1 # Replace NaNs with -1
  2779. # --- Create a custom jet colormap ---
  2780. jet = matplotlib.colormaps['viridis'] # Use the new Matplotlib API
  2781. # --- Create a colorscale (with -1 mapped to transparent) ---
  2782. jet_colors = jet(np.linspace(0, 1, 256)) # Get the jet colormap's RGBA values
  2783. jet_colors[0] = [0, 0, 0, 0] # Set the first color to fully transparent (for NaNs)
  2784. # Convert the RGBA colors to Plotly's 'rgba' format
  2785. jet_colors_rgba = [f'rgba({int(c[0]*255)}, {int(c[1]*255)}, {int(c[2]*255)}, {c[3]})' for c in jet_colors]
  2786. # --- Plotting with Plotly (3D Surface) ---
  2787. fig = go.Figure(data=[go.Surface(
  2788. z=z_scaled, # Apply the scaled height
  2789. x=x_grid, # Circular x coordinates with bulge and width effect
  2790. y=y_grid, # Circular y coordinates with bulge and width effect
  2791. surfacecolor=data_with_nan_replacement, # Color data
  2792. colorscale=jet_colors_rgba, # Custom colorscale with NaN handling
  2793. colorbar=dict(title="Intensity"), # Colorbar for intensity values
  2794. )])
  2795. # --- Adjusting Aspect Ratio and Axis Range ---
  2796. fig.update_layout(
  2797. scene=dict(
  2798. xaxis=dict(title='X', range=[-base_radius * width_factor * (1 + bulge_factor), base_radius * width_factor * (1 + bulge_factor)]),
  2799. yaxis=dict(title='Y', range=[-base_radius * width_factor * (1 + bulge_factor), base_radius * width_factor * (1 + bulge_factor)]),
  2800. zaxis=dict(title='Layer', range=[0, rows * height_factor]), # Adjust z-axis range for the scaled height
  2801. camera=dict(eye=dict(x=1.5, y=1.5, z=1.5)), # Adjust camera angle for better visualization
  2802. ),
  2803. margin=dict(l=0, r=0, b=0, t=50), # Adjust margins for a clean view
  2804. )
  2805. # --- Display the plot ---
  2806. fig.show()
  2807. # %%
  2808. import numpy as np
  2809. import plotly.graph_objects as go
  2810. import matplotlib
  2811. import matplotlib.pyplot as plt
  2812. # --- Example Data ---
  2813. testdata = intmap.T
  2814. # --- Downsampling the Array for Efficiency ---
  2815. downsample_factor = (5, 5) # (rows, cols)
  2816. data_ds = testdata[::downsample_factor[0], ::downsample_factor[1]]
  2817. # --- Circular Loop Parameters ---
  2818. rows, cols = data_ds.shape
  2819. # Limit the angle range to 300 degrees, which is approximately 5.24 radians
  2820. theta = np.linspace(0, np.deg2rad(300), cols) # 300 degrees in radians
  2821. base_radius = 10 # Base radius of the loop
  2822. # --- Create 2D Grid for Circular Loop ---
  2823. theta_grid, z_grid = np.meshgrid(theta, np.arange(rows))
  2824. # --- Apply Height Scaling ---
  2825. height_factor = 0.15 # Make the shape 30% as tall
  2826. z_scaled = z_grid * height_factor
  2827. # --- Apply Width Scaling ---
  2828. width_factor = 3 # Make the shape 50% wider
  2829. bulge_factor = 0.15 # How much wider it gets at the center
  2830. # Bulging radius with width scaling applied
  2831. radius_grid = base_radius * (1 + bulge_factor * np.sin(np.pi * z_grid / rows))
  2832. radius_scaled = radius_grid * width_factor # Scale the width
  2833. # Circular coordinates with varying radius and width scaling
  2834. x_grid = radius_scaled * np.cos(theta_grid)
  2835. y_grid = radius_scaled * np.sin(theta_grid)
  2836. # --- Preprocess Data ---
  2837. # Handle NaN values by replacing them with -1
  2838. data_with_nan_replacement = np.copy(data_ds)
  2839. data_with_nan_replacement[np.isnan(data_with_nan_replacement)] = -1 # Replace NaNs with -1
  2840. # --- Create a custom jet colormap ---
  2841. jet = matplotlib.colormaps['viridis'] # Use the new Matplotlib API
  2842. # --- Create a colorscale (with -1 mapped to transparent) ---
  2843. jet_colors = jet(np.linspace(0, 1, 256)) # Get the jet colormap's RGBA values
  2844. jet_colors[0] = [0, 0, 0, 0] # Set the first color to fully transparent (for NaNs)
  2845. # Convert the RGBA colors to Plotly's 'rgba' format
  2846. jet_colors_rgba = [f'rgba({int(c[0]*255)}, {int(c[1]*255)}, {int(c[2]*255)}, {c[3]})' for c in jet_colors]
  2847. # --- Plotting with Plotly (3D Surface) ---
  2848. fig = go.Figure(data=[go.Surface(
  2849. z=z_scaled, # Apply the scaled height
  2850. x=x_grid, # Circular x coordinates with bulge and width effect
  2851. y=y_grid, # Circular y coordinates with bulge and width effect
  2852. surfacecolor=data_with_nan_replacement, # Color data
  2853. colorscale=jet_colors_rgba, # Custom colorscale with NaN handling
  2854. colorbar=dict(title="Intensity"), # Colorbar for intensity values
  2855. )])
  2856. # --- Adjusting Aspect Ratio and Axis Range ---
  2857. fig.update_layout(
  2858. scene=dict(
  2859. xaxis=dict(title='X', range=[-base_radius * width_factor * (1 + bulge_factor), base_radius * width_factor * (1 + bulge_factor)]),
  2860. yaxis=dict(title='Y', range=[-base_radius * width_factor * (1 + bulge_factor), base_radius * width_factor * (1 + bulge_factor)]),
  2861. zaxis=dict(title='Layer', range=[0, rows * height_factor]), # Adjust z-axis range for the scaled height
  2862. camera=dict(eye=dict(x=1.5, y=1.5, z=1.5)), # Adjust camera angle for better visualization
  2863. ),
  2864. margin=dict(l=0, r=0, b=0, t=50), # Adjust margins for a clean view
  2865. )
  2866. # --- Display the plot ---
  2867. fig.show()
  2868. # %%
  2869. from biolab.bspm import interpolate
  2870. intmaptest = interpolate(valmap=vmaptest,method='cubic')
  2871. intmappred = interpolate(valmap=vmappred,method='cubic')
  2872. # %% [markdown]
  2873. # ## Convert `jet` colorspace to `viridis`
  2874. # %%
  2875. import fitz # PyMuPDF
  2876. import numpy as np
  2877. import cv2
  2878. import matplotlib.pyplot as plt
  2879. import matplotlib.cm as cm
  2880. from matplotlib.colors import Normalize
  2881. from PIL import Image
  2882. def extract_image_from_pdf(pdf_path):
  2883. doc = fitz.open(pdf_path)
  2884. for page_index in range(len(doc)):
  2885. page = doc.load_page(page_index)
  2886. images = page.get_images(full=True)
  2887. for img_index, img in enumerate(images):
  2888. xref = images[img_index][0]
  2889. base_image = doc.extract_image(xref)
  2890. image_bytes = base_image["image"]
  2891. img = cv2.imdecode(np.frombuffer(image_bytes, np.uint8), cv2.IMREAD_COLOR)
  2892. return img
  2893. return None
  2894. def convert_jet_to_viridis(image):
  2895. image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
  2896. # Convert to float32 and normalize
  2897. img_float = image_rgb.astype(np.float32) / 255.0
  2898. # Simulate inverse of `jet` colormap
  2899. jet = cm.get_cmap('jet', 256)
  2900. jet_colors = (jet(np.linspace(0, 1, 256))[:, :3]) # ignore alpha
  2901. # Reshape image and jet LUT
  2902. pixels = img_float.reshape(-1, 3)
  2903. distances = np.linalg.norm(pixels[:, None] - jet_colors[None, :], axis=2)
  2904. jet_indices = np.argmin(distances, axis=1)
  2905. # Normalize back to data values
  2906. normed_data = jet_indices / 255.0
  2907. # Apply viridis colormap
  2908. viridis = cm.get_cmap('viridis')
  2909. viridis_img = viridis(normed_data)[:, :3]
  2910. viridis_img = (viridis_img.reshape(image.shape[0], image.shape[1], 3) * 255).astype(np.uint8)
  2911. return cv2.cvtColor(viridis_img, cv2.COLOR_RGB2BGR)
  2912. def save_image_as_pdf(image, output_pdf_path):
  2913. img_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
  2914. img_pil.save(output_pdf_path, "PDF")
  2915. # === USAGE ===
  2916. filepath = r'C:\Users\Ruben\OneDrive - University of Cambridge\PhD\BSPM_for_ECGi\Publications\Wireless High-Density E-textile Eutectogel Electrode Arrays for Spatio-Temporal Machine Learning in Cutaneous Electrophysiology\Experiments\INTAN\Results\Figure_4\EMG_BSPM_regression_test_UP'
  2917. input_pdf = filepath+'.pdf'
  2918. output_pdf = filepath+'_viridis.pdf'
  2919. image = extract_image_from_pdf(input_pdf)
  2920. if image is not None:
  2921. viridis_image = convert_jet_to_viridis(image)
  2922. save_image_as_pdf(viridis_image, output_pdf)
  2923. print(f"Saved converted PDF as {output_pdf}")
  2924. else:
  2925. print("No image found in PDF.")
  2926. # %%
  2927. import numpy as np
  2928. import matplotlib.pyplot as plt
  2929. import seaborn as sns
  2930. def plot_confusion_matrix(cm, labels=None, title='Confusion Matrix', pdf_path='confusion_matrix.pdf'):
  2931. fig, ax = plt.subplots(figsize=(6, 5), constrained_layout=True)
  2932. sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
  2933. xticklabels=labels, yticklabels=labels,
  2934. cbar=True, square=True, linewidths=0.5, vmin=0, vmax=20, ax=ax)
  2935. ax.set_title(title)
  2936. ax.set_xlabel('Predicted')
  2937. ax.set_ylabel('Actual')
  2938. # Save to PDF
  2939. fig.savefig(pdf_path, format='pdf')
  2940. print(f"Saved confusion matrix to: {pdf_path}")
  2941. # === Example Usage ===
  2942. conf_matrix = np.array([[13, 4, 3],
  2943. [10, 11, 0],
  2944. [8, 7, 5]])
  2945. class_labels = ['Class A', 'Class B', 'Class C']
  2946. plot_confusion_matrix(conf_matrix, labels=class_labels, pdf_path='my_confusion_matrix.pdf')

Wireless_BSPM.ipynb at commit c1ccfee, no license · at the source

Overview

Authors: Ruben Ruiz-Mateos Serrano1,2, Charlie Brunt1,2, Xudong Tao1,2, Maciej Zajaczkowski3, Antonio Dominguez-Alfaro2,4, Matias L. Picchio5,6, Daniele Mantione7,5, Emmanuel M. Drakakis3, David Mecerreyes7,5, George G. Malliaras1,2
  1. Institute for Biomedical Innovation, University of Cambridge,Cambridge, UK
  2. Electrical Engineering Division, Department of Engineering, University of Cambridge,Cambridge, UK
  3. Department of Bioengineering, Faculty of Engineering, Imperial College London,London, UK
  4. Present Address: Instituto de Microelectrónica, IMSE-CNM, (CSIC Universidad de Sevilla),Av. Américo Vespucio 28, 41092 Sevilla, Spain
  5. IKERBASQUE, Basque Foundation for Science,Bilbao, Spain
  6. POLYMAT, Department of Mining-Metallurgy Engineering and Materials Science, School of Engineering, University of the Basque Country (UPV/EHU),Bilbao, Spain
  7. POLYMAT, University of the Basque Country UPV/EHU,Av.Tolosa 72, 20018 Donostia-San Sebastian, Gipuzkoa Spain
Institutions: University of Cambridge (United Kingdom); Imperial College London (United Kingdom); Universidad de Sevilla (Spain); Ikerbasque (Spain); University of the Basque Country (Spain)
Journal: Nature communications, volume 17, issue 1, article 8441
Dates: received 14 October 2025; accepted 17 June 2026; published online 8 July 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41467-026-75134-1 · PMID 42420293 · PMCID PMC13478180 · OpenAlex W7167728318
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: other (modality), human (organism)
Methods: Spectral & time-frequency, Preprocessing, Connectivity, Smoothing, state filtering, decompositions, Machine learning, Evoked potentials, Physiology & signal measures
Keywords: Biomedical engineering, Electrophysiology, Preclinical research, Sensorimotor processing
MeSH: Body Surface Potential Mapping*, Muscle, Skeletal*, Textiles*, Algorithms, Electrodes, Humans, Machine Learning, Wearable Electronic Devices (* major topic)
Topic: Neuroscience and Neural Engineering (Cellular and Molecular Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 64 references in the paper

Abstract

Cutaneous electrophysiology is a fundamental non-invasive technique for assessing electrically active organs such as the brain, heart, and muscles. Standard approaches, however, are limited in spatial resolution, reducing sensitivity to certain pathological features. The development of body surface potential mapping using electrode arrays has helped overcome these limitations, enhancing the diagnostic power of cutaneous recordings, yet clinical adoption remains constrained by challenges in electrode performance, wiring complexity, wearability, data transmission, and interpretability. Here, we present a hybrid e-textile electrode array system that overcomes these barriers, enabling simultaneous mapping of electrical activity along the cortico-muscular axis. The system combines application-specific conducting polymer coatings to improve electrode performance, a flexible fabrication process for robust connectivity and wearability, and interpretable machine learning algorithms for data analysis. In controlled single-subject experiments, we demonstrate reliable muscle and brain recordings, enabling classification of grasped object shapes and somatosensory stimuli. Simultaneous multi-site recordings along the cortico-muscular axis provide spatial maps of reaction time distributions and allow prediction of muscle activation patterns from cortical activity. This platform establishes a framework for wearable, multi-modal electrophysiological mapping and non-invasive study of cortico-muscular dynamics, representing a step towards practical brain–body interfaces with applications in neurorehabilitation, prosthetics, and human–machine interaction.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 19 matches between paragraphs and lines of code.

Zenodo 20041637

License: CC-BY-4.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: the text, “Preprocessing of recorded electrophysiological s”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (2 files), NumPy (2 files), pandas (2 files), scikit-learn (2 files), SciPy (2 files), seaborn (2 files), h5py (1 file), OpenCV (1 file), Pillow (1 file), Plotly (1 file), specparam (formerly FOOOF) (1 file), WFDB Python (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
3 files
At the source:

rr1017/cortico-muscular-axis-mapping

License: none: the authors keep all their rights
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: c1ccfee5ca3314ef5c3c2c2fb8b7aa6a14008166, 28 April 2026
Languages: Jupyter (3)
Size: 3 files, 3 scripts
Software Heritage: not archived
Found in: the Zenodo archive record
Holds: 3 notebooks
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (2 files), NumPy (2 files), pandas (2 files), scikit-learn (2 files), SciPy (2 files), seaborn (2 files), h5py (1 file), OpenCV (1 file), Pillow (1 file), Plotly (1 file), specparam (formerly FOOOF) (1 file), WFDB Python (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
3 files

Code availability

The codes used for collecting data are available from the corresponding author upon reasonable request.

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:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 6 scripts, each with its path and the digest of its content;
  • 19 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

All data supporting the findings of this study are available within the article and its supplementary files. Any additional requests for information can be directed to, and will be fulfilled by, the corresponding author. Source data are provided with this paper. The code used to process and analyse the data in this study is publicly available at 10.5281/zenodo.20041637. The electrophysiological datasets generated in this study consist of human EEG and EMG recordings and are not publicly available due to ethical and data protection constraints, including participant consent limitations and compliance with applicable data privacy regulations. Access to a de-identified minimum dataset sufficient to reproduce the analyses may be granted upon request for non-commercial research purposes, subject to institutional data-sharing agreements and ethical approval where required. Requests should be directed to the corresponding author. Applicants must provide a brief research proposal outlining the intended use of the data. Requests are typically reviewed within 2–4 weeks. Approved users will be granted access to the data for a defined period and under conditions that prohibit re-identification or onward sharing. Source data are provided with this paper.

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, 10 authors, 4 keywords, 8 MeSH terms, 3 funders, 58 references.

Cite

This paper

Serrano, R. R.-M., Brunt, C., Tao, X., Zajaczkowski, M., Dominguez-Alfaro, A., Picchio, M. L., Mantione, D., Drakakis, E. M., Mecerreyes, D., & Malliaras, G. G. (2026). Body surface potential mapping of the cortico-muscular axis using smart textile electrode arrays. Nature communications, 17(1), 8441. https://doi.org/10.1038/s41467-026-75134-1

BibTeX

@article{serrano2026body,
author = {Serrano, Ruben Ruiz-Mateos and Brunt, Charlie and Tao, Xudong and Zajaczkowski, Maciej and Dominguez-Alfaro, Antonio and Picchio, Matias L. and Mantione, Daniele and Drakakis, Emmanuel M. and Mecerreyes, David and Malliaras, George G.},
title = {{Body surface potential mapping of the cortico-muscular axis using smart textile electrode arrays}},
journal = {Nature communications},
year = {2026},
month = jul,
volume = {17},
number = {1},
pages = {8441},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-75134-1},
url = {https://doi.org/10.1038/s41467-026-75134-1},
pmid = {42420293},
pmcid = {PMC13478180}
}

RIS

TY - JOUR
AU - Serrano, Ruben Ruiz-Mateos
AU - Brunt, Charlie
AU - Tao, Xudong
AU - Zajaczkowski, Maciej
AU - Dominguez-Alfaro, Antonio
AU - Picchio, Matias L.
AU - Mantione, Daniele
AU - Drakakis, Emmanuel M.
AU - Mecerreyes, David
AU - Malliaras, George G.
TI - Body surface potential mapping of the cortico-muscular axis using smart textile electrode arrays
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/07/08
VL - 17
IS - 1
SP - 8441
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-75134-1
UR - https://doi.org/10.1038/s41467-026-75134-1
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-75134-1",
"type": "article-journal",
"title": "Body surface potential mapping of the cortico-muscular axis using smart textile electrode arrays",
"container-title": "Nature communications",
"author": [
{
"family": "Serrano",
"given": "Ruben Ruiz-Mateos"
},
{
"family": "Brunt",
"given": "Charlie"
},
{
"family": "Tao",
"given": "Xudong"
},
{
"family": "Zajaczkowski",
"given": "Maciej"
},
{
"family": "Dominguez-Alfaro",
"given": "Antonio"
},
{
"family": "Picchio",
"given": "Matias L."
},
{
"family": "Mantione",
"given": "Daniele"
},
{
"family": "Drakakis",
"given": "Emmanuel M."
},
{
"family": "Mecerreyes",
"given": "David"
},
{
"family": "Malliaras",
"given": "George G."
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "8441",
"DOI": "10.1038/s41467-026-75134-1",
"PMID": "42420293",
"PMCID": "PMC13478180",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-75134-1",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
8
]
]
}
}

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.1002/advs.202519893 [code]
NeuroSuite for Long-Term Functional and Structural Studies of Air-Liquid Interface Cerebral Organoids.
Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)
In common: specparam (formerly FOOOF), Pillow, pandas, 3 other tools, 2 authors
[2] doi:10.7554/elife.100605 [code]
Age-related changes in ‘cortical’ 1/f dynamics are linked to cardiac activity
Journal: n/a
In common: WFDB Python, specparam (formerly FOOOF), h5py, 6 other tools, other
[3] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: WFDB Python, Plotly, OpenCV, 7 other tools
[4] doi:10.1016/j.xcrm.2026.102766 [code]
A longitudinal single-cell and spatial multiomic atlas of pediatric high-grade glioma.
Journal: Cell reports. Medicine
In common: Plotly, OpenCV, h5py, 7 other tools
[5] doi:10.1038/s41597-025-05174-7 [code]
A large-scale MEG and EEG dataset for object recognition in naturalistic scenes
Journal: n/a
In common: Plotly, OpenCV, h5py, 7 other tools
[6] doi:10.1016/j.isci.2026.116825 [code]
Social hierarchy shapes behavioral and transcriptional responses to chronic stress and ketamine in male mice.
Journal: iScience
In common: Plotly, OpenCV, h5py, 7 other tools
[7] 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: specparam (formerly FOOOF), Plotly, seaborn, 5 other tools, 1 reference
[8] doi:10.3389/fncom.2026.1786996 [code]
Schumann-anchored golden ratio organization of human neural oscillations.
Journal: Frontiers in computational neuroscience
In common: specparam (formerly FOOOF), h5py, Pillow, 6 other tools
[9] doi:10.1371/journal.pone.0348866 [code]
Using deep learning to identify inherited retinal diseases based on wide-field retinal imaging data.
Journal: PloS one
In common: Plotly, OpenCV, Pillow, 6 other tools, other
[10] doi:10.1038/s41467-026-75128-z
Surface circumferential spinal cord recording in freely moving rodents.
Journal: Nature communications
In common: 2 authors

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.