From 050a23f11b9018ad35f939403dae60bab5978b09 Mon Sep 17 00:00:00 2001 From: LMBooth Date: Sat, 2 Sep 2023 20:08:36 +0100 Subject: [PATCH] pseudodevice - alpha pybci pseudodevice creation --- .../lib/pybci/Configuration/EpochSettings.py | 13 + .../pybci/Configuration/FeatureSettings.py | 15 + .../Configuration/PsuedoDeviceSettings.py | 0 build/lib/pybci/Configuration/__init__.py | 3 + .../ThreadClasses/AsyncDataReceiverThread.py | 126 +++++++ .../pybci/ThreadClasses/ClassifierThread.py | 111 ++++++ .../pybci/ThreadClasses/DataReceiverThread.py | 99 +++++ .../ThreadClasses/FeatureProcessorThread.py | 64 ++++ build/lib/pybci/ThreadClasses/MarkerThread.py | 28 ++ .../OptimisedDataReceiverThread.py | 139 +++++++ build/lib/pybci/ThreadClasses/__init__.py | 0 build/lib/pybci/Utils/Classifier.py | 120 ++++++ build/lib/pybci/Utils/FeatureExtractor.py | 141 +++++++ build/lib/pybci/Utils/LSLScanner.py | 106 ++++++ build/lib/pybci/Utils/Logger.py | 32 ++ build/lib/pybci/Utils/PseudoDevice.py | 130 +++++++ build/lib/pybci/Utils/__init__.py | 4 + build/lib/pybci/__init__.py | 2 + build/lib/pybci/pybci.py | 348 ++++++++++++++++++ build/lib/pybci/version.py | 1 + dist/install_pybci-1.0.0-py3.11.egg | Bin 0 -> 76776 bytes dist/install_pybci-1.0.2-py3.11.egg | Bin 0 -> 76913 bytes install_pybci.egg-info/PKG-INFO | 102 +++++ install_pybci.egg-info/SOURCES.txt | 29 ++ install_pybci.egg-info/dependency_links.txt | 1 + install_pybci.egg-info/requires.txt | 7 + install_pybci.egg-info/top_level.txt | 1 + pybci/Configuration/FeatureSettings.py | 8 +- pybci/Configuration/PseudoDeviceSettings.py | 18 + .../__pycache__/EpochSettings.cpython-311.pyc | Bin 0 -> 788 bytes .../FeatureSettings.cpython-311.pyc | Bin 0 -> 702 bytes .../PseudoDeviceSettings.cpython-311.pyc | Bin 0 -> 1087 bytes .../PsuedoDeviceSettings.cpython-311.pyc | Bin 0 -> 183 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 171 bytes .../README.md | 0 .../mainSend.py | 0 .../PseudoLSLStreamGenerator/pseudoEMG.py | 36 ++ pybci/Examples/asyncTest.py | 24 -- .../AsyncDataReceiverThread.cpython-311.pyc | Bin 0 -> 9961 bytes .../ClassifierThread.cpython-311.pyc | Bin 0 -> 7519 bytes .../DataReceiverThread.cpython-311.pyc | Bin 0 -> 9969 bytes .../FeatureProcessorThread.cpython-311.pyc | Bin 0 -> 5297 bytes .../__pycache__/MarkerThread.cpython-311.pyc | Bin 0 -> 2052 bytes ...ptimisedDataReceiverThread.cpython-311.pyc | Bin 0 -> 9777 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 171 bytes pybci/Utils/Logger.py | 47 ++- pybci/Utils/PseudoDevice.py | 278 ++++++++++++++ .../__pycache__/Classifier.cpython-311.pyc | Bin 0 -> 7476 bytes .../FeatureExtractor.cpython-311.pyc | Bin 0 -> 10333 bytes .../__pycache__/LSLScanner.cpython-311.pyc | Bin 0 -> 7837 bytes .../Utils/__pycache__/Logger.cpython-311.pyc | Bin 0 -> 3528 bytes .../__pycache__/PseudoDevice.cpython-311.pyc | Bin 0 -> 17244 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 163 bytes pybci/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 212 bytes pybci/__pycache__/pybci.cpython-311.pyc | Bin 0 -> 20556 bytes pybci/version.py | 2 +- 56 files changed, 1994 insertions(+), 41 deletions(-) create mode 100644 build/lib/pybci/Configuration/EpochSettings.py create mode 100644 build/lib/pybci/Configuration/FeatureSettings.py create mode 100644 build/lib/pybci/Configuration/PsuedoDeviceSettings.py create mode 100644 build/lib/pybci/Configuration/__init__.py create mode 100644 build/lib/pybci/ThreadClasses/AsyncDataReceiverThread.py create mode 100644 build/lib/pybci/ThreadClasses/ClassifierThread.py create mode 100644 build/lib/pybci/ThreadClasses/DataReceiverThread.py create mode 100644 build/lib/pybci/ThreadClasses/FeatureProcessorThread.py create mode 100644 build/lib/pybci/ThreadClasses/MarkerThread.py create mode 100644 build/lib/pybci/ThreadClasses/OptimisedDataReceiverThread.py create mode 100644 build/lib/pybci/ThreadClasses/__init__.py create mode 100644 build/lib/pybci/Utils/Classifier.py create mode 100644 build/lib/pybci/Utils/FeatureExtractor.py create mode 100644 build/lib/pybci/Utils/LSLScanner.py create mode 100644 build/lib/pybci/Utils/Logger.py create mode 100644 build/lib/pybci/Utils/PseudoDevice.py create mode 100644 build/lib/pybci/Utils/__init__.py create mode 100644 build/lib/pybci/__init__.py create mode 100644 build/lib/pybci/pybci.py create mode 100644 build/lib/pybci/version.py create mode 100644 dist/install_pybci-1.0.0-py3.11.egg create mode 100644 dist/install_pybci-1.0.2-py3.11.egg create mode 100644 install_pybci.egg-info/PKG-INFO create mode 100644 install_pybci.egg-info/SOURCES.txt create mode 100644 install_pybci.egg-info/dependency_links.txt create mode 100644 install_pybci.egg-info/requires.txt create mode 100644 install_pybci.egg-info/top_level.txt create mode 100644 pybci/Configuration/PseudoDeviceSettings.py create mode 100644 pybci/Configuration/__pycache__/EpochSettings.cpython-311.pyc create mode 100644 pybci/Configuration/__pycache__/FeatureSettings.cpython-311.pyc create mode 100644 pybci/Configuration/__pycache__/PseudoDeviceSettings.cpython-311.pyc create mode 100644 pybci/Configuration/__pycache__/PsuedoDeviceSettings.cpython-311.pyc create mode 100644 pybci/Configuration/__pycache__/__init__.cpython-311.pyc rename pybci/Examples/{PsuedoLSLStreamGenerator => PseudoLSLStreamGenerator}/README.md (100%) rename pybci/Examples/{PsuedoLSLStreamGenerator => PseudoLSLStreamGenerator}/mainSend.py (100%) create mode 100644 pybci/Examples/PseudoLSLStreamGenerator/pseudoEMG.py delete mode 100644 pybci/Examples/asyncTest.py create mode 100644 pybci/ThreadClasses/__pycache__/AsyncDataReceiverThread.cpython-311.pyc create mode 100644 pybci/ThreadClasses/__pycache__/ClassifierThread.cpython-311.pyc create mode 100644 pybci/ThreadClasses/__pycache__/DataReceiverThread.cpython-311.pyc create mode 100644 pybci/ThreadClasses/__pycache__/FeatureProcessorThread.cpython-311.pyc create mode 100644 pybci/ThreadClasses/__pycache__/MarkerThread.cpython-311.pyc create mode 100644 pybci/ThreadClasses/__pycache__/OptimisedDataReceiverThread.cpython-311.pyc create mode 100644 pybci/ThreadClasses/__pycache__/__init__.cpython-311.pyc create mode 100644 pybci/Utils/PseudoDevice.py create mode 100644 pybci/Utils/__pycache__/Classifier.cpython-311.pyc create mode 100644 pybci/Utils/__pycache__/FeatureExtractor.cpython-311.pyc create mode 100644 pybci/Utils/__pycache__/LSLScanner.cpython-311.pyc create mode 100644 pybci/Utils/__pycache__/Logger.cpython-311.pyc create mode 100644 pybci/Utils/__pycache__/PseudoDevice.cpython-311.pyc create mode 100644 pybci/Utils/__pycache__/__init__.cpython-311.pyc create mode 100644 pybci/__pycache__/__init__.cpython-311.pyc create mode 100644 pybci/__pycache__/pybci.cpython-311.pyc diff --git a/build/lib/pybci/Configuration/EpochSettings.py b/build/lib/pybci/Configuration/EpochSettings.py new file mode 100644 index 0000000..6823710 --- /dev/null +++ b/build/lib/pybci/Configuration/EpochSettings.py @@ -0,0 +1,13 @@ +class GlobalEpochSettings: + splitCheck = True # checks whether or not subdivide epochs + tmin = 0 # time in seconds to capture samples before trigger + tmax = 1 # time in seconds to capture samples after trigger + windowLength = 0.5 # if splitcheck true - time in seconds to split epoch + windowOverlap = 0.5 #if splitcheck true percentage value > 0 and < 1, example if epoch has tmin of 0 and tmax of 1 with window + # length of 0.5 we have 1 epoch between t 0 and t0.5 another at 0.25 to 0.75, 0.5 to 1 + +# customWindowSettings should be dict with marker name and IndividualEpochSetting +class IndividualEpochSetting: + splitCheck = True # checks whether or not subdivide epochs + tmin = 0 # time in seconds to capture samples before trigger + tmax= 1 # time in seconds to capture samples after trigger \ No newline at end of file diff --git a/build/lib/pybci/Configuration/FeatureSettings.py b/build/lib/pybci/Configuration/FeatureSettings.py new file mode 100644 index 0000000..f387cc7 --- /dev/null +++ b/build/lib/pybci/Configuration/FeatureSettings.py @@ -0,0 +1,15 @@ +class GeneralFeatureChoices: + psdBand = True + appr_entropy = False + perm_entropy = False + spec_entropy = False + svd_entropy = False + samp_entropy = False + rms = True + meanPSD = True + medianPSD = True + variance = True + meanAbs = True + waveformLength = False + zeroCross = False + slopeSignChange = False \ No newline at end of file diff --git a/build/lib/pybci/Configuration/PsuedoDeviceSettings.py b/build/lib/pybci/Configuration/PsuedoDeviceSettings.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/pybci/Configuration/__init__.py b/build/lib/pybci/Configuration/__init__.py new file mode 100644 index 0000000..f15ee6f --- /dev/null +++ b/build/lib/pybci/Configuration/__init__.py @@ -0,0 +1,3 @@ +#from .EpochSettings import EpochSettings +#from .GeneralFeatureChoices import GeneralFeatureChoices + diff --git a/build/lib/pybci/ThreadClasses/AsyncDataReceiverThread.py b/build/lib/pybci/ThreadClasses/AsyncDataReceiverThread.py new file mode 100644 index 0000000..114ee50 --- /dev/null +++ b/build/lib/pybci/ThreadClasses/AsyncDataReceiverThread.py @@ -0,0 +1,126 @@ +import threading, time +from collections import deque +import itertools +from bisect import bisect_left +def slice_fifo_by_time(fifo, start_time, end_time): + """Find the slice of fifo between start_time and end_time using binary search.""" + # separate times and data for easier indexing + times, data = zip(*fifo) + # find the index of the first time that is not less than start_time + start_index = bisect_left(times, start_time) + # find the index of the first time that is not less than end_time + end_index = bisect_left(times, end_time) + # return the slice of data between start_index and end_index + return data[start_index:end_index] + +class AsyncDataReceiverThread(threading.Thread): + """Responsible for receiving data from accepted LSL outlet, slices samples based on tmin+tmax basis, + starts counter for received samples after marker is received in ReceiveMarker. Relies on timestamps to slice array, + suspected more computationally intensive then synchronous method. + """ + startCounting = False + currentMarker = "" + def __init__(self, closeEvent, trainTestEvent, dataQueueTrain,dataQueueTest, dataStreamInlet, + customEpochSettings, globalEpochSettings,devCount, streamChsDropDict = [], maxExpectedSampleRate=100): + # large maxExpectedSampleRate can incur marker drop and slow procesing times for slicing arrays + super().__init__() + self.trainTestEvent = trainTestEvent + self.closeEvent = closeEvent + self.dataQueueTrain = dataQueueTrain + self.dataQueueTest = dataQueueTest + self.dataStreamInlet = dataStreamInlet + self.customEpochSettings = customEpochSettings + self.globalEpochSettings = globalEpochSettings + self.streamChsDropDict = streamChsDropDict + self.sr = maxExpectedSampleRate + #self.dataType = dataStreamInlet.info().type() + self.devCount = devCount # used for tracking which device is sending data to feature extractor + + def run(self): + posCount = 0 + chCount = self.dataStreamInlet.info().channel_count() + maxTime = (self.globalEpochSettings.tmin + self.globalEpochSettings.tmax) + if len(self.customEpochSettings.keys())>0: + if max([self.customEpochSettings[x].tmin + self.customEpochSettings[x].tmax for x in self.customEpochSettings]) > maxTime: + maxTime = max([self.customEpochSettings[x].tmin + self.customEpochSettings[x].tmax for x in self.customEpochSettings]) + fifoLength = int(self.sr*maxTime) + window_end_time = 0 + dataFIFOs = [deque(maxlen=fifoLength) for ch in range(chCount - len(self.streamChsDropDict))] + while not self.closeEvent.is_set(): + sample, timestamp = self.dataStreamInlet.pull_sample(timeout = 1) + if sample != None: + for index in sorted(self.streamChsDropDict, reverse=True): + del sample[index] # remove the desired channels from the sample + for i,fifo in enumerate(dataFIFOs): + fifo.append((timestamp, sample[i])) + if self.trainTestEvent.is_set(): # We're training! + if self.startCounting: # we received a marker + posCount += 1 + if posCount >= self.desiredCount: # enough samples are in FIFO, chop up and put in dataqueue + if len(self.customEpochSettings.keys())>0: # custom marker received + if self.customEpochSettings[self.currentMarker].splitCheck: # slice epochs into overlapping time windows + window_length = self.customEpochSettings[self.currentMarker].windowLength + window_overlap = self.customEpochSettings[self.currentMarker].windowOverlap + window_start_time = self.markerTimestamp + self.customEpochSettings[self.currentMarker].tmin + window_end_time = window_start_time + window_length + while window_end_time <= self.markerTimestamp + self.customEpochSettings[self.currentMarker].tmax: + sliceDataFIFOs = [slice_fifo_by_time(fifo, window_start_time, window_end_time) for fifo in dataFIFOs] + self.dataQueueTrain.put([sliceDataFIFOs, self.currentMarker, self.sr, self.devCount]) + window_start_time += window_length * (1 - window_overlap) + window_end_time = window_start_time + window_length + else: # don't slice just take tmin to tmax time + start_time = self.markerTimestamp + self.customEpochSettings[self.currentMarker].tmin + end_time = self.markerTimestamp + self.customEpochSettings[self.currentMarker].tmax + sliceDataFIFOs = [slice_fifo_by_time(fifo, start_time, end_time) for fifo in dataFIFOs] + self.dataQueueTrain.put([sliceDataFIFOs, self.currentMarker, self.sr, self.devCount]) + else: + if self.globalEpochSettings.splitCheck: # slice epochs in to overlapping time windows + window_length = self.globalEpochSettings.windowLength + window_overlap = self.globalEpochSettings.windowOverlap + window_start_time = self.markerTimestamp - self.globalEpochSettings.tmin + window_end_time = window_start_time + window_length + while window_end_time <= self.markerTimestamp + self.globalEpochSettings.tmax: + sliceDataFIFOs = [slice_fifo_by_time(fifo, window_start_time, window_end_time) for fifo in dataFIFOs] + self.dataQueueTrain.put([sliceDataFIFOs, self.currentMarker, self.sr, self.devCount]) + window_start_time += window_length * (1 - window_overlap) + window_end_time = window_start_time + window_length + self.startCounting = False + else: # don't slice just take tmin to tmax time + start_time = self.markerTimestamp + self.globalEpochSettings.tmin + end_time = self.markerTimestamp + self.globalEpochSettings.tmax + sliceDataFIFOs = [slice_fifo_by_time(fifo, start_time, end_time) for fifo in dataFIFOs] + self.dataQueueTrain.put([sliceDataFIFOs, self.currentMarker, self.sr, self.devCount]) + # reset flags and counters + posCount = 0 + self.startCounting = False + else: # in Test mode + if self.globalEpochSettings.splitCheck: + window_length = self.globalEpochSettings.windowLength + window_overlap = self.globalEpochSettings.windowOverlap + else: + window_length = self.globalEpochSettings.tmin+self.globalEpochSettings.tmax + + if timestamp >= window_end_time: + #sliceDataFIFOs = [[data for time, data in fifo if window_end_time - window_length <= time < window_end_time] for fifo in dataFIFOs] + sliceDataFIFOs = [slice_fifo_by_time(fifo, window_end_time - window_length, window_end_time) for fifo in dataFIFOs] + self.dataQueueTest.put([sliceDataFIFOs, None, self.devCount]) + #sliceDataFIFOs = [list(itertools.islice(d, fifoLength-window_samples, fifoLength)) for d in dataFIFOs] + if self.globalEpochSettings.splitCheck: + window_end_time += window_length * (1 - window_overlap) + else: + window_end_time = timestamp + window_length + else: + pass + # add levels of debug? + + def ReceiveMarker(self, marker, timestamp): # timestamp will be used for non sample rate specific devices (pupil-labs gazedata) + if self.startCounting == False: # only one marker at a time allow, other in windowed timeframe ignored + self.currentMarker = marker + self.markerTimestamp = timestamp + if len(self.customEpochSettings.keys())>0: # custom marker received + if marker in self.customEpochSettings.keys(): + self.desiredCount = int(self.customEpochSettings[marker].tmax * self.sr) # find number of samples after tmax to finish counting + self.startCounting = True + else: # no custom markers set, use global settings + self.desiredCount = int(self.globalEpochSettings.tmax * self.sr) # find number of samples after tmax to finish counting + self.startCounting = True diff --git a/build/lib/pybci/ThreadClasses/ClassifierThread.py b/build/lib/pybci/ThreadClasses/ClassifierThread.py new file mode 100644 index 0000000..5ec1b7f --- /dev/null +++ b/build/lib/pybci/ThreadClasses/ClassifierThread.py @@ -0,0 +1,111 @@ +from ..Utils.Classifier import Classifier +from ..Utils.Logger import Logger +import queue,threading, time +import numpy as np + +class ClassifierThread(threading.Thread): + features = np.array([])#[] + targets = np.array([]) + mode = "train" + guess = " " + #epochCountsc = {} + def __init__(self, closeEvent,trainTestEvent, featureQueueTest,featureQueueTrain, classifierInfoQueue, classifierInfoRetrieveEvent, + classifierGuessMarkerQueue, classifierGuessMarkerEvent, queryFeaturesQueue, queryFeaturesEvent, + logger = Logger(Logger.INFO), numStreamDevices = 1, + minRequiredEpochs = 10, clf = None, model = None, torchModel = None): + super().__init__() + self.trainTestEvent = trainTestEvent # responsible for tolling between train and test mode + self.closeEvent = closeEvent # responsible for cosing threads + self.featureQueueTest = featureQueueTest # gets feature data from feature processing thread + self.featureQueueTrain = featureQueueTrain # gets feature data from feature processing thread + self.classifier = Classifier(clf = clf, model = model, torchModel = torchModel) # sets classifier class, if clf and model passed, defaults to clf and sklearn + self.minRequiredEpochs = minRequiredEpochs # the minimum number of epochs required for classifier attempt + self.classifierInfoRetrieveEvent = classifierInfoRetrieveEvent + self.classifierInfoQueue = classifierInfoQueue + self.classifierGuessMarkerQueue = classifierGuessMarkerQueue + self.classifierGuessMarkerEvent = classifierGuessMarkerEvent + self.queryFeaturesQueue = queryFeaturesQueue + self.queryFeaturesEvent = queryFeaturesEvent + self.numStreamDevices = numStreamDevices + self.logger = logger + + def run(self): + epochCountsc={} + if self.numStreamDevices > 1: + tempdatatrain = {} + tempdatatest = {} + while not self.closeEvent.is_set(): + if self.trainTestEvent.is_set(): # We're training! + if self.featureQueueTrain.empty(): + if len(epochCountsc) > 1: # check if there is more then one test condition + minNumKeyEpochs = min([epochCountsc[key][1] for key in epochCountsc]) # check minimum viable number of training eochs have been obtained + if minNumKeyEpochs < self.minRequiredEpochs: + pass + else: + start = time.time() + self.classifier.TrainModel(self.features, self.targets) + if (self.logger.level == Logger.TIMING): + end = time.time() + self.logger.log(Logger.TIMING, f" classifier training time {end - start}") + if self.classifierGuessMarkerEvent.is_set(): + self.classifierGuessMarkerQueue.put(self.guess) + else: + try: + featuresSingle, devCount, target, epochCountsc = self.featureQueueTrain.get_nowait() #[dataFIFOs, self.currentMarker, self.sr, self.dataType] + if self.numStreamDevices > 1: # Collects multiple data strems feature sets and synchronise here + tempdatatrain[devCount] = featuresSingle + if len(tempdatatrain) == self.numStreamDevices: + flattened_list = np.array([item for sublist in tempdatatrain.values() for item in sublist]) + tempdatatrain = {} + self.targets = np.append(self.targets, [target], axis = 0) + #self.features = np.append(self.features, [flattened_list], axis = 0) + if self.features.shape[0] == 0: + self.features = self.features.reshape((0,) + flattened_list.shape) + self.features = np.append(self.features, [flattened_list], axis=0) + # need to check if all device data is captured, then flatten and append + else: # Only one device to collect from + if self.features.shape[0] == 0: + self.features = self.features.reshape((0,) + featuresSingle.shape) + self.targets = np.append(self.targets, [target], axis = 0) + self.features = np.append(self.features, [featuresSingle], axis = 0) + except queue.Empty: + pass + else: # We're testing! + try: + featuresSingle, devCount = self.featureQueueTest.get_nowait() #[dataFIFOs, self.currentMarker, self.sr, self.dataType] + if self.numStreamDevices > 1: + tempdatatest[devCount] = featuresSingle + if len(tempdatatest) == self.numStreamDevices: + flattened_list = [] + flattened_list = np.array([item for sublist in tempdatatest.values() for item in sublist]) + tempdatatest = {} + start = time.time() + self.guess = self.classifier.TestModel(flattened_list) + if (self.logger.level == Logger.TIMING): + end = time.time() + self.logger.log(Logger.TIMING, f" classifier testing time {end - start}") + else: + start = time.time() + self.guess = self.classifier.TestModel(featuresSingle) + if (self.logger.level == Logger.TIMING): + end = time.time() + self.logger.log(Logger.TIMING, f" classifier testing time {end - start}") + if self.classifierGuessMarkerEvent.is_set(): + self.classifierGuessMarkerQueue.put(self.guess) + except queue.Empty: + pass + if self.classifierInfoRetrieveEvent.is_set(): + a = self.classifier.accuracy + classdata = { + "clf":self.classifier.clf, + "model":self.classifier.model, + "torchModel":self.classifier.torchModel, + "accuracy":a + } + self.classifierInfoQueue.put(classdata) + if self.queryFeaturesEvent.is_set(): + featureData = { + "features":self.features, + "targets":self.targets + } + self.queryFeaturesQueue.put(featureData) \ No newline at end of file diff --git a/build/lib/pybci/ThreadClasses/DataReceiverThread.py b/build/lib/pybci/ThreadClasses/DataReceiverThread.py new file mode 100644 index 0000000..d5deddb --- /dev/null +++ b/build/lib/pybci/ThreadClasses/DataReceiverThread.py @@ -0,0 +1,99 @@ +import threading +from collections import deque +import itertools + +class DataReceiverThread(threading.Thread): + """Responsible for receiving data from accepted LSL outlet, slices samples based on tmin+tmax basis, + starts counter for received samples after marker is received in ReceiveMarker. + """ + startCounting = False + currentMarker = "" + def __init__(self, closeEvent, trainTestEvent, dataQueueTrain,dataQueueTest, dataStreamInlet, + customEpochSettings, globalEpochSettings,devCount, streamChsDropDict = []): + super().__init__() + self.trainTestEvent = trainTestEvent + self.closeEvent = closeEvent + self.dataQueueTrain = dataQueueTrain + self.dataQueueTest = dataQueueTest + self.dataStreamInlet = dataStreamInlet + self.customEpochSettings = customEpochSettings + self.globalEpochSettings = globalEpochSettings + self.streamChsDropDict = streamChsDropDict + self.sr = dataStreamInlet.info().nominal_srate() + self.devCount = devCount # used for tracking which device is sending data to feature extractor + + def run(self): + posCount = 0 + chCount = self.dataStreamInlet.info().channel_count() + maxTime = (self.globalEpochSettings.tmin + self.globalEpochSettings.tmax) + if len(self.customEpochSettings.keys())>0: + if max([self.customEpochSettings[x].tmin + self.customEpochSettings[x].tmax for x in self.customEpochSettings]) > maxTime: + maxTime = max([self.customEpochSettings[x].tmin + self.customEpochSettings[x].tmax for x in self.customEpochSettings]) + fifoLength = int(self.dataStreamInlet.info().nominal_srate()*maxTime) + dataFIFOs = [deque(maxlen=fifoLength) for ch in range(chCount - len(self.streamChsDropDict))] + while not self.closeEvent.is_set(): + sample, timestamp = self.dataStreamInlet.pull_sample(timeout = 1) + if sample != None: + for index in sorted(self.streamChsDropDict, reverse=True): + del sample[index] # remove the desired channels from the sample + for i,fifo in enumerate(dataFIFOs): + fifo.append(sample[i]) + if self.trainTestEvent.is_set(): # We're training! + if self.startCounting: # we received a marker + posCount+=1 + if posCount >= self.desiredCount: # enough samples are in FIFO, chop up and put in dataqueue + if len(self.customEpochSettings.keys())>0: # custom marker received + if self.customEpochSettings[self.currentMarker].splitCheck: # slice epochs in to overlapping time windows + window_samples =int(self.customEpochSettings[self.currentMarker].windowLength * self.sr) #number of samples in each window + increment = int((1-self.customEpochSettings[self.currentMarker].windowOverlap)*window_samples) # if windows overlap each other by how many samples + while posCount - window_samples > 0: + sliceDataFIFOs = [list(itertools.islice(d, posCount - window_samples, posCount)) for d in dataFIFOs] + self.dataQueueTrain.put([sliceDataFIFOs, self.currentMarker, self.sr, self.devCount]) + posCount-=increment + else: # don't slice just take tmin to tmax time + sliceDataFIFOs = [list(itertools.islice(d, fifoLength - int((self.customEpochSettings[self.currentMarker].tmax+self.customEpochSettings[self.currentMarker].tmin) * self.sr), fifoLength))for d in dataFIFOs] + self.dataQueueTrain.put([sliceDataFIFOs, self.currentMarker, self.sr, self.devCount]) + else: + if self.globalEpochSettings.splitCheck: # slice epochs in to overlapping time windows + window_samples =int(self.globalEpochSettings.windowLength * self.sr) #number of samples in each window + increment = int((1-self.globalEpochSettings.windowOverlap)*window_samples) # if windows overlap each other by how many samples + startCount = self.desiredCount + int(self.globalEpochSettings.tmin * self.sr) + while startCount - window_samples > 0: + sliceDataFIFOs = [list(itertools.islice(d, startCount - window_samples, startCount)) for d in dataFIFOs] + self.dataQueueTrain.put([sliceDataFIFOs, self.currentMarker, self.sr, self.devCount]) + startCount-=increment + else: # don't slice just take tmin to tmax time + sliceDataFIFOs = [list(itertools.islice(d, fifoLength - int((self.globalEpochSettings.tmin+self.globalEpochSettings.tmax) * self.sr), fifoLength)) for d in dataFIFOs] + self.dataQueueTrain.put([sliceDataFIFOs, self.currentMarker, self.sr, self.devCount]) + #end = time.time() + #print(f"Data slicing process time {end - start}") + # reset flags and counters + posCount = 0 + self.startCounting = False + else: # in Test mode + posCount+=1 + if self.globalEpochSettings.splitCheck: + window_samples = int(self.globalEpochSettings.windowLength * self.sr) #number of samples in each window + else: + window_samples = int((self.globalEpochSettings.tmin+self.globalEpochSettings.tmax) * self.sr) + if posCount >= window_samples: + sliceDataFIFOs = [list(itertools.islice(d, fifoLength-window_samples, fifoLength)) for d in dataFIFOs] + if self.globalEpochSettings.splitCheck: + posCount = int((1-self.globalEpochSettings.windowOverlap)*window_samples) # offset poscoutn based on window overlap + else: + posCount = 0 + self.dataQueueTest.put([sliceDataFIFOs, self.sr, self.devCount]) + else: + pass + # add levels of debug + + def ReceiveMarker(self, marker, timestamp): # timestamp will be used for non sample rate specific devices (pupil-labs gazedata) + if self.startCounting == False: # only one marker at a time allow, other in windowed timeframe ignored + self.currentMarker = marker + if len(self.customEpochSettings.keys())>0: # custom marker received + if marker in self.customEpochSettings.keys(): + self.desiredCount = int(self.customEpochSettings[marker].tmax * self.sr) # find number of samples after tmax to finish counting + self.startCounting = True + else: # no custom markers set, use global settings + self.desiredCount = int(self.globalEpochSettings.tmax * self.sr) # find number of samples after tmax to finish counting + self.startCounting = True diff --git a/build/lib/pybci/ThreadClasses/FeatureProcessorThread.py b/build/lib/pybci/ThreadClasses/FeatureProcessorThread.py new file mode 100644 index 0000000..2dd753b --- /dev/null +++ b/build/lib/pybci/ThreadClasses/FeatureProcessorThread.py @@ -0,0 +1,64 @@ +import threading, queue, time +from ..Utils.FeatureExtractor import GenericFeatureExtractor +from ..Utils.Logger import Logger +from ..Configuration.EpochSettings import GlobalEpochSettings +import copy + +class FeatureProcessorThread(threading.Thread): + tempDeviceEpochLogger = [] + def __init__(self, closeEvent, trainTestEvent, dataQueueTrain,dataQueueTest, + featureQueueTest,featureQueueTrain, totalDevices,markerCountRetrieveEvent,markerCountQueue, customEpochSettings = {}, + globalEpochSettings = GlobalEpochSettings(),logger = Logger(Logger.INFO), + featureExtractor = GenericFeatureExtractor()): + super().__init__() + self.markerCountQueue = markerCountQueue + self.trainTestEvent = trainTestEvent + self.closeEvent = closeEvent + self.dataQueueTrain = dataQueueTrain + self.dataQueueTest = dataQueueTest + self.featureQueueTrain = featureQueueTrain + self.featureQueueTest = featureQueueTest + self.featureExtractor = featureExtractor + self.logger = logger + self.totalDevices = totalDevices + self.markerCountRetrieveEvent = markerCountRetrieveEvent + self.epochCounts = {} + self.customEpochSettings = customEpochSettings + self.globalWindowSettings = globalEpochSettings + self.tempDeviceEpochLogger = [0 for x in range(self.totalDevices)] + + def run(self): + while not self.closeEvent.is_set(): + if self.markerCountRetrieveEvent.is_set(): + self.markerCountQueue.put(self.epochCounts) + if self.trainTestEvent.is_set(): # We're training! + try: + dataFIFOs, currentMarker, sr, devCount = self.dataQueueTrain.get_nowait() #[sliceDataFIFOs, self.currentMarker, self.sr, self.devCount + if currentMarker in self.epochCounts: + self.epochCounts[currentMarker][1] += 1 + else: + self.epochCounts[currentMarker] = [len(self.epochCounts.keys()),1] + target = self.epochCounts[currentMarker][0] + start = time.time() + features = self.featureExtractor.ProcessFeatures(dataFIFOs, sr, target) # allows custom epoch class to be passed + if (self.logger.level == Logger.TIMING): + end = time.time() + self.logger.log(Logger.TIMING, f" Feature Extraction time {end - start}") + if (end-start) >self.globalWindowSettings.windowLength: + self.logger.log(Logger.WARNING, f" Feature Extraction time > globalEpochSetting.windowLength, will create lag in classification output. Recommended to reduce channels, smapling rate, and features or reduce feature computational complexity.") + self.featureQueueTrain.put( [features, devCount, target, dict(self.epochCounts)] ) + except queue.Empty: + pass + else: + try: + dataFIFOs, sr, devCount = self.dataQueueTest.get_nowait() #[dataFIFOs, self.currentMarker, self.sr, ] + start = time.time() + features = self.featureExtractor.ProcessFeatures(dataFIFOs, sr, None) + if (self.logger.level == Logger.TIMING): + end = time.time() + self.logger.log(Logger.TIMING, f" Feature Extraction time {end - start}") + if (end-start) >self.globalWindowSettings.windowLength: + self.logger.log(Logger.WARNING, f" Feature Extraction time > globalEpochSetting.windowLength, will create lag in classification output. Recommended to reduce channels, smapling rate, and features or reduce feature computational complexity.") + self.featureQueueTest.put([features, devCount]) + except queue.Empty: + pass diff --git a/build/lib/pybci/ThreadClasses/MarkerThread.py b/build/lib/pybci/ThreadClasses/MarkerThread.py new file mode 100644 index 0000000..3533de1 --- /dev/null +++ b/build/lib/pybci/ThreadClasses/MarkerThread.py @@ -0,0 +1,28 @@ +import threading + +class MarkerThread(threading.Thread): + """Receives Marker on chosen lsl Marker outlet. Pushes marker to data threads for framing epochs, + also sends markers to featureprocessing thread for epoch counting and multiple device synchronisation. + """ + def __init__(self,closeEvent, trainTestEvent, markerStreamInlet, dataThreads, featureThreads):#, lock): + super().__init__() + self.trainTestEvent = trainTestEvent + self.closeEvent = closeEvent + self.markerStreamInlet = markerStreamInlet + self.dataThreads = dataThreads + self.featureThreads= featureThreads + + def run(self): + while not self.closeEvent.is_set(): + marker, timestamp = self.markerStreamInlet.pull_sample(timeout = 10) + if self.trainTestEvent.is_set(): # We're training! + if marker != None: + marker = marker[0] + for thread in self.dataThreads: + thread.ReceiveMarker(marker, timestamp) + for thread in self.featureThreads: + thread.ReceiveMarker(marker, timestamp) + else: + pass + # add levels of debug + # print("PyBCI: LSL pull_sample timed out, no marker on stream...") diff --git a/build/lib/pybci/ThreadClasses/OptimisedDataReceiverThread.py b/build/lib/pybci/ThreadClasses/OptimisedDataReceiverThread.py new file mode 100644 index 0000000..252efff --- /dev/null +++ b/build/lib/pybci/ThreadClasses/OptimisedDataReceiverThread.py @@ -0,0 +1,139 @@ +import threading, time +from collections import deque +import itertools +import numpy as np +from bisect import bisect_left + +class OptimisedDataReceiverThread(threading.Thread): + """Responsible for receiving data from accepted LSL outlet, slices samples based on tmin+tmax basis, + starts counter for received samples after marker is received in ReceiveMarker. Relies on timestamps to slice array, + suspected more computationally intensive then synchronous method. + """ + markerReceived = False + currentMarker = "" + def __init__(self, closeEvent, trainTestEvent, dataQueueTrain,dataQueueTest, dataStreamInlet, + customEpochSettings, globalEpochSettings,devCount, streamChsDropDict = [], maxExpectedSampleRate=100): + # large maxExpectedSampleRate can incur marker drop and slow procesing times for slicing arrays + super().__init__() + self.trainTestEvent = trainTestEvent + self.closeEvent = closeEvent + self.dataQueueTrain = dataQueueTrain + self.dataQueueTest = dataQueueTest + self.dataStreamInlet = dataStreamInlet + self.customEpochSettings = customEpochSettings + self.globalEpochSettings = globalEpochSettings + self.streamChsDropDict = streamChsDropDict + self.sr = maxExpectedSampleRate + self.devCount = devCount # used for tracking which device is sending data to feature extractor + + def run(self): + chCount = self.dataStreamInlet.info().channel_count() + maxTime = (self.globalEpochSettings.tmin + self.globalEpochSettings.tmax) + if len(self.customEpochSettings.keys()) > 0: # min time used for max_samples and temp array, maxTime used for longest epochs and permanentDataBuffers + if max([self.customEpochSettings[x].tmin + self.customEpochSettings[x].tmax for x in self.customEpochSettings]) > maxTime: + maxTime = max([self.customEpochSettings[x].tmin + self.customEpochSettings[x].tmax for x in self.customEpochSettings]) + if self.globalEpochSettings.splitCheck: + window_length = self.globalEpochSettings.windowLength + window_overlap = self.globalEpochSettings.windowOverlap + else: + window_length = self.globalEpochSettings.tmin+self.globalEpochSettings.tmax + minfifoLength = int(self.sr * window_length * (1-self.globalEpochSettings.windowOverlap)) # sets global window length with overlap as factor for minimum delay in test mode + dataBuffers = np.zeros((minfifoLength,chCount)) + chs_to_drop = np.ones(chCount, dtype=bool) + chs_to_drop[self.streamChsDropDict] = False + fifoLength = int(self.sr * (maxTime+20)) # adds twenty seconds to give more timestamps when buffering (assuming devices dont timeout for longer then 20.0 seconds, migth be worth making configurable) + permanentDataBuffers = np.zeros((fifoLength, chCount - len(self.streamChsDropDict))) + permanentTimestampBuffer = np.zeros(fifoLength) + next_window_time = 0 # sets testing mode window time, duration based on windowlength and overlap + while not self.closeEvent.is_set(): + _, timestamps = self.dataStreamInlet.pull_chunk(timeout=0.0, max_samples=dataBuffers.shape[0], dest_obj=dataBuffers) # optimised method of getting data to pull_sample, dest_obj saves memory re-allocation + if timestamps: + if len(self.streamChsDropDict) == 0: + dataBufferView = dataBuffers[:len(timestamps), :] # [:, :len(timestamps)] + else: + dataBufferView = dataBuffers[:len(timestamps), chs_to_drop] # [:, :len(timestamps)] + permanentDataBuffers = np.roll(permanentDataBuffers, shift=-len(timestamps), axis=0) + permanentTimestampBuffer = np.roll(permanentTimestampBuffer, shift=-len(timestamps)) + permanentDataBuffers[-len(timestamps):,:] = dataBufferView + permanentTimestampBuffer[-len(timestamps):] = timestamps + if self.trainTestEvent.is_set(): # We're training! + if self.markerReceived: # we received a marker + timestamp_tmin = self.targetTimes[1] + timestamp_tmax = self.targetTimes[0] + #print(timestamp_tmin, "timestamp_tmin >= permanentTimestampBuffer[0]:", permanentTimestampBuffer[0]) + #print(timestamp_tmax, "timestamp_tmax <= permanentTimestampBuffer[-1]:", permanentTimestampBuffer[-1]) + if timestamp_tmin >= permanentTimestampBuffer[0] and timestamp_tmax <= permanentTimestampBuffer[-1]: + if len(self.customEpochSettings.keys())>0: # custom marker received + if self.customEpochSettings[self.currentMarker].splitCheck: # slice epochs into overlapping time windows + window_start_time = timestamp_tmin + window_end_time = window_start_time + window_length + while window_end_time <= timestamp_tmax: + idx_tmin = (np.abs(permanentTimestampBuffer - window_start_time)).argmin() # find array index of start of window + idx_tmax = (np.abs(permanentTimestampBuffer - window_end_time)).argmin() # find array index of end of window + slices = permanentDataBuffers[idx_tmin:idx_tmax,:] + self.dataQueueTrain.put([slices, self.currentMarker, self.sr, self.devCount]) + window_start_time += window_length * (1 - window_overlap) + window_end_time = window_start_time + window_length + else: # don't slice just take tmin to tmax time + idx_tmin = (np.abs(permanentTimestampBuffer - timestamp_tmin)).argmin() + idx_tmax = (np.abs(permanentTimestampBuffer - timestamp_tmax)).argmin() + slices = permanentDataBuffers[idx_tmin:idx_tmax,:] + self.dataQueueTrain.put([slices, self.currentMarker, self.sr, self.devCount]) + else: + if self.globalEpochSettings.splitCheck: # slice epochs in to overlapping time windows + window_start_time = timestamp_tmin#self.markerTimestamp - self.globalEpochSettings.tmin + window_end_time = window_start_time + window_length + #print(window_end_time, " ", timestamp_tmax) + while window_end_time <= timestamp_tmax: #:self.markerTimestamp + self.globalEpochSettings.tmax: + idx_tmin = (np.abs(permanentTimestampBuffer - window_start_time)).argmin() + idx_tmax = (np.abs(permanentTimestampBuffer - window_end_time)).argmin() + #print(idx_tmin, " ", idx_tmax) + #print(permanentTimestampBuffer[idx_tmin], " ", permanentTimestampBuffer[idx_tmax]) + #print(permanentDataBuffers.shape) + slices = permanentDataBuffers[idx_tmin:idx_tmax,:] + #print(slices.shape) + self.dataQueueTrain.put([slices, self.currentMarker, self.sr, self.devCount]) + window_start_time += window_length * (1 - window_overlap) + window_end_time = window_start_time + window_length + self.startCounting = False + else: # don't slice just take tmin to tmax time + idx_tmin = (np.abs(permanentTimestampBuffer - timestamp_tmin)).argmin() + idx_tmax = (np.abs(permanentTimestampBuffer - timestamp_tmax)).argmin() + slices = permanentDataBuffers[idx_tmin:idx_tmax,:] + self.dataQueueTrain.put([slices, self.currentMarker, self.sr, self.devCount]) + self.markerReceived = False + else: # in Test mode + if next_window_time+(window_length/2) <= permanentTimestampBuffer[-1]: + idx_tmin = (np.abs(permanentTimestampBuffer - (next_window_time-(window_length/2)))).argmin() + idx_tmax = (np.abs(permanentTimestampBuffer - (next_window_time+(window_length/2)))).argmin() + #print(next_window_time+(window_length/2), "next_window_time+(window_length/2) <= permanentTimestampBuffer[-1]:", permanentTimestampBuffer[-1]) + #print(next_window_time, "next_window_time permanentTimestampBuffer[0]:", permanentTimestampBuffer[0]) + #print(idx_tmin, "idx_tmin idx_tmax", idx_tmax) + if idx_tmin == idx_tmax: + # oops we lost track, get window positions and start again + idx_tmin = (np.abs(permanentTimestampBuffer - (permanentTimestampBuffer[-1] - window_length))).argmin() + idx_tmax = -1 + next_window_time = permanentTimestampBuffer[-1] - (window_length/2) + slices = permanentDataBuffers[idx_tmin:idx_tmax,:] + self.dataQueueTest.put([slices, self.sr, self.devCount]) + if self.globalEpochSettings.splitCheck: + next_window_time += window_length * (1 - window_overlap) + else: + next_window_time += window_length + else: + pass + # add levels of debug? + + def ReceiveMarker(self, marker, timestamp): # timestamp will be used for non sample rate specific devices (pupil-labs gazedata) + if self.markerReceived == False: # only one marker at a time allow, other in windowed timeframe ignored + self.currentMarker = marker + self.markerTimestamp = timestamp + if len(self.customEpochSettings.keys())>0: # custom marker received + if marker in self.customEpochSettings.keys(): + #self.desiredCount = int(self.customEpochSettings[marker].tmax * self.sr) # find number of samples after tmax to finish counting + self.targetTimes = [timestamp+self.customEpochSettings[marker].tmax, timestamp-self.customEpochSettings[marker].tmin] + self.markerReceived = True + else: # no custom markers set, use global settings + #self.desiredCount = int(self.globalEpochSettings.tmax * self.sr) # find number of samples after tmax to finish counting + self.targetTimes = [timestamp+self.globalEpochSettings.tmax, timestamp-self.globalEpochSettings.tmin] + self.markerReceived = True \ No newline at end of file diff --git a/build/lib/pybci/ThreadClasses/__init__.py b/build/lib/pybci/ThreadClasses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/pybci/Utils/Classifier.py b/build/lib/pybci/Utils/Classifier.py new file mode 100644 index 0000000..3aabbb3 --- /dev/null +++ b/build/lib/pybci/Utils/Classifier.py @@ -0,0 +1,120 @@ +import sklearn +from sklearn.preprocessing import StandardScaler +from sklearn import svm +import tensorflow +import torch + +from sklearn.model_selection import train_test_split +import numpy as np + +class Classifier(): + classifierLibrary = "sklearn" # current default, should be none or somthing different? + clf = svm.SVC(kernel = "rbf")#C=c, kernel=k, degree=d, gamma=g, coef0=c0, tol=t, max_iter=i) + accuracy = 0 + model = None + torchModel = None + + def __init__(self, clf = None, model = None, torchModel = None): + super().__init__() + if clf != None: + self.clf = clf + elif model != None: + self.model = model + elif torchModel != None: + self.torchModel = torchModel + self.CheckClassifierLibrary() + + def CheckClassifierLibrary(self): + if self.model != None: # maybe requires actual check for tensorflow model + self.classifierLibrary = "tensor" + elif self.torchModel != None: # maybe requires actual check for sklearn clf + self.classifierLibrary = "pyTorch" + elif self.clf != None: # maybe requires actual check for sklearn clf + self.classifierLibrary = "sklearn" + + def TrainModel(self, features, targets): + x_train, x_test, y_train, y_test = train_test_split(features, targets, shuffle = True, test_size=0.2) + #print(features.shape) + #print(x_train.shape) + if len(features.shape)==3: + self.scaler = [StandardScaler() for scaler in range(features.shape[2])] # normalise our data (everything is a 0 or a 1 if you think about it, cheers georgey boy boole) + for e in range(features.shape[2]): # this would normalise the channel, maybe better to normalise across other dimension + x_train_channel = x_train[:,:,e].reshape(-1, 1) + x_test_channel = x_test[:,:,e].reshape(-1, 1) + x_train[:,:,e] = self.scaler[e].fit_transform(x_train_channel).reshape(x_train[:,:,e].shape) + x_test[:,:,e] = self.scaler[e].transform(x_test_channel).reshape(x_test[:,:,e].shape) + #x_train[:,:,e] = self.scaler[e].fit_transform(x_train[:,:,e]) # Compute the mean and standard deviation based on the training data + #x_test[:,:,e] = self.scaler[e].transform(x_test[:,:,e]) # Scale the test data + elif len(features.shape)== 2: + self.scaler = StandardScaler() # normalise our data (everything is a 0 or a 1 if you think about it, cheers georgey boy boole) + x_train = self.scaler.fit_transform(x_train) # Compute the mean and standard deviation based on the training data + x_test = self.scaler.transform(x_test) # Scale the test data + if all(item == y_train[0] for item in y_train): + pass + else: + #print(x_train, y_train) + if self.classifierLibrary == "pyTorch": + self.accuracy, self.pymodel = self.torchModel(x_train, x_test, y_train, y_test) + elif self.classifierLibrary == "sklearn": + self.clf.fit(x_train, y_train) + y_predictions = self.clf.predict(x_test) + self.accuracy = sklearn.metrics.accuracy_score(y_test, y_predictions) + elif self.classifierLibrary == "tensor": + self.model.fit(np.array(x_train), np.array(y_train)) # epochs and batch_size should be customisable + self.loss, self.accuracy = self.model.evaluate(np.array(x_test), np.array(y_test)) + else: + # no classifier library selected, print debug? + pass + + def TestModel(self, x): + if len(x.shape)==2: + for e in range(x.shape[1]): + x[:,e] = self.scaler[e].transform(x[:,e].reshape(-1, 1)).reshape(x[:,e].shape) + #x[:,e] = self.scaler[e].transform([x[:,e]])[0] + elif len(x.shape)== 1: + x = self.scaler.transform([x])[0] # Scale the test data + if self.classifierLibrary == "sklearn": + x = np.expand_dims(x, axis=0) + return self.clf.predict(x) + elif self.classifierLibrary == "tensor": + x = np.expand_dims(x, axis=0) + predictions = self.model.predict(x) + if len (predictions[0]) == 1: # assume binary classification + return 1 if predictions[0] > 0.5 else 0 + else: # assume multi-classification + return np.argmax(predictions[0]) + elif self.classifierLibrary == "pyTorch": + x = torch.Tensor(np.expand_dims(x, axis=0)) + self.pymodel.eval() + with torch.no_grad(): + predictions = self.pymodel(x) + if len (predictions[0]) == 1: # assume binary classification + return 1 if predictions[0] > 0.5 else 0 + else: # assume multi-classification + return torch.argmax(predictions).item() + + else: + print("no classifier library selected") + # no classifier library selected, print debug? + pass + +''' + def UpdateModel(self, featuresSingle, target): + # function currently not used, may be redundant, means thread function hold feature and target variables and passes reference to here, + # would be better to hold in classifier class? + featuresSingle = np.where(np.isnan(featuresSingle), 0, featuresSingle) + if (len(np.array(self.features).shape) ==3): + features = np.array(features).reshape(np.array(features).shape[0], -1) + self.features = np.vstack([self.features, featuresSingle]) + self.targets = np.hstack([self.targets, target]) + if self.classifierLibrary == "sklearn": + # Update the model with new data using partial_fit + self.clf.fit(self.features, self.targets) #, classes=np.unique(target)) + self.accuracy = self.clf.score(self.x_test, self.y_test) + elif self.classifierLibrary == "tensor": + self.model.fit(featuresSingle, target, epochs=1, batch_size=32) + self.loss, self.accuracy = self.model.evaluate(self.x_test, self.y_test) + else: + # no classifier library selected, print debug? + pass +''' \ No newline at end of file diff --git a/build/lib/pybci/Utils/FeatureExtractor.py b/build/lib/pybci/Utils/FeatureExtractor.py new file mode 100644 index 0000000..2f698e0 --- /dev/null +++ b/build/lib/pybci/Utils/FeatureExtractor.py @@ -0,0 +1,141 @@ +import antropy as ant +import numpy as np +from scipy.signal import welch +from scipy.integrate import simps +import warnings, time +from ..Configuration.FeatureSettings import GeneralFeatureChoices +# Filter out UserWarning messages from the scipy package, could be worth moving to init and applying printdebug print levels? (typically nans, 0 and infs causing errors) +warnings.filterwarnings("ignore", category=UserWarning, module="scipy") # used to reduce print statements from constant signals being applied +warnings.filterwarnings("ignore", category=UserWarning, module="antropy") # used to reduce print statements from constant signals being applied +warnings.filterwarnings("ignore", category=RuntimeWarning, module="antropy") # used to reduce print statements from constant signals being applied +warnings.filterwarnings("ignore", category=RuntimeWarning, module="numpy") # used to reduce print statements from constant signals being applied +#warnings.filterwarnings("ignore", category=RuntimeWarning, module="pybci") # used to reduce print statements from constant signals being applied + +class GenericFeatureExtractor(): + def __init__(self, freqbands = [[1.0, 4.0], [4.0, 8.0], [8.0, 12.0], [12.0, 20.0]], featureChoices = GeneralFeatureChoices()): + super().__init__() + self.freqbands = freqbands + self.featureChoices = featureChoices + #for key, value in self.featureChoices.__dict__.items(): + # print(f"{key} = {value}") + selFeats = sum([self.featureChoices.appr_entropy, + self.featureChoices.perm_entropy, + self.featureChoices.spec_entropy, + self.featureChoices.svd_entropy, + self.featureChoices.samp_entropy, + self.featureChoices.rms, + self.featureChoices.meanPSD, + self.featureChoices.medianPSD, + self.featureChoices.variance, + self.featureChoices.meanAbs, + self.featureChoices.waveformLength, + self.featureChoices.zeroCross, + self.featureChoices.slopeSignChange] + ) + self.numFeatures = (len(self.freqbands)*self.featureChoices.psdBand)+selFeats + + def ProcessFeatures(self, epoch, sr, target): + """Allows 2D time series data to be passed with given sample rate to get various time+frequency based features. + Best for EEG, EMG, EOG, or other consistent data with a consistent sample rate (pupil labs does not) + Which features are chosen is based on self.featureChoices with initialisation. self.freqbands sets the limits for + desired frequency bands average power. + Inputs: + epoch = 2D list or 2D numpy array [chs, samples] + target = string of received marker type + sr = samplerate of current device + Returns: + features = 2D numpy array of size (chs, (len(freqbands) + sum(True in self.featureChoices))) + target = same as input target, can be useful for using a baseline number differently + NOTE: Any channels with a constant value will generate warnings in any frequency based features (constant level == no frequency components). + """ + numchs = epoch.shape[1] + features = np.zeros(numchs * self.numFeatures) + + for ch in range(epoch.shape[1]): + #ch = np.isnan(ch) + if self.featureChoices.psdBand: # get custom average power within given frequency band from freqbands + freqs, psd = welch(epoch[:,ch], sr) + for l, band in enumerate(self.freqbands): + if len(freqs) > 0: # len(freqs) can be 0 if signal is all DC + idx_band = np.logical_and(freqs >= band[0], freqs <= band[1]) + #if len(psd[idx_band]) == 1: # if freq band is only in one field just pass single value instead of calculating average + #print(ch) + bp = np.mean(psd[idx_band]) + #else: + # bp = simps(psd[idx_band], dx=(freqs[1]-freqs[0])) / (band[1] - band[0]) + #bp = simpson(psd[idx_band], dx=freq_res) + features[(ch* self.numFeatures)+l] = bp + else: + features[(ch* self.numFeatures)+l] = 0 + else: + freqs, psd = welch(epoch[:,ch], sr)# calculate for mean and median + l = -1 # accounts for no freqbands being selected + if self.featureChoices.meanPSD: # mean power + l += 1 + if len(freqs) > 0: features[(ch* self.numFeatures)+l] = np.mean(psd) # len(freqs) can be 0 if signal is all DC + else: features[(ch* self.numFeatures)+l] = 0 + if self.featureChoices.medianPSD: # median Power + l += 1 + if len(freqs) > 0: features[(ch* self.numFeatures)+l] = np.median(psd) # len(freqs) can be 0 if signal is all DC + else: features[(ch* self.numFeatures)+l] = 0 + if self.featureChoices.appr_entropy: # Approximate entropy(X,M,R) X = data, M is , R is 30% standard deviation of X + l += 1 + features[(ch* self.numFeatures)+l] = ant.app_entropy(epoch[:,ch]) + if self.featureChoices.perm_entropy: # permutation_entropy + l += 1 + features[(ch* self.numFeatures)+l] = ant.perm_entropy(epoch[:,ch],normalize=True) + if self.featureChoices.spec_entropy: # spectral Entropy + l += 1 + features[(ch* self.numFeatures)+l] = ant.spectral_entropy(epoch[:,ch], sf=sr, method='welch', nperseg = len(epoch[:,ch]), normalize=True) + if self.featureChoices.svd_entropy:# svd Entropy + l += 1 + features[(ch* self.numFeatures)+l] = ant.svd_entropy(epoch[:,ch], normalize=True) + if self.featureChoices.samp_entropy: # sample Entropy + l += 1 + features[(ch* self.numFeatures)+l] = ant.sample_entropy(epoch[:,ch]) + if self.featureChoices.rms: # rms + l += 1 + features[(ch* self.numFeatures)+l] = np.sqrt(np.mean(np.array(epoch[:,ch])**2)) + if self.featureChoices.variance: # variance + l += 1 + features[(ch* self.numFeatures)+l] = np.var(epoch[:,ch]) + if self.featureChoices.meanAbs: # Mean Absolute Value + l += 1 + try: + features[(ch* self.numFeatures)+l] = sum([np.linalg.norm(c) for c in epoch[:,ch]])/len(epoch[:,ch]) + except: + features[(ch* self.numFeatures)+l] = 0 + if self.featureChoices.waveformLength: # waveformLength + l += 1 + try: + features[(ch* self.numFeatures)+l] = sum([np.linalg.norm(c-epoch[inum,ch]) for inum, c in enumerate(epoch[1:,ch])]) + except: + features[(ch* self.numFeatures)+l] = 0 + if self.featureChoices.zeroCross: # zeroCross + l += 1 + features[(ch* self.numFeatures)+l] = sum([1 if c*epoch[inum+1,ch]<0 else 0 for inum, c in enumerate(epoch[:-1,ch])]) + if self.featureChoices.slopeSignChange: # slopeSignChange + l += 1 + ssc = sum([1 if (c-epoch[inum+1,ch])*(c-epoch[inum+1,ch])>=0.1 else 0 for inum, c in enumerate(epoch[:-1,ch])]) + features[(ch* self.numFeatures)+l] = ssc + features[np.isnan(features)] = 0 # checks for nans + features[features == np.inf] = 0#np.iinfo(np.int32).max + #print(features) + + return features + +class GazeFeatureExtractor(): + def __init__(self): + super().__init__() + +'''pupil channels in order +confidence: 1 channel +norm_pos_x/y: 2 channels +gaze_point_3d_x/y/z: 3 channels +eye_center0_3d_x/y/z (right/left, x/y/z): 6 channels (3 channels for each eye) +gaze_normal0/1_x/y/z (right/left, x/y/z): 6 channels (3 channels for each eye) +norm_pos_x/y: 2 channels +diameter0/1_2d (right/left): 2 channels +diameter0/1_3d (right/left): 2 channels +22 total +''' \ No newline at end of file diff --git a/build/lib/pybci/Utils/LSLScanner.py b/build/lib/pybci/Utils/LSLScanner.py new file mode 100644 index 0000000..d88722e --- /dev/null +++ b/build/lib/pybci/Utils/LSLScanner.py @@ -0,0 +1,106 @@ +from pylsl import StreamInlet, resolve_stream +from ..Utils.Logger import Logger + +class LSLScanner: + streamTypes = ["EEG", "ECG", "EMG", "Gaze"] # list of strings, holds desired LSL stream types + markerTypes = ["Markers"] # list of strings, holds desired LSL marker types + dataStreams = [] # list of data StreamInlets, available on LSL as chosen by streamTypes + markerStream = [] # list of marker StreamInlets, available on LSL as chosen by markerTypes + markerStreamPredefined = False + dataStreamPredefined = False + + def __init__(self,parent, dataStreamsNames = None, markerStreamName = None, streamTypes = None, markerTypes = None, logger = Logger(Logger.INFO)): + """ + Intiialises LSLScanner, accepts custom data and marker stream strings to search for, if valid can be obtained after scans with LSLScanner.dataStreams and LSLScanner.makerStream. + Parameters: + streamTypes (List of strings): allows user to set custom acceptable EEG stream definitions, if None defaults to streamTypes scan + markerTypes (List of strings): allows user to set custom acceptable Marker stream definitions, if None defaults to markerTypes scan + streamTypes (List of strings): allows user to set custom acceptable EEG type definitions, ignored if streamTypes not None + markerTypes (List of strings): allows user to set custom acceptable Marker type definitions, ignored if markerTypes not None + logger (pybci.Logger): Custom Logger class or PyBCI, defaults to logger.info if not set, which prints all pybci messages. + """ + self.parent = parent + if streamTypes != None: + self.streamTypes = streamTypes + if markerTypes != None: + self.markerTypes = markerTypes + self.logger = logger + if dataStreamsNames != None: + self.dataStreamPredefined = True + self.dataStreamsNames = dataStreamsNames + else: + self.ScanDataStreams() + if markerStreamName != None: + self.markerStreamPredefined = True + self.markerStreamName = markerStreamName + else: + self.ScanMarkerStreams() + + def ScanStreams(self): + """Scans LSL for both data and marker channels.""" + self.ScanDataStreams() + self.ScanMarkerStreams() + + def ScanDataStreams(self): + """Scans available LSL streams and appends inlet to self.dataStreams""" + streams = resolve_stream() + dataStreams = [] + self.dataStreams = [] + for stream in streams: + if stream.type() in self.streamTypes: + dataStreams.append(StreamInlet(stream)) + if self.dataStreamPredefined: + for s in dataStreams: + name = s.info().name() + if name not in self.dataStreamsNames: + self.logger.log(Logger.WARNING," Predefined LSL Data Stream name not present.") + self.logger.log(Logger.WARNING, " Available Streams: "+str([s.info().name() for s in dataStreams])) + else: + self.dataStreams.append(s) + else: # just add all datastreams as none were specified + self.dataStreams = dataStreams + + def ScanMarkerStreams(self): + """Scans available LSL streams and appends inlet to self.markerStreams""" + streams = resolve_stream() + markerStreams = [] + self.markerStream = None + for stream in streams: + if stream.type() in self.markerTypes: + markerStreams.append(StreamInlet(stream)) + if self.markerStreamPredefined: + if len(markerStreams) > 1: + self.logger.log(Logger.WARNING," Too many Marker streams available, set single desired markerStream in bci.lslScanner.markerStream correctly.") + for s in markerStreams: + name = s.info().name() + if name != self.markerStreamName: + self.logger.log(Logger.WARNING," Predefined LSL Marker Stream name not present.") + self.logger.log(Logger.WARNING, " Available Streams: "+str([s.info().name() for s in markerStreams])) + else: + self.markerStream = s + else: + if len(markerStreams) > 0: + self.markerStream = markerStreams[0] # if none specified grabs first avaialble marker stream + + def CheckAvailableLSL(self): + """Checks streaminlets available, + Returns + ------- + bool : + True if 1 marker stream present and available datastreams are present. + False if no datastreams are present and/or more or less then one marker stream is present, requires hard selection or markser stream if too many. + """ + self.ScanStreams() + if self.markerStream == None: + self.logger.log(Logger.WARNING," No Marker streams available, make sure your accepted marker data Type have been set in bci.lslScanner.markerTypes correctly.") + if len(self.dataStreams) == 0: + self.logger.log(Logger.WARNING," No data streams available, make sure your streamTypes have been set in bci.lslScanner.dataStream correctly.") + if len(self.dataStreams) > 0 and self.markerStream !=None: + self.logger.log(Logger.INFO," Success - "+str(len(self.dataStreams))+" data stream(s) found, 1 marker stream found") + + if len(self.dataStreams) > 0 and self.markerStream != None: + self.parent.dataStreams = self.dataStreams + self.parent.markerStream = self.markerStream + return True + else: + return False \ No newline at end of file diff --git a/build/lib/pybci/Utils/Logger.py b/build/lib/pybci/Utils/Logger.py new file mode 100644 index 0000000..8b83294 --- /dev/null +++ b/build/lib/pybci/Utils/Logger.py @@ -0,0 +1,32 @@ +class Logger: + INFO = "INFO" + WARNING = "WARNING" + NONE = "NONE" + TIMING = "TIMING" + + def __init__(self, level=INFO): + self.level = level + self.check_level(level) + #print(self.level) + def set_level(self, level): + self.level = level + self.check_level(level) + + def check_level(self,level): + if level != self.WARNING and level != self.INFO and level != self.NONE and level != self.TIMING : + print("PyBCI: [INFO] - Invalid or no log level selected, defaulting to info. (options: info, warning, none)") + level = self.INFO + self.level = level + + def log(self, level, message): + if self.level == self.NONE: + return None + if level == self.INFO: + if self.level != self.NONE and self.level != self.WARNING: + print('PyBCI: [INFO] -' + message) + elif level == self.WARNING: + if self.level != self.NONE: + print('PyBCI: [WARNING] -' + message) + elif level == self.TIMING: + if self.level == self.TIMING: + print('PyBCI: [TIMING] -' + message) \ No newline at end of file diff --git a/build/lib/pybci/Utils/PseudoDevice.py b/build/lib/pybci/Utils/PseudoDevice.py new file mode 100644 index 0000000..4c56355 --- /dev/null +++ b/build/lib/pybci/Utils/PseudoDevice.py @@ -0,0 +1,130 @@ +################ PsuedoDevice.py ######################## +# used for creating fake LSL device data and markers # +# Please note! sample rate is not exact, # +# expect some drop over time! # +# Written by Liam Booth 19/08/2023 # +################################################# +from ..Utils.Logger import Logger +import random, time, threading, pylsl +import numpy as np +from collections import deque + +def precise_sleep(duration): + end_time = time.time() + duration + while time.time() < end_time: + pass + +class PseudoDataConfig: + duration = 1.0 + noise_level = 0.1 + amplitude = 0.2 + frequency = 1.0 + +class PseudoDevice: + + def __init__(self, markerName = "PyBCIPsuedoMarkers", markerType = "Markers", markerConfigStrings = ["Marker1", "Marker2", "Marker3", "Marker4", "Marker5"], + pseudoDataConfigs = None, + baselineMarkerString = "baseline", baselineConfig = PseudoDataConfig(), + dataStreamName = "PyBCIPsuedoDataStream" , dataStreamType="EMG", + sampleRate= 250, channelCount= 8, logger = Logger(Logger.INFO)): + self.markerConfigStrings = markerConfigStrings + if pseudoDataConfigs == None: + pseudoDataConfigs = [PseudoDataConfig(), PseudoDataConfig(), PseudoDataConfig(), PseudoDataConfig(),PseudoDataConfig()] + pseudoDataConfigs[0].amplitude = pseudoDataConfigs[0].amplitude*2 + pseudoDataConfigs[1].amplitude = pseudoDataConfigs[1].amplitude*3 + pseudoDataConfigs[2].amplitude = pseudoDataConfigs[2].amplitude*4 + pseudoDataConfigs[3].amplitude = pseudoDataConfigs[3].amplitude*5 + pseudoDataConfigs[4].amplitude = pseudoDataConfigs[4].amplitude*6 + pseudoDataConfigs[0].amplitude = pseudoDataConfigs[0].frequency*2 + pseudoDataConfigs[1].amplitude = pseudoDataConfigs[1].frequency*3 + pseudoDataConfigs[2].amplitude = pseudoDataConfigs[2].frequency*4 + pseudoDataConfigs[3].amplitude = pseudoDataConfigs[3].frequency*5 + pseudoDataConfigs[4].amplitude = pseudoDataConfigs[4].frequency*6 + self.pseudoDataConfigs = pseudoDataConfigs + self.sampleRate = sampleRate + self.channelCount = channelCount + markerInfo = pylsl.StreamInfo(markerName, markerType, 1, 0, 'string', 'Dev') + self.markerOutlet = pylsl.StreamOutlet(markerInfo) + info = pylsl.StreamInfo(dataStreamName, dataStreamType, self.channelCount, self.sampleRate, 'float32', 'Dev') + chns = info.desc().append_child("channels") + for label in range(self.channelCount): + ch = chns.append_child("channel") + ch.append_child_value("label", str(label+1)) + ch.append_child_value("type", dataStreamType) + self.outlet = pylsl.StreamOutlet(info) + + + def GeneratePseudoEMG(self,samplingRate, duration, noise_level, amplitude, frequency): + """ + Generate a pseudo EMG signal for a given gesture. + Arguments: + - sampling_rate: Number of samples per second + - duration: Duration of the signal in seconds + - noise_level: The amplitude of Gaussian noise to be added (default: 0.1) + - amplitude: The amplitude of the EMG signal (default: 1.0) + - frequency: The frequency of the EMG signal in Hz (default: 10.0) + Returns: + - emg_signal: The generated pseudo EMG signal as a 2D numpy array with shape (channels, samples) + """ + num_samples = int(duration * samplingRate) + # Initialize the EMG signal array + emg_signal = np.zeros((self.totchs, num_samples)) + # Generate the pseudo EMG signal for each channel + for channel in range(self.totchs): + # Calculate the time values for the channel + times = np.linspace(0, duration, num_samples) + # Generate the pseudo EMG signal based on the selected gesture + emg_channel = amplitude * np.sin(2 * np.pi * frequency * times)# * times) # Sinusoidal EMG signal + # Add Gaussian noise to the EMG signal + noise = np.random.normal(0, noise_level, num_samples) + emg_channel += noise + # Store the generated channel in the EMG signal array + emg_signal[channel, :] = emg_channel + return emg_signal + + + def update(self): + with self.lock: # Acquire the lock + if self.markerOccurred: + for i, command in enumerate(self.commandStrings): + if self.currentMarker == command: + num_samples = int(self.commandDataConfigs[i].duration/10 * self.sampleRate) + [self.y[0].popleft() for d in range(num_samples)] + num = self.GeneratePseudoEMG(self.sampleRate,self.commandDataConfigs[i].duration/10, self.commandDataConfigs[i].noise_level, + self.commandDataConfigs[i].amplitude, self.commandDataConfigs[i].frequency) + self.chunkCount += 1 + if self.chunkCount >= 10: + self.markerOccurred = False + self.chunkCount = 0 + else:# send baseline + num_samples = int(self.baselineConfig.duration/10 * self.sampleRate) + [self.y[0].popleft() for d in range(num_samples)] + num = self.GeneratePseudoEMG(self.sampleRate,self.baselineConfig.duration/10, self.baselineConfig.noise_level, + self.baselineConfig.amplitude, self.baselineConfig.frequency) + [self.y[0].extend([num[0][d]]) for d in range(num_samples)] + for n in range(num.shape[1]): + self.outlet.push_sample(num[:,n]) + + def StopTest(self): + self.stop_signal = True + self.thread.join() # wait for the thread to finish + + def BeginTest(self): + self.stop_signal = False + self.thread = threading.Thread(target=self._generate_signal) + self.thread.start() + + def _generate_signal(self): + while not self.stop_signal: + start_time = time.time() + self.update() + sleep_duration = max(0, (1.0 / 10) - (start_time - time.time())) + precise_sleep(sleep_duration) + + def SendMarker(self): + with self.lock: # Acquire the lock + self.markerOutlet.push_sample([self.currentMarker]) + self.markerOccurred = True + + def SendBaseline(self): + self.markerOutlet.push_sample(["Baseline"]) \ No newline at end of file diff --git a/build/lib/pybci/Utils/__init__.py b/build/lib/pybci/Utils/__init__.py new file mode 100644 index 0000000..fba0175 --- /dev/null +++ b/build/lib/pybci/Utils/__init__.py @@ -0,0 +1,4 @@ +#from .Classifier import Classifier +#from .FeatureExtractor import FeatureExtractor +#from .LSLScanner import LSLScanner + diff --git a/build/lib/pybci/__init__.py b/build/lib/pybci/__init__.py new file mode 100644 index 0000000..65e1181 --- /dev/null +++ b/build/lib/pybci/__init__.py @@ -0,0 +1,2 @@ +from .pybci import PyBCI +#from .Utils import Classifier, FeatureExtractor, LSLScanner \ No newline at end of file diff --git a/build/lib/pybci/pybci.py b/build/lib/pybci/pybci.py new file mode 100644 index 0000000..0a84d5f --- /dev/null +++ b/build/lib/pybci/pybci.py @@ -0,0 +1,348 @@ +from .Utils.LSLScanner import LSLScanner +from .Utils.Logger import Logger +from .ThreadClasses.FeatureProcessorThread import FeatureProcessorThread +from .ThreadClasses.DataReceiverThread import DataReceiverThread +from .ThreadClasses.AsyncDataReceiverThread import AsyncDataReceiverThread +from .ThreadClasses.OptimisedDataReceiverThread import OptimisedDataReceiverThread +from .ThreadClasses.MarkerThread import MarkerThread +from .ThreadClasses.ClassifierThread import ClassifierThread +from .Configuration.EpochSettings import GlobalEpochSettings, IndividualEpochSetting +from .Configuration.FeatureSettings import GeneralFeatureChoices +import queue, threading, copy +import tensorflow as tf +#import torch +import torch.nn as nn + +#tf.get_logger().setLevel('ERROR') + +class PyBCI: + globalEpochSettings = GlobalEpochSettings() + customEpochSettings = {} + minimumEpochsRequired = 10 + markerThread = [] + dataThreads = [] + streamChsDropDict= {} + dataStreams = [] + markerStream = None + connected = False + epochCounts = {} # holds markers received, their target ids and number received of each + classifierInformation = [] + clf= None + model = None + torchModel = None + def __init__(self, dataStreams = None, markerStream= None, streamTypes = None, markerTypes = None, loggingLevel = Logger.INFO, + globalEpochSettings = GlobalEpochSettings(), customEpochSettings = {}, streamChsDropDict = {}, + streamCustomFeatureExtract = {}, + minimumEpochsRequired = 10, clf= None, model = None, torchModel = None): + """ + The PyBCI object stores data from available lsl time series data streams (EEG, pupilometry, EMG, etc.) + and holds a configurable number of samples based on lsl marker strings. + If no marker strings are available on the LSL the class will close and return an error. + Parameters + ---------- + dataStreams: List[str] + Allows user to set custom acceptable EEG stream definitions, if None defaults to streamTypes scan + markerStream: List[str] + Allows user to set custom acceptable Marker stream definitions, if None defaults to markerTypes scan + streamTypes: List[str] + Allows user to set custom acceptable EEG type definitions, ignored if dataStreams not None + markerTypes: List[str] + Allows user to set custom acceptable Marker type definitions, ignored if markerStream not None + loggingLevel: string + Sets PyBCI print level, ('INFO' prints all statements, 'WARNING' is only warning messages, 'TIMING' gives estimated time for feature extraction, and classifier training or testing, 'NONE' means no prints from PyBCI) + globalEpochSettings (GlobalEpochSettings): + Sets global timing settings for epochs. + customEpochSettings: dict + Sets individual timing settings for epochs. {markerstring1:IndividualEpochSettings(),markerstring2:IndividualEpochSettings()} + streamChsDropDict: dict + Keys for dict should be respective datastreams with corresponding list of which channels to drop. {datastreamstring1: list(ints), datastreamstring2: list(ints)} + streamCustomFeatureExtract: dict + Allows dict to be passed of datastream with custom feature extractor class for analysing data. {datastreamstring1: customClass1(), datastreamstring2: customClass1(),} + minimumEpochsRequired: int + Minimm number of required epochs before model fitting begins, must be of each type of received markers and mroe then 1 type of marker to classify. + clf: sklearn.base.ClassifierMixin + Allows custom Sklearn model to be passed. + model: tf.keras.model + Allows custom tensorflow model to be passed. + torchmodel: [torchModel(), torch.nn.Module] + Currently a list where first item is torchmodel analysis function, second is torch model, check pytorch example - likely to change in future updates. + """ + self.streamCustomFeatureExtract = streamCustomFeatureExtract + self.globalEpochSettings = globalEpochSettings + self.customEpochSettings = customEpochSettings + self.streamChsDropDict = streamChsDropDict + self.loggingLevel = loggingLevel + self.logger = Logger(self.loggingLevel) + self.lslScanner = LSLScanner(self, dataStreams, markerStream,streamTypes, markerTypes, logger =self.logger) + self.ConfigureMachineLearning(minimumEpochsRequired, clf, model, torchModel) # configure first, connect second + self.Connect() + + def __enter__(self, dataStreams = None, markerStream= None, streamTypes = None, markerTypes = None, loggingLevel = Logger.INFO, + globalEpochSettings = GlobalEpochSettings(), customEpochSettings = {}, streamChsDropDict = {}, + streamCustomFeatureExtract = {}, + minimumEpochsRequired = 10, clf= None, model = None, torchModel = None): # with bci + """ + Please look at PyBCI.__init__ (same parameters, setup and description) + """ + self.streamCustomFeatureExtract = streamCustomFeatureExtract + self.globalEpochSettings = globalEpochSettings + self.customEpochSettings = customEpochSettings + self.streamChsDropDict = streamChsDropDict + self.lslScanner = LSLScanner(self, dataStreams, markerStream,streamTypes, markerTypes) + self.loggingLevel = loggingLevel + self.ConfigureMachineLearning(minimumEpochsRequired, clf, model, torchModel) # configure first, connect second + self.Connect() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.StopThreads() + + def Connect(self): # Checks valid data and markers streams are present, controls dependant functions by setting self.connected + if self.lslScanner.CheckAvailableLSL(): + self.__StartThreads() + self.connected = True + return True # uses return statements so user can check if connected with bool returned + else: + self.connected = False + return False + + # set test and train boolean for changing thread operation + def TrainMode(self): + """ + Starts BCI training If PyBCI is connected to valid LSL data and marker streams, if not tries to scan and connect. + """ + if self.connected: + self.logger.log(Logger.INFO," Started training...") + self.trainTestEvent.set() + else: + self.Connect() + + def TestMode(self): + """ + Starts BCI testing If PyBCI is connected to valid LSL data and marker streams, if not tries to scan and connect. + (Need to check if invalid number of epochs are obtained and this is set) + """ + if self.connected: + self.logger.log(Logger.INFO," Started testing...") + self.trainTestEvent.clear() + else: + self.Connect() + + # Get data from threads + def CurrentClassifierInfo(self): + """ + Gets dict with current clf, model, torchModel and accuracy. Accuracy will be 0 if not fiting has occurred. + Returns + ------- + classInfo: dict + dict of "clf", "model, "torchModel"" and "accuracy" where accuracy is 0 if no model training/fitting has occurred. If mode not used corresponding value is None. + If not connected returns {"Not Connected": None} + """ + if self.connected: + self.classifierInfoRetrieveEvent.set() + classInfo = self.classifierInfoQueue.get() + self.classifierInfoRetrieveEvent.clear() + return classInfo + else: + self.Connect() + return {"Not Connected": None} + + def CurrentClassifierMarkerGuess(self): + """ + Gets classifier current marker guess and targets. + Returns + ------- + classGuess: int | None + Returned int correlates to value of key from dict from ReceivedMarkerCount() when in testmode. + If in trainmode returns None. + """ + if self.connected: + # probably needs check that we're in test mode, maybe debu print if not? + self.classifierGuessMarkerEvent.set() + classGuess = self.classifierGuessMarkerQueue.get() + self.classifierGuessMarkerEvent.clear() + return classGuess + else: + self.Connect() + return None + + def CurrentFeaturesTargets(self): + """ + Gets classifier current features and targets. + Returns + ------- + featureTargets: dict + dict of "features" and "targets" where features is 2d list of feature data and targets is a 1d list of epoch targets as ints. + If not connected returns {"Not Connected": None} + """ + if self.connected: + self.queryFeaturesEvent.set() + featureTargets = self.queryFeaturesQueue.get() + self.queryFeaturesEvent.clear() # still needs coding + return featureTargets + else: + self.Connect() + return {"Not Connected": None} + + def ReceivedMarkerCount(self): + """ + Gets number of received training marker, their strings and their respective values to correlate with CurrentClassifierMarkerGuess(). + Returns + ------- + markers: dict + Every key is a string received on the selected LSL marker stream, the value is a list where the first item is the marker id value, + use with CurrentClassifierMarkerGuess() the second value is a received count for that marker type. Will be empty if no markers received. + """ + if self.connected: + self.markerCountRetrieveEvent.set() + markers = self.markerCountQueue.get() + self.markerCountRetrieveEvent.clear() + return markers + else: + self.Connect() + + def __StartThreads(self): + self.featureQueueTrain = queue.Queue() + self.featureQueueTest = queue.Queue() + self.classifierInfoQueue = queue.Queue() + self.markerCountQueue = queue.Queue() + self.classifierGuessMarkerQueue = queue.Queue() + self.classifierGuessMarkerEvent = threading.Event() + self.closeEvent = threading.Event() # used for closing threads + self.trainTestEvent = threading.Event() + self.markerCountRetrieveEvent = threading.Event() + self.classifierInfoRetrieveEvent = threading.Event() + + self.queryFeaturesQueue = queue.Queue() + self.queryFeaturesEvent = threading.Event() # still needs coding + + self.trainTestEvent.set() # if set we're in train mode, if not we're in test mode, always start in train... + self.logger.log(Logger.INFO," Starting threads initialisation...") + # setup data thread + self.dataThreads = [] + self.featureThreads = [] + for stream in self.dataStreams: + self.dataQueueTrain = queue.Queue() + self.dataQueueTest = queue.Queue() + + #if stream.info().nominal_srate() == 0: + # if stream.info().name() in self.streamChsDropDict.keys(): + # dt = AsyncDataReceiverThread(self.closeEvent, self.trainTestEvent, self.dataQueueTrain,self.dataQueueTest, stream, self.customEpochSettings, + # self.globalEpochSettings, len(self.dataThreads), streamChsDropDict=self.streamChsDropDict[stream.info().name()]) + # else: + # dt = AsyncDataReceiverThread(self.closeEvent, self.trainTestEvent, self.dataQueueTrain,self.dataQueueTest, stream, self.customEpochSettings, + # self.globalEpochSettings, len(self.dataThreads)) + #else: # cold be desirable to capture samples only relative to timestammps with async, so maybe make this configurable? + #if stream.info().nominal_srate() == 0: + print(len(self.dataThreads)) + print(stream.info().name()) + if stream.info().name() in self.streamChsDropDict.keys(): ## all use optimised now (pull_chunk and timestamp relative) + #print(self.streamChsDropDict[stream.info().name()]) + dt = OptimisedDataReceiverThread(self.closeEvent, self.trainTestEvent, self.dataQueueTrain,self.dataQueueTest, stream, self.customEpochSettings, + self.globalEpochSettings, len(self.dataThreads), streamChsDropDict=self.streamChsDropDict[stream.info().name()]) + else: + dt = OptimisedDataReceiverThread(self.closeEvent, self.trainTestEvent, self.dataQueueTrain,self.dataQueueTest, stream, self.customEpochSettings, + self.globalEpochSettings, len(self.dataThreads)) + #else: + # if stream.info().name() in self.streamChsDropDict.keys(): ## all use optimised now (pull_chunk and timestamp relative) + # #print(self.streamChsDropDict[stream.info().name()]) + # dt = OptimisedDataReceiverThread(self.closeEvent, self.trainTestEvent, self.dataQueueTrain,self.dataQueueTest, stream, self.customEpochSettings, + # self.globalEpochSettings, len(self.dataThreads), streamChsDropDict=self.streamChsDropDict[stream.info().name()], maxExpectedSampleRate = stream.info().nominal_srate()) + # else: + # dt = OptimisedDataReceiverThread(self.closeEvent, self.trainTestEvent, self.dataQueueTrain,self.dataQueueTest, stream, self.customEpochSettings, + # self.globalEpochSettings, len(self.dataThreads),maxExpectedSampleRate = stream.info().nominal_srate()) + dt.start() + self.dataThreads.append(dt) + if stream.info().name() in self.streamCustomFeatureExtract.keys(): + self.ft = FeatureProcessorThread(self.closeEvent,self.trainTestEvent, self.dataQueueTrain, self.dataQueueTest, + self.featureQueueTest,self.featureQueueTrain, len(self.dataStreams), + self.markerCountRetrieveEvent, self.markerCountQueue,logger=self.logger, + featureExtractor = self.streamCustomFeatureExtract[stream.info().name()], + globalEpochSettings = self.globalEpochSettings, customEpochSettings = self.customEpochSettings) + else: + self.ft = FeatureProcessorThread(self.closeEvent,self.trainTestEvent, self.dataQueueTrain, self.dataQueueTest, + self.featureQueueTest,self.featureQueueTrain, len(self.dataStreams), + self.markerCountRetrieveEvent, self.markerCountQueue,logger=self.logger, + globalEpochSettings = self.globalEpochSettings, customEpochSettings = self.customEpochSettings) + self.ft.start() + self.featureThreads.append(dt) + # marker thread requires data and feature threads to push new markers too + self.markerThread = MarkerThread(self.closeEvent,self.trainTestEvent, self.markerStream,self.dataThreads, self.featureThreads) + self.markerThread.start() + self.classifierThread = ClassifierThread(self.closeEvent,self.trainTestEvent, self.featureQueueTest,self.featureQueueTrain, + self.classifierInfoQueue, self.classifierInfoRetrieveEvent, + self.classifierGuessMarkerQueue, self.classifierGuessMarkerEvent, self.queryFeaturesQueue, self.queryFeaturesEvent, + logger = self.logger, numStreamDevices = len(self.dataThreads), minRequiredEpochs = self.minimumEpochsRequired, + clf = self.clf, model = self.model, torchModel = self.torchModel) + self.classifierThread.start() + self.logger.log(Logger.INFO," Threads initialised.") + + + def StopThreads(self): + """ + Stops all PyBCI threads. + """ + self.closeEvent.set() + self.markerThread.join() + # wait for all threads to finish processing, probably worth pulling out finalised classifier information stored for later use. + for dt in self.dataThreads: + dt.join() + for ft in self.featureThreads: + ft.join() + self.classifierThread.join() + self.connected = False + self.logger.log(Logger.INFO," Threads stopped.") + + def ConfigureMachineLearning(self, minimumEpochsRequired = 10, clf = None, model = None, torchModel = None): + from sklearn.base import ClassifierMixin + self.minimumEpochsRequired = minimumEpochsRequired + + if isinstance(clf, ClassifierMixin): + self.clf = clf + else: + self.clf = None + self.logger.log(Logger.INFO," Invalid or no sklearn classifier passed to clf. Checking tensorflow model... ") + if isinstance(model, tf.keras.Model): + self.model = model + else: + self.model = None + self.logger.log(Logger.INFO," Invalid or no tensorflow model passed to model. Checking pytorch torchModel...") + if callable(torchModel): # isinstance(torchModel, torch.nn.Module): + self.torchModel = torchModel + else: + self.torchModel = None + self.logger.log(Logger.INFO," Invalid or no PyTorch model passed to model. Defaulting to SVM by SkLearn") + + + # Could move all configures to a configuration class, might make options into more descriptive classes? + def ConfigureEpochWindowSettings(self, globalEpochSettings = GlobalEpochSettings(), customEpochSettings = {}): + """ + Allows globalWindowSettings to be modified, customWindowSettings is a dict with value names for marker strings which will appear on avalable markerStreams. + """ + valid = False + for key in customEpochSettings.keys(): + if isinstance(customEpochSettings[key], IndividualEpochSetting): + valid = True + else: + valid = False + self.logger.log(Logger.WARNING," Invalid datatype passed for customWindowSettings, create dict of wanted markers \ + using class bci.IndividualEpochSetting() as value to configure individual epoch window settings.") + break + #if isinstance(customWindowSettings[key], GlobalEpochSettings()): + if valid: + self.customEpochSettings = customEpochSettings + if globalEpochSettings.windowLength > globalEpochSettings.tmax + globalEpochSettings.tmin: + self.logger.log(Logger.WARNING," windowLength < (tmin+tmax), pass vaid settings to ConfigureEpochWindowSettings") + else: + self.globalWindowglobalEpochSettingsSettings = globalEpochSettings + self.ResetThreadsAfterConfigs() + + def ConfigureDataStreamChannels(self,streamChsDropDict = {}): + # potentially should move configuration to generic class which can be used for both test and train + self.streamChsDropDict = streamChsDropDict + self.ResetThreadsAfterConfigs() + + def ResetThreadsAfterConfigs(self): + if self.connected: + self.logger.log(Logger.INFO,"Resetting threads after BCI reconfiguration...") + self.StopThreads() + self.Connect() diff --git a/build/lib/pybci/version.py b/build/lib/pybci/version.py new file mode 100644 index 0000000..a6221b3 --- /dev/null +++ b/build/lib/pybci/version.py @@ -0,0 +1 @@ +__version__ = '1.0.2' diff --git a/dist/install_pybci-1.0.0-py3.11.egg b/dist/install_pybci-1.0.0-py3.11.egg new file mode 100644 index 0000000000000000000000000000000000000000..e5b6463346d37937e56f5598f5ee2a8dddf91189 GIT binary patch literal 76776 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 literal 0 HcmV?d00001 diff --git a/dist/install_pybci-1.0.2-py3.11.egg b/dist/install_pybci-1.0.2-py3.11.egg new file mode 100644 index 0000000000000000000000000000000000000000..587c3c1a1872b651e1f7ea737090e07b69f72814 GIT binary patch literal 76913 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⁡-it{V?e*pY&h#N0Q?O>mmC;u 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

A+`VYHt9+T7MDAIQ)B!H0JD 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 ATmS$7 literal 0 HcmV?d00001 diff --git a/install_pybci.egg-info/PKG-INFO b/install_pybci.egg-info/PKG-INFO new file mode 100644 index 0000000..5602234 --- /dev/null +++ b/install_pybci.egg-info/PKG-INFO @@ -0,0 +1,102 @@ +Metadata-Version: 2.1 +Name: install-pybci +Version: 1.0.2 +Summary: A Python interface to create a BCI with the Lab Streaming Layer, Pytorch, SciKit-Learn and Tensorflow packages +Home-page: https://github.com/lmbooth/pybci +Author: Liam Booth +Author-email: liambooth123@hotmail.co.uk +License: MIT +Keywords: machine-learning tensorflow sklearn pytorch human-computer-interaction bci lsl brain-computer-interface labstreaminglayer +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Science/Research +Classifier: Intended Audience :: Healthcare Industry +Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces +Classifier: Topic :: Scientific/Engineering :: Bio-Informatics +Classifier: Topic :: Scientific/Engineering :: Medical Science Apps. +Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence +Classifier: Topic :: Scientific/Engineering +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: MacOS +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-File: LICENSE + +[![PyPI - Downloads](https://img.shields.io/pypi/dm/install-pybci)](https://pypi.org/project/install-pybci) [![PyPI - version](https://img.shields.io/pypi/v/install-pybci)](https://pypi.org/project/install-pybci) [![Documentation Status](https://readthedocs.org/projects/pybci/badge/?version=latest)](https://pybci.readthedocs.io/en/latest/?badge=latest) + +[![pybci](https://raw.githubusercontent.com/LMBooth/pybci/main/docs/Images/pyBCITitle.svg)](https://github.com/LMBooth/pybci) + +A Python package to create real-time Brain Computer Interfaces (BCI's). Data synchronisation and pipelining handled by the [Lab Streaming Layer](https://github.com/sccn/labstreaminglayer), machine learning with [Pytorch](https://pytorch.org/), [scikit-learn](https://scikit-learn.org/stable/#) or [TensorFlow](https://www.tensorflow.org/install), leveraging packages like [AntroPy](https://github.com/raphaelvallat/antropy), [SciPy](https://scipy.org/) and [NumPy](https://numpy.org/) for generic time and/or frequency based feature extraction or optionally have the users own custom feature extraction class used. + +The goal of PyBCI is to enable quick iteration when creating pipelines for testing human machine and brain computer interfaces, namely testing applied data processing and feature extraction techniques on custom machine learning models. Training the BCI requires LSL enabled devices and an LSL marker stream for training stimuli. (The [examples folder](https://github.com/LMBooth/pybci/tree/main/pybci/Examples) found on the github has a [pseudo LSL data generator and marker creator](https://github.com/LMBooth/pybci/tree/main/pybci/Examples/PsuedoLSLStreamGenerator) in the [mainSend.py](https://github.com/LMBooth/pybci/tree/main/pybci/Examples/PsuedoLSLStreamGenerator/mainSend.py) file so the examples can run without the need of LSL capable hardware.) + +# Installation +For stable releases use: ```pip install install-pybci``` + +For unstable dev installations and up-to-date git pushes use: ```pip install --index-url https://test.pypi.org/simple/ install-pybci``` + +## Prerequisite for Non-Windows Users +If you are not using windows then there is a prerequisite stipulated on the [pylsl repository](https://github.com/labstreaminglayer/pylsl) to obtain a liblsl shared library. See the [liblsl repo documentation](https://github.com/sccn/liblsl) for more information. +Once the liblsl library has been downloaded ```pip install install-pybci``` should work. + +(currently using install-pybci due to pybci having name too similar with another package on pypi, [issue here.](https://github.com/pypi/support/issues/2840)) + +[ReadTheDocs available here!](https://pybci.readthedocs.io/en/latest/) [Examples found here!](https://github.com/LMBooth/pybci/tree/main/pybci/Examples) + +[Examples of supported LSL hardware here!](https://labstreaminglayer.readthedocs.io/info/supported_devices.html) + +## Python Package Dependencies Version Minimums +The following package versions define the minimum supported by PyBCI, also defined in setup.py: + + "pylsl>=1.16.1", + "scipy>=1.11.1", + "numpy>=1.24.3", + "antropy>=0.1.6", + "tensorflow>=2.13.0", + "scikit-learn>=1.3.0", + "torch>=2.0.1" + +Earlier packages may work but are not guaranteed to be supported. + +## Basic implementation +```python +import time +from pybci import PyBCI +bci = PyBCI() # set default epoch timing, looks for first available lsl marker stream and all data streams +bci.TrainMode() # assume both marker and datastreams available to start training on received epochs +accuracy = 0 +try: + while(True): # training based on couple epochs more then min threshold for classifying + currentMarkers = bci.ReceivedMarkerCount() # check to see how many received epochs, if markers sent are too close together will be ignored till done processing + time.sleep(1) # wait for marker updates + print("Markers received: " + str(currentMarkers) +" Class accuracy: " + str(accuracy), end="\r") + if len(currentMarkers) > 1: # check there is more then one marker type received + if min([currentMarkers[key][1] for key in currentMarkers]) > bci.minimumEpochsRequired: + classInfo = bci.CurrentClassifierInfo() # hangs if called too early + accuracy = classInfo["accuracy"] + if min([currentMarkers[key][1] for key in currentMarkers]) > bci.minimumEpochsRequired+1: + bci.TestMode() + break + while True: # now sufficient epochs gathered start testing + markerGuess = bci.CurrentClassifierMarkerGuess() # when in test mode only y_pred returned + guess = [key for key, value in currentMarkers.items() if value[0] == markerGuess] + print("Current marker estimation: " + str(guess), end="\r") + time.sleep(0.5) +except KeyboardInterrupt: # allow user to break while loop + pass +``` + +## Background Information +PyBCI is a python brain computer interface software designed to receive a varying number, be it singular or multiple, Lab Streaming Layer enabled data streams. An understanding of time-series data analysis, the lab streaming layer protocol, and machine learning techniques are a must to integrate innovative ideas with this interface. An LSL marker stream is required to train the model, where a received marker epochs the data received on the accepted datastreams based on a configurable time window around certain markers - where custom marker strings can optionally have its epoch timewindow split and overlapped to count as more then one marker, example: in training mode a baseline marker may have one marker sent for a 60 second window, whereas target actions may only be ~0.5s long, when testing the model and data is constantly analysed it would be desirable to standardise the window length, we do this by splitting the 60s window after the received baseline marker in to ~0.5s windows. PyBCI allows optional overlapping of time windows to try to account for potential missed signal patterns/aliasing - as a rule of thumb it would be advised when testing a model to have a time window overlap >= 50% (Shannon-Nyquist criterion). [See here for more information on epoch timing](https://pybci.readthedocs.io/en/latest/BackgroundInformation/Epoch_Timing.html). + +Once the data has been epoched it is sent for feature extraction, there is a general feature extraction class which can be configured for general time and/or frequency analysis based features, ideal for data stream types like "EEG" and "EMG". Since data analysis, preprocessing and feature extraction trechniques can vary greatly between device data inputs, a custom feature extraction class can be created for each data stream maker type. [See here for more information on feature extraction](https://pybci.readthedocs.io/en/latest/BackgroundInformation/Feature_Selection.html). + +Finally a passable pytorch, sklearn or tensorflow classifier can be given to the bci class, once a defined number of epochs have been obtained for each received epoch/marker type the classifier can begin to fit the model. It's advised to use bci.ReceivedMarkerCount() to get the number of received training epochs received, once the min num epochs received of each type is >= pybci.minimumEpochsRequired (default 10 of each epoch) the model will begin to fit. Once fit the classifier info can be queried with CurrentClassifierInfo, this returns the model used and accuracy. If enough epochs are received or high enough accuracy is obtained TestMode() can be called. Once in test mode you can query what pybci estimates the current bci epoch is(typically baseline is used for no state). [Review the examples for sklearn and model implementations](https://pybci.readthedocs.io/en/latest/BackgroundInformation/Examples.html). + +## All issues, recommendations, pull-requests and suggestions are welcome and encouraged! diff --git a/install_pybci.egg-info/SOURCES.txt b/install_pybci.egg-info/SOURCES.txt new file mode 100644 index 0000000..e66f9e3 --- /dev/null +++ b/install_pybci.egg-info/SOURCES.txt @@ -0,0 +1,29 @@ +LICENSE +MANIFEST.in +README.md +setup.py +install_pybci.egg-info/PKG-INFO +install_pybci.egg-info/SOURCES.txt +install_pybci.egg-info/dependency_links.txt +install_pybci.egg-info/requires.txt +install_pybci.egg-info/top_level.txt +pybci/__init__.py +pybci/pybci.py +pybci/version.py +pybci/Configuration/EpochSettings.py +pybci/Configuration/FeatureSettings.py +pybci/Configuration/PsuedoDeviceSettings.py +pybci/Configuration/__init__.py +pybci/ThreadClasses/AsyncDataReceiverThread.py +pybci/ThreadClasses/ClassifierThread.py +pybci/ThreadClasses/DataReceiverThread.py +pybci/ThreadClasses/FeatureProcessorThread.py +pybci/ThreadClasses/MarkerThread.py +pybci/ThreadClasses/OptimisedDataReceiverThread.py +pybci/ThreadClasses/__init__.py +pybci/Utils/Classifier.py +pybci/Utils/FeatureExtractor.py +pybci/Utils/LSLScanner.py +pybci/Utils/Logger.py +pybci/Utils/PseudoDevice.py +pybci/Utils/__init__.py \ No newline at end of file diff --git a/install_pybci.egg-info/dependency_links.txt b/install_pybci.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/install_pybci.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/install_pybci.egg-info/requires.txt b/install_pybci.egg-info/requires.txt new file mode 100644 index 0000000..f93d47e --- /dev/null +++ b/install_pybci.egg-info/requires.txt @@ -0,0 +1,7 @@ +pylsl>=1.16.1 +scipy>=1.11.1 +numpy>=1.24.3 +antropy>=0.1.6 +tensorflow>=2.13.0 +scikit-learn>=1.3.0 +torch>=2.0.1 diff --git a/install_pybci.egg-info/top_level.txt b/install_pybci.egg-info/top_level.txt new file mode 100644 index 0000000..3064b26 --- /dev/null +++ b/install_pybci.egg-info/top_level.txt @@ -0,0 +1 @@ +pybci diff --git a/pybci/Configuration/FeatureSettings.py b/pybci/Configuration/FeatureSettings.py index f387cc7..406b851 100644 --- a/pybci/Configuration/FeatureSettings.py +++ b/pybci/Configuration/FeatureSettings.py @@ -1,5 +1,5 @@ class GeneralFeatureChoices: - psdBand = True + psdBand = False appr_entropy = False perm_entropy = False spec_entropy = False @@ -7,9 +7,9 @@ class GeneralFeatureChoices: samp_entropy = False rms = True meanPSD = True - medianPSD = True - variance = True - meanAbs = True + medianPSD = False + variance = False + meanAbs = False waveformLength = False zeroCross = False slopeSignChange = False \ No newline at end of file diff --git a/pybci/Configuration/PseudoDeviceSettings.py b/pybci/Configuration/PseudoDeviceSettings.py new file mode 100644 index 0000000..22789de --- /dev/null +++ b/pybci/Configuration/PseudoDeviceSettings.py @@ -0,0 +1,18 @@ + +class PseudoDataConfig: + duration = 1.0 + noise_level = 0.5 + amplitude = 2 + frequency = 5 + +class PseudoMarkerConfig: + markerName = "PyBCIPseudoMarkers" + markerType = "Markers" + baselineMarkerString = "baseline" + repeat = True + autoplay = True + num_baseline_markers = 10 + number_marker_iterations = 10 + seconds_between_markers = 5 + seconds_between_baseline_marker = 10 + baselineConfig = PseudoDataConfig() \ No newline at end of file diff --git a/pybci/Configuration/__pycache__/EpochSettings.cpython-311.pyc b/pybci/Configuration/__pycache__/EpochSettings.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15426cfa0baac8c3d9c5485568de40f2871921b2 GIT binary patch literal 788 zcmZ{i%}(1u5XWcz5mHFnLRyNDdf|d{Xc6iSA$mw!)G7@iI9ZWaj@LLVoV8gy&|E4X z`VPE7tEw*)`BbS=C2m3C#EBgnL;2d({yek)o$+|qKdaSgP~+pbe{4qw_=`?4j4NZZ zri>HNU=tX82S$Eq0Ke7Bhm|XuQuI3tVa6`hO{X}DPjcldZOtTE8 zTP8ECl3G*DRMDlhw&@35+y4*-PH)$ZV=laC?-bLVVlodcfIsis^>SLKRM>r&(sY{A ze!vDk+7(I<2ew}*QRwry(Q}=H)QJ0BAnmVd<(LZ=9JgHI#l5teds|1Y^zHB$Ozfu@osgs)8gGu#AEkOXgdeC=SH1y*md|yBM|%C8%R6mfmpd!_}g$ORihyT zmLDxEB-e8^`!e}FnyDns9JTqRHG1(X`GQkED7tS!YbjntI{!&9e#Dt-#{X6A zNxW-mDzxh*Swd$DQARvL;BO*l5Kj?VAssE4EW_=r v7v=T*@-}2m9nJbgl`+-{A!E%X3uCyCGu%7B5$1GK9m9Q`;okWJLRQ=*?AWon literal 0 HcmV?d00001 diff --git a/pybci/Configuration/__pycache__/FeatureSettings.cpython-311.pyc b/pybci/Configuration/__pycache__/FeatureSettings.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c78670819eebb9f693c1320fb884f7c727e2ec5 GIT binary patch literal 702 zcmZvZ&x_MQ6vrn?wLdmZ*Im22s0Xh-xUhH-L?i2qQTtN%cyp7!KR(^f&>@Okt7zBic(Ghds{2EkZ0;49&nI!(gm7LCAi}>4ncJ<2Ax~F76hCo5Cpmr1WC%KlS11; z@P0}sn@&|I_MCF2Z7PHefM+rlb8QQd$seZ#ggdvVW^89{MH8`emQ0kcCP4Z5@P)1? zz~WNYW>lgKq3k{zDeZiqGq_A;(gz-8V_p9UGIeFDly0d>Dqt8#+#ORM!8fuhuI~D! z``AC9XXBJVJUjFI@yJ(k1`h-c-_Zz^FXp2#_T7|U#?e&LEKa$qYB!{;K?136SV7}d4ytV8HTYm9pm1T{F{CjwO{{ftT+A* D`isfs literal 0 HcmV?d00001 diff --git a/pybci/Configuration/__pycache__/PseudoDeviceSettings.cpython-311.pyc b/pybci/Configuration/__pycache__/PseudoDeviceSettings.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..330a6bc2e3186ac6d429a5d68a049c3022699705 GIT binary patch literal 1087 zcmaJ=Pfyf96rXmxyJca4UD-%PK{P9H(1ds+MiMdTqHajw)NC^8?hI=~w=JC((_0h$ z4txWQ;j4I96A#UtOgwR0vYa^irn5!<7+*8LdGr3yd(*GwatQ%_|2!mp1)<;KkdZcG zhHJnaAr-wr1bsn7IaU!mfhEb18Wl9T8ybaZJ7^`}$xOu#SyCF#!*+xyG8BRgl_*Az zsD?&zMxJO!f#kunz^Atv4T<~O49pGJ*|WM`={;Bliy`k7CSA z824Nf+Jc|J_#iaxkwzz>Zw8j@@Nvhr7{xX{q&6Qjd!B6tA)$P1&!-0=bvn^EG!z*x zwl>yU+fne*b)K)Tw%%Lq7PA8S)H6H#W|y*-7qvT9OV&7w-Wt_T53LT}p+R6dUG~h2 z{2T;KK~&Bj0mPSIGF6S=Ce>Q}E}5>xjTGWW-0&wer)%BJrZC(D^?$ml8AXyebXa4g zV3c4VR}q~!2CoxXl8iDbLf3U(m6d-m{e9}oJAo{Ewb8h$V7w?T7}wh-qqgPH-Ba7`UEoMtsHzdpNVj zG8p5tAZ=5BbjC0iIbFu@GTLz+!f=}gpD1-MT^4RCZUjxI!zVMTY=K`E7fI(ZCAdlk z;%=-7aAk~@dc2iX?g7lz``T<$uEd+k)E$`Z+yil5SnO-}llg_dHkUkE?rV>eTD@PZ z|EMkftS!Y&@yS#oOLIk)k0wody^IU}g?#2O{fA&GelLar202Ah2AZZ+2k17O3EBBy U#Y9P29-!NBCS>RTP^88E0m+Xf-v9sr literal 0 HcmV?d00001 diff --git a/pybci/Configuration/__pycache__/PsuedoDeviceSettings.cpython-311.pyc b/pybci/Configuration/__pycache__/PsuedoDeviceSettings.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abd863aa549ecf3c75ec68420cf127e1dac12f9d GIT binary patch literal 183 zcmZ3^%ge<81a5WDQb6=$5CH>>P{wCAAY(d13PUi1CZpdQRXW=X1UL1J=tVtQ(EOhIK*a;Cm>eqLH;dTCK&NoIbYen4?)YD&IK zYFTD-YH(^vNoHPpv0g#tFAkgB{FKt1RJ$TppwS@9i}``X2WCb_#t#fIqKFwN1^{z) BE>8de literal 0 HcmV?d00001 diff --git a/pybci/Configuration/__pycache__/__init__.cpython-311.pyc b/pybci/Configuration/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8657222e81b9b4e7461493bb14bdc31445d1c897 GIT binary patch literal 171 zcmZ3^%ge<81a5WDQc{8RV-N=h7@>^MY(U0zh7^Wi22Do4l?+8pK>lZt?m%-GapA?8Nlc;+TTUq~uI}=ls01%=FTt#FEVXJpK6i%)HE! q_;|g7%3mBdx%nxjIjMF 1: # check there is more then one marker type received + if min([currentMarkers[key][1] for key in currentMarkers]) > bci.minimumEpochsRequired: + classInfo = bci.CurrentClassifierInfo() # hangs if called too early + accuracy = classInfo["accuracy"] + if min([currentMarkers[key][1] for key in currentMarkers]) > bci.minimumEpochsRequired+10: + bci.TestMode() + break + while True: + markerGuess = bci.CurrentClassifierMarkerGuess() # when in test mode only y_pred returned + guess = [key for key, value in currentMarkers.items() if value[0] == markerGuess] + print("Current marker estimation: " + str(guess), end="\r") + time.sleep(0.2) + except KeyboardInterrupt: # allow user to break while loop + pseudoDevice.StopStreaming() + print("\nLoop interrupted by user.") diff --git a/pybci/Examples/asyncTest.py b/pybci/Examples/asyncTest.py deleted file mode 100644 index 4360b7d..0000000 --- a/pybci/Examples/asyncTest.py +++ /dev/null @@ -1,24 +0,0 @@ -from pylsl import StreamInlet, resolve_stream -import threading -import time -class MyInlet(threading.Thread): - def run(self): - streams = resolve_stream() - for stream in streams: - print(stream.name()) - if stream.name() == "pupil_capture": - my_inlet = StreamInlet(stream) - # ... setup inlet for an irregular stream - while True: - data, timestamps = my_inlet.pull_chunk(timeout=5.0) - print(f"Received {len(timestamps)} samples.", time.time()) -class waiter(threading.Thread): - def run(self): - while True: - print("waiting...", time.time()) - time.sleep(1) -def run(): - MyInlet().start() - time.sleep(1.0) - waiter().start() -run() \ No newline at end of file diff --git a/pybci/ThreadClasses/__pycache__/AsyncDataReceiverThread.cpython-311.pyc b/pybci/ThreadClasses/__pycache__/AsyncDataReceiverThread.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0132adcb5059cb975035a7108ca35309417faa6 GIT binary patch literal 9961 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 literal 0 HcmV?d00001 diff --git a/pybci/ThreadClasses/__pycache__/ClassifierThread.cpython-311.pyc b/pybci/ThreadClasses/__pycache__/ClassifierThread.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ff1d6b9daf7264ebefe5433b6b5b28a4c47910c GIT binary patch literal 7519 zcmdrxYfKwgx@Y`0#@Ij-6YRuJAPMGWoKQ+0CEfBWAwW||(|Ac7;~Bt?F=S?(hSaRG zQlup-eo(2q^>$gUyFy*6=IWw*sV+aND|B^NLh7Fx??@hv7AexzUa9}+&0Q(mU;CXI zdyIiJ>Fw(3>K>23Io~pbT7R%;1@)bo@7B>$}%q5r^0KIwCrrzB)PLL?%I z7@8si1cCFKm}W{F&_Z5I>SDTpPJZhHdiiY#805DxVA7yoMCu=6vU$Y7zPz z{z?jzkh)>yHGZuD+)GF$ov{$ZM8+c&4VdBb^uz>3e~oDe!%7PU8gj_fUqa?*z$AqN z1c?F~4WhJBU7mCTeM-P^@K;hm|AKl0Nd%0f1~X5YDRWd75^h`E51t~a$G>sIJQ^oR z!$WhxLK+{UK}luQ1N+=5TW#r8ntj24S|xHBno2b$|*?zrb<7l1xyuIQU_RG_lULT z+aQ!IOWKn5$3(I2fZCX+Nm>Cjsh9yURpUuLV5%UKM!+m8r3tW76*B{-j+-n2tW2#d z1Lo;D$YtJ;n*}oDS7)i6=D-KbtOS1C`=-i7nNWo(pZeRRo-Bw@~KGtZR$=U zLQ`b-%yf8?k(`Ch_u%)y{Mr(lr5y?S=12jBK|o*R8@mYtlApYi)g zFGt2knFvcY&4j|YLlYD;Ix{;Kj`$Q8>csM<7@z!>1BFuFJTpsUN4kG_PfcFs*v^<= z19>ic*r#@%B6o$vjS3%aq) z!|y4uhr&BO8;3eMBAf=V`I+@26hS`+Z;N6=Ig!L|*@tOe1kDlf2ak`cJ)~9N&(q^~ zaU@yPGf`-x#)X$^qoMx^+NjYZtwxUT7>A&BJLAeD>#nexpUJd1!iM z?b^k6aqBrPj%QId3f-0&p^5k+i%mM0cC18DQi!H=5d8|_KCMQmd^`LGibAYHQ8iM1 z3Nrp&l{3U#6jkF^RGkUyDfAUpWBzTq9o_L#JVGQb^qj`tv?uL93aN!Cth*0U_}Az+ zntk|HCG;}7r`^B@4i0ik;|Q@Q0mY$wVX>YFw5W~Gn6Z=!)`J7bo>Xk5qd+}pm^03q zo@4)f(g5q5@dEU-)5?-W@%S*VI#N#h|IGT;A{52JPEMrmAxaWy9P;+0V%}b0nP%*n zMPuLyDktDZwWFkIf*|#e4SUwv0>ussjN$++CsMDn3_GseoK8W{scVA-G_(oxu5;VNqi=h&xNIfG!|>oo*M5vu;n~$1b|Q36R;b( zVV@%&X_AB*)}VbM|AA(q98Q^p+}URc&YGI$7D;=Xnw3f^eAw%pPQ+Oz%-~(L$Mb;3 z+W)rXLM+0t;pwTFj{9|c4kmOW-h3f89S+5q4#?ywv3wY80B}y>nn#sqQ@fH}?-YEm zZrY8Nl#79u(OgjM#Uv9+)c=*b4iEIg2pSMR5F4uGUOW)`ZN^KXE4Cq5lJ6|Yl;|+ z9Yr#X^bPb4_CiO~6Uxya?}#PsOoEm46Zn!sDgkza@#*(M5mwT~XC!@yrbDxm;ch4f z^^zepGegDU`iq$i%}|n&rj(2xE-ez2r0>Rz%GV8&8DF<3EXnr`cw@dxD#~qyeDi?q z*jq+>Fh5qX%Na7nhGYkjN>vo!-`HUOgNdnrYBqN>qh~;ftfa+kmE|Qn462Axl8L15 z%I+)~6iEX`EX1-@oFap=6A&TEEV~>YPqO6NcL74n-itREMfV)9%bp@>!R7BC*e?*^ zDKLFt*5LHwLC1WlXt4r8isY%T)UKO21I){{|f^KeFYhPF>&~ zb%LXA#h-CBa*oEW`X)81KxQ)ufNvfZ0M_3S>TjfMqO*QwL}=>FI6FCKr|9zhxwdY( z@00%J{!}+$OG8;#TgKJ4%C60AkpCXzUBiNFm~#z_p4w%L;PHR)&f1NP=MB#D#@5OD z<;W+umv5)Fa^>%Bzq5T|ShI2$Biv|^8=K%qCvg5^g!e=RPn7dSMTb*xv}GNyWgM^Z zj*Eih;x-}vY`vtdubwqgO zQpSCWb6?u>pINI?0@LPPb|{<~3iCshFhoHaf9}2j(Eq;Re?MI&wwz8|w|xG`v+D=G zbZr_o?s4y4=X?R)7Z7}bv~IcdNzJL{*Myq3)jLAX+4U=#nhvg}Lu_qd8_2fyWm@|- zYq={y;R?lHqWIQvp>;gnwLGxZa(d0YUi)R!W;54!ox2s{TE_U6F`;D)y0kvqHXu*! z(wyKvy$YPavRG9dsk+so{o{e^H zGt+;Q@4qGV z--0r}Eg=AGx+^r@O$~|-O{vRUJ1o@O^w^#CzLD|1vC+YM-xj=Y!z36;?UZWjH`olb!;6!x$OOT?=qG#`tSvIDekx zU6kOWI2ZN3o#5HCEr;vF_dz@5hYLVFP_@_vDk?t=Dtdwghc9bu%h=j@Tf1Ou-$o{D zJJ5VMB!HfXC)G!nd_r}T?BasUFCIIw>=url7u}w1v;N4TZ3JM^yj_aQ4+{3Wti3T~ zZ(O;~+g}yzuX6TR<;Zk0Yir8bns}QJ27?yXvrpVLOLOAU<4dt^z1Cf^jQ}iKwoP&` zr?U2jjJ;u{pSQOQ_IA$RzI|1LDjoYpzHK@F?S%~9!~Ef7_xb)B^i_?mzfJqqD+Gq; zdiBEy{Sn!QE3|(sCjffO@P~nTXo`Xl10}&Ad_YLVaK1DcypssUlp6YV=#c&q2I^`l z>427^J8+@ASnB!)dxqqjX!sb@^kS%N#4xK~i)6pbMq*5JVJTC-hm|E zd<+1Lmmoys<%%B>9pj205xKY*JtC^$^3S#|LJ%O0{o&%Q;5hY-T>0P6A(4gjH|=AA ACIA2c literal 0 HcmV?d00001 diff --git a/pybci/ThreadClasses/__pycache__/DataReceiverThread.cpython-311.pyc b/pybci/ThreadClasses/__pycache__/DataReceiverThread.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb7528fb5b68872783517f68f92c6901abc7ec89 GIT binary patch literal 9969 zcmc&aTTB~SmQ`gNY`~D%(18$w)6l^tJQD~Ep%cO@AqfdI^mLkrxQ(j-gI`n?Ng#%e zR#^q2{-9UeYWB|`qn`A3j~1&%(<^~U*~ntJypN7+Y1eIK|G+}}v7|CDE<@%^hG%#-$V_0$ z1=$cc!LfRsX~M)Yml)pkfZ@#oGjC20@Rf;SzJY)JnXvHe7~`@09*ft%W;F{hEG`ND zR5MM|$S=B}_z)%)m{mkQ-c5FyI#?m*fjYgOE=9BtVFSU2-T8J|%~Icd;rUHMj~~ z@JO;xlqHuxvJ{pDF>5C1lpex2gH0iyxBy8&N-YHt0%2Eb*~7H4i4HlaF@DOa6ty@= zkytN|u?+JC`z0eontr~5%s1%*6VGZfDw$jymWgpO#*3rUC&of4%?u`Hf|9}2m>EiG z*kf_j57^FjU`j;d*5*mm`{BvW1EQ35rnsYW7nf3*9D-TPwTi=}2FGiyM z0l%!71L2v7W)DX|l6*n0B>H4Q<0M!sFWl}!3Gx(bW=ROnh`37;2_oVS#bN{{2o51Q zjNlyv4gi|X>-7hHlH~PD*agK>fA0VBa$ncv$g(^a37=_goxB#9o|FQz&=~dk7ksmV zG#Oo<_6M4Eo$13d1gW|IfHpNnmqp~@O65!1(sahDrN0F5!+qv=R{KW&XZbtU@`SZq zwH_tbql)!t@_56#nOG~6M{C#3pOqNJs(P$GnyhZZ^jNayET%3aJ#iY#$CHi|JC2hH z$4S-UCJy(G<7~olR&{g|M<)!IpDcGlVqb^97qAM(pREfhr&Nay!9E@`4+|d_J+wb8 z22002D%xP?u>*bHM1$gn?xcHRnk%WRER1SsRmM){)Eb(#0{vGx*^p+20X##u;)Y(m zdcLn?WJk8cb) z?PsPet9f9}^WJAZ%ah;B^oyBtX97$Yoh^UW8nec;q$jJ7IhLOxXl{R&`RoL&r8JHG zMpWU%G3aAR$R;OV*(l;IE3hNbPSj5{cJx`GIs+I#+sDQK2W{D8#T>nZd5@&$&Fpu+ zJY6S#vrnpxHN2vyF$lCDO^@;txh>8fm$v}+pBlN+7!9O$qxj&Mt2UrxG0L=I8?LMu ztryR5yzPNCW;!6-1u*Ne^r&(07Y{x$#4#w8y(h>YPvg&;_e;n|QOvdi{U~p9*0g

A)-3V)$*@yf`Qec-lO& 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 literal 0 HcmV?d00001 diff --git a/pybci/ThreadClasses/__pycache__/FeatureProcessorThread.cpython-311.pyc b/pybci/ThreadClasses/__pycache__/FeatureProcessorThread.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04ce33909d5daf74ddfba707b6d3aefdd6afbe8c GIT binary patch literal 5297 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 literal 0 HcmV?d00001 diff --git a/pybci/ThreadClasses/__pycache__/OptimisedDataReceiverThread.cpython-311.pyc b/pybci/ThreadClasses/__pycache__/OptimisedDataReceiverThread.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb5415bb98be18186cdfe2361826eaffa1803ed9 GIT binary patch literal 9777 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 AhyVZp literal 0 HcmV?d00001 diff --git a/pybci/ThreadClasses/__pycache__/__init__.cpython-311.pyc b/pybci/ThreadClasses/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ffb97a442c17d771a4fb87f4d04ad22dd2340a46 GIT binary patch literal 171 zcmZ3^%ge<81a5WDQb6=$5CH>>P{wCAAY(d13PUi1CZpdQRXW=X1UL1J=tVtQ(EOhIK*a;APrMp0^FigQk4adB#~etdjpUS>&r pyk0@&FAkgB{FKt1RJ$Tppm87zi}``X2WCb_#t#fIqKFwN1_0fiDKG#4 literal 0 HcmV?d00001 diff --git a/pybci/Utils/Logger.py b/pybci/Utils/Logger.py index 8b83294..fc08ea5 100644 --- a/pybci/Utils/Logger.py +++ b/pybci/Utils/Logger.py @@ -1,10 +1,13 @@ +import multiprocessing + class Logger: INFO = "INFO" WARNING = "WARNING" NONE = "NONE" TIMING = "TIMING" - def __init__(self, level=INFO): + def __init__(self, level=INFO, log_queue=None): + self.queue = log_queue self.level = level self.check_level(level) #print(self.level) @@ -19,14 +22,34 @@ def check_level(self,level): self.level = level def log(self, level, message): - if self.level == self.NONE: - return None - if level == self.INFO: - if self.level != self.NONE and self.level != self.WARNING: - print('PyBCI: [INFO] -' + message) - elif level == self.WARNING: - if self.level != self.NONE: - print('PyBCI: [WARNING] -' + message) - elif level == self.TIMING: - if self.level == self.TIMING: - print('PyBCI: [TIMING] -' + message) \ No newline at end of file + if self.queue is not None and isinstance(self.queue, multiprocessing.Queue): + if self.level == self.NONE: + return None + if level == self.INFO: + if self.level != self.NONE and self.level != self.WARNING: + self.queue.put('PyBCI: [INFO] -' + message) + elif level == self.WARNING: + if self.level != self.NONE: + self.queue.put('PyBCI: [WARNING] -' + message) + elif level == self.TIMING: + if self.level == self.TIMING: + self.queue.put('PyBCI: [TIMING] -' + message) + else: + if self.level == self.NONE: + return None + if level == self.INFO: + if self.level != self.NONE and self.level != self.WARNING: + print('PyBCI: [INFO] -' + message) + elif level == self.WARNING: + if self.level != self.NONE: + print('PyBCI: [WARNING] -' + message) + elif level == self.TIMING: + if self.level == self.TIMING: + print('PyBCI: [TIMING] -' + message) + + def start_queue_reader(self): + while True: + message = self.queue.get() + if message == "STOP": + break + print(message) diff --git a/pybci/Utils/PseudoDevice.py b/pybci/Utils/PseudoDevice.py new file mode 100644 index 0000000..bd7829b --- /dev/null +++ b/pybci/Utils/PseudoDevice.py @@ -0,0 +1,278 @@ +################ PsuedoDevice.py ######################## +# used for creating fake LSL device data and markers # +# Please note! sample rate is not exact, expect # +# some drop over time! linked to overall cpu strain # +# Written by Liam Booth 19/08/2023 # +######################################################### +from ..Utils.Logger import Logger +from ..Configuration.PseudoDeviceSettings import PseudoDataConfig, PseudoMarkerConfig +import multiprocessing, time, threading, pylsl, queue +import numpy as np +from collections import deque + +class PseudoDeviceController: + log_queue = None + def __init__(self, execution_mode='process', *args, **kwargs): + self.execution_mode = execution_mode + self.args = args + self.kwargs = kwargs + + # Create a command queue for the worker + if self.execution_mode == 'process': + self.command_queue = multiprocessing.Queue() + self.stop_signal = multiprocessing.Event() + self.worker = multiprocessing.Process(target=self._run_device) + self.log_queue = multiprocessing.Queue() + # Note: Don't initialize self.device here for 'process' mode! + elif self.execution_mode == 'thread': + self.command_queue = None # Not needed for threads, but kept for consistency + self.stop_signal = False + self.device = PseudoDevice(*self.args, **self.kwargs, stop_signal=self.stop_signal, is_multiprocessing=False) # Initialize for 'thread' mode + self.worker = threading.Thread(target=self._run_device) + else: + raise ValueError(f"Unsupported execution mode: {execution_mode}") + + self.worker.start() + # Initialize the logger + + self.logger = Logger(log_queue=self.log_queue) + self.log_reader_process = None + if self.execution_mode == 'process': + self.log_reader_process = multiprocessing.Process(target=self.logger.start_queue_reader) + self.log_reader_process.start() + + + def _run_device(self): + if self.execution_mode == 'process': + device = PseudoDevice(*self.args, **self.kwargs, stop_signal=self.stop_signal, log_queue=self.log_queue, is_multiprocessing=True) # Initialize locally for 'process' mode + + while not self._should_stop(): + if not self.command_queue.empty(): + command = self.command_queue.get() + if command == "BeginStreaming": + device.BeginStreaming() + # Sleep for a brief moment before checking again + time.sleep(0.01) + + elif self.execution_mode == 'thread': + while not self._should_stop(): + self.device.update() # or any other method you want to run continuously + time.sleep(0.01) + + def _should_stop(self): + if self.execution_mode == 'process': + return self.stop_signal.is_set() + else: # thread + return self.stop_signal + + def BeginStreaming(self): + if self.execution_mode == 'process': + self.command_queue.put("BeginStreaming") + else: # thread + self.device.BeginStreaming() + + def StopStreaming(self): + if self.execution_mode == 'process': + self.stop_signal.set() + else: # thread + self.stop_signal = True + self.worker.join() # Wait for the worker to finish + + def close(): + # add close logic + print("close it") + +def precise_sleep(duration): + end_time = time.time() + duration + while time.time() < end_time: + pass + +class PseudoDevice: + samples_generated = 0 + chunkCount = 0 + #markerOccurred = False + current_marker = None + def __init__(self,stop_signal, is_multiprocessing=True, markerConfigStrings = ["Marker1", "Marker2", "Marker3"], + pseudoMarkerDataConfigs = None, + pseudoMarkerConfig = PseudoMarkerConfig, + dataStreamName = "PyBCIPseudoDataStream" , dataStreamType="EMG", + sampleRate= 250, channelCount= 8, logger = Logger(Logger.INFO),log_queue=None): + #self.currentMarker_lock = threading.Lock() + self.markerQueue = queue.Queue() + self.is_multiprocessing = is_multiprocessing + self.stop_signal = stop_signal + self.logger = logger + self.log_queue = log_queue + self.lock = threading.Lock() # Lock for thread safety + self.markerConfigStrings = markerConfigStrings + self.baselineConfig = pseudoMarkerConfig.baselineConfig + self.baselineMarkerString = pseudoMarkerConfig.baselineMarkerString + self.currentMarker = markerConfigStrings[0] + if pseudoMarkerDataConfigs == None: + pseudoMarkerDataConfigs = [PseudoDataConfig(), PseudoDataConfig(), PseudoDataConfig()] + pseudoMarkerDataConfigs[0].amplitude = 5 + pseudoMarkerDataConfigs[1].amplitude = 6 + pseudoMarkerDataConfigs[2].amplitude = 7 + pseudoMarkerDataConfigs[0].frequency = 6 + pseudoMarkerDataConfigs[1].frequency = 9 + pseudoMarkerDataConfigs[2].frequency = 12 + self.pseudoMarkerDataConfigs = pseudoMarkerDataConfigs + self.pseudoMarkerConfig = pseudoMarkerConfig + self.sampleRate = sampleRate + self.channelCount = channelCount + markerInfo = pylsl.StreamInfo(pseudoMarkerConfig.markerName, pseudoMarkerConfig.markerType, 1, 0, 'string', 'Dev') + self.markerOutlet = pylsl.StreamOutlet(markerInfo) + info = pylsl.StreamInfo(dataStreamName, dataStreamType, self.channelCount, self.sampleRate, 'float32', 'Dev') + chns = info.desc().append_child("channels") + for label in range(self.channelCount): + ch = chns.append_child("channel") + ch.append_child_value("label", str(label+1)) + ch.append_child_value("type", dataStreamType) + self.outlet = pylsl.StreamOutlet(info) + self.last_update_time = time.time() + self.phase_offset = 0 + + def log_message(self, level='INFO', message = ""): + if self.log_queue is not None and isinstance(self.log_queue, type(multiprocessing.Queue)): + self.log_queue.put(f'PyBCI: [{level}] - {message}') + else: + self.logger.log(level, message) + + def _should_stop(self): + if isinstance(self.stop_signal, multiprocessing.synchronize.Event): + return self.stop_signal.is_set() + else: # boolean flag for threads + return self.stop_signal + + + def GeneratePseudoEMG(self,samplingRate, duration, noise_level, amplitude, frequency): + """ + Generate a pseudo EMG signal for a given gesture. + Arguments: + - sampling_rate: Number of samples per second + - duration: Duration of the signal in seconds + - noise_level: The amplitude of Gaussian noise to be added (default: 0.1) + - amplitude: The amplitude of the EMG signal (default: 1.0) + - frequency: The frequency of the EMG signal in Hz (default: 10.0) + Returns: + - emg_signal: The generated pseudo EMG signal as a 2D numpy array with shape (channels, samples) + """ + num_samples = int(samplingRate * duration) + # Initialize the EMG signal array + emg_signal = np.zeros((num_samples, self.channelCount)) + times = np.linspace(0, duration, num_samples) + # Generate the pseudo EMG signal based on the marker settings + emg_channel = amplitude * np.sin(2 * np.pi * frequency * times + self.phase_offset)# * times) # Sinusoidal EMG signal + # Add Gaussian noise to the EMG signal + noise = np.random.normal(0, noise_level, num_samples) + emg_channel += noise + # Store the generated channel in the EMG signal array + self.phase_offset = (self.phase_offset + 2 * np.pi * frequency * (num_samples/samplingRate)) % (2*np.pi) + emg_channel = emg_channel[::-1] + for channel in range(self.channelCount): + emg_signal[:, channel] = emg_channel + return emg_signal + + + def update(self): + with self.lock: # Acquire the lock + if not (self.stop_signal.is_set() if self.is_multiprocessing else self.stop_signal): + current_time = time.time() + delta_time = current_time - self.last_update_time + if not self.markerQueue.empty(): + self.current_marker = self.markerQueue.get() + if self.current_marker != None: + if not self.markerQueue.empty(): + self.current_marker = self.markerQueue.get() + for i, command in enumerate(self.markerConfigStrings): + if self.current_marker == command: + total_samples_required = int(self.sampleRate * self.pseudoMarkerDataConfigs[i].duration) + num = self.GeneratePseudoEMG(self.sampleRate,delta_time, self.pseudoMarkerDataConfigs[i].noise_level, + self.pseudoMarkerDataConfigs[i].amplitude, self.pseudoMarkerDataConfigs[i].frequency) + # If this is the start of marker data generation, set the end time for this marker + self.samples_generated += len(num) + if self.samples_generated >= total_samples_required: + self.current_marker = None + self.samples_generated = 0 + else:# send baseline + num = self.GeneratePseudoEMG(self.sampleRate,delta_time, self.baselineConfig.noise_level, + self.baselineConfig.amplitude, self.baselineConfig.frequency) + self.outlet.push_chunk(num.tolist()) + self.last_update_time = current_time + + def StopStreaming(self): + if self.is_multiprocessing: + self.stop_signal.set() + else: # For threading + self.stop_signal = True + self.thread.join() # Wait for the thread to finish + + if self.pseudoMarkerConfig.autoplay: + self.marker_thread.join() # Wait for the marker thread to finish + self.log_message(Logger.INFO, " PseudoDevice - Stopped streaming.") + + + def BeginStreaming(self): + if self.is_multiprocessing: + # For multiprocessing, we assume the worker process is already running + self.stop_signal.clear() + else: + self.stop_signal = False + #else: # For threading + self.thread = threading.Thread(target=self._generate_signal) + self.thread.start() + if self.pseudoMarkerConfig.autoplay: + self.StartMarkers() + self.log_message(Logger.INFO, " PseudoDevice - Begin streaming.") + + def _generate_signal(self): + while not self._should_stop(): + start_time = time.time() + self.update() + sleep_duration = max(0, (1.0 / 10) - (start_time - time.time())) + time.sleep(sleep_duration) +# precise_sleep(sleep_duration) + + def _should_stop(self): + if self.is_multiprocessing: + return self.stop_signal.is_set() + else: + return self.stop_signal + + def StartMarkers(self): + self.marker_thread = threading.Thread(target=self._maker_timing) + self.marker_thread.start() + + def _maker_timing(self): + marker_iterations = 0 + baseline_iterations = 0 + while not (self.stop_signal.is_set() if self.is_multiprocessing else self.stop_signal): + if marker_iterations < self.pseudoMarkerConfig.number_marker_iterations: + for marker in self.markerConfigStrings: + self.markerOutlet.push_sample([marker]) + self.markerQueue.put(marker) # Put the marker into the queue + self.log_message(Logger.INFO," PseudoDevice - sending marker " + marker) + time.sleep(self.pseudoMarkerConfig.seconds_between_markers) + if baseline_iterations < self.pseudoMarkerConfig.num_baseline_markers: + self.markerOutlet.push_sample([self.pseudoMarkerConfig.baselineMarkerString]) + self.log_message(Logger.INFO," PseudoDevice - sending " + self.pseudoMarkerConfig.baselineMarkerString) + baseline_iterations += 1 + marker_iterations += 1 + time.sleep(self.pseudoMarkerConfig.seconds_between_baseline_marker) + if baseline_iterations < self.pseudoMarkerConfig.num_baseline_markers and marker_iterations < self.pseudoMarkerConfig.number_marker_iterations: + if self.pseudoMarkerConfig.repeat: + marker_iterations = 0 + baseline_iterations = 0 + else: + break + +''' +class PseudoMarkerConfig: + markerName = "PyBCIPsuedoMarkers" + markerType = "Markers" + repeat = True + num_baseline_markers = 10 + number_marker_iterations = 10 + seconds_between_markers = 5 + seconds_between_baseline_marker = 10 +''' \ No newline at end of file diff --git a/pybci/Utils/__pycache__/Classifier.cpython-311.pyc b/pybci/Utils/__pycache__/Classifier.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a6a7868ffd91e0e8e01d16b8f6467b01d0815c5 GIT binary patch literal 7476 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 literal 0 HcmV?d00001 diff --git a/pybci/Utils/__pycache__/FeatureExtractor.cpython-311.pyc b/pybci/Utils/__pycache__/FeatureExtractor.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64d56e672be43488f06fd868e277c607a8ffe652 GIT binary patch literal 10333 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≪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 literal 0 HcmV?d00001 diff --git a/pybci/Utils/__pycache__/__init__.cpython-311.pyc b/pybci/Utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..204c519ad5da2eca2ef15c6cf8912145c5ba0263 GIT binary patch literal 163 zcmZ3^%ge<81a5WDQYwM;V-N=h7@>^MY(U0zh7^Wi22Do4l?+8pK>lZt?m%-GapA?8Nlc;+TTUq~uKf(2~rYV*U8|%)HE!_;|g7%3mBd ix%nxjIjMF(44TX@8G#a- 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 literal 0 HcmV?d00001 diff --git a/pybci/version.py b/pybci/version.py index a6221b3..930d377 100644 --- a/pybci/version.py +++ b/pybci/version.py @@ -1 +1 @@ -__version__ = '1.0.2' +__version__ = '1.1.0-alpha'