BCI2000Tools 1.0.0__py2.py3-none-any.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.
- BCI2000Tools/AllTools.py +48 -0
- BCI2000Tools/Bootstrapping.py +145 -0
- BCI2000Tools/Chain.py +775 -0
- BCI2000Tools/Container.py +440 -0
- BCI2000Tools/ElectrodeGrids.py +590 -0
- BCI2000Tools/ElectrodePositions.py +601 -0
- BCI2000Tools/Electrodes.py +1657 -0
- BCI2000Tools/EventRelated.py +906 -0
- BCI2000Tools/Expressions.py +117 -0
- BCI2000Tools/FileReader.py +676 -0
- BCI2000Tools/LoadStream2Mat.py +154 -0
- BCI2000Tools/Numerics.py +359 -0
- BCI2000Tools/Parameters.py +950 -0
- BCI2000Tools/Plotting.py +1049 -0
- BCI2000Tools/Remote.py +249 -0
- BCI2000Tools/TimingAnalysis.py +332 -0
- BCI2000Tools/__init__.py +40 -0
- bci2000tools-1.0.0.dist-info/METADATA +50 -0
- bci2000tools-1.0.0.dist-info/RECORD +21 -0
- bci2000tools-1.0.0.dist-info/WHEEL +6 -0
- bci2000tools-1.0.0.dist-info/top_level.txt +1 -0
BCI2000Tools/Chain.py
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# ///////////////////////////////////////////////////////////////////////////
|
|
4
|
+
# $Id: Chain.py 9041 2025-09-09 20:29:54Z jhill $
|
|
5
|
+
# Author: jeremy.hill@neurotechcenter.org
|
|
6
|
+
# Description: wrapper around BCI2000 command-line tool processing
|
|
7
|
+
#
|
|
8
|
+
# $BEGIN_BCI2000_LICENSE$
|
|
9
|
+
#
|
|
10
|
+
# This file is part of BCI2000, a platform for real-time bio-signal research.
|
|
11
|
+
# [ Copyright (C) 2000-2022: BCI2000 team and many external contributors ]
|
|
12
|
+
#
|
|
13
|
+
# BCI2000 is free software: you can redistribute it and/or modify it under the
|
|
14
|
+
# terms of the GNU General Public License as published by the Free Software
|
|
15
|
+
# Foundation, either version 3 of the License, or (at your option) any later
|
|
16
|
+
# version.
|
|
17
|
+
#
|
|
18
|
+
# BCI2000 is distributed in the hope that it will be useful, but
|
|
19
|
+
# WITHOUT ANY WARRANTY
|
|
20
|
+
# - without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
21
|
+
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
22
|
+
#
|
|
23
|
+
# You should have received a copy of the GNU General Public License along with
|
|
24
|
+
# this program. If not, see <http://www.gnu.org/licenses/>.
|
|
25
|
+
#
|
|
26
|
+
# $END_BCI2000_LICENSE$
|
|
27
|
+
# ///////////////////////////////////////////////////////////////////////////
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
This module provides the `bci2000chain()` function and
|
|
31
|
+
supporting tools, the purpose of which is to wrap calls
|
|
32
|
+
to chains of `tools/cmdline/*` BCI2000 binaries.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
'bci2000chain',
|
|
37
|
+
'bci2000root', 'bci2000path',
|
|
38
|
+
'CallbackObject', 'TrapFilterCallbackObject',
|
|
39
|
+
'BCI2000ChainError', 'TerminateChain',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
import os
|
|
43
|
+
import sys
|
|
44
|
+
import ast
|
|
45
|
+
import time
|
|
46
|
+
import struct
|
|
47
|
+
import select
|
|
48
|
+
import weakref
|
|
49
|
+
import warnings
|
|
50
|
+
import tempfile
|
|
51
|
+
import threading
|
|
52
|
+
import subprocess
|
|
53
|
+
|
|
54
|
+
import numpy
|
|
55
|
+
|
|
56
|
+
from . import Bootstrapping; from .Bootstrapping import bci2000root, bci2000path, bci2000_exe_extension
|
|
57
|
+
from . import Parameters; from .Parameters import make_bciprm, read_bcidate
|
|
58
|
+
from . import Container; from .Container import Bunch as CONTAINER
|
|
59
|
+
from . import LoadStream2Mat; from .LoadStream2Mat import ReadMatFile
|
|
60
|
+
from . import Numerics; from .Numerics import pdiff
|
|
61
|
+
|
|
62
|
+
try: from . import Electrodes; from .Electrodes import ChannelSet # TODO: Electrodes, ElectrodeGrids, ElectrodePositions and Plotting don't really belong in this package
|
|
63
|
+
except ImportError: ChannelSet = None # in this case, the optional .ChannelSet attribute will simply not be added to the output, at the end of bci2000chain()
|
|
64
|
+
|
|
65
|
+
if sys.version < '3': bytes = str
|
|
66
|
+
else: unicode = str; basestring = ( unicode, bytes )
|
|
67
|
+
|
|
68
|
+
DAT2STREAM_HAS_P_FLAG = True # new-style bci_dat2stream has a -p flag allowing it to read a .prm file to override parameter values that came from the .dat file. Without this, some source parameters like SampleBlockSize and SourceChGain cannot be overridden.
|
|
69
|
+
STREAM2MAT_SAVES_PARMS = True # new-style bci_stream2mat saves a string representation of the collated output parameters in the mat file, so we don't have to rely on a separate parameter file.
|
|
70
|
+
USE_INPUT_AND_OUTPUT_FLAGS = True # using ' "--input=$DATFILE"' yields an order-of-magnitude speedup relative to ' < "$DATFILE"'. With `callback=None` (so that `bci_stream2mat` gets used) I see no great corresponding speedup for ' "--output=$MATFILE"' relative to ' > "$MATFILE"'
|
|
71
|
+
|
|
72
|
+
class BCI2000ChainError( OSError ):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
class TerminateChain( Exception ):
|
|
76
|
+
def __init__( self, keepBlock=False ):
|
|
77
|
+
self.keepBlock = keepBlock
|
|
78
|
+
|
|
79
|
+
def bci2000chain(datfile, chain='SpectralSignalProcessing', parms=(), dims='auto', start=None, duration=None, verbose=False, keep=False, binpath=None, callback='default', **kwargs):
|
|
80
|
+
"""
|
|
81
|
+
This function wraps the processing of a BCI2000 `.dat` file through
|
|
82
|
+
`bci_dat2stream.exe` followed by a specified series of command-line
|
|
83
|
+
filter tools, as described at
|
|
84
|
+
https://www.bci2000.org/mediawiki/index.php/User_Reference:Command_Line_Processing
|
|
85
|
+
|
|
86
|
+
See also the equivalent Matlab wrapper, described at
|
|
87
|
+
https://www.bci2000.org/mediawiki/index.php/User_Reference:Matlab_Tools
|
|
88
|
+
and in the Matlab documentation of `bci2000chain.m`.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
datfile (str):
|
|
92
|
+
a path to a BCI2000 `.dat` file.
|
|
93
|
+
|
|
94
|
+
chain (str):
|
|
95
|
+
a list of BCI2000 filter tool names, or a pipe-delimited string
|
|
96
|
+
composed of filter tool names. The default value,
|
|
97
|
+
`'SpectralSignalProcessing'`, is a shortcut for::
|
|
98
|
+
|
|
99
|
+
chain = [ 'TransmissionFilter', 'SpatialFilter',
|
|
100
|
+
'SpectralEstimator', 'LinearClassifier', 'LPFilter'
|
|
101
|
+
'ExpressionFilter', 'Normalizer' ]
|
|
102
|
+
|
|
103
|
+
parms (str, tuple, list):
|
|
104
|
+
By default the parameter values used in the processing chain are
|
|
105
|
+
the ones saved in the `.dat` file (where relevant) or the
|
|
106
|
+
filter defaults (where the `.dat` file does not specify the
|
|
107
|
+
parameter). You can override parameter values by using this
|
|
108
|
+
argument, which can take the form of:
|
|
109
|
+
|
|
110
|
+
- A name of, or path to, a BCI2000 `.prm` or `.dat` file.
|
|
111
|
+
- A sequence of such filenames/paths.
|
|
112
|
+
- A `dict` of `{ paramName1: paramValue1, paramName2: paramValue2, ... }`
|
|
113
|
+
- A sequence of `[ paramName1, paramValue1, paramName2, paramValue2, ... ]`.
|
|
114
|
+
- Sequence of name + value pairs and/or filenames and/or dicts
|
|
115
|
+
(different formats may be interspersed).
|
|
116
|
+
|
|
117
|
+
Sequences are read from left to right, with later parameter
|
|
118
|
+
settings overwriting earlier ones. Any additional `**kwargs`
|
|
119
|
+
are also interpreted as parameter specifications, and are
|
|
120
|
+
applied last.
|
|
121
|
+
|
|
122
|
+
dims (int, str):
|
|
123
|
+
This should be 2, 3 or 'auto':
|
|
124
|
+
|
|
125
|
+
- `dims=2` means the signal should be two-dimensional:
|
|
126
|
+
elements (concatenated across sample-blocks) by channels.
|
|
127
|
+
- `dims=3` means the signal should be three-dimensional:
|
|
128
|
+
sample-blocks by channels by elements
|
|
129
|
+
- `dims='auto'` means the choice of 2- or 3-dimensional
|
|
130
|
+
output should be made automatically.
|
|
131
|
+
|
|
132
|
+
start (int, float, str):
|
|
133
|
+
Passed through to `bci_dat2stream` as the `--start=` value:
|
|
134
|
+
hence, specifies the number of blocks to skip at the beginning
|
|
135
|
+
(or number of seconds or milliseconds, if expressed as a string
|
|
136
|
+
with `'s'` or `'ms'` appended).
|
|
137
|
+
|
|
138
|
+
duration (int, float, str):
|
|
139
|
+
Passed through to `bci_dat2stream` as the `--duration=` value:
|
|
140
|
+
hence, specifies the number of blocks to process (or number of
|
|
141
|
+
seconds or milliseconds, if expressed as a string with `'s'`
|
|
142
|
+
or `'ms'` appended).
|
|
143
|
+
|
|
144
|
+
verbose (bool):
|
|
145
|
+
If `True`, output diagnostic/debugging messages to the console.
|
|
146
|
+
|
|
147
|
+
keep (bool):
|
|
148
|
+
Whether or not to keep the temporary files created by the chain.
|
|
149
|
+
By default, the files are deleted if there were no errors.
|
|
150
|
+
|
|
151
|
+
binpath (str):
|
|
152
|
+
Optionally specifies the path (or partial path relative to a
|
|
153
|
+
previously-established `bci2000root()`) to the directory in
|
|
154
|
+
which the filter-tool executables reside.
|
|
155
|
+
|
|
156
|
+
callback (None, 'default', or a callable):
|
|
157
|
+
Optionally specifies a function that will be called on every
|
|
158
|
+
sampleblock. The callback should take one argument: a dict
|
|
159
|
+
(let's call it `d`) and should return a boolean value that
|
|
160
|
+
dictates whether the sampleblock should be kept. This decision
|
|
161
|
+
might be made based on `d['NumberOfBlocksProcessed']` and the
|
|
162
|
+
contents of `d['States']` and `d['Changed']`. The callback
|
|
163
|
+
will also be called one additional final time when processing
|
|
164
|
+
is completed. On this call, `d['Changed']` will be empty (on
|
|
165
|
+
all previous calls, it will contain `'States.SourceTime'` at
|
|
166
|
+
the very least, and usually also `'Signal'`).
|
|
167
|
+
|
|
168
|
+
If this is `None`, `bci_stream2mat` is used at the end of the
|
|
169
|
+
chain. If not, `bci_stream2hybrid` is used.
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
Examples:
|
|
173
|
+
|
|
174
|
+
Excuse the relative paths - this example works if you're currently
|
|
175
|
+
working in the root directory of the BCI2000 distro::
|
|
176
|
+
|
|
177
|
+
bci2000chain( datfile='data/samplefiles/eeg3_2.dat',
|
|
178
|
+
chain='TransmissionFilter|SpatialFilter|ARFilter',
|
|
179
|
+
binpath='tools/cmdline',
|
|
180
|
+
parms=[ 'tools/matlab/ExampleParameters.prm' ],
|
|
181
|
+
SpatialFilterType=3 )
|
|
182
|
+
|
|
183
|
+
Or for more portable operation, you can use `bci2000root()` and
|
|
184
|
+
`bci2000path()`::
|
|
185
|
+
|
|
186
|
+
bci2000root( '/PATH/TO/BCI2000' )
|
|
187
|
+
|
|
188
|
+
bci2000chain( datfile=bci2000path( 'data/samplefiles/eeg3_2.dat' ),
|
|
189
|
+
chain='TransmissionFilter|SpatialFilter|ARFilter',
|
|
190
|
+
parms=[ bci2000path( 'tools/matlab/ExampleParameters.prm' ) ],
|
|
191
|
+
SpatialFilterType=3 )
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
if verbose: verbosityFlag = '-v'
|
|
196
|
+
else: verbosityFlag = '-q'
|
|
197
|
+
|
|
198
|
+
if isinstance(chain, basestring):
|
|
199
|
+
if chain.lower() == 'SpectralSignalProcessing'.lower():
|
|
200
|
+
chain = 'TransmissionFilter|SpatialFilter|SpectralEstimator|LinearClassifier|LPFilter|ExpressionFilter|Normalizer'
|
|
201
|
+
elif chain.lower() == 'ARSignalProcessing'.lower():
|
|
202
|
+
chain = 'TransmissionFilter|SpatialFilter|ARFilter|LinearClassifier|LPFilter|ExpressionFilter|Normalizer'
|
|
203
|
+
elif chain.lower() == 'P3SignalProcessing'.lower():
|
|
204
|
+
chain = 'TransmissionFilter|SpatialFilter|P3TemporalFilter|LinearClassifier'
|
|
205
|
+
chain = chain.split('|')
|
|
206
|
+
chain = [c.strip() for c in chain if len(c.strip())]
|
|
207
|
+
|
|
208
|
+
if len(chain) == 0: print('WARNING: chain is empty')
|
|
209
|
+
|
|
210
|
+
if start is not None and len(str(start).strip()): start = ' --start=' + str(start).replace(' ', '')
|
|
211
|
+
else: start = ''
|
|
212
|
+
if duration is not None and len(str(duration).strip()): duration = ' --duration=' + str(duration).replace(' ', '')
|
|
213
|
+
else: duration = ''
|
|
214
|
+
|
|
215
|
+
if dims is None or str(dims).lower() == 'auto': dims = 0
|
|
216
|
+
if dims not in (0, 2, 3): raise ValueError("dims must be 2, 3 or 'auto'")
|
|
217
|
+
|
|
218
|
+
out = CONTAINER()
|
|
219
|
+
out._summarize = 60
|
|
220
|
+
err = ''
|
|
221
|
+
|
|
222
|
+
cmd = ''
|
|
223
|
+
binaries = []
|
|
224
|
+
tmpdir = tempfile.mkdtemp(prefix='bci2000chain_')
|
|
225
|
+
|
|
226
|
+
tmpdatfile = os.path.join(tmpdir, 'in.dat')
|
|
227
|
+
prmfile_in = os.path.join(tmpdir, 'in.prm')
|
|
228
|
+
prmfile_out = os.path.join(tmpdir, 'out.prm')
|
|
229
|
+
matfile = os.path.join(tmpdir, 'out.mat')
|
|
230
|
+
bcifile = os.path.join(tmpdir, 'out.bci')
|
|
231
|
+
shfile = os.path.join(tmpdir, 'go.bat')
|
|
232
|
+
logfile = os.path.join(tmpdir, 'log.txt')
|
|
233
|
+
|
|
234
|
+
if not isinstance(datfile, basestring):
|
|
235
|
+
raise ValueError('datfile must be a filename') # TODO: if datfile contains the appropriate info, use some create_bcidat equivalent and do datfile = tmpdatfile
|
|
236
|
+
datfile = os.path.realpath( os.path.expanduser( datfile ) )
|
|
237
|
+
if not os.path.isfile(datfile): raise IOError('file not found: %s' % datfile)
|
|
238
|
+
|
|
239
|
+
mappings = {
|
|
240
|
+
'$DATFILE': datfile,
|
|
241
|
+
'$PRMFILE_IN': prmfile_in,
|
|
242
|
+
'$PRMFILE_OUT': prmfile_out,
|
|
243
|
+
'$MATFILE': matfile,
|
|
244
|
+
'$BCIFILE': bcifile,
|
|
245
|
+
'$SHFILE': shfile,
|
|
246
|
+
'$LOGFILE': logfile,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if binpath is None and bci2000root() is not None: binpath = 'tools/cmdline'
|
|
250
|
+
if binpath is None: raise ValueError( 'must either pre-configure `bci2000root()` or supply `binpath` explicitly' )
|
|
251
|
+
binpath = bci2000path( binpath )
|
|
252
|
+
exeExtension = bci2000_exe_extension('tools/cmdline')
|
|
253
|
+
def exe(name):
|
|
254
|
+
fullName = name + exeExtension
|
|
255
|
+
if exeExtension == '.app': fullName += '/Contents/MacOS/' + name
|
|
256
|
+
if binpath: return '"' + os.path.join(binpath, fullName) + '"'
|
|
257
|
+
else: return fullName
|
|
258
|
+
|
|
259
|
+
if parms is None: parms = []
|
|
260
|
+
if isinstance(parms, tuple): parms = list(parms)
|
|
261
|
+
if not isinstance(parms, list): parms = [parms]
|
|
262
|
+
else: parms = list(parms)
|
|
263
|
+
if len(kwargs): parms.append(kwargs)
|
|
264
|
+
|
|
265
|
+
def StartChain( target ): return ( ' "--input=%s"' if USE_INPUT_AND_OUTPUT_FLAGS else ' < "%s"' ) % target
|
|
266
|
+
def FinishChain( target ): return ( ' "--output=%s"' if USE_INPUT_AND_OUTPUT_FLAGS else ' > "%s"' ) % target
|
|
267
|
+
|
|
268
|
+
if parms is None or len(parms) == 0:
|
|
269
|
+
cmd += exe('bci_dat2stream') + start + duration + StartChain( '$DATFILE' )
|
|
270
|
+
binaries.append('bci_dat2stream')
|
|
271
|
+
else:
|
|
272
|
+
if verbose: print( '# writing custom parameter file %s' % prmfile_in )
|
|
273
|
+
try: parms = make_bciprm(verbosityFlag, datfile, parms, '>', prmfile_in)
|
|
274
|
+
except:
|
|
275
|
+
try: make_bciprm( verbosityFlag, datfile )
|
|
276
|
+
except: raise BCI2000ChainError( 'failed to decode BCI2000 parameters from ' + datfile )
|
|
277
|
+
raise
|
|
278
|
+
|
|
279
|
+
if DAT2STREAM_HAS_P_FLAG:
|
|
280
|
+
cmd += exe('bci_dat2stream') + ' "-p$PRMFILE_IN"' + start + duration + StartChain( '$DATFILE' ) # new-style bci_dat2stream with -p option
|
|
281
|
+
binaries.append('bci_dat2stream')
|
|
282
|
+
else:
|
|
283
|
+
if len(start) or len(duration):
|
|
284
|
+
raise ValueError('old versions of bci_dat2stream have no --start or --duration option')
|
|
285
|
+
cmd += '(' # old-style bci_dat2stream with no -p option
|
|
286
|
+
cmd += exe('bci_prm2stream') + ' < "$PRMFILE_IN"'
|
|
287
|
+
cmd += '&& ' + exe('bci_dat2stream') + ' --transmit-sd' + StartChain( '$DATFILE' )
|
|
288
|
+
cmd += ')'
|
|
289
|
+
binaries.append('bci_dat2stream')
|
|
290
|
+
binaries.append('bci_prm2stream')
|
|
291
|
+
|
|
292
|
+
for c in chain:
|
|
293
|
+
cmd += ' | ' + exe(c)
|
|
294
|
+
binaries.append(c)
|
|
295
|
+
|
|
296
|
+
if callback == 'default': callback = CallbackObject()
|
|
297
|
+
if callback:
|
|
298
|
+
exe_name = 'bci_stream2hybrid'
|
|
299
|
+
cmd += ' | ' + exe(exe_name)
|
|
300
|
+
binaries.append(exe_name)
|
|
301
|
+
elif STREAM2MAT_SAVES_PARMS:
|
|
302
|
+
cmd += ' | ' + exe('bci_stream2mat') + FinishChain( '$MATFILE' ) # new-style bci_stream2mat with Parms output
|
|
303
|
+
binaries.append('bci_stream2mat')
|
|
304
|
+
else:
|
|
305
|
+
cmd += FinishChain( '$BCIFILE' )
|
|
306
|
+
cmd += ' && ' + exe('bci_stream2mat') + StartChain( '$BCIFILE' ) + FinishChain( '$MATFILE' )
|
|
307
|
+
cmd += ' && ' + exe('bci_stream2prm') + StartChain( '$BCIFILE' ) + FinishChain( '$PRMFILE_OUT' ) # old-style bci_stream2mat without Parms output
|
|
308
|
+
binaries.append('bci_stream2mat')
|
|
309
|
+
binaries.append('bci_stream2prm')
|
|
310
|
+
|
|
311
|
+
for k,v in mappings.items(): cmd = cmd.replace(k, v)
|
|
312
|
+
|
|
313
|
+
win = sys.platform.lower().startswith('win')
|
|
314
|
+
if win: preamble, postscript = '@', ' %*'
|
|
315
|
+
else: preamble, postscript = '#!/bin/bash\n\n', ' "$@"'
|
|
316
|
+
fh = open(shfile, 'wt')
|
|
317
|
+
fh.write( preamble + cmd + postscript + '\n' )
|
|
318
|
+
fh.close()
|
|
319
|
+
if not win: os.chmod(shfile, 484) # rwxr--r--
|
|
320
|
+
|
|
321
|
+
failsig = 'Configuration Error: '
|
|
322
|
+
failsig = ' Error: ' # TODO: this used to be 'Configuration Error:' but that misses some errors.
|
|
323
|
+
# Hopefully the more general signature doesn't give us false positives...
|
|
324
|
+
# There has to be a better way of detecting failure...
|
|
325
|
+
def tidytext(x):
|
|
326
|
+
return x.strip().replace('\r\n', '\n').replace('\r', '\n')
|
|
327
|
+
def getoutput(cmd):
|
|
328
|
+
return getstatusoutput(cmd)[1]
|
|
329
|
+
def getstatusoutput(cmd): # simpler case, older implementation, used for chains that have no callback and use bci_stream2mat
|
|
330
|
+
try: import fcntl; SYS_STDOUT_FILENO = sys.stdout.fileno(); SYS_STDOUT_FL = fcntl.fcntl( SYS_STDOUT_FILENO, fcntl.F_GETFL ) # see %%%, below
|
|
331
|
+
except: SYS_STDOUT_FL = None
|
|
332
|
+
try:
|
|
333
|
+
pipe = os.popen(cmd + ' 2>&1', 'r')
|
|
334
|
+
output = pipe.read()
|
|
335
|
+
status = pipe.close()
|
|
336
|
+
if status is None: status = 0
|
|
337
|
+
return status,tidytext(output)
|
|
338
|
+
finally:
|
|
339
|
+
# os.popen() can put the file descriptor behind the pipe into non-blocking mode so it can poll the child’s output without
|
|
340
|
+
# hanging the UI. Unfortunately, on POSIX systems the O_NONBLOCK flag is inherited by every duplicate
|
|
341
|
+
# of that descriptor, including the original stdout that the REPL prints to. This means that os.popen() can break your stdout!
|
|
342
|
+
# It will work for short outputs, but will hang (or be caught by IPython which then throws a BlockingIOError) for longer outputs.
|
|
343
|
+
# Here is a workaround: (see also %%%, above)
|
|
344
|
+
if SYS_STDOUT_FL is not None: fcntl.fcntl( SYS_STDOUT_FILENO, fcntl.F_SETFL, SYS_STDOUT_FL ) # %%%
|
|
345
|
+
|
|
346
|
+
def process(cmd, callback, logfile): # block-by-block processing with a callback
|
|
347
|
+
def bytes2str(s): return s if str is bytes else s.decode( 'utf-8' )
|
|
348
|
+
def str2bytes(s): return s if str is bytes else s.encode( 'utf-8' )
|
|
349
|
+
emptyBytes = str2bytes( '' )
|
|
350
|
+
d = CONTAINER( Changed=[], Parameters=[], CollatedSignal=[], CollatedStates=CONTAINER(), CollatedEvents=CONTAINER(), RetainedBlockNumbers=[] )
|
|
351
|
+
try:
|
|
352
|
+
#pipe = os.popen( cmd + ' 2>"%s"' % logfile, 'r' ) # the problem with this is that it opens the output in text mode. unsure how to switch it to binary, so instead we'll use subprocess:
|
|
353
|
+
sp = subprocess.Popen( [ cmd ], shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE ); pipe = sp.stdout # text=False option does not exist in Python 2 but it seems to be fine in both if omitted
|
|
354
|
+
shape = None
|
|
355
|
+
class StandardErrorWatcher( object ):
|
|
356
|
+
def __init__( self, sp ):
|
|
357
|
+
self.sp = weakref.ref( sp )
|
|
358
|
+
self.stderr = sp.stderr
|
|
359
|
+
self.configFailed = False
|
|
360
|
+
self.configPassed = False
|
|
361
|
+
self.text = ''
|
|
362
|
+
self.keepgoing = True
|
|
363
|
+
self.thread = threading.Thread( target=self )
|
|
364
|
+
try: self.thread.daemon = True # last resort - even if the thread hangs around forever zombified, at least don't prevent Python from terminating
|
|
365
|
+
except: self.thread.setDaemon( True ) # older (now deprecated) syntax
|
|
366
|
+
self.thread.start()
|
|
367
|
+
def __call__( self ):
|
|
368
|
+
while self.keepgoing:
|
|
369
|
+
try: ready, _, _ = select.select( [ self.stderr ], [], [], 0.010 )
|
|
370
|
+
except: ready = [ self.stderr ] # fails on Python 2.7 under Windows with error message "An operation was attempted on something that is not a socket" - fall back on .setDaemon( True ) above
|
|
371
|
+
if not ready: continue # timed out: so, take a moment to check whether self.keepgoing has been unset, and if it hasn't then try again
|
|
372
|
+
try: line = self.stderr.read() if self.configFailed else self.stderr.readline()
|
|
373
|
+
except: break
|
|
374
|
+
if not line: break
|
|
375
|
+
if isinstance( line, bytes ): line = bytes2str( line )
|
|
376
|
+
#sys.stderr.write( 'STDERR: ' + line ); sys.stderr.flush()
|
|
377
|
+
if 'bci_stream2hybrid: CONFIGURATION ACCEPTED' in line: self.configPassed = True; continue
|
|
378
|
+
self.text += line
|
|
379
|
+
if failsig in line:
|
|
380
|
+
self.configFailed = True
|
|
381
|
+
try: self.sp().kill()
|
|
382
|
+
except: pass
|
|
383
|
+
break # without this, the thread will never end even after the chain has returned; unfortunately it means only the first error message is preserved
|
|
384
|
+
stderr = StandardErrorWatcher( sp )
|
|
385
|
+
try:
|
|
386
|
+
while not stderr.configPassed and not stderr.configFailed:
|
|
387
|
+
if sp.poll() is not None: stderr.configFailed = True
|
|
388
|
+
time.sleep( 0.001 )
|
|
389
|
+
terminator = None
|
|
390
|
+
while not stderr.configFailed and sp.poll() is None:
|
|
391
|
+
line = pipe.readline()
|
|
392
|
+
if not line: break
|
|
393
|
+
#sys.stderr.write( 'STDOUT: ' + bytes2str( line ) ); sys.stderr.flush()
|
|
394
|
+
line = line.strip()
|
|
395
|
+
if not line: continue
|
|
396
|
+
line = bytes2str( line )
|
|
397
|
+
if line == 'Parameter':
|
|
398
|
+
d[ 'Parameters' ].append( bytes2str( pipe.readline().strip() ) )
|
|
399
|
+
continue
|
|
400
|
+
words = line.split()
|
|
401
|
+
valueType = words.pop( -1 )
|
|
402
|
+
if valueType == ':': fmt = words.pop( -1 )
|
|
403
|
+
container = d
|
|
404
|
+
for key in words[ :-1 ]: container = container[ key ]
|
|
405
|
+
lastKey = words[ -1 ]
|
|
406
|
+
fullKey = '.'.join( words )
|
|
407
|
+
if valueType == ':':
|
|
408
|
+
raw = pipe.read( struct.calcsize( fmt ) )
|
|
409
|
+
#value = struct.unpack( fmt, raw ) # non-numpy-dependent version, decodes values as tuple
|
|
410
|
+
#value = numpy.fromfile( pipe, dtype=fmt[ -1 ], count=int( fmt[ 1:-1 ] ) ) # this looked like it might be a good way of going straight to numpy array from the filehandle - but it results in OSError: seeking file failed
|
|
411
|
+
value = numpy.frombuffer( raw, dtype=fmt[ -1 ] )
|
|
412
|
+
elif valueType == '=':
|
|
413
|
+
arg = bytes2str( pipe.readline().strip() )
|
|
414
|
+
value = ast.literal_eval( arg )
|
|
415
|
+
if isinstance( value, dict ) and isinstance( CONTAINER(), dict ): value = CONTAINER( value )
|
|
416
|
+
else:
|
|
417
|
+
raise RuntimeError( 'unrecognized valueType %r' % valueType )
|
|
418
|
+
if fullKey == 'Signal':
|
|
419
|
+
if shape is None: shape = ( d[ 'NumberOfElements' ], d[ 'NumberOfChannels' ] )
|
|
420
|
+
value = value.reshape( shape )
|
|
421
|
+
container[ lastKey ] = value
|
|
422
|
+
if fullKey == 'NumberOfBlocksProcessed' and value:
|
|
423
|
+
try:
|
|
424
|
+
keepBlock = callback( d )
|
|
425
|
+
except TerminateChain as exc:
|
|
426
|
+
keepBlock = exc.keepBlock
|
|
427
|
+
terminator = exc
|
|
428
|
+
if keepBlock:
|
|
429
|
+
d[ 'RetainedBlockNumbers' ].append( value - 1 )
|
|
430
|
+
d[ 'CollatedSignal' ].append( d[ 'Signal' ] ) # NB: inefficient for very large files, unless you write your callback such that it clears this field
|
|
431
|
+
for name, value in d[ 'States' ].items(): d[ 'CollatedStates' ].setdefault( name, [] ).append( value )
|
|
432
|
+
for name, value in d[ 'Events' ].items(): d[ 'CollatedEvents' ].setdefault( name, [] ).append( value.astype( 'int64' ) )
|
|
433
|
+
d[ 'Changed' ][ : ] = []
|
|
434
|
+
if terminator: raise terminator
|
|
435
|
+
else:
|
|
436
|
+
d[ 'Changed' ].append( fullKey )
|
|
437
|
+
#status = pipe.close()
|
|
438
|
+
status = sp.wait()
|
|
439
|
+
except KeyboardInterrupt:
|
|
440
|
+
pass
|
|
441
|
+
except TerminateChain:
|
|
442
|
+
pass
|
|
443
|
+
finally:
|
|
444
|
+
try: sp.kill()
|
|
445
|
+
except: pass
|
|
446
|
+
status = sp.returncode
|
|
447
|
+
if status == 0 or terminator: # only call the callback again if everything's OK
|
|
448
|
+
try: callback( d ) # d['Changed'] will be empty, to signal to the callback that it should wrap things up, if necessary
|
|
449
|
+
except TerminateChain: pass
|
|
450
|
+
stderr.keepgoing = False # some implementations don't need this; some do
|
|
451
|
+
return d, status, tidytext( stderr.text )
|
|
452
|
+
finally: # suppress ResourceWarning messages
|
|
453
|
+
try: sp.stderr.close()
|
|
454
|
+
except: pass
|
|
455
|
+
try: sp.stdout.close()
|
|
456
|
+
except: pass
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
notfound = [exe(binary) for binary in binaries if not os.path.isfile(exe(binary).strip('"'))]
|
|
460
|
+
if notfound: raise BCI2000ChainError( '\n'.join( [ 'failed to find the following command-line binaries:' ] + [ ' ' + binary for binary in notfound ] ) )
|
|
461
|
+
if verbose: print( '# querying version information' )
|
|
462
|
+
binaries = CONTAINER([(binary, getoutput(exe(binary) + ' --version').replace('\n', ' ') ) for binary in binaries])
|
|
463
|
+
|
|
464
|
+
if verbose: print( cmd )
|
|
465
|
+
t0 = time.time()
|
|
466
|
+
if callback: callbackResult, failed, output = process(shfile, callback, logfile)
|
|
467
|
+
else: failed, output = getstatusoutput(shfile)
|
|
468
|
+
chaintime = time.time() - t0
|
|
469
|
+
|
|
470
|
+
if failsig in output: failed = 1
|
|
471
|
+
printable_output = output
|
|
472
|
+
printable_lines = output.split('\n')
|
|
473
|
+
maxlines = 10
|
|
474
|
+
if len(printable_lines) > maxlines:
|
|
475
|
+
printable_output = '\n'.join(printable_lines[:maxlines] + ['[%d more lines omitted]' % (len(printable_lines) - maxlines)])
|
|
476
|
+
def AddContextToError( message ):
|
|
477
|
+
if verbose: return 'system call failed:\n' + message # cmd has already been printed, so don't clutter things further
|
|
478
|
+
else: return 'system call failed:\n\n%s\n\n%s' % ( cmd, message )
|
|
479
|
+
if failed: err = AddContextToError( printable_output )
|
|
480
|
+
|
|
481
|
+
if not err and not callback:
|
|
482
|
+
if verbose: print( '# loading %s' % matfile )
|
|
483
|
+
try:
|
|
484
|
+
mat = ReadMatFile(matfile, containerClass=CONTAINER)
|
|
485
|
+
except:
|
|
486
|
+
err = "The chain must have failed: could not load %s\nShell output was as follows:\n%s" % (matfile, printable_output)
|
|
487
|
+
else:
|
|
488
|
+
if 'Data' not in mat: err = "The chain must have failed: no 'Data' variable found in %s\nShell output was as follows:\n%s" % (matfile, printable_output)
|
|
489
|
+
if 'Index' not in mat: err = "The chain must have failed: no 'Index' variable found in %s\nShell output was as follows:\n%s" % (matfile, printable_output)
|
|
490
|
+
|
|
491
|
+
if not err:
|
|
492
|
+
out.FileName = datfile
|
|
493
|
+
if callback:
|
|
494
|
+
if verbose: print( '# decoding parameters loaded from callback result' )
|
|
495
|
+
parms = make_bciprm( verbosityFlag, callbackResult.get( 'Parameters', [] ) )
|
|
496
|
+
if not parms: err = AddContextToError( output )
|
|
497
|
+
elif STREAM2MAT_SAVES_PARMS:
|
|
498
|
+
if verbose: print( '# decoding parameters loaded from the mat-file' )
|
|
499
|
+
parms = make_bciprm( verbosityFlag, mat[ 'Parms' ][ 0 ] )
|
|
500
|
+
else:
|
|
501
|
+
if verbose: print( '# reading output parameter file' + prmfile_out )
|
|
502
|
+
parms = ParmList(prmfile_out) # if you get an error that prmfile_out does not exist, recompile your bci_dat2stream and bci_stream2mat binaries from up-to-date sources, and ensure that DAT2STREAM_HAS_P_FLAG and STREAM2MAT_SAVES_PARMS, at the top of this file, are both set to 1
|
|
503
|
+
|
|
504
|
+
if not err:
|
|
505
|
+
out.DateStr = read_bcidate(parms, 'ISO')
|
|
506
|
+
out.DateNum = read_bcidate(parms)
|
|
507
|
+
out.FilterChain = chain
|
|
508
|
+
out.ToolVersions = binaries
|
|
509
|
+
out.ShellInput = cmd
|
|
510
|
+
out.ShellOutput = output
|
|
511
|
+
out.ChainTime = chaintime
|
|
512
|
+
out.ChainSpeedFactor = None
|
|
513
|
+
out.Megabytes = None
|
|
514
|
+
out.Parms = parms
|
|
515
|
+
out.Parms.sort( key=lambda p: p.Name )
|
|
516
|
+
|
|
517
|
+
if callback:
|
|
518
|
+
nChannels = callbackResult.get( 'NumberOfChannels', None )
|
|
519
|
+
nElements = callbackResult.get( 'NumberOfElements', None )
|
|
520
|
+
nBlocksProcessed = callbackResult.get( 'NumberOfBlocksProcessed', None )
|
|
521
|
+
nBlocksRetained = len( callbackResult.get( 'CollatedSignal', [] ) )
|
|
522
|
+
mat = callbackResult
|
|
523
|
+
else:
|
|
524
|
+
mat['Index'] = mat['Index'][0,0]
|
|
525
|
+
sigind = mat['Index'].Signal - 1 # indices vary across channels fastest, then elements
|
|
526
|
+
nChannels,nElements = sigind.shape
|
|
527
|
+
nBlocksRetained = nBlocksProcessed = mat['Data'].shape[1]
|
|
528
|
+
|
|
529
|
+
out.BlocksProcessed = nBlocksProcessed
|
|
530
|
+
out.BlocksRetained = nBlocksRetained
|
|
531
|
+
out.BlocksPerSecond = float(parms.SamplingRate.ScaledValue) / float(parms.SampleBlockSize.ScaledValue)
|
|
532
|
+
out.SecondsPerBlock = float(parms.SampleBlockSize.ScaledValue) / float(parms.SamplingRate.ScaledValue)
|
|
533
|
+
out.ChainSpeedFactor = float(out.BlocksProcessed * out.SecondsPerBlock) / float(out.ChainTime)
|
|
534
|
+
|
|
535
|
+
def unnumpify(x):
|
|
536
|
+
while isinstance(x, numpy.ndarray) and x.size == 1: x = x[0]
|
|
537
|
+
if isinstance(x, (numpy.ndarray,tuple,list)): x = [unnumpify(xi) for xi in x]
|
|
538
|
+
return x
|
|
539
|
+
|
|
540
|
+
out.Channels = nChannels
|
|
541
|
+
out.ChannelLabels = unnumpify(mat.get('ChannelLabels', []))
|
|
542
|
+
if ChannelSet: out.ChannelSet = ChannelSet(out.ChannelLabels)
|
|
543
|
+
else: out.ChannelSet = None
|
|
544
|
+
|
|
545
|
+
out.Elements = nElements
|
|
546
|
+
out.ElementLabels = unnumpify(mat.get('ElementLabels', []))
|
|
547
|
+
out.ElementValues = numpy.ravel(mat.get('ElementValues', []))
|
|
548
|
+
out.ElementUnit = unnumpify(mat.get('ElementUnit', None))
|
|
549
|
+
out.ElementRate = out.BlocksPerSecond * out.Elements
|
|
550
|
+
|
|
551
|
+
out.Time = out.SecondsPerBlock * ( numpy.array( callbackResult[ 'RetainedBlockNumbers' ] ) if callback else numpy.arange(0, nBlocksProcessed) )
|
|
552
|
+
out.FullTime = out.Time
|
|
553
|
+
out.FullElementValues = out.ElementValues
|
|
554
|
+
|
|
555
|
+
def seconds(s): # -1 means "no information", 0 means "not units of time", >0 means the scaling factor
|
|
556
|
+
if getattr(s, 'ElementUnit', None) in ('',None,()) or s.ElementUnit == []: return -1
|
|
557
|
+
s = s.ElementUnit
|
|
558
|
+
if s.endswith('seconds'): s = s[:-6]
|
|
559
|
+
elif s.endswith('second'): s = s[:-5]
|
|
560
|
+
elif s.endswith('sec'): s = s[:-2]
|
|
561
|
+
if s.endswith('s'): return { 'ps': 1e-12, 'ns': 1e-9, 'us': 1e-6, 'mus': 1e-6, 'ms': 1e-3, 's' : 1e+0, 'ks': 1e+3, 'Ms': 1e+6, 'Gs': 1e+9, 'Ts': 1e+12,}.get(s, 0)
|
|
562
|
+
else: return 0
|
|
563
|
+
|
|
564
|
+
if dims == 0: # dimensionality has not been specified explicitly: so guess, based on ElementUnit and/or filter name
|
|
565
|
+
# 3-dimensional output makes more sense than continuous 2-D whenever "elements" can't just be concatenated into an unbroken time-stream
|
|
566
|
+
if len(chain): lastfilter = chain[-1].lower()
|
|
567
|
+
else: lastfilter = ''
|
|
568
|
+
if nBlocksRetained != nBlocksProcessed:
|
|
569
|
+
dims = 3
|
|
570
|
+
elif lastfilter in [ 'p3temporalfilter', 'trapfilter' ]:
|
|
571
|
+
dims = 3
|
|
572
|
+
else:
|
|
573
|
+
factor = seconds(out)
|
|
574
|
+
if factor > 0: # units of time. TODO: could detect whether the out.ElementValues*factor are (close enough to) contiguous from block to block; then p3temporalfilter and trapfilter wouldn't have to be special cases above
|
|
575
|
+
dims = 2
|
|
576
|
+
elif factor == 0: # not units of time: use 3D by default
|
|
577
|
+
dims = 3
|
|
578
|
+
elif lastfilter in ['trapfilter', 'p3temporalfilter', 'arfilter', 'fftfilter', 'coherencefilter', 'coherencefftfilter']: # no ElementUnit info? guess based on filter name
|
|
579
|
+
dims = 3
|
|
580
|
+
else:
|
|
581
|
+
dims = 2
|
|
582
|
+
|
|
583
|
+
if dims == 2:
|
|
584
|
+
out.FullTime = numpy.repeat(out.Time, nElements) # but this means time is standing still during a block...
|
|
585
|
+
factor = seconds(out)
|
|
586
|
+
if len(out.ElementValues):
|
|
587
|
+
out.FullElementValues = numpy.tile(out.ElementValues, nBlocksRetained)
|
|
588
|
+
if factor > 0: out.FullTime = out.FullTime + out.FullElementValues * factor # ...so here we fix that if we can
|
|
589
|
+
|
|
590
|
+
if callback:
|
|
591
|
+
collatedSignal = callbackResult.get( 'CollatedSignal', [] )
|
|
592
|
+
collatedStates = CONTAINER( { name : numpy.array( values ) for name, values in callbackResult.get( 'CollatedStates', {} ).items() } )
|
|
593
|
+
collatedEvents = CONTAINER( { name : numpy.concatenate( values, axis=0 ) for name, values in callbackResult.get( 'CollatedEvents', {} ).items() } )
|
|
594
|
+
if nBlocksRetained == 0:
|
|
595
|
+
out.Signal = numpy.zeros( ( 0, nChannels, nElements ) if dims == 3 else ( 0, nChannels ), dtype='f' )
|
|
596
|
+
out.States = CONTAINER( { name : numpy.array( [], dtype='L' ) for name in callbackResult.get( 'States', {} ) } )
|
|
597
|
+
out.Events = CONTAINER( { name : numpy.array( [], dtype='L' ) for name in callbackResult.get( 'Events', {} ) } )
|
|
598
|
+
elif dims == 3:
|
|
599
|
+
out.Signal = numpy.array( collatedSignal ) # nBlocksRetained - by - nElements - by - nChannels
|
|
600
|
+
out.Signal = numpy.transpose(out.Signal, (0,2,1)) # nBlocksRetained - by - nChannels - by - nElements
|
|
601
|
+
out.Signal = out.Signal + 0.0 # make a contiguous copy
|
|
602
|
+
out.States = collatedStates
|
|
603
|
+
out.Events = collatedEvents
|
|
604
|
+
elif dims == 2:
|
|
605
|
+
out.Signal = numpy.concatenate( collatedSignal, axis=0 ) # nBlocksRetained*nElements - by - nChannels
|
|
606
|
+
# out.States = CONTAINER( { name : value.repeat( nElements ) for name, value in collatedStates.items() } )
|
|
607
|
+
# The line above is what we *would* do if the bci_dat2stream protocol was able to distinguish between event- and non-event states.
|
|
608
|
+
# Unfortunately neither the .dat file nor the protocol is able to make this distinction, for backward compatibility reasons.
|
|
609
|
+
# So bci_stream2hybrid transfers *both* scalar and vector-covering-entire-sampleblock representations of *all* states
|
|
610
|
+
# (see the comments marked @@@ in bci_stream2hybrid.cpp). Therefore, the right thing to do is simply:
|
|
611
|
+
out.States = collatedEvents # <- because the "event" representation has lost less information (or no information, if the sampleblock size hasn't changed during the chain)
|
|
612
|
+
out.Events = collatedEvents # <- this we would do in any case
|
|
613
|
+
else:
|
|
614
|
+
raise RuntimeError('internal error')
|
|
615
|
+
# TODO: events might need their own (possibly original-sampleclock-based) timebase, and it will have discontinuities if some blocks were not retained
|
|
616
|
+
|
|
617
|
+
else:
|
|
618
|
+
# Why does sigind have to be transposed before vectorizing to achieve the same result as sigind(:) WITHOUT a transpose in Matlab?
|
|
619
|
+
# This will probably forever be one of the many deep mysteries of numpy dimensionality handling
|
|
620
|
+
out.Signal = mat['Data'][sigind.T.ravel(), :] # nChannels*nElements - by - nBlocks
|
|
621
|
+
out.Signal = out.Signal + 0.0 # make a contiguous copy
|
|
622
|
+
|
|
623
|
+
if dims == 3:
|
|
624
|
+
out.Signal = numpy.reshape(out.Signal, (nChannels, nElements, nBlocksRetained), order='F') # nChannels - by - nElements - by - nBlocksRetained
|
|
625
|
+
out.Signal = numpy.transpose(out.Signal, (2,0,1)) # nBlocksRetained - by - nChannels - by - nElements
|
|
626
|
+
out.Signal = out.Signal + 0.0 # make a contiguous copy
|
|
627
|
+
elif dims == 2:
|
|
628
|
+
out.Signal = numpy.reshape(out.Signal, (nChannels, nElements*nBlocksRetained), order='F') # nChannels - by - nSamples
|
|
629
|
+
out.Signal = numpy.transpose(out.Signal, (1,0)) # nSamples - by - nChannels
|
|
630
|
+
out.Signal = out.Signal + 0.0 # make a contiguous copy
|
|
631
|
+
else:
|
|
632
|
+
raise RuntimeError('internal error')
|
|
633
|
+
|
|
634
|
+
out.States = CONTAINER()
|
|
635
|
+
try: fieldnames = mat['Index']._fieldnames
|
|
636
|
+
except AttributeError: items = mat['Index'].items()
|
|
637
|
+
else: items = [ ( k, getattr(mat['Index'], k) ) for k in fieldnames ]
|
|
638
|
+
states = [(k,int(v)-1) for k, v in items if k != 'Signal']
|
|
639
|
+
for k,v in states: setattr(out.States, k, mat['Data'][v,:])
|
|
640
|
+
# bci_stream2mat.cpp does not handle Event states
|
|
641
|
+
|
|
642
|
+
try: out.Megabytes = megs(out)
|
|
643
|
+
except: out.Megabytes = sys.exc_info()
|
|
644
|
+
def NotifyCleanup( message ): print( message )
|
|
645
|
+
else: # non-empty err
|
|
646
|
+
#print( err ); print( '' )
|
|
647
|
+
out = [ err + '\n\n' ]
|
|
648
|
+
keep = True
|
|
649
|
+
def NotifyCleanup( message ): out[ 0 ] += message + '\n'
|
|
650
|
+
|
|
651
|
+
if os.path.isdir(tmpdir):
|
|
652
|
+
files = sorted([os.path.join(tmpdir, file) for file in os.listdir(tmpdir) if file not in ['.','..']])
|
|
653
|
+
if keep: NotifyCleanup( 'The following commands should be executed to clean up temporary files:' )
|
|
654
|
+
elif verbose: print( '# removing temp files and directory ' + tmpdir )
|
|
655
|
+
|
|
656
|
+
for file in files:
|
|
657
|
+
if keep: NotifyCleanup( "os.remove(r'%s')" % file )
|
|
658
|
+
else:
|
|
659
|
+
try: os.remove(file)
|
|
660
|
+
except Exception as err: sys.stderr.write( "failed to remove %s:\n %s\n" % ( file, str( err ) ) )
|
|
661
|
+
|
|
662
|
+
if keep: NotifyCleanup( "os.rmdir(r'%s')" % tmpdir )
|
|
663
|
+
else:
|
|
664
|
+
try: os.rmdir(tmpdir)
|
|
665
|
+
except Exception as err: sys.stderr.write( "failed to remove %s:\n %s" % ( tmpdir, str( err ) ) )
|
|
666
|
+
if keep: NotifyCleanup( "" )
|
|
667
|
+
|
|
668
|
+
if isinstance( out, list ): raise BCI2000ChainError( out[ 0 ].strip() )
|
|
669
|
+
return out
|
|
670
|
+
|
|
671
|
+
def megs(x, rec=0):
|
|
672
|
+
"""
|
|
673
|
+
Very rough calculation of the number of megabytes occupied by an object and its children.
|
|
674
|
+
Note that some of the children (and hence the memory) may be shared by other objects.
|
|
675
|
+
Note also that mutual references by objects to each other will screw things up.
|
|
676
|
+
"""
|
|
677
|
+
rec += 1
|
|
678
|
+
if rec > 100: raise ValueError( 'megs failed (too much recursion)' )
|
|
679
|
+
denom = 1024.0 * 1024.0
|
|
680
|
+
def siz(x, rec):
|
|
681
|
+
if hasattr(sys, 'getsizeof'): m = sys.getsizeof(x) / denom # only the container overhead
|
|
682
|
+
else: # Python versions <2.6 do not have sys.getsizeof. Container overhead will be underestimated.
|
|
683
|
+
try: m = numpy.dtype( x.__class__ ).itemsize
|
|
684
|
+
except: m = 0
|
|
685
|
+
if hasattr(x, '__dict__'): m += megs(x.__dict__, rec) # works for sstruct and Param objects very nicely; should work for most old- and new-type objects
|
|
686
|
+
return m
|
|
687
|
+
if isinstance(x, dict): return siz(x, rec) + sum( megs( key, rec ) + megs( value, rec ) for key, value in x.items() )
|
|
688
|
+
if isinstance(x, numpy.ndarray):
|
|
689
|
+
m = siz( x, rec )
|
|
690
|
+
if m < x.nbytes / denom: m += x.nbytes / denom # depending on Python and/or numpy version, x.nbytes (size of raw packed content) may or may not be included in the unadjusted siz() output
|
|
691
|
+
return m
|
|
692
|
+
if isinstance(x, basestring ): return siz(x, rec)
|
|
693
|
+
try: # try treating it as a sequence of any kind
|
|
694
|
+
for xi in x: break
|
|
695
|
+
except: pass
|
|
696
|
+
else: return siz( x, rec ) + sum( [ megs( xi, rec ) for xi in x ] )
|
|
697
|
+
return siz(x, rec) # other esoteric types may not be covered - numpy.ndarray, for example, has no __dict__ and requires special handling, and perhaps there are others like it out there
|
|
698
|
+
|
|
699
|
+
class sys_dot_stderr: pass
|
|
700
|
+
|
|
701
|
+
class CallbackObject( object ):
|
|
702
|
+
def __init__( self, stream=sys_dot_stderr, announcementPeriodInSeconds=2, debug=False, **counters ):
|
|
703
|
+
if stream is sys_dot_stderr: stream = sys.stderr # defer the actual dereferencing until now, in case someone has been messing with it
|
|
704
|
+
self.counters = { 'StimulusCode' : 'StimulusCode events', 'TrialsCompleted' : 'trials' }
|
|
705
|
+
self.counters.update( counters )
|
|
706
|
+
self.counters = { k : v for k, v in self.counters.items() if v is not None }
|
|
707
|
+
self.counts = {}
|
|
708
|
+
self.lastValues = {}
|
|
709
|
+
self.stream = stream
|
|
710
|
+
self.announcementPeriodInSeconds = announcementPeriodInSeconds
|
|
711
|
+
self.debug = debug
|
|
712
|
+
def set( self, **kwargs ):
|
|
713
|
+
for k, v in kwargs.items(): setattr( self, k, v )
|
|
714
|
+
return self
|
|
715
|
+
def write( self, x ):
|
|
716
|
+
self.stream.write( x )
|
|
717
|
+
try: self.stream.flush()
|
|
718
|
+
except: pass
|
|
719
|
+
def __call__( self, d ):
|
|
720
|
+
if self.debug: sys._debug_bci2000chain = d
|
|
721
|
+
changed = d.get( 'Changed', [] )
|
|
722
|
+
numberOfBlocksProcessed = d.get( 'NumberOfBlocksProcessed', 0 )
|
|
723
|
+
if changed and numberOfBlocksProcessed > 1:
|
|
724
|
+
for stateName in self.counters:
|
|
725
|
+
values = d[ 'Events' ].get( stateName, None )
|
|
726
|
+
if values is None: continue
|
|
727
|
+
if values.size == 0: values = d[ 'States' ].get( stateName, None )
|
|
728
|
+
if values is None: continue
|
|
729
|
+
values = numpy.asarray( values ).ravel()
|
|
730
|
+
nEvents = numpy.logical_and( pdiff( values ) != 0, values != 0 ).sum()
|
|
731
|
+
if values[ 0 ] not in [ 0, self.lastValues.get( stateName, 0 ) ]: nEvents += 1
|
|
732
|
+
if nEvents: self.counts[ stateName ] = self.counts.setdefault( stateName, 0 ) + nEvents
|
|
733
|
+
self.lastValues[ stateName ] = values[ -1 ]
|
|
734
|
+
t = time.time()
|
|
735
|
+
secondsElapsed = t - d.setdefault( 'ProcessingStartTime', t )
|
|
736
|
+
if self.debug: d.setdefault( 'SecondsElapsedAtCalls', [] ).append( secondsElapsed )
|
|
737
|
+
#self.write( '%s %s %s\n' % ( numberOfBlocksProcessed, bool(changed), self.announcementPeriodInSeconds + d.setdefault( 'LastAnnouncementTime', t ) - t ) )
|
|
738
|
+
if numberOfBlocksProcessed == 0: return False
|
|
739
|
+
if not changed or t >= self.announcementPeriodInSeconds + d.setdefault( 'LastAnnouncementTime', t ):
|
|
740
|
+
secondsProcessed = numberOfBlocksProcessed * d[ 'SamplesPerBlock' ] / float( d[ 'SamplesPerSecond' ] )
|
|
741
|
+
eventsString = ', '.join( self.counters[ stateName ] + ': ' + str( nEvents ) for stateName, nEvents in self.counts.items() )
|
|
742
|
+
if eventsString: eventsString = ' - ' + eventsString
|
|
743
|
+
self.write( 'processed %5.1f seconds of signal in %4.1fs%s\n' % ( secondsProcessed, secondsElapsed, eventsString ) )
|
|
744
|
+
d[ 'LastAnnouncementTime' ] = t
|
|
745
|
+
return self.KeepBlock( d )
|
|
746
|
+
def KeepBlock( self, d ):
|
|
747
|
+
# to terminate the chain early: raise TerminateChain(keepBlock=...)
|
|
748
|
+
return True
|
|
749
|
+
|
|
750
|
+
class TrapFilterCallbackObject( CallbackObject ):
|
|
751
|
+
def KeepBlock( self, d ):
|
|
752
|
+
# to terminate the chain early: raise TerminateChain(keepBlock=...)
|
|
753
|
+
isNewTrial = d.NumberOfBlocksProcessed > 1 and 'States.TrialsCompleted' in d.Changed and d.States.TrialsCompleted > 0
|
|
754
|
+
return isNewTrial
|
|
755
|
+
|
|
756
|
+
exeExtension = bci2000_exe_extension('tools/cmdline')
|
|
757
|
+
if not os.path.isdir(bci2000path('tools/cmdline')):
|
|
758
|
+
bci2000_tools_cmdline_not_found = \
|
|
759
|
+
'Found no directory %s, so the %s module will not work unless you explicitly set `bci2000root()` ' \
|
|
760
|
+
'(NB: %s is being imported from %s - this warning can be avoided if the BCI2000Tools package is ' \
|
|
761
|
+
'installed as "editable", with `python -m pip install -e`, from its original location as part of ' \
|
|
762
|
+
'a full BCI2000 distribution).' % ( bci2000path('tools/cmdline'), __name__, __name__, __file__ )
|
|
763
|
+
warnings.warn(bci2000_tools_cmdline_not_found)
|
|
764
|
+
elif not os.path.exists(bci2000path('tools/cmdline/bci_dat2stream' + exeExtension)):
|
|
765
|
+
bci_dat2stream_not_found = \
|
|
766
|
+
'%s found directory %s, but did not find the bci_dat2stream%s executable inside it. ' \
|
|
767
|
+
'You may need to set a different `bci2000root()`, or you may need to compile the ' \
|
|
768
|
+
'BCI2000 command-line binaries in the current one.' % ( __name__, bci2000path('tools/cmdline'), exeExtension )
|
|
769
|
+
warnings.warn(bci_dat2stream_not_found)
|
|
770
|
+
elif not os.path.exists(bci2000path('tools/cmdline/bci_stream2hybrid' + exeExtension)):
|
|
771
|
+
bci_stream2hybrid_not_found = \
|
|
772
|
+
'%s found directory %s, but did not find the bci_stream2hybrid%s executable inside it. ' \
|
|
773
|
+
'You may need to set a different `bci2000root()`, or you may need to compile the ' \
|
|
774
|
+
'BCI2000 command-line binaries in the current one.' % ( __name__, bci2000path('tools/cmdline'), exeExtension )
|
|
775
|
+
warnings.warn(bci_stream2hybrid_not_found)
|