-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathripmap.py
More file actions
1457 lines (1306 loc) · 59.6 KB
/
Copy pathripmap.py
File metadata and controls
1457 lines (1306 loc) · 59.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Imports
import os, sys
import numpy as np
# For interactive plots
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from matplotlib.widgets import TextBox, Button, PolygonSelector
from matplotlib.backend_bases import MouseButton
from matplotlib.path import Path
# Topological utils
from topol_utils import *
# Manual curation
from manual_utils import *
# For detrending/z-score
import scipy.signal, scipy.stats
# For UMAP
import umap
# For clustering
import hdbscan
import sklearn.cluster as cluster
# Global variables
COLORS = np.array([[.7,.2,.2], [.2,.6,.9], [.5,.5,.5]])
LABELS = ['pIEDs', 'pSWRs', 'FPs']
CMAP = ListedColormap(COLORS)
def event_curation(lfp, sf, times, power_spectrum=[], use=['lfp'],
win_size_show=0.075, win_size_umap=0.075, do_detrend=True, do_zscore=False,
list_n_neighbors=[10, 50, 100, 200], list_min_dists=[0.0, 0.1, 0.2, 0.3],
intrinsic_dimension=4, n_elements=30, n_axis_bins=9, axis_method='centroids',
plot_fp_separately=False, do_axis_grid=False,
file_name='', saveas_folder='', save_format='png'):
'''
event_curation(lfp, sf, t_swrs, t_ieds, id_fps=[], win_size_show=0.075, win_size_umap=0.075,
do_detrend=True, do_zscore=False,
list_n_neighbors=[10, 50, 100, 200], list_min_dists=[0.0, 0.1, 0.2, 0.3],
intrinsic_dimension=4, n_elements=30, n_axis_bins=9, file_name='', saveas_folder='')
Implements an event curation based on waveform similarity, using dimensionality reduction. First, multiple
embeddings for all the combinations of 'n_neighbors' and 'min_dist' from the optinal input variables
'list_n_neighbors' and 'list_min_dists' are computed with UMAP and presented in a figure, along with
extra summary plots of topological features of the original space (mean intrinsic dimension, computed
using ABID) and persistant homology for betti number = 0. In this plot, the user has to specify the optimal
UMAP's 'n_neighbors' and 'min_dist' parameters, and if events can be divided into clusters or
not. When the 'Continue' button is pressed, the interactive curation GUI appears.
If there are clusters (do_clusters=1), the GUI will allow to select which is the cluster that represents the
putative SWRs. Events that are not in the box will be labeled as 'False' in the output variable 'curated_labels'.
If the 'Finish' button is pressed, the GUI will close and the curated labels will be returned. If the 'Update'
button is pressed, then a new UMAP will be computed, and a new embedding will be shown. This process can be done
multiple times until the cluster is as clean as possible.
If there are not clusters (do_clusters=0), then the whole UMAP cloud is divided into 'n_axis_bins' bins along
the main axis of the cloud shape. Events are projected into this axis, and the mean of the events of each bin
are displayed with colors that reflect the amount of events that come from the SWR or IED detectors. The GUI
contains two boxes to define from which to which bin ('min_bin' to 'max_bin') are the optimal events.
Inputs:
-------
lfp (np.ndarray):
Array containing the LFP signal of the channel used to detect events
sf (float):
Sampling frequency, in Hz
times (dict):
Dictionary containing:
'swrs': np.ndarray of times (sec) of the center of the events automatically detected by the SWR detector
'ieds': np.ndarray of times (sec) of the center of the events automatically detected by the IED detector
'id_fps': np.ndarray, containing the indexes of the 't_swrs' variable that are False Positives.
If not given, id_fps = []
UMAP will use all the entries that are in this dict. If there is no 'ieds', UMAP will be done only with pSWRs
power_spectrum (dict):
Dictionary containing:
'swrs': np.ndarray of power spectrum of all events detected by the SWR detector (same order)
'ieds': np.ndarray of power spectrum of all events detected by the IED detector (same order)
use (list):
List of strings indicating what to use for UMAP. Options are: 'lfp' and/or 'power_spectrum' ('ps' or 'powspctrm'
are also accepted)
win_size_show (float, optional):
Duration (in seconds) of the window before/after t_swrs (and t_ieds) over which to show events
By default win_size_show = 0.075
win_size_umap (float, optional):
Duration (in seconds) of the window before/after t_swrs (and t_ieds) over which to perform UMAP
By default win_size_umap = 0.075
do_detrend (bool, optional):
To specify if dentrend should be applied to each event (True) or not (False).
By default do_detrend = True
do_zscore (bool, optional):
Boolean to specify if zscore should be applied to each event (True) or not (False).
By default do_zscore = False
list_n_neighbors (np.ndarray or list, optional):
List of 'n_neighbor' parameters to perform the Intrinsic Dimension and UMAP analysis with.
By default list_n_neighbors = [10, 50, 100, 200]
list_min_dists (np.ndarray or list, optional):
List of 'min_dist' parameters to compute UMAP with
By default list_min_dists = [0.0, 0.1, 0.2, 0.3]
intrinsic_dimension (int, optional):
Integer to specify intrinsic dimension. It should be 4, but if the 'Intrinsic dimension' plot
shows something different, close and re-run this function changing this variable
n_elements (int, optional):
Number of elements shown in the persistent homology analysis for betti number H=0.
By default n_elements = 30
n_axis_bins (int, optional):
Number of bins by which to divide the UMAP cloud, in case there is no possibility of clustering.
By default n_axis_bins = 9
axis_method (string, optional):
Method to use for computing the axis.
'centroids' - computes the axis from the centroid of events from each
detector, and traces a line between them (default)
Automatically switches to 'fit' if no IED or no SWR data is provided
'fit' - fits all data to a quadratic line (topol_utils.fit_axis).
do_axis_grid (bool, optional):
In axis method: plot events over and below the axis in different subplots.
By default do_axis_grid = False
plot_fp_separately (bool, optional):
In axis method: plot False Positives separately.
By default plot_fp_separately = False
file_name (string, optional):
Name of the file, to show it in plots (if provided)
saveas_folder (string, optional):
Full path to folder in which to save plots (if provided)
save_format (string, optional):
Format to save the figure. By default save_format='png'
Outputs:
--------
curated_swrs (np.ndarray):
Boolean array with the curated labels for 't_swrs', selected through the interactive plots
curated_ieds (np.ndarray):
Boolean array with the curated labels for 't_ieds', selected through the interactive plots
events (np.ndarray):
Array of size (#events, time) with all events to be curated
params (dict):
A dict with all the parameters
'''
# ========= PREPARE DATA ===========================================================================
# Extract variables from inputs
t_swrs = times['swrs'] if 'swrs' in times else np.empty((0))
t_ieds = times['ieds'] if 'ieds' in times else np.empty((0))
id_fps = times['id_fps'] if 'id_fps' in times else []
# Variables
max_B = 0
# Convert them to arrays
list_n_neighbors = np.array(list_n_neighbors)
list_min_dists = np.array(list_min_dists)
# Make dict
params = {'sf':sf, 'id_fps':id_fps, 'use':use, 'win_size_show':win_size_show, 'win_size_umap':win_size_umap, 'do_detrend':do_detrend, 'do_zscore':do_zscore,
'list_n_neighbors':list_n_neighbors, 'list_min_dists':list_min_dists, 'max_B':max_B,
'intrinsic_dimension':intrinsic_dimension, 'n_elements':n_elements,
'n_axis_bins':n_axis_bins, 'axis_method':axis_method, 'do_axis_grid':do_axis_grid,
'plot_fp_separately':plot_fp_separately,'file_name':file_name,
'saveas_folder':saveas_folder, 'save_format':save_format}
# Join times from SWR and IED arrays
t_all = np.append(t_swrs, t_ieds)
from_swr_detector = np.append(np.ones_like(t_swrs), np.zeros_like(t_ieds)).astype(int)
if len(t_swrs) > 0:
from_swr_detector[id_fps] = 2
# Update params
params['t_all'] = t_all
params['from_detector'] = from_swr_detector
# Make matrix of events
id_win = np.arange(-win_size_show*sf, win_size_show*sf +1).astype(int).reshape(1,-1)
id_events = (t_all*sf).astype(int).reshape(-1,1)
lfp_events = lfp[id_win+id_events]
# Process data
if do_detrend: lfp_events = scipy.signal.detrend(lfp_events, axis=1, type='linear')
if do_zscore: lfp_events = scipy.stats.zscore(lfp_events, axis=1)
# What to use for umap
umap_events = np.empty((len(t_all), 0))
if 'lfp' in use:
# Make matrix of umap_events
id_win = np.arange(-win_size_umap*sf, win_size_umap*sf +1).astype(int).reshape(1,-1)
id_events = (t_all*sf).astype(int).reshape(-1,1)
lfp_umap = lfp[id_win+id_events]
# Process data
if do_detrend: lfp_umap = scipy.signal.detrend(lfp_umap, axis=1, type='linear')
if do_zscore: lfp_umap = scipy.stats.zscore(lfp_umap, axis=1)
umap_events = np.hstack((umap_events, lfp_umap))
if ('power_spectrum' in use) | ('ps' in use) | ('powspectrm' in use):
if len(power_spectrum) > 0:
# SWRs
if ('swrs' in times) & ('swrs' in power_spectrum):
ps_swrs = power_spectrum['swrs']
elif ('swrs' in times) & ('swrs' not in power_spectrum):
print("Power spectrum indicated to be used, but power_spectrum['swrs'] not provided")
return None, None, None, None
# IEDs
if ('ieds' in times) & ('ieds' in power_spectrum):
ps_ieds = power_spectrum['ieds']
elif ('ieds' in times) & ('ieds' not in power_spectrum):
print("Power spectrum indicated to be used, but power_spectrum['ieds'] not provided")
return None, None, None, None
# Append
ps_all = np.vstack((ps_swrs, ps_ieds))
umap_events = np.hstack((umap_events, ps_all))
params['ps_freqs'] = power_spectrum['freqs']
else:
print('Power spectrum indicated to be used, but not provided')
return None, None, None, None
# ========= STEP 1: EVENT TOPOLOGY SUMMARY =========================================================
# --- Intrinsic dimension ------
# Comupte intrinsic dimension using ABID: Angle Based Intrinsic Dimensionality (by Erik Thordsen, Erich Schubert)
abids = np.array([compute_abids(umap_events, n_neigh=n_neigh) for n_neigh in list_n_neighbors])
# Remove nans
if np.sum(np.isnan(abids)) > 0:
abids = [intrdim[~np.isnan(intrdim)] for intrdim in abids]
# --- Persistent homology ------
# Compute diagrams
diagrams, ds, thresh = compute_diagrams(umap_events, max_B=max_B)
# Compute dense diagrams
dense_diagrams, cumsum_distances = compute_dense_diagrams(diagrams, ds)
# --- UMAP ------
# Create embedding
umap_embeddings = np.empty((len(list_n_neighbors), len(list_min_dists), len(umap_events), intrinsic_dimension))
for iin, n_neigh in enumerate(list_n_neighbors):
for iid, min_dist in enumerate(list_min_dists):
embedding_umap = umap.UMAP(
n_neighbors=n_neigh,
min_dist=min_dist,
n_components=intrinsic_dimension,
metric='euclidean',
metric_kwds=None,
# random_state=42
)
print(f'making embedding (n_neighbors={n_neigh:.0f}, min_dist={min_dist:.1f})...\t\t\t\t', end='\r')
# Fit the data
embedding_umap.fit(umap_events)
umap_embeddings[iin,iid] = embedding_umap.embedding_
curated_labels = None
do_finish = False
while np.all(curated_labels == None):
# ========= STEP 2: VISUALIZE =======================================================================
# Make plot
n_neighbors, min_dist, do_cluster = event_topology_summary(abids, diagrams, dense_diagrams, umap_embeddings, from_swr_detector, params)
# Get selected embedding
iin = np.argwhere(list_n_neighbors==n_neighbors)[0,0]
iid = np.argwhere(list_min_dists==min_dist)[0,0]
embedding = umap_embeddings[iin,iid]
# Update parameters
params['n_neighbors'] = int(n_neighbors)
params['min_dist'] = min_dist
params['do_cluster'] = do_cluster
params['embedding'] = embedding
# ========= STEP 3: CURATION =======================================================================
if do_cluster: # Option 1: do_clusters=False -> Axis projection
curated_labels, params = cluster_curation(embedding, lfp_events, umap_events, from_swr_detector, params)
# Save info of events selected by axis
params['selected_by_cluster'] = np.zeros((embedding.shape[0]))
params['selected_by_cluster'] = curated_labels
else: # Option 2: do_clusters=True -> Clusterization
curated_labels, params = axis_projection_curation(embedding, lfp_events, umap_events, from_swr_detector, params)
# Save info of events selected by axis
params['selected_by_axis'] = np.zeros((embedding.shape[0]))
params['selected_by_axis'] = curated_labels
# ========= STEP 4: RETURN =========================================================================
# Outputs
curated_swrs = curated_labels[from_swr_detector>0]
curated_ieds = curated_labels[from_swr_detector==0]
return curated_swrs, curated_ieds, umap_events, params
def event_topology_summary(abids, diagrams, dense_diagrams, umap_embeddings, from_swr_detector, params):
'''
event_topology_summary(abids, diagrams, dense_diagrams, umap_embeddings, from_swr_detector, params)
Inputs:
-------
abids (np.ndarray):
Angle Based Intrinsic Dimensionality array, which is the output topol_utils.compute_abids()
diagrams (np.ndarray):
Array with persistent homology element lifes, which is the output of tolo_utils.compute_diagrams()
dense_diagrams (np.ndarray):
Array with persistent homology element lifes, but computed as a function of the density,
which is the output of tolo_utils.compute_dense_diagrams()
umap_embeddings (np.ndarray):
All the UMAP projections of 'umap_events' into the low-dimension embedding for each n_neighbors
and each min_dist
from_swr_detector (np.ndarray):
Array of size (#events,) specifying for each event how was it detected:
0 - from IED detector
1 - from SWR detector
2 - manually labeled as FP
params (dict):
Dictionary of parameters, including: id_fps, win_size, do_detrend, do_zscore,
list_n_neighbors, list_min_dists, max_B, intrinsic_dimension, n_elements,
n_axis_bins, file_name, saveas_folder, n_neighbors, min_dist, do_cluster,
embedding
Outputs:
--------
curated_labels (np.ndarray):
Boolean array of size (#events,) indicating if the curation has classified
each event as SWR (True) or IED (False).
'''
# Retrieve parameters
list_n_neighbors = params['list_n_neighbors']
list_min_dists = params['list_min_dists']
max_B = params['max_B']
intrinsic_dimension = params['intrinsic_dimension']
n_elements = params['n_elements']
file_name = params['file_name']
saveas_folder = params['saveas_folder']
save_format = params['save_format']
use = params['use']
# --- Generate the figure ------
fig, axes = plt.subplots(1+len(list_n_neighbors), len(list_min_dists), figsize=(12,12))
for iax in range(len(list_min_dists)-2):
fig.delaxes(axes[0][iax+2])
# --- Intrinsic dimension subplot ---
mean_intrdim = np.mean(abids, axis=1)
std_intrdim = np.std(abids, axis=1)
# Plot distribution of mean+/-std of intrinsic dimensions
axes[0,0].fill_between(list_n_neighbors, mean_intrdim-std_intrdim, mean_intrdim+std_intrdim, color='k', alpha=0.3, edgecolor=None)
axes[0,0].plot(list_n_neighbors, mean_intrdim, color='k')
axes[0,0].plot(list_n_neighbors, intrinsic_dimension*np.ones_like(list_n_neighbors), '--', color=[.1,.4,.2], label='current assumption')
axes[0,0].set_xlabel('n_neighbor')
axes[0,0].set_xticks(list_n_neighbors)
axes[0,0].set_ylabel('intrinsic dimension')
axes[0,0].set_ylim([0,10])
axes[0,0].legend()
axes[0,0].set_title('Intrinsic dimension analysis')
# --- Plot diagrams plot for H=0 ---
lifes = np.diff(diagrams[max_B][-n_elements:],axis=1).flatten()
axes[0,1].barh(np.arange(n_elements), lifes, 0.8,left=diagrams[max_B][-n_elements:,0], color=[.3,.3,.3])
# Plot dense plots
lifes_dense = np.diff(dense_diagrams[max_B][-n_elements:],axis=1).flatten()
for element in range(n_elements):
axes[0,1].plot( [np.max(lifes)*.4, np.max(lifes)*.4 + lifes_dense[-(element+1)]/np.max(lifes_dense)*np.max(lifes)*.4],
[n_elements/2.5*(2-element/n_elements)]*2, linewidth=.2, color=[.5,.5,.5])
axes[0,1].text(np.max(lifes)*.4, n_elements/2.5-3, 'density', color=[.7,.7,.7])
axes[0,1].set_xlabel('radius')
axes[0,1].set_xticks([])
axes[0,1].set_ylabel(r'$\beta_0$')
axes[0,1].set_ylim([-1,n_elements])
axes[0,1].set_yticks(np.arange(0,n_elements+1,10), labels=n_elements-np.arange(0,n_elements+1,10))
axes[0,1].set_title('Persistent homology analysis')
# --- UMAP subplots ---
for iin, n_neigh in enumerate(list_n_neighbors):
for iid, min_dist in enumerate(list_min_dists):
# Sort: pIEDs - pSWRs - FPs
idsort = np.argsort(from_swr_detector)
# Plot cloud
axes[1+iin,iid].scatter(umap_embeddings[iin,iid,idsort,0], umap_embeddings[iin,iid,idsort,1], c=from_swr_detector[idsort], s=5, alpha=.4, cmap=CMAP, vmin=0, vmax=2)
axes[1+iin,iid].set_xticks([])
axes[1+iin,iid].set_yticks([])
if iin==0: axes[1+iin,iid].set_title(f'min_dist={min_dist:.1f}')
if iid==0: axes[1+iin,iid].set_ylabel(f'n_neighbors={n_neigh:.0f}')
# Legend
fig.text(1-(len(list_min_dists)-2)/len(list_min_dists)*0.4, 1-1/len(list_n_neighbors)/8*4.3, LABELS[0], fontsize=14, color=COLORS[0])
fig.text(1-(len(list_min_dists)-2)/len(list_min_dists)*0.4, 1-1/len(list_n_neighbors)/8*5, LABELS[1], fontsize=14, color=COLORS[1])
fig.text(1-(len(list_min_dists)-2)/len(list_min_dists)*0.4, 1-1/len(list_n_neighbors)/8*5.7, LABELS[2], fontsize=14, color=COLORS[2])
# --- Axes ---
if len(file_name) > 0: plt.suptitle(f'{file_name} - events topology summary')
else: plt.suptitle('Events topology summary')
plt.tight_layout()
# --- Buttons ---
class InputValues:
n_neighbors = None
min_dist = None
do_cluster = None
textbox_n = None
textbox_d = None
textbox_c = None
savefig = ''
def continue_button(self, event):
if len(self.textbox_n.text)>0:
self.n_neighbors = float(self.textbox_n.text)
print(f'input n_neighbors = {self.n_neighbors}')
if len(self.textbox_d.text)>0:
self.min_dist = float(self.textbox_d.text)
print(f'input min_dist = {self.min_dist}')
if len(self.textbox_c.text)>0:
self.do_cluster = bool(float(self.textbox_c.text))
print(f'input do_cluster = {self.do_cluster}')
plt.savefig(self.savefig)
plt.close()
# Make callback class
callback = InputValues()
if len(file_name) > 0:
callback.savefig = os.path.join(saveas_folder, f'{file_name}_events_topology_summary'+'_using'+''.join(use)+'.'+save_format)
else:
callback.savefig = os.path.join(saveas_folder, 'events_topology_summary'+'_using'+''.join(use)+'.'+save_format)
# n_neighbors button
axbox_n = plt.axes([1-(len(list_min_dists)-2)/len(list_min_dists)*0.7, 1-1/len(list_n_neighbors)/8*3, 0.1, 1/len(list_n_neighbors)/8]) if len(list_n_neighbors)>2 else plt.axes([0.95,0.95,0.05,0.05])
callback.textbox_n = TextBox(axbox_n, 'n_neighbors: ')
# min_dist button
axbox_d = plt.axes([1-(len(list_min_dists)-2)/len(list_min_dists)*0.7, 1-1/len(list_n_neighbors)/8*4.5, 0.1, 1/len(list_n_neighbors)/8]) if len(list_n_neighbors)>2 else plt.axes([0.95,0.90,0.05,0.05])
callback.textbox_d = TextBox(axbox_d, 'min_dist: ')
# do_cluster button
axbox_c = plt.axes([1-(len(list_min_dists)-2)/len(list_min_dists)*0.7, 1-1/len(list_n_neighbors)/8*6, 0.1, 1/len(list_n_neighbors)/8]) if len(list_n_neighbors)>2 else plt.axes([0.95,0.85,0.05,0.05])
callback.textbox_c = TextBox(axbox_c, 'do_cluster: ')
# Continue button
axbut_continue = plt.axes([1-(len(list_min_dists)-2)/len(list_min_dists)*0.4, 1-1/len(list_n_neighbors)/8*3, 0.1, 1/len(list_n_neighbors)/8*1.5]) if len(list_n_neighbors)>2 else plt.axes([0.95,0.80,0.05,0.05])
axbut = Button(axbut_continue, 'Continue')
axbut.on_clicked(callback.continue_button)
plt.show()
# Extract input values
n_neighbors = callback.n_neighbors
min_dist = callback.min_dist
do_cluster = callback.do_cluster
return n_neighbors, min_dist, do_cluster
def make_axis_figure(fig, axes, from_swr_detector, params, embedding, lfp_events, umap_events, iteration=0):
intrinsic_dimension = params['intrinsic_dimension']
n_neighbors = params['n_neighbors']
min_dist = params['min_dist']
n_axis_bins = params['n_axis_bins']
file_name = params['file_name']
saveas_folder = params['saveas_folder']
axis_method = params['axis_method']
use = params['use']
do_axis_grid = params['do_axis_grid']
fpsep = params['plot_fp_separately']
dops = ('power_spectrum' in use) | ('ps' in use) | ('powspectrm' in use)
# Check axis method
if (np.sum(from_swr_detector==0) == 0) | (np.sum(from_swr_detector==1) == 0):
axis_method = 'fit'
params['axis_method'] = axis_method
# Get xs and ys
xs = embedding[:,0] - np.mean(embedding[:,0])
ys = embedding[:,1] - np.mean(embedding[:,1])
# Fit a curve
if axis_method == 'centroids':
popt = centroid_curve(xs[from_swr_detector==1], ys[from_swr_detector==1], xs[from_swr_detector==0], ys[from_swr_detector==0])
elif axis_method == 'fit':
popt, _ = scipy.optimize.curve_fit(fit_axis, xs, ys)
# Project to the curve
xproj, yproj = project_to_curve(xs, ys, popt)
# Fit a UMAP axis
rini = np.array([xproj[np.argmin(xproj)], yproj[np.argmin(xproj)]])
rend = np.array([xproj[np.argmax(xproj)], yproj[np.argmax(xproj)]])
xdivs, ydivs = divide_axis(rini, rend, n_axis_bins, *popt)
# Cluster events along the axis
event_bins = bin_events_in_axis(xproj, xdivs)
swr_iis_index_list = np.arange(np.max(event_bins))
# Make histogram from xdivs and ydivs
yhists = make_projected_histogram(xproj, xdivs, n_axis_bins, from_swr_detector)
# Bin over/below the axis
event_bins_isup = ((ys-yproj) > 0)
# --- Plot UMAP cloud ---
plt.subplot(3, int(n_axis_bins//1.5), (1,int(n_axis_bins//1.5)+1))
for itype in range(len(COLORS)):
plt.scatter(xs, ys, 6, color=COLORS[from_swr_detector], alpha=1, linewidth=0)
# Plot projection
# for i in np.argwhere(from_swr_detector>0).flatten():
# plt.plot([xs[i],xproj[i]],[ys[i],yproj[i]], color=COLORS[from_swr_detector[i]], alpha=0.05)
# Plot fit axis
plt.plot(np.sort(xproj), fit_axis(np.sort(xproj), *popt), color='k', linewidth=0.8)
# Plot division
plt.scatter(xdivs, ydivs, 4, 'k')
# Plot grid division
if do_axis_grid:
r = 100
for xdiv, ydiv in zip(xdivs, ydivs):
xt, yt = axis_tangent(xdiv, r, *popt)
plt.plot([xdiv,xdiv-(yt-ydiv)], [ydiv,ydiv+(xt-xdiv)], 'k', linewidth=0.5, alpha=0.8)
plt.plot([xdiv,xdiv+(yt-ydiv)], [ydiv,ydiv-(xt-xdiv)], 'k', linewidth=0.5, alpha=0.8)
# Axis
plt.xlim([np.min([xs,ys])-0.1, np.max([xs,ys])+0.1])
plt.ylim([np.min([xs,ys])-0.1, np.max([xs,ys])+0.1])
plt.xticks([])
plt.yticks([])
plt.xlabel('UMAP 1')
plt.ylabel('UMAP 2')
# Legend
if np.sum((xs<np.mean(xs)) & (ys<np.mean(ys))) > np.sum((xs<np.mean(xs)) & (ys>np.mean(ys))):
plt.text(np.min(xs), np.max(ys)-(np.max(ys)-np.min(ys))*0.10, LABELS[0], color=COLORS[0])
plt.text(np.min(xs), np.max(ys)-(np.max(ys)-np.min(ys))*0.15, LABELS[1], color=COLORS[1])
plt.text(np.min(xs), np.max(ys)-(np.max(ys)-np.min(ys))*0.20, LABELS[2], color=COLORS[2])
else:
plt.text(np.min(xs), np.min(ys)+(np.max(ys)-np.min(ys))*0.10, LABELS[0], color=COLORS[0])
plt.text(np.min(xs), np.min(ys)+(np.max(ys)-np.min(ys))*0.15, LABELS[1], color=COLORS[1])
plt.text(np.min(xs), np.min(ys)+(np.max(ys)-np.min(ys))*0.20, LABELS[2], color=COLORS[2])
# --- Plot both histograms separately ---
plt.subplot(3, int(n_axis_bins//1.5), 2*int(n_axis_bins//1.5)+1)
types = np.unique(from_swr_detector)
n_type = len(types)
xhist = np.linspace(np.min(xproj), np.max(xproj), yhists.shape[1])
dxhist = xhist[1]-xhist[0]
for itype in range(n_type):
plt.bar(xhist+dxhist/2., yhists[itype], width=dxhist, color=COLORS[types[itype]], alpha=0.5)
if np.abs(np.sum(yhists)-n_type) < 0.05:
plt.ylabel('Event distribution')
else:
plt.ylabel('# events')
plt.xlabel('UMAP axis')
plt.plot(xhist, xhist*0, '-ok')
plt.xlim([xhist[0]-dxhist, xhist[-1]+dxhist])
plt.xticks(xhist, labels=1+np.arange(len(xhist)))
# --- Plot mean events ---
ylims = [np.inf, -np.inf]
ylims_ps = [np.inf, -np.inf]
updown_label = ['up','down']
for b in range(n_axis_bins):
for updw in range(1+do_axis_grid):
# If there is up/down grid
if do_axis_grid:
ids = (event_bins==b) & (from_swr_detector<(3-fpsep)) & (event_bins_isup==(1-updw))
# If all is merged
else:
ids = (event_bins==b) & (from_swr_detector<(3-fpsep))
if np.sum(ids)>0:
# Compute mean event
mean_event = np.mean(lfp_events[ids,:], axis=0)
std_event = np.std(lfp_events[ids,:], axis=0)
ylims[0] = np.nanmin([ylims[0], np.nanmin(mean_event-std_event)])
ylims[1] = np.nanmax([ylims[1], np.nanmax(mean_event+std_event)])
# Get mean color
n_pswr = np.sum(from_swr_detector[ids]==1)
n_pied = np.sum(from_swr_detector[ids]==0)
n_fps = np.sum(from_swr_detector[ids]==2)
color = (COLORS[0]*n_pied + COLORS[1]*n_pswr + COLORS[2]*n_fps)/(n_pswr+n_pied+n_fps) if (n_pswr+n_pied+n_fps) > 0 else np.array([.6,.6,.6])
# Plot LFP
axes[updw*(1+dops),2+b].fill_between(np.arange(len(mean_event)), (mean_event-std_event), (mean_event+std_event), color=color, alpha=0.3)
axes[updw*(1+dops),2+b].plot(np.arange(len(mean_event)), mean_event, color=color*0.8)
# Title
axes[updw*(1+dops),2+b].set_title(f'Bin {b+1}')
axes[updw*(1+dops),2+b].set_yticks([])
axes[updw*(1+dops),2+b].set_xticks([])
# Plot FPs separately
if fpsep:
if do_axis_grid:
ids_fp = (event_bins==b) & (from_swr_detector==2) & (event_bins_isup==(1-updw))
else:
ids_fp = (event_bins==b) & (from_swr_detector==2)
if np.sum(ids_fp) > 0:
mean_fp = np.mean(lfp_events[ids_fp,:], axis=0)
std_fp = np.std(lfp_events[ids_fp,:], axis=0)
h = 2*np.max(np.mean(lfp_events,axis=0))
ylims[1] = np.nanmax([ylims[1], h+np.nanmax(mean_fp+std_fp)])
# Plot LFP
axes[updw*(1+dops),2+b].fill_between(np.arange(len(mean_fp)), h+(mean_fp-std_fp), h+(mean_fp+std_fp), color=COLORS[2], alpha=0.3)
axes[updw*(1+dops),2+b].plot(np.arange(len(mean_fp)), h+mean_fp, color=COLORS[2]*0.8)
# Power spectrum
if dops:
# Get mean
freqs = params['ps_freqs']
mean_ps = np.mean(umap_events[ids,-len(freqs):], axis=0)
std_ps = np.std(umap_events[ids,-len(freqs):], axis=0)
ylims_ps[0] = np.nanmin([ylims_ps[0], np.nanmin(mean_ps-std_ps)])
ylims_ps[1] = np.nanmax([ylims_ps[1], np.nanmax(mean_ps+std_ps)])
axes[updw*(1+dops)+1,2+b].fill_between(freqs, (mean_ps-std_ps), (mean_ps+std_ps), color=color, alpha=0.3)
axes[updw*(1+dops)+1,2+b].plot(freqs, mean_ps, color=color*0.8)
axes[updw*(1+dops)+1,2+b].set_yticks([])
if updw==do_axis_grid: # 0=up, 1=down
axes[updw*(1+dops)+1,2+b].set_xlabel('Freq (Hz)')
else:
axes[updw*(1+dops)+1,2+b].set_xticks([])
if fpsep & np.sum((event_bins==b) & (from_swr_detector==2))>0:
mean_fp = np.mean(umap_events[ids_fp,-len(freqs)], axis=0)
std_fp = np.std(umap_events[ids_fp,-len(freqs)], axis=0)
h = 2*np.max(np.mean(umap_events[:,-len(freqs):],axis=0))
ylims_ps[1] = np.nanmax([ylims_ps[1], h+np.nanmax(mean_fp+std_fp)])
# Plot LFP
axes[updw*(1+dops),2+b].fill_between(freqs, h+(mean_fp-std_fp), h+(mean_fp+std_fp), color=COLORS[2], alpha=0.3)
axes[updw*(1+dops),2+b].plot(freqs, h+mean_fp, color=COLORS[2]*0.8)
else:
if updw==0:
axes[updw*(1+dops),2+b].set_title(f'Bin {b+1}')
for irow in range(axes.shape[0]):
axes[irow,2+b].set_xticks([])
axes[irow,2+b].set_yticks([])
axes[updw*(1+dops),2].set_ylabel(updown_label[updw])
if do_axis_grid & dops: axes[updw*(1+dops)+1,2].set_ylabel(updown_label[updw])
# Plot mean events
for b in range(n_axis_bins):
for updw in range(1+do_axis_grid):
axes[updw*(1+dops),2+b].set_ylim(ylims+np.array([-0.1,0.1]))
if dops:
axes[updw*(1+dops)+1,2+b].set_ylim(ylims_ps)
# --- Axes ---
if len(file_name) > 0:
plt.suptitle(f'{file_name} - UMAP axis : {intrinsic_dimension}D, n_neighbors={n_neighbors:.0f}, min_dist={min_dist:.1f}, using {"+".join(use)}')
else:
plt.suptitle(f' UMAP axis - {intrinsic_dimension}D, n_neighbors={n_neighbors:.0f}, min_dist={min_dist:.1f}, using {"+".join(use)} (iteration {iteration})')
return event_bins, event_bins_isup
def axis_projection_curation(embedding, lfp_events, umap_events, from_swr_detector, params):
'''
axis_projection_curation(embedding, lfp_events, umap_events, from_swr_detector, params)
Inputs:
-------
embedding (np.ndarray):
UMAP projection of 'lfp_events' into the low-dimension embedding
lfp_events (np.ndarray):
Array of size (#events, time) with all events to be curated
umap_events (np.ndarray):
Array of size (#events, time) with all inputs to umap
from_swr_detector (np.ndarray):
Array of size (#events,) specifying for each event how was it detected:
0 - from IED detector
1 - from SWR detector
2 - manually labeled as FP
params (dict):
Dictionary of parameters, including: id_fps, win_size, do_detrend, do_zscore,
list_n_neighbors, list_min_dists, max_B, intrinsic_dimension, n_elements,
n_axis_bins, file_name, saveas_folder, n_neighbors, min_dist, do_cluster,
embedding
Outputs:
--------
curated_labels (np.ndarray):
Boolean array of size (#events,) indicating if the curation has classified
each event as SWR (True) or IED (False).
'''
# Retrieve parameters
intrinsic_dimension = params['intrinsic_dimension']
n_neighbors = params['n_neighbors']
min_dist = params['min_dist']
n_axis_bins = params['n_axis_bins']
file_name = params['file_name']
saveas_folder = params['saveas_folder']
axis_method = params['axis_method']
save_format = params['save_format']
use = params['use']
do_axis_grid = params['do_axis_grid']
plot_fp_separately = params['plot_fp_separately']
# --- Generate the figure ------
n_rows = 1
if ('power_spectrum' in use) | ('ps' in use) | ('powspectrm' in use):
n_rows +=1
if do_axis_grid:
n_rows *= 2
fig, axes = plt.subplots(n_rows, n_axis_bins+2, figsize=((n_axis_bins+2)*1.5,4))
axes = axes.reshape(n_rows, n_axis_bins+2)
for irow in range(n_rows):
fig.delaxes(axes[irow,0])
fig.delaxes(axes[irow,1])
event_bins, event_bins_isup = make_axis_figure(fig, axes, from_swr_detector, params, embedding, lfp_events, umap_events)
# --- Buttons ---
class InputValues:
min_bin = None
max_bin = None
min_bin_up = None
max_bin_up = None
min_bin_down = None
max_bin_down = None
textbox_min = None
textbox_max = None
textbox_min_up = None
textbox_max_up = None
textbox_min_down = None
textbox_max_down = None
go_back = False
savefig = ''
lfp_events = None
curated_labels = None
params = None
method_b = None
event_bins = None
event_bins_isup = None
axes = None
def back_button(self, event):
self.go_back = True
plt.close()
def events_button(self, event):
if self.params['do_axis_grid']:
curated_labels_plot = np.copy(self.curated_labels)
min_bin_up = float(self.textbox_min_up.text)-1 if len(self.textbox_min_up.text)>0 else 0
min_bin_down = float(self.textbox_min_down.text)-1 if len(self.textbox_min_down.text)>0 else 0
max_bin_up = float(self.textbox_max_up.text)-1 if len(self.textbox_max_up.text)>0 else np.inf
max_bin_down = float(self.textbox_max_down.text)-1 if len(self.textbox_max_down.text)>0 else np.inf
curated_labels_plot = ((event_bins>=min_bin_up) & (event_bins<=max_bin_up) & event_bins_isup) | ((event_bins>=min_bin_down) & (event_bins<=max_bin_down) & (~event_bins_isup))
else:
curated_labels_plot = np.copy(self.curated_labels)
min_bin = float(self.textbox_min.text)-1 if len(self.textbox_min.text)>0 else 0
max_bin = float(self.textbox_max.text)-1 if len(self.textbox_max.text)>0 else np.inf
curated_labels_plot = (event_bins>=min_bin) & (event_bins<=max_bin)
plot_curated_events(self.lfp_events, curated_labels_plot, self.params, from_swr_detector=from_swr_detector)
def finish_button(self, event):
if self.params['do_axis_grid']:
if len(self.textbox_min_up.text)>0:
self.min_bin_up = float(self.textbox_min_up.text)-1
print(f'input min_bin_up = {self.min_bin_up+1}')
if len(self.textbox_max_up.text)>0:
self.max_bin_up = float(self.textbox_max_up.text)-1
print(f'input max_bin_up = {self.max_bin_up+1}')
if len(self.textbox_min_down.text)>0:
self.min_bin_down = float(self.textbox_min_down.text)-1
print(f'input min_bin_down = {self.min_bin_down+1}')
if len(self.textbox_max_down.text)>0:
self.max_bin_down = float(self.textbox_max_down.text)-1
print(f'input max_bin_down = {self.max_bin_down+1}')
else:
if len(self.textbox_min.text)>0:
self.min_bin = float(self.textbox_min.text)-1
print(f'input min_bin = {self.min_bin+1}')
if len(self.textbox_max.text)>0:
self.max_bin = float(self.textbox_max.text)-1
print(f'input max_bin = {self.max_bin+1}')
plt.savefig(self.savefig)
plt.close()
def method_button(self, event):
self.params['axis_method'] = 'fit' if (self.params['axis_method'] == 'centroids') else 'centroids'
plt.subplot(3, int(n_axis_bins//1.5), (1,int(n_axis_bins//1.5)+1)); plt.cla()
plt.subplot(3, int(n_axis_bins//1.5), 2*int(n_axis_bins//1.5)+1); plt.cla()
for ii in range(n_axis_bins):
for irow in range(axes.shape[0]):
axes[irow,2+ii].cla()
self.method_b.label.set_text('Wait...')
plt.draw() #redraw
self.event_bins, self.event_bins_isup = make_axis_figure(fig, axes, from_swr_detector, params, embedding, lfp_events, umap_events)
self.method_b.label.set_text('Change axis\nto '+('fit' if self.params['axis_method'] == 'centroids' else 'centroids'))
plt.draw() #redraw
# Make callback class
callback = InputValues()
callback.lfp_events = lfp_events
callback.curated_labels = np.ones(embedding.shape[0]).astype(bool)
callback.params = params
callback.event_bins = event_bins
callback.event_bins_isup = event_bins_isup
callback.axes = axes
savefig_details = 'umap_axis'+'_using'+''.join(use)+'_gridon'*do_axis_grid+'_FPseparately'*plot_fp_separately +'.'+save_format
if len(file_name) > 0:
callback.savefig = os.path.join(saveas_folder, f'{file_name}_{savefig_details}')
else:
callback.savefig = os.path.join(saveas_folder, savefig_details)
# Min/Max buttons
if do_axis_grid:
# min/max up button
axbox_min_up = plt.axes([0.97, 0.835, 0.025, 0.075])
callback.textbox_min_up = TextBox(axbox_min_up, 'up min_bin: ', textalignment='center')
axbox_max_up = plt.axes([0.97, 0.76, 0.025, 0.075])
callback.textbox_max_up = TextBox(axbox_max_up, 'up max_bin: ', textalignment='center')
# min/max down button
axbox_min_down = plt.axes([0.97, 0.675, 0.025, 0.075])
callback.textbox_min_down = TextBox(axbox_min_down, 'down min_bin: ', textalignment='center')
axbox_max_down = plt.axes([0.97, 0.60, 0.025, 0.075])
callback.textbox_max_down = TextBox(axbox_max_down, 'down max_bin: ', textalignment='center')
else:
# min_bin button
axbox_min = plt.axes([0.95, 0.75, 0.025, 0.075])
callback.textbox_min = TextBox(axbox_min, 'min_bin: ', textalignment='center')
# max_bin button
axbox_max = plt.axes([0.95, 0.65, 0.025, 0.075])
callback.textbox_max = TextBox(axbox_max, 'max_bin: ', textalignment='center')
# Back button
axbut_back = plt.axes([0.91, 0.50, 0.085, 0.09])
axbut_b = Button(axbut_back, 'Back')
axbut_b.on_clicked(callback.back_button)
# Plot events button
axbut_events = plt.axes([0.91, 0.40, 0.085, 0.09])
axbut_e = Button(axbut_events, 'Plot events')
axbut_e.on_clicked(callback.events_button)
# Finish button
axbut_finish = plt.axes([0.91, 0.30, 0.085, 0.09])
axbut_f = Button(axbut_finish, 'Finish')
axbut_f.on_clicked(callback.finish_button)
# Method button
axbut_method = plt.axes([0.91, 0.16, 0.085, 0.13])
axbut_m = Button(axbut_method, 'Change axis\nto '+('fit' if axis_method == 'centroids' else 'centroids'))
axbut_m.on_clicked(callback.method_button)
callback.method_b = axbut_m
plt.show()
# Extract input values
if callback.go_back:
# Go back
curated_labels = None
else:
# Return curated vector
if do_axis_grid:
curated_up = (callback.event_bins>=callback.min_bin_up) & (callback.event_bins<=callback.max_bin_up) & callback.event_bins_isup
curated_down = (callback.event_bins>=callback.min_bin_down) & (callback.event_bins<=callback.max_bin_down) & (~callback.event_bins_isup)
curated_labels = curated_up | curated_down
else:
min_bin = callback.min_bin
max_bin = callback.max_bin
curated_labels = (callback.event_bins>=min_bin) & (callback.event_bins<=max_bin)
params['axis_bins'] = callback.event_bins
return curated_labels, params
def cluster_curation(embedding, lfp_events, umap_events, from_swr_detector, params):
'''
cluster_curation(embedding, lfp_events, umap_events, from_swr_detector, params)
Inputs:
-------
embedding (np.ndarray):
UMAP projection of 'lfp_events' into the low-dimension embedding
lfp_events (np.ndarray):
Array of size (#events, time) with all events to be curated
umap_events (np.ndarray):
Array of size (#events, time) with all inputs to umap
from_swr_detector (np.ndarray):
Array of size (#events,) specifying for each event how was it detected:
0 - from IED detector
1 - from SWR detector
2 - manually labeled as FP
params (dict):
Dictionary of parameters, including: id_fps, win_size, do_detrend, do_zscore,
list_n_neighbors, list_min_dists, max_B, intrinsic_dimension, n_elements,
n_axis_bins, file_name, saveas_folder, n_neighbors, min_dist, do_cluster,
embedding
Outputs:
--------
curated_labels (np.ndarray):
Boolean array of size (#events,) indicating if the curation has classified
each event as SWR (True) or IED (False).
'''
# Retrieve parameters
intrinsic_dimension = params['intrinsic_dimension']
n_neighbors = params['n_neighbors']
min_dist = params['min_dist']
n_axis_bins = params['n_axis_bins']
file_name = params['file_name']
saveas_folder = params['saveas_folder']
save_format = params['save_format']
use = params['use']
# Select the embedding
curated_labels = np.ones_like(from_swr_detector).astype(bool)
embedding_original = np.copy(embedding)
# Prepare the iterative process
iteration = 1
do_update = True
while do_update:
if iteration > 1:
# Create embedding
embedding_umap = umap.UMAP(
n_neighbors=int(n_neighbors),
min_dist=min_dist,
n_components=intrinsic_dimension,
metric='euclidean',
metric_kwds=None,
# random_state=42
)
print(f'making embedding (n_neighbors={n_neighbors:.0f}, min_dist={min_dist:.1f})...\t\t\t\t', end='\r')
# Fit the data
embedding_umap.fit(lfp_events[curated_labels])
embedding = embedding_umap.embedding_
params['embedding'] = embedding
# Take 1st and 2nd dimension
xs = embedding[:,0] - np.mean(embedding[:,0])
ys = embedding[:,1] - np.mean(embedding[:,1])
# Cluster parameters depending on points in embedding
min_clus_size = int(np.ceil(len(embedding)*0.1))
min_samples = int(np.ceil(min_clus_size*0.05))
# Cluster
clusters = hdbscan.HDBSCAN(
min_samples=min_samples,
min_cluster_size=min_clus_size,
allow_single_cluster = False,
).fit_predict(embedding)
n_clusters = np.max(clusters)+1
# Make: pIEDs = cluster 0, pSWRs = cluster 1
event_sort = np.argsort([np.mean(from_swr_detector[curated_labels][clusters==ci]) for ci in np.unique(clusters)])
clusters_tmp = np.copy(clusters)
for iclu in range(n_clusters):
clusters[clusters_tmp==event_sort[iclu]] = iclu
# --- Generate the figure ------
fig, axes = plt.subplots(1,2, figsize=(14,7), sharey=True)
# --- Plot UMAP cloud ---
# Plot
axes[0].scatter(xs, ys, 6, color=COLORS[from_swr_detector[curated_labels]], alpha=1, linewidth=0)
axes[0].set_xticks([])
axes[0].set_yticks([])
axes[0].set_xlabel('UMAP 1')
axes[0].set_ylabel('UMAP 2')
axes[0].set_title('Colored by detector')
# Legend
if np.sum((xs<np.mean(xs)) & (ys<np.mean(ys))) > np.sum((xs<np.mean(xs)) & (ys>np.mean(ys))):
axes[0].text(np.min(xs), np.max(ys)-(np.max(ys)-np.min(ys))*0.10, LABELS[0], color=COLORS[0])
axes[0].text(np.min(xs), np.max(ys)-(np.max(ys)-np.min(ys))*0.15, LABELS[1], color=COLORS[1])
axes[0].text(np.min(xs), np.max(ys)-(np.max(ys)-np.min(ys))*0.20, LABELS[2], color=COLORS[2])
else:
axes[0].text(np.min(xs), np.min(ys)+(np.max(ys)-np.min(ys))*0.10, LABELS[0], color=COLORS[0])
axes[0].text(np.min(xs), np.min(ys)+(np.max(ys)-np.min(ys))*0.15, LABELS[1], color=COLORS[1])
axes[0].text(np.min(xs), np.min(ys)+(np.max(ys)-np.min(ys))*0.20, LABELS[2], color=COLORS[2])
# --- Cluster UMAP events ---
# Plot UMAP clusters
for i in np.unique(clusters):
if i == -1: axes[1].scatter(xs[clusters==i], ys[clusters==i], s=6, color=[.8,.8,.8], edgecolor='none')
else: axes[1].scatter(xs[clusters==i], ys[clusters==i], c=f'C{i+(i>=0)+(i>=2)}', s=6, edgecolor='none')
axes[1].set_xticks([])
axes[1].set_yticks([])
axes[1].set_xlabel('UMAP 1')
axes[1].set_ylabel('UMAP 2')
axes[1].set_title('Draw a polygon with clicks\nto select cluster')
for i in np.unique(clusters[clusters>=0]):
dx = np.mean(xs[clusters==i])
dy = np.mean(ys[clusters==i])
lx = np.max(xs)-np.min(xs)
ly = np.max(ys)-np.min(ys)
# Plot mean events
mean_event = np.mean(lfp_events[curated_labels][clusters==i], axis=0)
std_event = np.std(lfp_events[curated_labels][clusters==i], axis=0)
std_event = (std_event-np.min(std_event))/(np.max(np.mean(lfp_events[curated_labels],axis=0))-np.min(np.mean(lfp_events[curated_labels],axis=0)))
mean_event = (mean_event-np.min(mean_event))/(np.max(np.mean(lfp_events[curated_labels],axis=0))-np.min(np.mean(lfp_events[curated_labels],axis=0)))
axes[1].fill_between(dx-lx/6 + np.linspace(0,lx/3,lfp_events[curated_labels].shape[1]),
dy-ly/6 + (mean_event-std_event)*ly/3, dy-ly/6 + (mean_event+std_event)*ly/3,
color='k', alpha=0.4, edgecolor='none')
axes[1].plot(dx-lx/6 + np.linspace(0,lx/3,lfp_events[curated_labels].shape[1]), dy-ly/6 + mean_event*ly/3, 'k', linewidth=1.2)
# --- Axis ---
if len(file_name) > 0:
plt.suptitle(f'{file_name} - UMAP clustering (iteration {iteration})\n{intrinsic_dimension}D, n_neighbors={n_neighbors:.0f}, min_dist={min_dist:.1f}, using {"+".join(use)}')
else:
plt.suptitle(f'UMAP clustering - {intrinsic_dimension}D, n_neighbors={n_neighbors:.0f}, min_dist={min_dist:.1f}, using {"+".join(use)} (iteration {iteration})')
# --- Buttons
class InputPolygon:
verts = None
go_back = False
do_axis = False
do_finish = False
iteration = 0
savefig = ''
lfp_events = None
curated_labels = None
params = None
X = None
Y = None
embedding = None
def line_select(self, verts):
self.verts = verts
def back_button(self, event):