lunapi 1.3.1__cp39-cp39-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/__init__.py +13 -0
- lunapi/lunapi0.cpp +391 -0
- lunapi/lunapi0.cpython-39-x86_64-linux-gnu.so +0 -0
- lunapi/lunapi1.py +2546 -0
- lunapi-1.3.1.dist-info/METADATA +37 -0
- lunapi-1.3.1.dist-info/RECORD +10 -0
- lunapi-1.3.1.dist-info/WHEEL +5 -0
- lunapi-1.3.1.dist-info/licenses/LICENSE +674 -0
- lunapi.libs/libgcc_s-a3a07607.so.1 +0 -0
- lunapi.libs/libstdc++-496613c0.so.6.0.32 +0 -0
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
|
+
|