lunapi 1.3.1__cp313-cp313-musllinux_1_2_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lunapi/lunapi1.py ADDED
@@ -0,0 +1,2546 @@
1
+ """lunapi1 module: a high-level wrapper around lunapi0 module functions"""
2
+
3
+ # Luna Python interface (lunapi)
4
+ # v1.3.1, 16-Sep-2025
5
+
6
+ import lunapi.lunapi0 as _luna
7
+
8
+ import pandas as pd
9
+ import numpy as np
10
+ from scipy.stats.mstats import winsorize
11
+ import matplotlib.pyplot as plt
12
+ from matplotlib import cm
13
+ from IPython.core import display as ICD
14
+ import plotly.graph_objects as go
15
+ import plotly.express as px
16
+ from ipywidgets import widgets, AppLayout
17
+ from itertools import cycle
18
+ import re
19
+ import requests
20
+ import io
21
+ import tempfile
22
+ import os
23
+ import tempfile
24
+ from ipywidgets import IntProgress
25
+ from IPython.display import display
26
+ import time
27
+ import pathlib
28
+
29
+
30
+ # resource set for Docker container version
31
+ class resources:
32
+ POPS_PATH = '/build/nsrr/common/resources/pops/'
33
+ POPS_LIB = 's2'
34
+ MODEL_PATH = '/build/luna-models/'
35
+
36
+ lp_version = "v1.3.1"
37
+
38
+ # C++ singleton class (engine & sample list)
39
+ # lunapi_t --> luna
40
+
41
+ # one observation
42
+ # lunapi_inst_T --> inst
43
+
44
+ # --------------------------------------------------------------------------------
45
+ # luna class
46
+
47
+ class proj:
48
+ """Instance of the underlying Luna engine
49
+
50
+ Only a single instance of this will be generated per session, via proj().
51
+
52
+ This class also contains a sample-list and utility functions for importing
53
+ Luna output databases.
54
+ """
55
+
56
+ # single static engine class
57
+ eng = _luna.inaugurate()
58
+
59
+ def __init__(self, verbose = True ):
60
+ self.n = 0
61
+ if verbose: print( "initiated lunapi",lp_version,proj.eng ,"\n" )
62
+ self.silence( False )
63
+ self.eng = _luna.inaugurate()
64
+
65
+ def retire(self):
66
+ """Retires an existing Luna engine generated via proj()"""
67
+ return _luna.retire()
68
+
69
+ def build( self, args ):
70
+ """Builds an internal sample-list object given a set of folders
71
+
72
+ This generates an internal sample-list by finding EDF and annotation
73
+ files across one or more folders. This provides the same functionality
74
+ as the `--build` option of Luna, which is described here:
75
+
76
+ https://zzz.bwh.harvard.edu/luna/ref/helpers/#-build
77
+
78
+ After building, a call to sample_list() will return the number of individuals
79
+
80
+ Parameters
81
+ ----------
82
+ args : [ str ]
83
+ a list of folder names and optional arguments to be passed to build
84
+
85
+ """
86
+
87
+ # first clear any existing sample list
88
+ proj.eng.clear()
89
+
90
+ # then try to build a new one
91
+ if type( args ) is not list: args = [ args ]
92
+ return proj.eng.build_sample_list( args )
93
+
94
+
95
+ def sample_list(self, filename = None , path = None , df = True ):
96
+ """Reads a sample-list 'filenamne', optionally setting 'path' and returns the number of observations
97
+
98
+ If filename is not defined, this returns the internal sample list
99
+ as an object
100
+
101
+ Parameters
102
+ ----------
103
+ filename : str
104
+ optional filename of a sample-list to read
105
+
106
+ path : str
107
+ optional path to preprend to the sample-list when reading (sets the 'path' variable)
108
+
109
+ df : bool
110
+ if returning a sample-list, return as a Pandas dataframe
111
+
112
+ Returns
113
+ -------
114
+ list
115
+ a list of strings representing the sample-list (IDs, EDFs, annotations for each individual)
116
+ """
117
+
118
+ # return sample list
119
+ if filename is None:
120
+ sl = proj.eng.get_sample_list()
121
+ if df is True:
122
+ sl = pd.DataFrame( sl )
123
+ sl.columns = [ 'ID' , 'EDF', 'Annotations' ]
124
+ sl.index += 1
125
+ return sl
126
+
127
+ # set path?
128
+ if path is not None:
129
+ print( "setting path to " , path )
130
+ self.var( 'path' , path )
131
+
132
+ # read sample list from file, after clearing anything present
133
+ proj.eng.clear()
134
+ self.n = proj.eng.read_sample_list( filename )
135
+ print( "read",self.n,"individuals from" , filename )
136
+
137
+
138
+ #------------------------------------------------------------------------
139
+
140
+ def nobs(self):
141
+ """The number of observations in the internal sample list
142
+
143
+ Returns
144
+ -------
145
+ int
146
+ the number of observations in the sample-list
147
+ """
148
+
149
+ return proj.eng.nobs()
150
+
151
+ #------------------------------------------------------------------------
152
+
153
+ def validate( self ):
154
+ """Validates an internal sample-list
155
+
156
+ This provides the same functionality
157
+ as the `--validate` option of Luna, which is described here:
158
+
159
+ https://zzz.bwh.harvard.edu/luna/ref/helpers/#-validate
160
+
161
+ Parameters
162
+ ----------
163
+ none
164
+
165
+ """
166
+
167
+ tbl = proj.eng.validate_sample_list()
168
+ tbl = pd.DataFrame( tbl )
169
+ tbl.columns = [ 'ID' , 'Filename', 'Valid' ]
170
+ tbl.index += 1
171
+ return tbl
172
+
173
+
174
+ #------------------------------------------------------------------------
175
+
176
+ def reset(self):
177
+ """ Drop Luna problem flag """
178
+ proj.eng.reset()
179
+
180
+ def reinit(self):
181
+ """ Re-initialize project """
182
+ proj.eng.reinit()
183
+
184
+ #------------------------------------------------------------------------
185
+
186
+ def inst( self, n ):
187
+ """Generates a new instance"""
188
+
189
+ # check bounds
190
+ if type(n) is int:
191
+ # use 1-based counts as inputs
192
+ n = n - 1
193
+ if n < 0 or n >= self.nobs():
194
+ print( "index out-of-bounds given sample list of " + str(self.nobs()) + " records" )
195
+ return
196
+
197
+ # if the arg is a str that matches a sample-list
198
+ if type(n) is str:
199
+ sn = self.get_n(n)
200
+ if type(sn) is int: n = sn
201
+
202
+ # return based on n (from sample-list) or string/empty (new instance)
203
+ return inst(proj.eng.inst( n ))
204
+
205
+
206
+ #------------------------------------------------------------------------
207
+
208
+ def empty_inst( self, id, nr, rs, startdate = '01.01.00', starttime = '00.00.00' ):
209
+ """Generates a new instance with empty fixed-size EDF"""
210
+
211
+ # check inputs
212
+ nr = int( nr )
213
+ rs = int( rs )
214
+ if nr < 0:
215
+ print( "expecting nr (number of records) to be a positive integer" )
216
+ return
217
+ if rs < 0:
218
+ print( "expecting rs (record duration, secs) to be a positive integer" )
219
+ return
220
+
221
+ # return instance of fixed size
222
+ return inst(proj.eng.empty_inst(id, nr, rs, startdate, starttime ))
223
+
224
+ #------------------------------------------------------------------------
225
+ def clear(self):
226
+ """Clears any existing project sample-list"""
227
+ proj.eng.clear()
228
+
229
+
230
+ #------------------------------------------------------------------------
231
+ def silence(self, b = True , verbose = False ):
232
+ """Toggles the output mode on/off"""
233
+ if verbose:
234
+ if b: print( 'silencing console outputs' )
235
+ else: print( 'enabling console outputs' )
236
+ proj.eng.silence(b)
237
+
238
+ #------------------------------------------------------------------------
239
+ def is_silenced(self, b = True ):
240
+ """Reports on whether log is silenced"""
241
+ return proj.eng.is_silenced()
242
+
243
+
244
+ #------------------------------------------------------------------------
245
+ def flush(self):
246
+ """Internal command, to flush the output buffer"""
247
+ proj.eng.flush()
248
+
249
+ # --------------------------------------------------------------------------------
250
+ def include( self, f ):
251
+ """Include options/variables from a @parameter-file"""
252
+ return proj.eng.include( f )
253
+
254
+
255
+ #------------------------------------------------------------------------
256
+ def aliases( self ):
257
+ """Return a table of signal/annotation aliases"""
258
+ t = pd.DataFrame( proj.eng.aliases() )
259
+ t.index = t.index + 1
260
+ if len( t ) == 0: return t
261
+ t.columns = ["Type", "Preferred", "Case-insensitive, sanitized alias" ]
262
+ with pd.option_context('display.max_rows', None,):
263
+ display(t)
264
+
265
+ #------------------------------------------------------------------------
266
+ def var(self , key=None , value=None):
267
+ """Set or get project-level options(s)/variables(s)"""
268
+ return self.vars( key, value )
269
+
270
+ #------------------------------------------------------------------------
271
+ def vars(self , key=None , value=None):
272
+ """Set or get project-level options(s)/variables(s)"""
273
+
274
+ # return all vars?
275
+ if key is None:
276
+ return proj.eng.get_all_opts()
277
+
278
+ # return one or more vars?
279
+ if value is None:
280
+
281
+ # return 1?
282
+ if type( key ) is str:
283
+ return proj.eng.get_opt( key )
284
+
285
+ # return some?
286
+ if type( key ) is list:
287
+ return proj.eng.get_opts( key )
288
+
289
+ # set from a dict
290
+ if isinstance(key, dict):
291
+ for k, v in key.items():
292
+ self.vars(k,v)
293
+ return
294
+
295
+ # set a single pair
296
+ proj.eng.opt( key, str( value ) )
297
+
298
+
299
+ #------------------------------------------------------------------------
300
+ # def clear_var(self,key):
301
+ # """Clear project-level option(s)/variable(s)"""
302
+ # self.clear_vars(key)
303
+
304
+ #------------------------------------------------------------------------
305
+ def clear_vars(self,key = None ):
306
+ """Clear project-level option(s)/variable(s)"""
307
+
308
+ # clear all
309
+ if key is None:
310
+ proj.eng.clear_all_opts()
311
+ # and a spectial case: the sig list
312
+ self.vars( 'sig', '' )
313
+ return
314
+
315
+ # clear some/one
316
+ if type(key) is not list: key = [ key ]
317
+ proj.eng.clear_opts(key)
318
+
319
+
320
+ #------------------------------------------------------------------------
321
+ def clear_ivars(self):
322
+ """Clear individual-level variables for all individuals"""
323
+ proj.eng.clear_ivars()
324
+
325
+ #------------------------------------------------------------------------
326
+ def get_n(self,id):
327
+ """Return the number of individuals in the sample-list"""
328
+ return proj.eng.get_n(id)
329
+
330
+ #------------------------------------------------------------------------
331
+ def get_id(self,n):
332
+ """Return the ID of an individual from the sample-list"""
333
+ return proj.eng.get_id(n)
334
+
335
+ #------------------------------------------------------------------------
336
+ def get_edf(self,x):
337
+ """Return the EDF filename for an individual from the sample-list"""
338
+ if ( isinstance(x,int) ):
339
+ return proj.eng.get_edf(x)
340
+ else:
341
+ return proj.eng.get_edf(proj.eng.get_n(x))
342
+
343
+
344
+ #------------------------------------------------------------------------
345
+ def get_annots(self,x):
346
+ """Return the annotation filenames for an individual from the sample-list"""
347
+ if ( isinstance(x,int) ):
348
+ return proj.eng.get_annot(x)
349
+ else:
350
+ return proj.eng.get_annot(proj.eng.get_n(x))
351
+
352
+
353
+ #------------------------------------------------------------------------
354
+ def import_db(self,f,s=None):
355
+ """Import a destrat-style Luna output database"""
356
+ if s is None:
357
+ return proj.eng.import_db(f)
358
+ else:
359
+ return proj.eng.import_db_subset(f,s)
360
+
361
+ #------------------------------------------------------------------------
362
+ def desc( self ):
363
+ """Returns table of descriptives for all sample-list individuals"""
364
+ silence_mode = self.is_silenced()
365
+ self.silence(True,False)
366
+ t = pd.DataFrame( proj.eng.desc() )
367
+ self.silence( silence_mode , False )
368
+ t.index = t.index + 1
369
+ if len( t ) == 0: return t
370
+ t.columns = ["ID","Gapped","Date","Start(hms)","Stop(hms)","Dur(hms)","Dur(s)","# sigs","# annots","Signals" ]
371
+ with pd.option_context('max_colwidth',None):
372
+ display(t)
373
+
374
+
375
+ #------------------------------------------------------------------------
376
+ def proc(self, cmdstr ):
377
+ """Evaluates one or more Luna commands for all sample-list individuals"""
378
+ r = proj.eng.eval(cmdstr)
379
+ return tables( r )
380
+
381
+ #------------------------------------------------------------------------
382
+ def silent_proc(self, cmdstr ):
383
+ """Silently evaluates one or more Luna commands for all sample-list individuals"""
384
+ silence_mode = self.is_silenced()
385
+ self.silence(True,False)
386
+ r = proj.eng.eval(cmdstr)
387
+ self.silence( silence_mode , False )
388
+ return tables( r )
389
+
390
+ #------------------------------------------------------------------------
391
+ def commands( self ):
392
+ """Return a list of commands in the output set (following proc()"""
393
+ t = pd.DataFrame( proj.eng.commands() )
394
+ t.columns = ["Command"]
395
+ return t
396
+
397
+ #------------------------------------------------------------------------
398
+ def empty_result_set( self ):
399
+ return len( proj.eng.strata() ) == 0
400
+
401
+ #------------------------------------------------------------------------
402
+ def strata( self ):
403
+ """Return a datraframe of command/strata pairs from the output set"""
404
+
405
+ if self.empty_result_set(): return None
406
+ t = pd.DataFrame( proj.eng.strata() )
407
+ t.columns = ["Command","Strata"]
408
+ return t
409
+
410
+ #------------------------------------------------------------------------
411
+ def table( self, cmd , strata = 'BL' ):
412
+ """Return a dataframe from the output set"""
413
+ if self.empty_result_set(): return None
414
+ r = proj.eng.table( cmd , strata )
415
+ t = pd.DataFrame( r[1] ).T
416
+ t.columns = r[0]
417
+ return t
418
+
419
+ #------------------------------------------------------------------------
420
+ def variables( self, cmd , strata = 'BL' ):
421
+ """Return a list of all variables for an output table"""
422
+ if self.empty_result_set(): return None
423
+ return proj.eng.vars( cmd , strata )
424
+
425
+
426
+ #
427
+ # --------------------------------------------------------------------------------
428
+ # project level wrapper functions
429
+ #
430
+
431
+ # --------------------------------------------------------------------------------
432
+ def pops( self, s = None, s1 = None , s2 = None,
433
+ path = None , lib = None ,
434
+ do_edger = True ,
435
+ no_filter = False ,
436
+ do_reref = False ,
437
+ m = None , m1 = None , m2 = None,
438
+ lights_off = '.' , lights_on = '.' ,
439
+ ignore_obs = False,
440
+ args = '' ):
441
+ """Run the POPS stager"""
442
+
443
+ if path is None: path = resources.POPS_PATH
444
+ if lib is None: lib = resources.POPS_LIB
445
+
446
+ import os
447
+ if not os.path.isdir( path ):
448
+ return 'could not open POPS resource path ' + path
449
+
450
+ if s is None and s1 is None:
451
+ print( 'must set s or s1 and s2 to EEGs' )
452
+ return
453
+
454
+ if ( s1 is None ) != ( s2 is None ):
455
+ print( 'must set s or s1 and s2 to EEGs' )
456
+ return
457
+
458
+ # set options
459
+ self.var( 'mpath' , path )
460
+ self.var( 'lib' , lib )
461
+ self.var( 'do_edger' , '1' if do_edger else '0' )
462
+ self.var( 'do_reref' , '1' if do_reref else '0' )
463
+ self.var( 'no_filter' , '1' if no_filter else '0' )
464
+ self.var( 'LOFF' , lights_off )
465
+ self.var( 'LON' , lights_on )
466
+
467
+ if s is not None: self.var( 's' , s )
468
+ else: self.clear_vars( 's' )
469
+
470
+ if m is not None: self.var( 'm' , m )
471
+ else: self.clear_vars( 'm' )
472
+
473
+ if s1 is not None: self.var( 's1' , s1 )
474
+ else: self.clear_vars( 's1' )
475
+
476
+ if s2 is not None: self.var( 's2' , s2 )
477
+ else: self.clear_vars( 's2' )
478
+
479
+ if m1 is not None: self.var( 'm1' , m1 )
480
+ else: self.clear_vars( 'm1' )
481
+
482
+ if m2 is not None: self.var( 'm2' , m2 )
483
+ else: self.clear_vars( 'm2' )
484
+
485
+ # get either one- or two-channel mode Luna script from POPS folder
486
+ twoch = s1 is not None and s2 is not None;
487
+ if twoch: cmdstr = cmdfile( path + '/s2.ch2.txt' )
488
+ else: cmdstr = cmdfile( path + '/s2.ch1.txt' )
489
+
490
+ # swap in any additional options to POPS
491
+ if ignore_obs is True:
492
+ args = args + ' ignore-obs-staging';
493
+ if do_edger is True:
494
+ cmdstr = cmdstr.replace( 'EDGER' , 'EDGER all' )
495
+ if args != '':
496
+ cmdstr = cmdstr.replace( 'POPS' , 'POPS ' + args + ' ')
497
+
498
+ # run the command
499
+ self.proc( cmdstr )
500
+
501
+ # return of results
502
+ return self.table( 'POPS' )
503
+
504
+
505
+ # --------------------------------------------------------------------------------
506
+ def predict_SUN2019( self, cen , th = '3' , path = None ):
507
+ """Run SUN2019 prediction model for a project
508
+
509
+ This assumes that ${age} will be set via a vars file, i.e.
510
+
511
+ proj.var( 'vars' , 'ages.txt' )
512
+
513
+ """
514
+ if path is None: path = resources.MODEL_PATH
515
+ if type( cen ) is list: cen = ','.join( cen )
516
+ self.var( 'cen' , cen )
517
+ self.var( 'mpath' , path )
518
+ self.var( 'th' , str(th) )
519
+ self.proc( cmdfile( resources.MODEL_PATH + '/m1-adult-age-luna.txt' ) )
520
+ return self.table( 'PREDICT' )
521
+
522
+
523
+
524
+
525
+
526
+
527
+ # ================================================================================
528
+ # --------------------------------------------------------------------------------
529
+ #
530
+ # inst class
531
+ #
532
+ # --------------------------------------------------------------------------------
533
+ # ================================================================================
534
+
535
+ class inst:
536
+ """This class represents a single individual/instance (signals & annotations)"""
537
+
538
+ def __init__(self,p=None):
539
+ if ( isinstance(p,str) ):
540
+ self.edf = _luna.inst(p)
541
+ elif (isinstance(p,_luna.inst)):
542
+ self.edf = p
543
+ else:
544
+ self.edf = _luna.inst()
545
+
546
+ def __repr__(self):
547
+ return f'{self.edf}'
548
+
549
+ #------------------------------------------------------------------------
550
+ def id(self):
551
+ return self.edf.get_id()
552
+
553
+ #------------------------------------------------------------------------
554
+ def attach_edf( self, f ):
555
+ """Attach an EDF from a file"""
556
+ return self.edf.attach_edf( f )
557
+
558
+ #------------------------------------------------------------------------
559
+ def attach_annot( self, annot ):
560
+ """Attach annotations from a file"""
561
+ return self.edf.attach_annot( annot )
562
+
563
+ #------------------------------------------------------------------------
564
+ def stat( self ):
565
+ """Return a dataframe of basic statistics"""
566
+ t = pd.DataFrame( self.edf.stat(), index=[0] ).T
567
+ t.columns = ["Value"]
568
+ return t
569
+
570
+ #------------------------------------------------------------------------
571
+ def refresh( self ):
572
+ """Refresh an attached EDF"""
573
+ self.edf.refresh()
574
+ # also need to reset Luna problem flag
575
+ # note: current kludge: problem is proj-wide
576
+ # so this will not play well w/ multiple EDFs
577
+ # todo: implement inst-specific prob flag
578
+
579
+ _proj = proj(False)
580
+ _proj.reset();
581
+
582
+
583
+ #------------------------------------------------------------------------
584
+ def clear_vars(self, keys = None ):
585
+ """Clear some or all individual-level variable(s)"""
586
+
587
+ # all
588
+ if keys is None:
589
+ self.edf.clear_ivar()
590
+ return
591
+
592
+ # one/some
593
+ if type( keys ) is not set: keys = set( keys )
594
+ self.edf.clear_selected_ivar( keys )
595
+
596
+ #------------------------------------------------------------------------
597
+ def var( self , key = None , value = None ):
598
+ """Set or get individual-level variable(s)"""
599
+ return self.vars( key , value )
600
+
601
+ #------------------------------------------------------------------------
602
+ def vars( self , key = None , value = None ):
603
+ """Set or get individual-level variable(s)"""
604
+
605
+ # return all i-vars
606
+ if key is None:
607
+ return self.edf.ivars()
608
+
609
+ # return one i-var
610
+ if value is None and type( key ) is str:
611
+ return self.edf.get_ivar( key )
612
+
613
+ # set from a dict of key-value pairs
614
+ if isinstance(key, dict):
615
+ for k, v in key.items():
616
+ self.vars(k,v)
617
+ return
618
+
619
+ # set a single pair
620
+ self.edf.ivar( key , str(value) )
621
+
622
+
623
+ #------------------------------------------------------------------------
624
+ def desc( self ):
625
+ """Returns of dataframe of current channels"""
626
+ t = pd.DataFrame( self.edf.desc() ).T
627
+ t.index = t.index + 1
628
+ if len( t ) == 0: return t
629
+ t.columns = ["ID","Gapped","Date","Start(hms)","Stop(hms)","Dur(hms)","Dur(s)","# sigs","# annots","Signals" ]
630
+ with pd.option_context('display.max_colwidth',None):
631
+ display(t)
632
+
633
+ #------------------------------------------------------------------------
634
+ def channels( self ):
635
+ """Returns of dataframe of current channels"""
636
+ t = pd.DataFrame( self.edf.channels() )
637
+ if len( t ) == 0: return t
638
+ t.columns = ["Channels"]
639
+ return t
640
+
641
+ #------------------------------------------------------------------------
642
+ def chs( self ):
643
+ """Returns of dataframe of current channels"""
644
+ t = pd.DataFrame( self.edf.channels() )
645
+ if len( t ) == 0: return t
646
+ t.columns = ["Channels"]
647
+ return t
648
+
649
+ #------------------------------------------------------------------------
650
+ def headers(self):
651
+ """Return channel header info"""
652
+ _proj = proj(False)
653
+ silence_mode = _proj.is_silenced()
654
+ _proj.silence(True,False)
655
+ df = self.proc( "HEADERS" )[ 'HEADERS: CH' ]
656
+ _proj.silence( silence_mode , False )
657
+ return df
658
+
659
+ #------------------------------------------------------------------------
660
+ def annots( self ):
661
+ """Returns of dataframe of current annotations"""
662
+ t = pd.DataFrame( self.edf.annots() )
663
+ if len( t ) == 0: return t
664
+ t.columns = ["Annotations"]
665
+ return t
666
+
667
+ #------------------------------------------------------------------------
668
+ def fetch_annots( self , anns , interp = -1 ):
669
+ """Returns of dataframe of annotation events"""
670
+ if type( anns ) is not list: anns = [ anns ]
671
+ t = pd.DataFrame( self.edf.fetch_annots( anns , interp ) )
672
+ if len( t ) == 0: return t
673
+ t.columns = ['Class', 'Start', 'Stop' ]
674
+ t = t.sort_values(by=['Start', 'Stop', 'Class'])
675
+ t['Start'] = t['Start'].round(decimals=3)
676
+ t['Stop'] = t['Stop'].round(decimals=3)
677
+ return t
678
+
679
+ #------------------------------------------------------------------------
680
+ def fetch_fulls_annots( self , anns ):
681
+ """Returns of dataframe of annotation events"""
682
+ if type( anns ) is not list: anns = [ anns ]
683
+ t = pd.DataFrame( self.edf.fetch_full_annots( anns ) )
684
+ if len( t ) == 0: return t
685
+ t.columns = ['Class', 'Instance','Channel','Meta','Start', 'Stop' ]
686
+ t = t.sort_values(by=['Start', 'Stop', 'Class','Instance'])
687
+ t['Start'] = t['Start'].round(decimals=3)
688
+ t['Stop'] = t['Stop'].round(decimals=3)
689
+ return t
690
+
691
+ #------------------------------------------------------------------------
692
+ def eval( self, cmdstr ):
693
+ """Evaluate one or more Luna commands, storing results internally"""
694
+ self.edf.eval( cmdstr )
695
+ return self.strata()
696
+
697
+ #------------------------------------------------------------------------
698
+ def proc( self, cmdstr ):
699
+ """Evaluate one or more Luna commands, returning results as an object"""
700
+ # < log , tables >
701
+ r = self.edf.proc( cmdstr )
702
+ # extract and return result tables
703
+ return tables( r[1] )
704
+
705
+ #------------------------------------------------------------------------
706
+ def silent_proc( self, cmdstr ):
707
+ """Silently evaluate one or more Luna commands (for internal use)"""
708
+
709
+ _proj = proj(False)
710
+ silence_mode = _proj.is_silenced()
711
+ _proj.silence(True,False)
712
+
713
+ r = self.edf.proc( cmdstr )
714
+
715
+ _proj.silence( silence_mode , False )
716
+
717
+ # extract and return result tables
718
+ return tables( r[1] )
719
+
720
+ #------------------------------------------------------------------------
721
+ def empty_result_set( self ):
722
+ return len( self.edf.strata() ) == 0
723
+
724
+ #------------------------------------------------------------------------
725
+ def strata( self ):
726
+ """Return a dataframe of command/strata pairs from the output set"""
727
+ if ( self.empty_result_set() ): return None
728
+ t = pd.DataFrame( self.edf.strata() )
729
+ t.columns = ["Command","Strata"]
730
+ return t
731
+
732
+ #------------------------------------------------------------------------
733
+ def table( self, cmd , strata = 'BL' ):
734
+ """Return a dataframe for a given command/strata pair from the output set"""
735
+ if ( self.empty_result_set() ): return None
736
+ r = self.edf.table( cmd , strata )
737
+ t = pd.DataFrame( r[1] ).T
738
+ t.columns = r[0]
739
+ return t
740
+
741
+ #------------------------------------------------------------------------
742
+ def variables( self, cmd , strata = 'BL' ):
743
+ """Return a list of all variables for a output set table"""
744
+ if ( self.empty_result_set() ): return None
745
+ return self.edf.variables( cmd , strata )
746
+
747
+
748
+ #------------------------------------------------------------------------
749
+ def e2i( self, epochs ):
750
+ """Helper function to convert epoch (1-based) to intervals"""
751
+ if type( epochs ) is not list: epochs = [ epochs ]
752
+ return self.edf.e2i( epochs )
753
+
754
+ # --------------------------------------------------------------------------------
755
+ def s2i( self, secs ):
756
+ """Helper function to convert seconds to intervals"""
757
+ return self.edf.s2i( secs )
758
+
759
+ # --------------------------------------------------------------------------------
760
+ def data( self, chs , annots = None , time = False ):
761
+ """Returns all data for certain channels and annotations"""
762
+ if type( chs ) is not list: chs = [ chs ]
763
+ if annots is not None:
764
+ if type( annots ) is not list: annots = [ annots ]
765
+ if annots is None: annots = [ ]
766
+ return self.edf.data( chs , annots , time )
767
+
768
+ # --------------------------------------------------------------------------------
769
+ def slice( self, intervals, chs , annots = None , time = False ):
770
+ """Return signal/annotation data aggregated over a set of intervals"""
771
+ if type( chs ) is not list: chs = [ chs ]
772
+ if annots is not None:
773
+ if type( annots ) is not list: annots = [ annots ]
774
+ if annots is None: annots = [ ]
775
+ return self.edf.slice( intervals, chs , annots , time )
776
+
777
+ # --------------------------------------------------------------------------------
778
+ def slices( self, intervals, chs , annots = None , time = False ):
779
+ """Return a series of signal/annotation data objects for each requested interval"""
780
+ if type( chs ) is not list: chs = [ chs ]
781
+ if annots is not None:
782
+ if type( annots ) is not list: annots = [ annots ]
783
+ if annots is None: annots = [ ]
784
+ return self.edf.slices( intervals, chs , annots , time )
785
+
786
+ # --------------------------------------------------------------------------------
787
+ def insert_signal( self, label , data , sr ):
788
+ """Insert a signal into an in-memory EDF"""
789
+ return self.edf.insert_signal( label , data , sr )
790
+
791
+ # --------------------------------------------------------------------------------
792
+ def update_signal( self, label , data ):
793
+ """Update an existing signal in an in-memory EDF"""
794
+ return self.edf.update_signal( label , data )
795
+
796
+ # --------------------------------------------------------------------------------
797
+ def insert_annot( self, label , intervals, durcol2 = False ):
798
+ """Insert annotations into an in-memory dataset"""
799
+ return self.edf.insert_annot( label , intervals , durcol2 )
800
+
801
+
802
+
803
+ # --------------------------------------------------------------------------------
804
+ #
805
+ # Luna function wrappers
806
+ #
807
+ # --------------------------------------------------------------------------------
808
+
809
+
810
+ # --------------------------------------------------------------------------------
811
+ def freeze( self , f ):
812
+ self.eval( 'FREEZE ' + f )
813
+
814
+ # --------------------------------------------------------------------------------
815
+ def thaw( self , f , remove = False ):
816
+ if remove:
817
+ self.eval( 'THAW tag=' + f + 'remove' )
818
+ else:
819
+ self.eval( 'THAW ' + f )
820
+
821
+ # --------------------------------------------------------------------------------
822
+ def empty_freezer( self ):
823
+ self.eval( 'CLEAN-FREEZER' )
824
+
825
+ # --------------------------------------------------------------------------------
826
+ def mask( self , f = None ):
827
+ if f is None: return
828
+ if type(f) is not list: f = [ f ]
829
+ [ self.eval( 'MASK ' + _f ) for _f in f ]
830
+ self.eval( 'RE' )
831
+
832
+
833
+ # --------------------------------------------------------------------------------
834
+ def segments( self ):
835
+ self.eval( 'SEGMENTS' )
836
+ return self.table( 'SEGMENTS' , 'SEG' )
837
+
838
+ # --------------------------------------------------------------------------------
839
+ def epoch( self , f = '' ):
840
+ self.eval( 'EPOCH ' + f )
841
+
842
+
843
+ # --------------------------------------------------------------------------------
844
+ def epochs( self ):
845
+ self.eval( 'EPOCH table' )
846
+ df = self.table( 'EPOCH' , 'E' )
847
+ df = df[[ 'E', 'E1', 'LABEL', 'HMS', 'START','STOP','DUR' ]]
848
+ #df = df.drop(columns = ['ID','TP','MID','INTERVAL'] )
849
+ return df
850
+
851
+
852
+ # --------------------------------------------------------------------------------
853
+ # tfview : spectral regional viewer
854
+
855
+ # for high-def plots:
856
+ # import matplotlib as mpl
857
+ # mpl.rcParams['figure.dpi'] = 300
858
+
859
+ def tfview( self , ch,
860
+ e = None , t = None , a = None,
861
+ tw = 2, sec = 2 , inc = 0.1 ,
862
+ f = ( 0.5 , 30 ) , winsor = 0.025 ,
863
+ anns = None , norm = None ,
864
+ traces = True,
865
+ xlines = None , ylines = None , silent = True , pal = 'turbo' ):
866
+
867
+ """Generates an MTM spectrogram
868
+
869
+ Main channel
870
+ ------------
871
+ ch : a single channel --> MTM
872
+
873
+ Selection of intervals
874
+ ----------------------
875
+ e : one or [ start , stop ] epochs (1-based)
876
+
877
+ Optional views
878
+ --------------
879
+ traces : T/F show raw signal
880
+ anns : show optional annotations
881
+
882
+ Misc
883
+ ----
884
+ norm : normalization mode
885
+ todo: option to specify hms times
886
+ todo: collapse over time-locked values (e.g. TLOCK)
887
+
888
+ """
889
+
890
+ # for now, accept only a single channel
891
+ assert type(ch) is str
892
+
893
+ # units
894
+ hdr = self.headers()
895
+ units = dict( zip( hdr.CH , hdr.PDIM ) )
896
+
897
+ # define window
898
+ w = None
899
+ if type(e) is list and len(e) == 2 :
900
+ w = self.e2i( e )
901
+ w = [ i for tuple in w for i in tuple ]
902
+ w = [ min(w) , max(w) ]
903
+ elif type(e) is int:
904
+ w = self.e2i( e )
905
+ w = [ i for tuple in w for i in tuple ]
906
+ elif type( t ) is list and len( t ) == 2:
907
+ w = t
908
+
909
+ if w is None: return
910
+
911
+ # window in seconds
912
+ ws = [ x * 1e-9 for x in w ]
913
+ ls = ws[1] - ws[0]
914
+
915
+ # build command
916
+ cstr = 'MTM dB segment-sec=' + str(sec) + ' segment-inc=' + str(inc) + ' tw=' + str(tw)
917
+ cstr += ' segment-spectra segment-output sig=' + ','.join( [ ch ] )
918
+ cstr += ' start=' + str(ws[0]) + ' stop=' + str(ws[1])
919
+ if f is not None: cstr += ' min=' + str(f[0]) + ' max=' + str(f[1])
920
+
921
+ # run MTM
922
+ if silent is True: self.silent_proc( cstr )
923
+ else: self.proc( cstr )
924
+
925
+ if self.empty_result_set(): return
926
+
927
+ # extract
928
+ tf = self.table( 'MTM' , 'CH_F_SEG' )
929
+ tf = tf.astype({'SEG': int })
930
+ tf.drop( 'ID' , axis=1, inplace=True)
931
+
932
+ tt = self.table('MTM','CH_SEG')
933
+ tt = tt.astype({'SEG': int })
934
+ tt['T'] = tt[['START', 'STOP']].mean(axis=1)
935
+ tt.drop( ['ID','DISC','START','STOP'] , axis=1, inplace=True)
936
+
937
+ m = pd.merge( tt ,tf , on= ['CH','SEG'] )
938
+
939
+ x = m['T'].to_numpy(dtype=float)
940
+ y = m['F'].to_numpy(dtype=float)
941
+ z = m['MTM'].to_numpy(dtype=float)
942
+ u = m['T'].unique()
943
+
944
+ # normalize?
945
+ if norm == 't':
946
+ groups = m.groupby(['CH','F'])[['MTM']]
947
+ mean, std = groups.transform("mean"), groups.transform("std")
948
+ mz = (m[mean.columns] - mean) / std
949
+ z = mz['MTM'].to_numpy(dtype=float)
950
+
951
+
952
+ # clip y-axes to observed
953
+ if max(y) < f[1]: f = (f[0] , max(y))
954
+ if min(y) > f[0]: f = (min(y) , f[1])
955
+
956
+ # get time domain signal/annotations
957
+ d = self.slices( [ (w[0] , w[1] )] , chs = ch , annots = anns , time = True )
958
+ dt = d[1][0]
959
+ tx = dt[:,0]
960
+ dvs = d[0][1:]
961
+
962
+ # make spectrogram
963
+ xn = np.unique(x).size
964
+ yn = np.unique(y).size
965
+
966
+ # winsorize power
967
+ z = winsorize( z , limits=[winsor, winsor] )
968
+
969
+ zi, yi, xi = np.histogram2d(y, x, bins=(yn,xn), weights=z, density=False )
970
+ counts, _, _ = np.histogram2d(y, x, bins=(yn,xn))
971
+ with np.errstate(divide='ignore', invalid='ignore'): zi = zi / counts
972
+ zi = np.ma.masked_invalid(zi)
973
+
974
+ # do plot
975
+
976
+ if traces is True:
977
+ fig, axs = plt.subplots( nrows = 2 , ncols = 1 , sharex=True, height_ratios=[1, 2] )
978
+ axs[0].set_title( self.id() )
979
+ fig.set_figheight(5)
980
+ fig.set_figwidth(15)
981
+ axs[0].set_ylabel( ch + ' (' + units[ch] + ')' )
982
+ axs[0].set(xlim=ws)
983
+ axs[1].set(xlim=ws, ylim=f)
984
+ axs[1].set_xlabel('Time (secs)')
985
+ axs[1].set_ylabel('Frequency (Hz)')
986
+ p1 = axs[1].pcolormesh(xi, yi, zi, cmap = pal )
987
+ fig.colorbar(p1, orientation="horizontal", drawedges = False, shrink = 0.2 , pad = 0.3)
988
+ [ axs[0].plot( tx , dt[:,di+1] , label = dvs[di] , linewidth=0.5 ) for di in range(0,len(dvs)) if dvs[di] in [ ch ] ]
989
+
990
+ if traces is False:
991
+ fig, ax = plt.subplots( nrows = 1 , ncols = 1 , sharex=True )
992
+ ax.set_title( self.id() )
993
+ fig.set_figheight(5)
994
+ fig.set_figwidth(15)
995
+ ax.set(xlim=ws, ylim=f)
996
+ ax.set_xlabel('Time (secs)')
997
+ ax.set_ylabel('Frequency (Hz)')
998
+ p1 = ax.pcolormesh(xi, yi, zi, cmap = pal )
999
+ fig.colorbar(p1, orientation="horizontal", drawedges = False, shrink = 0.2 , pad = 0.3)
1000
+
1001
+ return
1002
+
1003
+
1004
+ # --------------------------------------------------------------------------------
1005
+ def pops( self, s = None, s1 = None , s2 = None,
1006
+ path = None , lib = None ,
1007
+ do_edger = True ,
1008
+ no_filter = False ,
1009
+ do_reref = False ,
1010
+ m = None , m1 = None , m2 = None ,
1011
+ lights_off = '.' , lights_on = '.' ,
1012
+ ignore_obs = False,
1013
+ args = '' ):
1014
+ """Run the POPS stager"""
1015
+
1016
+ if path is None: path = resources.POPS_PATH
1017
+ if lib is None: lib = resources.POPS_LIB
1018
+
1019
+ import os
1020
+ if not os.path.isdir( path ):
1021
+ return 'could not open POPS resource path ' + path
1022
+
1023
+ if s is None and s1 is None:
1024
+ print( 'must set s or s1 and s2 to EEGs' )
1025
+ return
1026
+
1027
+ if ( s1 is None ) != ( s2 is None ):
1028
+ print( 'must set s or s1 and s2 to EEGs' )
1029
+ return
1030
+
1031
+ # set options
1032
+ self.var( 'mpath' , path )
1033
+ self.var( 'lib' , lib )
1034
+ self.var( 'do_edger' , '1' if do_edger else '0' )
1035
+ self.var( 'do_reref' , '1' if do_reref else '0' )
1036
+ self.var( 'no_filter' , '1' if no_filter else '0' )
1037
+ self.var( 'LOFF' , lights_off )
1038
+ self.var( 'LON' , lights_on )
1039
+
1040
+ if s is not None: self.var( 's' , s )
1041
+ if m is not None: self.var( 'm' , m )
1042
+ if s1 is not None: self.var( 's1' , s1 )
1043
+ if s2 is not None: self.var( 's2' , s2 )
1044
+ if m1 is not None: self.var( 'm1' , m1 )
1045
+ if m2 is not None: self.var( 'm2' , m2 )
1046
+
1047
+ # get either one- or two-channel mode Luna script from POPS folder
1048
+ twoch = s1 is not None and s2 is not None;
1049
+ if twoch: cmdstr = cmdfile( path + '/s2.ch2.txt' )
1050
+ else: cmdstr = cmdfile( path + '/s2.ch1.txt' )
1051
+
1052
+ # swap in any additional options to POPS
1053
+ if ignore_obs is True:
1054
+ args = args + ' ignore-obs-staging';
1055
+ if do_edger is True:
1056
+ cmdstr = cmdstr.replace( 'EDGER' , 'EDGER all' )
1057
+ if args != '':
1058
+ cmdstr = cmdstr.replace( 'POPS' , 'POPS ' + args + ' ')
1059
+
1060
+
1061
+ # run the command
1062
+ self.proc( cmdstr )
1063
+
1064
+ # return of results
1065
+ return self.table( 'POPS' , 'E' )
1066
+
1067
+
1068
+ # --------------------------------------------------------------------------------
1069
+ def predict_SUN2019( self, cen , age = None , th = '3' , path = None ):
1070
+ """Run SUN2019 prediction model for a single individual"""
1071
+ if path is None: path = resources.MODEL_PATH
1072
+ if type( cen ) is list : cen = ','.join( cen )
1073
+
1074
+ # set i-vars
1075
+ if age is None:
1076
+ print( 'need to set age indiv-var' )
1077
+ return
1078
+ self.var( 'age' , str(age) )
1079
+ self.var( 'cen' , cen )
1080
+ self.var( 'mpath' , path )
1081
+ self.var( 'th' , str(th) )
1082
+ self.eval( cmdfile( resources.MODEL_PATH + '/m1-adult-age-luna.txt' ) )
1083
+ return self.table( 'PREDICT' )
1084
+
1085
+ # --------------------------------------------------------------------------------
1086
+ def stages(self):
1087
+ """Return of a list of stages"""
1088
+ hyp = self.silent_proc( "STAGE" )
1089
+ if type(hyp) is type(None): return
1090
+ if 'STAGE: E' in hyp:
1091
+ return hyp[ 'STAGE: E' ]
1092
+ return
1093
+
1094
+ # --------------------------------------------------------------------------------
1095
+ def hypno(self):
1096
+ """Hypnogram of sleep stages"""
1097
+ if self.has_staging() is not True:
1098
+ print( "no staging attached" )
1099
+ return
1100
+ return hypno( self.stages()[ 'STAGE' ] )
1101
+
1102
+ # --------------------------------------------------------------------------------
1103
+ def has_staging(self):
1104
+ """Returns bool for whether staging is present"""
1105
+ _proj = proj(False)
1106
+ silence_mode = _proj.is_silenced()
1107
+ _proj.silence(True,False)
1108
+ res = self.edf.has_staging()
1109
+ _proj.silence( silence_mode , False )
1110
+ return res
1111
+
1112
+ # --------------------------------------------------------------------------------
1113
+ def has_annots(self,anns):
1114
+ """Returns bool for which annotations are present"""
1115
+ if anns is None: return
1116
+ if type( anns ) is not list: anns = [ anns ]
1117
+ return self.edf.has_annots( anns )
1118
+
1119
+ # --------------------------------------------------------------------------------
1120
+ def has_annot(self,anns):
1121
+ """Returns bool for which annotations are present"""
1122
+ return self.has_annots(anns)
1123
+
1124
+ # --------------------------------------------------------------------------------
1125
+ def has_channels(self,ch):
1126
+ """Return a bool to indicate whether a given channel exists"""
1127
+ if ch is None: return
1128
+ if type(ch) is not list: ch = [ ch ]
1129
+ return self.edf.has_channels( ch )
1130
+
1131
+ # --------------------------------------------------------------------------------
1132
+ def has(self,ch):
1133
+ """Return a bool to indicate whether a given channel exists"""
1134
+ if ch is None: return
1135
+ if type(ch) is not list: ch = [ ch ]
1136
+ return self.edf.has_channels( ch )
1137
+
1138
+ # --------------------------------------------------------------------------------
1139
+ # def psd(self, ch, minf = None, maxf = 25, minp = None, maxp = None , xlines = None , ylines = None ):
1140
+ # """Spectrogram plot for a given channel 'ch'"""
1141
+ # if type( ch ) is not str: return
1142
+ # if all( self.has( ch ) ) is not True: return
1143
+ # res = self.silent_proc( 'PSD spectrum dB max=' + str(maxf) + ' sig=' + ','.join(ch) )[ 'PSD: CH_F' ]
1144
+ # return psd( res , ch, minf = minf, maxf = maxf, minp = minp, maxp = maxp , xlines = xlines , ylines = ylines )
1145
+
1146
+ # --------------------------------------------------------------------------------
1147
+ def psd( self, ch, var = 'PSD' , minf = None, maxf = 25, minp = None, maxp = None , xlines = None , ylines = None ):
1148
+ """Generates a PSD plot (from PSD or MTM) for one or more channel(s)"""
1149
+ if ch is None: return
1150
+ if type(ch) is not list: ch = [ ch ]
1151
+
1152
+ if var == 'PSD':
1153
+ res = self.silent_proc( 'PSD spectrum dB max=' + str(maxf) + ' sig=' + ','.join(ch) )
1154
+ df = res[ 'PSD: CH_F' ]
1155
+ else:
1156
+ res = self.silent_proc( 'MTM tw=15 dB max=' + str(maxf) + ' sig=' + ','.join(ch) )
1157
+ df = res[ 'MTM: CH_F' ]
1158
+
1159
+ psd( df = df , ch = ch , var = var ,
1160
+ minf = minf , maxf = maxf , minp = minp , maxp = maxp ,
1161
+ xlines = xlines , ylines = ylines )
1162
+
1163
+
1164
+ # --------------------------------------------------------------------------------
1165
+ # def spec( self, ch, var = 'PSD' , mine = None, maxe = None, minf = None, maxf = 25 , w = 0.025 ):
1166
+ # """Generates an epoch-level PSD spectrogram (from PSD or MTM)"""
1167
+ # if ch is None: return
1168
+ # if type(ch) is not list: ch = [ ch ]
1169
+ #
1170
+ # if var == 'PSD':
1171
+ # self.eval( 'PSD epoch-spectrum dB max=' + str(maxf) + ' sig=' + ','.join(ch) )
1172
+ # df = self.table( 'PSD' , 'CH_E_F' )
1173
+ # else:
1174
+ # self.eval( 'MTM epoch-spectra epoch epoch-output dB tw=15 max=' + str(maxf) + ' sig=' + ','.join(ch) )
1175
+ # df = self.table( 'MTM' , 'CH_E_F' )
1176
+ #
1177
+ # spec( df = df , ch = None , var = var ,
1178
+ # mine = mine , maxe = maxe , minf = minf , maxf = maxf , w = w )
1179
+
1180
+ # --------------------------------------------------------------------------------
1181
+ def spec(self,ch,mine = None , maxe = None , minf = None, maxf = None, w = 0.025 ):
1182
+ """PSD given channel 'ch'"""
1183
+ if type( ch ) is not str:
1184
+ return
1185
+ if all( self.has( ch ) ) is not True:
1186
+ return
1187
+ if minf is None:
1188
+ minf=0.5
1189
+ if maxf is None:
1190
+ maxf=25
1191
+ res = self.silent_proc( "PSD epoch-spectrum dB sig="+ch+" min="+str(minf)+" max="+str(maxf) )[ 'PSD: CH_E_F' ]
1192
+ return spec( res , ch=ch, var='PSD', mine=mine,maxe=maxe,minf=minf,maxf=maxf,w=w)
1193
+
1194
+
1195
+
1196
+ # --------------------------------------------------------------------------------
1197
+ #
1198
+ # misc non-member utilities functions
1199
+ #
1200
+ # --------------------------------------------------------------------------------
1201
+
1202
+
1203
+ def fetch_doms():
1204
+ """Fetch all command domains"""
1205
+ return _luna.fetch_doms( True )
1206
+
1207
+ def fetch_cmds( dom ):
1208
+ """Fetch all commands"""
1209
+ return _luna.fetch_cmds( dom, True )
1210
+
1211
+ def fetch_params( cmd ):
1212
+ """Fetch all command parameters"""
1213
+ return _luna.fetch_params( cmd, True )
1214
+
1215
+ def fetch_tbls( cmd ):
1216
+ """Fetch all command tables"""
1217
+ return _luna.fetch_tbls( cmd, True )
1218
+
1219
+ def fetch_vars( cmd, tbl ):
1220
+ """Fetch all command/table variables"""
1221
+ return _luna.fetch_vars( cmd, tbl, True )
1222
+
1223
+ def fetch_desc_dom( dom ):
1224
+ """Description for a domain"""
1225
+ return _luna.fetch_desc_dom( dom )
1226
+
1227
+ def fetch_desc_cmd( cmd ):
1228
+ """Description for a command"""
1229
+ return _luna.fetch_desc_cmd( cmd )
1230
+
1231
+ def fetch_desc_param( cmd , param ):
1232
+ """Description for a command/parameter"""
1233
+ return _luna.fetch_desc_param( cmd, param )
1234
+
1235
+ def fetch_desc_tbl( cmd , tbl ):
1236
+ """Description for a command/table"""
1237
+ return _luna.fetch_desc_tbl( cmd, tbl )
1238
+
1239
+ def fetch_desc_var( cmd, tbl, var ):
1240
+ """Fetch all command/table variable"""
1241
+ return _luna.fetch_desc_var( cmd, tbl, var )
1242
+
1243
+
1244
+ # --------------------------------------------------------------------------------
1245
+ def cmdfile( f ):
1246
+ """load and parse a Luna command script from a file"""
1247
+
1248
+ return _luna.cmdfile( f )
1249
+
1250
+
1251
+ # --------------------------------------------------------------------------------
1252
+ def strata( ts ):
1253
+ """Utility function to format tables"""
1254
+ r = [ ]
1255
+ for cmd in ts:
1256
+ strata = ts[cmd].keys()
1257
+ for stratum in strata:
1258
+ r.append( ( cmd , strata ) )
1259
+ return r
1260
+
1261
+ t = pd.DataFrame( self.edf.strata() )
1262
+ t.columns = ["Command","Strata"]
1263
+ return t
1264
+
1265
+ # --------------------------------------------------------------------------------
1266
+ def table( ts, cmd , strata = 'BL' ):
1267
+ """Utility function to format tables"""
1268
+ r = ts[cmd][strata]
1269
+ t = pd.DataFrame( r[1] ).T
1270
+ t.columns = r[0]
1271
+ return t
1272
+
1273
+ # --------------------------------------------------------------------------------
1274
+ def tables( ts ):
1275
+ """Utility function to format tables"""
1276
+ r = { }
1277
+ for cmd in ts.keys():
1278
+ strata = ts[cmd].keys()
1279
+ for stratum in strata:
1280
+ r[ cmd + ": " + stratum ] = _table2df( ts[cmd][stratum] )
1281
+ return r
1282
+
1283
+ # --------------------------------------------------------------------------------
1284
+ def _table2df( r ):
1285
+ """Utility function to format tables"""
1286
+ t = pd.DataFrame( r[1] ).T
1287
+ t.columns = r[0]
1288
+ return t
1289
+
1290
+ # --------------------------------------------------------------------------------
1291
+ def show( dfs ):
1292
+ """Utility function to format tables"""
1293
+ for title , df in dfs.items():
1294
+ print( _color.BOLD + _color.DARKCYAN + title + _color.END )
1295
+ ICD.display(df)
1296
+
1297
+
1298
+ # --------------------------------------------------------------------------------
1299
+ def subset( df , ids = None , qry = None , vars = None ):
1300
+ """Utility function to subset table rows/columns"""
1301
+
1302
+ # subset rows (ID)
1303
+ if ids is not None:
1304
+ if type(ids) is not list: ids = [ ids ]
1305
+ df = df[ df[ 'ID' ].isin( ids ) ]
1306
+
1307
+ # subset rows (factors/levels)
1308
+ if type(qry) is str:
1309
+ df = df.query( qry )
1310
+
1311
+ # subset cols (vars)
1312
+ if vars is not None:
1313
+ if type(vars) is not list: vars = [ vars ]
1314
+ vars.insert( 0, 'ID' )
1315
+ df = df[ vars ]
1316
+
1317
+ return df
1318
+
1319
+ # --------------------------------------------------------------------------------
1320
+ def concat( dfs , tlab , vars = None , add_index = None , ignore_index = True ):
1321
+ """Utility function to extract and concatenate tables"""
1322
+
1323
+ # assume dict[k]['cmd: faclvl']->table
1324
+ # and we want to concatenate over 'k'
1325
+ # assume 'k' will be tracked in the tables (e.g. via TAG)
1326
+
1327
+ if add_index is not None:
1328
+ for k in dfs.keys():
1329
+ dfs[k][tlab][ [add_index] ] = k
1330
+
1331
+ if vars is not None:
1332
+ if type(vars) is not list: vars = [ vars ]
1333
+ dfs = pd.concat( [ dfs[ k ][ tlab ][ vars ] for k in dfs.keys() ] , ignore_index = ignore_index )
1334
+ if vars is None:
1335
+ dfs = pd.concat( [ dfs[ k ][ tlab ] for k in dfs.keys() ] , ignore_index = ignore_index )
1336
+
1337
+ return dfs
1338
+
1339
+
1340
+ # --------------------------------------------------------------------------------
1341
+ #
1342
+ # Helpers
1343
+ #
1344
+ # --------------------------------------------------------------------------------
1345
+
1346
+ def version():
1347
+ """Return version of lunapi & luna"""
1348
+ return { "lunapi": lp_version , "luna": _luna.version() }
1349
+
1350
+ class _color:
1351
+ PURPLE = '\033[95m'
1352
+ CYAN = '\033[96m'
1353
+ DARKCYAN = '\033[36m'
1354
+ BLUE = '\033[94m'
1355
+ GREEN = '\033[92m'
1356
+ YELLOW = '\033[93m'
1357
+ RED = '\033[91m'
1358
+ BOLD = '\033[1m'
1359
+ UNDERLINE = '\033[4m'
1360
+ END = '\033[0m'
1361
+
1362
+
1363
+
1364
+
1365
+ def default_xy():
1366
+ """Default channel locations (64-ch EEG only, currently)"""
1367
+ vals = [["FP1", "AF7", "AF3", "F1", "F3", "F5", "F7", "FT7",
1368
+ "FC5", "FC3", "FC1", "C1", "C3", "C5", "T7", "TP7", "CP5",
1369
+ "CP3", "CP1", "P1", "P3", "P5", "P7", "P9", "PO7", "PO3",
1370
+ "O1", "IZ", "OZ", "POZ", "PZ", "CPZ", "FPZ", "FP2", "AF8",
1371
+ "AF4", "AFZ", "FZ", "F2", "F4", "F6", "F8", "FT8", "FC6",
1372
+ "FC4", "FC2", "FCZ", "CZ", "C2", "C4", "C6", "T8", "TP8",
1373
+ "CP6", "CP4", "CP2", "P2", "P4", "P6", "P8", "P10", "PO8",
1374
+ "PO4", "O2"],
1375
+ [-0.139058, -0.264503, -0.152969, -0.091616, -0.184692,
1376
+ -0.276864, -0.364058, -0.427975, -0.328783, -0.215938,
1377
+ -0.110678, -0.1125, -0.225, -0.3375, -0.45, -0.427975,
1378
+ -0.328783, -0.215938, -0.110678, -0.091616, -0.184692,
1379
+ -0.276864, -0.364058, -0.4309, -0.264503, -0.152969,
1380
+ -0.139058, 0, 0, 0, 0, 0, 0, 0.139058, 0.264503, 0.152969,
1381
+ 0, 0, 0.091616, 0.184692, 0.276864, 0.364058, 0.427975,
1382
+ 0.328783, 0.215938, 0.110678, 0, 0, 0.1125, 0.225, 0.3375,
1383
+ 0.45, 0.427975, 0.328783, 0.215938, 0.110678, 0.091616,
1384
+ 0.184692, 0.276864, 0.364058, 0.4309, 0.264503, 0.152969,
1385
+ 0.139058],
1386
+ [0.430423, 0.373607, 0.341595, 0.251562, 0.252734,
1387
+ 0.263932, 0.285114, 0.173607, 0.162185, 0.152059, 0.14838,
1388
+ 0.05, 0.05, 0.05, 0.05, -0.073607, -0.062185, -0.052059,
1389
+ -0.04838, -0.151562, -0.152734, -0.163932, -0.185114,
1390
+ -0.271394, -0.273607, -0.241595, -0.330422, -0.45, -0.35,
1391
+ -0.25, -0.15, -0.05, 0.45, 0.430423, 0.373607, 0.341595,
1392
+ 0.35, 0.25, 0.251562, 0.252734, 0.263932, 0.285114, 0.173607,
1393
+ 0.162185, 0.152059, 0.14838, 0.15, 0.05, 0.05, 0.05,
1394
+ 0.05, 0.05, -0.073607, -0.062185, -0.052059, -0.04838,
1395
+ -0.151562, -0.152734, -0.163932, -0.185114, -0.271394,
1396
+ -0.273607, -0.241595, -0.330422]]
1397
+
1398
+ topo = pd.DataFrame(np.array(vals).T, columns=['CH', 'X', 'Y'])
1399
+ topo[['X', 'Y']] = topo[['X', 'Y']].apply(pd.to_numeric)
1400
+ return topo
1401
+
1402
+
1403
+
1404
+
1405
+ # --------------------------------------------------------------------------------
1406
+ def stgcol(ss):
1407
+ """Utility function: translate a sleep stage string to a colour for plotting"""
1408
+ stgcols = { 'N1' : "#00BEFAFF" ,
1409
+ 'N2' : "#0050C8FF" ,
1410
+ 'N3' : "#000050FF" ,
1411
+ 'NREM4' : "#000032FF",
1412
+ 'R' : "#FA1432FF",
1413
+ 'W' : "#31AD52FF",
1414
+ 'L' : "#F6F32AFF",
1415
+ '?' : "#64646464",
1416
+ None : "#00000000" }
1417
+ return [ stgcols.get(item,item) for item in ss ]
1418
+
1419
+
1420
+
1421
+ # --------------------------------------------------------------------------------
1422
+ def stgn(ss):
1423
+ """Utility function: translate a sleep stage string to a number for plotting"""
1424
+
1425
+ stgns = { 'N1' : -1,
1426
+ 'N2' : -2,
1427
+ 'N3' : -3,
1428
+ 'NREM4' : -4,
1429
+ 'R' : 0,
1430
+ 'W' : 1,
1431
+ 'L' : 2,
1432
+ '?' : 2,
1433
+ None : 2 }
1434
+ return [ stgns.get(item,item) for item in ss ]
1435
+
1436
+
1437
+
1438
+
1439
+ # --------------------------------------------------------------------------------
1440
+ #
1441
+ # Visualizations
1442
+ #
1443
+ # --------------------------------------------------------------------------------
1444
+
1445
+
1446
+ # --------------------------------------------------------------------------------
1447
+ def hypno( ss , e = None , xsize = 20 , ysize = 2 , title = None ):
1448
+ """Plot a hypnogram"""
1449
+ ssn = stgn( ss )
1450
+ if e is None: e = np.arange(0, len(ssn), 1)
1451
+ e = e/120
1452
+ plt.figure(figsize=(xsize , ysize ))
1453
+ plt.plot( e , ssn , c = 'gray' , lw = 0.5 )
1454
+ plt.scatter( e , ssn , c = stgcol( ss ) , zorder=2.5 , s = 10 )
1455
+ plt.ylabel('Sleep stage')
1456
+ plt.xlabel('Time (hrs)')
1457
+ plt.ylim(-3.5, 2.5)
1458
+ plt.xlim(0,max(e))
1459
+ plt.yticks([-3,-2,-1,0,1,2] , ['N3','N2','N1','R','W','?'] )
1460
+ if ( title != None ): plt.title( title )
1461
+ plt.show()
1462
+
1463
+ # --------------------------------------------------------------------------------
1464
+ def hypno_density( probs , e = None , xsize = 20 , ysize = 2 , title = None ):
1465
+ """Generate a hypno-density plot from a prior POPS/SOAP run"""
1466
+
1467
+ # no data?
1468
+ if len(probs) == 0: return
1469
+
1470
+ res = probs[ ["PP_N1","PP_N2","PP_N3","PP_R","PP_W" ] ]
1471
+ ne = len(res)
1472
+ x = np.arange(1, ne+1, 1)
1473
+ y = res.to_numpy(dtype=float)
1474
+ fig, ax = plt.subplots()
1475
+ xsize = 20
1476
+ ysize=2.5
1477
+ fig.set_figheight(ysize)
1478
+ fig.set_figwidth(xsize)
1479
+ ax.set_xlabel('Epoch')
1480
+ ax.set_ylabel('Prob(stage)')
1481
+ ax.stackplot(x, y.T , colors = stgcol([ 'N1','N2','N3','R','W']) )
1482
+ ax.set(xlim=(1, ne), xticks=[ 1 , ne ] ,
1483
+ ylim=(0, 1), yticks=np.arange(0, 1))
1484
+ plt.show()
1485
+
1486
+
1487
+
1488
+ # --------------------------------------------------------------------------------
1489
+ def psd(df , ch, var = 'PSD' , minf = None, maxf = None, minp = None, maxp = None ,
1490
+ xlines = None , ylines = None, dB = False ):
1491
+ """Returns a PSD plot from PSD or MTM epoch table (CH_F)"""
1492
+ if ch is None: return
1493
+ if type( ch ) is not list: ch = [ ch ]
1494
+ if type( xlines ) is not list and xlines != None: xlines = [ xlines ]
1495
+ if type( ylines ) is not list and ylines != None: ylines = [ ylines ]
1496
+ df = df[ df['CH'].isin(ch) ]
1497
+ if len(df) == 0: return
1498
+ f = df['F'].to_numpy(dtype=float)
1499
+ p = df[var].to_numpy(dtype=float)
1500
+ if dB is True: p = 10*np.log10(p)
1501
+ cx = df['CH'].to_numpy(dtype=str)
1502
+ if minp is None: minp = min(p)
1503
+ if maxp is None: maxp = max(p)
1504
+ if minf is None: minf = min(f)
1505
+ if maxf is None: maxf = max(f)
1506
+ incl = np.zeros(len(df), dtype=bool)
1507
+ incl[ (f >= minf) & (f <= maxf) ] = True
1508
+ f = f[ incl ]
1509
+ p = p[ incl ]
1510
+ cx = cx[ incl ]
1511
+ p[ p > maxp ] = maxp
1512
+ p[ p < minp ] = minp
1513
+ [ plt.plot(f[ cx == _ch ], p[ cx == _ch ] , label = _ch ) for _ch in ch ]
1514
+ plt.legend()
1515
+ plt.xlabel('Frequency (Hz)')
1516
+ plt.ylabel('Power (dB)')
1517
+ if xlines is not None: [plt.axvline(_x, linewidth=1, color='gray') for _x in xlines ]
1518
+ if ylines is not None: [plt.axhline(_y, linewidth=1, color='gray') for _y in ylines ]
1519
+ plt.show()
1520
+
1521
+
1522
+ # --------------------------------------------------------------------------------
1523
+ def spec(df , ch = None , var = 'PSD' , mine = None , maxe = None , minf = None, maxf = None, w = 0.025 ):
1524
+ """Returns a spectrogram from a PSD or MTM epoch table (CH_E_F)"""
1525
+ if ch is not None: df = df.loc[ df['CH'] == ch ]
1526
+ if len(df) == 0: return
1527
+ x = df['E'].to_numpy(dtype=int)
1528
+ y = df['F'].to_numpy(dtype=float)
1529
+ z = df[ var ].to_numpy(dtype=float)
1530
+ if mine is None: mine = min(x)
1531
+ if maxe is None: maxe = max(x)
1532
+ if minf is None: minf = min(y)
1533
+ if maxf is None: maxf = max(y)
1534
+ incl = np.zeros(len(df), dtype=bool)
1535
+ incl[ (x >= mine) & (x <= maxe) & (y >= minf) & (y <= maxf) ] = True
1536
+ x = x[ incl ]
1537
+ y = y[ incl ]
1538
+ z = z[ incl ]
1539
+ z = winsorize( z , limits=[w, w] )
1540
+
1541
+ #include/exclude here...
1542
+ spec0( x,y,z,mine,maxe,minf,maxf)
1543
+
1544
+
1545
+ # --------------------------------------------------------------------------------
1546
+ def spec0( x , y , z , mine , maxe , minf, maxf ):
1547
+ xn = max(x) - min(x) + 1
1548
+ yn = np.unique(y).size
1549
+ zi, yi, xi = np.histogram2d(y, x, bins=(yn,xn), weights=z, density=False )
1550
+ counts, _, _ = np.histogram2d(y, x, bins=(yn,xn))
1551
+ with np.errstate(divide='ignore', invalid='ignore'):
1552
+ zi = zi / counts
1553
+ zi = np.ma.masked_invalid(zi)
1554
+ fig, ax = plt.subplots()
1555
+ fig.set_figheight(2)
1556
+ fig.set_figwidth(15)
1557
+ ax.set_xlabel('Epoch')
1558
+ ax.set_ylabel('Frequency (Hz)')
1559
+ ax.set(xlim=(mine, maxe), ylim=(minf,maxf) )
1560
+ p1 = ax.pcolormesh(xi, yi, zi, cmap = 'turbo' )
1561
+ fig.colorbar(p1)
1562
+ ax.margins(0.1)
1563
+ plt.show()
1564
+
1565
+ # --------------------------------------------------------------------------------
1566
+ def topo_heat(chs, z, ths = None , th=0.05 ,
1567
+ topo = None ,
1568
+ lmts= None , sz=70, colormap = "bwr", title = "",
1569
+ rimcolor="black", lab = "dB"):
1570
+ """Generate a channel-wise topoplot"""
1571
+
1572
+ z = np.array(z)
1573
+ if ths is not None: ths = np.array(ths)
1574
+ if topo is None: topo = default_xy()
1575
+
1576
+ xlim = [-0.6, 0.6]
1577
+ ylim = [-0.6, 0.6]
1578
+ rng = [np.min(z), np.max(z)]
1579
+
1580
+ if lmts is None : lmts = rng
1581
+ else: assert lmts[0] <= rng[0] <= lmts[1] and lmts[0] <= rng[1] <= lmts[1], "channel values are out of specified limits"
1582
+
1583
+ assert len(set(topo['CH']).intersection(chs)) > 0, "no matching channels"
1584
+
1585
+ chs = chs.apply(lambda x: x.upper())
1586
+ topo = topo[topo['CH'].isin(chs)]
1587
+ topo["vals"] = np.nan
1588
+ topo["th_vals"] = np.nan
1589
+ topo["rims"] = 0.5
1590
+
1591
+ for ix, ch in topo.iterrows():
1592
+ topo.loc[ix,'vals'] = z[chs == ch["CH"]]
1593
+ if ths is None:
1594
+ topo.loc[ix,'th_vals'] = 999;
1595
+ else:
1596
+ topo.loc[ix,'th_vals'] = ths[chs == ch["CH"]]
1597
+
1598
+ if topo.loc[ix,'th_vals'] < th:
1599
+ topo.loc[ix,'rims'] = 1.5
1600
+
1601
+ fig, ax = plt.subplots()
1602
+ sc = ax.scatter(topo.loc[:,"X"], topo.loc[:,"Y"],cmap=colormap,
1603
+ c=topo.loc[:, "vals"], edgecolors=rimcolor,
1604
+ linewidths=topo['rims'], s=sz, vmin=lmts[0], vmax=lmts[1])
1605
+ plt.text(-0.4, 0.5, s=title, fontsize=10, ha='center', va='center')
1606
+ plt.text(0.15, -0.48, s=np.round(lmts[0], 2), fontsize=8, ha='center', va='center')
1607
+ plt.text(0.53, -0.48, s=np.round(lmts[1], 2), fontsize=8, ha='center', va='center')
1608
+ plt.text(0.35, -0.47, s=lab, fontsize=10, ha='center', va='center')
1609
+
1610
+ plt.xlim(xlim)
1611
+ plt.ylim(ylim)
1612
+ plt.axis('off')
1613
+
1614
+ cax = fig.add_axes([0.6, 0.15, 0.25, 0.02]) # [x, y, width, height]
1615
+ plt.colorbar(sc, cax=cax, orientation='horizontal')
1616
+ plt.axis('off')
1617
+
1618
+ # arguments
1619
+ #topo = default_xy()
1620
+ #ch_names = topo.loc[:, "CH"] # vector of channel names
1621
+ #ch_vals = np.random.uniform(0, 3, size=len(ch_names))
1622
+ #ch_vals[0:3] = -18
1623
+ #th_vals = np.random.uniform(0.06, 1, size=len(ch_names)) # vector of channel values
1624
+ #th_vals[ch_names == "O2"] = 0
1625
+ #lmts=[-4, 4]#"default"
1626
+ #ltopo_heat(ch_names, ch_vals, th_vals = th_vals, th=0.05,
1627
+ # lmts=lmts, sz=70,
1628
+ # colormap = "bwr", title = "DENSITY",
1629
+ # rimcolor="black", lab = "n/min")
1630
+
1631
+
1632
+ # --------------------------------------------------------------------------------
1633
+ # segsrv
1634
+
1635
+ class segsrv:
1636
+ """Segment server instance"""
1637
+
1638
+ def __init__(self,p):
1639
+ assert isinstance(p,inst)
1640
+ self.p = p
1641
+ self.segsrv = _luna.segsrv(p.edf)
1642
+
1643
+ def populate(self,chs=None,anns=None,max_sr=None):
1644
+ if chs is None: chs = self.p.edf.channels()
1645
+ if anns is None: anns = self.p.edf.annots()
1646
+ if type(chs) is not list: chs = [ chs ]
1647
+ if type(anns) is not list: anns = [ anns ]
1648
+ if type(max_sr) is int: self.segsrv.input_throttle( max_sr )
1649
+ self.segsrv.populate( chs , anns )
1650
+
1651
+ def window(self,a,b):
1652
+ assert isinstance(a, (int, float) )
1653
+ assert isinstance(b, (int, float) )
1654
+ self.segsrv.set_window( a, b )
1655
+
1656
+ def get_signal(self,ch):
1657
+ assert isinstance(ch, str )
1658
+ return self.segsrv.get_signal( ch )
1659
+
1660
+ def get_timetrack(self,ch):
1661
+ assert isinstance(ch, str )
1662
+ return self.segsrv.get_timetrack( ch )
1663
+
1664
+ def get_time_scale(self):
1665
+ return self.segsrv.get_time_scale()
1666
+
1667
+ def get_gaps(self):
1668
+ return self.segsrv.get_gaps()
1669
+
1670
+ def set_scaling(self, nchs, nanns = 0 , yscale = 1 , ygroup = 1 , yheader = 0.05 , yfooter = 0.05 , scaling_fixed_annot = 0.1 , clip = True):
1671
+ self.segsrv.set_scaling( nchs, nanns, yscale, ygroup, yheader, yfooter, scaling_fixed_annot , clip )
1672
+
1673
+ def get_scaled_signal(self, ch, n1):
1674
+ return self.segsrv.get_scaled_signal( ch , n1 )
1675
+
1676
+ def fix_physical_scale(self,ch,lwr,upr):
1677
+ self.segsrv.fix_physical_scale( ch, lwr, upr )
1678
+
1679
+ def empirical_physical_scale(self,ch):
1680
+ self.segsrv.empirical_physical_scale( ch )
1681
+
1682
+ def free_physical_scale( self, ch ):
1683
+ self.segsrv.free_physical_scale( ch )
1684
+
1685
+ def set_epoch_size( self, s ):
1686
+ self.segsrv.set_epoch_size( s )
1687
+
1688
+ def get_epoch_size( self):
1689
+ return self.segsrv.get_epoch_size()
1690
+
1691
+ # def get_epoch_timetrack(self):
1692
+ # return self.segsrv.get_epoch_timetrack()
1693
+
1694
+ def num_epochs( self) :
1695
+ return self.segsrv.nepochs()
1696
+
1697
+ # def num_seconds( self ):
1698
+ # return self.segsrv.get_ungapped_total_sec()
1699
+
1700
+ def num_seconds_clocktime( self ):
1701
+ return self.segsrv.get_total_sec()
1702
+
1703
+ def num_seconds_clocktime_original( self ):
1704
+ return self.segsrv.get_total_sec_original()
1705
+
1706
+ def calc_bands( self, chs ):
1707
+ if type( chs ) is not list: chs = [ chs ]
1708
+ self.segsrv.calc_bands( chs );
1709
+
1710
+ def calc_hjorths( self, chs ):
1711
+ if type( chs ) is not list: chs = [ chs ]
1712
+ self.segsrv.calc_hjorths( chs );
1713
+
1714
+ def get_bands( self, ch ):
1715
+ return self.segsrv.get_bands( ch )
1716
+
1717
+ def get_hjorths( self, ch ):
1718
+ return self.segsrv.get_hjorths( ch )
1719
+
1720
+ def valid_window( self ):
1721
+ return self.segsrv.is_window_valid()
1722
+
1723
+ def is_clocktime( self ):
1724
+ return self.segsrv.is_clocktime()
1725
+
1726
+ def get_window_left( self ):
1727
+ return self.segsrv.get_window_left()
1728
+
1729
+ def get_window_right( self ):
1730
+ return self.segsrv.get_window_right()
1731
+
1732
+ def get_window_left_hms( self ):
1733
+ return self.segsrv.get_window_left_hms()
1734
+
1735
+ def get_window_right_hms( self ):
1736
+ return self.segsrv.get_window_right_hms()
1737
+
1738
+ def get_clock_ticks( self , n = 6 ):
1739
+ assert type( n ) is int
1740
+ return self.segsrv.get_clock_ticks( n )
1741
+
1742
+ def get_window_phys_range( self , ch ):
1743
+ assert type(ch) is str
1744
+ return self.segsrv.get_window_phys_range( ch )
1745
+
1746
+ def get_ylabel( self , n ):
1747
+ assert type(n) is int
1748
+ return self.segsrv.get_ylabel( n )
1749
+
1750
+ def throttle(self,n):
1751
+ assert type(n) is int
1752
+ self.segsrv.throttle(n)
1753
+
1754
+ def input_throttle(self,n):
1755
+ assert type(n) is int
1756
+ self.segsrv.input_throttle(n)
1757
+
1758
+ def summary_threshold_mins(self,m):
1759
+ assert type(m) is int or type(m) is float
1760
+ self.segsrv.summary_threshold_mins(m)
1761
+
1762
+ def get_annots(self):
1763
+ return self.segsrv.fetch_annots()
1764
+
1765
+ def get_all_annots(self,anns):
1766
+ return self.segsrv.fetch_all_annots(anns)
1767
+
1768
+ def compile_windowed_annots(self,anns):
1769
+ self.segsrv.compile_evts( anns )
1770
+
1771
+ def get_annots_xaxes(self,ann):
1772
+ return self.segsrv.get_evnts_xaxes( ann )
1773
+
1774
+ def get_annots_yaxes(self,ann):
1775
+ return self.segsrv.get_evnts_yaxes( ann )
1776
+
1777
+ def set_annot_format6(self,b):
1778
+ self.segsrv.set_evnt_format6(b)
1779
+
1780
+ def get_annots_xaxes_ends(self,ann):
1781
+ return self.segsrv.get_evnts_xaxes_ends( ann )
1782
+
1783
+ def get_annots_yaxes_ends(self,ann):
1784
+ return self.segsrv.get_evnts_yaxes_ends( ann )
1785
+
1786
+
1787
+
1788
+ # --------------------------------------------------------------------------------
1789
+ #
1790
+ # Scope viewer
1791
+ #
1792
+ # --------------------------------------------------------------------------------
1793
+
1794
+ def scope( p,
1795
+ chs = None,
1796
+ bsigs = None ,
1797
+ hsigs = None,
1798
+ anns = None ,
1799
+ stgs = [ 'N1' , 'N2' , 'N3' , 'R' , 'W' , '?' , 'L' ] ,
1800
+ stgcols = { 'N1':'blue' , 'N2':'blue', 'N3':'navy','R':'red','W':'green','?':'gray','L':'yellow' } ,
1801
+ stgns = { 'N1':-1 , 'N2':-2, 'N3':-3,'R':0,'W':1,'?':2,'L':2 } ,
1802
+ sigcols = None,
1803
+ anncols = None,
1804
+ throttle1_sr = 100 ,
1805
+ throttle2_np = 5 * 30 * 100 ,
1806
+ summary_mins = 30 ,
1807
+ height = 600 ,
1808
+ annot_height = 0.15 ,
1809
+ header_height = 0.04 ,
1810
+ footer_height = 0.01
1811
+ ):
1812
+
1813
+ # defaults
1814
+ scope_epoch_sec = 30
1815
+
1816
+ # internally, we use 'sigs' but 'chs' is a more lunapi-consistent label
1817
+ sigs = chs
1818
+
1819
+ # all signals/annotations present
1820
+ all_sigs = p.edf.channels()
1821
+ all_annots = p.edf.annots()
1822
+
1823
+ # units
1824
+ hdr = p.headers()
1825
+ units = dict( zip( hdr.CH , hdr.PDIM ) )
1826
+
1827
+ # defaults
1828
+ if sigs is None: sigs = all_sigs
1829
+ if bsigs is None: bsigs = p.var( 'eeg' ).split(",")
1830
+ if hsigs is None: hsigs = p.var( 'eeg' ).split(",")
1831
+ if anns is None: anns = all_annots
1832
+
1833
+ # ensure we do not have weird channels
1834
+ sigs = [x for x in all_sigs if x in sigs]
1835
+ bsigs = [x for x in sigs if x in bsigs ]
1836
+ hsigs = [x for x in sigs if x in hsigs ]
1837
+ anns = [x for x in all_annots if x in anns ]
1838
+ sig2n = dict( zip( sigs , list(range(0,len(sigs)))) )
1839
+
1840
+ # empty?
1841
+ if len( sigs ) == 0 and len( anns ) == 0:
1842
+ print( 'No valid channels or annotations to display')
1843
+ return None
1844
+
1845
+ # initiate segment-serverns
1846
+ ss = segsrv( p )
1847
+ ss.calc_bands( bsigs )
1848
+ ss.calc_hjorths( hsigs )
1849
+ if type( throttle1_sr ) is int: ss.input_throttle( throttle1_sr )
1850
+ if type( throttle2_np ) is int: ss.throttle( throttle2_np )
1851
+ if type( summary_mins ) is int or type( summary_mins ) is float: ss.summary_threshold_mins( summary_mins )
1852
+
1853
+ ss.populate( chs = sigs , anns = anns )
1854
+
1855
+ # some key variables
1856
+ nsecs_clk = ss.num_seconds_clocktime_original()
1857
+ epoch_max = int( nsecs_clk / scope_epoch_sec )
1858
+
1859
+ # color palette
1860
+ pcyc = cycle(px.colors.qualitative.Bold)
1861
+ palette = dict( zip( sigs , [ next(pcyc) for i in list(range(0,len(sigs))) ] ) )
1862
+ apalette = dict( zip( anns , [ next(pcyc) for i in list(range(0,len(anns))) ] ) )
1863
+ # update w/ any user-specified cols, from anncols = { 'ann':'col' }
1864
+ if sigcols is not None:
1865
+ for key, value in sigcols.items(): palette[ key ] = value
1866
+ if stgcols is not None:
1867
+ for key, value in stgcols.items(): apalette[ key ] = value
1868
+ if anncols is not None:
1869
+ for key, value in anncols.items(): apalette[ key ] = value
1870
+
1871
+
1872
+ # define widgets
1873
+
1874
+ wlay1 = widgets.Layout( width='95%' )
1875
+
1876
+ # channel selection box
1877
+ chlab = widgets.Label( value = 'Channels:' )
1878
+ chbox = widgets.SelectMultiple( options=sigs, value=sigs, rows=7, description='', disabled=False , layout = wlay1 )
1879
+ if len(bsigs) != 0: pow_sel = widgets.Dropdown( options = bsigs, value=bsigs[0],description='',disabled=False,layout = wlay1 )
1880
+ else: pow_sel = widgets.Dropdown( options = bsigs, value=None,description="Band power:",disabled=False,layout = wlay1 )
1881
+ band_hjorth_sel = widgets.Checkbox( value = True , description = 'Hjorth' , disabled=False, indent=False )
1882
+
1883
+ # annotations (display)
1884
+ anlab = widgets.Label( value = 'Annotations:' )
1885
+ anbox = widgets.SelectMultiple( options=anns , value=[], rows=3, description='', disabled=False , layout = wlay1 )
1886
+
1887
+ # annotations (instance list/navigation)
1888
+ a1lab = widgets.Label( value = 'Instances:' )
1889
+ ansel = widgets.SelectMultiple( options=anns , value=[], rows=3, description='', disabled=False , layout = wlay1 )
1890
+ a1box = widgets.Select( options=[None] , value=None, rows=3, description='', disabled=False , layout = wlay1 )
1891
+
1892
+ # time display labels
1893
+ tbox = widgets.Label( value = 'T: ' )
1894
+ tbox2 = widgets.Label( value = '' )
1895
+ tbox3 = widgets.Label( value = '' )
1896
+
1897
+ # misc buttons
1898
+ reset_button = widgets.Button( description='Reset', disabled=False,button_style='',tooltip='',layout=widgets.Layout(width='98%') )
1899
+ keep_xscale = widgets.Checkbox( value = False , description = 'Fixed int.' , disabled=False, indent=False )
1900
+ show_ranges = widgets.Checkbox( value = True , description = 'Units' , disabled=False, indent=False )
1901
+
1902
+
1903
+ # naviation: main slider (top)
1904
+ smid = widgets.IntSlider(min=scope_epoch_sec/2, max=nsecs_clk - scope_epoch_sec/2, value=scope_epoch_sec/2, step=30, description='', readout=False,layout=widgets.Layout(width='100%') )
1905
+
1906
+ # left panel buttons: interval width
1907
+ swid_label = widgets.Label( value = 'Width' )
1908
+ swid_dec_button = widgets.Button( description='<', disabled=False,button_style='',tooltip='', layout=widgets.Layout(width='30px'))
1909
+ swid = widgets.Label( value = '30' )
1910
+ swid_inc_button = widgets.Button( description='>', disabled=False,button_style='',tooltip='', layout=widgets.Layout(width='30px'))
1911
+
1912
+ # left panel buttons: left/right advances
1913
+ epoch_label = widgets.Label( value = 'Epoch' )
1914
+ epoch_dec_button = widgets.Button( description='<', disabled=False,button_style='',tooltip='', layout=widgets.Layout(width='30px'))
1915
+ epoch = widgets.Label( value = '1' )
1916
+ epoch_inc_button = widgets.Button( description='>', disabled=False,button_style='',tooltip='', layout=widgets.Layout(width='30px'))
1917
+
1918
+ # left panel buttons: Y-spacing
1919
+ yspace_label = widgets.Label( value = 'Space' )
1920
+ yspace_dec_button = widgets.Button( description='<', disabled=False,button_style='',tooltip='', layout=widgets.Layout(width='30px'))
1921
+ yspace = widgets.Label( value = '1' )
1922
+ yspace_inc_button = widgets.Button( description='>', disabled=False,button_style='',tooltip='', layout=widgets.Layout(width='30px'))
1923
+
1924
+ # left panel buttons: Y-scaling
1925
+ yscale_label = widgets.Label( value = 'Scale' )
1926
+ yscale_dec_button = widgets.Button( description='<', disabled=False,button_style='',tooltip='', layout=widgets.Layout(width='30px'))
1927
+ yscale = widgets.Label( value = '0' )
1928
+ yscale_inc_button = widgets.Button( description='>', disabled=False,button_style='',tooltip='', layout=widgets.Layout(width='30px'))
1929
+
1930
+
1931
+ # --------------------- signal plotter (g)
1932
+
1933
+ # traces (xNS), gaps(x1), labels (xNS), annots(xNA), clock-ticks(x1)
1934
+ fig = [go.Scatter(x = None,
1935
+ y = None,
1936
+ mode = 'lines',
1937
+ line=dict(color=palette[sig], width=1),
1938
+ hoverinfo='none',
1939
+ name = sig ) for sig in sigs
1940
+ ] + [ go.Scatter( x = None , y = None ,
1941
+ mode = 'lines' ,
1942
+ fill='toself' ,
1943
+ fillcolor='#223344',
1944
+ line=dict(color='#888888', width=1),
1945
+ hoverinfo='none',
1946
+ name='Gap' )
1947
+ ] + [ go.Scatter( x = None , y = None ,
1948
+ mode='text' ,
1949
+ textposition='middle right',
1950
+ textfont=dict(
1951
+ size=11,
1952
+ color='white'),
1953
+ hoverinfo='none' ,
1954
+ showlegend=False ) for sig in sigs
1955
+ ] + [ go.Scatter( x = None ,
1956
+ y = None ,
1957
+ mode = 'lines',
1958
+ fill='toself',
1959
+ line=dict(color=apalette[ann], width=1),
1960
+ hoverinfo='none',
1961
+ name = ann ) for ann in anns
1962
+ ] + [ go.Scatter( x = None , y = None ,
1963
+ mode = 'text' ,
1964
+ textposition='bottom right',
1965
+ textfont=dict(
1966
+ size=11,
1967
+ color='white'),
1968
+ hoverinfo='none' ,
1969
+ showlegend=False ) ]
1970
+
1971
+
1972
+ layout = go.Layout( margin=dict(l=8, r=8, t=0, b=0),
1973
+ yaxis=dict(range=[0,1]),
1974
+ modebar={'orientation': 'v','bgcolor': '#E9E9E9','color': 'white','activecolor': 'white' },
1975
+ yaxis_visible=False,
1976
+ yaxis_showticklabels=False,
1977
+ xaxis_visible=False,
1978
+ xaxis_showticklabels=False,
1979
+ autosize=True,
1980
+ height=height,
1981
+ plot_bgcolor='rgb(02,15,50)' )
1982
+
1983
+ g = go.FigureWidget(data=fig, layout= layout )
1984
+ g._config = g._config | {'displayModeBar': False}
1985
+ #g.update_xaxes(showgrid=True, gridwidth=0.1, gridcolor='#445555')
1986
+ g.update_xaxes(showgrid=False)
1987
+ g.update_yaxes(showgrid=False)
1988
+
1989
+
1990
+ # -------------------- segment-plotter (sg)
1991
+
1992
+ num_epochs = ss.num_epochs()
1993
+ tscale = ss.get_time_scale()
1994
+ tstarts = [ tscale[idx] for idx in range(0,len(tscale),2)]
1995
+ tstops = [ tscale[idx] for idx in range(1,len(tscale),2)]
1996
+ times = np.concatenate((tstarts, tstops), axis=1)
1997
+
1998
+ # upper/lower boxes, then frame select, then actual segs
1999
+ sfig = [ go.Scatter(x=[0,0],y=[0.05,0.05],
2000
+ mode='markers+lines',
2001
+ marker=dict(color="navy",size=8))
2002
+ ] + [ go.Scatter(x=[0,0],y=[0.95,0.95],
2003
+ mode='markers+lines',
2004
+ marker=dict(color="navy",size=8))
2005
+ ] + [ go.Scatter(x=[0,0,0,0,0,None],y=[0,0,1,1,0,None],
2006
+ mode='lines',
2007
+ fill='toself',
2008
+ fillcolor = 'rgba( 18, 65, 92, 0.75)' ,
2009
+ line=dict(color="red",width=0.5))
2010
+ ] + [ go.Scatter(x=[x[1],x[1],x[3],x[3]],y=[0,1,1,0], # was 0 1 3 2
2011
+ fill="toself",
2012
+ mode = 'lines',
2013
+ hoverinfo = 'none',
2014
+ line=dict(color='rgb(19,114,38)', width=1), ) for x in times ]
2015
+
2016
+ slayout = go.Layout( margin=dict(l=8, r=8, t=2, b=4),
2017
+ showlegend=False,
2018
+ xaxis=dict(range=[0,1]),
2019
+ yaxis=dict(range=[0,1]),
2020
+ yaxis_visible=False,
2021
+ yaxis_showticklabels=False,
2022
+ xaxis_visible=False,
2023
+ xaxis_showticklabels=False,
2024
+ autosize=True,
2025
+ height=15,
2026
+ plot_bgcolor='rgb(255,255,255)' )
2027
+
2028
+ sg = go.FigureWidget( data=sfig, layout=slayout )
2029
+ sg._config = sg._config | {'displayModeBar': False}
2030
+
2031
+ # --------------------- hypnogram-level summary
2032
+
2033
+ stgs = [ 'N1' , 'N2' , 'N3' , 'R' , 'W' , '?' , 'L' ]
2034
+ stgcols = { 'N1':'rgba(32, 178, 218, 1)' , 'N2':'blue', 'N3':'navy','R':'red','W':'green','?':'gray','L':'yellow' }
2035
+ stgns = { 'N1':-1 , 'N2':-2, 'N3':-3,'R':0,'W':1,'?':2,'L':2 }
2036
+
2037
+ # clock-time stage info (in units no larger than 30 seconds)
2038
+ stg_evts = p.fetch_annots( stgs , 30 )
2039
+ if len( stg_evts ) != 0:
2040
+ stg_evts2 = stg_evts.copy()
2041
+ stg_evts2[ 'Start' ] = stg_evts2[ 'Stop' ]
2042
+ stg_evts[ 'IDX' ] = range(len(stg_evts))
2043
+ stg_evts2[ 'IDX' ] = range(len(stg_evts))
2044
+ stg_evts = pd.concat( [stg_evts2, stg_evts] )
2045
+ stg_evts = stg_evts.sort_values(by=['Start', 'IDX'])
2046
+ times = stg_evts['Start'].to_numpy()
2047
+ ys = [ stgns[c] for c in stg_evts['Class'].tolist() ]
2048
+ cols = [ stgcols[c] for c in stg_evts['Class'].tolist() ]
2049
+ else:
2050
+ times = None
2051
+ ys = None
2052
+ cols = None
2053
+
2054
+ hypfig = [ go.Scatter( x = times, y=ys, mode='lines', line=dict(color='gray')) ]
2055
+
2056
+ hypfig.append( go.Scatter(x = times,
2057
+ y = ys ,
2058
+ mode = 'markers' ,
2059
+ marker=dict( color = cols , size=2),
2060
+ hoverinfo='none' ) )
2061
+
2062
+ hyplayout = go.Layout( margin=dict(l=8, r=8, t=0, b=0),
2063
+ showlegend=False,
2064
+ xaxis=dict(range=[0,nsecs_clk]),
2065
+ yaxis=dict(range=[-4,3]),
2066
+ yaxis_visible=False,
2067
+ yaxis_showticklabels=False,
2068
+ xaxis_visible=False,
2069
+ xaxis_showticklabels=False,
2070
+ autosize=True,
2071
+ height=35,
2072
+ plot_bgcolor='rgb(255,255,255)' )
2073
+
2074
+ hypg = go.FigureWidget( data = hypfig , layout = hyplayout )
2075
+ hypg._config = hypg._config | {'displayModeBar': False}
2076
+
2077
+
2078
+ # --------------------- band power/spectrogram (bg)
2079
+
2080
+ #bfig = go.Heatmap( z = None , type = 'heatmap', colorscale = 'RdBu_r', showscale = False , hoverinfo = 'none' )
2081
+ bfig = go.Heatmap( z = None , type = 'heatmap', colorscale = 'turbo', showscale = False , hoverinfo = 'none' )
2082
+
2083
+ blayout = go.Layout( margin=dict(l=8, r=8, t=0, b=0),
2084
+ modebar={'orientation': 'h','bgcolor': '#E9E9E9','color': 'white','activecolor': 'white' },
2085
+ showlegend=False,
2086
+ yaxis_visible=False,
2087
+ yaxis_showticklabels=False,
2088
+ xaxis_visible=False,
2089
+ xaxis_showticklabels=False,
2090
+ autosize=True,
2091
+ height=50,
2092
+ plot_bgcolor='rgb(255,255,255)' )
2093
+
2094
+ bg = go.FigureWidget( bfig , blayout )
2095
+ bg._config = bg._config | {'displayModeBar': False}
2096
+
2097
+
2098
+ # --------------------- build overall box (containerP)
2099
+
2100
+ # ----- containers - left panel
2101
+
2102
+ ctr_lab_container = widgets.VBox(children=[ swid_label , epoch_label, yspace_label , yscale_label ] ,
2103
+ layout = widgets.Layout( width='30%', align_items='center' , display='flex', flex_flow='column' ) )
2104
+
2105
+ ctr_dec_container = widgets.VBox(children=[ swid_dec_button , epoch_dec_button, yspace_dec_button , yscale_dec_button ] ,
2106
+ layout = widgets.Layout( width='20%', align_items='center' , display='flex', flex_flow='column' ))
2107
+
2108
+ ctr_val_container = widgets.VBox(children=[ swid , epoch , yspace , yscale ] ,
2109
+ layout = widgets.Layout( width='30%', align_items='center' , display='flex', flex_flow='column' ))
2110
+
2111
+ ctr_inc_container = widgets.VBox(children=[ swid_inc_button , epoch_inc_button, yspace_inc_button , yscale_inc_button ] ,
2112
+ layout = widgets.Layout( width='20%', align_items='center' , display='flex', flex_flow='column' ))
2113
+
2114
+ # left panel: group top set of widgets
2115
+ ctr_container = widgets.VBox( children=[ tbox, widgets.HBox(children=[ ctr_lab_container, ctr_dec_container, ctr_val_container, ctr_inc_container ] ) , reset_button ] ,
2116
+ layout = widgets.Layout( width='100%' ) )
2117
+
2118
+ # left panel: lower buttons
2119
+ lower_buttons = widgets.HBox( children=[ keep_xscale , show_ranges ] ,
2120
+ layout = widgets.Layout( width='100%' ) )
2121
+
2122
+ # left panel: construct all
2123
+ left_panel = widgets.VBox(children=[ ctr_container,
2124
+ chlab, chbox,
2125
+ widgets.HBox( children = [ band_hjorth_sel, pow_sel ] ),
2126
+ anlab, anbox, a1lab, ansel, a1box,
2127
+ lower_buttons ] ,
2128
+ layout = widgets.Layout( width='95%' , margin='0 0 0 5px' , overflow_x = 'hidden' ) )
2129
+
2130
+ # right panel: combine plots
2131
+ containerS = widgets.VBox(children=[ smid , hypg, sg, bg, g ] , layout = widgets.Layout( width='95%' , margin='0 5px 0 5px' , overflow_x = 'hidden' ) )
2132
+
2133
+ # make the final app (just join left+right panels)
2134
+ container_app = AppLayout(header=None,
2135
+ left_sidebar=left_panel,
2136
+ center=containerS,
2137
+ right_sidebar=None,
2138
+ pane_widths=[1, 8, 0],
2139
+ align_items = 'stretch' ,
2140
+ footer=None , layout = widgets.Layout( border='3px none #708090' , margin='10px 5px 10px 5px' , overflow_x = 'hidden' ) )
2141
+
2142
+
2143
+ # --------------------- callback functions
2144
+
2145
+ def redraw():
2146
+
2147
+ # update hms message
2148
+ tbox.value = 'T: ' + ss.get_window_left_hms() + ' - ' + ss.get_window_right_hms()
2149
+
2150
+ # get annots
2151
+ ss.compile_windowed_annots( anbox.value )
2152
+
2153
+ x1 = ss.get_window_left()
2154
+ x2 = ss.get_window_right()
2155
+
2156
+ # update pointers on segment plot
2157
+ s1 = x1 / nsecs_clk
2158
+ s2 = x2 / nsecs_clk
2159
+ sg.data[0].x = [ s1, s2 ]
2160
+ sg.data[1].x = [ s1, s2 ]
2161
+ sg.data[2].x = [ s1 , s2 , s2 , s1 , s1 , None ]
2162
+
2163
+ # update main plot
2164
+ with g.batch_update():
2165
+ ns = len(sigs)
2166
+ na = len(anns)
2167
+
2168
+ # axes
2169
+ g.update_xaxes(range = [x1,x2])
2170
+
2171
+ # signals (0)
2172
+ selected = [ x in chbox.value for x in sigs ]
2173
+ idx=0
2174
+ for i in list(range(0,ns)):
2175
+ if selected[i] is True:
2176
+ g.data[i].x = ss.get_timetrack( sigs[i] )
2177
+ g.data[i].y = ss.get_scaled_signal( sigs[i] , idx )
2178
+ g.data[i].visible = True
2179
+ idx += 1
2180
+ else:
2181
+ g.data[i].visible = False
2182
+
2183
+ # gaps (last trace)
2184
+ gidx = ns
2185
+ gaps = list( ss.get_gaps() )
2186
+ if len(gaps) == 0:
2187
+ g.data[ gidx ].visible = False
2188
+ else:
2189
+ # make into 6-value formats
2190
+ xgaps = [(a, b, b, a, a, None ) for a, b in gaps ]
2191
+ ygaps = [(0, 0, 1-header_height, 1-header_height, 0, None ) for a, b in gaps ]
2192
+ g.data[ gidx ].x = [x for sub in xgaps for x in sub]
2193
+ g.data[ gidx ].y = [y for sub in ygaps for y in sub]
2194
+ g.data[ gidx ].visible = True
2195
+
2196
+ # ranges? (+ns)
2197
+ if show_ranges.value is True:
2198
+ idx=0
2199
+ xl = x1 + (x2-x1 ) * 0.01
2200
+ for i in list(range(0,ns)):
2201
+ if selected[i] is True:
2202
+ ylim = ss.get_window_phys_range( sigs[i] )
2203
+ ylab = sigs[i] + ' ' + str(round(ylim[0],3)) + ':' + str(round(ylim[1],3)) + ' (' + units[sigs[i]] +')'
2204
+ g.data[i+ns+1].x = [ xl ]
2205
+ g.data[i+ns+1].y = [ ss.get_ylabel( idx ) * (1 - header_height ) ]
2206
+ g.data[i+ns+1].text = [ ylab ]
2207
+ g.data[i+ns+1].visible = True
2208
+ idx += 1
2209
+ else:
2210
+ g.data[i+ns+1].visible = False
2211
+
2212
+
2213
+ # annots (+2ns + gap)
2214
+ ns2 = 2 * ns + 1
2215
+ selected = [ x in anbox.value for x in anns ]
2216
+ for i in list(range(0,na)):
2217
+ if selected[i] is True:
2218
+ g.data[i+ns2].x = ss.get_annots_xaxes( anns[i] )
2219
+ g.data[i+ns2].y = ss.get_annots_yaxes( anns[i] )
2220
+ g.data[i+ns2].visible = True
2221
+ else:
2222
+ g.data[i+ns2].visible = False
2223
+
2224
+ # clock-ticks
2225
+ gidx = 2 * ns + na + 1
2226
+ tks = ss.get_clock_ticks(6)
2227
+ tx = list( tks.keys() )
2228
+ tv = list( tks.values() )
2229
+ if len( tx ) == 0:
2230
+ g.data[ gidx ].visible = False
2231
+ else:
2232
+ g.data[ gidx ].x = tx
2233
+ g.data[ gidx ].y = [ 1 - header_height + ( header_height ) * 0.5 for x in tx ]
2234
+ g.data[ gidx ].text = tv
2235
+ g.data[ gidx ].visible = True
2236
+
2237
+ def rescale(change):
2238
+ ss.set_scaling( len(chbox.value) , len( anbox.value) , 2**float(yscale.value) , float(yspace.value) , header_height, footer_height , annot_height )
2239
+ redraw()
2240
+
2241
+ def update_bandpower(change):
2242
+ if pow_sel.value is None: return
2243
+ if len( pow_sel.value ) == 0: return
2244
+ if band_hjorth_sel.value is True:
2245
+ S = np.transpose( ss.get_hjorths( pow_sel.value ) )
2246
+ S = np.asarray(S,dtype=object)
2247
+ S[np.isnan(S.astype(np.float64))] = None
2248
+ bg.update_traces({'z': S } , selector = {'type':'heatmap'} )
2249
+ else:
2250
+ S = np.transpose( ss.get_bands( pow_sel.value ) )
2251
+ S = np.asarray(S,dtype=object)
2252
+ S[np.isnan(S.astype(np.float64))] = None
2253
+ bg.update_traces({'z': S } , selector = {'type':'heatmap'} )
2254
+
2255
+ def pop_a1(change):
2256
+ a1box.options = ss.get_all_annots( ansel.value )
2257
+
2258
+ def a1_win(change):
2259
+ # format <annot> | t1-t2 (seconds)
2260
+ # allow for pipe in <annot> name
2261
+ nwin = a1box.value.split( '| ')[-1]
2262
+ nwin = nwin.split('-')
2263
+ nwin = [ float(x) for x in nwin ]
2264
+
2265
+ # center on mid of annot
2266
+ mid = nwin[0] + ( nwin[1] - nwin[0] ) / 2
2267
+
2268
+ # width: either based on annot, or keep as is
2269
+ if keep_xscale.value is False:
2270
+ swid.unobserve(set_window_from_sliders, names="value")
2271
+ swid.value = str( round( nwin[1] - nwin[0] , 2 ) )
2272
+ swid.observe(set_window_from_sliders, names="value")
2273
+
2274
+ # update smid, and trigger redraw via set_window_from_sliders()
2275
+ smid.value = mid
2276
+
2277
+ def set_window_from_sliders(change):
2278
+ w = float( swid.value )
2279
+ p1 = smid.value - 0.5 * w
2280
+ if p1 < 0: p1 = 0
2281
+ p2 = p1 + w
2282
+ if p2 >= ss.num_seconds_clocktime():
2283
+ p2 = ss.num_seconds_clocktime() - 1
2284
+ ss.window( p1 , p2 )
2285
+ epoch.value = str(1+int(smid.value/30))
2286
+ redraw()
2287
+
2288
+ def fn_reset(b):
2289
+ swid.value = str( 30 )
2290
+ yspace.value = str( 1 )
2291
+ yscale.value = str( 0 )
2292
+
2293
+ def fn_dec_epoch(b):
2294
+ if ( smid.value - scope_epoch_sec ) >= smid.min :
2295
+ smid.value = smid.value - scope_epoch_sec
2296
+
2297
+ def fn_inc_epoch(b):
2298
+ if ( smid.value + scope_epoch_sec ) <= smid.max :
2299
+ smid.value = smid.value + scope_epoch_sec
2300
+
2301
+ def fn_dec_swid(b):
2302
+ swid_var = float( swid.value )
2303
+ if swid_var > 3.5: swid_var = swid_var / 2
2304
+ if swid_var > 100: swid.value = str( int( swid_var ))
2305
+ else: swid.value = str( swid_var )
2306
+
2307
+ def fn_inc_swid(b):
2308
+ swid_var = float( swid.value )
2309
+ if swid_var < 40000: swid_var = swid_var * 2
2310
+ if swid_var > 100: swid.value = str( int( swid_var ) )
2311
+ else: swid.value = str( swid_var )
2312
+
2313
+ def fn_yspace_dec(b):
2314
+ yspace_var = float( yspace.value )
2315
+ if yspace_var > 0.05: yspace_var = yspace_var - 0.1
2316
+ yspace.value = str( round( yspace_var , 1 ) )
2317
+
2318
+ def fn_yspace_inc(b):
2319
+ yspace_var = float( yspace.value )
2320
+ if yspace_var < 0.95: yspace_var = yspace_var + 0.1
2321
+ yspace.value = str( round( yspace_var , 1 ) )
2322
+
2323
+ def fn_yscale_dec(b):
2324
+ yscale_var = float( yscale.value )
2325
+ if yscale_var > -2: yscale_var = yscale_var - 0.2
2326
+ yscale.value = str( round( yscale_var , 1 ) )
2327
+
2328
+ def fn_yscale_inc(b):
2329
+ yscale_var = float( yscale.value )
2330
+ if yscale_var < 2: yscale_var = yscale_var + 0.2
2331
+ yscale.value = str( round( yscale_var , 1 ) )
2332
+
2333
+ def fn_hjorth_band(b):
2334
+ if band_hjorth_sel.value is True:
2335
+ pow_sel.options = hsigs
2336
+ else:
2337
+ pow_sel.options = bsigs
2338
+
2339
+ # --------------------- hook up widgets
2340
+
2341
+ # observers
2342
+ smid.observe(set_window_from_sliders, names="value")
2343
+ swid.observe(set_window_from_sliders, names="value")
2344
+
2345
+ show_ranges.observe(set_window_from_sliders)
2346
+
2347
+ band_hjorth_sel.observe( fn_hjorth_band )
2348
+
2349
+ swid_dec_button.on_click(fn_dec_swid)
2350
+ swid_inc_button.on_click(fn_inc_swid)
2351
+
2352
+ epoch_dec_button.on_click(fn_dec_epoch)
2353
+ epoch_inc_button.on_click(fn_inc_epoch)
2354
+
2355
+ reset_button.on_click(fn_reset)
2356
+
2357
+ # summaries
2358
+ pow_sel.observe(update_bandpower,names="value")
2359
+
2360
+ # rescale plots
2361
+ yscale_dec_button.on_click( fn_yscale_dec )
2362
+ yscale_inc_button.on_click( fn_yscale_inc )
2363
+ yspace_dec_button.on_click( fn_yspace_dec )
2364
+ yspace_inc_button.on_click( fn_yspace_inc )
2365
+
2366
+ yscale.observe( rescale , names="value")
2367
+ yspace.observe( rescale , names="value")
2368
+
2369
+
2370
+ # channel selection
2371
+ chbox.observe( rescale ,names="value")
2372
+
2373
+ # annots
2374
+ anbox.observe( rescale , names="value")
2375
+ ansel.observe( pop_a1 , names="value")
2376
+ a1box.observe( a1_win , names="value")
2377
+
2378
+
2379
+ # --------------------- display
2380
+ update_bandpower(None)
2381
+ ss.set_scaling( len(chbox.value) , len( anbox.value) , 2**float(yscale.value) , float(yspace.value) , header_height, footer_height , annot_height )
2382
+
2383
+ ss.window( 0 , 30 )
2384
+ epoch.value = str(1);
2385
+
2386
+ redraw()
2387
+ return container_app
2388
+
2389
+
2390
+ # --------------------------------------------------------------------------------
2391
+ # moonbeam
2392
+
2393
+ class moonbeam:
2394
+ """Moonbeam utility to pull NSRR data"""
2395
+
2396
+ df1 = None # available cohorts
2397
+ df2 = None # available files for current cohort
2398
+ curr_cohort = None
2399
+
2400
+ def __init__(self, nsrr_tok , cdir = None ):
2401
+ """ Initiate Moonbeam with an NSRR token """
2402
+ self.nsrr_tok = nsrr_tok
2403
+ self.df1 = self.cohorts()
2404
+ if cdir is None: cdir = os.path.join( tempfile.gettempdir() , 'luna-moonbeam' )
2405
+ self.set_cache(cdir)
2406
+
2407
+ def set_cache(self,cdir):
2408
+ """ Set the folder for caching downloaded records """
2409
+ self.cdir = cdir
2410
+ print( 'using cache folder for downloads: ' + self.cdir )
2411
+ os.makedirs( os.path.dirname(self.cdir), exist_ok=True)
2412
+
2413
+ def cached(self,file):
2414
+ """ Check whether a file is already cached """
2415
+ return os.path.exists( os.path.join( self.cdir , file ) )
2416
+
2417
+ def cohorts(self):
2418
+ """ List all available cohorts accessible from the given NSRR user token """
2419
+ req = requests.get( 'https://zzz.bwh.harvard.edu/cgi-bin/moonbeam.cgi?t=' + self.nsrr_tok ).content
2420
+ self.df1 = pd.read_csv(io.StringIO(req.decode('utf-8')),sep='\t',header=None)
2421
+ self.df1.columns = ['Cohort','Description']
2422
+ return self.df1
2423
+
2424
+ def cohort(self,cohort1):
2425
+ """ List all files (EDFs and annotations) available for a given cohort """
2426
+ if type(cohort1) is int: cohort1 = self.df1.loc[cohort1,'Cohort']
2427
+ if type(cohort1) is not str: return
2428
+ self.curr_cohort = cohort1
2429
+ req = requests.get( 'https://zzz.bwh.harvard.edu/cgi-bin/moonbeam.cgi?t=' + self.nsrr_tok + "&c=" + cohort1).content
2430
+ df = pd.read_csv(io.StringIO(req.decode('utf-8')),sep='\t',header=None)
2431
+ df.columns = [ 'cohort' , 'IID' , 'file' ]
2432
+
2433
+ # get EDFs, annots then merge
2434
+ df_edfs = df[ df['file'].str.contains(".edf$|.edf.gz$" , case = False ) ][[ 'IID' , 'file' ]]
2435
+ df_annots = df[ ~ df['file'].str.contains(".edf$|.edf.gz$" , case = False ) ][[ 'IID' , 'file' ]]
2436
+ self.df2 = pd.merge( df_edfs , df_annots , on='IID' , how='left' )
2437
+ self.df2.columns = [ 'ID' , 'EDF' , 'Annot' ]
2438
+ return self.df2
2439
+
2440
+
2441
+ def inst(self, iid ):
2442
+ """ Create an instance of a record, either downloaded or cached """
2443
+ if self.df2 is None: return
2444
+ if self.curr_cohort is None: return
2445
+
2446
+ # ensure we have this file
2447
+ self.pull( iid , self.curr_cohort )
2448
+
2449
+ # ensure we have a proj (from proj singleton)
2450
+ proj1 = proj(False)
2451
+ p = proj1.inst( self.curr_id )
2452
+ edf1 = str( pathlib.Path( self.cdir ).joinpath( self.curr_edf ).expanduser().resolve() )
2453
+ p.attach_edf( edf1 )
2454
+
2455
+ if self.curr_annot is not None:
2456
+ annot1 = str( pathlib.Path( self.cdir ).joinpath( self.curr_annot ).expanduser().resolve() )
2457
+ p.attach_annot( annot1 )
2458
+
2459
+ # return handle back
2460
+ return p
2461
+
2462
+
2463
+ def pull(self, iid , cohort ):
2464
+ """ Download an individual record (if not already cached) """
2465
+ if self.df2.empty: return False
2466
+
2467
+ # iid
2468
+ if type(iid) is int: iid = self.df2.loc[iid,'ID']
2469
+ self.curr_id = iid
2470
+
2471
+ # EDF
2472
+ self.curr_edf = self.df2.loc[ self.df2['ID'] == iid,'EDF'].item()
2473
+ self.pull_file( self.curr_edf )
2474
+
2475
+ # EDFZ .idx
2476
+ if re.search(r'\.edf\.gz$',self.curr_edf,re.IGNORECASE) or re.search(r'\.edfz$',self.curr_edf,re.IGNORECASE):
2477
+ self.pull_file( self.curr_edf + '.idx' )
2478
+
2479
+ # annots (optional)
2480
+ self.curr_annot = self.df2.loc[ self.df2['ID'] == iid,'Annot'].item()
2481
+ if self.curr_annot is not None:
2482
+ self.pull_file( self.curr_annot )
2483
+
2484
+ def pull_file( self , file ):
2485
+ import functools
2486
+ import pathlib
2487
+ import shutil
2488
+ import requests
2489
+ from tqdm.auto import tqdm
2490
+
2491
+ if self.cached( file ) is True:
2492
+ print( file + ' is already cached' )
2493
+ return
2494
+
2495
+ # save file to cdir/{path/}file, e.g. path will be cohort
2496
+ path = pathlib.Path( self.cdir ).joinpath( file ).expanduser().resolve()
2497
+ path.parent.mkdir(parents=True, exist_ok=True)
2498
+
2499
+ print( '\nbeaming ' + self.curr_id + ' : ' + file )
2500
+
2501
+ url = 'https://zzz.bwh.harvard.edu/cgi-bin/moonbeam.cgi?t=' + self.nsrr_tok + "&f=" + file
2502
+ r = requests.get(url, stream=True, allow_redirects=True)
2503
+
2504
+ if r.status_code != 200:
2505
+ r.raise_for_status() # Will only raise for 4xx codes, so...
2506
+ raise RuntimeError(f"Request to {url} returned status code {r.status_code}")
2507
+ file_size = int(r.headers.get('Content-Length', 0))
2508
+
2509
+ desc = "(Unknown total file size)" if file_size == 0 else ""
2510
+ r.raw.read = functools.partial(r.raw.read, decode_content=True) # Decompress if needed
2511
+ with tqdm.wrapattr(r.raw, "read", total=file_size, desc=desc) as r_raw:
2512
+ with path.open("wb") as f:
2513
+ shutil.copyfileobj(r_raw, f)
2514
+
2515
+
2516
+ def pheno(self, cohort = None, iid = None):
2517
+ """ Pull phenotypes for a given individual """
2518
+
2519
+ coh1 = cohort
2520
+ id1 = iid
2521
+
2522
+ if coh1 is None:
2523
+ if self.curr_cohort is None: return
2524
+ coh1 = self.curr_cohort
2525
+
2526
+ if id1 is None:
2527
+ if self.curr_id is None: return
2528
+ id1 = self.curr_id
2529
+
2530
+ url = 'https://zzz.bwh.harvard.edu/cgi-bin/moonbeam.cgi?t=' + self.nsrr_tok + '&c=' + self.curr_cohort + '&p=' + id1
2531
+ req = requests.get( url )
2532
+
2533
+ if req.status_code != 200:
2534
+ req.raise_for_status() # Will only raise for 4xx codes, so...
2535
+ raise RuntimeError(f"Moonbeam returned status code {req.status_code}")
2536
+
2537
+ df = pd.read_csv(io.StringIO(req.content.decode('utf-8')),sep='\t',header=None)
2538
+ df.columns = ['Variable', 'Value', 'Units', 'Description' ]
2539
+ pri = [ "nsrr_age", "nsrr_sex", "nsrr_bmi", "nsrr_flag_spsw", "nsrr_ahi_hp3r_aasm15", "nsrr_ahi_hp4u_aasm15" ]
2540
+ df1 = df[ df['Variable'].isin( pri ) ]
2541
+ df2 = df[ ~ df['Variable'].isin( pri ) ]
2542
+ df = pd.concat( [ df1 , df2 ] )
2543
+ return df
2544
+
2545
+
2546
+