From 14a02298413a4a162733eb0945f57e9c5c86ee27 Mon Sep 17 00:00:00 2001 From: Marian Dovgialo Date: Mon, 29 Feb 2016 19:28:41 +0100 Subject: [PATCH 01/28] calibration peer, setup for bci peer starting to transfer to new architecture Offline calibration works! Looks like works online --- obci/control/gui/presets/budzik.ini | 12 + obci/control/gui/presets/p300_MD.ini | 30 ++ obci/interfaces/bci/analysis_master.py | 1 + obci/interfaces/bci/p300_MD/__init__.py | 0 .../bci/p300_MD/classifier_tests.py | 227 ++++++++++ .../bci/p300_MD/helper_functions.py | 400 ++++++++++++++++++ obci/interfaces/bci/p300_MD/p300_class.py | 274 ++++++++++++ obci/interfaces/bci/p300_MD/p300_classm.py | 155 +++++++ .../bci/p300_MD/p300_master_peer.ini | 26 ++ .../bci/p300_MD/p300_master_peer.py | 333 +++++++++++++++ .../p300_MD/p300_offline_learning_peer.ini | 7 + .../bci/p300_MD/p300_offline_learning_peer.py | 93 ++++ .../bci/p300_MD/p300_online_decision_peer.ini | 17 + .../bci/p300_MD/p300_online_decision_peer.py | 93 ++++ .../p300_configs/cap_brain2013_dummy.ini | 6 + ...0_amplifier_offline_calibration_config.ini | 7 + .../p300_labyrinth_clasifier_config.ini | 25 ++ .../p300_offline_calibration_config.ini | 24 ++ .../budzik/prototypes/p300_labyrinth.ini | 96 +++++ .../prototypes/p300_labyrinth_dummy.ini | 96 +++++ .../prototypes/p300calibrate_offline.ini | 20 + obci/scenarios/p300_MD/calibration_p300.ini | 69 +++ .../p300_MD/calibration_p300_dummy.ini | 72 ++++ .../p300_MD/calibration_p300_offline.ini | 21 + .../calibration_p300_clasifier_config.ini | 19 + ...line_calibration_p300_clasifier_config.ini | 13 + .../p300_labyrinth_clasifier_config.ini | 10 + .../p300_labyrinth_signal_saver_config.ini | 2 + .../configs/p300_labyrinth_switch_config.ini | 2 + obci/scenarios/p300_MD/p300_labyrinth.ini | 96 +++++ .../p300_MD/p300_labyrinth_dummy.ini | 96 +++++ 31 files changed, 2342 insertions(+) create mode 100644 obci/control/gui/presets/p300_MD.ini create mode 100644 obci/interfaces/bci/p300_MD/__init__.py create mode 100644 obci/interfaces/bci/p300_MD/classifier_tests.py create mode 100644 obci/interfaces/bci/p300_MD/helper_functions.py create mode 100644 obci/interfaces/bci/p300_MD/p300_class.py create mode 100644 obci/interfaces/bci/p300_MD/p300_classm.py create mode 100644 obci/interfaces/bci/p300_MD/p300_master_peer.ini create mode 100644 obci/interfaces/bci/p300_MD/p300_master_peer.py create mode 100644 obci/interfaces/bci/p300_MD/p300_offline_learning_peer.ini create mode 100644 obci/interfaces/bci/p300_MD/p300_offline_learning_peer.py create mode 100644 obci/interfaces/bci/p300_MD/p300_online_decision_peer.ini create mode 100644 obci/interfaces/bci/p300_MD/p300_online_decision_peer.py create mode 100644 obci/scenarios/budzik/prototypes/p300_configs/cap_brain2013_dummy.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_configs/p300_amplifier_offline_calibration_config.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_clasifier_config.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_configs/p300_offline_calibration_config.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_labyrinth.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_labyrinth_dummy.ini create mode 100644 obci/scenarios/budzik/prototypes/p300calibrate_offline.ini create mode 100644 obci/scenarios/p300_MD/calibration_p300.ini create mode 100644 obci/scenarios/p300_MD/calibration_p300_dummy.ini create mode 100644 obci/scenarios/p300_MD/calibration_p300_offline.ini create mode 100644 obci/scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini create mode 100644 obci/scenarios/p300_MD/configs/offline_calibration_p300_clasifier_config.ini create mode 100644 obci/scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini create mode 100644 obci/scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini create mode 100644 obci/scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini create mode 100644 obci/scenarios/p300_MD/p300_labyrinth.ini create mode 100644 obci/scenarios/p300_MD/p300_labyrinth_dummy.ini diff --git a/obci/control/gui/presets/budzik.ini b/obci/control/gui/presets/budzik.ini index 815cac26..5a0327d6 100644 --- a/obci/control/gui/presets/budzik.ini +++ b/obci/control/gui/presets/budzik.ini @@ -20,3 +20,15 @@ info=Run eyetracker (sample2d) with eyetracker signal saver(sample2d) launch_file=scenarios/budzik/prototypes/etr_ws_signal_saver_sample2d.ini public_params= category=Prototypes + +[P300 offline calibration] +info=run p300 calibration on recorded signal +launch_file=scenarios/budzik/prototypes/p300calibrate_offline.ini +public_params= +category=Prototypes P300 + +[P300 online test maze] +info=run p300 on calibrated classifier +launch_file=scenarios/budzik/prototypes/p300_labyrinth_dummy.ini +public_params= +category=Prototypes P300 diff --git a/obci/control/gui/presets/p300_MD.ini b/obci/control/gui/presets/p300_MD.ini new file mode 100644 index 00000000..d8ed7d25 --- /dev/null +++ b/obci/control/gui/presets/p300_MD.ini @@ -0,0 +1,30 @@ +[P300 Calibration] +info=... +launch_file=scenarios/p300_MD/calibration_p300.ini +public_params= +category=P300 + +[P300 Calibration - dummy] +info=... +launch_file=scenarios/p300_MD/calibration_p300_dummy.ini +public_params= +category=P300 + +[P300 Offline Calibration] +info=... +launch_file=scenarios/p300_MD/calibration_p300_offline.ini +public_params= +category=P300 + +[P300 Labyrinth] +info=... +launch_file=scenarios/p300_MD/p300_labyrinth.ini +public_params= +category=P300 + +[P300 Labyrinth - dummy] +info=... +launch_file=scenarios/p300_MD/p300_labyrinth_dummy.ini +public_params= +category=P300 + diff --git a/obci/interfaces/bci/analysis_master.py b/obci/interfaces/bci/analysis_master.py index a080bb49..bb454542 100644 --- a/obci/interfaces/bci/analysis_master.py +++ b/obci/interfaces/bci/analysis_master.py @@ -126,6 +126,7 @@ def learn(self, classifier, chunk, target): @log_crash def __init__(self, addresses, type=peers.ANALYSIS): super(AnalysisMaster, self).__init__(addresses=addresses, type=type) + self.logger.info('Initialising parameters') # initialize parameters of the subclass self.init_params() channel_count = len(self.config.get_param('channel_names').split(';')) diff --git a/obci/interfaces/bci/p300_MD/__init__.py b/obci/interfaces/bci/p300_MD/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/obci/interfaces/bci/p300_MD/classifier_tests.py b/obci/interfaces/bci/p300_MD/classifier_tests.py new file mode 100644 index 00000000..c3b87a2e --- /dev/null +++ b/obci/interfaces/bci/p300_MD/classifier_tests.py @@ -0,0 +1,227 @@ +# P300 classifier mockup +# Marian Dovgialo +from __future__ import print_function +from obci.analysis.obci_signal_processing import read_manager +from obci.analysis.obci_signal_processing.smart_tags_manager import SmartTagsManager +from obci.analysis.obci_signal_processing.tags.smart_tag_definition import SmartTagDurationDefinition +from sklearn.externals import joblib +from sklearn.discriminant_analysis import LinearDiscriminantAnalysis +import numpy as np +import pylab as pb +from scipy import linalg +from scipy import signal +import scipy.stats +from helper_functions import mgr_filter +from helper_functions import montage_custom +from helper_functions import montage_csa +from helper_functions import montage_ears +from helper_functions import exclude_channels +from helper_functions import get_channel_indexes +import p300_class +from p300_class import P300EasyClassifier +import sys +#dataset +#~ ds = u'../../../dane_od/test1.obci' + +if len(sys.argv)>1: + ds = sys.argv[1] +else: + ds = u'../../../dane_od/diody3' + +def target_tags_func(tag): + return tag['desc'][u'index']==tag['desc'][u'target'] + +def nontarget_tags_func(tag): + return tag['desc'][u'index']!=tag['desc'][u'target'] + +def get_epochs_fromfile(ds, start_offset=-0.1,duration=2.0, + filter=None, montage=None, + drop_chnls = [ u'AmpSaw', u'DriverSaw', u'trig1', u'trig2']): + '''For offline calibration and testing, load target and nontarget + epochs using obci read_manager. + ds - dataset file name without extension. + start_offset - baseline, + duration - duration of the epoch (including baseline), + filter - list of [wp, ws, gpass, gstop] for scipy.signal.iirdesign + in Hz, Db + montage - list of ['montage name', ...] ...-channel names if required + montage name can be 'ears', 'csa', 'custom' + ears require 2 channel names for ear channels + custom requires list of reference channel names + returns - two lists of smart tags: target_tags, nontarget_tags''' + eeg_rm = read_manager.ReadManager(ds+'.xml', ds+'.raw', ds+'.tag') + eeg_rm = exclude_channels(eeg_rm, drop_chnls) + + if filter: + eeg_rm = mgr_filter(eeg_rm, filter[0], filter[1],filter[2], + filter[3], ftype='cheby2', use_filtfilt=True) + if montage: + if montage[0] == 'ears': + eeg_rm = montage_ears(eeg_rm, montage[1], montage[2]) + elif montage[0] == 'csa': + eeg_rm = montage_csa(eeg_rm) + elif montage[0] == 'custom': + eeg_rm = montage_custom(eeg_rm, montage[1:]) + else: + raise Exception('Unknown montage') + + + tag_def = SmartTagDurationDefinition(start_tag_name=u'blink', + start_offset=start_offset, + end_offset=0.0, + duration=duration) + stags = SmartTagsManager(tag_def, '', '' ,'', p_read_manager=eeg_rm) + target_tags = stags.get_smart_tags(p_func = target_tags_func, p_from = 60.0, p_len=21400.0*512) + nontarget_tags = stags.get_smart_tags(p_func = nontarget_tags_func, p_from = 60.0, p_len=21400.0*512) + + return target_tags, nontarget_tags + +def evoked_from_smart_tags(tags, chnames, bas = -0.1): + '''tags - smart tag list, to average + chnames - list of channels to use for averaging, + bas - baseline (in seconds)''' + min_length = min(i.get_samples().shape[1] for i in tags) + # really don't like this, but epochs generated by smart tags can vary in length by 1 sample + channels_data = [] + Fs = float(tags[0].get_param('sampling_frequency')) + for i in tags: + data = i.get_channels_samples(chnames)[:,:min_length] + for nr, chnl in enumerate(data): + data[nr] = chnl - np.mean(chnl[0:-Fs*bas])# baseline correction + if np.max(np.abs(data))<4000: + channels_data.append(data) + + return np.mean(channels_data, axis=0), scipy.stats.sem(channels_data, axis=0) + +def evoked_pair_plot_smart_tags(tags1, tags2, chnames=['O1', 'O2', 'Pz', 'PO7', 'PO8', 'PO3', 'PO4', 'Cz',], + start_offset=-0.1, labels=['target', 'nontarget']): + '''debug evoked potential plot, + pairwise comparison of 2 smarttag lists + chnames - channels to plot + start_offset - baseline in seconds''' + ev1, std1 = evoked_from_smart_tags(tags1, chnames, start_offset) + ev2, std2 = evoked_from_smart_tags(tags2, chnames, start_offset) + Fs = float(tags1[0].get_param('sampling_frequency')) + time = np.linspace(0+start_offset, ev1.shape[1]/Fs+start_offset, ev1.shape[1]) + pb.figure() + for nr, i in enumerate(chnames): + pb.subplot( (len(chnames)+1)/2, 2, nr+1) + pb.plot(time, ev1[nr], 'r',label = labels[0]+' N:{}'.format(len(tags1))) + pb.fill_between(time, ev1[nr]-std1[nr], ev1[nr]+std1[nr], + color = 'red', alpha=0.3, ) + pb.plot(time, ev2[nr], 'b', label = labels[1]+' N:{}'.format(len(tags2))) + pb.fill_between(time, ev2[nr]-std2[nr], ev2[nr]+std2[nr], + color = 'blue', alpha=0.3) + + pb.title(i) + pb.legend() + + pb.show() + +def testing_class(epochs, cl, target=1): + ''' testing p300easy, for one class + epochs - 3d array or list smart tag object of epochs of + one type (target or nontarget) + cl - p300easyclassifier + target - class target or nontarget (1, 0) + + returns accuracy + ''' + ndec = 0 + ncorr = 0 + nepochs = [] + for i in epochs: + nepoch = len(cl.epoch_buffor) + dec = cl.run(i) + if not dec is None: + ndec += 1 + nepochs.append(nepoch+1) + if int(target)==int(dec): + ncorr +=1 + + + #~ print dec, target, cl.decision_buffor + print ('ndec', ndec) + return ncorr*1./ndec, np.mean(nepochs) + + +if __name__=='__main__': + filter = [[1, 30.0], [0.5, 35.0], 3, 12] + filter=None + #~ filter = [30, 35, 3, 30] + montage = ['custom', 'ref'] + baseline = -.2 + window = 0.6 + ept, epnt = get_epochs_fromfile(ds, filter = filter, duration = 1, + montage = montage, + start_offset = baseline, + ) + print ('parameters:\n', ept[0].get_params()) + channel_names = ept[0].get_params()['channels_names'] + evoked_pair_plot_smart_tags(ept, epnt, labels=['target', 'nontarget'], chnames=['O1', 'O2']) + + training_split = 20 + tFs = 24 + feature_reduction = None + + cl = P300EasyClassifier(decision_stop=3, max_avr=1000, targetFs = tFs, + feature_reduction = feature_reduction) + + + print ("Accuracy on training set", cl.calibrate(ept[:training_split], epnt[:training_split], bas=baseline, window=window)) + result = testing_class(ept[training_split:], cl, 1) + print ("Accuracy on TARGETS", result[0], 'Mean epochs averaged:', result[1]) + result = testing_class(epnt[training_split:], cl, 0) + print ("Accuracy on NONTARGETS", result[0], 'Mean epochs averaged:', result[1]) + + + + + et = p300_class._tags_to_array(ept) + ft = p300_class._feature_extraction(et, 128., baseline, window, targetFs=tFs) + ent = p300_class._tags_to_array(epnt) + fnt = p300_class._feature_extraction(ent, 128., baseline, window, targetFs=tFs) + f = np.vstack((ft, fnt)) + labels = np.zeros(len(f)) + labels[:len(ft)] = 1 + if feature_reduction: + rmask = p300_class._feature_reduction_mask(f, labels, feature_reduction) + else: + rmask = np.ones(ft.shape[1],dtype=bool) + + + pb.subplot(121) + pb.plot(ft[:, rmask].T) + pb.title('Target features artifact removal off') + pb.subplot(122) + pb.title('NON Target features') + pb.plot(fnt[:, rmask].T) + pb.figure() + pb.title('Features without artifact removal') + pb.plot(np.mean(ft[:, rmask], axis=0).T, label='targets') + pb.plot(np.mean(fnt[:, rmask], axis=0).T, label='non targets') + pb.legend() + pb.figure() + pb.title('Features with artifact removal') + + aet, _ = p300_class._remove_artifact_epochs(et, len(et)*[1]) + aent, _ = p300_class._remove_artifact_epochs(ent, len(ent)*[1]) + aft = p300_class._feature_extraction(aet, 128., baseline, window, targetFs=tFs) + afnt = p300_class._feature_extraction(aent, 128., baseline, window, targetFs=tFs) + + pb.plot(np.mean(aft[:, rmask], axis=0).T, label='targets') + pb.plot(np.mean(afnt[:, rmask], axis=0).T, label='non targets') + pb.legend() + + pb.figure() + + pb.subplot(121) + pb.plot(aft[:, rmask].T) + pb.title('Target features artifact removal on') + pb.subplot(122) + pb.title('NON Target features') + pb.plot(afnt[:, rmask].T) + + pb.show() + + diff --git a/obci/interfaces/bci/p300_MD/helper_functions.py b/obci/interfaces/bci/p300_MD/helper_functions.py new file mode 100644 index 00000000..8c441631 --- /dev/null +++ b/obci/interfaces/bci/p300_MD/helper_functions.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# based on obci.analysis.p300.analysis_offline +# Marian Dovgialo +from scipy import * +from scipy import linalg +import numpy as np +from scipy import signal +import copy +from obci.analysis.obci_signal_processing.signal import read_info_source +from obci.analysis.obci_signal_processing.signal import read_data_source +from obci.analysis.obci_signal_processing.tags import read_tags_source +from obci.analysis.obci_signal_processing import read_manager + +def mgr_filter(mgr, wp, ws, gpass, gstop, analog=0, ftype='ellip', output='ba', unit='hz', use_filtfilt=False, meancorr=1.0): + if unit == 'radians': + b,a = signal.iirdesign(wp, ws, gpass, gstop, analog, ftype, output) + elif unit == 'hz': + nyquist = float(mgr.get_param('sampling_frequency'))/2.0 + try: + wp = wp/nyquist + ws = ws/nyquist + except TypeError: + wp = [i/nyquist for i in wp] + ws = [i/nyquist for i in ws] + + b,a = signal.iirdesign(wp, ws, gpass, gstop, analog, ftype, output) + if use_filtfilt: + from scipy.signal import filtfilt + #samples_source = read_data_source.MemoryDataSource(mgr.get_samples(), False) + for i in range(int(mgr.get_param('number_of_channels'))): + print("FILT FILT CHANNEL "+str(i)) + mgr.get_samples()[i,:] = signal.filtfilt(b, a, mgr.get_samples()[i]-np.mean(mgr.get_samples()[i])*meancorr) + samples_source = read_data_source.MemoryDataSource(mgr.get_samples(), False) + else: + print("FILTER CHANNELs") + filtered = signal.lfilter(b, a, mgr.get_samples()) + print("FILTER CHANNELs finished") + samples_source = read_data_source.MemoryDataSource(filtered, True) + + info_source = copy.deepcopy(mgr.info_source) + tags_source = copy.deepcopy(mgr.tags_source) + new_mgr = read_manager.ReadManager(info_source, samples_source, tags_source) + return new_mgr +##### + +def leave_channels_array(arr, channels, available): + ''' + :param arr: - 2d array of eeg data channels x samples + :param channels: - channels to leave, + :param available: - channel names in data + ''' + exclude = list(set(available).difference(set(channels))) + new_samples, new_channels = exclude_channels_array( + arr, exclude, available) + return new_samples, new_channels + +def exclude_channels_array(arr, channels, available): + '''arr - 2d array of eeg data channels x samples + channels - channels to exclude, + available - channel names in data''' + exclude = set(channels) + channels = list(set(available).intersection(exclude)) + + ex_channels_inds = [available.index(ch) for ch in channels] + assert(-1 not in ex_channels_inds) + new_samples = zeros((len(available) - len(channels), + len(arr[0]))) + + + new_ind = 0 + new_channels = [] + for ch_ind, ch in enumerate(arr): + if ch_ind in ex_channels_inds: + continue + else: + new_samples[new_ind, :] = ch + new_channels.append(available[ch_ind]) + + new_ind += 1 + return new_samples, new_channels + +def exclude_channels(mgr, channels): + '''exclude all channels in channels list''' + available = set(mgr.get_param('channels_names')) + exclude = set(channels) + channels = list(available.intersection(exclude)) + + new_params = copy.deepcopy(mgr.get_params()) + samples = mgr.get_samples() + new_tags = copy.deepcopy(mgr.get_tags()) + + + ex_channels_inds = [new_params['channels_names'].index(ch) for ch in channels] + assert(-1 not in ex_channels_inds) + + new_samples = zeros((int(new_params['number_of_channels']) - len(channels), + len(samples[0]))) + # Define new samples and params list values + keys = ['channels_names', 'channels_numbers', 'channels_gains', 'channels_offsets'] + keys_to_remove = [] + for k in keys: + try: + #Exclude from keys those keys that are missing in mgr + mgr.get_params()[k] + except KeyError: + keys_to_remove.append(k) + continue + new_params[k] = [] + + for k in keys_to_remove: + keys.remove(k) + new_ind = 0 + for ch_ind, ch in enumerate(samples): + if ch_ind in ex_channels_inds: + continue + else: + new_samples[new_ind, :] = ch + for k in keys: + new_params[k].append(mgr.get_params()[k][ch_ind]) + + new_ind += 1 + + # Define other new new_params + new_params['number_of_channels'] = str(int(new_params['number_of_channels']) - len(channels)) + new_params['number_of_samples'] = str(int(new_params['number_of_samples']) - \ + len(channels)*len(samples[0])) + + + info_source = read_info_source.MemoryInfoSource(new_params) + tags_source = read_tags_source.MemoryTagsSource(new_tags) + samples_source = read_data_source.MemoryDataSource(new_samples) + return read_manager.ReadManager(info_source, samples_source, tags_source) + + +def leave_channels(mgr, channels): + '''exclude all channels except those in channels list''' + chans = copy.deepcopy(mgr.get_param('channels_names')) + for leave in channels: + chans.remove(leave) + return exclude_channels(mgr, chans) + + +def filter(mgr, wp, ws, gpass, gstop, analog=0, ftype='ellip', output='ba', unit='radians', use_filtfilt=False): + if unit == 'radians': + b,a = signal.iirdesign(wp, ws, gpass, gstop, analog, ftype, output) + elif unit == 'hz': + sampling = float(mgr.get_param('sampling_frequency')) + try: + wp = wp/sampling + ws = ws/sampling + except TypeError: + wp = [i/sampling for i in wp] + ws = [i/sampling for i in ws] + + b,a = signal.iirdesign(wp, ws, gpass, gstop, analog, ftype, output) + if use_filtfilt: + import filtfilt + #samples_source = read_data_source.MemoryDataSource(mgr.get_samples(), False) + for i in range(int(mgr.get_param('number_of_channels'))): + print("FILT FILT CHANNEL "+str(i)) + mgr.get_samples()[i,:] = filtfilt.filtfilt(b, a, mgr.get_samples()[i]) + samples_source = read_data_source.MemoryDataSource(mgr.get_samples(), False) + else: + print("FILTER CHANNELs") + filtered = signal.lfilter(b, a, mgr.get_samples()) + print("FILTER CHANNELs finished") + samples_source = read_data_source.MemoryDataSource(filtered, True) + + info_source = copy.deepcopy(mgr.info_source) + tags_source = copy.deepcopy(mgr.tags_source) + new_mgr = read_manager.ReadManager(info_source, samples_source, tags_source) + return new_mgr + +def normalize(mgr, norm): + if norm == 0: + return mgr + new_mgr = copy.deepcopy(mgr) + for i in range(len(new_mgr.get_samples())): + n = linalg.norm(new_mgr.get_samples()[i, :], norm) + new_mgr.get_samples()[i, :] /= n + return new_mgr + + +def downsample(mgr, factor): + assert(factor >= 1) + + + ch_num = len(mgr.get_samples()) + ch_len = len(mgr.get_samples()[0]) + + # To be determined ... + ret_ch_len = 0 + i = 0 + left_inds = [] + + # Determine ret_ch_len - a size of returned channel + while i < ch_len: + left_inds.append(i) + ret_ch_len += 1 + i += factor + + new_samples = array([zeros(ret_ch_len) for i in range(ch_num)]) + for i in range(ch_num): + for j, ind in enumerate(left_inds): + new_samples[i, j] = mgr.get_samples()[i, ind] + + + info_source = copy.deepcopy(mgr.info_source) + info_source.get_params()['number_of_samples'] = str(ret_ch_len*ch_num) + info_source.get_params()['sampling_frequency'] = str(float(mgr.get_param('sampling_frequency'))/factor) + + tags_source = copy.deepcopy(mgr.tags_source) + samples_source = read_data_source.MemoryDataSource(new_samples) + return read_manager.ReadManager(info_source, samples_source, tags_source) + + + + + + + + + + + + + + +def montage(mgr, montage_type, **montage_params): + if montage_type == 'common_spatial_average': + return montage_csa(mgr, **montage_params) + elif montage_type == 'ears': + return montage_ears(mgr, **montage_params) + elif montage_type == 'custom': + return montage_custom(mgr, **montage_params) + elif montage_type == 'no_montage': + return mgr + else: + raise Exception("Montage - unknown montaget type: "+str(montage_type)) + +def montage_csa(mgr): + new_samples = get_montage(mgr.get_samples(), + get_montage_matrix_csa(int(mgr.get_param('number_of_channels')))) + info_source = copy.deepcopy(mgr.info_source) + tags_source = copy.deepcopy(mgr.tags_source) + samples_source = read_data_source.MemoryDataSource(new_samples) + return read_manager.ReadManager(info_source, samples_source, tags_source) + +def montage_ears(mgr, l_ear_channel, r_ear_channel): + left_index = mgr.get_param('channels_names').index(l_ear_channel) + right_index = mgr.get_param('channels_names').index(r_ear_channel) + if left_index < 0 or right_index < 0: + raise Exception("Montage - couldn`t find ears channels: "+str(l_ear_channel)+", "+str(r_ear_channel)) + + new_samples = get_montage(mgr.get_samples(), + get_montage_matrix_ears( + int(mgr.get_param('number_of_channels')), + left_index, + right_index + )) + info_source = copy.deepcopy(mgr.info_source) + tags_source = copy.deepcopy(mgr.tags_source) + samples_source = read_data_source.MemoryDataSource(new_samples) + return read_manager.ReadManager(info_source, samples_source, tags_source) + + +def get_channel_indexes(channels, toindex): + '''get list of indexes of channels in toindex list as found in + channels list''' + indexes = [] + for chnl in toindex: + index = channels.index(chnl) + if index<0: + raise Exception("Montage - couldn`t channel: "+str(chnl)) + else: + indexes.append(index) + return indexes + +def montage_custom(mgr, chnls): + '''apply custom montage to manager, by chnls''' + indexes = [] + for chnl in chnls: + print mgr.get_param('channels_names') + index = mgr.get_param('channels_names').index(chnl) + if index<0: + raise Exception("Montage - couldn`t channel: "+str(chnl)) + else: + indexes.append(index) + + new_samples = get_montage(mgr.get_samples(), + get_montage_matrix_custom( + int(mgr.get_param('number_of_channels')), + indexes + )) + info_source = copy.deepcopy(mgr.info_source) + tags_source = copy.deepcopy(mgr.tags_source) + samples_source = read_data_source.MemoryDataSource(new_samples) + return read_manager.ReadManager(info_source, samples_source, tags_source) + +def montage_custom_matrix(mgr, montage_matrix): + new_samples = get_montage(mgr.get_samples(), + montage_matrix) + info_source = copy.deepcopy(mgr.info_source) + tags_source = copy.deepcopy(mgr.tags_source) + samples_source = read_data_source.MemoryDataSource(new_samples) + return read_manager.ReadManager(info_source, samples_source, tags_source) + +def get_montage(data, montage_matrix): + """ + montage_matrix[i] = linear transformation of all channels to achieve _new_ channel i + data[i] = original data from channel i + + >>> montage_matrix = np.array([[ 1. , -0.25, -0.25, -0.25, -0.25], [-0.25, 1. , -0.25, -0.25, -0.25], [-0.25, -0.25, 1. , -0.25, -0.25],[-0.25, -0.25, -0.25, 1. , -0.25], [-0.25, -0.25, -0.25, -0.25, 1. ]]) + >>> data = np.array(5 * [np.ones(10)]) + >>> montage(data,montage_matrix) + array([[ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]]) + + + """ + + return dot(montage_matrix, data) + + + + +def get_montage_matrix_csa(n): + """ + Return nxn array representing extraction from + every channel an avarage of all other channels. + + >>> get_montage_matrix_avg(5) + array([[ 1. , -0.25, -0.25, -0.25, -0.25], + [-0.25, 1. , -0.25, -0.25, -0.25], + [-0.25, -0.25, 1. , -0.25, -0.25], + [-0.25, -0.25, -0.25, 1. , -0.25], + [-0.25, -0.25, -0.25, -0.25, 1. ]]) + + """ + + factor = -1.0/(n - 1) + mx = ones((n, n)) + for i in range(n): + for j in range(n): + if i != j: + mx[i, j] = factor + return mx + + + +def get_montage_matrix_ears(n, l_ear_index, r_ear_index): + """ + Return nxn array representing extraction from + every channel an avarage of channels l_ear_index + and r_ear_index. + + >>> get_montage_matrix_ears(5, 2, 4) + array([[ 1. , 0. , -0.5, 0. , -0.5], + [ 0. , 1. , -0.5, 0. , -0.5], + [ 0. , 0. , 1. , 0. , 0. ], + [ 0. , 0. , -0.5, 1. , -0.5], + [ 0. , 0. , 0. , 0. , 1. ]]) + """ + + factor = -0.5 + mx = diag([1.0]*n) + for i in range(n): + for j in range(n): + if j in [r_ear_index, l_ear_index] \ + and j != i \ + and not i in [r_ear_index, l_ear_index]: + mx[i, j] = factor + return mx + +def get_montage_matrix_custom(n, indexes): + """ + Return nxn array representing extraction from + every channel an avarage of channels in indexes list + + >>> get_montage_matrix_ears(5, 2, 4) + array([[ 1. , 0. , -0.5, 0. , -0.5], + [ 0. , 1. , -0.5, 0. , -0.5], + [ 0. , 0. , 1. , 0. , 0. ], + [ 0. , 0. , -0.5, 1. , -0.5], + [ 0. , 0. , 0. , 0. , 1. ]]) + """ + + factor = -1.0/len(indexes) + mx = diag([1.0]*n) + for i in range(n): + for j in range(n): + if j in indexes \ + and j != i \ + and not i in indexes: + mx[i, j] = factor + return mx diff --git a/obci/interfaces/bci/p300_MD/p300_class.py b/obci/interfaces/bci/p300_MD/p300_class.py new file mode 100644 index 00000000..cde8c027 --- /dev/null +++ b/obci/interfaces/bci/p300_MD/p300_class.py @@ -0,0 +1,274 @@ +# P300 classifier mockup +# Marian Dovgialo + +import numpy as np +import scipy.stats +import scipy.signal as ss +from sklearn.externals import joblib +from sklearn.discriminant_analysis import LinearDiscriminantAnalysis +from collections import deque +from obci.interfaces.bci.abstract_classifier import AbstractClassifier + +def _tags_to_array(tags): + '''returns 3D numpy array from OBCI smart tags + epochs x channels x time''' + min_length = min(i.get_samples().shape[1] for i in tags) +# really don't like this, but epochs generated by smart tags can vary in length by 1 sample + array = np.dstack([i.get_samples()[:,:min_length] for i in tags]) + return np.rollaxis(array,2) + +def _remove_artifact_epochs(data, labels): + ''' data - 3D numpy array epoch x channels x time, + labels - list of epochs labels + returns clean data and labels + Provisional version''' + mask = np.ones(len(data), dtype = bool) + for id, i in enumerate(data): + if np.max(np.abs(i-i[:,0][:, None]))>2000: + mask[id]=False + newlabels = [l for l, m in zip(labels, mask) if m] + newdata = data[mask] + return newdata, newlabels + + +def _feature_extraction(data, Fs, bas=-0.1, window=0.4, targetFs=34): + '''data - 3D numpy array epoch x channels x time, + returns spatiotemporal features array epoch x features''' + features = [] + for epoch in data: + features.append(_feature_extraction_singular(epoch, Fs, bas, + window, targetFs)) + return np.array(features) + + + +def _feature_extraction_singular(epoch, Fs, bas=-0.1, + window = 0.5, + targetFs=30,): + '''performs feature extraction on epoch (array channels x time), + Fs - sampling in Hz + bas - baseline in seconds + targetFs = target sampling in Hz (will be approximated) + window - timewindow after baseline to select in seconds + returns 1D array downsampled, len = downsampled samples x channels + + epoch minus mean of baseline, downsampled by factor int(Fs/targetFs) + samples used - from end of baseline to window timepoint + ''' + mean = np.mean(epoch[:, :-bas*Fs], axis=1) + decimation_factor = int(1.0*Fs/targetFs) + selected = epoch[:,-bas*Fs:(-bas+window)*Fs]-mean[:, None] + features = ss.decimate(selected, decimation_factor, axis=1, ftype='fir') + return features.flatten() + +def _feature_reduction_mask(ft, labels, mode): + ''' ft - features 2d array nsamples x nfeatures + labels - nsamples array of labels 0, 1, + mode - 'auto', int + returns - features mask''' + tscore, p = scipy.stats.ttest_ind(ft[labels==1], ft[labels==0]) + if mode == 'auto': + mask = p<0.05 + if mask.sum()<1: + raise Exception('Feature reduction produced zero usable features') + elif isinstance(mode, int): + mask_ind = np.argsort(p)[-mode:] + mask = np.zeros_like(p, dtype=bool) + mask[mask_ind] = True + return mask + + + +class P300EasyClassifier(object): + '''Easy and modular P300 classifier + attributes" + fname - classifier save filename + epoch_buffor - current epoch buffor + max_avr - maximum epochs to average + decision_buffor - last decisions buffor, when full of identical + decisions final decision is made + clf - core classifier from sklearn + feature_s - feature length''' + + def __init__(self, fname='./class.joblib.pkl', max_avr=10, + decision_stop=3, targetFs=30, clf=None, + feature_reduction = None,): + '''fname - classifier file to save or load classifier on disk + while classifying produce decision after max_avr epochs averaged, + or after decision_stop succesfull same decisions + targetFs - on feature extraction downsample to this Hz + clf - sklearn type classifier to use as core + feature_reduction - 'auto', int, None. If 'auto' - features are + reduced, features left are those which have statistically + significant (p<0.05) difference in target and nontarget, + if int - use feature_reduction most significant features, if + None don't use reduction + ''' + + self.targetFs = targetFs + self.fname = fname + self.epoch_buffor = [] + self.max_avr = max_avr + self.decision_buffor = deque([], decision_stop) + self.feature_reduction = feature_reduction + if clf is None: + self.clf = LinearDiscriminantAnalysis(solver = 'lsqr', shrinkage='auto') + + def load_classifier(self, fname=None): + '''loads classifier from disk, provide fname - path to joblib + pickle with classifier, or will be used from init''' + self.clf = joblib.load(fname) + + def reset(self): + self.epoch_buffor = [] + self.decision_buffor.clear() + + def calibrate(self, targets, nontargets, bas=-0.1, window=0.4, Fs=None): + '''targets, nontargets - 3D arrays (epoch x channel x time) + or list of OBCI smart tags + if arrays - need to provide Fs (sampling frequency) in Hz + bas - baseline in seconds(negative), in other words start offset''' + + if Fs is None: + Fs = float(targets[0].get_param('sampling_frequency')) + target_data = _tags_to_array(targets) + nontarget_data = _tags_to_array(nontargets) + data = np.vstack((target_data, nontarget_data)) + self.epoch_l = data.shape[2] + labels = np.zeros(len(data)) + labels[:len(target_data)] = 1 + data, labels = _remove_artifact_epochs(data, labels) + features = _feature_extraction(data, Fs, bas, window, self.targetFs) + + if self.feature_reduction: + mask = _feature_reduction_mask(features, labels, self.feature_reduction) + self.feature_reduction_mask = mask + features = features[:, mask] + + + self.feature_s = features.shape[1] + self.bas = bas + self.window = window + self.Fs + + + self.clf.fit(features, labels) + joblib.dump(self.clf, self.fname, compress=9) + return self.clf.score(features, labels) + + + + + def run(self, epoch, Fs=None): + '''epoch - array (channels x time) or smarttag/readmanager object, + Fs - sampling frequency Hz, leave None if epoch is smart tag, + returns decision - 1 for target, 0 for nontarget, + None - for no decision''' + if self.Fs is not None: + Fs = self.Fs + + decision = int(self.run_forced_percentage(self, epoch, Fs)>=0.5) + if len(self.decision_buffor) == self.decision_buffor.maxlen: + if len(set(self.decision_buffor))==1: + self.decision_buffor.clear() + self.epoch_buffor = [] + return decision + if len(self.epoch_buffor) == self.max_avr: + self.decision_buffor.clear() + self.epoch_buffor = [] + return decision + return None + + def run_forced_percentage(self, epoch, Fs=None): + '''epoch - array (channels x time) or smarttag/readmanager object, + Fs - sampling frequency Hz, leave None if epoch is smart tag, + returns probability for target''' + if self.Fs is not None: + Fs = self.Fs + bas = self.bas + window = self.window + if Fs is None: + Fs = float(epoch.get_param('sampling_frequency')) + epoch = epoch.get_samples()[:,:self.epoch_l] + if len(self.epoch_buffor)< self.max_avr: + self.epoch_buffor.append(epoch) + avr_epoch = np.mean(self.epoch_buffor, axis=0) + + features = _feature_extraction_singular(avr_epoch, + Fs, bas, window, self.targetFs)[None, :] + if self.feature_reduction: + mask = self.feature_reduction_mask + features = features[:, mask] + decision = self.clf.proba(features)[0][1] #probability of target + return decision + +class P300MetaClassifier(AbstractClassifier): + '''knows how many buttons are there loads P300EasyClassifiers and + uses them to decide which button were pressed + fname - classifier save filename (of P300EasyClassifier) + epoch_buffor - current epoch buffor + max_avr - maximum epochs to average + decision_buffor - last decisions buffor, when full of identical + decisions final decision is made + clf - core classifier from sklearn + feature_s - feature length''' + def __init__(self, fname='./class.joblib.pkl', max_avr=10, + decision_stop=3, targetFs=30, clf=None, + feature_reduction = None, buttons=2, + learnOnTheFly=False, + Fs = None, + baseline = None, + window = None): + '''fname - classifier file to save or load classifier on disk + while classifying produce decision after max_avr epochs averaged, + or after decision_stop succesfull same decisions + targetFs - on feature extraction downsample to this Hz + clf - sklearn type classifier to use as core + feature_reduction - 'auto', int, None. If 'auto' - features are + reduced, features left are those which have statistically + significant (p<0.05) difference in target and nontarget, + if int - use feature_reduction most significant features, if + None don't use reduction + buttons - how many virtual buttons are there to be selected from + ''' + self.learOnTheFly = learnOnTheFly + self.targetFs = targetFs + self.fname = fname + self.max_avr = max_avr + self.decision_buffor = deque([], decision_stop) + self.feature_reduction = feature_reduction + self.buttons = buttons + self.classifiers = [joblib.load(fname) for i in range(buttons)] + + def run(self, epoch, Fs, buttonId): + '''run classifiers on epoch + epoch - array (channels x time) or smarttag/readmanager object, + Fs - sampling frequency Hz, leave None if epoch is smart tag + buttonId - which button this epoch is associated with + (from 0 to buttons-1)''' + proba = self.classifiers[buttonId].run_forced_percentage(epoch, Fs) + if proba>=0.5: + decision = buttonId + if len(self.decision_buffor) == self.decision_buffor.maxlen: + if len(set(self.decision_buffor))==1: + self.decision_buffor.clear() + for clf in self.classifiers: + clf.epoch_buffor = [] + clf.decision_buffor.clear() + + return decision + if len(self.epoch_buffor) == self.max_avr: + self.decision_buffor.clear() + for clf in self.classifiers: + clf.epoch_buffor = [] + clf.decision_buffor.clear() + return decision + return None + + def new_button_config(buttons=2): + '''screen changed, new buttons, + buttons - how many buttons on new screen''' + self.buttons = buttons + self.classifiers = [joblib.load(self.fname) for i in range(buttons)] + + diff --git a/obci/interfaces/bci/p300_MD/p300_classm.py b/obci/interfaces/bci/p300_MD/p300_classm.py new file mode 100644 index 00000000..5c7e3fbe --- /dev/null +++ b/obci/interfaces/bci/p300_MD/p300_classm.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# P300 classifier mockup +# Marian Dovgialo +from __future__ import print_function +import numpy as np +import scipy.stats +import scipy.signal as ss +from sklearn.externals import joblib +from sklearn.discriminant_analysis import LinearDiscriminantAnalysis +from collections import deque +from obci.interfaces.bci.abstract_classifier import AbstractClassifier +import pickle + +LEARN_EVERY = 20 # relearn every # provided targets + +def _tags_to_array(tags): + '''returns 3D numpy array from OBCI smart tags + epochs x channels x time''' + min_length = min(i.get_samples().shape[1] for i in tags) +# really don't like this, but epochs generated by smart tags can vary in length by 1 sample + array = np.dstack([i.get_samples()[:,:min_length] for i in tags]) + return np.rollaxis(array,2) + +def _remove_artifact_epochs(data, labels): + ''' data - 3D numpy array epoch x channels x time, + labels - list of epochs labels + returns clean data and labels + Provisional version''' + mask = np.ones(len(data), dtype = bool) + for id, i in enumerate(data): + if np.max(np.abs(i-i[:,0][:, None]))>2000: + mask[id]=False + newlabels = [l for l, m in zip(labels, mask) if m] + newdata = data[mask] + return newdata, newlabels + + +def _feature_extraction(data, Fs, bas=-0.1, window=0.4, targetFs=34): + '''data - 3D numpy array epoch x channels x time, + returns spatiotemporal features array epoch x features''' + features = [] + for epoch in data: + features.append(_feature_extraction_singular(epoch, Fs, bas, + window, targetFs)) + return np.array(features) + + + +def _feature_extraction_singular(epoch, Fs, bas=-0.1, + window = 0.5, + targetFs=30,): + '''performs feature extraction on epoch (array channels x time), + Fs - sampling in Hz + bas - baseline in seconds + targetFs = target sampling in Hz (will be approximated) + window - timewindow after baseline to select in seconds + returns 1D array downsampled, len = downsampled samples x channels + + epoch minus mean of baseline, downsampled by factor int(Fs/targetFs) + samples used - from end of baseline to window timepoint + ''' + mean = np.mean(epoch[:, :-bas*Fs], axis=1) + decimation_factor = int(1.0*Fs/targetFs) + selected = epoch[:,-bas*Fs:(-bas+window)*Fs]-mean[:, None] + features = ss.decimate(selected, decimation_factor, axis=1, ftype='fir') + return features.flatten() + +def _feature_reduction_mask(ft, labels, mode): + ''' ft - features 2d array nsamples x nfeatures + labels - nsamples array of labels 0, 1, + mode - 'auto', int + returns - features mask''' + tscore, p = scipy.stats.ttest_ind(ft[labels==1], ft[labels==0]) + if mode == 'auto': + mask = p<0.05 + if mask.sum()<1: + raise Exception('Feature reduction produced zero usable features') + elif isinstance(mode, int): + mask_ind = np.argsort(p)[-mode:] + mask = np.zeros_like(p, dtype=bool) + mask[mask_ind] = True + return mask + + + +class P300EasyClassifier(object): + '''Easy and modular P300 classifier + attributes" + fname - classifier save filename + epoch_buffor - current epoch buffor + max_avr - maximum epochs to average + decision_buffor - last decisions buffor, when full of identical + decisions final decision is made + clf - core classifier from sklearn + feature_s - feature length''' + + def __init__(self, clf=None, fname = './test.class', + targetFs=24): + if clf is None: + self.clf = LinearDiscriminantAnalysis(solver = 'lsqr', shrinkage='auto') + # store all examples + self.fname = fname + self.targetFs = targetFs + + + def classify(self, features): + ''' + Args: + features - 1D vector of extracted features + ''' + #probability of target + return self.clf.predict_proba(features.reshape(1, -1))[0][1] + + def learn(self, chunk, target): + ''' + For online learning thread + + Args: + chunk: numpy 2D data array (channels × samples) + target: name of the class + ''' + if target == 'target': + self.learning_buffor_classes.append(1) + else: + self.learning_buffor_classes.append(0) + self.learning_buffor_features.append(chunk) + if sum(learning_buffor_classes)%LEARN_EVERY == 0: + self.clf.fit( + self.learning_buffor_features, + self.learning_buffor_classes, + ) + + + + def calibrate(self, targets, nontargets, bas=-0.1, window=0.4, Fs=None): + '''Offline learning function + + targets, nontargets - 3D arrays (epoch x channel x time) + or list of OBCI smart tags + if arrays - need to provide Fs (sampling frequency) in Hz + bas - baseline in seconds(negative), in other words start offset''' + + if Fs is None: + Fs = float(targets[0].get_param('sampling_frequency')) + target_data = _tags_to_array(targets) + nontarget_data = _tags_to_array(nontargets) + data = np.vstack((target_data, nontarget_data)) + self.epoch_l = data.shape[2] + labels = np.zeros(len(data)) + labels[:len(target_data)] = 1 + data, labels = _remove_artifact_epochs(data, labels) + features = _feature_extraction(data, Fs, bas, window, self.targetFs) + self.clf.fit(features, labels) + return self.clf.score(features, labels) diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.ini b/obci/interfaces/bci/p300_MD/p300_master_peer.ini new file mode 100644 index 00000000..78f16e4d --- /dev/null +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.ini @@ -0,0 +1,26 @@ +[local_params] +wisdom_path=~/classifier.dump +channels_for_classification=O1;O2;Pz +montage_channels=Cz +baseline=-0.2 +window=0.5 +decision_stop=3 +maximum_to_average=10 +downsample_to=3 +targetFs=24 +;which field is used in online calibration +calibration_field_index= +;if want to run offline calibration provide this file (without .raw): +offline_learning=1 +offline_learning_dataset_path= +hold_after_dec=3 + +[config_sources] +amplifier= + +[external_params] +channel_names=amplifier.channel_names +sampling_rate=amplifier.sampling_rate + +[launch_dependencies] +amplifier= diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.py b/obci/interfaces/bci/p300_MD/p300_master_peer.py new file mode 100644 index 00000000..bd18913b --- /dev/null +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# OpenBCI - framework for Brain-Computer Interfaces based on EEG signal +# Project was initiated by Magdalena Michalska and Krzysztof Kulewski +# as part of their MSc theses at the University of Warsaw. +# Copyright (C) 2008-2009 Krzysztof Kulewski and Magdalena Michalska +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Author: +# Marian Dovgialo + +from obci.configs import settings +from multiplexer.multiplexer_constants import peers, types +from obci.interfaces.bci.analysis_master import AnalysisMaster +from obci.interfaces.bci.p300_MD.p300_classm import P300EasyClassifier +from obci.interfaces.bci.p300_MD.p300_classm import _feature_extraction_singular + +from helper_functions import get_montage, get_montage_matrix_custom +from helper_functions import leave_channels_array, get_channel_indexes + +from obci.analysis.buffers.auto_blink_buffer import AutoBlinkBuffer +from collections import defaultdict, deque +from operator import itemgetter +from obci.utils.openbci_logging import log_crash +import sys +from classifier_tests import get_epochs_fromfile +from classifier_tests import evoked_pair_plot_smart_tags +import numpy as np +import os.path +import pickle + +from obci.gui.ugm import ugm_helper + +class P300MasterPeer(AnalysisMaster): + '''P300 classifier master peer''' + @log_crash + def __init__(self, addresses): + super(P300MasterPeer, self).__init__(addresses=addresses, + type=peers.P300_ANALYSIS) + ugm_helper.send_start_blinking(self.conn) + + def _reset_buffors(self): + #probabilities of selected input for single epochs + self.singular_proba_buffor = defaultdict(list) + #probabilities of selected input for cumulative mean + self.averaged_proba_buffor = defaultdict(list) + #features buffor to classify on single epochs or cumulative mean + self.features = defaultdict(list) + #final decision buffor + self.decision_buffor = deque(maxlen = self.decision_stop) + + def _prepare_chunk(self, chunk, blink): + ''' + Performs channel selection, montage and feature extraction. + + Args: + chunk: numpy 2D data array (channels × samples) + blink: information contained in a single blink + ''' + chunk_clean_channels, _ = leave_channels_array( + chunk, + self.channels_for_classification, + self.channel_names + ) + + chunk_montage = get_montage( + chunk_clean_channels, + self.montage_matrix + ) + chunk_ready = _feature_extraction_singular( + chunk_montage, + self.sampling_rate, + self.baseline, + self.window, + self.targetFs, + ) + return chunk_ready + def _send_decision(self, decision): + ugm_helper.send_stop_blinking(self.conn) + self.conn.send_message(message = str(decision), type = types.DECISION_MESSAGE, flush=True) + self._reset_buffors() + + def add_result(self, blink, probabilities): + ''' + Sends decision if some last decisions from cumulative mean + are the same or a lot of averaging was done + Args: + blink: information contained in a single blink + probabilities (dict): dictionary of {target: probability} + ''' + #planning for future + self.singular_proba_buffor[blink.index].append( + probabilities['targetSingle'] + ) + self.averaged_proba_buffor[blink.index].append( + probabilities['targetCMean'] + ) + last_single = [blink.index, probabilities['targetSingle']] + last_mean = [blink.index, probabilities['targetCMean']] + self.logger.info('Last mean proba: {}'.format(last_mean)) + if last_mean[1]>0.5: + self.decision_buffor.append(blink.index) + + # enough of the same decisions condition + one_decision = (len(set(self.decision_buffor)) == 1) + buffor_full = (len(self.decision_buffor) == self.decision_stop) + if one_decision and buffor_full: + decision = self.decision_buffor[-1] + self.logger.info('Decision by decision_stop {}'.format(decision)) + self._send_decision(decision) + return + + # number of averaged epochs condition + maximum_averaged = max( + len(self.averaged_proba_buffor[i]) for i in self.averaged_proba_buffor.keys() + ) + if maximum_averaged > self.maximum_to_average: + most_confident_decision = max( + self.averaged_proba_buffor.items(), + key = lambda key: key[1][-1] + )[0] + self.logger.info('Decision by max_avr {}'.format(most_confident_decision)) + self._send_decision(most_confident_decision) + return + + + + def create_buffer(self, channel_count, ret_func): + + return AutoBlinkBuffer( + from_blink=int(self.sampling_rate*self.baseline), + samples_count=int((self.window-self.baseline)*self.sampling_rate), + sampling=self.sampling_rate, + num_of_channels=channel_count, + ret_func=ret_func, + ret_format="NUMPY_CHANNELS", + copy_on_ret=0 + ) + + def create_classifier(self): + try: + with open(self.wisdom_path) as clf_file: + classifier = pickle.load(clf_file) + self.logger.info('loaded classifier from wisdom_path') + except Exception as e: + classifier = P300EasyClassifier(fname=self.wisdom_path, + targetFs=self.targetFs) + self.logger.info("Loading classifier failed: {}".format(e)) + self.logger.info('Creating new, untrained classifier') + return classifier + def identify_blink(self, blink): + if self.training_index is not None: + + if blink.index == training_index: + return 'target' + else: + return 'nontarget' + return None + + @log_crash + def init_params(self): + self.logger.info('Initialasing parameters') + #available channels + self.channel_names = self.config.get_param( + 'channel_names').split(';') + #used channels + self.channels_for_classification = self.config.get_param( + 'channels_for_classification' + ).split(';') + self.sampling_rate = float( + self.config.get_param('sampling_rate') + ) + #channel montage: + #montage type + self.montage = 'custom' + #montage channels + channels_count = len(self.channels_for_classification) + self.montage_channels = self.config.get_param("montage_channels").strip().split(';') + montage_ids = get_channel_indexes(self.channel_names, self.montage_channels) + self.montage_matrix = get_montage_matrix_custom(channels_count, + montage_ids, + ) + + #pre event baseline in seconds (negative) seconds + self.baseline = float( + self.config.get_param('baseline') + ) + #post event window to consider seconds + self.window = float( + self.config.get_param('window') + ) + + #maximum averaged epochs + self.maximum_to_average = int( + self.config.get_param('maximum_to_average') + ) + #identical decisions to get final answer + self.decision_stop = int(self.config.get_param('decision_stop')) + # downsample to + self.targetFs = float(self.config.get_param('downsample_to')) + # how many virtual "buttons" + # in calibration session logic or something should set + # this parameter to show which field is being used for calibration + try: + self.training_index = int( + self.config.get_param( + 'calibration_field_index') + ) + except ValueError: + self.training_index = None + + + self.logger.info('Initialasing buffers') + self._reset_buffors() + self.ready() + + if self.config.get_param('offline_learning') == '1': + self.logger.info('STARTING ONLINE LEARNING') + self.learn_offline() + self.logger.info('DONE LEARNING') + sys.exit(0) + + + @log_crash + def learn_offline(self): + '''Function that reads saved calibration signal, splits it + to epochs and trains classifier''' + self.logger.info("STARTING LEARNING") + + + filter = None + baseline = self.baseline + window = self.window + montage = [self.montage,] + chnls = self.montage_channels + self.logger.info("reference channels {}".format(chnls)) + montage = montage+chnls + #dataset + ds_path = self.config.get_param('offline_learning_dataset_path') + ds_path = os.path.expanduser(ds_path) + + + exclude_channels = exclude = list( + set(self.channel_names + ).difference( + set( + self.channels_for_classification + ) + ) + ) + + ept, epnt = get_epochs_fromfile(ds_path, filter = filter, duration = 1, + montage = montage, + start_offset = baseline, + drop_chnls = exclude_channels + ) + self.logger.info("GOT {} TARGET EPOCHS AND {} NONTARGET".format(len(ept), len(epnt))) + + self.logger.info('EPOCH PARAMS:\n{}'.format(ept[0].get_params())) + evoked_pair_plot_smart_tags(ept, epnt, labels=['target', 'nontarget'], chnames=['O1', 'O2']) + self.wisdom_path = self.wisdom_path = self.config.get_param('wisdom_path') + cl = P300EasyClassifier(fname = self.wisdom_path) + result = cl.calibrate(ept, epnt, bas=baseline, window=window) + + self.logger.info('classifier self score on training set: {}'.format(result)) + with open(self.wisdom_path, 'w') as fname: + pickle.dump(cl, fname) + self.logger.info("classifier -- DONE") + + + + def classify(self, classifier, chunk, blink): + """Compute set of classification probabilities for a given blink. + + May be re-implemented in subclass to perform some pre-processing + or feature extraction on the signal (chunk). + This method will be run by a separate thread. + + Args: + classifier (AbstractClassifier): classifier instance to be used for classification + chunk: numpy 2D data array (channels × samples) + blink: information contained in a single blink + + Returns: + dict. dictionary of {target: probability}, + or None if classification could not be performed + + """ + chunk_ready = self._prepare_chunk(chunk, blink) + + self.features[blink.index].append(chunk_ready) + probabilities = {} + probabilities['targetSingle'] = classifier.classify( + chunk_ready + ) + chunk_mean = np.array(self.features[blink.index]).mean(axis=0) + probabilities['targetCMean'] = classifier.classify( + chunk_mean + ) + + + return probabilities + + def learn(self, classifier, chunk, target): + """Learn that given chunk represents given target. + + May be re-implemented in subclass to perform some pre-processing + or feature extraction on the signal (chunk). + This method will be run by a separate thread. + + Args: + classifier (AbstractClassifier): classifier instance to be used for learning + chunk: numpy 2D data array (channels × samples) + target: name of the target + """ + chunk_ready = self._prepare_chunk(chunk, blink) + classifier.learn(chunk, target) + +if __name__ == '__main__': + P300MasterPeer(settings.MULTIPLEXER_ADDRESSES).loop() diff --git a/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.ini b/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.ini new file mode 100644 index 00000000..38ebc6c1 --- /dev/null +++ b/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.ini @@ -0,0 +1,7 @@ +[local_params] +data_file_name=test1 +data_file_path=~ +montage=custom +montage_channels=Cz +offline = 1 +cl_filename = class diff --git a/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.py b/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.py new file mode 100644 index 00000000..87dc72cf --- /dev/null +++ b/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.py @@ -0,0 +1,93 @@ +from obci.control.peer.configured_multiplexer_server import ConfiguredMultiplexerServer +from multiplexer.multiplexer_constants import peers, types + +from classifier import get_epochs_fromfile +from classifier import evoked_pair_plot_smart_tags +from p300_class import P300EasyClassifier + +from obci.utils.openbci_logging import log_crash +from obci.configs import settings +import os.path +import sys +from sklearn.externals import joblib + +class P300OfflineLearner(ConfiguredMultiplexerServer): + """Class to run after acquiring calibration signal""" + @log_crash + def __init__(self, addresses): + super(P300OfflineLearner, self).__init__(addresses=addresses, + type=peers.LOGIC_P300_CSP) + self._data_finished = False + self._info_finished = False + self._tags_finished = False + + self.ready() + self.offline = int(self.config.get_param("offline")) + self.logger.info("offline? {} ".format(self.offline)) + if self.offline==1: + self.logger.info("offline learning has started") + self.learn() + + def _all_ready(self): + return self._data_finished and self._info_finished and self._tags_finished + + def handle_message(self, mxmsg): + if mxmsg.type == types.SIGNAL_SAVER_FINISHED: + self._data_finished = True + elif mxmsg.type == types.INFO_SAVER_FINISHED: + self._info_finished = True + elif mxmsg.type == types.TAG_SAVER_FINISHED: + self._tags_finished = True + else: + self.logger.warning("Unrecognised message received!!!!") + self.no_response() + + if self._all_ready(): + self.learn() + + def learn(self): + '''Function that reads saved calibration signal, splits it + to epochs and trains classifier''' + self.logger.info("STARTING LEARNING") + f_name = self.config.get_param("data_file_name") + f_dir = self.config.get_param("data_file_path") + cl_fname= self.config.get_param("cl_filename") + cl_fname = os.path.expanduser(os.path.join(f_dir, cl_fname)) + + filter = None + baseline = -.2 #read from file + window = 0.6 + montage = [self.config.get_param("montage")] + chnls = self.config.get_param("montage_channels").strip().split(';') + self.logger.info("reference channels {}".format(chnls)) + montage = montage+chnls + ds = os.path.expanduser(os.path.join(f_dir, f_name))+'.obci' + + + + ept, epnt = get_epochs_fromfile(ds, filter = filter, duration = 1, + montage = montage, + start_offset = baseline, + ) + self.logger.info("GOT {} TARGET EPOCHS AND {} NONTARGET".format(len(ept), len(epnt))) + if self.offline ==1: + self.logger.info('EPOCH PARAMS:\n{}'.format(ept[0].get_params())) + evoked_pair_plot_smart_tags(ept, epnt, labels=['target', 'nontarget'], chnames=['O1', 'O2']) + + cl = P300EasyClassifier(decision_stop=3, + max_avr=1000, + targetFs = 24, + feature_reduction = None, + fname = cl_fname) + result = cl.calibrate(ept, epnt, bas=baseline, window=window) + + self.logger.info('classifier self score on training set: {}'.format(result)) + + joblib.dump(cl, cl_fname, compress=3) + + self.logger.info("classifier -- DONE") + sys.exit(0) + + +if __name__ == '__main__': + P300OfflineLearner(settings.MULTIPLEXER_ADDRESSES).loop() diff --git a/obci/interfaces/bci/p300_MD/p300_online_decision_peer.ini b/obci/interfaces/bci/p300_MD/p300_online_decision_peer.ini new file mode 100644 index 00000000..dfbcccb0 --- /dev/null +++ b/obci/interfaces/bci/p300_MD/p300_online_decision_peer.ini @@ -0,0 +1,17 @@ +[local_params] +montage=custom +montage_channels=Cz +cl_filename = class +hold_after_dec = 1 +data_file_path = ~ +data_file_name=test1 +cl_filename = class + +[config_sources] +logic= +amplifier= + +[external_params] +blink_field_ids=logic.active_field_ids +sampling_rate=amplifier.sampling_rate +channel_names=amplifier.channel_names diff --git a/obci/interfaces/bci/p300_MD/p300_online_decision_peer.py b/obci/interfaces/bci/p300_MD/p300_online_decision_peer.py new file mode 100644 index 00000000..e82ca524 --- /dev/null +++ b/obci/interfaces/bci/p300_MD/p300_online_decision_peer.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# +from multiplexer.multiplexer_constants import peers, types +from obci.configs import settings, variables_pb2 + +from obci.control.peer.configured_multiplexer_server import ConfiguredMultiplexerServer +from obci.analysis.buffers import auto_blink_buffer +from obci.gui.ugm import ugm_helper +from obci.utils.openbci_logging import log_crash +from sklearn.externals import joblib +import os.path +from helper_functions import get_montage, get_montage_matrix_custom, get_channel_indexes +from p300_class import P300MetaClassifier + +MAX_AVR = 1000 +DECISION_STOP = 3 +TARGETFS = 24 +FEATURE_REDUCTION = None + +class P300AnalysisPeer(ConfiguredMultiplexerServer): + @log_crash + def __init__(self, addresses): + #Create a helper object to get configuration from the system + super(P300AnalysisPeer, self).__init__(addresses=addresses, + type=peers.P300_ANALYSIS) + self.clf_filepath = os.path.join( + self.config.get_param('data_file_path'), + self.config.get_param('cl_filename'), + ) + blink_field_ids = self.config.get_param('blink_field_ids').split(';') + self.blink_field_ids = [int(ids) for ids in blink_field_ids] + sampling = int(self.config.get_param('sampling_rate')) + self.sampling = sampling + self.channel_names = self.config.get_param('channel_names').split(';') + channels_count = len(self.channel_names) + + window = 0.6 + baseline = -.2 + self.buffer = auto_blink_buffer.AutoBlinkBuffer( + from_blink=int(sampling*baseline), + samples_count=int(window*sampling), + sampling=sampling, + num_of_channels=channels_count, + ret_func=self.return_blink, + ret_format="NUMPY_CHANNELS", + copy_on_ret=0 + ) + + self.clf = P300MetaClassifier(self.clf_filepath, MAX_AVR, + DECISION_STOP, TARGETFS, None, FEATURE_REDUCTION, + len(self.blink_field_ids)) + + + + self.montage = [self.config.get_param("montage")] + self.montage_channels = self.config.get_param("montage_channels").strip().split(';') + montage_ids = get_channel_indexes(self.channel_names, montage_channels) + self.montage_matrix = get_montage_matrix_custom(channels_count, + montage_ids, + ) + ugm_helper.send_start_blinking(self.conn) + self.ready() + def return_blink(self, blink, data): + '''function run after getting blink for a "button" highlight + runs classifier for that button, if classifier says it's a target + sends a message with decision + (implies that classifier is sure about blink being a target)''' + ind = blink.index + self.logger.info('got blink index:{}\ndata:{}'.format(ind, data)) + + data_ready = get_montage(data, self.montage_matrix) + + dec = self.clf.run(data_ready, self.sampling, self.blink_field_ids.index(ind)) #returns button ID + self.buffer.clear_blinks() # buffor is used to get one blink, classifiers have their own + if dec: + ugm_helper.send_stop_blinking(self.conn) + self.conn.send_message(message = str(self.blink_field_ids[ind]), type = types.DECISION_MESSAGE, flush=True) + + def handle_message(self, mxmsg): + 'sends signal to autoblinkbuffor''' + if mxmsg.type == types.AMPLIFIER_SIGNAL_MESSAGE: + l_msg = variables_pb2.SampleVector() + l_msg.ParseFromString(mxmsg.message) + self.buffer.handle_sample_vect(l_msg) + if mxmsg.type == types.BLINK_MESSAGE: + l_msg = variables_pb2.Blink() + l_msg.ParseFromString(mxmsg.message) + self.buffer.handle_blink(l_msg) + self.no_response() +if __name__ == "__main__": + P300AnalysisPeer(settings.MULTIPLEXER_ADDRESSES).loop() diff --git a/obci/scenarios/budzik/prototypes/p300_configs/cap_brain2013_dummy.ini b/obci/scenarios/budzik/prototypes/p300_configs/cap_brain2013_dummy.ini new file mode 100644 index 00000000..7524e631 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_configs/cap_brain2013_dummy.ini @@ -0,0 +1,6 @@ +[local_params] +sampling_rate=512 +channel_names=PO7;O1;Oz;O2;PO8;PO3;PO4;Pz;Cz;AmpSaw;DriverSaw +active_channels=0;1;2;3;4;5;6;7;8;Saw;Driver_Saw + + diff --git a/obci/scenarios/budzik/prototypes/p300_configs/p300_amplifier_offline_calibration_config.ini b/obci/scenarios/budzik/prototypes/p300_configs/p300_amplifier_offline_calibration_config.ini new file mode 100644 index 00000000..90213c80 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_configs/p300_amplifier_offline_calibration_config.ini @@ -0,0 +1,7 @@ +[local_params] +data_file_dir=~/dataset/ +data_file_name=diody3 +info_file_dir= +info_file_name= +tags_file_dir= +tags_file_name= diff --git a/obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_clasifier_config.ini b/obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_clasifier_config.ini new file mode 100644 index 00000000..f7ac8565 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_clasifier_config.ini @@ -0,0 +1,25 @@ +[local_params] +wisdom_path=~/classifier.dump +channels_for_classification=O1;O2;Pz;Cz +montage_channels=Cz +baseline=-0.2 +window=0.5 +maximum_to_average=10 +decision_stop=3 +downsample_to=24 +;which field is used in online calibration +calibration_field_index= +;if want to run offline calibration provide this file: +offline_learning=0 +offline_learning_dataset_path= + +[config_sources] +amplifier= + +[external_params] +channel_names=amplifier.channel_names +sampling_rate=amplifier.sampling_rate + +[launch_dependencies] +amplifier= + diff --git a/obci/scenarios/budzik/prototypes/p300_configs/p300_offline_calibration_config.ini b/obci/scenarios/budzik/prototypes/p300_configs/p300_offline_calibration_config.ini new file mode 100644 index 00000000..0225192c --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_configs/p300_offline_calibration_config.ini @@ -0,0 +1,24 @@ +[local_params] +wisdom_path=~/classifier.dump +channels_for_classification=O1;O2;Pz;Cz +montage_channels=Cz +baseline=-0.2 +window=0.5 +maximum_to_average=10 +decision_stop=3 +downsample_to=24 +;which field is used in online calibration +calibration_field_index= +;if want to run offline calibration provide this file: +offline_learning=1 +offline_learning_dataset_path=~/dataset/diody3.obci + +[config_sources] +amplifier= + +[external_params] +channel_names=amplifier.channel_names +sampling_rate=amplifier.sampling_rate + +[launch_dependencies] +amplifier= diff --git a/obci/scenarios/budzik/prototypes/p300_labyrinth.ini b/obci/scenarios/budzik/prototypes/p300_labyrinth.ini new file mode 100644 index 00000000..f0745856 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_labyrinth.ini @@ -0,0 +1,96 @@ +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** +[peers.amplifier] +path=drivers/eeg/cpp_amplifiers/amplifier_tmsi.py +config=scenarios/brain2013/configs/cap_brain2013.ini + +;*********************************************** +[peers.signal_saver] +path=acquisition/signal_saver_peer.py +config=scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini + +[peers.signal_saver.launch_dependencies] +amplifier=amplifier + +;*********************************************** +[peers.tag_saver] +path=acquisition/tag_saver_peer.py + +[peers.tag_saver.launch_dependencies] +signal_saver=signal_saver + +;*********************************************** +[peers.info_saver] +path=acquisition/info_saver_peer.py + +[peers.info_saver.launch_dependencies] +amplifier=amplifier +signal_saver=signal_saver + +;*********************************************** +[peers.ugm_engine] +path=gui/ugm/blinking/ugm_blinking_engine_peer.py +config=scenarios/brain2013/configs/p300_fast_colour_bci.ini + +[peers.ugm_engine.config_sources] +logic=logic + +;*********************************************** +[peers.ugm_server] +path=gui/ugm/ugm_server_peer.py + +[peers.ugm_server.launch_dependencies] +ugm_engine=ugm_engine + +;*********************************************** +[peers.analysis] +path=interfaces/bci/p300_MD/p300_master_peer.py +config=scenarios/budzik/prototypes/p300_configs/p300_labyrinth_clasifier_config.ini + +[peers.analysis.config_sources] +logic=logic +amplifier=amplifier + +[peers.analysis.launch_dependencies] +logic=logic +amplifier=amplifier + +;************************************************* +[peers.logic] +path=logic/logic_maze_peer.py +config=scenarios/brain2013/configs/brain2013_logic_maze_peer.ini + +[peers.logic.launch_dependencies] +ugm=ugm_server +signal_saver=signal_saver + +;************************************ +[peers.feedback] +path=logic/feedback/logic_decision_feedback_peer.py + +[peers.feedback.launch_dependencies] +ugm_engine=ugm_engine +ugm_server=ugm_server +logic=logic +analysis=analysis + +;*********************************************** +[peers.switch_backup] +path=interfaces/switch/backup/switch_backup_peer.py +config=scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini + +;*********************************************** +[peers.switch] +path=drivers/switch/switch_amplifier_peer.py + +[peers.switch.launch_dependencies] +ugm_engine=ugm_engine diff --git a/obci/scenarios/budzik/prototypes/p300_labyrinth_dummy.ini b/obci/scenarios/budzik/prototypes/p300_labyrinth_dummy.ini new file mode 100644 index 00000000..d0b6dea7 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_labyrinth_dummy.ini @@ -0,0 +1,96 @@ +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** +[peers.amplifier] +path=drivers/eeg/amplifier_virtual.py +config=scenarios/budzik/prototypes/p300_configs/cap_brain2013_dummy.ini + +;*********************************************** +[peers.signal_saver] +path=acquisition/signal_saver_peer.py +config=scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini + +[peers.signal_saver.launch_dependencies] +amplifier=amplifier + +;*********************************************** +[peers.tag_saver] +path=acquisition/tag_saver_peer.py + +[peers.tag_saver.launch_dependencies] +signal_saver=signal_saver + +;*********************************************** +[peers.info_saver] +path=acquisition/info_saver_peer.py + +[peers.info_saver.launch_dependencies] +amplifier=amplifier +signal_saver=signal_saver + +;*********************************************** +[peers.ugm_engine] +path=gui/ugm/blinking/ugm_blinking_engine_peer.py +config=scenarios/brain2013/configs/p300_fast_colour_bci.ini + +[peers.ugm_engine.config_sources] +logic=logic + +;*********************************************** +[peers.ugm_server] +path=gui/ugm/ugm_server_peer.py + +[peers.ugm_server.launch_dependencies] +ugm_engine=ugm_engine + +;*********************************************** +[peers.analysis] +path=interfaces/bci/p300_MD/p300_master_peer.py +config=scenarios/budzik/prototypes/p300_configs/p300_labyrinth_clasifier_config.ini + +[peers.analysis.config_sources] +logic=logic +amplifier=amplifier + +[peers.analysis.launch_dependencies] +logic=logic +amplifier=amplifier + +;************************************************* +[peers.logic] +path=logic/logic_maze_peer.py +config=scenarios/brain2013/configs/brain2013_logic_maze_peer.ini + +[peers.logic.launch_dependencies] +ugm=ugm_server +signal_saver=signal_saver + +;************************************ +[peers.feedback] +path=logic/feedback/logic_decision_feedback_peer.py + +[peers.feedback.launch_dependencies] +ugm_engine=ugm_engine +ugm_server=ugm_server +logic=logic +analysis=analysis + +;*********************************************** +[peers.switch_backup] +path=interfaces/switch/backup/switch_backup_peer.py +config=scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini + +;*********************************************** +[peers.switch] +path=drivers/switch/switch_amplifier_peer.py + +[peers.switch.launch_dependencies] +ugm_engine=ugm_engine diff --git a/obci/scenarios/budzik/prototypes/p300calibrate_offline.ini b/obci/scenarios/budzik/prototypes/p300calibrate_offline.ini new file mode 100644 index 00000000..855a59d8 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300calibrate_offline.ini @@ -0,0 +1,20 @@ +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** +[peers.amplifier] +path=drivers/eeg/amplifier_file.py +config=scenarios/budzik/prototypes/p300_configs/p300_amplifier_offline_calibration_config.ini + +[peers.analysis] +path=interfaces/bci/p300_MD/p300_master_peer.py +config=scenarios/budzik/prototypes/p300_configs/p300_offline_calibration_config.ini +[peers.analysis.launch_dependencies] +amplifier=amplifier diff --git a/obci/scenarios/p300_MD/calibration_p300.ini b/obci/scenarios/p300_MD/calibration_p300.ini new file mode 100644 index 00000000..14e16f3a --- /dev/null +++ b/obci/scenarios/p300_MD/calibration_p300.ini @@ -0,0 +1,69 @@ +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** +[peers.amplifier] +path=drivers/eeg/cpp_amplifiers/amplifier_tmsi.py +config=scenarios/brain2013/configs/cap_brain2013.ini + +;*********************************************** +[peers.signal_saver] +path=acquisition/signal_saver_peer.py + +[peers.signal_saver.launch_dependencies] +amplifier=amplifier + +;*********************************************** +[peers.info_saver] +path=acquisition/info_saver_peer.py + +[peers.info_saver.launch_dependencies] +amplifier=amplifier +signal_saver=signal_saver + +;*********************************************** +[peers.tag_saver] +path=acquisition/tag_saver_peer.py + +[peers.tag_saver.launch_dependencies] +signal_saver=signal_saver + + +;*********************************************** +[peers.ugm_engine] +path=gui/ugm/blinking/ugm_blinking_engine_peer.py +config=scenarios/brain2013/configs/p300_fast_colour_calibration.ini + +;*********************************************** +[peers.ugm_server] +path=gui/ugm/ugm_server_peer.py + +[peers.ugm_server.launch_dependencies] +ugm_engine=ugm_engine + +;*********************************************** +[peers.logic] +path=interfaces/bci/p300_fda/logic_p300_calibration_peer.py +config=scenarios/brain2013/configs/brain2013_logic_p300_calibraction.ini + +[peers.logic.launch_dependencies] +ugm_engine=ugm_engine +ugm_server=ugm_server +signal_saver=signal_saver + +;*********************************************** +[peers.clasifier] +path=interfaces/bci/p300_MD/p300_offline_learning_peer.py +config=scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini + +[peers.clasifier.launch_dependencies] +signal_saver=signal_saver +logic=logic + diff --git a/obci/scenarios/p300_MD/calibration_p300_dummy.ini b/obci/scenarios/p300_MD/calibration_p300_dummy.ini new file mode 100644 index 00000000..3454d0ce --- /dev/null +++ b/obci/scenarios/p300_MD/calibration_p300_dummy.ini @@ -0,0 +1,72 @@ + + +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** +[peers.amplifier] +path=drivers/eeg/amplifier_virtual.py +config=scenarios/brain2013/configs/cap_brain2013_dummy.ini + +;*********************************************** +[peers.signal_saver] +path=acquisition/signal_saver_peer.py + +[peers.signal_saver.launch_dependencies] +amplifier=amplifier + +;*********************************************** +[peers.info_saver] +path=acquisition/info_saver_peer.py + +[peers.info_saver.launch_dependencies] +amplifier=amplifier +signal_saver=signal_saver + +;*********************************************** +[peers.tag_saver] +path=acquisition/tag_saver_peer.py + +[peers.tag_saver.launch_dependencies] +signal_saver=signal_saver + + +;*********************************************** +[peers.ugm_engine] +path=gui/ugm/blinking/ugm_blinking_engine_peer.py +config=scenarios/brain2013/configs/p300_fast_colour_calibration.ini + +;*********************************************** +[peers.ugm_server] +path=gui/ugm/ugm_server_peer.py + +[peers.ugm_server.launch_dependencies] +ugm_engine=ugm_engine + +;*********************************************** +[peers.logic] +path=interfaces/bci/p300_fda/logic_p300_calibration_peer.py +config=scenarios/brain2013/configs/brain2013_logic_p300_calibraction.ini + +[peers.logic.launch_dependencies] +ugm_engine=ugm_engine +ugm_server=ugm_server +signal_saver=signal_saver + +;*********************************************** +[peers.clasifier] +path=interfaces/bci/p300_MD/p300_offline_learning_peer.py +config=scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini + +[peers.clasifier.launch_dependencies] +signal_saver=signal_saver +logic=logic + + diff --git a/obci/scenarios/p300_MD/calibration_p300_offline.ini b/obci/scenarios/p300_MD/calibration_p300_offline.ini new file mode 100644 index 00000000..01266af4 --- /dev/null +++ b/obci/scenarios/p300_MD/calibration_p300_offline.ini @@ -0,0 +1,21 @@ + + +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** + +[peers.clasifier] +path=interfaces/bci/p300_MD/p300_offline_learning_peer.py +config=scenarios/p300_MD/configs/offline_calibration_p300_clasifier_config.ini + + + + diff --git a/obci/scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini b/obci/scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini new file mode 100644 index 00000000..c319b294 --- /dev/null +++ b/obci/scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini @@ -0,0 +1,19 @@ +[local_params] + +montage=custom +montage_channels=Cz +offline = 0 +cl_filename = test1_class.dump + +[config_sources] +signal_saver= +logic= + +[external_params] + +data_file_name=signal_saver.save_file_name +data_file_path=signal_saver.save_file_path + +[launch_dependencies] +signal_saver= +logic= diff --git a/obci/scenarios/p300_MD/configs/offline_calibration_p300_clasifier_config.ini b/obci/scenarios/p300_MD/configs/offline_calibration_p300_clasifier_config.ini new file mode 100644 index 00000000..ee3507b1 --- /dev/null +++ b/obci/scenarios/p300_MD/configs/offline_calibration_p300_clasifier_config.ini @@ -0,0 +1,13 @@ +[local_params] + +montage=custom +montage_channels=Cz +offline = 1 +data_file_name=test1 +data_file_path=~ +cl_filename = test1_class.dump + + + + + diff --git a/obci/scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini b/obci/scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini new file mode 100644 index 00000000..ee8f95eb --- /dev/null +++ b/obci/scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini @@ -0,0 +1,10 @@ +[local_params] +data_file_name=test1 +data_file_path=~ +cl_filename = test1_class.dump + +[config_sources] +logic= + +[external_params] +blink_field_ids=logic.active_field_ids diff --git a/obci/scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini b/obci/scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini new file mode 100644 index 00000000..da4e0d7b --- /dev/null +++ b/obci/scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini @@ -0,0 +1,2 @@ +[local_params] +save_file_name=test2 \ No newline at end of file diff --git a/obci/scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini b/obci/scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini new file mode 100644 index 00000000..65a8e6fd --- /dev/null +++ b/obci/scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini @@ -0,0 +1,2 @@ +[local_params] +finish_saving=1 \ No newline at end of file diff --git a/obci/scenarios/p300_MD/p300_labyrinth.ini b/obci/scenarios/p300_MD/p300_labyrinth.ini new file mode 100644 index 00000000..a03e518f --- /dev/null +++ b/obci/scenarios/p300_MD/p300_labyrinth.ini @@ -0,0 +1,96 @@ +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** +[peers.amplifier] +path=drivers/eeg/cpp_amplifiers/amplifier_tmsi.py +config=scenarios/brain2013/configs/cap_brain2013.ini + +;*********************************************** +[peers.signal_saver] +path=acquisition/signal_saver_peer.py +config=scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini + +[peers.signal_saver.launch_dependencies] +amplifier=amplifier + +;*********************************************** +[peers.tag_saver] +path=acquisition/tag_saver_peer.py + +[peers.tag_saver.launch_dependencies] +signal_saver=signal_saver + +;*********************************************** +[peers.info_saver] +path=acquisition/info_saver_peer.py + +[peers.info_saver.launch_dependencies] +amplifier=amplifier +signal_saver=signal_saver + +;*********************************************** +[peers.ugm_engine] +path=gui/ugm/blinking/ugm_blinking_engine_peer.py +config=scenarios/brain2013/configs/p300_fast_colour_bci.ini + +[peers.ugm_engine.config_sources] +logic=logic + +;*********************************************** +[peers.ugm_server] +path=gui/ugm/ugm_server_peer.py + +[peers.ugm_server.launch_dependencies] +ugm_engine=ugm_engine + +;*********************************************** +[peers.analysis] +path=interfaces/bci/p300_MD/p300_online_decision_peer.py +config=scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini + +[peers.analysis.config_sources] +logic=logic +amplifier=amplifier + +[peers.analysis.launch_dependencies] +logic=logic +amplifier=amplifier + +;************************************************* +[peers.logic] +path=logic/logic_maze_peer.py +config=scenarios/brain2013/configs/brain2013_logic_maze_peer.ini + +[peers.logic.launch_dependencies] +ugm=ugm_server +signal_saver=signal_saver + +;************************************ +[peers.feedback] +path=logic/feedback/logic_decision_feedback_peer.py + +[peers.feedback.launch_dependencies] +ugm_engine=ugm_engine +ugm_server=ugm_server +logic=logic +analysis=analysis + +;*********************************************** +[peers.switch_backup] +path=interfaces/switch/backup/switch_backup_peer.py +config=scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini + +;*********************************************** +[peers.switch] +path=drivers/switch/switch_amplifier_peer.py + +[peers.switch.launch_dependencies] +ugm_engine=ugm_engine diff --git a/obci/scenarios/p300_MD/p300_labyrinth_dummy.ini b/obci/scenarios/p300_MD/p300_labyrinth_dummy.ini new file mode 100644 index 00000000..266ecf9e --- /dev/null +++ b/obci/scenarios/p300_MD/p300_labyrinth_dummy.ini @@ -0,0 +1,96 @@ +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** +[peers.amplifier] +path=drivers/eeg/amplifier_virtual.py +config=scenarios/brain2013/configs/cap_brain2013_dummy.ini + +;*********************************************** +[peers.signal_saver] +path=acquisition/signal_saver_peer.py +config=scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini + +[peers.signal_saver.launch_dependencies] +amplifier=amplifier + +;*********************************************** +[peers.tag_saver] +path=acquisition/tag_saver_peer.py + +[peers.tag_saver.launch_dependencies] +signal_saver=signal_saver + +;*********************************************** +[peers.info_saver] +path=acquisition/info_saver_peer.py + +[peers.info_saver.launch_dependencies] +amplifier=amplifier +signal_saver=signal_saver + +;*********************************************** +[peers.ugm_engine] +path=gui/ugm/blinking/ugm_blinking_engine_peer.py +config=scenarios/brain2013/configs/p300_fast_colour_bci.ini + +[peers.ugm_engine.config_sources] +logic=logic + +;*********************************************** +[peers.ugm_server] +path=gui/ugm/ugm_server_peer.py + +[peers.ugm_server.launch_dependencies] +ugm_engine=ugm_engine + +;*********************************************** +[peers.analysis] +path=interfaces/bci/p300_MD/p300_online_decision_peer.py +config=scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini + +[peers.analysis.config_sources] +logic=logic +amplifier=amplifier + +[peers.analysis.launch_dependencies] +logic=logic +amplifier=amplifier + +;************************************************* +[peers.logic] +path=logic/logic_maze_peer.py +config=scenarios/brain2013/configs/brain2013_logic_maze_peer.ini + +[peers.logic.launch_dependencies] +ugm=ugm_server +signal_saver=signal_saver + +;************************************ +[peers.feedback] +path=logic/feedback/logic_decision_feedback_peer.py + +[peers.feedback.launch_dependencies] +ugm_engine=ugm_engine +ugm_server=ugm_server +logic=logic +analysis=analysis + +;*********************************************** +[peers.switch_backup] +path=interfaces/switch/backup/switch_backup_peer.py +config=scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini + +;*********************************************** +[peers.switch] +path=drivers/switch/switch_amplifier_peer.py + +[peers.switch.launch_dependencies] +ugm_engine=ugm_engine From be9e0a31cba7378b9df2f1a4b2b95dff4105ba8b Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 19 Apr 2016 13:57:00 +0200 Subject: [PATCH 02/28] cleaning up --- obci/control/gui/presets/p300_MD.ini | 30 -- .../bci/p300_MD/classifier_tests.py | 227 --------------- .../bci/p300_MD/helper_functions.py | 102 +++++++ obci/interfaces/bci/p300_MD/p300_class.py | 274 ------------------ obci/interfaces/bci/p300_MD/p300_classm.py | 40 ++- .../bci/p300_MD/p300_master_peer.py | 16 +- .../p300_MD/p300_offline_learning_peer.ini | 7 - .../bci/p300_MD/p300_offline_learning_peer.py | 93 ------ .../bci/p300_MD/p300_online_decision_peer.ini | 17 -- .../bci/p300_MD/p300_online_decision_peer.py | 93 ------ .../p300_labyrinth_signal_saver_config.ini | 2 + .../p300_labyrinth_switch_config.ini | 2 + .../prototypes/p300_labyrinth_dummy.ini | 4 +- obci/scenarios/p300_MD/calibration_p300.ini | 69 ----- .../p300_MD/calibration_p300_dummy.ini | 72 ----- .../p300_MD/calibration_p300_offline.ini | 21 -- .../calibration_p300_clasifier_config.ini | 19 -- ...line_calibration_p300_clasifier_config.ini | 13 - .../p300_labyrinth_clasifier_config.ini | 10 - .../p300_labyrinth_signal_saver_config.ini | 2 - .../configs/p300_labyrinth_switch_config.ini | 2 - obci/scenarios/p300_MD/p300_labyrinth.ini | 96 ------ .../p300_MD/p300_labyrinth_dummy.ini | 96 ------ 23 files changed, 139 insertions(+), 1168 deletions(-) delete mode 100644 obci/control/gui/presets/p300_MD.ini delete mode 100644 obci/interfaces/bci/p300_MD/classifier_tests.py delete mode 100644 obci/interfaces/bci/p300_MD/p300_class.py delete mode 100644 obci/interfaces/bci/p300_MD/p300_offline_learning_peer.ini delete mode 100644 obci/interfaces/bci/p300_MD/p300_offline_learning_peer.py delete mode 100644 obci/interfaces/bci/p300_MD/p300_online_decision_peer.ini delete mode 100644 obci/interfaces/bci/p300_MD/p300_online_decision_peer.py create mode 100644 obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_signal_saver_config.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_switch_config.ini delete mode 100644 obci/scenarios/p300_MD/calibration_p300.ini delete mode 100644 obci/scenarios/p300_MD/calibration_p300_dummy.ini delete mode 100644 obci/scenarios/p300_MD/calibration_p300_offline.ini delete mode 100644 obci/scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini delete mode 100644 obci/scenarios/p300_MD/configs/offline_calibration_p300_clasifier_config.ini delete mode 100644 obci/scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini delete mode 100644 obci/scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini delete mode 100644 obci/scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini delete mode 100644 obci/scenarios/p300_MD/p300_labyrinth.ini delete mode 100644 obci/scenarios/p300_MD/p300_labyrinth_dummy.ini diff --git a/obci/control/gui/presets/p300_MD.ini b/obci/control/gui/presets/p300_MD.ini deleted file mode 100644 index d8ed7d25..00000000 --- a/obci/control/gui/presets/p300_MD.ini +++ /dev/null @@ -1,30 +0,0 @@ -[P300 Calibration] -info=... -launch_file=scenarios/p300_MD/calibration_p300.ini -public_params= -category=P300 - -[P300 Calibration - dummy] -info=... -launch_file=scenarios/p300_MD/calibration_p300_dummy.ini -public_params= -category=P300 - -[P300 Offline Calibration] -info=... -launch_file=scenarios/p300_MD/calibration_p300_offline.ini -public_params= -category=P300 - -[P300 Labyrinth] -info=... -launch_file=scenarios/p300_MD/p300_labyrinth.ini -public_params= -category=P300 - -[P300 Labyrinth - dummy] -info=... -launch_file=scenarios/p300_MD/p300_labyrinth_dummy.ini -public_params= -category=P300 - diff --git a/obci/interfaces/bci/p300_MD/classifier_tests.py b/obci/interfaces/bci/p300_MD/classifier_tests.py deleted file mode 100644 index c3b87a2e..00000000 --- a/obci/interfaces/bci/p300_MD/classifier_tests.py +++ /dev/null @@ -1,227 +0,0 @@ -# P300 classifier mockup -# Marian Dovgialo -from __future__ import print_function -from obci.analysis.obci_signal_processing import read_manager -from obci.analysis.obci_signal_processing.smart_tags_manager import SmartTagsManager -from obci.analysis.obci_signal_processing.tags.smart_tag_definition import SmartTagDurationDefinition -from sklearn.externals import joblib -from sklearn.discriminant_analysis import LinearDiscriminantAnalysis -import numpy as np -import pylab as pb -from scipy import linalg -from scipy import signal -import scipy.stats -from helper_functions import mgr_filter -from helper_functions import montage_custom -from helper_functions import montage_csa -from helper_functions import montage_ears -from helper_functions import exclude_channels -from helper_functions import get_channel_indexes -import p300_class -from p300_class import P300EasyClassifier -import sys -#dataset -#~ ds = u'../../../dane_od/test1.obci' - -if len(sys.argv)>1: - ds = sys.argv[1] -else: - ds = u'../../../dane_od/diody3' - -def target_tags_func(tag): - return tag['desc'][u'index']==tag['desc'][u'target'] - -def nontarget_tags_func(tag): - return tag['desc'][u'index']!=tag['desc'][u'target'] - -def get_epochs_fromfile(ds, start_offset=-0.1,duration=2.0, - filter=None, montage=None, - drop_chnls = [ u'AmpSaw', u'DriverSaw', u'trig1', u'trig2']): - '''For offline calibration and testing, load target and nontarget - epochs using obci read_manager. - ds - dataset file name without extension. - start_offset - baseline, - duration - duration of the epoch (including baseline), - filter - list of [wp, ws, gpass, gstop] for scipy.signal.iirdesign - in Hz, Db - montage - list of ['montage name', ...] ...-channel names if required - montage name can be 'ears', 'csa', 'custom' - ears require 2 channel names for ear channels - custom requires list of reference channel names - returns - two lists of smart tags: target_tags, nontarget_tags''' - eeg_rm = read_manager.ReadManager(ds+'.xml', ds+'.raw', ds+'.tag') - eeg_rm = exclude_channels(eeg_rm, drop_chnls) - - if filter: - eeg_rm = mgr_filter(eeg_rm, filter[0], filter[1],filter[2], - filter[3], ftype='cheby2', use_filtfilt=True) - if montage: - if montage[0] == 'ears': - eeg_rm = montage_ears(eeg_rm, montage[1], montage[2]) - elif montage[0] == 'csa': - eeg_rm = montage_csa(eeg_rm) - elif montage[0] == 'custom': - eeg_rm = montage_custom(eeg_rm, montage[1:]) - else: - raise Exception('Unknown montage') - - - tag_def = SmartTagDurationDefinition(start_tag_name=u'blink', - start_offset=start_offset, - end_offset=0.0, - duration=duration) - stags = SmartTagsManager(tag_def, '', '' ,'', p_read_manager=eeg_rm) - target_tags = stags.get_smart_tags(p_func = target_tags_func, p_from = 60.0, p_len=21400.0*512) - nontarget_tags = stags.get_smart_tags(p_func = nontarget_tags_func, p_from = 60.0, p_len=21400.0*512) - - return target_tags, nontarget_tags - -def evoked_from_smart_tags(tags, chnames, bas = -0.1): - '''tags - smart tag list, to average - chnames - list of channels to use for averaging, - bas - baseline (in seconds)''' - min_length = min(i.get_samples().shape[1] for i in tags) - # really don't like this, but epochs generated by smart tags can vary in length by 1 sample - channels_data = [] - Fs = float(tags[0].get_param('sampling_frequency')) - for i in tags: - data = i.get_channels_samples(chnames)[:,:min_length] - for nr, chnl in enumerate(data): - data[nr] = chnl - np.mean(chnl[0:-Fs*bas])# baseline correction - if np.max(np.abs(data))<4000: - channels_data.append(data) - - return np.mean(channels_data, axis=0), scipy.stats.sem(channels_data, axis=0) - -def evoked_pair_plot_smart_tags(tags1, tags2, chnames=['O1', 'O2', 'Pz', 'PO7', 'PO8', 'PO3', 'PO4', 'Cz',], - start_offset=-0.1, labels=['target', 'nontarget']): - '''debug evoked potential plot, - pairwise comparison of 2 smarttag lists - chnames - channels to plot - start_offset - baseline in seconds''' - ev1, std1 = evoked_from_smart_tags(tags1, chnames, start_offset) - ev2, std2 = evoked_from_smart_tags(tags2, chnames, start_offset) - Fs = float(tags1[0].get_param('sampling_frequency')) - time = np.linspace(0+start_offset, ev1.shape[1]/Fs+start_offset, ev1.shape[1]) - pb.figure() - for nr, i in enumerate(chnames): - pb.subplot( (len(chnames)+1)/2, 2, nr+1) - pb.plot(time, ev1[nr], 'r',label = labels[0]+' N:{}'.format(len(tags1))) - pb.fill_between(time, ev1[nr]-std1[nr], ev1[nr]+std1[nr], - color = 'red', alpha=0.3, ) - pb.plot(time, ev2[nr], 'b', label = labels[1]+' N:{}'.format(len(tags2))) - pb.fill_between(time, ev2[nr]-std2[nr], ev2[nr]+std2[nr], - color = 'blue', alpha=0.3) - - pb.title(i) - pb.legend() - - pb.show() - -def testing_class(epochs, cl, target=1): - ''' testing p300easy, for one class - epochs - 3d array or list smart tag object of epochs of - one type (target or nontarget) - cl - p300easyclassifier - target - class target or nontarget (1, 0) - - returns accuracy - ''' - ndec = 0 - ncorr = 0 - nepochs = [] - for i in epochs: - nepoch = len(cl.epoch_buffor) - dec = cl.run(i) - if not dec is None: - ndec += 1 - nepochs.append(nepoch+1) - if int(target)==int(dec): - ncorr +=1 - - - #~ print dec, target, cl.decision_buffor - print ('ndec', ndec) - return ncorr*1./ndec, np.mean(nepochs) - - -if __name__=='__main__': - filter = [[1, 30.0], [0.5, 35.0], 3, 12] - filter=None - #~ filter = [30, 35, 3, 30] - montage = ['custom', 'ref'] - baseline = -.2 - window = 0.6 - ept, epnt = get_epochs_fromfile(ds, filter = filter, duration = 1, - montage = montage, - start_offset = baseline, - ) - print ('parameters:\n', ept[0].get_params()) - channel_names = ept[0].get_params()['channels_names'] - evoked_pair_plot_smart_tags(ept, epnt, labels=['target', 'nontarget'], chnames=['O1', 'O2']) - - training_split = 20 - tFs = 24 - feature_reduction = None - - cl = P300EasyClassifier(decision_stop=3, max_avr=1000, targetFs = tFs, - feature_reduction = feature_reduction) - - - print ("Accuracy on training set", cl.calibrate(ept[:training_split], epnt[:training_split], bas=baseline, window=window)) - result = testing_class(ept[training_split:], cl, 1) - print ("Accuracy on TARGETS", result[0], 'Mean epochs averaged:', result[1]) - result = testing_class(epnt[training_split:], cl, 0) - print ("Accuracy on NONTARGETS", result[0], 'Mean epochs averaged:', result[1]) - - - - - et = p300_class._tags_to_array(ept) - ft = p300_class._feature_extraction(et, 128., baseline, window, targetFs=tFs) - ent = p300_class._tags_to_array(epnt) - fnt = p300_class._feature_extraction(ent, 128., baseline, window, targetFs=tFs) - f = np.vstack((ft, fnt)) - labels = np.zeros(len(f)) - labels[:len(ft)] = 1 - if feature_reduction: - rmask = p300_class._feature_reduction_mask(f, labels, feature_reduction) - else: - rmask = np.ones(ft.shape[1],dtype=bool) - - - pb.subplot(121) - pb.plot(ft[:, rmask].T) - pb.title('Target features artifact removal off') - pb.subplot(122) - pb.title('NON Target features') - pb.plot(fnt[:, rmask].T) - pb.figure() - pb.title('Features without artifact removal') - pb.plot(np.mean(ft[:, rmask], axis=0).T, label='targets') - pb.plot(np.mean(fnt[:, rmask], axis=0).T, label='non targets') - pb.legend() - pb.figure() - pb.title('Features with artifact removal') - - aet, _ = p300_class._remove_artifact_epochs(et, len(et)*[1]) - aent, _ = p300_class._remove_artifact_epochs(ent, len(ent)*[1]) - aft = p300_class._feature_extraction(aet, 128., baseline, window, targetFs=tFs) - afnt = p300_class._feature_extraction(aent, 128., baseline, window, targetFs=tFs) - - pb.plot(np.mean(aft[:, rmask], axis=0).T, label='targets') - pb.plot(np.mean(afnt[:, rmask], axis=0).T, label='non targets') - pb.legend() - - pb.figure() - - pb.subplot(121) - pb.plot(aft[:, rmask].T) - pb.title('Target features artifact removal on') - pb.subplot(122) - pb.title('NON Target features') - pb.plot(afnt[:, rmask].T) - - pb.show() - - diff --git a/obci/interfaces/bci/p300_MD/helper_functions.py b/obci/interfaces/bci/p300_MD/helper_functions.py index 8c441631..d9515b11 100644 --- a/obci/interfaces/bci/p300_MD/helper_functions.py +++ b/obci/interfaces/bci/p300_MD/helper_functions.py @@ -5,12 +5,114 @@ from scipy import * from scipy import linalg import numpy as np +import pylab as pb from scipy import signal +import scipy.stats import copy from obci.analysis.obci_signal_processing.signal import read_info_source from obci.analysis.obci_signal_processing.signal import read_data_source from obci.analysis.obci_signal_processing.tags import read_tags_source from obci.analysis.obci_signal_processing import read_manager +from obci.analysis.obci_signal_processing.smart_tags_manager import SmartTagsManager +from obci.analysis.obci_signal_processing.tags.smart_tag_definition import SmartTagDurationDefinition + +def target_tags_func(tag): + return tag['desc'][u'index']==tag['desc'][u'target'] + +def nontarget_tags_func(tag): + return tag['desc'][u'index']!=tag['desc'][u'target'] + +def get_epochs_fromfile(ds, start_offset=-0.1,duration=2.0, + filter=None, montage=None, + drop_chnls = [ u'AmpSaw', u'DriverSaw', u'trig1', u'trig2']): + '''For offline calibration and testing, load target and nontarget + epochs using obci read_manager. + Args: + ds: dataset file name without extension. + start_offset: baseline in negative seconds, + duration: duration of the epoch (including baseline), + filter: list of [wp, ws, gpass, gstop] for scipy.signal.iirdesign + in Hz, Db, or None if no filtering is required + montage: list of ['montage name', ...] ...-channel names if required + montage name can be 'ears', 'csa', 'custom' + ears require 2 channel names for ear channels + custom requires list of reference channel names + Return: two lists of smart tags: target_tags, nontarget_tags''' + eeg_rm = read_manager.ReadManager(ds+'.xml', ds+'.raw', ds+'.tag') + eeg_rm = exclude_channels(eeg_rm, drop_chnls) + + if filter: + eeg_rm = mgr_filter(eeg_rm, filter[0], filter[1],filter[2], + filter[3], ftype='cheby2', use_filtfilt=True) + if montage: + if montage[0] == 'ears': + eeg_rm = montage_ears(eeg_rm, montage[1], montage[2]) + elif montage[0] == 'csa': + eeg_rm = montage_csa(eeg_rm) + elif montage[0] == 'custom': + eeg_rm = montage_custom(eeg_rm, montage[1:]) + else: + raise Exception('Unknown montage') + + + tag_def = SmartTagDurationDefinition(start_tag_name=u'blink', + start_offset=start_offset, + end_offset=0.0, + duration=duration) + stags = SmartTagsManager(tag_def, '', '' ,'', p_read_manager=eeg_rm) + target_tags = stags.get_smart_tags(p_func = target_tags_func, p_from = 60.0, p_len=21400.0*512) + nontarget_tags = stags.get_smart_tags(p_func = nontarget_tags_func, p_from = 60.0, p_len=21400.0*512) + + return target_tags, nontarget_tags + +def evoked_from_smart_tags(tags, chnames, bas = -0.1): + ''' + Args: + tags: smart tag list, to average + chnames: list of channels to use for averaging, + bas: baseline (in negative seconds)''' + min_length = min(i.get_samples().shape[1] for i in tags) + # really don't like this, but epochs generated by smart tags can vary in length by 1 sample + channels_data = [] + Fs = float(tags[0].get_param('sampling_frequency')) + for i in tags: + data = i.get_channels_samples(chnames)[:,:min_length] + for nr, chnl in enumerate(data): + data[nr] = chnl - np.mean(chnl[0:-Fs*bas])# baseline correction + if np.max(np.abs(data))<4000: + channels_data.append(data) + + return np.mean(channels_data, axis=0), scipy.stats.sem(channels_data, axis=0) + + +def evoked_pair_plot_smart_tags(tags1, tags2, chnames=['O1', 'O2', 'Pz', 'PO7', 'PO8', 'PO3', 'PO4', 'Cz',], + start_offset=-0.1, labels=['target', 'nontarget']): + '''debug evoked potential plot, + pairwise comparison of 2 smarttag lists, + blocks thread + Args: + chnames: channels to plot + start_offset: baseline in seconds''' + ev1, std1 = evoked_from_smart_tags(tags1, chnames, start_offset) + ev2, std2 = evoked_from_smart_tags(tags2, chnames, start_offset) + Fs = float(tags1[0].get_param('sampling_frequency')) + time = np.linspace(0+start_offset, ev1.shape[1]/Fs+start_offset, ev1.shape[1]) + pb.figure() + for nr, i in enumerate(chnames): + pb.subplot( (len(chnames)+1)/2, 2, nr+1) + pb.plot(time, ev1[nr], 'r',label = labels[0]+' N:{}'.format(len(tags1))) + pb.fill_between(time, ev1[nr]-std1[nr], ev1[nr]+std1[nr], + color = 'red', alpha=0.3, ) + pb.plot(time, ev2[nr], 'b', label = labels[1]+' N:{}'.format(len(tags2))) + pb.fill_between(time, ev2[nr]-std2[nr], ev2[nr]+std2[nr], + color = 'blue', alpha=0.3) + + pb.title(i) + pb.legend() + + pb.show() + + def mgr_filter(mgr, wp, ws, gpass, gstop, analog=0, ftype='ellip', output='ba', unit='hz', use_filtfilt=False, meancorr=1.0): if unit == 'radians': diff --git a/obci/interfaces/bci/p300_MD/p300_class.py b/obci/interfaces/bci/p300_MD/p300_class.py deleted file mode 100644 index cde8c027..00000000 --- a/obci/interfaces/bci/p300_MD/p300_class.py +++ /dev/null @@ -1,274 +0,0 @@ -# P300 classifier mockup -# Marian Dovgialo - -import numpy as np -import scipy.stats -import scipy.signal as ss -from sklearn.externals import joblib -from sklearn.discriminant_analysis import LinearDiscriminantAnalysis -from collections import deque -from obci.interfaces.bci.abstract_classifier import AbstractClassifier - -def _tags_to_array(tags): - '''returns 3D numpy array from OBCI smart tags - epochs x channels x time''' - min_length = min(i.get_samples().shape[1] for i in tags) -# really don't like this, but epochs generated by smart tags can vary in length by 1 sample - array = np.dstack([i.get_samples()[:,:min_length] for i in tags]) - return np.rollaxis(array,2) - -def _remove_artifact_epochs(data, labels): - ''' data - 3D numpy array epoch x channels x time, - labels - list of epochs labels - returns clean data and labels - Provisional version''' - mask = np.ones(len(data), dtype = bool) - for id, i in enumerate(data): - if np.max(np.abs(i-i[:,0][:, None]))>2000: - mask[id]=False - newlabels = [l for l, m in zip(labels, mask) if m] - newdata = data[mask] - return newdata, newlabels - - -def _feature_extraction(data, Fs, bas=-0.1, window=0.4, targetFs=34): - '''data - 3D numpy array epoch x channels x time, - returns spatiotemporal features array epoch x features''' - features = [] - for epoch in data: - features.append(_feature_extraction_singular(epoch, Fs, bas, - window, targetFs)) - return np.array(features) - - - -def _feature_extraction_singular(epoch, Fs, bas=-0.1, - window = 0.5, - targetFs=30,): - '''performs feature extraction on epoch (array channels x time), - Fs - sampling in Hz - bas - baseline in seconds - targetFs = target sampling in Hz (will be approximated) - window - timewindow after baseline to select in seconds - returns 1D array downsampled, len = downsampled samples x channels - - epoch minus mean of baseline, downsampled by factor int(Fs/targetFs) - samples used - from end of baseline to window timepoint - ''' - mean = np.mean(epoch[:, :-bas*Fs], axis=1) - decimation_factor = int(1.0*Fs/targetFs) - selected = epoch[:,-bas*Fs:(-bas+window)*Fs]-mean[:, None] - features = ss.decimate(selected, decimation_factor, axis=1, ftype='fir') - return features.flatten() - -def _feature_reduction_mask(ft, labels, mode): - ''' ft - features 2d array nsamples x nfeatures - labels - nsamples array of labels 0, 1, - mode - 'auto', int - returns - features mask''' - tscore, p = scipy.stats.ttest_ind(ft[labels==1], ft[labels==0]) - if mode == 'auto': - mask = p<0.05 - if mask.sum()<1: - raise Exception('Feature reduction produced zero usable features') - elif isinstance(mode, int): - mask_ind = np.argsort(p)[-mode:] - mask = np.zeros_like(p, dtype=bool) - mask[mask_ind] = True - return mask - - - -class P300EasyClassifier(object): - '''Easy and modular P300 classifier - attributes" - fname - classifier save filename - epoch_buffor - current epoch buffor - max_avr - maximum epochs to average - decision_buffor - last decisions buffor, when full of identical - decisions final decision is made - clf - core classifier from sklearn - feature_s - feature length''' - - def __init__(self, fname='./class.joblib.pkl', max_avr=10, - decision_stop=3, targetFs=30, clf=None, - feature_reduction = None,): - '''fname - classifier file to save or load classifier on disk - while classifying produce decision after max_avr epochs averaged, - or after decision_stop succesfull same decisions - targetFs - on feature extraction downsample to this Hz - clf - sklearn type classifier to use as core - feature_reduction - 'auto', int, None. If 'auto' - features are - reduced, features left are those which have statistically - significant (p<0.05) difference in target and nontarget, - if int - use feature_reduction most significant features, if - None don't use reduction - ''' - - self.targetFs = targetFs - self.fname = fname - self.epoch_buffor = [] - self.max_avr = max_avr - self.decision_buffor = deque([], decision_stop) - self.feature_reduction = feature_reduction - if clf is None: - self.clf = LinearDiscriminantAnalysis(solver = 'lsqr', shrinkage='auto') - - def load_classifier(self, fname=None): - '''loads classifier from disk, provide fname - path to joblib - pickle with classifier, or will be used from init''' - self.clf = joblib.load(fname) - - def reset(self): - self.epoch_buffor = [] - self.decision_buffor.clear() - - def calibrate(self, targets, nontargets, bas=-0.1, window=0.4, Fs=None): - '''targets, nontargets - 3D arrays (epoch x channel x time) - or list of OBCI smart tags - if arrays - need to provide Fs (sampling frequency) in Hz - bas - baseline in seconds(negative), in other words start offset''' - - if Fs is None: - Fs = float(targets[0].get_param('sampling_frequency')) - target_data = _tags_to_array(targets) - nontarget_data = _tags_to_array(nontargets) - data = np.vstack((target_data, nontarget_data)) - self.epoch_l = data.shape[2] - labels = np.zeros(len(data)) - labels[:len(target_data)] = 1 - data, labels = _remove_artifact_epochs(data, labels) - features = _feature_extraction(data, Fs, bas, window, self.targetFs) - - if self.feature_reduction: - mask = _feature_reduction_mask(features, labels, self.feature_reduction) - self.feature_reduction_mask = mask - features = features[:, mask] - - - self.feature_s = features.shape[1] - self.bas = bas - self.window = window - self.Fs - - - self.clf.fit(features, labels) - joblib.dump(self.clf, self.fname, compress=9) - return self.clf.score(features, labels) - - - - - def run(self, epoch, Fs=None): - '''epoch - array (channels x time) or smarttag/readmanager object, - Fs - sampling frequency Hz, leave None if epoch is smart tag, - returns decision - 1 for target, 0 for nontarget, - None - for no decision''' - if self.Fs is not None: - Fs = self.Fs - - decision = int(self.run_forced_percentage(self, epoch, Fs)>=0.5) - if len(self.decision_buffor) == self.decision_buffor.maxlen: - if len(set(self.decision_buffor))==1: - self.decision_buffor.clear() - self.epoch_buffor = [] - return decision - if len(self.epoch_buffor) == self.max_avr: - self.decision_buffor.clear() - self.epoch_buffor = [] - return decision - return None - - def run_forced_percentage(self, epoch, Fs=None): - '''epoch - array (channels x time) or smarttag/readmanager object, - Fs - sampling frequency Hz, leave None if epoch is smart tag, - returns probability for target''' - if self.Fs is not None: - Fs = self.Fs - bas = self.bas - window = self.window - if Fs is None: - Fs = float(epoch.get_param('sampling_frequency')) - epoch = epoch.get_samples()[:,:self.epoch_l] - if len(self.epoch_buffor)< self.max_avr: - self.epoch_buffor.append(epoch) - avr_epoch = np.mean(self.epoch_buffor, axis=0) - - features = _feature_extraction_singular(avr_epoch, - Fs, bas, window, self.targetFs)[None, :] - if self.feature_reduction: - mask = self.feature_reduction_mask - features = features[:, mask] - decision = self.clf.proba(features)[0][1] #probability of target - return decision - -class P300MetaClassifier(AbstractClassifier): - '''knows how many buttons are there loads P300EasyClassifiers and - uses them to decide which button were pressed - fname - classifier save filename (of P300EasyClassifier) - epoch_buffor - current epoch buffor - max_avr - maximum epochs to average - decision_buffor - last decisions buffor, when full of identical - decisions final decision is made - clf - core classifier from sklearn - feature_s - feature length''' - def __init__(self, fname='./class.joblib.pkl', max_avr=10, - decision_stop=3, targetFs=30, clf=None, - feature_reduction = None, buttons=2, - learnOnTheFly=False, - Fs = None, - baseline = None, - window = None): - '''fname - classifier file to save or load classifier on disk - while classifying produce decision after max_avr epochs averaged, - or after decision_stop succesfull same decisions - targetFs - on feature extraction downsample to this Hz - clf - sklearn type classifier to use as core - feature_reduction - 'auto', int, None. If 'auto' - features are - reduced, features left are those which have statistically - significant (p<0.05) difference in target and nontarget, - if int - use feature_reduction most significant features, if - None don't use reduction - buttons - how many virtual buttons are there to be selected from - ''' - self.learOnTheFly = learnOnTheFly - self.targetFs = targetFs - self.fname = fname - self.max_avr = max_avr - self.decision_buffor = deque([], decision_stop) - self.feature_reduction = feature_reduction - self.buttons = buttons - self.classifiers = [joblib.load(fname) for i in range(buttons)] - - def run(self, epoch, Fs, buttonId): - '''run classifiers on epoch - epoch - array (channels x time) or smarttag/readmanager object, - Fs - sampling frequency Hz, leave None if epoch is smart tag - buttonId - which button this epoch is associated with - (from 0 to buttons-1)''' - proba = self.classifiers[buttonId].run_forced_percentage(epoch, Fs) - if proba>=0.5: - decision = buttonId - if len(self.decision_buffor) == self.decision_buffor.maxlen: - if len(set(self.decision_buffor))==1: - self.decision_buffor.clear() - for clf in self.classifiers: - clf.epoch_buffor = [] - clf.decision_buffor.clear() - - return decision - if len(self.epoch_buffor) == self.max_avr: - self.decision_buffor.clear() - for clf in self.classifiers: - clf.epoch_buffor = [] - clf.decision_buffor.clear() - return decision - return None - - def new_button_config(buttons=2): - '''screen changed, new buttons, - buttons - how many buttons on new screen''' - self.buttons = buttons - self.classifiers = [joblib.load(self.fname) for i in range(buttons)] - - diff --git a/obci/interfaces/bci/p300_MD/p300_classm.py b/obci/interfaces/bci/p300_MD/p300_classm.py index 5c7e3fbe..d4a05579 100644 --- a/obci/interfaces/bci/p300_MD/p300_classm.py +++ b/obci/interfaces/bci/p300_MD/p300_classm.py @@ -87,22 +87,21 @@ def _feature_reduction_mask(ft, labels, mode): class P300EasyClassifier(object): '''Easy and modular P300 classifier attributes" - fname - classifier save filename - epoch_buffor - current epoch buffor - max_avr - maximum epochs to average - decision_buffor - last decisions buffor, when full of identical - decisions final decision is made - clf - core classifier from sklearn - feature_s - feature length''' + ''' - def __init__(self, clf=None, fname = './test.class', + def __init__(self, clf=None, targetFs=24): + '''Args: + clf: scikit learn type classifier, None if you want to + use standard LDA with shrinkage + targetFS: target sampling rate after downsampling for + feature extraction + ''' if clf is None: self.clf = LinearDiscriminantAnalysis(solver = 'lsqr', shrinkage='auto') - # store all examples - self.fname = fname self.targetFs = targetFs - + self.learning_buffor_features = list() + self.learning_buffor_classes = list() def classify(self, features): ''' @@ -136,10 +135,17 @@ def learn(self, chunk, target): def calibrate(self, targets, nontargets, bas=-0.1, window=0.4, Fs=None): '''Offline learning function - targets, nontargets - 3D arrays (epoch x channel x time) - or list of OBCI smart tags - if arrays - need to provide Fs (sampling frequency) in Hz - bas - baseline in seconds(negative), in other words start offset''' + Args: + + targets, nontargets: 3D arrays (epoch x channel x time) + or list of OBCI smart tags. + If arrays - need to provide Fs + (sampling frequency) in Hz + bas: baseline in seconds(negative), in other words start + offset + window: seconds after event to consider in classification + Fs: needed if 3D numpy array is provided + ''' if Fs is None: Fs = float(targets[0].get_param('sampling_frequency')) @@ -152,4 +158,8 @@ def calibrate(self, targets, nontargets, bas=-0.1, window=0.4, Fs=None): data, labels = _remove_artifact_epochs(data, labels) features = _feature_extraction(data, Fs, bas, window, self.targetFs) self.clf.fit(features, labels) + # building a list of features + #will be saved and can be extended in online learning later + self.learning_buffor_classes=[i for i in labels] + self.learning_buffor_features=[i for i in features] return self.clf.score(features, labels) diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.py b/obci/interfaces/bci/p300_MD/p300_master_peer.py index bd18913b..0b880e4c 100644 --- a/obci/interfaces/bci/p300_MD/p300_master_peer.py +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.py @@ -28,16 +28,16 @@ from obci.interfaces.bci.p300_MD.p300_classm import P300EasyClassifier from obci.interfaces.bci.p300_MD.p300_classm import _feature_extraction_singular -from helper_functions import get_montage, get_montage_matrix_custom -from helper_functions import leave_channels_array, get_channel_indexes +from obci.interfaces.bci.p300_MD.helper_functions import get_montage, get_montage_matrix_custom +from obci.interfaces.bci.p300_MD.helper_functions import leave_channels_array, get_channel_indexes from obci.analysis.buffers.auto_blink_buffer import AutoBlinkBuffer from collections import defaultdict, deque from operator import itemgetter from obci.utils.openbci_logging import log_crash import sys -from classifier_tests import get_epochs_fromfile -from classifier_tests import evoked_pair_plot_smart_tags +from obci.interfaces.bci.p300_MD.helper_functions import get_epochs_fromfile +from obci.interfaces.bci.p300_MD.helper_functions import evoked_pair_plot_smart_tags import numpy as np import os.path import pickle @@ -89,7 +89,6 @@ def _prepare_chunk(self, chunk, blink): ) return chunk_ready def _send_decision(self, decision): - ugm_helper.send_stop_blinking(self.conn) self.conn.send_message(message = str(decision), type = types.DECISION_MESSAGE, flush=True) self._reset_buffors() @@ -156,14 +155,12 @@ def create_classifier(self): classifier = pickle.load(clf_file) self.logger.info('loaded classifier from wisdom_path') except Exception as e: - classifier = P300EasyClassifier(fname=self.wisdom_path, - targetFs=self.targetFs) + classifier = P300EasyClassifier(targetFs=self.targetFs) self.logger.info("Loading classifier failed: {}".format(e)) self.logger.info('Creating new, untrained classifier') return classifier def identify_blink(self, blink): if self.training_index is not None: - if blink.index == training_index: return 'target' else: @@ -272,7 +269,7 @@ def learn_offline(self): self.logger.info('EPOCH PARAMS:\n{}'.format(ept[0].get_params())) evoked_pair_plot_smart_tags(ept, epnt, labels=['target', 'nontarget'], chnames=['O1', 'O2']) self.wisdom_path = self.wisdom_path = self.config.get_param('wisdom_path') - cl = P300EasyClassifier(fname = self.wisdom_path) + cl = P300EasyClassifier(targetFs=self.targetFs) result = cl.calibrate(ept, epnt, bas=baseline, window=window) self.logger.info('classifier self score on training set: {}'.format(result)) @@ -300,7 +297,6 @@ def classify(self, classifier, chunk, blink): """ chunk_ready = self._prepare_chunk(chunk, blink) - self.features[blink.index].append(chunk_ready) probabilities = {} probabilities['targetSingle'] = classifier.classify( diff --git a/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.ini b/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.ini deleted file mode 100644 index 38ebc6c1..00000000 --- a/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.ini +++ /dev/null @@ -1,7 +0,0 @@ -[local_params] -data_file_name=test1 -data_file_path=~ -montage=custom -montage_channels=Cz -offline = 1 -cl_filename = class diff --git a/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.py b/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.py deleted file mode 100644 index 87dc72cf..00000000 --- a/obci/interfaces/bci/p300_MD/p300_offline_learning_peer.py +++ /dev/null @@ -1,93 +0,0 @@ -from obci.control.peer.configured_multiplexer_server import ConfiguredMultiplexerServer -from multiplexer.multiplexer_constants import peers, types - -from classifier import get_epochs_fromfile -from classifier import evoked_pair_plot_smart_tags -from p300_class import P300EasyClassifier - -from obci.utils.openbci_logging import log_crash -from obci.configs import settings -import os.path -import sys -from sklearn.externals import joblib - -class P300OfflineLearner(ConfiguredMultiplexerServer): - """Class to run after acquiring calibration signal""" - @log_crash - def __init__(self, addresses): - super(P300OfflineLearner, self).__init__(addresses=addresses, - type=peers.LOGIC_P300_CSP) - self._data_finished = False - self._info_finished = False - self._tags_finished = False - - self.ready() - self.offline = int(self.config.get_param("offline")) - self.logger.info("offline? {} ".format(self.offline)) - if self.offline==1: - self.logger.info("offline learning has started") - self.learn() - - def _all_ready(self): - return self._data_finished and self._info_finished and self._tags_finished - - def handle_message(self, mxmsg): - if mxmsg.type == types.SIGNAL_SAVER_FINISHED: - self._data_finished = True - elif mxmsg.type == types.INFO_SAVER_FINISHED: - self._info_finished = True - elif mxmsg.type == types.TAG_SAVER_FINISHED: - self._tags_finished = True - else: - self.logger.warning("Unrecognised message received!!!!") - self.no_response() - - if self._all_ready(): - self.learn() - - def learn(self): - '''Function that reads saved calibration signal, splits it - to epochs and trains classifier''' - self.logger.info("STARTING LEARNING") - f_name = self.config.get_param("data_file_name") - f_dir = self.config.get_param("data_file_path") - cl_fname= self.config.get_param("cl_filename") - cl_fname = os.path.expanduser(os.path.join(f_dir, cl_fname)) - - filter = None - baseline = -.2 #read from file - window = 0.6 - montage = [self.config.get_param("montage")] - chnls = self.config.get_param("montage_channels").strip().split(';') - self.logger.info("reference channels {}".format(chnls)) - montage = montage+chnls - ds = os.path.expanduser(os.path.join(f_dir, f_name))+'.obci' - - - - ept, epnt = get_epochs_fromfile(ds, filter = filter, duration = 1, - montage = montage, - start_offset = baseline, - ) - self.logger.info("GOT {} TARGET EPOCHS AND {} NONTARGET".format(len(ept), len(epnt))) - if self.offline ==1: - self.logger.info('EPOCH PARAMS:\n{}'.format(ept[0].get_params())) - evoked_pair_plot_smart_tags(ept, epnt, labels=['target', 'nontarget'], chnames=['O1', 'O2']) - - cl = P300EasyClassifier(decision_stop=3, - max_avr=1000, - targetFs = 24, - feature_reduction = None, - fname = cl_fname) - result = cl.calibrate(ept, epnt, bas=baseline, window=window) - - self.logger.info('classifier self score on training set: {}'.format(result)) - - joblib.dump(cl, cl_fname, compress=3) - - self.logger.info("classifier -- DONE") - sys.exit(0) - - -if __name__ == '__main__': - P300OfflineLearner(settings.MULTIPLEXER_ADDRESSES).loop() diff --git a/obci/interfaces/bci/p300_MD/p300_online_decision_peer.ini b/obci/interfaces/bci/p300_MD/p300_online_decision_peer.ini deleted file mode 100644 index dfbcccb0..00000000 --- a/obci/interfaces/bci/p300_MD/p300_online_decision_peer.ini +++ /dev/null @@ -1,17 +0,0 @@ -[local_params] -montage=custom -montage_channels=Cz -cl_filename = class -hold_after_dec = 1 -data_file_path = ~ -data_file_name=test1 -cl_filename = class - -[config_sources] -logic= -amplifier= - -[external_params] -blink_field_ids=logic.active_field_ids -sampling_rate=amplifier.sampling_rate -channel_names=amplifier.channel_names diff --git a/obci/interfaces/bci/p300_MD/p300_online_decision_peer.py b/obci/interfaces/bci/p300_MD/p300_online_decision_peer.py deleted file mode 100644 index e82ca524..00000000 --- a/obci/interfaces/bci/p300_MD/p300_online_decision_peer.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# -from multiplexer.multiplexer_constants import peers, types -from obci.configs import settings, variables_pb2 - -from obci.control.peer.configured_multiplexer_server import ConfiguredMultiplexerServer -from obci.analysis.buffers import auto_blink_buffer -from obci.gui.ugm import ugm_helper -from obci.utils.openbci_logging import log_crash -from sklearn.externals import joblib -import os.path -from helper_functions import get_montage, get_montage_matrix_custom, get_channel_indexes -from p300_class import P300MetaClassifier - -MAX_AVR = 1000 -DECISION_STOP = 3 -TARGETFS = 24 -FEATURE_REDUCTION = None - -class P300AnalysisPeer(ConfiguredMultiplexerServer): - @log_crash - def __init__(self, addresses): - #Create a helper object to get configuration from the system - super(P300AnalysisPeer, self).__init__(addresses=addresses, - type=peers.P300_ANALYSIS) - self.clf_filepath = os.path.join( - self.config.get_param('data_file_path'), - self.config.get_param('cl_filename'), - ) - blink_field_ids = self.config.get_param('blink_field_ids').split(';') - self.blink_field_ids = [int(ids) for ids in blink_field_ids] - sampling = int(self.config.get_param('sampling_rate')) - self.sampling = sampling - self.channel_names = self.config.get_param('channel_names').split(';') - channels_count = len(self.channel_names) - - window = 0.6 - baseline = -.2 - self.buffer = auto_blink_buffer.AutoBlinkBuffer( - from_blink=int(sampling*baseline), - samples_count=int(window*sampling), - sampling=sampling, - num_of_channels=channels_count, - ret_func=self.return_blink, - ret_format="NUMPY_CHANNELS", - copy_on_ret=0 - ) - - self.clf = P300MetaClassifier(self.clf_filepath, MAX_AVR, - DECISION_STOP, TARGETFS, None, FEATURE_REDUCTION, - len(self.blink_field_ids)) - - - - self.montage = [self.config.get_param("montage")] - self.montage_channels = self.config.get_param("montage_channels").strip().split(';') - montage_ids = get_channel_indexes(self.channel_names, montage_channels) - self.montage_matrix = get_montage_matrix_custom(channels_count, - montage_ids, - ) - ugm_helper.send_start_blinking(self.conn) - self.ready() - def return_blink(self, blink, data): - '''function run after getting blink for a "button" highlight - runs classifier for that button, if classifier says it's a target - sends a message with decision - (implies that classifier is sure about blink being a target)''' - ind = blink.index - self.logger.info('got blink index:{}\ndata:{}'.format(ind, data)) - - data_ready = get_montage(data, self.montage_matrix) - - dec = self.clf.run(data_ready, self.sampling, self.blink_field_ids.index(ind)) #returns button ID - self.buffer.clear_blinks() # buffor is used to get one blink, classifiers have their own - if dec: - ugm_helper.send_stop_blinking(self.conn) - self.conn.send_message(message = str(self.blink_field_ids[ind]), type = types.DECISION_MESSAGE, flush=True) - - def handle_message(self, mxmsg): - 'sends signal to autoblinkbuffor''' - if mxmsg.type == types.AMPLIFIER_SIGNAL_MESSAGE: - l_msg = variables_pb2.SampleVector() - l_msg.ParseFromString(mxmsg.message) - self.buffer.handle_sample_vect(l_msg) - if mxmsg.type == types.BLINK_MESSAGE: - l_msg = variables_pb2.Blink() - l_msg.ParseFromString(mxmsg.message) - self.buffer.handle_blink(l_msg) - self.no_response() -if __name__ == "__main__": - P300AnalysisPeer(settings.MULTIPLEXER_ADDRESSES).loop() diff --git a/obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_signal_saver_config.ini b/obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_signal_saver_config.ini new file mode 100644 index 00000000..9de26e07 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_signal_saver_config.ini @@ -0,0 +1,2 @@ +[local_params] +save_file_name=test2 diff --git a/obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_switch_config.ini b/obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_switch_config.ini new file mode 100644 index 00000000..0ef39675 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_configs/p300_labyrinth_switch_config.ini @@ -0,0 +1,2 @@ +[local_params] +finish_saving=1 diff --git a/obci/scenarios/budzik/prototypes/p300_labyrinth_dummy.ini b/obci/scenarios/budzik/prototypes/p300_labyrinth_dummy.ini index d0b6dea7..305c7130 100644 --- a/obci/scenarios/budzik/prototypes/p300_labyrinth_dummy.ini +++ b/obci/scenarios/budzik/prototypes/p300_labyrinth_dummy.ini @@ -16,7 +16,7 @@ config=scenarios/budzik/prototypes/p300_configs/cap_brain2013_dummy.ini ;*********************************************** [peers.signal_saver] path=acquisition/signal_saver_peer.py -config=scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini +config=scenarios/budzik/prototypes/p300_configs/p300_labyrinth_signal_saver_config.ini [peers.signal_saver.launch_dependencies] amplifier=amplifier @@ -86,7 +86,7 @@ analysis=analysis ;*********************************************** [peers.switch_backup] path=interfaces/switch/backup/switch_backup_peer.py -config=scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini +config=scenarios/budzik/prototypes/p300_configs/p300_labyrinth_switch_config.ini ;*********************************************** [peers.switch] diff --git a/obci/scenarios/p300_MD/calibration_p300.ini b/obci/scenarios/p300_MD/calibration_p300.ini deleted file mode 100644 index 14e16f3a..00000000 --- a/obci/scenarios/p300_MD/calibration_p300.ini +++ /dev/null @@ -1,69 +0,0 @@ -[peers] -scenario_dir= -;*********************************************** -[peers.mx] -path=multiplexer-install/bin/mxcontrol - -;*********************************************** -[peers.config_server] -path=control/peer/config_server.py - -;*********************************************** -[peers.amplifier] -path=drivers/eeg/cpp_amplifiers/amplifier_tmsi.py -config=scenarios/brain2013/configs/cap_brain2013.ini - -;*********************************************** -[peers.signal_saver] -path=acquisition/signal_saver_peer.py - -[peers.signal_saver.launch_dependencies] -amplifier=amplifier - -;*********************************************** -[peers.info_saver] -path=acquisition/info_saver_peer.py - -[peers.info_saver.launch_dependencies] -amplifier=amplifier -signal_saver=signal_saver - -;*********************************************** -[peers.tag_saver] -path=acquisition/tag_saver_peer.py - -[peers.tag_saver.launch_dependencies] -signal_saver=signal_saver - - -;*********************************************** -[peers.ugm_engine] -path=gui/ugm/blinking/ugm_blinking_engine_peer.py -config=scenarios/brain2013/configs/p300_fast_colour_calibration.ini - -;*********************************************** -[peers.ugm_server] -path=gui/ugm/ugm_server_peer.py - -[peers.ugm_server.launch_dependencies] -ugm_engine=ugm_engine - -;*********************************************** -[peers.logic] -path=interfaces/bci/p300_fda/logic_p300_calibration_peer.py -config=scenarios/brain2013/configs/brain2013_logic_p300_calibraction.ini - -[peers.logic.launch_dependencies] -ugm_engine=ugm_engine -ugm_server=ugm_server -signal_saver=signal_saver - -;*********************************************** -[peers.clasifier] -path=interfaces/bci/p300_MD/p300_offline_learning_peer.py -config=scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini - -[peers.clasifier.launch_dependencies] -signal_saver=signal_saver -logic=logic - diff --git a/obci/scenarios/p300_MD/calibration_p300_dummy.ini b/obci/scenarios/p300_MD/calibration_p300_dummy.ini deleted file mode 100644 index 3454d0ce..00000000 --- a/obci/scenarios/p300_MD/calibration_p300_dummy.ini +++ /dev/null @@ -1,72 +0,0 @@ - - -[peers] -scenario_dir= -;*********************************************** -[peers.mx] -path=multiplexer-install/bin/mxcontrol - -;*********************************************** -[peers.config_server] -path=control/peer/config_server.py - -;*********************************************** -[peers.amplifier] -path=drivers/eeg/amplifier_virtual.py -config=scenarios/brain2013/configs/cap_brain2013_dummy.ini - -;*********************************************** -[peers.signal_saver] -path=acquisition/signal_saver_peer.py - -[peers.signal_saver.launch_dependencies] -amplifier=amplifier - -;*********************************************** -[peers.info_saver] -path=acquisition/info_saver_peer.py - -[peers.info_saver.launch_dependencies] -amplifier=amplifier -signal_saver=signal_saver - -;*********************************************** -[peers.tag_saver] -path=acquisition/tag_saver_peer.py - -[peers.tag_saver.launch_dependencies] -signal_saver=signal_saver - - -;*********************************************** -[peers.ugm_engine] -path=gui/ugm/blinking/ugm_blinking_engine_peer.py -config=scenarios/brain2013/configs/p300_fast_colour_calibration.ini - -;*********************************************** -[peers.ugm_server] -path=gui/ugm/ugm_server_peer.py - -[peers.ugm_server.launch_dependencies] -ugm_engine=ugm_engine - -;*********************************************** -[peers.logic] -path=interfaces/bci/p300_fda/logic_p300_calibration_peer.py -config=scenarios/brain2013/configs/brain2013_logic_p300_calibraction.ini - -[peers.logic.launch_dependencies] -ugm_engine=ugm_engine -ugm_server=ugm_server -signal_saver=signal_saver - -;*********************************************** -[peers.clasifier] -path=interfaces/bci/p300_MD/p300_offline_learning_peer.py -config=scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini - -[peers.clasifier.launch_dependencies] -signal_saver=signal_saver -logic=logic - - diff --git a/obci/scenarios/p300_MD/calibration_p300_offline.ini b/obci/scenarios/p300_MD/calibration_p300_offline.ini deleted file mode 100644 index 01266af4..00000000 --- a/obci/scenarios/p300_MD/calibration_p300_offline.ini +++ /dev/null @@ -1,21 +0,0 @@ - - -[peers] -scenario_dir= -;*********************************************** -[peers.mx] -path=multiplexer-install/bin/mxcontrol - -;*********************************************** -[peers.config_server] -path=control/peer/config_server.py - -;*********************************************** - -[peers.clasifier] -path=interfaces/bci/p300_MD/p300_offline_learning_peer.py -config=scenarios/p300_MD/configs/offline_calibration_p300_clasifier_config.ini - - - - diff --git a/obci/scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini b/obci/scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini deleted file mode 100644 index c319b294..00000000 --- a/obci/scenarios/p300_MD/configs/calibration_p300_clasifier_config.ini +++ /dev/null @@ -1,19 +0,0 @@ -[local_params] - -montage=custom -montage_channels=Cz -offline = 0 -cl_filename = test1_class.dump - -[config_sources] -signal_saver= -logic= - -[external_params] - -data_file_name=signal_saver.save_file_name -data_file_path=signal_saver.save_file_path - -[launch_dependencies] -signal_saver= -logic= diff --git a/obci/scenarios/p300_MD/configs/offline_calibration_p300_clasifier_config.ini b/obci/scenarios/p300_MD/configs/offline_calibration_p300_clasifier_config.ini deleted file mode 100644 index ee3507b1..00000000 --- a/obci/scenarios/p300_MD/configs/offline_calibration_p300_clasifier_config.ini +++ /dev/null @@ -1,13 +0,0 @@ -[local_params] - -montage=custom -montage_channels=Cz -offline = 1 -data_file_name=test1 -data_file_path=~ -cl_filename = test1_class.dump - - - - - diff --git a/obci/scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini b/obci/scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini deleted file mode 100644 index ee8f95eb..00000000 --- a/obci/scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini +++ /dev/null @@ -1,10 +0,0 @@ -[local_params] -data_file_name=test1 -data_file_path=~ -cl_filename = test1_class.dump - -[config_sources] -logic= - -[external_params] -blink_field_ids=logic.active_field_ids diff --git a/obci/scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini b/obci/scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini deleted file mode 100644 index da4e0d7b..00000000 --- a/obci/scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini +++ /dev/null @@ -1,2 +0,0 @@ -[local_params] -save_file_name=test2 \ No newline at end of file diff --git a/obci/scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini b/obci/scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini deleted file mode 100644 index 65a8e6fd..00000000 --- a/obci/scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini +++ /dev/null @@ -1,2 +0,0 @@ -[local_params] -finish_saving=1 \ No newline at end of file diff --git a/obci/scenarios/p300_MD/p300_labyrinth.ini b/obci/scenarios/p300_MD/p300_labyrinth.ini deleted file mode 100644 index a03e518f..00000000 --- a/obci/scenarios/p300_MD/p300_labyrinth.ini +++ /dev/null @@ -1,96 +0,0 @@ -[peers] -scenario_dir= -;*********************************************** -[peers.mx] -path=multiplexer-install/bin/mxcontrol - -;*********************************************** -[peers.config_server] -path=control/peer/config_server.py - -;*********************************************** -[peers.amplifier] -path=drivers/eeg/cpp_amplifiers/amplifier_tmsi.py -config=scenarios/brain2013/configs/cap_brain2013.ini - -;*********************************************** -[peers.signal_saver] -path=acquisition/signal_saver_peer.py -config=scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini - -[peers.signal_saver.launch_dependencies] -amplifier=amplifier - -;*********************************************** -[peers.tag_saver] -path=acquisition/tag_saver_peer.py - -[peers.tag_saver.launch_dependencies] -signal_saver=signal_saver - -;*********************************************** -[peers.info_saver] -path=acquisition/info_saver_peer.py - -[peers.info_saver.launch_dependencies] -amplifier=amplifier -signal_saver=signal_saver - -;*********************************************** -[peers.ugm_engine] -path=gui/ugm/blinking/ugm_blinking_engine_peer.py -config=scenarios/brain2013/configs/p300_fast_colour_bci.ini - -[peers.ugm_engine.config_sources] -logic=logic - -;*********************************************** -[peers.ugm_server] -path=gui/ugm/ugm_server_peer.py - -[peers.ugm_server.launch_dependencies] -ugm_engine=ugm_engine - -;*********************************************** -[peers.analysis] -path=interfaces/bci/p300_MD/p300_online_decision_peer.py -config=scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini - -[peers.analysis.config_sources] -logic=logic -amplifier=amplifier - -[peers.analysis.launch_dependencies] -logic=logic -amplifier=amplifier - -;************************************************* -[peers.logic] -path=logic/logic_maze_peer.py -config=scenarios/brain2013/configs/brain2013_logic_maze_peer.ini - -[peers.logic.launch_dependencies] -ugm=ugm_server -signal_saver=signal_saver - -;************************************ -[peers.feedback] -path=logic/feedback/logic_decision_feedback_peer.py - -[peers.feedback.launch_dependencies] -ugm_engine=ugm_engine -ugm_server=ugm_server -logic=logic -analysis=analysis - -;*********************************************** -[peers.switch_backup] -path=interfaces/switch/backup/switch_backup_peer.py -config=scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini - -;*********************************************** -[peers.switch] -path=drivers/switch/switch_amplifier_peer.py - -[peers.switch.launch_dependencies] -ugm_engine=ugm_engine diff --git a/obci/scenarios/p300_MD/p300_labyrinth_dummy.ini b/obci/scenarios/p300_MD/p300_labyrinth_dummy.ini deleted file mode 100644 index 266ecf9e..00000000 --- a/obci/scenarios/p300_MD/p300_labyrinth_dummy.ini +++ /dev/null @@ -1,96 +0,0 @@ -[peers] -scenario_dir= -;*********************************************** -[peers.mx] -path=multiplexer-install/bin/mxcontrol - -;*********************************************** -[peers.config_server] -path=control/peer/config_server.py - -;*********************************************** -[peers.amplifier] -path=drivers/eeg/amplifier_virtual.py -config=scenarios/brain2013/configs/cap_brain2013_dummy.ini - -;*********************************************** -[peers.signal_saver] -path=acquisition/signal_saver_peer.py -config=scenarios/p300_MD/configs/p300_labyrinth_signal_saver_config.ini - -[peers.signal_saver.launch_dependencies] -amplifier=amplifier - -;*********************************************** -[peers.tag_saver] -path=acquisition/tag_saver_peer.py - -[peers.tag_saver.launch_dependencies] -signal_saver=signal_saver - -;*********************************************** -[peers.info_saver] -path=acquisition/info_saver_peer.py - -[peers.info_saver.launch_dependencies] -amplifier=amplifier -signal_saver=signal_saver - -;*********************************************** -[peers.ugm_engine] -path=gui/ugm/blinking/ugm_blinking_engine_peer.py -config=scenarios/brain2013/configs/p300_fast_colour_bci.ini - -[peers.ugm_engine.config_sources] -logic=logic - -;*********************************************** -[peers.ugm_server] -path=gui/ugm/ugm_server_peer.py - -[peers.ugm_server.launch_dependencies] -ugm_engine=ugm_engine - -;*********************************************** -[peers.analysis] -path=interfaces/bci/p300_MD/p300_online_decision_peer.py -config=scenarios/p300_MD/configs/p300_labyrinth_clasifier_config.ini - -[peers.analysis.config_sources] -logic=logic -amplifier=amplifier - -[peers.analysis.launch_dependencies] -logic=logic -amplifier=amplifier - -;************************************************* -[peers.logic] -path=logic/logic_maze_peer.py -config=scenarios/brain2013/configs/brain2013_logic_maze_peer.ini - -[peers.logic.launch_dependencies] -ugm=ugm_server -signal_saver=signal_saver - -;************************************ -[peers.feedback] -path=logic/feedback/logic_decision_feedback_peer.py - -[peers.feedback.launch_dependencies] -ugm_engine=ugm_engine -ugm_server=ugm_server -logic=logic -analysis=analysis - -;*********************************************** -[peers.switch_backup] -path=interfaces/switch/backup/switch_backup_peer.py -config=scenarios/p300_MD/configs/p300_labyrinth_switch_config.ini - -;*********************************************** -[peers.switch] -path=drivers/switch/switch_amplifier_peer.py - -[peers.switch.launch_dependencies] -ugm_engine=ugm_engine From 54db28b20abc99e2f00295348025fe7a02105f0d Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 19 Apr 2016 18:31:20 +0200 Subject: [PATCH 03/28] added Working tester for p300 classifiers WIP --- obci/control/gui/presets/budzik.ini | 12 + .../bci/p300_MD/helper_functions.py | 4 +- obci/interfaces/bci/p300_MD/p300_classm.py | 11 +- .../bci/p300_MD/p300_master_peer.py | 13 +- .../bci/p300_MD/tests/synthetic_generator.ini | 23 ++ .../bci/p300_MD/tests/synthetic_generator.py | 223 ++++++++++++++++++ ...lassifier_synthetic_calibration_config.ini | 24 ++ .../p300_classifier_synthetic_test_config.ini | 24 ++ ...p300_tag_catcher_synthetic_test_config.ini | 8 + .../prototypes/p300_synthetic_test_online.ini | 31 +++ .../p300calibrate_synthetic_online.ini | 25 ++ 11 files changed, 386 insertions(+), 12 deletions(-) create mode 100644 obci/interfaces/bci/p300_MD/tests/synthetic_generator.ini create mode 100644 obci/interfaces/bci/p300_MD/tests/synthetic_generator.py create mode 100644 obci/scenarios/budzik/prototypes/p300_configs/p300_classifier_synthetic_calibration_config.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_configs/p300_classifier_synthetic_test_config.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_configs/p300_tag_catcher_synthetic_test_config.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_synthetic_test_online.ini create mode 100644 obci/scenarios/budzik/prototypes/p300calibrate_synthetic_online.ini diff --git a/obci/control/gui/presets/budzik.ini b/obci/control/gui/presets/budzik.ini index 5a0327d6..c0deb699 100644 --- a/obci/control/gui/presets/budzik.ini +++ b/obci/control/gui/presets/budzik.ini @@ -32,3 +32,15 @@ info=run p300 on calibrated classifier launch_file=scenarios/budzik/prototypes/p300_labyrinth_dummy.ini public_params= category=Prototypes P300 + +[P300 synthetic online calibration] +info=run p300 calibration on synthetic signal online +launch_file=scenarios/budzik/prototypes/p300calibrate_synthetic_online.ini +public_params= +category=Prototypes P300 + +[P300 online synthetic test] +info=run interactive synthetic test +launch_file=scenarios/budzik/prototypes/p300_synthetic_test_online.ini +public_params= +category=Prototypes P300 diff --git a/obci/interfaces/bci/p300_MD/helper_functions.py b/obci/interfaces/bci/p300_MD/helper_functions.py index d9515b11..9c131938 100644 --- a/obci/interfaces/bci/p300_MD/helper_functions.py +++ b/obci/interfaces/bci/p300_MD/helper_functions.py @@ -60,8 +60,8 @@ def get_epochs_fromfile(ds, start_offset=-0.1,duration=2.0, end_offset=0.0, duration=duration) stags = SmartTagsManager(tag_def, '', '' ,'', p_read_manager=eeg_rm) - target_tags = stags.get_smart_tags(p_func = target_tags_func, p_from = 60.0, p_len=21400.0*512) - nontarget_tags = stags.get_smart_tags(p_func = nontarget_tags_func, p_from = 60.0, p_len=21400.0*512) + target_tags = stags.get_smart_tags(p_func = target_tags_func, ) + nontarget_tags = stags.get_smart_tags(p_func = nontarget_tags_func) return target_tags, nontarget_tags diff --git a/obci/interfaces/bci/p300_MD/p300_classm.py b/obci/interfaces/bci/p300_MD/p300_classm.py index d4a05579..96678634 100644 --- a/obci/interfaces/bci/p300_MD/p300_classm.py +++ b/obci/interfaces/bci/p300_MD/p300_classm.py @@ -84,7 +84,7 @@ def _feature_reduction_mask(ft, labels, mode): -class P300EasyClassifier(object): +class P300EasyClassifier(AbstractClassifier): '''Easy and modular P300 classifier attributes" ''' @@ -103,6 +103,8 @@ def __init__(self, clf=None, self.learning_buffor_features = list() self.learning_buffor_classes = list() + + def classify(self, features): ''' Args: @@ -116,7 +118,7 @@ def learn(self, chunk, target): For online learning thread Args: - chunk: numpy 2D data array (channels × samples) + chunk: numpy 1D features array target: name of the class ''' if target == 'target': @@ -124,11 +126,14 @@ def learn(self, chunk, target): else: self.learning_buffor_classes.append(0) self.learning_buffor_features.append(chunk) - if sum(learning_buffor_classes)%LEARN_EVERY == 0: + if sum(self.learning_buffor_classes)%LEARN_EVERY == 0: + print('Classifier: fitting clf with new data') self.clf.fit( self.learning_buffor_features, self.learning_buffor_classes, ) + score = self.clf.score(features, labels) + print ('Classifier: Test on training set: {}'.format(score)) diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.py b/obci/interfaces/bci/p300_MD/p300_master_peer.py index 0b880e4c..ad01fbb7 100644 --- a/obci/interfaces/bci/p300_MD/p300_master_peer.py +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.py @@ -62,13 +62,12 @@ def _reset_buffors(self): #final decision buffor self.decision_buffor = deque(maxlen = self.decision_stop) - def _prepare_chunk(self, chunk, blink): + def _prepare_chunk(self, chunk): ''' Performs channel selection, montage and feature extraction. Args: chunk: numpy 2D data array (channels × samples) - blink: information contained in a single blink ''' chunk_clean_channels, _ = leave_channels_array( chunk, @@ -161,7 +160,7 @@ def create_classifier(self): return classifier def identify_blink(self, blink): if self.training_index is not None: - if blink.index == training_index: + if blink.index == self.training_index: return 'target' else: return 'nontarget' @@ -259,7 +258,7 @@ def learn_offline(self): ) ) - ept, epnt = get_epochs_fromfile(ds_path, filter = filter, duration = 1, + ept, epnt = get_epochs_fromfile(ds_path, filter = filter, duration = self.window+1, montage = montage, start_offset = baseline, drop_chnls = exclude_channels @@ -296,7 +295,7 @@ def classify(self, classifier, chunk, blink): or None if classification could not be performed """ - chunk_ready = self._prepare_chunk(chunk, blink) + chunk_ready = self._prepare_chunk(chunk) self.features[blink.index].append(chunk_ready) probabilities = {} probabilities['targetSingle'] = classifier.classify( @@ -322,8 +321,8 @@ def learn(self, classifier, chunk, target): chunk: numpy 2D data array (channels × samples) target: name of the target """ - chunk_ready = self._prepare_chunk(chunk, blink) - classifier.learn(chunk, target) + chunk_ready = self._prepare_chunk(chunk) + classifier.learn(chunk_ready, target) if __name__ == '__main__': P300MasterPeer(settings.MULTIPLEXER_ADDRESSES).loop() diff --git a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.ini b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.ini new file mode 100644 index 00000000..dba2eefe --- /dev/null +++ b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.ini @@ -0,0 +1,23 @@ +[local_params] +channel_names=O1;O2;Pz;Cz +active_channels=0;1;2;3 +channel_gains= +channel_offsets= +sampling_rate=512 +learning=0 +learning_target_field=1 +window=0.5 +blink_duration=0.1 +fields=1;2;3 +isi=0.25 +samples_per_packet=4 +test_trials_n=100 +noise_level=1 +delay=3 +statistics_path=~/synthetic_results +sample_type=FLOAT +synthetic=1 +targets_path= +nontargets_path= +meta_path= + diff --git a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py new file mode 100644 index 00000000..bd40007d --- /dev/null +++ b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# OpenBCI - framework for Brain-Computer Interfaces based on EEG signal +# Project was initiated by Magdalena Michalska and Krzysztof Kulewski +# as part of their MSc theses at the University of Warsaw. +# Copyright (C) 2008-2009 Krzysztof Kulewski and Magdalena Michalska +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Author: +# Marian Dovgialo + +from obci.configs import settings +from multiplexer.multiplexer_constants import peers, types +from obci.control.peer.configured_multiplexer_server import ConfiguredMultiplexerServer +from obci.configs import variables_pb2 +from obci.utils.openbci_logging import log_crash +import numpy as np +import time +import random +import sys +from scipy.signal import gaussian +import json +from threading import Thread +import pickle + +AMPLITUDE=20 + +class SyntheticGenerator(ConfiguredMultiplexerServer): + '''Peer to send synthetic blinks and signals, and receive + P300 decisions to make some statistics''' + @log_crash + def __init__(self, addresses): + super(SyntheticGenerator, self).__init__(addresses=addresses, + type=peers.LOGIC_FEEDBACK) + + self.synthetic = int(self.config.get_param('synthetic')) + if self.synthetic==0: + self.targets = np.load(self.config.get_param('targets_path')) + self.nontargets = np.load(self.config.get_param('nontargets_path')) + self.channels_names, self.sampling_rate, self.baseline, channel_gains = self.load_meta() + self.set_param('channel_gains', ';'.join(channel_gains)) + + else: + self.channels_names = self.config.get_param('channel_names').split(';') + self.sampling_rate = float(self.config.get_param('sampling_rate')) + self.set_param('channel_gains', ';'.join( + [str(1.0) for i in self.channels_names])) + self.set_param('channel_offsets', ';'.join( + [str(0.0) for i in self.channels_names])) + + self.learning = int(self.config.get_param('learning')) + self.samples_per_packet = int(self.config.get_param('samples_per_packet')) + self.window = float(self.config.get_param('window')) + self.noise_level = float(self.config.get_param('noise_level')) + self.fields_s = self.config.get_param('fields').split(';') + self.fields = [int(i) for i in self.fields_s] + #inter stimulus interval seconds + self.isi = float(self.config.get_param('isi')) + #selected field + if self.learning: + self.focus = int(self.config.get_param('learning_target_field')) + else: + self.focus = self.fields[0] + #seconds + self.time = time.time() + #timestamp of last blink + self.time_of_blink=0 + self.decisions = [] + self.sent_targets=0 + self.test_trials_n = int(self.config.get_param('test_trials_n')) + self.delay = float(self.config.get_param('delay')) + self.statistics_path = self.config.get_param('statistics_path') + self.command_thread = Thread(target=self.run) + self.command_thread.start() + + + self.logger.info('Init done') + self.ready() + + def load_meta(self): + with open(self.config.get_param('meta_path')) as datafile: + meta = json.load(datafile) + return meta['channels_names'], meta['sampling_rate'], meta['baseline'], meta['channel_gains'] + + + def send_nontarget_blink(self,): + self.logger.info('sending distractor blink') + choice = random.choice(tuple(set(self.fields)-set([self.focus]))) + b = variables_pb2.Blink() + timestamp = self.time + self.time_of_blink=timestamp + b.timestamp=timestamp + b.index=choice + self.conn.send_message(message = b.SerializeToString(), + type = types.BLINK_MESSAGE, flush=True) + + def send_target_blink(self, timestamp=None): + self.logger.info('sending target blink') + b = variables_pb2.Blink() + if not timestamp: + timestamp=self.time + self.time_of_blink=timestamp + b.timestamp=timestamp + b.index=self.focus + self.conn.send_message(message = b.SerializeToString(), + type = types.BLINK_MESSAGE, flush=True) + + def send_isi(self): + '''send empty signal''' + #send blink on "distractor" field + self.send_nontarget_blink() + packets = int((self.isi*self.sampling_rate)/self.samples_per_packet) + length = int(packets*self.samples_per_packet) + if self.synthetic==1: + signal=np.random.normal(scale=self.noise_level, + size=(length, len(self.channels_names))) + else: + selected_nontarget = random.randint(0, self.nontargets.shape[0]-1) + #nontargets are cut to isi + start=int(-self.baseline*self.sampling_rate) + end = start+length + signal = self.nontargets[selected_nontarget, :, start:end] + for p in xrange(packets): + sv = variables_pb2.SampleVector() + for sn in xrange(self.samples_per_packet): + s = sv.samples.add() + ind = p*self.samples_per_packet+sn + s.channels.extend(signal[:,ind].tolist()) + s.timestamp = self.time + self.time+=1.0/self.sampling_rate + time.sleep(self.samples_per_packet*1.0/self.sampling_rate) + self.conn.send_message(message = sv.SerializeToString(), + type = types.AMPLIFIER_SIGNAL_MESSAGE, flush=True) + + + + def send_target(self): + self.logger.info('Sending target, focus: {}'.format(self.focus)) + self.sent_targets+=1 + length_window = int(self.window*self.sampling_rate) + length_packet_aligned = length_window - length_window % self.samples_per_packet + gauss_w = gaussian(length_packet_aligned, length_packet_aligned/10) + chnl_n = len(self.channels_names) + + + if self.synthetic: + signal = np.empty((chnl_n, length_packet_aligned), dtype=float) + for nr in xrange(chnl_n): + #first channel will have biggest amplitude, last smallest + noise = np.random.normal(scale=self.noise_level, + size=length_packet_aligned) + signal[nr] = AMPLITUDE*gauss_w*((chnl_n*1.0-nr)/chnl_n)+noise + self.send_target_blink() + else: + selected_target = random.randint(0, self.targets.shape[0]-1) + signal = self.targets[selected_target] + self.send_target_blink(self.time-self.baseline) + + + + for i in xrange(length_packet_aligned/self.samples_per_packet): + sv = variables_pb2.SampleVector() + for sn in xrange(self.samples_per_packet): + s = sv.samples.add() + s.channels.extend(signal[:,i*self.samples_per_packet+sn].tolist()) + s.timestamp = self.time + self.time+=1.0/self.sampling_rate + time.sleep(self.samples_per_packet*1.0/self.sampling_rate) + self.conn.send_message(message = sv.SerializeToString(), + type = types.AMPLIFIER_SIGNAL_MESSAGE, flush=True) + if (self.time-self.time_of_blink)>self.isi: + self.send_nontarget_blink() + + + @log_crash + def run(self): + time.sleep(self.delay) + for i in xrange(self.test_trials_n): + for k in xrange(len(self.fields)-1): + self.send_isi() + self.send_target() + self.save_statistics() + sys.exit(0) + + def save_statistics(self): + + self.logger.info('saving statistics') + with open(self.statistics_path, 'w') as f: + json.dump(self.decisions, f) + self.logger.info('saving statistics - done: {}'.format(self.statistics_path)) + + def handle_message(self, mxmsg): + if mxmsg.type == types.DECISION_MESSAGE: + self.logger.info('Got message: {}'.format(mxmsg.message)) + + decision = int(mxmsg.message) + self.decisions.append({'sent_targets':self.sent_targets, + 'focus':self.focus, + 'got_decision':decision + } + ) + self.logger.info('got decision: {}'.format(self.decisions[-1])) + self.sent_targets = 0 + if self.learning == 0: + self.focus=random.choice(self.fields) + + + +if __name__=='__main__': + SyntheticGenerator(settings.MULTIPLEXER_ADDRESSES).loop() diff --git a/obci/scenarios/budzik/prototypes/p300_configs/p300_classifier_synthetic_calibration_config.ini b/obci/scenarios/budzik/prototypes/p300_configs/p300_classifier_synthetic_calibration_config.ini new file mode 100644 index 00000000..68d39c0b --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_configs/p300_classifier_synthetic_calibration_config.ini @@ -0,0 +1,24 @@ +[local_params] +wisdom_path=~/classifier_synthetic.dump +channels_for_classification=O1;O2;Pz;Cz +montage_channels=Cz +baseline=-0.2 +window=0.5 +maximum_to_average=10 +decision_stop=3 +downsample_to=24 +;which field is used in online calibration +calibration_field_index=1 +;if want to run offline calibration provide this file: +offline_learning=0 +offline_learning_dataset_path= + +[config_sources] +amplifier= + +[external_params] +channel_names=amplifier.channel_names +sampling_rate=amplifier.sampling_rate + +[launch_dependencies] +amplifier= diff --git a/obci/scenarios/budzik/prototypes/p300_configs/p300_classifier_synthetic_test_config.ini b/obci/scenarios/budzik/prototypes/p300_configs/p300_classifier_synthetic_test_config.ini new file mode 100644 index 00000000..ce8a6a82 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_configs/p300_classifier_synthetic_test_config.ini @@ -0,0 +1,24 @@ +[local_params] +wisdom_path=~/classifier_synthetic.dump +channels_for_classification=O1;O2;Pz;Cz +montage_channels=Cz +baseline=-0.2 +window=0.5 +maximum_to_average=10 +decision_stop=3 +downsample_to=24 +;which field is used in online calibration +calibration_field_index= +;if want to run offline calibration provide this file: +offline_learning=0 +offline_learning_dataset_path= + +[config_sources] +amplifier= + +[external_params] +channel_names=amplifier.channel_names +sampling_rate=amplifier.sampling_rate + +[launch_dependencies] +amplifier= diff --git a/obci/scenarios/budzik/prototypes/p300_configs/p300_tag_catcher_synthetic_test_config.ini b/obci/scenarios/budzik/prototypes/p300_configs/p300_tag_catcher_synthetic_test_config.ini new file mode 100644 index 00000000..16a9031c --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_configs/p300_tag_catcher_synthetic_test_config.ini @@ -0,0 +1,8 @@ +[config_sources] +ugm_engine= + +[external_params] +blink_duration=ugm_engine.blink_duration + +[launch_dependencies] +ugm_engine= diff --git a/obci/scenarios/budzik/prototypes/p300_synthetic_test_online.ini b/obci/scenarios/budzik/prototypes/p300_synthetic_test_online.ini new file mode 100644 index 00000000..bb32283c --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_synthetic_test_online.ini @@ -0,0 +1,31 @@ +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** +[peers.amplifier] +path=interfaces/bci/p300_MD/tests/synthetic_generator.py + +;*********************************************** +[peers.analysis] +path=interfaces/bci/p300_MD/p300_master_peer.py +config=scenarios/budzik/prototypes/p300_configs/p300_classifier_synthetic_test_config.ini + +[peers.analysis.config_sources] +amplifier=amplifier + +[peers.analysis.launch_dependencies] +amplifier=amplifier + +[peers.blink_catcher] +path=utils/blink_catcher_peer.py +config=scenarios/budzik/prototypes/p300_configs/p300_tag_catcher_synthetic_test_config.ini +[peers.blink_catcher.launch_dependencies] +ugm_engine=amplifier + diff --git a/obci/scenarios/budzik/prototypes/p300calibrate_synthetic_online.ini b/obci/scenarios/budzik/prototypes/p300calibrate_synthetic_online.ini new file mode 100644 index 00000000..df3b27f3 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300calibrate_synthetic_online.ini @@ -0,0 +1,25 @@ +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** +[peers.amplifier] +path=interfaces/bci/p300_MD/tests/synthetic_generator.py + +;*********************************************** +[peers.analysis] +path=interfaces/bci/p300_MD/p300_master_peer.py +config=scenarios/budzik/prototypes/p300_configs/p300_classifier_synthetic_calibration_config.ini + +[peers.analysis.config_sources] +amplifier=amplifier + +[peers.analysis.launch_dependencies] +amplifier=synthetic_generator + From 7f2d613cf7025560d4af535ad25fdc036f982506 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Thu, 21 Apr 2016 17:05:06 +0200 Subject: [PATCH 04/28] Working synthetic on real data --- obci/control/gui/presets/budzik.ini | 8 ++- .../bci/p300_MD/helper_functions.py | 5 +- obci/interfaces/bci/p300_MD/p300_classm.py | 13 ++-- .../bci/p300_MD/p300_master_peer.py | 38 ++++++---- .../bci/p300_MD/tests/synthetic_generator.py | 70 +++++++++++++------ .../p300_offline_calibration_config.ini | 4 +- .../p300_online_synthetic_real_data_test.ini | 35 ++++++++++ .../amplifier.ini | 21 ++++++ .../analysis.ini | 18 +++++ .../blink_catcher.ini | 14 ++++ .../config_server.ini | 14 ++++ .../mx.ini | 14 ++++ 12 files changed, 211 insertions(+), 43 deletions(-) create mode 100644 obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/amplifier.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/analysis.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/blink_catcher.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/config_server.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/mx.ini diff --git a/obci/control/gui/presets/budzik.ini b/obci/control/gui/presets/budzik.ini index c0deb699..f1b19d73 100644 --- a/obci/control/gui/presets/budzik.ini +++ b/obci/control/gui/presets/budzik.ini @@ -39,8 +39,14 @@ launch_file=scenarios/budzik/prototypes/p300calibrate_synthetic_online.ini public_params= category=Prototypes P300 -[P300 online synthetic test] +[P300 online synthetic test generated data] info=run interactive synthetic test launch_file=scenarios/budzik/prototypes/p300_synthetic_test_online.ini public_params= category=Prototypes P300 + +[P300 online synthetic test using real data] +info=run interactive synthetic test target and nontarget epochs from real EEG signal +launch_file=scenarios/budzik/prototypes/p300_online_synthetic_real_data_test.ini +public_params= +category=Prototypes P300 diff --git a/obci/interfaces/bci/p300_MD/helper_functions.py b/obci/interfaces/bci/p300_MD/helper_functions.py index 9c131938..987398aa 100644 --- a/obci/interfaces/bci/p300_MD/helper_functions.py +++ b/obci/interfaces/bci/p300_MD/helper_functions.py @@ -39,7 +39,7 @@ def get_epochs_fromfile(ds, start_offset=-0.1,duration=2.0, custom requires list of reference channel names Return: two lists of smart tags: target_tags, nontarget_tags''' eeg_rm = read_manager.ReadManager(ds+'.xml', ds+'.raw', ds+'.tag') - eeg_rm = exclude_channels(eeg_rm, drop_chnls) + if filter: eeg_rm = mgr_filter(eeg_rm, filter[0], filter[1],filter[2], @@ -53,6 +53,7 @@ def get_epochs_fromfile(ds, start_offset=-0.1,duration=2.0, eeg_rm = montage_custom(eeg_rm, montage[1:]) else: raise Exception('Unknown montage') + eeg_rm = exclude_channels(eeg_rm, drop_chnls) tag_def = SmartTagDurationDefinition(start_tag_name=u'blink', @@ -483,7 +484,7 @@ def get_montage_matrix_custom(n, indexes): Return nxn array representing extraction from every channel an avarage of channels in indexes list - >>> get_montage_matrix_ears(5, 2, 4) + >>> get_montage_matrix_custom(5, [2, 4]) array([[ 1. , 0. , -0.5, 0. , -0.5], [ 0. , 1. , -0.5, 0. , -0.5], [ 0. , 0. , 1. , 0. , 0. ], diff --git a/obci/interfaces/bci/p300_MD/p300_classm.py b/obci/interfaces/bci/p300_MD/p300_classm.py index 96678634..4fc4d56b 100644 --- a/obci/interfaces/bci/p300_MD/p300_classm.py +++ b/obci/interfaces/bci/p300_MD/p300_classm.py @@ -51,12 +51,13 @@ def _feature_extraction_singular(epoch, Fs, bas=-0.1, window = 0.5, targetFs=30,): '''performs feature extraction on epoch (array channels x time), - Fs - sampling in Hz - bas - baseline in seconds - targetFs = target sampling in Hz (will be approximated) - window - timewindow after baseline to select in seconds - returns 1D array downsampled, len = downsampled samples x channels - + Args: + Fs: sampling in Hz + bas: baseline in seconds + targetFs: target sampling in Hz (will be approximated) + window: timewindow after baseline to select in seconds + + Returns: 1D array downsampled, len = downsampled samples x channels epoch minus mean of baseline, downsampled by factor int(Fs/targetFs) samples used - from end of baseline to window timepoint ''' diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.py b/obci/interfaces/bci/p300_MD/p300_master_peer.py index ad01fbb7..4dc2c20b 100644 --- a/obci/interfaces/bci/p300_MD/p300_master_peer.py +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.py @@ -41,7 +41,7 @@ import numpy as np import os.path import pickle - +import pylab as pb from obci.gui.ugm import ugm_helper class P300MasterPeer(AnalysisMaster): @@ -66,21 +66,27 @@ def _prepare_chunk(self, chunk): ''' Performs channel selection, montage and feature extraction. + First Montage - look out for average montage and technical channels!! + then channel selection + then feature extraction + Args: chunk: numpy 2D data array (channels × samples) ''' + + chunk_montage = get_montage( + chunk, + self.montage_matrix + ) chunk_clean_channels, _ = leave_channels_array( - chunk, + chunk_montage, self.channels_for_classification, self.channel_names ) - chunk_montage = get_montage( - chunk_clean_channels, - self.montage_matrix - ) + chunk_ready = _feature_extraction_singular( - chunk_montage, + chunk_clean_channels, self.sampling_rate, self.baseline, self.window, @@ -108,7 +114,9 @@ def add_result(self, blink, probabilities): ) last_single = [blink.index, probabilities['targetSingle']] last_mean = [blink.index, probabilities['targetCMean']] - self.logger.info('Last mean proba: {}'.format(last_mean)) + self.logger.info('Last mean dec: {}, proba: {:.2f}'.format( + last_mean[0], + last_mean[1])) if last_mean[1]>0.5: self.decision_buffor.append(blink.index) @@ -122,10 +130,11 @@ def add_result(self, blink, probabilities): return # number of averaged epochs condition - maximum_averaged = max( + # ensure all buttons have been averaged self.maximum_to_average number of time + minimum_averaged = min( len(self.averaged_proba_buffor[i]) for i in self.averaged_proba_buffor.keys() ) - if maximum_averaged > self.maximum_to_average: + if minimum_averaged > self.maximum_to_average: most_confident_decision = max( self.averaged_proba_buffor.items(), key = lambda key: key[1][-1] @@ -183,7 +192,7 @@ def init_params(self): #montage type self.montage = 'custom' #montage channels - channels_count = len(self.channels_for_classification) + channels_count = len(self.channel_names) self.montage_channels = self.config.get_param("montage_channels").strip().split(';') montage_ids = get_channel_indexes(self.channel_names, self.montage_channels) self.montage_matrix = get_montage_matrix_custom(channels_count, @@ -275,7 +284,12 @@ def learn_offline(self): with open(self.wisdom_path, 'w') as fname: pickle.dump(cl, fname) self.logger.info("classifier -- DONE") - + #plot features: + f = np.array(cl.learning_buffor_features) + l = np.array(cl.learning_buffor_classes) + pb.plot(np.median(f[l==0], axis=0)) + pb.plot(np.median(f[l==1], axis=0)) + pb.show() def classify(self, classifier, chunk, blink): diff --git a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py index bd40007d..e99b3643 100644 --- a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py +++ b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py @@ -34,6 +34,7 @@ from scipy.signal import gaussian import json from threading import Thread +from threading import RLock import pickle AMPLITUDE=20 @@ -46,6 +47,7 @@ def __init__(self, addresses): super(SyntheticGenerator, self).__init__(addresses=addresses, type=peers.LOGIC_FEEDBACK) + self.lock = RLock() self.synthetic = int(self.config.get_param('synthetic')) if self.synthetic==0: self.targets = np.load(self.config.get_param('targets_path')) @@ -120,8 +122,6 @@ def send_target_blink(self, timestamp=None): def send_isi(self): '''send empty signal''' - #send blink on "distractor" field - self.send_nontarget_blink() packets = int((self.isi*self.sampling_rate)/self.samples_per_packet) length = int(packets*self.samples_per_packet) if self.synthetic==1: @@ -133,17 +133,24 @@ def send_isi(self): start=int(-self.baseline*self.sampling_rate) end = start+length signal = self.nontargets[selected_nontarget, :, start:end] + sleeping_time = self.samples_per_packet*1.0/self.sampling_rate for p in xrange(packets): sv = variables_pb2.SampleVector() for sn in xrange(self.samples_per_packet): + t0 = time.time() s = sv.samples.add() ind = p*self.samples_per_packet+sn s.channels.extend(signal[:,ind].tolist()) s.timestamp = self.time self.time+=1.0/self.sampling_rate - time.sleep(self.samples_per_packet*1.0/self.sampling_rate) self.conn.send_message(message = sv.SerializeToString(), type = types.AMPLIFIER_SIGNAL_MESSAGE, flush=True) + if (self.time-self.time_of_blink)>self.isi: + #send blink on "distractor" field + self.send_nontarget_blink() + + left_to_sleep = sleeping_time-(time.time()-t0) + time.sleep(left_to_sleep if left_to_sleep>0 else 0) @@ -170,19 +177,21 @@ def send_target(self): self.send_target_blink(self.time-self.baseline) - + sleeping_time = self.samples_per_packet*1.0/self.sampling_rate for i in xrange(length_packet_aligned/self.samples_per_packet): + t0 = time.time() sv = variables_pb2.SampleVector() for sn in xrange(self.samples_per_packet): s = sv.samples.add() s.channels.extend(signal[:,i*self.samples_per_packet+sn].tolist()) s.timestamp = self.time self.time+=1.0/self.sampling_rate - time.sleep(self.samples_per_packet*1.0/self.sampling_rate) self.conn.send_message(message = sv.SerializeToString(), type = types.AMPLIFIER_SIGNAL_MESSAGE, flush=True) if (self.time-self.time_of_blink)>self.isi: self.send_nontarget_blink() + left_to_sleep = sleeping_time-(time.time()-t0) + time.sleep(left_to_sleep if left_to_sleep>0 else 0) @log_crash @@ -190,9 +199,13 @@ def run(self): time.sleep(self.delay) for i in xrange(self.test_trials_n): for k in xrange(len(self.fields)-1): - self.send_isi() - self.send_target() - self.save_statistics() + #handle_message can wait a while + with self.lock: + self.send_isi() + with self.lock: + self.send_target() + with self.lock: + self.save_statistics() sys.exit(0) def save_statistics(self): @@ -201,21 +214,38 @@ def save_statistics(self): with open(self.statistics_path, 'w') as f: json.dump(self.decisions, f) self.logger.info('saving statistics - done: {}'.format(self.statistics_path)) + N = len(self.decisions) + correct = 0 + targetsN = 0 + for s in self.decisions: + if s['focus'] == s['got_decision']: + correct+=1 + targetsN += s['sent_targets'] + correctperc = 100.0*correct/N + mean_targets = targetsN*1.0/N + self.logger.info( + '''Basic statistics: + correct classifications: {:.2f}\% {} out of {} + mean targets required {:.2f}'''.format(channel_names, correct, N, mean_targets) + ) + def handle_message(self, mxmsg): if mxmsg.type == types.DECISION_MESSAGE: - self.logger.info('Got message: {}'.format(mxmsg.message)) - - decision = int(mxmsg.message) - self.decisions.append({'sent_targets':self.sent_targets, - 'focus':self.focus, - 'got_decision':decision - } - ) - self.logger.info('got decision: {}'.format(self.decisions[-1])) - self.sent_targets = 0 - if self.learning == 0: - self.focus=random.choice(self.fields) + with self.lock: + self.logger.info('Got message: {}'.format(mxmsg.message)) + + decision = int(mxmsg.message) + self.decisions.append({'sent_targets':self.sent_targets, + 'focus':self.focus, + 'got_decision':decision + } + ) + self.logger.info('got decision: {}'.format(self.decisions[-1])) + self.sent_targets = 0 + if self.learning == 0: + self.focus=random.choice(self.fields) + self.no_response() diff --git a/obci/scenarios/budzik/prototypes/p300_configs/p300_offline_calibration_config.ini b/obci/scenarios/budzik/prototypes/p300_configs/p300_offline_calibration_config.ini index 0225192c..b8611ac0 100644 --- a/obci/scenarios/budzik/prototypes/p300_configs/p300_offline_calibration_config.ini +++ b/obci/scenarios/budzik/prototypes/p300_configs/p300_offline_calibration_config.ini @@ -1,7 +1,7 @@ [local_params] wisdom_path=~/classifier.dump -channels_for_classification=O1;O2;Pz;Cz -montage_channels=Cz +channels_for_classification=O1;O2;P3;P4 +montage_channels=ref baseline=-0.2 window=0.5 maximum_to_average=10 diff --git a/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test.ini b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test.ini new file mode 100644 index 00000000..8885f232 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test.ini @@ -0,0 +1,35 @@ +[peers] +scenario_dir = + +[peers.blink_catcher] +path = utils/blink_catcher_peer.py +config = scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/blink_catcher.ini + +[peers.blink_catcher.config_sources] +ugm_engine = amplifier + +[peers.blink_catcher.launch_dependencies] +ugm_engine = amplifier + +[peers.amplifier] +path = interfaces/bci/p300_MD/tests/synthetic_generator.py +config =scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/amplifier.ini + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/mx.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/analysis.ini + +[peers.analysis.config_sources] +amplifier = amplifier + +[peers.analysis.launch_dependencies] +amplifier = amplifier + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/config_server.ini + diff --git a/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/amplifier.ini b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/amplifier.ini new file mode 100644 index 00000000..47c908e7 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/amplifier.ini @@ -0,0 +1,21 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +synthetic = 0 +targets_path = ~/dataset/nonsythetic/targets.npy +active_channels = 0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19 +statistics_path = ~/real_results +channel_names = T5;O1;O2;T6;P3;Pz;P4;T3;C3;Cz;C4;T4;F7;F3;Fz;F4;F8;Fp1;Fp2;ref +experiment_uuid = +meta_path = ~/dataset/nonsythetic/meta.json +nontargets_path = ~/dataset/nonsythetic/nontargets.npy +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/analysis.ini b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/analysis.ini new file mode 100644 index 00000000..571c3727 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/analysis.ini @@ -0,0 +1,18 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +file_log_level = debug +montage_channels = ref +sentry_log_level = error +offline_learning = 0 +mx_log_level = info +channels_for_classification = O1;O2;P3;P4 +log_dir = ~/.obci/logs +downsample_to = 24 diff --git a/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/blink_catcher.ini b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/blink_catcher.ini new file mode 100644 index 00000000..a1fff3bd --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/blink_catcher.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = error +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/config_server.ini b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/config_server.ini new file mode 100644 index 00000000..a1fff3bd --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/config_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = error +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/mx.ini b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/mx.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_online_synthetic_real_data_test_configs/mx.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error From ec2be6352f40917cc3df288ed3d400a7e9a7e47c Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Thu, 21 Apr 2016 17:43:45 +0200 Subject: [PATCH 05/28] Thread locks typos --- obci/interfaces/bci/p300_MD/p300_classm.py | 6 +++++- .../bci/p300_MD/tests/synthetic_generator.py | 11 +++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/obci/interfaces/bci/p300_MD/p300_classm.py b/obci/interfaces/bci/p300_MD/p300_classm.py index 4fc4d56b..954b04ad 100644 --- a/obci/interfaces/bci/p300_MD/p300_classm.py +++ b/obci/interfaces/bci/p300_MD/p300_classm.py @@ -7,7 +7,11 @@ import scipy.stats import scipy.signal as ss from sklearn.externals import joblib -from sklearn.discriminant_analysis import LinearDiscriminantAnalysis +try: + from sklearn.lda import LDA as LinearDiscriminantAnalysis +except ImportError: + from sklearn.discriminant_analysis import LinearDiscriminantAnalysis + from collections import deque from obci.interfaces.bci.abstract_classifier import AbstractClassifier import pickle diff --git a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py index e99b3643..06ce85ee 100644 --- a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py +++ b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py @@ -156,7 +156,8 @@ def send_isi(self): def send_target(self): self.logger.info('Sending target, focus: {}'.format(self.focus)) - self.sent_targets+=1 + with self.lock: + self.sent_targets+=1 length_window = int(self.window*self.sampling_rate) length_packet_aligned = length_window - length_window % self.samples_per_packet gauss_w = gaussian(length_packet_aligned, length_packet_aligned/10) @@ -200,10 +201,8 @@ def run(self): for i in xrange(self.test_trials_n): for k in xrange(len(self.fields)-1): #handle_message can wait a while - with self.lock: - self.send_isi() - with self.lock: - self.send_target() + self.send_isi() + self.send_target() with self.lock: self.save_statistics() sys.exit(0) @@ -226,7 +225,7 @@ def save_statistics(self): self.logger.info( '''Basic statistics: correct classifications: {:.2f}\% {} out of {} - mean targets required {:.2f}'''.format(channel_names, correct, N, mean_targets) + mean targets required {:.2f}'''.format(correctperc, correct, N, mean_targets) ) From 3f3fcdd5f9aa1aa02828266b11c8e2ea57372d0c Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Thu, 5 May 2016 19:21:21 +0200 Subject: [PATCH 06/28] Auditory (untested) and haptic blining reworked sound on pyo --- obci/control/gui/presets/budzik.ini | 6 + obci/gui/ugm/blinking/ugm_blinking_engine.py | 2 +- .../ugm/blinking/ugm_modal_blinking_engine.py | 132 ++++++++++++++++++ .../ugm_modal_blinking_engine_peer.ini | 54 +++++++ .../ugm_modal_blinking_engine_peer.py | 57 ++++++++ .../modal_p300/config/p300_all_modalities.ini | 53 +++++++ .../modal_p300/p300_labyrinth_dummy.ini | 96 +++++++++++++ 7 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 obci/gui/ugm/blinking/ugm_modal_blinking_engine.py create mode 100644 obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini create mode 100644 obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.py create mode 100644 obci/scenarios/budzik/prototypes/modal_p300/config/p300_all_modalities.ini create mode 100644 obci/scenarios/budzik/prototypes/modal_p300/p300_labyrinth_dummy.ini diff --git a/obci/control/gui/presets/budzik.ini b/obci/control/gui/presets/budzik.ini index f1b19d73..665645b1 100644 --- a/obci/control/gui/presets/budzik.ini +++ b/obci/control/gui/presets/budzik.ini @@ -50,3 +50,9 @@ info=run interactive synthetic test target and nontarget epochs from real EEG si launch_file=scenarios/budzik/prototypes/p300_online_synthetic_real_data_test.ini public_params= category=Prototypes P300 + +[P300 online labirynth all modalities] +info=run interactive labirynth P300 on dummy amp with 3 modalities +launch_file=scenarios/budzik/prototypes/modal_p300/p300_labyrinth_dummy.ini +public_params= +category=Prototypes P300 diff --git a/obci/gui/ugm/blinking/ugm_blinking_engine.py b/obci/gui/ugm/blinking/ugm_blinking_engine.py index 75bdada1..2cda217d 100644 --- a/obci/gui/ugm/blinking/ugm_blinking_engine.py +++ b/obci/gui/ugm/blinking/ugm_blinking_engine.py @@ -66,7 +66,7 @@ def _schedule_blink(self, start_time): self._curr_blink_id = self.id_mgr.get_id() self._curr_blink_ugm = self.ugm_mgr.get_blink_ugm(self._curr_blink_id) self._curr_unblink_ugm = self.ugm_mgr.get_unblink_ugm(self._curr_blink_id) - curr_time = self.time_mgr.get_time() + curr_time = self.time_mgr.get_time() #time if next blink I guess? t = 1000*(curr_time - (time.time()-start_time)) if t < 0: t = 0.0 diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py new file mode 100644 index 00000000..012d581f --- /dev/null +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: +# Marian Dovgialo + +# requires pyo for sound +from __future__ import print_function +from ugm_blinking_engine import UgmBlinkingEngine +try: + import pyo +except ImportError: + print ('ERROR no sound library.\n\t\t Installl pyo!\n\t\tsudo apt-get install python-pyo') +from obci.devices.haptics.HapticsControl import HapticStimulator +from obci.utils import context as ctx +from obci.gui.ugm import ugm_engine +from PyQt4 import QtCore +import time + + +class UgmModalBlinkingEngine(UgmBlinkingEngine): + """A class representing ugm application. It is supposed to fire ugm, + receive messages from outside (UGM_UPDATE_MESSAGES) and send`em to + ugm pyqt structure so that it can refresh. + Can be used to evoke P300 of different modalities + (visual, haptic, auditory, combinations). + """ + def __init__(self, p_config_manager, p_connection, + context=ctx.get_dummy_context('UgmBlinkingEngine'), + modalities=['visual',]): + """Store config manager. + Args: + Modalities: modalities used for stimulation, list of strings. + available options: 'visual', 'auditory', 'haptic' + """ + + super(UgmModalBlinkingEngine, self).__init__(p_config_manager, + p_connection, + context) + self.visual = 'visual' in modalities + self.haptic = 'haptic' in modalities + self.auditory = 'auditory' in modalities + + #must be here or else they connect to parent class methods + self._blink_timer = QtCore.QTimer(self) + self._blink_timer.setSingleShot(True) + self._blink_timer.connect(self._blink_timer, QtCore.SIGNAL("timeout()"), self._blink) + + self._unblink_timer = QtCore.QTimer(self) + self._unblink_timer.setSingleShot(True) + self._unblink_timer.connect(self._unblink_timer, QtCore.SIGNAL("timeout()"), self._unblink) + + self._stop_timer = QtCore.QTimer(self) + self._stop_timer.setSingleShot(True) + self._stop_timer.connect(self._stop_timer, QtCore.SIGNAL("timeout()"), self._stop) + + + if self._run_on_start: + self.start_blinking() + + def _init_auditory(self, configs): + soundfiles = configs.get_param('soundfiles').split(';') + self.audio_server = pyo.Server() + self.audio_server.boot() + self.audio_server.start() + sounds = [pyo.SfPlayer(f) for f in soundfiles] + assert len(soundfiles) == len(self._active_ids) + assert len(sounds) == len(self._active_ids) + self._sounds = dict(zip(self._active_ids, sounds)) + + def _init_haptic(self, configs): + ids = configs.get_param("haptic_device").split(":") + vid, pid = [int(i, base=16) for i in ids] + self.stimulator = HapticStimulator(vid, pid) + channel_map = (int(i) for i in configs.get_param('haptic_channels_map').split(';')) + self._haptic_map = dict(zip(self._active_ids, channel_map)) + self._haptic_duration = float(configs.get_param('haptic_duration')) + + + def set_configs(self, configs): + for m in self.mgrs: + m.set_configs(configs) + self._active_ids = [int(i) for i in configs.get_param('active_field_ids').split(';')] + self._blink_duration = float(configs.get_param('blink_duration')) + if self.auditory: + self._init_auditory(configs) + if self.haptic: + self._init_haptic(configs) + self._run_on_start = int(configs.get_param('running_on_start')) + + + def _blink(self): + '''Do blinking of configured modality''' + start_time = time.time() + curr_blink_global_id = self._active_ids[self._curr_blink_id] + if self.visual: + self.update_from(self._curr_blink_ugm) + update_time = time.time() + if self.auditory: + self._sounds[curr_blink_global_id].out() + if self.haptic: + self.stimulator.stimulate( + self._haptic_map[curr_blink_global_id], + self._haptic_duration) + + if self._blinks_count >= 0: + self._blinks_count -= 1 + self.connection.send_blink(self._curr_blink_id, update_time) + t = 1000*(self._blink_duration - (time.time() - start_time)) + if t < 0: + t = 0.0 + self.context['logger'].warning("BLINKER WARNING: blink duration to short for that computer ...") + self._unblink_timer.start(t) + + + def _unblink(self): + start_time = time.time() + if self.visual: + self.update_from(self._curr_unblink_ugm) + update_time = time.time() + if self._blinks_count == 0 or self.STOP: + self.STOP = False + curr_time = self.time_mgr.get_time() + for m in self.mgrs: + m.reset() + self._stop_timer.start(1000*(curr_time - (time.time()-start_time))) + else: + self._schedule_blink(start_time) + + +if __name__ == '__main__': + ugm_engine.run() + diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini new file mode 100644 index 00000000..dee8af87 --- /dev/null +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini @@ -0,0 +1,54 @@ +[local_params] +; engin params ************************* +internal_ip=127.0.0.1 +internal_port= +use_tagger=0 +ugm_config=speller_config_8 + + +; blinking engine params *************** +;common to all modalities +modalities=visual;auditory;haptic +;length of visual stimulation, also time of trial epoch before break kicks in +blink_duration=0.1 +running_on_start=1 + +;auditory +;should be in order of active_field_ids +soundfiles=~/dataset/sounds/1.wav;~/dataset/sounds/2.wav;~/dataset/sounds/3.wav + +;haptic +haptic_duration=0.1 +haptic_device=0403:6010 +;in order of active_field_ids +haptic_channels_map=1;2;3 + + +; time manager +blink_min_break=0.1 +blink_max_break=0.1 + +; count manager +blink_count_type=inf +; inf, random, random_sequential, sequential +blink_count_min=10 +blink_count_max=15 + +; id manager +blink_id_type=random_sequential +; sequential, random, random_sequential +blink_id_count=8 + +; ugm manager +blink_ugm_id_start=101 +blink_ugm_id_count=8 +blink_id_count=8 +blink_ugm_key=color +blink_ugm_value=#00cb21 +blink_ugm_type=single +active_field_ids = 0;1;2;3;4;5;6;7 +; single, classic + +; classic... +blink_ugm_row_count=2 +blink_ugm_col_count=4 diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.py new file mode 100644 index 00000000..2419253d --- /dev/null +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import thread, os + +from multiplexer.multiplexer_constants import peers, types +from obci.control.peer.configured_client import ConfiguredClient + +from obci.configs import settings, variables_pb2 +from obci.utils import context as ctx +from obci.gui.ugm import ugm_internal_server +from obci.gui.ugm import ugm_config_manager +from obci.gui.ugm.blinking import ugm_modal_blinking_engine +from obci.gui.ugm.blinking import ugm_blinking_connection +from obci.utils.openbci_logging import log_crash + +class DummyClient(object): + def __init__(self, params): + self.params = params + def get_param(self, key): + return self.params[key] + +class UgmModalBlinkingEnginePeer(ConfiguredClient): + @log_crash + def __init__(self, addresses): + super(UgmModalBlinkingEnginePeer, self).__init__(addresses=addresses, type=peers.UGM_ENGINE_PEER) + context = ctx.get_new_context() + context['logger'] = self.logger + connection = ugm_blinking_connection.UgmBlinkingConnection(settings.MULTIPLEXER_ADDRESSES, + context) + + _modalities = self.config.get_param('modalities').split(';') + ENG = ugm_modal_blinking_engine.UgmModalBlinkingEngine( + ugm_config_manager.UgmConfigManager(self.config.get_param('ugm_config')), + connection, + context, + _modalities) + ENG.set_configs(DummyClient(self.config.param_values())) + srv = ugm_internal_server.UdpServer( + ENG, + self.config.get_param('internal_ip'), + int(self.config.get_param('use_tagger')), + context + ) + self.set_param('internal_port', str(srv.socket.getsockname()[1])) + thread.start_new_thread( + srv.run, + () + ) + self.ready() + ENG.run() + +if __name__ == "__main__": + UgmModalBlinkingEnginePeer(settings.MULTIPLEXER_ADDRESSES) + #assume closing ugm should stop all other peers... + sys.exit(1) diff --git a/obci/scenarios/budzik/prototypes/modal_p300/config/p300_all_modalities.ini b/obci/scenarios/budzik/prototypes/modal_p300/config/p300_all_modalities.ini new file mode 100644 index 00000000..5b3bb339 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/modal_p300/config/p300_all_modalities.ini @@ -0,0 +1,53 @@ +[local_params] +ugm_config=brain2013_config_8_fields_tablet + +modalities=visual;auditory;haptic +; blinking engine params *************** +; visual +blink_duration=1 +running_on_start=1 + +;auditory +;should be in order of active_field_ids +soundfiles=~/dataset/sounds/1.wav;~/dataset/sounds/2.wav;~/dataset/sounds/3.wav + +;hapitc +haptic_duration=0.8 +haptic_device=0403:6010 +;in order of active_field_ids +haptic_channels_map=1;2;3 + + +; time manager +blink_min_break=0.08 +blink_max_break=0.12 + +; count manager +blink_count_type=inf +; inf, random, random_sequential, sequential +blink_count_min=64 +blink_count_max=80 + +; id manager +blink_id_type=random_sequential +; sequential, random, random_sequential +blink_id_count=3 + +; ugm manager +blink_ugm_id_start=1001 +blink_ugm_id_count=3 +blink_id_count=3 +active_field_ids=0;2;5 +blink_ugm_key=font_color +blink_ugm_value=#E42525 +;blink_ugm_value=#4c05ef + +blink_ugm_type=single +; single, classic + +; classic... +blink_ugm_row_count=2 +blink_ugm_col_count=4 + + + diff --git a/obci/scenarios/budzik/prototypes/modal_p300/p300_labyrinth_dummy.ini b/obci/scenarios/budzik/prototypes/modal_p300/p300_labyrinth_dummy.ini new file mode 100644 index 00000000..ba2d69a3 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/modal_p300/p300_labyrinth_dummy.ini @@ -0,0 +1,96 @@ +[peers] +scenario_dir= +;*********************************************** +[peers.mx] +path=multiplexer-install/bin/mxcontrol + +;*********************************************** +[peers.config_server] +path=control/peer/config_server.py + +;*********************************************** +[peers.amplifier] +path=drivers/eeg/amplifier_virtual.py +config=scenarios/budzik/prototypes/p300_configs/cap_brain2013_dummy.ini + +;*********************************************** +[peers.signal_saver] +path=acquisition/signal_saver_peer.py +config=scenarios/budzik/prototypes/p300_configs/p300_labyrinth_signal_saver_config.ini + +[peers.signal_saver.launch_dependencies] +amplifier=amplifier + +;*********************************************** +[peers.tag_saver] +path=acquisition/tag_saver_peer.py + +[peers.tag_saver.launch_dependencies] +signal_saver=signal_saver + +;*********************************************** +[peers.info_saver] +path=acquisition/info_saver_peer.py + +[peers.info_saver.launch_dependencies] +amplifier=amplifier +signal_saver=signal_saver + +;*********************************************** +[peers.ugm_engine] +path=gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config=scenarios/budzik/prototypes/modal_p300/config/p300_all_modalities.ini + +[peers.ugm_engine.config_sources] +logic=logic + +;*********************************************** +[peers.ugm_server] +path=gui/ugm/ugm_server_peer.py + +[peers.ugm_server.launch_dependencies] +ugm_engine=ugm_engine + +;*********************************************** +[peers.analysis] +path=interfaces/bci/p300_MD/p300_master_peer.py +config=scenarios/budzik/prototypes/p300_configs/p300_labyrinth_clasifier_config.ini + +[peers.analysis.config_sources] +logic=logic +amplifier=amplifier + +[peers.analysis.launch_dependencies] +logic=logic +amplifier=amplifier + +;************************************************* +[peers.logic] +path=logic/logic_maze_peer.py +config=scenarios/brain2013/configs/brain2013_logic_maze_peer.ini + +[peers.logic.launch_dependencies] +ugm=ugm_server +signal_saver=signal_saver + +;************************************ +[peers.feedback] +path=logic/feedback/logic_decision_feedback_peer.py + +[peers.feedback.launch_dependencies] +ugm_engine=ugm_engine +ugm_server=ugm_server +logic=logic +analysis=analysis + +;*********************************************** +[peers.switch_backup] +path=interfaces/switch/backup/switch_backup_peer.py +config=scenarios/budzik/prototypes/p300_configs/p300_labyrinth_switch_config.ini + +;*********************************************** +[peers.switch] +path=drivers/switch/switch_amplifier_peer.py + +[peers.switch.launch_dependencies] +ugm_engine=ugm_engine From 96c215a7e80168ca9a4477733182fdfd00ba467c Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Mon, 9 May 2016 21:00:40 +0200 Subject: [PATCH 07/28] Sound bugfixing --- .../gui/ugm/blinking/ugm_modal_blinking_engine.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py index 012d581f..2b6d0869 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py @@ -15,7 +15,7 @@ from obci.gui.ugm import ugm_engine from PyQt4 import QtCore import time - +import os.path class UgmModalBlinkingEngine(UgmBlinkingEngine): """A class representing ugm application. It is supposed to fire ugm, @@ -59,10 +59,17 @@ def __init__(self, p_config_manager, p_connection, def _init_auditory(self, configs): soundfiles = configs.get_param('soundfiles').split(';') - self.audio_server = pyo.Server() + self.context['logger'].info(str(soundfiles)) + self.audio_server = pyo.Server(audio='pa') self.audio_server.boot() + while not self.audio_server.getIsBooted(): + time.sleep(1) + self.context['logger'].info('audio server bootup'+str(self.audio_server.getIsBooted())) self.audio_server.start() - sounds = [pyo.SfPlayer(f) for f in soundfiles] + while not self.audio_server.getIsStarted(): + time.sleep(1) + self.context['logger'].info('soundfiles: {}'.format(soundfiles)) + sounds = [pyo.SfPlayer(os.path.expanduser(f)) for f in soundfiles] assert len(soundfiles) == len(self._active_ids) assert len(sounds) == len(self._active_ids) self._sounds = dict(zip(self._active_ids, sounds)) @@ -128,5 +135,5 @@ def _unblink(self): if __name__ == '__main__': - ugm_engine.run() + ugm_engine.run() From e25072c10e7cc47bf113531a7434c1110747f7ba Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Wed, 11 May 2016 13:49:35 +0200 Subject: [PATCH 08/28] review fixes --- obci/gui/ugm/blinking/ugm_modal_blinking_engine.py | 4 ++++ obci/interfaces/bci/p300_MD/helper_functions.py | 2 +- obci/interfaces/bci/p300_MD/p300_classm.py | 8 ++++++-- obci/interfaces/bci/p300_MD/tests/synthetic_generator.py | 6 +++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py index 2b6d0869..ae49e2d1 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py @@ -6,10 +6,12 @@ # requires pyo for sound from __future__ import print_function from ugm_blinking_engine import UgmBlinkingEngine +import sys try: import pyo except ImportError: print ('ERROR no sound library.\n\t\t Installl pyo!\n\t\tsudo apt-get install python-pyo') + sys.exit() from obci.devices.haptics.HapticsControl import HapticStimulator from obci.utils import context as ctx from obci.gui.ugm import ugm_engine @@ -65,9 +67,11 @@ def _init_auditory(self, configs): while not self.audio_server.getIsBooted(): time.sleep(1) self.context['logger'].info('audio server bootup'+str(self.audio_server.getIsBooted())) + self.audio_server.boot() self.audio_server.start() while not self.audio_server.getIsStarted(): time.sleep(1) + self.audio_server.start() self.context['logger'].info('soundfiles: {}'.format(soundfiles)) sounds = [pyo.SfPlayer(os.path.expanduser(f)) for f in soundfiles] assert len(soundfiles) == len(self._active_ids) diff --git a/obci/interfaces/bci/p300_MD/helper_functions.py b/obci/interfaces/bci/p300_MD/helper_functions.py index 987398aa..56765140 100644 --- a/obci/interfaces/bci/p300_MD/helper_functions.py +++ b/obci/interfaces/bci/p300_MD/helper_functions.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # based on obci.analysis.p300.analysis_offline # Marian Dovgialo -from scipy import * +from scipy import zeros, diag, dot from scipy import linalg import numpy as np import pylab as pb diff --git a/obci/interfaces/bci/p300_MD/p300_classm.py b/obci/interfaces/bci/p300_MD/p300_classm.py index 954b04ad..54be4f2f 100644 --- a/obci/interfaces/bci/p300_MD/p300_classm.py +++ b/obci/interfaces/bci/p300_MD/p300_classm.py @@ -131,13 +131,17 @@ def learn(self, chunk, target): else: self.learning_buffor_classes.append(0) self.learning_buffor_features.append(chunk) - if sum(self.learning_buffor_classes)%LEARN_EVERY == 0: + targets_count = sum(self.learning_buffor_classes) + nontargets_count = sum(i==0 for i in self.learning_buffor_classes) + enough = (targets_count>2 and nontargets_count>2) + if sum(self.learning_buffor_classes)%LEARN_EVERY == 0 and enough: print('Classifier: fitting clf with new data') self.clf.fit( self.learning_buffor_features, self.learning_buffor_classes, ) - score = self.clf.score(features, labels) + score = self.clf.score(self.learning_buffor_features, + self.learning_buffor_classes) print ('Classifier: Test on training set: {}'.format(score)) diff --git a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py index 06ce85ee..1683b69c 100644 --- a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py +++ b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py @@ -126,7 +126,7 @@ def send_isi(self): length = int(packets*self.samples_per_packet) if self.synthetic==1: signal=np.random.normal(scale=self.noise_level, - size=(length, len(self.channels_names))) + size=(len(self.channels_names), length)) else: selected_nontarget = random.randint(0, self.nontargets.shape[0]-1) #nontargets are cut to isi @@ -140,7 +140,7 @@ def send_isi(self): t0 = time.time() s = sv.samples.add() ind = p*self.samples_per_packet+sn - s.channels.extend(signal[:,ind].tolist()) + s.channels.extend(signal[:, ind].tolist()) s.timestamp = self.time self.time+=1.0/self.sampling_rate self.conn.send_message(message = sv.SerializeToString(), @@ -200,7 +200,7 @@ def run(self): time.sleep(self.delay) for i in xrange(self.test_trials_n): for k in xrange(len(self.fields)-1): - #handle_message can wait a while + self.send_isi() self.send_target() with self.lock: From 2a5b4a3936bfd975a8a8d0455530416074c24bd5 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Wed, 11 May 2016 17:21:34 +0200 Subject: [PATCH 09/28] Highlighting by images - done --- .../ugm/blinking/ugm_blinking_ugm_manager.py | 140 +++++++++++++++++- .../ugm_modal_blinking_engine_peer.ini | 27 +++- .../ugm/configs/labyrinth_face_highlight.ugm | Bin 0 -> 3063 bytes .../bci/p300_MD/helper_functions.py | 13 -- 4 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 obci/gui/ugm/configs/labyrinth_face_highlight.ugm diff --git a/obci/gui/ugm/blinking/ugm_blinking_ugm_manager.py b/obci/gui/ugm/blinking/ugm_blinking_ugm_manager.py index 107353af..cbfcb462 100644 --- a/obci/gui/ugm/blinking/ugm_blinking_ugm_manager.py +++ b/obci/gui/ugm/blinking/ugm_blinking_ugm_manager.py @@ -4,6 +4,7 @@ # Mateusz Kruszyński from obci.gui.ugm import ugm_config_manager +import os.path class _SingleUgmManager(object): def __init__(self, configs): mgr = ugm_config_manager.UgmConfigManager(configs.get_param('ugm_config')) @@ -111,9 +112,146 @@ def get_unblink_ugm(self, area_id): +class _SingleTextImageOddballUgmManager(object): + '''Ugm config manager for blinks - text with synchronous nontarget + blinks and additional target oddball - image. + + BCI design maximising efficiency based on studies: + Jin, Jing, et al. "A P300 Brain–Computer Interface Based on a + Modification of the Mismatch Negativity Paradigm." + International journal of neural systems 25.03 (2015): 1550011. + + Kaufmann, Tobias, et al. "Face stimuli effectively + prevent brain–computer interface inefficiency in patients with + neurodegenerative disease." + Clinical Neurophysiology 124.5 (2013): 893-900. + + ''' + def __init__(self, configs): + mgr = ugm_config_manager.UgmConfigManager(configs.get_param('ugm_config')) + start_id = int(configs.get_param('blink_ugm_id_start')) + count = int(configs.get_param('blink_ugm_id_count')) + dec_count = int(configs.get_param('blink_id_count')) + active_field_ids = [int(field) for field in configs.get_param('active_field_ids').split(';')] + target_image_path = configs.get_param('target_image_path') + image_fields_id_offset = int(configs.get_param('image_fields_id_offset')) + + assert(start_id >= 0) + assert(count >= 0) + assert(count == dec_count) + + self.blink_ugm = [] + self.unblink_ugm = [] + #create blinks and unblinks for every field + for dec in active_field_ids:#range(count): + new_blink_cfgs = [] + new_unblink_cfgs = [] + cfg_target = mgr.get_config_for(start_id+dec+image_fields_id_offset) + new_blink_cfgs.append({'id':cfg_target['id'], + 'image_path':target_image_path + }) + new_unblink_cfgs.append({'id':cfg_target['id'], + 'image_path':'' + }) + #highlight every nontarget field: + + for ndec in active_field_ids: + cfg = mgr.get_config_for(start_id+ndec) + new_blink_cfgs.append({'id':cfg['id'], + configs.get_param('blink_ugm_key'):configs.get_param('blink_ugm_value') + }) + new_unblink_cfgs.append({'id':cfg['id'], + configs.get_param('blink_ugm_key'):cfg[configs.get_param('blink_ugm_key')] + }) + self.blink_ugm.append(new_blink_cfgs) + self.unblink_ugm.append(new_unblink_cfgs) + + def get_blink_ugm(self, area_id): + return self.blink_ugm[area_id] + + def get_unblink_ugm(self, area_id): + return self.unblink_ugm[area_id] + + +class _SingleImageOddballUgmManager(object): + '''Ugm config manager for blinks - images with synchronous nontarget + blinks and additional target oddball - image - another image. + + BCI design maximising efficiency based on studies: + Jin, Jing, et al. "A P300 Brain–Computer Interface Based on a + Modification of the Mismatch Negativity Paradigm." + International journal of neural systems 25.03 (2015): 1550011. + + Kaufmann, Tobias, et al. "Face stimuli effectively + prevent brain–computer interface inefficiency in patients with + neurodegenerative disease." + Clinical Neurophysiology 124.5 (2013): 893-900. + + ''' + def __init__(self, configs): + mgr = ugm_config_manager.UgmConfigManager(configs.get_param('ugm_config')) + start_id = int(configs.get_param('blink_ugm_id_start')) + count = int(configs.get_param('blink_ugm_id_count')) + dec_count = int(configs.get_param('blink_id_count')) + active_field_ids = [int(field) for field in configs.get_param('active_field_ids').split(';')] + images_folder = configs.get_param('images_path') + images_extension = configs.get_param('images_extension') + + assert(start_id >= 0) + assert(count >= 0) + assert(count == dec_count) + + self.blink_ugm = [] + self.unblink_ugm = [] + #create blinks and unblinks for every field + for dec in active_field_ids: + new_blink_cfgs = [] + new_unblink_cfgs = [] + + cfg_target = mgr.get_config_for(start_id+dec) + targ_image = os.path.join(images_folder, + '{}t.{}'.format(dec, images_extension)) + idle_image = os.path.join(images_folder, + '{}i.{}'.format(dec, images_extension)) + new_blink_cfgs.append({'id':cfg_target['id'], + 'image_path':targ_image + }) + new_unblink_cfgs.append({'id':cfg_target['id'], + 'image_path':idle_image + }) + #highlight every nontarget field: + + for ndec in active_field_ids: + #change only nontarget, here this is important + if ndec!=dec: + + high_image = os.path.join(images_folder, + '{}n.{}'.format(ndec, images_extension)) + idle_image = os.path.join(images_folder, + '{}i.{}'.format(ndec, images_extension)) + cfg = mgr.get_config_for(start_id+ndec) + new_blink_cfgs.append({'id':cfg['id'], + 'image_path':high_image + }) + new_unblink_cfgs.append({'id':cfg['id'], + 'image_path':idle_image + }) + + self.blink_ugm.append(new_blink_cfgs) + self.unblink_ugm.append(new_unblink_cfgs) + + def get_blink_ugm(self, area_id): + return self.blink_ugm[area_id] + + def get_unblink_ugm(self, area_id): + return self.unblink_ugm[area_id] + + MGRS = { 'single':_SingleUgmManager, - 'classic':_ClassicUgmManager + 'classic':_ClassicUgmManager, + 'singletextimageoddball':_SingleTextImageOddballUgmManager, + 'singleimageoddball':_SingleImageOddballUgmManager, } class UgmBlinkingUgmManager(object): diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini index dee8af87..c30434f7 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini @@ -45,10 +45,33 @@ blink_ugm_id_count=8 blink_id_count=8 blink_ugm_key=color blink_ugm_value=#00cb21 -blink_ugm_type=single +blink_ugm_type=single +; active_field_ids = 0;1;2;3;4;5;6;7 -; single, classic +; single, classic, singletextimageoddball, singleimageoddball +;single - simple blinks +;classig - blinks in groups (rows, columns) +;singletextimageoddball - flash every text field, but targets are +;flashed by image. Behaves like single +;singleimageoddball - the same as above, but uses images instead of text ; classic... blink_ugm_row_count=2 blink_ugm_col_count=4 + +;for highlighting by image (singletextimageoddball): +target_image_path=obci.gui.ugm.resources.bci.png +;text fields are overlaid with image fields ids of image fields +; are offsetted by some integer +; id_image = id_text + offset +image_fields_id_offset=1000 + +;for images and highlighting by image (singleimageoddball): +;images must be named same as active_field_ids adding state and extension: +; there are 3 states - idle i, nontarget n and target t +; ex. for image in field 0 there would be required images: +;0i.png 0t.png 0n.png +; in the "images_path" folder +;no need for offset, but blink_ugm_id_start should point to image field +images_path= +images_extension= diff --git a/obci/gui/ugm/configs/labyrinth_face_highlight.ugm b/obci/gui/ugm/configs/labyrinth_face_highlight.ugm new file mode 100644 index 0000000000000000000000000000000000000000..0491a8c22d007f2d55f72484af77892d6d7258a9 GIT binary patch literal 3063 zcma);X>?On5XVCn(xyeM1q7u9Tg2cJX;BJlt$UGJrRdeF)cBg-WL|k$=DnBF2BWCp zuDI_zxGV08yMp_2jyS7`QJ&_dHc~*z)4{i zTB8rz5VJ?K9KEc@Bj@Y1)LfNFZh7k1JPHa_~3zTN;$NYrh(O(JZ@|0q`~nZ2to}|Lpv4~ zA8I;aOq-AJjYg;{NafMS-O=Quq|$0}Sipn>^qK4Rm+tgaZ8&{DcFiZfZj;-;R! zSs=B3h{$;;3l$?vD$ZseY+}I_w=6BmvAIuZgL7CzMT%i=+IpPioLeb-81w3b*wWMX z1Do-$xDsirBiOuL0S~7M>nW-b%W5{@{JwcqCNmJ%Bgvc|$t6<}lUtdb8Ru4nK8;x^ z%`hU_bP|t%V6>Sq1I6PQCY~B?z{(goo8tljbR;;P9R)2tzMny=el%4-CJgiY!v9^- z)@dZVB~^`($N^LM4v%$;FO6Sdo=+KZrN`H+8-N3MQcj#Grs&Sdd;YHid9tx!)L zEhfK6F7E)p^lpjlp`}z_GR+AP?Cx5h-`%}s+FIeA@5Q`49^svD!~3^wT(v+5RJlGU ziY8AY7j=eVdUTd#os%op>4J(|PLTmm6=zR_+2i-DBY~$wGt7~Q&mg4V3CWrDnZ$M$ zsZC*yOS>Hfm&par76q0|`J$9>EGXwYN9fnxwKLS2-Lx|VEm2P2I(R))?p#&M^Assp z$Wr#IQmzzIj**c0-ODB*<@uu93rI@lmhwVq01^=t^)3?OeX@ENlX^`O?IlF}hoau4 zgtJP;>1TB*Eze`LY|PjQUKaJzlIfSJVq%aY1}~R1*W`1fI1u#<#PQXqY#L{HanbT1 z%ms}#IzV%cn36NRZ+uJ)jI#4I8$KB&gDB~L< z-qk9vp^PsjgQiL5;>>oD6*{w&Aj;CrXm6=Ie65+Nyvz%*Y!!)x!%FNaf z!-#NvlcfGAWml}87n#>e!@q(O?NF>qE_R-h}V?0`mJ}* zhUTp+g;rzYRqtLg{O`nV>O^H#Tq{~!U%(A+sjjOg)%7C!23e{bT~ggdgnJdKZq8}q zGXF?hD!hL?U1m@SZy~~4Rl?g8!i_TF?PWq~8U7F99irzu3wW2C@a}4a_lW3wWy1Sh zg!dESp9 zmJy$FAwErre<_I15W}->zduLW6~8|(GQS}C{Y6Ujlj8T6N`C*9)-UZ~a>J0K)fTGr zvZ~c9idL`6Zg{P%)vgKq{dLje8wGsRE!A7qqtE`5PmI2 ST>AI(4V@jKv-Iyri~Iv=SsSbX literal 0 HcmV?d00001 diff --git a/obci/interfaces/bci/p300_MD/helper_functions.py b/obci/interfaces/bci/p300_MD/helper_functions.py index 56765140..ff78bbd9 100644 --- a/obci/interfaces/bci/p300_MD/helper_functions.py +++ b/obci/interfaces/bci/p300_MD/helper_functions.py @@ -317,19 +317,6 @@ def downsample(mgr, factor): samples_source = read_data_source.MemoryDataSource(new_samples) return read_manager.ReadManager(info_source, samples_source, tags_source) - - - - - - - - - - - - - def montage(mgr, montage_type, **montage_params): if montage_type == 'common_spatial_average': return montage_csa(mgr, **montage_params) From 6427150fb0d877be41c0f9b5fbc61dc551d8da24 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Wed, 11 May 2016 17:31:27 +0200 Subject: [PATCH 10/28] adding scenarious --- obci/control/gui/presets/budzik.ini | 13 +++ .../visual_audio_image_highlight.ini | 110 ++++++++++++++++++ .../amplifier.ini | 17 +++ .../analysis.ini | 19 +++ .../config_server.ini | 14 +++ .../feedback.ini | 14 +++ .../info_saver.ini | 14 +++ .../logic.ini | 19 +++ .../mx.ini | 14 +++ .../signal_saver.ini | 15 +++ .../switch.ini | 14 +++ .../switch_backup.ini | 15 +++ .../tag_saver.ini | 14 +++ .../ugm_engine.ini | 30 +++++ .../ugm_server.ini | 14 +++ .../visual_audio_no_text_image_highlight.ini | 110 ++++++++++++++++++ .../amplifier.ini | 17 +++ .../analysis.ini | 19 +++ .../config_server.ini | 14 +++ .../feedback.ini | 14 +++ .../info_saver.ini | 14 +++ .../logic.ini | 19 +++ .../mx.ini | 14 +++ .../signal_saver.ini | 15 +++ .../switch.ini | 14 +++ .../switch_backup.ini | 15 +++ .../tag_saver.ini | 14 +++ .../ugm_engine.ini | 32 +++++ .../ugm_server.ini | 14 +++ .../visual_audio_image_highlight.ini | 110 ++++++++++++++++++ .../amplifier.ini | 17 +++ .../analysis.ini | 19 +++ .../config_server.ini | 14 +++ .../feedback.ini | 14 +++ .../info_saver.ini | 14 +++ .../logic.ini | 19 +++ .../mx.ini | 14 +++ .../signal_saver.ini | 15 +++ .../switch.ini | 14 +++ .../switch_backup.ini | 15 +++ .../tag_saver.ini | 14 +++ .../ugm_engine.ini | 30 +++++ .../ugm_server.ini | 14 +++ .../visual_audio_no_text_image_highlight.ini | 110 ++++++++++++++++++ .../amplifier.ini | 17 +++ .../analysis.ini | 19 +++ .../config_server.ini | 14 +++ .../feedback.ini | 14 +++ .../info_saver.ini | 14 +++ .../logic.ini | 19 +++ .../mx.ini | 14 +++ .../signal_saver.ini | 15 +++ .../switch.ini | 14 +++ .../switch_backup.ini | 15 +++ .../tag_saver.ini | 14 +++ .../ugm_engine.ini | 32 +++++ .../ugm_server.ini | 14 +++ 57 files changed, 1309 insertions(+) create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/amplifier.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/analysis.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/config_server.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/feedback.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/info_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/logic.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/mx.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/signal_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/switch.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/switch_backup.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/tag_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/ugm_engine.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/ugm_server.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/amplifier.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/analysis.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/config_server.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/feedback.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/info_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/logic.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/mx.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/signal_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/switch.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/switch_backup.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/tag_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/ugm_engine.ini create mode 100644 obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/ugm_server.ini create mode 100644 obci/scenarios/visual_audio_image_highlight.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/amplifier.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/analysis.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/config_server.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/feedback.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/info_saver.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/logic.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/mx.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/signal_saver.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/switch.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/switch_backup.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/tag_saver.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/ugm_engine.ini create mode 100644 obci/scenarios/visual_audio_image_highlight_configs/ugm_server.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/amplifier.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/analysis.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/config_server.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/feedback.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/info_saver.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/logic.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/mx.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/signal_saver.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/switch.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/switch_backup.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/tag_saver.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_engine.ini create mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_server.ini diff --git a/obci/control/gui/presets/budzik.ini b/obci/control/gui/presets/budzik.ini index 665645b1..64379dde 100644 --- a/obci/control/gui/presets/budzik.ini +++ b/obci/control/gui/presets/budzik.ini @@ -56,3 +56,16 @@ info=run interactive labirynth P300 on dummy amp with 3 modalities launch_file=scenarios/budzik/prototypes/modal_p300/p300_labyrinth_dummy.ini public_params= category=Prototypes P300 + + +[P300 online labirynth visual audio text highlight by image] +info=run interactive labirynth P300 on dummy amp with 2 modalities +launch_file=scenarios/budzik/prototypes/visual_audio_image_highlight.ini +public_params= +category=Prototypes P300 + +[P300 online labirynth visual audio images no text] +info=run interactive labirynth P300 on dummy amp with 2 modalities +launch_file=scenarios/budzik/prototypes/visual_audio_no_text_image_highlight.ini +public_params= +category=Prototypes P300 diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight.ini new file mode 100644 index 00000000..5010aca4 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight.ini @@ -0,0 +1,110 @@ +[peers] +scenario_dir = + +[peers.switch_backup] +path = interfaces/switch/backup/switch_backup_peer.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/switch_backup.ini + +[peers.feedback] +path = logic/feedback/logic_decision_feedback_peer.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/feedback.ini + +[peers.feedback.config_sources] +ugm_engine = ugm_engine +analysis = analysis +logic = logic + +[peers.feedback.launch_dependencies] +ugm_server = ugm_server +ugm_engine = ugm_engine +analysis = analysis +logic = logic + +[peers.tag_saver] +path = acquisition/tag_saver_peer.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/tag_saver.ini + +[peers.tag_saver.config_sources] +signal_saver = signal_saver + +[peers.tag_saver.launch_dependencies] +signal_saver = signal_saver + +[peers.amplifier] +path = drivers/eeg/amplifier_virtual.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/amplifier.ini + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/config_server.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/analysis.ini + +[peers.analysis.config_sources] +amplifier = amplifier +logic = logic + +[peers.analysis.launch_dependencies] +amplifier = amplifier +logic = logic + +[peers.switch] +path = drivers/switch/switch_amplifier_peer.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/switch.ini + +[peers.switch.launch_dependencies] +ugm_engine = ugm_engine + +[peers.ugm_server] +path = gui/ugm/ugm_server_peer.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/ugm_server.ini + +[peers.ugm_server.config_sources] +ugm_engine = ugm_engine + +[peers.ugm_server.launch_dependencies] +ugm_engine = ugm_engine + +[peers.info_saver] +path = acquisition/info_saver_peer.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/info_saver.ini + +[peers.info_saver.config_sources] +signal_saver = signal_saver +amplifier = amplifier + +[peers.info_saver.launch_dependencies] +signal_saver = signal_saver +amplifier = amplifier + +[peers.logic] +path = logic/logic_maze_peer.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/logic.ini + +[peers.logic.launch_dependencies] +signal_saver = signal_saver +ugm = ugm_server + +[peers.signal_saver] +path = acquisition/signal_saver_peer.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/signal_saver.ini + +[peers.signal_saver.config_sources] +amplifier = amplifier + +[peers.signal_saver.launch_dependencies] +amplifier = amplifier + +[peers.ugm_engine] +path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/ugm_engine.ini + +[peers.ugm_engine.config_sources] +logic = logic + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/mx.ini + diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/amplifier.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/amplifier.ini new file mode 100644 index 00000000..2fa97c86 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/amplifier.ini @@ -0,0 +1,17 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +active_channels = 0;1;2;3;4;5;6;7;8;Saw;Driver_Saw +channel_names = PO7;O1;Oz;O2;PO8;PO3;PO4;Pz;Cz;AmpSaw;DriverSaw +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sampling_rate = 512 +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/analysis.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/analysis.ini new file mode 100644 index 00000000..7c2e5286 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/analysis.ini @@ -0,0 +1,19 @@ + +[config_sources] +logic = + +[launch_dependencies] +logic = + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +file_log_level = debug +sentry_log_level = error +offline_learning = 0 +mx_log_level = info +channels_for_classification = O1;O2;Pz;Cz +log_dir = ~/.obci/logs +downsample_to = 24 diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/config_server.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/config_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/config_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/feedback.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/feedback.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/feedback.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/info_saver.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/info_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/info_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/logic.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/logic.ini new file mode 100644 index 00000000..02bf4bc9 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/logic.ini @@ -0,0 +1,19 @@ + +[config_sources] + +[launch_dependencies] +signal_saver = + +[external_params] + +[local_params] +experiment_uuid = +active_field_ids = 0;2;5 +dec_count = 3 +logic_decision_config = obci.logic.configs.config_maze_rovio.Config +robot_ip = 192.168.1.18 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/mx.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/mx.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/mx.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/signal_saver.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/signal_saver.ini new file mode 100644 index 00000000..cd1348d6 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/signal_saver.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +save_file_name = test2 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/switch.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/switch.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/switch.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/switch_backup.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/switch_backup.ini new file mode 100644 index 00000000..81e49483 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/switch_backup.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +sentry_log_level = error +mx_log_level = info +file_log_level = debug +finish_saving = 1 diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/tag_saver.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/tag_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/tag_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/ugm_engine.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/ugm_engine.ini new file mode 100644 index 00000000..b30579e3 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/ugm_engine.ini @@ -0,0 +1,30 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +ugm_config = labyrinth_face_highlight +blink_ugm_id_start = 1001 +experiment_uuid = +blink_ugm_key = font_color +blink_count_min = 64 +blink_max_break = 1.12 +modalities = visual;auditory +blink_min_break = 1.08 +blink_count_max = 80 +blink_ugm_value = #E42525 +console_log_level = info +blink_id_count = 3 +blink_duration = 0.3 +blink_ugm_type = singletextimageoddball +sentry_log_level = error +mx_log_level = info +file_log_level = debug +active_field_ids = 0;2;5 +haptic_duration = 0.8 +blink_ugm_id_count = 3 +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/ugm_server.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/ugm_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight_configs/ugm_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight.ini new file mode 100644 index 00000000..e307b0ed --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight.ini @@ -0,0 +1,110 @@ +[peers] +scenario_dir = + +[peers.switch_backup] +path = interfaces/switch/backup/switch_backup_peer.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/switch_backup.ini + +[peers.feedback] +path = logic/feedback/logic_decision_feedback_peer.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/feedback.ini + +[peers.feedback.config_sources] +ugm_engine = ugm_engine +analysis = analysis +logic = logic + +[peers.feedback.launch_dependencies] +ugm_server = ugm_server +ugm_engine = ugm_engine +analysis = analysis +logic = logic + +[peers.tag_saver] +path = acquisition/tag_saver_peer.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/tag_saver.ini + +[peers.tag_saver.config_sources] +signal_saver = signal_saver + +[peers.tag_saver.launch_dependencies] +signal_saver = signal_saver + +[peers.amplifier] +path = drivers/eeg/amplifier_virtual.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/amplifier.ini + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/config_server.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/analysis.ini + +[peers.analysis.config_sources] +amplifier = amplifier +logic = logic + +[peers.analysis.launch_dependencies] +amplifier = amplifier +logic = logic + +[peers.switch] +path = drivers/switch/switch_amplifier_peer.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/switch.ini + +[peers.switch.launch_dependencies] +ugm_engine = ugm_engine + +[peers.ugm_server] +path = gui/ugm/ugm_server_peer.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/ugm_server.ini + +[peers.ugm_server.config_sources] +ugm_engine = ugm_engine + +[peers.ugm_server.launch_dependencies] +ugm_engine = ugm_engine + +[peers.info_saver] +path = acquisition/info_saver_peer.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/info_saver.ini + +[peers.info_saver.config_sources] +signal_saver = signal_saver +amplifier = amplifier + +[peers.info_saver.launch_dependencies] +signal_saver = signal_saver +amplifier = amplifier + +[peers.logic] +path = logic/logic_maze_peer.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/logic.ini + +[peers.logic.launch_dependencies] +signal_saver = signal_saver +ugm = ugm_server + +[peers.signal_saver] +path = acquisition/signal_saver_peer.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/signal_saver.ini + +[peers.signal_saver.config_sources] +amplifier = amplifier + +[peers.signal_saver.launch_dependencies] +amplifier = amplifier + +[peers.ugm_engine] +path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/ugm_engine.ini + +[peers.ugm_engine.config_sources] +logic = logic + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/mx.ini + diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/amplifier.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/amplifier.ini new file mode 100644 index 00000000..2fa97c86 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/amplifier.ini @@ -0,0 +1,17 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +active_channels = 0;1;2;3;4;5;6;7;8;Saw;Driver_Saw +channel_names = PO7;O1;Oz;O2;PO8;PO3;PO4;Pz;Cz;AmpSaw;DriverSaw +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sampling_rate = 512 +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/analysis.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/analysis.ini new file mode 100644 index 00000000..7c2e5286 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/analysis.ini @@ -0,0 +1,19 @@ + +[config_sources] +logic = + +[launch_dependencies] +logic = + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +file_log_level = debug +sentry_log_level = error +offline_learning = 0 +mx_log_level = info +channels_for_classification = O1;O2;Pz;Cz +log_dir = ~/.obci/logs +downsample_to = 24 diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/config_server.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/config_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/config_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/feedback.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/feedback.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/feedback.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/info_saver.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/info_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/info_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/logic.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/logic.ini new file mode 100644 index 00000000..02bf4bc9 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/logic.ini @@ -0,0 +1,19 @@ + +[config_sources] + +[launch_dependencies] +signal_saver = + +[external_params] + +[local_params] +experiment_uuid = +active_field_ids = 0;2;5 +dec_count = 3 +logic_decision_config = obci.logic.configs.config_maze_rovio.Config +robot_ip = 192.168.1.18 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/mx.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/mx.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/mx.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/signal_saver.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/signal_saver.ini new file mode 100644 index 00000000..cd1348d6 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/signal_saver.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +save_file_name = test2 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/switch.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/switch.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/switch.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/switch_backup.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/switch_backup.ini new file mode 100644 index 00000000..81e49483 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/switch_backup.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +sentry_log_level = error +mx_log_level = info +file_log_level = debug +finish_saving = 1 diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/tag_saver.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/tag_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/tag_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/ugm_engine.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/ugm_engine.ini new file mode 100644 index 00000000..09ad4e32 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/ugm_engine.ini @@ -0,0 +1,32 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +ugm_config = labyrinth_face_highlight +blink_ugm_id_start = 2001 +images_path = ~/dataset/pictures +modalities = visual;auditory +images_extension = png +blink_ugm_key = font_color +haptic_duration = 0.8 +sentry_log_level = error +blink_ugm_value = #E42525 +log_dir = ~/.obci/logs +blink_duration = 0.3 +blink_count_min = 64 +blink_count_max = 80 +mx_log_level = info +console_log_level = info +file_log_level = debug +experiment_uuid = +active_field_ids = 0;2;5 +blink_max_break = 1.12 +blink_min_break = 1.08 +blink_id_count = 3 +blink_ugm_id_count = 3 +blink_ugm_type = singleimageoddball diff --git a/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/ugm_server.ini b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/ugm_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/visual_audio_no_text_image_highlight_configs/ugm_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_image_highlight.ini b/obci/scenarios/visual_audio_image_highlight.ini new file mode 100644 index 00000000..649aaced --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight.ini @@ -0,0 +1,110 @@ +[peers] +scenario_dir = + +[peers.switch_backup] +path = interfaces/switch/backup/switch_backup_peer.py +config = scenarios/visual_audio_image_highlight_configs/switch_backup.ini + +[peers.feedback] +path = logic/feedback/logic_decision_feedback_peer.py +config = scenarios/visual_audio_image_highlight_configs/feedback.ini + +[peers.feedback.config_sources] +ugm_engine = ugm_engine +analysis = analysis +logic = logic + +[peers.feedback.launch_dependencies] +ugm_server = ugm_server +ugm_engine = ugm_engine +analysis = analysis +logic = logic + +[peers.tag_saver] +path = acquisition/tag_saver_peer.py +config = scenarios/visual_audio_image_highlight_configs/tag_saver.ini + +[peers.tag_saver.config_sources] +signal_saver = signal_saver + +[peers.tag_saver.launch_dependencies] +signal_saver = signal_saver + +[peers.amplifier] +path = drivers/eeg/amplifier_virtual.py +config = scenarios/visual_audio_image_highlight_configs/amplifier.ini + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/visual_audio_image_highlight_configs/config_server.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/visual_audio_image_highlight_configs/analysis.ini + +[peers.analysis.config_sources] +amplifier = amplifier +logic = logic + +[peers.analysis.launch_dependencies] +amplifier = amplifier +logic = logic + +[peers.switch] +path = drivers/switch/switch_amplifier_peer.py +config = scenarios/visual_audio_image_highlight_configs/switch.ini + +[peers.switch.launch_dependencies] +ugm_engine = ugm_engine + +[peers.ugm_server] +path = gui/ugm/ugm_server_peer.py +config = scenarios/visual_audio_image_highlight_configs/ugm_server.ini + +[peers.ugm_server.config_sources] +ugm_engine = ugm_engine + +[peers.ugm_server.launch_dependencies] +ugm_engine = ugm_engine + +[peers.info_saver] +path = acquisition/info_saver_peer.py +config = scenarios/visual_audio_image_highlight_configs/info_saver.ini + +[peers.info_saver.config_sources] +signal_saver = signal_saver +amplifier = amplifier + +[peers.info_saver.launch_dependencies] +signal_saver = signal_saver +amplifier = amplifier + +[peers.logic] +path = logic/logic_maze_peer.py +config = scenarios/visual_audio_image_highlight_configs/logic.ini + +[peers.logic.launch_dependencies] +signal_saver = signal_saver +ugm = ugm_server + +[peers.signal_saver] +path = acquisition/signal_saver_peer.py +config = scenarios/visual_audio_image_highlight_configs/signal_saver.ini + +[peers.signal_saver.config_sources] +amplifier = amplifier + +[peers.signal_saver.launch_dependencies] +amplifier = amplifier + +[peers.ugm_engine] +path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config = scenarios/visual_audio_image_highlight_configs/ugm_engine.ini + +[peers.ugm_engine.config_sources] +logic = logic + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/visual_audio_image_highlight_configs/mx.ini + diff --git a/obci/scenarios/visual_audio_image_highlight_configs/amplifier.ini b/obci/scenarios/visual_audio_image_highlight_configs/amplifier.ini new file mode 100644 index 00000000..2fa97c86 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/amplifier.ini @@ -0,0 +1,17 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +active_channels = 0;1;2;3;4;5;6;7;8;Saw;Driver_Saw +channel_names = PO7;O1;Oz;O2;PO8;PO3;PO4;Pz;Cz;AmpSaw;DriverSaw +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sampling_rate = 512 +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_image_highlight_configs/analysis.ini b/obci/scenarios/visual_audio_image_highlight_configs/analysis.ini new file mode 100644 index 00000000..7c2e5286 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/analysis.ini @@ -0,0 +1,19 @@ + +[config_sources] +logic = + +[launch_dependencies] +logic = + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +file_log_level = debug +sentry_log_level = error +offline_learning = 0 +mx_log_level = info +channels_for_classification = O1;O2;Pz;Cz +log_dir = ~/.obci/logs +downsample_to = 24 diff --git a/obci/scenarios/visual_audio_image_highlight_configs/config_server.ini b/obci/scenarios/visual_audio_image_highlight_configs/config_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/config_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_image_highlight_configs/feedback.ini b/obci/scenarios/visual_audio_image_highlight_configs/feedback.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/feedback.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_image_highlight_configs/info_saver.ini b/obci/scenarios/visual_audio_image_highlight_configs/info_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/info_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_image_highlight_configs/logic.ini b/obci/scenarios/visual_audio_image_highlight_configs/logic.ini new file mode 100644 index 00000000..02bf4bc9 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/logic.ini @@ -0,0 +1,19 @@ + +[config_sources] + +[launch_dependencies] +signal_saver = + +[external_params] + +[local_params] +experiment_uuid = +active_field_ids = 0;2;5 +dec_count = 3 +logic_decision_config = obci.logic.configs.config_maze_rovio.Config +robot_ip = 192.168.1.18 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/visual_audio_image_highlight_configs/mx.ini b/obci/scenarios/visual_audio_image_highlight_configs/mx.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/mx.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_image_highlight_configs/signal_saver.ini b/obci/scenarios/visual_audio_image_highlight_configs/signal_saver.ini new file mode 100644 index 00000000..cd1348d6 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/signal_saver.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +save_file_name = test2 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/visual_audio_image_highlight_configs/switch.ini b/obci/scenarios/visual_audio_image_highlight_configs/switch.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/switch.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_image_highlight_configs/switch_backup.ini b/obci/scenarios/visual_audio_image_highlight_configs/switch_backup.ini new file mode 100644 index 00000000..81e49483 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/switch_backup.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +sentry_log_level = error +mx_log_level = info +file_log_level = debug +finish_saving = 1 diff --git a/obci/scenarios/visual_audio_image_highlight_configs/tag_saver.ini b/obci/scenarios/visual_audio_image_highlight_configs/tag_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/tag_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_image_highlight_configs/ugm_engine.ini b/obci/scenarios/visual_audio_image_highlight_configs/ugm_engine.ini new file mode 100644 index 00000000..b30579e3 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/ugm_engine.ini @@ -0,0 +1,30 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +ugm_config = labyrinth_face_highlight +blink_ugm_id_start = 1001 +experiment_uuid = +blink_ugm_key = font_color +blink_count_min = 64 +blink_max_break = 1.12 +modalities = visual;auditory +blink_min_break = 1.08 +blink_count_max = 80 +blink_ugm_value = #E42525 +console_log_level = info +blink_id_count = 3 +blink_duration = 0.3 +blink_ugm_type = singletextimageoddball +sentry_log_level = error +mx_log_level = info +file_log_level = debug +active_field_ids = 0;2;5 +haptic_duration = 0.8 +blink_ugm_id_count = 3 +log_dir = ~/.obci/logs diff --git a/obci/scenarios/visual_audio_image_highlight_configs/ugm_server.ini b/obci/scenarios/visual_audio_image_highlight_configs/ugm_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_image_highlight_configs/ugm_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_no_text_image_highlight.ini b/obci/scenarios/visual_audio_no_text_image_highlight.ini new file mode 100644 index 00000000..1b61d499 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight.ini @@ -0,0 +1,110 @@ +[peers] +scenario_dir = + +[peers.switch_backup] +path = interfaces/switch/backup/switch_backup_peer.py +config = scenarios/visual_audio_no_text_image_highlight_configs/switch_backup.ini + +[peers.feedback] +path = logic/feedback/logic_decision_feedback_peer.py +config = scenarios/visual_audio_no_text_image_highlight_configs/feedback.ini + +[peers.feedback.config_sources] +ugm_engine = ugm_engine +analysis = analysis +logic = logic + +[peers.feedback.launch_dependencies] +ugm_server = ugm_server +ugm_engine = ugm_engine +analysis = analysis +logic = logic + +[peers.tag_saver] +path = acquisition/tag_saver_peer.py +config = scenarios/visual_audio_no_text_image_highlight_configs/tag_saver.ini + +[peers.tag_saver.config_sources] +signal_saver = signal_saver + +[peers.tag_saver.launch_dependencies] +signal_saver = signal_saver + +[peers.amplifier] +path = drivers/eeg/amplifier_virtual.py +config = scenarios/visual_audio_no_text_image_highlight_configs/amplifier.ini + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/visual_audio_no_text_image_highlight_configs/config_server.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/visual_audio_no_text_image_highlight_configs/analysis.ini + +[peers.analysis.config_sources] +amplifier = amplifier +logic = logic + +[peers.analysis.launch_dependencies] +amplifier = amplifier +logic = logic + +[peers.switch] +path = drivers/switch/switch_amplifier_peer.py +config = scenarios/visual_audio_no_text_image_highlight_configs/switch.ini + +[peers.switch.launch_dependencies] +ugm_engine = ugm_engine + +[peers.ugm_server] +path = gui/ugm/ugm_server_peer.py +config = scenarios/visual_audio_no_text_image_highlight_configs/ugm_server.ini + +[peers.ugm_server.config_sources] +ugm_engine = ugm_engine + +[peers.ugm_server.launch_dependencies] +ugm_engine = ugm_engine + +[peers.info_saver] +path = acquisition/info_saver_peer.py +config = scenarios/visual_audio_no_text_image_highlight_configs/info_saver.ini + +[peers.info_saver.config_sources] +signal_saver = signal_saver +amplifier = amplifier + +[peers.info_saver.launch_dependencies] +signal_saver = signal_saver +amplifier = amplifier + +[peers.logic] +path = logic/logic_maze_peer.py +config = scenarios/visual_audio_no_text_image_highlight_configs/logic.ini + +[peers.logic.launch_dependencies] +signal_saver = signal_saver +ugm = ugm_server + +[peers.signal_saver] +path = acquisition/signal_saver_peer.py +config = scenarios/visual_audio_no_text_image_highlight_configs/signal_saver.ini + +[peers.signal_saver.config_sources] +amplifier = amplifier + +[peers.signal_saver.launch_dependencies] +amplifier = amplifier + +[peers.ugm_engine] +path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config = scenarios/visual_audio_no_text_image_highlight_configs/ugm_engine.ini + +[peers.ugm_engine.config_sources] +logic = logic + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/visual_audio_no_text_image_highlight_configs/mx.ini + diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/amplifier.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/amplifier.ini new file mode 100644 index 00000000..2fa97c86 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/amplifier.ini @@ -0,0 +1,17 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +active_channels = 0;1;2;3;4;5;6;7;8;Saw;Driver_Saw +channel_names = PO7;O1;Oz;O2;PO8;PO3;PO4;Pz;Cz;AmpSaw;DriverSaw +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sampling_rate = 512 +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/analysis.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/analysis.ini new file mode 100644 index 00000000..7c2e5286 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/analysis.ini @@ -0,0 +1,19 @@ + +[config_sources] +logic = + +[launch_dependencies] +logic = + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +file_log_level = debug +sentry_log_level = error +offline_learning = 0 +mx_log_level = info +channels_for_classification = O1;O2;Pz;Cz +log_dir = ~/.obci/logs +downsample_to = 24 diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/config_server.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/config_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/config_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/feedback.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/feedback.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/feedback.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/info_saver.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/info_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/info_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/logic.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/logic.ini new file mode 100644 index 00000000..02bf4bc9 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/logic.ini @@ -0,0 +1,19 @@ + +[config_sources] + +[launch_dependencies] +signal_saver = + +[external_params] + +[local_params] +experiment_uuid = +active_field_ids = 0;2;5 +dec_count = 3 +logic_decision_config = obci.logic.configs.config_maze_rovio.Config +robot_ip = 192.168.1.18 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/mx.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/mx.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/mx.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/signal_saver.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/signal_saver.ini new file mode 100644 index 00000000..cd1348d6 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/signal_saver.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +save_file_name = test2 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/switch.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/switch.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/switch.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/switch_backup.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/switch_backup.ini new file mode 100644 index 00000000..81e49483 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/switch_backup.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +sentry_log_level = error +mx_log_level = info +file_log_level = debug +finish_saving = 1 diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/tag_saver.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/tag_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/tag_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_engine.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_engine.ini new file mode 100644 index 00000000..09ad4e32 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_engine.ini @@ -0,0 +1,32 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +ugm_config = labyrinth_face_highlight +blink_ugm_id_start = 2001 +images_path = ~/dataset/pictures +modalities = visual;auditory +images_extension = png +blink_ugm_key = font_color +haptic_duration = 0.8 +sentry_log_level = error +blink_ugm_value = #E42525 +log_dir = ~/.obci/logs +blink_duration = 0.3 +blink_count_min = 64 +blink_count_max = 80 +mx_log_level = info +console_log_level = info +file_log_level = debug +experiment_uuid = +active_field_ids = 0;2;5 +blink_max_break = 1.12 +blink_min_break = 1.08 +blink_id_count = 3 +blink_ugm_id_count = 3 +blink_ugm_type = singleimageoddball diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_server.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error From c9b20d8fd7f3f093938817535ac5a07a22f431c4 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Thu, 12 May 2016 15:33:01 +0200 Subject: [PATCH 11/28] ADD: half blink capability (global target probability) --- obci/gui/ugm/blinking/ugm_blinking_engine.py | 2 +- .../ugm/blinking/ugm_blinking_ugm_manager.py | 53 +++++++++++++------ .../ugm/blinking/ugm_modal_blinking_engine.py | 44 +++++++++++---- .../ugm_modal_blinking_engine_peer.ini | 6 +++ 4 files changed, 78 insertions(+), 27 deletions(-) diff --git a/obci/gui/ugm/blinking/ugm_blinking_engine.py b/obci/gui/ugm/blinking/ugm_blinking_engine.py index 2cda217d..75bdada1 100644 --- a/obci/gui/ugm/blinking/ugm_blinking_engine.py +++ b/obci/gui/ugm/blinking/ugm_blinking_engine.py @@ -66,7 +66,7 @@ def _schedule_blink(self, start_time): self._curr_blink_id = self.id_mgr.get_id() self._curr_blink_ugm = self.ugm_mgr.get_blink_ugm(self._curr_blink_id) self._curr_unblink_ugm = self.ugm_mgr.get_unblink_ugm(self._curr_blink_id) - curr_time = self.time_mgr.get_time() #time if next blink I guess? + curr_time = self.time_mgr.get_time() t = 1000*(curr_time - (time.time()-start_time)) if t < 0: t = 0.0 diff --git a/obci/gui/ugm/blinking/ugm_blinking_ugm_manager.py b/obci/gui/ugm/blinking/ugm_blinking_ugm_manager.py index cbfcb462..5a2a449e 100644 --- a/obci/gui/ugm/blinking/ugm_blinking_ugm_manager.py +++ b/obci/gui/ugm/blinking/ugm_blinking_ugm_manager.py @@ -142,6 +142,8 @@ def __init__(self, configs): self.blink_ugm = [] self.unblink_ugm = [] + self.half_blink_ugm = [] + self.half_unblink_ugm = [] #create blinks and unblinks for every field for dec in active_field_ids:#range(count): new_blink_cfgs = [] @@ -157,23 +159,35 @@ def __init__(self, configs): for ndec in active_field_ids: cfg = mgr.get_config_for(start_id+ndec) - new_blink_cfgs.append({'id':cfg['id'], - configs.get_param('blink_ugm_key'):configs.get_param('blink_ugm_value') - }) - new_unblink_cfgs.append({'id':cfg['id'], - configs.get_param('blink_ugm_key'):cfg[configs.get_param('blink_ugm_key')] - }) + half_blink = {'id':cfg['id'], + configs.get_param('blink_ugm_key'):configs.get_param('blink_ugm_value') + } + new_blink_cfgs.append(half_blink) + self.half_blink_ugm.append(half_blink) + half_unblink = {'id':cfg['id'], + configs.get_param('blink_ugm_key'):cfg[configs.get_param('blink_ugm_key')] + } + new_unblink_cfgs.append(half_unblink) + self.half_unblink_ugm.append(half_unblink) self.blink_ugm.append(new_blink_cfgs) self.unblink_ugm.append(new_unblink_cfgs) + #creating half blink + def get_blink_ugm(self, area_id): - return self.blink_ugm[area_id] + if area_id is not None: + return self.blink_ugm[area_id] + else: + return self.half_blink_ugm def get_unblink_ugm(self, area_id): - return self.unblink_ugm[area_id] + if area_id is not None: + return self.unblink_ugm[area_id] + else: + return self.half_unblink_ugm -class _SingleImageOddballUgmManager(object): +class _SingleImageOddballUgmManager(_SingleTextImageOddballUgmManager): '''Ugm config manager for blinks - images with synchronous nontarget blinks and additional target oddball - image - another image. @@ -203,6 +217,8 @@ def __init__(self, configs): self.blink_ugm = [] self.unblink_ugm = [] + self.half_unblink_ugm = [] + self.half_blink_ugm = [] #create blinks and unblinks for every field for dec in active_field_ids: new_blink_cfgs = [] @@ -239,12 +255,19 @@ def __init__(self, configs): self.blink_ugm.append(new_blink_cfgs) self.unblink_ugm.append(new_unblink_cfgs) - - def get_blink_ugm(self, area_id): - return self.blink_ugm[area_id] - - def get_unblink_ugm(self, area_id): - return self.unblink_ugm[area_id] + #creating half blinks: + for ndec in active_field_ids: + high_image = os.path.join(images_folder, + '{}n.{}'.format(ndec, images_extension)) + idle_image = os.path.join(images_folder, + '{}i.{}'.format(ndec, images_extension)) + cfg = mgr.get_config_for(start_id+ndec) + self.half_blink_ugm.append({'id':cfg['id'], + 'image_path':high_image + }) + self.half_unblink_ugm.append({'id':cfg['id'], + 'image_path':idle_image + }) MGRS = { diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py index ae49e2d1..24c75a99 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py @@ -18,6 +18,7 @@ from PyQt4 import QtCore import time import os.path +import random class UgmModalBlinkingEngine(UgmBlinkingEngine): """A class representing ugm application. It is supposed to fire ugm, @@ -86,6 +87,22 @@ def _init_haptic(self, configs): self._haptic_map = dict(zip(self._active_ids, channel_map)) self._haptic_duration = float(configs.get_param('haptic_duration')) + def _schedule_blink(self, start_time): + '''Schedule the next blink with possibility of "half blink"''' + + #do halfblinks sometimes + if random.random()= 0: - self._blinks_count -= 1 - self.connection.send_blink(self._curr_blink_id, update_time) + #only visual supports half blinks + if self._curr_blink_id: + curr_blink_global_id = self._active_ids[self._curr_blink_id] + if self.auditory: + self._sounds[curr_blink_global_id].out() + if self.haptic: + self.stimulator.stimulate( + self._haptic_map[curr_blink_global_id], + self._haptic_duration) + #half blinks doesnt count to blinks count + if self._blinks_count >= 0: + self._blinks_count -= 1 + #and half blinks don't need to be sent + self.connection.send_blink(self._curr_blink_id, update_time) t = 1000*(self._blink_duration - (time.time() - start_time)) if t < 0: t = 0.0 diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini index c30434f7..0589c58c 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini @@ -75,3 +75,9 @@ image_fields_id_offset=1000 ;no need for offset, but blink_ugm_id_start should point to image field images_path= images_extension= + + +;singletextimageoddball and singleimageoddball support "half blinks" +;you can set probability of target blink any target blink and do +;"half blinks" in meantime +global_target_proba=1.0 From 5f9a3aa15d7d8cb934460ba740ed280a4edfdf30 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 17 May 2016 14:45:59 +0200 Subject: [PATCH 12/28] option to send halfblink events with id -1, p300 classifier should now can ignore some ids --- .../ugm/blinking/ugm_modal_blinking_engine.py | 6 ++ .../ugm_modal_blinking_engine_peer.ini | 3 + .../bci/p300_MD/p300_master_peer.ini | 1 + .../bci/p300_MD/p300_master_peer.py | 79 ++++++++++--------- .../bci/p300_MD/tests/synthetic_generator.py | 7 +- .../visual_audio_image_highlight.ini | 9 +++ 6 files changed, 64 insertions(+), 41 deletions(-) diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py index 24c75a99..9bf7e855 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py @@ -20,6 +20,8 @@ import os.path import random +HALFBLINKID=-1 + class UgmModalBlinkingEngine(UgmBlinkingEngine): """A class representing ugm application. It is supposed to fire ugm, receive messages from outside (UGM_UPDATE_MESSAGES) and send`em to @@ -115,6 +117,7 @@ def set_configs(self, configs): self._init_haptic(configs) self._run_on_start = int(configs.get_param('running_on_start')) self._global_target_proba = float(configs.get_param('global_target_proba')) + self._send_halfblinks=int(configs.get_param('send_halfblinks')) def _blink(self): @@ -138,6 +141,9 @@ def _blink(self): self._blinks_count -= 1 #and half blinks don't need to be sent self.connection.send_blink(self._curr_blink_id, update_time) + elif self._send_halfblinks: + self.connection.send_blink(HALFBLINKID, update_time) + t = 1000*(self._blink_duration - (time.time() - start_time)) if t < 0: t = 0.0 diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini index 0589c58c..6bf6a138 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini @@ -81,3 +81,6 @@ images_extension= ;you can set probability of target blink any target blink and do ;"half blinks" in meantime global_target_proba=1.0 +;send nontarget blink events +;will be sent with id -1 +send_halfblinks=1 diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.ini b/obci/interfaces/bci/p300_MD/p300_master_peer.ini index 78f16e4d..8fe5fc01 100644 --- a/obci/interfaces/bci/p300_MD/p300_master_peer.ini +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.ini @@ -14,6 +14,7 @@ calibration_field_index= offline_learning=1 offline_learning_dataset_path= hold_after_dec=3 +ignored_blink_ids=-1 [config_sources] amplifier= diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.py b/obci/interfaces/bci/p300_MD/p300_master_peer.py index 4dc2c20b..fb6e5066 100644 --- a/obci/interfaces/bci/p300_MD/p300_master_peer.py +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.py @@ -106,42 +106,43 @@ def add_result(self, blink, probabilities): probabilities (dict): dictionary of {target: probability} ''' #planning for future - self.singular_proba_buffor[blink.index].append( - probabilities['targetSingle'] - ) - self.averaged_proba_buffor[blink.index].append( - probabilities['targetCMean'] - ) - last_single = [blink.index, probabilities['targetSingle']] - last_mean = [blink.index, probabilities['targetCMean']] - self.logger.info('Last mean dec: {}, proba: {:.2f}'.format( - last_mean[0], - last_mean[1])) - if last_mean[1]>0.5: - self.decision_buffor.append(blink.index) - - # enough of the same decisions condition - one_decision = (len(set(self.decision_buffor)) == 1) - buffor_full = (len(self.decision_buffor) == self.decision_stop) - if one_decision and buffor_full: - decision = self.decision_buffor[-1] - self.logger.info('Decision by decision_stop {}'.format(decision)) - self._send_decision(decision) - return + if probabilities: + self.singular_proba_buffor[blink.index].append( + probabilities['targetSingle'] + ) + self.averaged_proba_buffor[blink.index].append( + probabilities['targetCMean'] + ) + last_single = [blink.index, probabilities['targetSingle']] + last_mean = [blink.index, probabilities['targetCMean']] + self.logger.info('Last mean dec: {}, proba: {:.2f}'.format( + last_mean[0], + last_mean[1])) + if last_mean[1]>0.5: + self.decision_buffor.append(blink.index) - # number of averaged epochs condition - # ensure all buttons have been averaged self.maximum_to_average number of time - minimum_averaged = min( - len(self.averaged_proba_buffor[i]) for i in self.averaged_proba_buffor.keys() - ) - if minimum_averaged > self.maximum_to_average: - most_confident_decision = max( - self.averaged_proba_buffor.items(), - key = lambda key: key[1][-1] - )[0] - self.logger.info('Decision by max_avr {}'.format(most_confident_decision)) - self._send_decision(most_confident_decision) - return + # enough of the same decisions condition + one_decision = (len(set(self.decision_buffor)) == 1) + buffor_full = (len(self.decision_buffor) == self.decision_stop) + if one_decision and buffor_full: + decision = self.decision_buffor[-1] + self.logger.info('Decision by decision_stop {}'.format(decision)) + self._send_decision(decision) + return + + # number of averaged epochs condition + # ensure all buttons have been averaged self.maximum_to_average number of time + minimum_averaged = min( + len(self.averaged_proba_buffor[i]) for i in self.averaged_proba_buffor.keys() + ) + if minimum_averaged > self.maximum_to_average: + most_confident_decision = max( + self.averaged_proba_buffor.items(), + key = lambda key: key[1][-1] + )[0] + self.logger.info('Decision by max_avr {}'.format(most_confident_decision)) + self._send_decision(most_confident_decision) + return @@ -209,7 +210,8 @@ def init_params(self): ) #maximum averaged epochs - self.maximum_to_average = int( + #can be inf + self.maximum_to_average = float( self.config.get_param('maximum_to_average') ) #identical decisions to get final answer @@ -227,6 +229,7 @@ def init_params(self): except ValueError: self.training_index = None + self.ignored_blink_ids = [int(i) for i in self.config.get_param('ignored_blink_ids').strip().split(';')] self.logger.info('Initialasing buffers') self._reset_buffors() @@ -309,6 +312,8 @@ def classify(self, classifier, chunk, blink): or None if classification could not be performed """ + if blink.index in self.ignored_blink_ids: + return None chunk_ready = self._prepare_chunk(chunk) self.features[blink.index].append(chunk_ready) probabilities = {} @@ -319,8 +324,6 @@ def classify(self, classifier, chunk, blink): probabilities['targetCMean'] = classifier.classify( chunk_mean ) - - return probabilities def learn(self, classifier, chunk, target): diff --git a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py index 1683b69c..820d7add 100644 --- a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py +++ b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py @@ -99,7 +99,8 @@ def load_meta(self): def send_nontarget_blink(self,): - self.logger.info('sending distractor blink') + with self.lock: + self.logger.info('sending distractor blink') choice = random.choice(tuple(set(self.fields)-set([self.focus]))) b = variables_pb2.Blink() timestamp = self.time @@ -110,7 +111,6 @@ def send_nontarget_blink(self,): type = types.BLINK_MESSAGE, flush=True) def send_target_blink(self, timestamp=None): - self.logger.info('sending target blink') b = variables_pb2.Blink() if not timestamp: timestamp=self.time @@ -155,8 +155,9 @@ def send_isi(self): def send_target(self): - self.logger.info('Sending target, focus: {}'.format(self.focus)) + with self.lock: + self.logger.info('Sending target, focus: {}'.format(self.focus)) self.sent_targets+=1 length_window = int(self.window*self.sampling_rate) length_packet_aligned = length_window - length_window % self.samples_per_packet diff --git a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight.ini b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight.ini index 5010aca4..25ed0148 100644 --- a/obci/scenarios/budzik/prototypes/visual_audio_image_highlight.ini +++ b/obci/scenarios/budzik/prototypes/visual_audio_image_highlight.ini @@ -108,3 +108,12 @@ logic = logic path = multiplexer-install/bin/mxcontrol config = scenarios/budzik/prototypes/visual_audio_image_highlight_configs/mx.ini +[peers.blink_catcher] +path = utils/blink_catcher_peer.py + +[peers.blink_catcher.config_sources] +ugm_engine = ugm_engine + +[peers.blink_catcher.launch_dependencies] +ugm_engine = ugm_engine + From 615b657e9c8a3b12150f888e7d9f4b5683e34cd1 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Sat, 21 May 2016 13:29:04 +0200 Subject: [PATCH 13/28] helper functions will be more usable --- .../bci/p300_MD/helper_functions.py | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/obci/interfaces/bci/p300_MD/helper_functions.py b/obci/interfaces/bci/p300_MD/helper_functions.py index ff78bbd9..b1941874 100644 --- a/obci/interfaces/bci/p300_MD/helper_functions.py +++ b/obci/interfaces/bci/p300_MD/helper_functions.py @@ -24,7 +24,11 @@ def nontarget_tags_func(tag): def get_epochs_fromfile(ds, start_offset=-0.1,duration=2.0, filter=None, montage=None, - drop_chnls = [ u'AmpSaw', u'DriverSaw', u'trig1', u'trig2']): + drop_chnls = [ u'AmpSaw', u'DriverSaw', u'trig1', u'trig2'], + target_tags_func = target_tags_func, + nontarget_tags_func = nontarget_tags_func, + tag_name = u'blink' + ): '''For offline calibration and testing, load target and nontarget epochs using obci read_manager. Args: @@ -37,6 +41,12 @@ def get_epochs_fromfile(ds, start_offset=-0.1,duration=2.0, montage name can be 'ears', 'csa', 'custom' ears require 2 channel names for ear channels custom requires list of reference channel names + target_tags_func: must return True for provided tag if tag + defines a target + nontarget_tags_func: must return False for provided tag if tag + defines a target + tag_name: tag name to be considered, if you want to use all + tags use None Return: two lists of smart tags: target_tags, nontarget_tags''' eeg_rm = read_manager.ReadManager(ds+'.xml', ds+'.raw', ds+'.tag') @@ -56,7 +66,7 @@ def get_epochs_fromfile(ds, start_offset=-0.1,duration=2.0, eeg_rm = exclude_channels(eeg_rm, drop_chnls) - tag_def = SmartTagDurationDefinition(start_tag_name=u'blink', + tag_def = SmartTagDurationDefinition(start_tag_name=tag_name, start_offset=start_offset, end_offset=0.0, duration=duration) @@ -87,7 +97,7 @@ def evoked_from_smart_tags(tags, chnames, bas = -0.1): def evoked_pair_plot_smart_tags(tags1, tags2, chnames=['O1', 'O2', 'Pz', 'PO7', 'PO8', 'PO3', 'PO4', 'Cz',], - start_offset=-0.1, labels=['target', 'nontarget']): + start_offset=-0.1, labels=['target', 'nontarget'], show= True): '''debug evoked potential plot, pairwise comparison of 2 smarttag lists, blocks thread @@ -98,20 +108,22 @@ def evoked_pair_plot_smart_tags(tags1, tags2, chnames=['O1', 'O2', 'Pz', 'PO7', ev2, std2 = evoked_from_smart_tags(tags2, chnames, start_offset) Fs = float(tags1[0].get_param('sampling_frequency')) time = np.linspace(0+start_offset, ev1.shape[1]/Fs+start_offset, ev1.shape[1]) - pb.figure() + fig = pb.figure() for nr, i in enumerate(chnames): - pb.subplot( (len(chnames)+1)/2, 2, nr+1) - pb.plot(time, ev1[nr], 'r',label = labels[0]+' N:{}'.format(len(tags1))) - pb.fill_between(time, ev1[nr]-std1[nr], ev1[nr]+std1[nr], + ax = fig.add_subplot( (len(chnames)+1)/2, 2, nr+1) + ax.plot(time, ev1[nr], 'r',label = labels[0]+' N:{}'.format(len(tags1))) + ax.fill_between(time, ev1[nr]-std1[nr], ev1[nr]+std1[nr], color = 'red', alpha=0.3, ) - pb.plot(time, ev2[nr], 'b', label = labels[1]+' N:{}'.format(len(tags2))) - pb.fill_between(time, ev2[nr]-std2[nr], ev2[nr]+std2[nr], + ax.plot(time, ev2[nr], 'b', label = labels[1]+' N:{}'.format(len(tags2))) + ax.fill_between(time, ev2[nr]-std2[nr], ev2[nr]+std2[nr], color = 'blue', alpha=0.3) - pb.title(i) - pb.legend() + ax.set_title(i) + ax.legend() - pb.show() + if show: + pb.show() + return fig From 37981089b680b6d146080fc9ad982cf80ce2dcc4 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 24 May 2016 18:51:01 +0200 Subject: [PATCH 14/28] blinking engine pygame sound backend, fixed some imports --- .../ugm/blinking/ugm_modal_blinking_engine.py | 48 +++++++++++++++---- .../ugm_modal_blinking_engine_peer.ini | 2 + .../bci/p300_MD/helper_functions.py | 2 +- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py index 9bf7e855..bb4805ea 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py @@ -7,11 +7,7 @@ from __future__ import print_function from ugm_blinking_engine import UgmBlinkingEngine import sys -try: - import pyo -except ImportError: - print ('ERROR no sound library.\n\t\t Installl pyo!\n\t\tsudo apt-get install python-pyo') - sys.exit() + from obci.devices.haptics.HapticsControl import HapticStimulator from obci.utils import context as ctx from obci.gui.ugm import ugm_engine @@ -22,6 +18,12 @@ HALFBLINKID=-1 +class pygameSoundCompat: + def __init__(self, sound): + self.sound = sound + def out(self): + self.sound.play() + class UgmModalBlinkingEngine(UgmBlinkingEngine): """A class representing ugm application. It is supposed to fire ugm, receive messages from outside (UGM_UPDATE_MESSAGES) and send`em to @@ -62,10 +64,15 @@ def __init__(self, p_config_manager, p_connection, if self._run_on_start: self.start_blinking() - def _init_auditory(self, configs): - soundfiles = configs.get_param('soundfiles').split(';') - self.context['logger'].info(str(soundfiles)) + def initpyo(self, soundfiles): + try: + import pyo + except ImportError: + print ('ERROR no sound library.\n\t\t Installl pyo!\n\t\tsudo apt-get install python-pyo') + sys.exit(1) self.audio_server = pyo.Server(audio='pa') + device = pyo.pa_get_output_devices()[1][-1] + self.audio_server.setInOutDevice(device) self.audio_server.boot() while not self.audio_server.getIsBooted(): time.sleep(1) @@ -75,11 +82,34 @@ def _init_auditory(self, configs): while not self.audio_server.getIsStarted(): time.sleep(1) self.audio_server.start() - self.context['logger'].info('soundfiles: {}'.format(soundfiles)) + time.sleep(2) sounds = [pyo.SfPlayer(os.path.expanduser(f)) for f in soundfiles] assert len(soundfiles) == len(self._active_ids) assert len(sounds) == len(self._active_ids) self._sounds = dict(zip(self._active_ids, sounds)) + + def initpygame(self, soundfiles): + try: + import pygame + except ImportError: + print ('ERROR no sound library.\n\t\t Installl pygame!\n\t\tsudo apt-get install python-pygame') + sys.exit(1) + pygame.mixer.init(44100, -16, 2) + sounds = [pygame.mixer.Sound(os.path.expanduser(f)) for f in soundfiles] + soundscompat = [pygameSoundCompat(i) for i in sounds] + assert len(soundfiles) == len(self._active_ids) + assert len(sounds) == len(self._active_ids) + self._sounds = dict(zip(self._active_ids, soundscompat)) + + + def _init_auditory(self, configs): + soundfiles = configs.get_param('soundfiles').split(';') + self.soundbackend = configs.get_param('soundbackend') + if self.soundbackend == 'pyo': + self.initpyo(soundfiles) + elif self.soundbackend == 'pygame': + self.initpygame(soundfiles) + self.context['logger'].info('soundfiles: {}'.format(soundfiles)) def _init_haptic(self, configs): ids = configs.get_param("haptic_device").split(":") diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini index 6bf6a138..3e734827 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini @@ -16,6 +16,8 @@ running_on_start=1 ;auditory ;should be in order of active_field_ids soundfiles=~/dataset/sounds/1.wav;~/dataset/sounds/2.wav;~/dataset/sounds/3.wav +;available backends: pyo pygame +soundbackend=pyo ;haptic haptic_duration=0.1 diff --git a/obci/interfaces/bci/p300_MD/helper_functions.py b/obci/interfaces/bci/p300_MD/helper_functions.py index b1941874..f5d6db49 100644 --- a/obci/interfaces/bci/p300_MD/helper_functions.py +++ b/obci/interfaces/bci/p300_MD/helper_functions.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # based on obci.analysis.p300.analysis_offline # Marian Dovgialo -from scipy import zeros, diag, dot +from scipy import zeros, diag, dot, ones from scipy import linalg import numpy as np import pylab as pb From 8dfea1ea642836b34ae50b0ad81f26c54ff01232 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 24 May 2016 18:57:22 +0200 Subject: [PATCH 15/28] reducing pygame latency - configurable buffors --- obci/gui/ugm/blinking/ugm_modal_blinking_engine.py | 8 +++++--- obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py index bb4805ea..92ac77dc 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py @@ -88,13 +88,14 @@ def initpyo(self, soundfiles): assert len(sounds) == len(self._active_ids) self._sounds = dict(zip(self._active_ids, sounds)) - def initpygame(self, soundfiles): + def initpygame(self, soundfiles, pygame_buffor): try: import pygame except ImportError: print ('ERROR no sound library.\n\t\t Installl pygame!\n\t\tsudo apt-get install python-pygame') sys.exit(1) - pygame.mixer.init(44100, -16, 2) + + pygame.mixer.init(44100, -16, 2, pygame_buffor) sounds = [pygame.mixer.Sound(os.path.expanduser(f)) for f in soundfiles] soundscompat = [pygameSoundCompat(i) for i in sounds] assert len(soundfiles) == len(self._active_ids) @@ -105,10 +106,11 @@ def initpygame(self, soundfiles): def _init_auditory(self, configs): soundfiles = configs.get_param('soundfiles').split(';') self.soundbackend = configs.get_param('soundbackend') + pygame_buffor = int(configs.get_param('pygamebuffor')) if self.soundbackend == 'pyo': self.initpyo(soundfiles) elif self.soundbackend == 'pygame': - self.initpygame(soundfiles) + self.initpygame(soundfiles, pygame_buffor) self.context['logger'].info('soundfiles: {}'.format(soundfiles)) def _init_haptic(self, configs): diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini index 3e734827..71846cac 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini @@ -18,7 +18,8 @@ running_on_start=1 soundfiles=~/dataset/sounds/1.wav;~/dataset/sounds/2.wav;~/dataset/sounds/3.wav ;available backends: pyo pygame soundbackend=pyo - +;try to keep it at minimum, while the sound doesnt get distorted (must be power of 2) +pygamebuffor=64 ;haptic haptic_duration=0.1 haptic_device=0403:6010 From 7c221707ba28a5d94b4eaf6945312b8a497a3a3f Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 24 May 2016 19:32:44 +0200 Subject: [PATCH 16/28] fixed first field sound not playing --- obci/gui/ugm/blinking/ugm_modal_blinking_engine.py | 8 +++++--- obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py index 92ac77dc..91461268 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py @@ -65,14 +65,13 @@ def __init__(self, p_config_manager, p_connection, self.start_blinking() def initpyo(self, soundfiles): + self.context['logger'].info('initialising pyo audio backend') try: import pyo except ImportError: print ('ERROR no sound library.\n\t\t Installl pyo!\n\t\tsudo apt-get install python-pyo') sys.exit(1) self.audio_server = pyo.Server(audio='pa') - device = pyo.pa_get_output_devices()[1][-1] - self.audio_server.setInOutDevice(device) self.audio_server.boot() while not self.audio_server.getIsBooted(): time.sleep(1) @@ -89,6 +88,8 @@ def initpyo(self, soundfiles): self._sounds = dict(zip(self._active_ids, sounds)) def initpygame(self, soundfiles, pygame_buffor): + self.context['logger'].info('initialising pygame audio backend') + try: import pygame except ImportError: @@ -104,6 +105,7 @@ def initpygame(self, soundfiles, pygame_buffor): def _init_auditory(self, configs): + soundfiles = configs.get_param('soundfiles').split(';') self.soundbackend = configs.get_param('soundbackend') pygame_buffor = int(configs.get_param('pygamebuffor')) @@ -160,7 +162,7 @@ def _blink(self): self.update_from(self._curr_blink_ugm) update_time = time.time() #only visual supports half blinks - if self._curr_blink_id: + if self._curr_blink_id is not None: curr_blink_global_id = self._active_ids[self._curr_blink_id] if self.auditory: self._sounds[curr_blink_global_id].out() diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini index 71846cac..002a82cc 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.ini @@ -19,7 +19,7 @@ soundfiles=~/dataset/sounds/1.wav;~/dataset/sounds/2.wav;~/dataset/sounds/3.wav ;available backends: pyo pygame soundbackend=pyo ;try to keep it at minimum, while the sound doesnt get distorted (must be power of 2) -pygamebuffor=64 +pygamebuffor=256 ;haptic haptic_duration=0.1 haptic_device=0403:6010 From 96a19e472ef4bcef56d353b93f7cfc3baa32c995 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Mon, 30 May 2016 21:12:16 +0200 Subject: [PATCH 17/28] initial ugm field for 2 class bci --- .../gui/ugm/configs/budzik_visual_p300_2class.ugm | Bin 0 -> 992 bytes obci/interfaces/bci/analysis_master.py | 1 + 2 files changed, 1 insertion(+) create mode 100644 obci/gui/ugm/configs/budzik_visual_p300_2class.ugm diff --git a/obci/gui/ugm/configs/budzik_visual_p300_2class.ugm b/obci/gui/ugm/configs/budzik_visual_p300_2class.ugm new file mode 100644 index 0000000000000000000000000000000000000000..8c979462e1df30a84e3426d467912e2ffac73090 GIT binary patch literal 992 zcmah{TTc@~6t2BrKrB^6K}FGaMMb>g9jhW@txulB7t$>4v}cm-PS5Tv3P}_HqW%J3 z^~s;$+1)^AqY1Owm-%MC+nI0a9ZHSqgazdPt!Ye(i+o@g{JXSXnL)pVR9Cx53k4EH7I$at8I7S z2s({L$U360=?qT1BAfgf+r|nMY)1}U(W5zJ^|P!#Z$YWgVLQG&5Ol89W0rhZxLSC0 z9&3=*;;W_ws6>bQ_E0+evsZ5ZZaFV}s`Tji!95QBJ>kJUwJ Date: Tue, 31 May 2016 01:15:34 +0200 Subject: [PATCH 18/28] 2 class visual p300 scenarious (dummy amplifier) for bci and calibration, logic peers, mx rules --- obci/configs/multiplexer.rules | 5 + obci/control/gui/presets/budzik.ini | 14 +++ .../ugm/blinking/ugm_modal_blinking_engine.py | 39 +++++-- .../p300_MD/logic_p300_calibration_peer.ini | 20 ++++ .../p300_MD/logic_p300_calibration_peer.py | 96 +++++++++++++++ .../bci/p300_MD/p300_master_peer.py | 2 +- obci/logic/feedback/__init__.py | 1 + .../logic_decision_feedback_budzik_peer.ini | 16 +++ .../logic_decision_feedback_budzik_peer.py | 85 ++++++++++++++ .../P300_visual_2_class_budzik.ini} | 59 ++++------ ...P300_visual_2_class_budzik_calibration.ini | 88 ++++++++++++++ .../amplifier.ini | 0 .../analysis.ini | 11 +- .../blink_catcher.ini} | 0 .../config_server.ini | 0 .../feedback.ini | 15 +++ .../info_saver.ini | 0 .../logic.ini} | 6 +- .../mx.ini | 0 .../signal_saver.ini | 0 .../tag_saver.ini | 0 .../ugm_engine.ini | 32 +++++ .../ugm_server.ini | 0 .../amplifier.ini | 0 .../analysis.ini | 10 +- .../blink_catcher.ini} | 0 .../config_server.ini} | 0 .../feedback.ini} | 4 +- .../info_saver.ini | 0 .../logic.ini | 0 .../mx.ini | 0 .../signal_saver.ini | 0 .../switch.ini | 0 .../switch_backup.ini | 0 .../tag_saver.ini | 0 .../ugm_engine.ini | 31 ++--- .../ugm_server.ini | 0 .../visual_audio_no_text_image_highlight.ini | 110 ------------------ .../logic.ini | 19 --- .../ugm_engine.ini | 32 ----- 40 files changed, 454 insertions(+), 241 deletions(-) create mode 100644 obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.ini create mode 100644 obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.py create mode 100644 obci/logic/feedback/__init__.py create mode 100644 obci/logic/feedback/logic_decision_feedback_budzik_peer.ini create mode 100644 obci/logic/feedback/logic_decision_feedback_budzik_peer.py rename obci/scenarios/{visual_audio_image_highlight.ini => budzik/prototypes/P300_visual_2_class_budzik.ini} (52%) create mode 100644 obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration.ini rename obci/scenarios/{visual_audio_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_calibration_configs}/amplifier.ini (100%) rename obci/scenarios/{visual_audio_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_calibration_configs}/analysis.ini (91%) rename obci/scenarios/{visual_audio_image_highlight_configs/config_server.ini => budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/blink_catcher.ini} (100%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_calibration_configs}/config_server.ini (100%) create mode 100644 obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/feedback.ini rename obci/scenarios/{visual_audio_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_calibration_configs}/info_saver.ini (100%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs/feedback.ini => budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini} (86%) rename obci/scenarios/{visual_audio_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_calibration_configs}/mx.ini (100%) rename obci/scenarios/{visual_audio_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_calibration_configs}/signal_saver.ini (100%) rename obci/scenarios/{visual_audio_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_calibration_configs}/tag_saver.ini (100%) create mode 100644 obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine.ini rename obci/scenarios/{visual_audio_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_calibration_configs}/ugm_server.ini (100%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/amplifier.ini (100%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/analysis.ini (94%) rename obci/scenarios/{visual_audio_image_highlight_configs/feedback.ini => budzik/prototypes/P300_visual_2_class_budzik_configs/blink_catcher.ini} (100%) rename obci/scenarios/{visual_audio_image_highlight_configs/switch.ini => budzik/prototypes/P300_visual_2_class_budzik_configs/config_server.ini} (100%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs/switch_backup.ini => budzik/prototypes/P300_visual_2_class_budzik_configs/feedback.ini} (75%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/info_saver.ini (100%) rename obci/scenarios/{visual_audio_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/logic.ini (100%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/mx.ini (100%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/signal_saver.ini (100%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/switch.ini (100%) rename obci/scenarios/{visual_audio_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/switch_backup.ini (100%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/tag_saver.ini (100%) rename obci/scenarios/{visual_audio_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/ugm_engine.ini (65%) rename obci/scenarios/{visual_audio_no_text_image_highlight_configs => budzik/prototypes/P300_visual_2_class_budzik_configs}/ugm_server.ini (100%) delete mode 100644 obci/scenarios/visual_audio_no_text_image_highlight.ini delete mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/logic.ini delete mode 100644 obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_engine.ini diff --git a/obci/configs/multiplexer.rules b/obci/configs/multiplexer.rules index 70c8f426..0325c443 100644 --- a/obci/configs/multiplexer.rules +++ b/obci/configs/multiplexer.rules @@ -1024,6 +1024,11 @@ type { whom: ALL report_delivery_error: false } + to { + peer: "LOGIC_FEEDBACK" + whom: ALL + report_delivery_error: false + } } type { diff --git a/obci/control/gui/presets/budzik.ini b/obci/control/gui/presets/budzik.ini index 64379dde..a73bc640 100644 --- a/obci/control/gui/presets/budzik.ini +++ b/obci/control/gui/presets/budzik.ini @@ -69,3 +69,17 @@ info=run interactive labirynth P300 on dummy amp with 2 modalities launch_file=scenarios/budzik/prototypes/visual_audio_no_text_image_highlight.ini public_params= category=Prototypes P300 + +[P300 visual 2 classes bci] +info=run interactive p300 2 class bci with faces as highlight +launch_file=scenarios/budzik/prototypes/P300_visual_2_class_budzik.ini +public_params= +category=Prototypes P300 visual bci + +[P300 visual 2 classes bci calibration] +info=run 2 class bci calibration with faces as highlight +launch_file=scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration.ini +public_params= +category=Prototypes P300 visual bci + + diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py index 91461268..c8cccece 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine.py @@ -48,21 +48,18 @@ def __init__(self, p_config_manager, p_connection, self.auditory = 'auditory' in modalities #must be here or else they connect to parent class methods - self._blink_timer = QtCore.QTimer(self) - self._blink_timer.setSingleShot(True) - self._blink_timer.connect(self._blink_timer, QtCore.SIGNAL("timeout()"), self._blink) - - self._unblink_timer = QtCore.QTimer(self) - self._unblink_timer.setSingleShot(True) - self._unblink_timer.connect(self._unblink_timer, QtCore.SIGNAL("timeout()"), self._unblink) + #~ self._blink_timer = QtCore.QTimer(self) + #~ self._blink_timer.setSingleShot(True) + #~ self._blink_timer.connect(self._blink_timer, QtCore.SIGNAL("timeout()"), self._blink) - self._stop_timer = QtCore.QTimer(self) - self._stop_timer.setSingleShot(True) - self._stop_timer.connect(self._stop_timer, QtCore.SIGNAL("timeout()"), self._stop) + #~ self._unblink_timer = QtCore.QTimer(self) + #~ self._unblink_timer.setSingleShot(True) + #~ self._unblink_timer.connect(self._unblink_timer, QtCore.SIGNAL("timeout()"), self._unblink) + #~ self._stop_timer = QtCore.QTimer(self) + #~ self._stop_timer.setSingleShot(True) + #~ self._stop_timer.connect(self._stop_timer, QtCore.SIGNAL("timeout()"), self._stop) - if self._run_on_start: - self.start_blinking() def initpyo(self, soundfiles): self.context['logger'].info('initialising pyo audio backend') @@ -139,6 +136,24 @@ def _schedule_blink(self, start_time): t = 0.0 self.context['logger'].warning("BLINKER WARNING: time between blinks to short for that computer ...") self._blink_timer.start(t) + + def _timer_on_run(self): + super(UgmBlinkingEngine, self)._timer_on_run() + self._blink_timer = QtCore.QTimer(self) + self._blink_timer.setSingleShot(True) + self._blink_timer.connect(self._blink_timer, QtCore.SIGNAL("timeout()"), self._blink) + + self._unblink_timer = QtCore.QTimer(self) + self._unblink_timer.setSingleShot(True) + self._unblink_timer.connect(self._unblink_timer, QtCore.SIGNAL("timeout()"), self._unblink) + + self._stop_timer = QtCore.QTimer(self) + self._stop_timer.setSingleShot(True) + self._stop_timer.connect(self._stop_timer, QtCore.SIGNAL("timeout()"), self._stop) + + self.context['logger'].info('RUN ON START: {}'.format(self._run_on_start)) + if self._run_on_start: + self.start_blinking() def set_configs(self, configs): for m in self.mgrs: diff --git a/obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.ini b/obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.ini new file mode 100644 index 00000000..1ff7578b --- /dev/null +++ b/obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.ini @@ -0,0 +1,20 @@ +[local_params] +hi_text=Witamy. Zliczaj pojawianie się twarzy na napisie TAK +bye_text=Dziekujemy. +break_text=Przerwa. Teraz mrugaj. +break_duration=2 +trials_count=17 + +[config_sources] +ugm_engine= +analysis= + +[external_params] +ugm_config=ugm_engine.ugm_config +blink_duration=ugm_engine.blink_duration +target=analysis.calibration_field_index + +[launch_dependencies] +ugm_server= +ugm_engine= +analysis= \ No newline at end of file diff --git a/obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.py b/obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.py new file mode 100644 index 00000000..0aa058e0 --- /dev/null +++ b/obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: +# Mateusz Kruszyński + +import os.path, sys, time + +from multiplexer.multiplexer_constants import peers, types +from obci.control.peer.configured_multiplexer_server import ConfiguredMultiplexerServer + +from obci.configs import settings, variables_pb2 +from obci.gui.ugm import ugm_config_manager +from obci.gui.ugm import ugm_helper +from obci.utils import keystroke +from obci.utils import tags_helper + +from obci.acquisition import acquisition_helper + +class LogicP300Calibration(ConfiguredMultiplexerServer): + '''BCI calibration controller''' + def __init__(self, addresses): + super(LogicP300Calibration, self).__init__(addresses=addresses, + type=peers.LOGIC_P300_CALIBRATION) + self.blinking_ugm = ugm_config_manager.UgmConfigManager(self.config.get_param("ugm_config")).config_to_message() + self.hi_text = self.config.get_param("hi_text") + self.bye_text = self.config.get_param("bye_text") + self.break_text = self.config.get_param("break_text") + self.break_duration = float(self.config.get_param("break_duration")) + self.trials_count = int(self.config.get_param("trials_count")) + self.current_target = int(self.config.get_param("target")) + self.blink_duration = float(self.config.get_param("blink_duration")) + + self._trials_counter = self.trials_count + self.ready() + self.begin() + + def handle_message(self, mxmsg): + """Method fired by multiplexer. It conveys decision to logic engine.""" + if (mxmsg.type == types.UGM_ENGINE_MESSAGE): + self._handle_ugm_engine(mxmsg.message) + elif mxmsg.type == types.BLINK_MESSAGE: + self._handle_blink(mxmsg.message) + else: + self.logger.warning("Got unrecognised message type: "+mxmsg.type) + self.no_response() + + def _handle_blink(self, msg): + b = variables_pb2.Blink() + b.ParseFromString(msg) + self.logger.debug("GOT BLINK: "+str(b.timestamp)+" / "+str(b.index)) + tags_helper.send_tag(self.conn, + b.timestamp, b.timestamp+self.blink_duration, "blink", + {"index" : b.index, + "target":self.current_target + }) + + def _handle_ugm_engine(self, msg): + m = variables_pb2.Variable() + m.ParseFromString(msg) + if m.key == "blinking_stopped": + self.logger.info("Got blinking stopped message!") + self._trials_counter -= 1 + if self._trials_counter <= 0: + self.logger.info("All trials passed") + self.end() + else: + self.logger.info("Blinking stopped...") + self.blinking_stopped() + else: + self.logger.info("Got unrecognised ugm engine message: "+str(m.key)) + + def begin(self): + ugm_helper.send_text(self.conn, self.hi_text) + #keystroke.wait([" "]) + time.sleep(5) + self.logger.info("Send begin config ...") + ugm_helper.send_config(self.conn, self.blinking_ugm) + self.logger.info("Send start blinking on begin ...") + ugm_helper.send_start_blinking(self.conn) + + def end(self): + ugm_helper.send_text(self.conn, self.bye_text) + #acquire some more data + time.sleep(2) + acquisition_helper.send_finish_saving(self.conn) + + def blinking_stopped(self): + time.sleep(1) + ugm_helper.send_text(self.conn, self.break_text) + time.sleep(self.break_duration) + ugm_helper.send_config(self.conn, self.blinking_ugm) + time.sleep(1) + ugm_helper.send_start_blinking(self.conn) + +if __name__ == "__main__": + LogicP300Calibration(settings.MULTIPLEXER_ADDRESSES).loop() diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.py b/obci/interfaces/bci/p300_MD/p300_master_peer.py index fb6e5066..614b8b1e 100644 --- a/obci/interfaces/bci/p300_MD/p300_master_peer.py +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.py @@ -50,7 +50,7 @@ class P300MasterPeer(AnalysisMaster): def __init__(self, addresses): super(P300MasterPeer, self).__init__(addresses=addresses, type=peers.P300_ANALYSIS) - ugm_helper.send_start_blinking(self.conn) + def _reset_buffors(self): #probabilities of selected input for single epochs diff --git a/obci/logic/feedback/__init__.py b/obci/logic/feedback/__init__.py new file mode 100644 index 00000000..8d1c8b69 --- /dev/null +++ b/obci/logic/feedback/__init__.py @@ -0,0 +1 @@ + diff --git a/obci/logic/feedback/logic_decision_feedback_budzik_peer.ini b/obci/logic/feedback/logic_decision_feedback_budzik_peer.ini new file mode 100644 index 00000000..3c3231d9 --- /dev/null +++ b/obci/logic/feedback/logic_decision_feedback_budzik_peer.ini @@ -0,0 +1,16 @@ +[local_params] +hello_message= +[launch_dependencies] +analysis= +ugm_server= +ugm_engine= + +[config_sources] +analysis= +ugm_engine= + +[external_params] +blink_ugm_id_start=ugm_engine.blink_ugm_id_start +active_field_ids=ugm_engine.active_field_ids +feed_time=analysis.hold_after_dec +ugm_config=ugm_engine.ugm_config diff --git a/obci/logic/feedback/logic_decision_feedback_budzik_peer.py b/obci/logic/feedback/logic_decision_feedback_budzik_peer.py new file mode 100644 index 00000000..e3f498f6 --- /dev/null +++ b/obci/logic/feedback/logic_decision_feedback_budzik_peer.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import time +from multiplexer.multiplexer_constants import peers, types +from obci.control.peer.configured_multiplexer_server import ConfiguredMultiplexerServer + +from obci.gui.ugm import ugm_helper +from obci.configs import settings, variables_pb2 +from obci.utils.openbci_logging import log_crash +from obci.gui.ugm import ugm_config_manager + +class LogicDecisionFeedbackBudzik(ConfiguredMultiplexerServer): + '''Logic to controll N classes BCI + The cartaker should ask a question and then press + space to activate BCI. The BCI user should then concentrate on + answer''' + + @log_crash + def __init__(self, addresses): + #Create a helper object to get configuration from the system + super(LogicDecisionFeedbackBudzik, self).__init__(addresses=addresses, + type=peers.LOGIC_FEEDBACK) + + self.feed_time = float(self.config.get_param('feed_time')) + first_field_id = self.config.get_param('blink_ugm_id_start') + #we need background field id which for id 1001 would be 10001: + first_field_id = int(first_field_id[0] + '0' +first_field_id[1:]) + active_field_offsets = [int(i) for i in self.config.get_param('active_field_ids').split(';')] + ugm_field_ids = [i+first_field_id for i in active_field_offsets] + #~ self.dec_count = int(self.config.get_param('dec_count')) + self.blinking_config = self.config.get_param('ugm_config') + self.blinking_config_ugm = ugm_config_manager.UgmConfigManager(self.blinking_config).config_to_message() + self.feed_manager = ugm_helper.UgmColorUpdater( + self.blinking_config, + ugm_field_ids + ) + + self.HELLO_MESSAGE = self.config.get_param('hello_message') + assert(self.feed_time >= 0) + #~ assert(self.dec_count > 0) + self.ready() + self.begin() + + + def begin(self): + ugm_helper.send_text(self.conn, self.HELLO_MESSAGE) + time.sleep(5) + ugm_helper.send_config(self.conn, self.blinking_config_ugm) + #~ ugm_helper.send_start_blinking(self.conn) + + def handle_message(self, mxmsg): + if mxmsg.type == types.DECISION_MESSAGE: + dec = int(mxmsg.message) + self.logger.info("Got decision: "+str(dec)) + assert(dec >= 0) + dec_time = time.time() + self._send_feedback(dec, dec_time) + elif mxmsg.type == types.UGM_ENGINE_MESSAGE: + msg = variables_pb2.Variable() + msg.ParseFromString(mxmsg.message) + if msg.key == 'keybord_event': + self.logger.info("Got keypress: {}".format(msg.value)) + if msg.value == '32':#space + ugm_helper.send_start_blinking(self.conn) + + self.no_response() + + def _send_feedback(self, dec, dec_time): + ugm_helper.send_stop_blinking(self.conn) + while True: + t = time.time() - dec_time + if t > self.feed_time: + ugm_config = self.feed_manager.update_ugm(dec, -1) + ugm_helper.send_config(self.conn, ugm_config, 1) + break + else: + self.logger.debug("t="+str(t)+"FEED: "+str(t/self.feed_time)) + ugm_config = self.feed_manager.update_ugm(dec, 1-(t/self.feed_time)) + ugm_helper.send_config(self.conn, ugm_config, 1) + time.sleep(0.05) + + +if __name__ == "__main__": + LogicDecisionFeedbackBudzik(settings.MULTIPLEXER_ADDRESSES).loop() + diff --git a/obci/scenarios/visual_audio_image_highlight.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik.ini similarity index 52% rename from obci/scenarios/visual_audio_image_highlight.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik.ini index 649aaced..35d4b755 100644 --- a/obci/scenarios/visual_audio_image_highlight.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik.ini @@ -1,28 +1,22 @@ [peers] scenario_dir = -[peers.switch_backup] -path = interfaces/switch/backup/switch_backup_peer.py -config = scenarios/visual_audio_image_highlight_configs/switch_backup.ini - [peers.feedback] -path = logic/feedback/logic_decision_feedback_peer.py -config = scenarios/visual_audio_image_highlight_configs/feedback.ini +path = logic/feedback/logic_decision_feedback_budzik_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/feedback.ini [peers.feedback.config_sources] ugm_engine = ugm_engine analysis = analysis -logic = logic [peers.feedback.launch_dependencies] ugm_server = ugm_server ugm_engine = ugm_engine analysis = analysis -logic = logic -[peers.tag_saver] +[peers.tag_saver] path = acquisition/tag_saver_peer.py -config = scenarios/visual_audio_image_highlight_configs/tag_saver.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/tag_saver.ini [peers.tag_saver.config_sources] signal_saver = signal_saver @@ -32,34 +26,25 @@ signal_saver = signal_saver [peers.amplifier] path = drivers/eeg/amplifier_virtual.py -config = scenarios/visual_audio_image_highlight_configs/amplifier.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/amplifier.ini [peers.config_server] path = control/peer/config_server.py -config = scenarios/visual_audio_image_highlight_configs/config_server.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/config_server.ini [peers.analysis] path = interfaces/bci/p300_MD/p300_master_peer.py -config = scenarios/visual_audio_image_highlight_configs/analysis.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/analysis.ini [peers.analysis.config_sources] amplifier = amplifier -logic = logic [peers.analysis.launch_dependencies] amplifier = amplifier -logic = logic - -[peers.switch] -path = drivers/switch/switch_amplifier_peer.py -config = scenarios/visual_audio_image_highlight_configs/switch.ini - -[peers.switch.launch_dependencies] -ugm_engine = ugm_engine [peers.ugm_server] path = gui/ugm/ugm_server_peer.py -config = scenarios/visual_audio_image_highlight_configs/ugm_server.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_server.ini [peers.ugm_server.config_sources] ugm_engine = ugm_engine @@ -69,7 +54,7 @@ ugm_engine = ugm_engine [peers.info_saver] path = acquisition/info_saver_peer.py -config = scenarios/visual_audio_image_highlight_configs/info_saver.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/info_saver.ini [peers.info_saver.config_sources] signal_saver = signal_saver @@ -79,17 +64,9 @@ amplifier = amplifier signal_saver = signal_saver amplifier = amplifier -[peers.logic] -path = logic/logic_maze_peer.py -config = scenarios/visual_audio_image_highlight_configs/logic.ini - -[peers.logic.launch_dependencies] -signal_saver = signal_saver -ugm = ugm_server - [peers.signal_saver] path = acquisition/signal_saver_peer.py -config = scenarios/visual_audio_image_highlight_configs/signal_saver.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/signal_saver.ini [peers.signal_saver.config_sources] amplifier = amplifier @@ -99,12 +76,22 @@ amplifier = amplifier [peers.ugm_engine] path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py -config = scenarios/visual_audio_image_highlight_configs/ugm_engine.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine.ini [peers.ugm_engine.config_sources] -logic = logic +logic = [peers.mx] path = multiplexer-install/bin/mxcontrol -config = scenarios/visual_audio_image_highlight_configs/mx.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/mx.ini + +[peers.blink_catcher] +path = utils/blink_catcher_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/blink_catcher.ini + +[peers.blink_catcher.config_sources] +ugm_engine = ugm_engine + +[peers.blink_catcher.launch_dependencies] +ugm_engine = ugm_engine diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration.ini new file mode 100644 index 00000000..d9a9a8a0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration.ini @@ -0,0 +1,88 @@ +[peers] +scenario_dir = + +[peers.tag_saver] +path = acquisition/tag_saver_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/tag_saver.ini + +[peers.tag_saver.config_sources] +signal_saver = signal_saver + +[peers.tag_saver.launch_dependencies] +signal_saver = signal_saver + +[peers.amplifier] +path = drivers/eeg/amplifier_virtual.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier.ini + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/config_server.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis.ini + +[peers.analysis.config_sources] +amplifier = amplifier + +[peers.analysis.launch_dependencies] +amplifier = amplifier + +[peers.ugm_server] +path = gui/ugm/ugm_server_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_server.ini + +[peers.ugm_server.config_sources] +ugm_engine = ugm_engine + +[peers.ugm_server.launch_dependencies] +ugm_engine = ugm_engine + +[peers.info_saver] +path = acquisition/info_saver_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/info_saver.ini + +[peers.info_saver.config_sources] +signal_saver = signal_saver +amplifier = amplifier + +[peers.info_saver.launch_dependencies] +signal_saver = signal_saver +amplifier = amplifier + +[peers.logic] +path = interfaces/bci/p300_MD/logic_p300_calibration_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini + +[peers.logic.config_sources] +ugm_engine = ugm_engine +analysis = analysis + +[peers.logic.launch_dependencies] +signal_saver = signal_saver +ugm_server = ugm_server +ugm_engine = ugm_engine +analysis = analysis + +[peers.signal_saver] +path = acquisition/signal_saver_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/signal_saver.ini + +[peers.signal_saver.config_sources] +amplifier = amplifier + +[peers.signal_saver.launch_dependencies] +amplifier = amplifier + +[peers.ugm_engine] +path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine.ini + +[peers.ugm_engine.config_sources] +logic = + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/mx.ini + diff --git a/obci/scenarios/visual_audio_image_highlight_configs/amplifier.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/amplifier.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier.ini diff --git a/obci/scenarios/visual_audio_image_highlight_configs/analysis.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis.ini similarity index 91% rename from obci/scenarios/visual_audio_image_highlight_configs/analysis.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis.ini index 7c2e5286..422b3c15 100644 --- a/obci/scenarios/visual_audio_image_highlight_configs/analysis.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis.ini @@ -1,19 +1,18 @@ [config_sources] -logic = [launch_dependencies] -logic = [external_params] [local_params] experiment_uuid = console_log_level = info -file_log_level = debug -sentry_log_level = error -offline_learning = 0 -mx_log_level = info channels_for_classification = O1;O2;Pz;Cz log_dir = ~/.obci/logs +offline_learning = 0 +mx_log_level = info +file_log_level = debug downsample_to = 24 +sentry_log_level = error +calibration_field_index = 0 diff --git a/obci/scenarios/visual_audio_image_highlight_configs/config_server.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/blink_catcher.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/config_server.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/blink_catcher.ini diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/config_server.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/config_server.ini similarity index 100% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/config_server.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/config_server.ini diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/feedback.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/feedback.ini new file mode 100644 index 00000000..a26604ca --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/feedback.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +hello_message = Start BCI, wysłuchaj pytania i skup się na odpowiedzi +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/visual_audio_image_highlight_configs/info_saver.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/info_saver.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/info_saver.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/info_saver.ini diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/feedback.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini similarity index 86% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/feedback.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini index cb6640e0..64550eab 100644 --- a/obci/scenarios/visual_audio_no_text_image_highlight_configs/feedback.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini @@ -2,13 +2,15 @@ [config_sources] [launch_dependencies] +signal_saver = [external_params] [local_params] experiment_uuid = console_log_level = info -log_dir = ~/.obci/logs +sentry_log_level = error mx_log_level = info file_log_level = debug -sentry_log_level = error +trials_count = 4 +log_dir = ~/.obci/logs diff --git a/obci/scenarios/visual_audio_image_highlight_configs/mx.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/mx.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/mx.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/mx.ini diff --git a/obci/scenarios/visual_audio_image_highlight_configs/signal_saver.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/signal_saver.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/signal_saver.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/signal_saver.ini diff --git a/obci/scenarios/visual_audio_image_highlight_configs/tag_saver.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/tag_saver.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/tag_saver.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/tag_saver.ini diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine.ini new file mode 100644 index 00000000..33f187ae --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine.ini @@ -0,0 +1,32 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +ugm_config = budzik_visual_p300_2class +blink_ugm_id_start = 1001 +modalities = visual +running_on_start = 0 +haptic_duration = 0.8 +blink_ugm_key = font_color +blink_count_type = random +sentry_log_level = error +blink_ugm_value = #E42525 +log_dir = ~/.obci/logs +blink_count_min = 5 +blink_count_max = 10 +global_target_proba = 0.3 +console_log_level = info +file_log_level = debug +mx_log_level = info +experiment_uuid = +active_field_ids = 0;1 +blink_max_break = 0.22 +blink_min_break = 0.18 +blink_id_count = 2 +blink_ugm_id_count = 2 +blink_ugm_type = singletextimageoddball diff --git a/obci/scenarios/visual_audio_image_highlight_configs/ugm_server.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_server.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/ugm_server.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_server.ini diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/amplifier.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/amplifier.ini similarity index 100% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/amplifier.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/amplifier.ini diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/analysis.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/analysis.ini similarity index 94% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/analysis.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/analysis.ini index 7c2e5286..9489a15b 100644 --- a/obci/scenarios/visual_audio_no_text_image_highlight_configs/analysis.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/analysis.ini @@ -1,19 +1,17 @@ [config_sources] -logic = [launch_dependencies] -logic = [external_params] [local_params] experiment_uuid = console_log_level = info -file_log_level = debug -sentry_log_level = error -offline_learning = 0 -mx_log_level = info channels_for_classification = O1;O2;Pz;Cz log_dir = ~/.obci/logs +offline_learning = 0 +mx_log_level = info +file_log_level = debug downsample_to = 24 +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_image_highlight_configs/feedback.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/blink_catcher.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/feedback.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/blink_catcher.ini diff --git a/obci/scenarios/visual_audio_image_highlight_configs/switch.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/config_server.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/switch.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/config_server.ini diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/switch_backup.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/feedback.ini similarity index 75% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/switch_backup.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/feedback.ini index 81e49483..139b81a8 100644 --- a/obci/scenarios/visual_audio_no_text_image_highlight_configs/switch_backup.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/feedback.ini @@ -6,10 +6,10 @@ [external_params] [local_params] +hello_message=Start BCI, wysłuchaj pytania i skup się na odpowiedzi experiment_uuid = console_log_level = info log_dir = ~/.obci/logs -sentry_log_level = error mx_log_level = info file_log_level = debug -finish_saving = 1 +sentry_log_level = error diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/info_saver.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/info_saver.ini similarity index 100% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/info_saver.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/info_saver.ini diff --git a/obci/scenarios/visual_audio_image_highlight_configs/logic.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/logic.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/logic.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/logic.ini diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/mx.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/mx.ini similarity index 100% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/mx.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/mx.ini diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/signal_saver.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/signal_saver.ini similarity index 100% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/signal_saver.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/signal_saver.ini diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/switch.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/switch.ini similarity index 100% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/switch.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/switch.ini diff --git a/obci/scenarios/visual_audio_image_highlight_configs/switch_backup.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/switch_backup.ini similarity index 100% rename from obci/scenarios/visual_audio_image_highlight_configs/switch_backup.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/switch_backup.ini diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/tag_saver.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/tag_saver.ini similarity index 100% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/tag_saver.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/tag_saver.ini diff --git a/obci/scenarios/visual_audio_image_highlight_configs/ugm_engine.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine.ini similarity index 65% rename from obci/scenarios/visual_audio_image_highlight_configs/ugm_engine.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine.ini index b30579e3..47bb60fc 100644 --- a/obci/scenarios/visual_audio_image_highlight_configs/ugm_engine.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine.ini @@ -7,24 +7,25 @@ logic = [external_params] [local_params] -ugm_config = labyrinth_face_highlight +ugm_config = budzik_visual_p300_2class blink_ugm_id_start = 1001 -experiment_uuid = +modalities = visual +running_on_start = 0 +haptic_duration = 0.8 blink_ugm_key = font_color +sentry_log_level = error +blink_ugm_value = #E42525 +log_dir = ~/.obci/logs blink_count_min = 64 -blink_max_break = 1.12 -modalities = visual;auditory -blink_min_break = 1.08 blink_count_max = 80 -blink_ugm_value = #E42525 +global_target_proba = 0.3 console_log_level = info -blink_id_count = 3 -blink_duration = 0.3 -blink_ugm_type = singletextimageoddball -sentry_log_level = error -mx_log_level = info file_log_level = debug -active_field_ids = 0;2;5 -haptic_duration = 0.8 -blink_ugm_id_count = 3 -log_dir = ~/.obci/logs +mx_log_level = info +experiment_uuid = +active_field_ids = 0;1 +blink_max_break = 0.22 +blink_min_break = 0.18 +blink_id_count = 2 +blink_ugm_id_count = 2 +blink_ugm_type = singletextimageoddball diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_server.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_server.ini similarity index 100% rename from obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_server.ini rename to obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_server.ini diff --git a/obci/scenarios/visual_audio_no_text_image_highlight.ini b/obci/scenarios/visual_audio_no_text_image_highlight.ini deleted file mode 100644 index 1b61d499..00000000 --- a/obci/scenarios/visual_audio_no_text_image_highlight.ini +++ /dev/null @@ -1,110 +0,0 @@ -[peers] -scenario_dir = - -[peers.switch_backup] -path = interfaces/switch/backup/switch_backup_peer.py -config = scenarios/visual_audio_no_text_image_highlight_configs/switch_backup.ini - -[peers.feedback] -path = logic/feedback/logic_decision_feedback_peer.py -config = scenarios/visual_audio_no_text_image_highlight_configs/feedback.ini - -[peers.feedback.config_sources] -ugm_engine = ugm_engine -analysis = analysis -logic = logic - -[peers.feedback.launch_dependencies] -ugm_server = ugm_server -ugm_engine = ugm_engine -analysis = analysis -logic = logic - -[peers.tag_saver] -path = acquisition/tag_saver_peer.py -config = scenarios/visual_audio_no_text_image_highlight_configs/tag_saver.ini - -[peers.tag_saver.config_sources] -signal_saver = signal_saver - -[peers.tag_saver.launch_dependencies] -signal_saver = signal_saver - -[peers.amplifier] -path = drivers/eeg/amplifier_virtual.py -config = scenarios/visual_audio_no_text_image_highlight_configs/amplifier.ini - -[peers.config_server] -path = control/peer/config_server.py -config = scenarios/visual_audio_no_text_image_highlight_configs/config_server.ini - -[peers.analysis] -path = interfaces/bci/p300_MD/p300_master_peer.py -config = scenarios/visual_audio_no_text_image_highlight_configs/analysis.ini - -[peers.analysis.config_sources] -amplifier = amplifier -logic = logic - -[peers.analysis.launch_dependencies] -amplifier = amplifier -logic = logic - -[peers.switch] -path = drivers/switch/switch_amplifier_peer.py -config = scenarios/visual_audio_no_text_image_highlight_configs/switch.ini - -[peers.switch.launch_dependencies] -ugm_engine = ugm_engine - -[peers.ugm_server] -path = gui/ugm/ugm_server_peer.py -config = scenarios/visual_audio_no_text_image_highlight_configs/ugm_server.ini - -[peers.ugm_server.config_sources] -ugm_engine = ugm_engine - -[peers.ugm_server.launch_dependencies] -ugm_engine = ugm_engine - -[peers.info_saver] -path = acquisition/info_saver_peer.py -config = scenarios/visual_audio_no_text_image_highlight_configs/info_saver.ini - -[peers.info_saver.config_sources] -signal_saver = signal_saver -amplifier = amplifier - -[peers.info_saver.launch_dependencies] -signal_saver = signal_saver -amplifier = amplifier - -[peers.logic] -path = logic/logic_maze_peer.py -config = scenarios/visual_audio_no_text_image_highlight_configs/logic.ini - -[peers.logic.launch_dependencies] -signal_saver = signal_saver -ugm = ugm_server - -[peers.signal_saver] -path = acquisition/signal_saver_peer.py -config = scenarios/visual_audio_no_text_image_highlight_configs/signal_saver.ini - -[peers.signal_saver.config_sources] -amplifier = amplifier - -[peers.signal_saver.launch_dependencies] -amplifier = amplifier - -[peers.ugm_engine] -path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py -config = scenarios/visual_audio_no_text_image_highlight_configs/ugm_engine.ini - -[peers.ugm_engine.config_sources] -logic = logic - -[peers.mx] -path = multiplexer-install/bin/mxcontrol -config = scenarios/visual_audio_no_text_image_highlight_configs/mx.ini - diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/logic.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/logic.ini deleted file mode 100644 index 02bf4bc9..00000000 --- a/obci/scenarios/visual_audio_no_text_image_highlight_configs/logic.ini +++ /dev/null @@ -1,19 +0,0 @@ - -[config_sources] - -[launch_dependencies] -signal_saver = - -[external_params] - -[local_params] -experiment_uuid = -active_field_ids = 0;2;5 -dec_count = 3 -logic_decision_config = obci.logic.configs.config_maze_rovio.Config -robot_ip = 192.168.1.18 -console_log_level = info -sentry_log_level = error -mx_log_level = info -file_log_level = debug -log_dir = ~/.obci/logs diff --git a/obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_engine.ini b/obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_engine.ini deleted file mode 100644 index 09ad4e32..00000000 --- a/obci/scenarios/visual_audio_no_text_image_highlight_configs/ugm_engine.ini +++ /dev/null @@ -1,32 +0,0 @@ - -[config_sources] -logic = - -[launch_dependencies] - -[external_params] - -[local_params] -ugm_config = labyrinth_face_highlight -blink_ugm_id_start = 2001 -images_path = ~/dataset/pictures -modalities = visual;auditory -images_extension = png -blink_ugm_key = font_color -haptic_duration = 0.8 -sentry_log_level = error -blink_ugm_value = #E42525 -log_dir = ~/.obci/logs -blink_duration = 0.3 -blink_count_min = 64 -blink_count_max = 80 -mx_log_level = info -console_log_level = info -file_log_level = debug -experiment_uuid = -active_field_ids = 0;2;5 -blink_max_break = 1.12 -blink_min_break = 1.08 -blink_id_count = 3 -blink_ugm_id_count = 3 -blink_ugm_type = singleimageoddball From a9b644cdda3b3ac4ad625a3d66eb587fb4de87c8 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 31 May 2016 01:35:01 +0200 Subject: [PATCH 19/28] adding author names --- obci/gui/ugm/blinking/ugm_blinking_count_manager.py | 2 +- obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.py | 3 +++ obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.py | 3 ++- obci/logic/feedback/logic_decision_feedback_budzik_peer.py | 3 +++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/obci/gui/ugm/blinking/ugm_blinking_count_manager.py b/obci/gui/ugm/blinking/ugm_blinking_count_manager.py index be9823f2..17b9968f 100644 --- a/obci/gui/ugm/blinking/ugm_blinking_count_manager.py +++ b/obci/gui/ugm/blinking/ugm_blinking_count_manager.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Author: # Mateusz Kruszyński - +# Marian Dovgialo from obci.utils import sequence_provider class DummyProvider(object): def get_value(self): diff --git a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.py b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.py index 2419253d..1889304c 100644 --- a/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +++ b/obci/gui/ugm/blinking/ugm_modal_blinking_engine_peer.py @@ -1,5 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# Author: +# Mateusz Kruszyński +# Marian Dovgialo import sys import thread, os diff --git a/obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.py b/obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.py index 0aa058e0..662ebe4b 100644 --- a/obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.py +++ b/obci/interfaces/bci/p300_MD/logic_p300_calibration_peer.py @@ -1,7 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # Author: -# Mateusz Kruszyński +# Mateusz Kruszyński +# Marian Dovgialo import os.path, sys, time diff --git a/obci/logic/feedback/logic_decision_feedback_budzik_peer.py b/obci/logic/feedback/logic_decision_feedback_budzik_peer.py index e3f498f6..439b4103 100644 --- a/obci/logic/feedback/logic_decision_feedback_budzik_peer.py +++ b/obci/logic/feedback/logic_decision_feedback_budzik_peer.py @@ -1,5 +1,8 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# Author: +# Mateusz Kruszyński +# Marian Dovgialo import time from multiplexer.multiplexer_constants import peers, types from obci.control.peer.configured_multiplexer_server import ConfiguredMultiplexerServer From 1dbc4f4ae7748a51a9a9ccf3ef7a9f35be88032b Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 31 May 2016 15:13:58 +0200 Subject: [PATCH 20/28] scenarios for live BCI --- obci/control/gui/presets/budzik.ini | 16 ++- .../P300_visual_2_class_budzik_amp.ini | 97 +++++++++++++++++++ ..._visual_2_class_budzik_calibration_amp.ini | 88 +++++++++++++++++ .../amplifier_cap.ini | 17 ++++ .../analysis_amp.ini | 19 ++++ 5 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_amp.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini diff --git a/obci/control/gui/presets/budzik.ini b/obci/control/gui/presets/budzik.ini index a73bc640..6366536c 100644 --- a/obci/control/gui/presets/budzik.ini +++ b/obci/control/gui/presets/budzik.ini @@ -70,16 +70,28 @@ launch_file=scenarios/budzik/prototypes/visual_audio_no_text_image_highlight.ini public_params= category=Prototypes P300 -[P300 visual 2 classes bci] +[P300 visual 2 classes bci dummy] info=run interactive p300 2 class bci with faces as highlight launch_file=scenarios/budzik/prototypes/P300_visual_2_class_budzik.ini public_params= category=Prototypes P300 visual bci -[P300 visual 2 classes bci calibration] +[P300 visual 2 classes bci calibration dummy] info=run 2 class bci calibration with faces as highlight launch_file=scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration.ini public_params= category=Prototypes P300 visual bci +[P300 visual 2 classes bci] +info=run interactive p300 2 class bci with faces as highlight +launch_file=scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini +public_params= +category=Prototypes P300 visual bci + +[P300 visual 2 classes bci calibration] +info=run 2 class bci calibration with faces as highlight +launch_file=scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_amp.ini +public_params= +category=Prototypes P300 visual bci + diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini new file mode 100644 index 00000000..6d4ca5da --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini @@ -0,0 +1,97 @@ +[peers] +scenario_dir = + +[peers.feedback] +path = logic/feedback/logic_decision_feedback_budzik_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/feedback.ini + +[peers.feedback.config_sources] +ugm_engine = ugm_engine +analysis = analysis + +[peers.feedback.launch_dependencies] +ugm_server = ugm_server +ugm_engine = ugm_engine +analysis = analysis + +[peers.tag_saver] +path = acquisition/tag_saver_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/tag_saver.ini + +[peers.tag_saver.config_sources] +signal_saver = signal_saver + +[peers.tag_saver.launch_dependencies] +signal_saver = signal_saver + +[peers.amplifier] +path = drivers/eeg/cpp_amplifiers/amplifier_tmsi.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/config_server.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini + +[peers.analysis.config_sources] +amplifier = amplifier + +[peers.analysis.launch_dependencies] +amplifier = amplifier + +[peers.ugm_server] +path = gui/ugm/ugm_server_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_server.ini + +[peers.ugm_server.config_sources] +ugm_engine = ugm_engine + +[peers.ugm_server.launch_dependencies] +ugm_engine = ugm_engine + +[peers.info_saver] +path = acquisition/info_saver_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/info_saver.ini + +[peers.info_saver.config_sources] +signal_saver = signal_saver +amplifier = amplifier + +[peers.info_saver.launch_dependencies] +signal_saver = signal_saver +amplifier = amplifier + +[peers.signal_saver] +path = acquisition/signal_saver_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/signal_saver.ini + +[peers.signal_saver.config_sources] +amplifier = amplifier + +[peers.signal_saver.launch_dependencies] +amplifier = amplifier + +[peers.ugm_engine] +path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine.ini + +[peers.ugm_engine.config_sources] +logic = + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/mx.ini + +[peers.blink_catcher] +path = utils/blink_catcher_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/blink_catcher.ini + +[peers.blink_catcher.config_sources] +ugm_engine = ugm_engine + +[peers.blink_catcher.launch_dependencies] +ugm_engine = ugm_engine + diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_amp.ini new file mode 100644 index 00000000..ffce96b0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_amp.ini @@ -0,0 +1,88 @@ +[peers] +scenario_dir = + +[peers.tag_saver] +path = acquisition/tag_saver_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/tag_saver.ini + +[peers.tag_saver.config_sources] +signal_saver = signal_saver + +[peers.tag_saver.launch_dependencies] +signal_saver = signal_saver + +[peers.amplifier] +path = drivers/eeg/cpp_amplifiers/amplifier_tmsi.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/config_server.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini + +[peers.analysis.config_sources] +amplifier = amplifier + +[peers.analysis.launch_dependencies] +amplifier = amplifier + +[peers.ugm_server] +path = gui/ugm/ugm_server_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_server.ini + +[peers.ugm_server.config_sources] +ugm_engine = ugm_engine + +[peers.ugm_server.launch_dependencies] +ugm_engine = ugm_engine + +[peers.info_saver] +path = acquisition/info_saver_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/info_saver.ini + +[peers.info_saver.config_sources] +signal_saver = signal_saver +amplifier = amplifier + +[peers.info_saver.launch_dependencies] +signal_saver = signal_saver +amplifier = amplifier + +[peers.logic] +path = interfaces/bci/p300_MD/logic_p300_calibration_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini + +[peers.logic.config_sources] +ugm_engine = ugm_engine +analysis = analysis + +[peers.logic.launch_dependencies] +signal_saver = signal_saver +ugm_server = ugm_server +ugm_engine = ugm_engine +analysis = analysis + +[peers.signal_saver] +path = acquisition/signal_saver_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/signal_saver.ini + +[peers.signal_saver.config_sources] +amplifier = amplifier + +[peers.signal_saver.launch_dependencies] +amplifier = amplifier + +[peers.ugm_engine] +path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine.ini + +[peers.ugm_engine.config_sources] +logic = + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/mx.ini + diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini new file mode 100644 index 00000000..80e35ee7 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini @@ -0,0 +1,17 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +active_channels=0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22 +channel_names=Fp1;Fpz;Fp2;F7;F3;Fz;F4;F8;M1;T7;C3;Cz;C4;T8;M2;P7;P3;Pz;P4;P8;O1;Oz;O2 +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sampling_rate = 512 +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini new file mode 100644 index 00000000..bb4eb6c1 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini @@ -0,0 +1,19 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +channels_for_classification = O1;O2;Pz;Cz +montage_channels=M1;M2 +log_dir = ~/.obci/logs +offline_learning = 0 +mx_log_level = info +file_log_level = debug +downsample_to = 24 +sentry_log_level = error +calibration_field_index = 0 From 07074dee4185f9e88598004c79565662403128f6 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 31 May 2016 15:43:44 +0200 Subject: [PATCH 21/28] tweaked live scenarios with Einstein --- obci/gui/ugm/resources/einstein.png | Bin 0 -> 171963 bytes .../P300_visual_2_class_budzik_amp.ini | 5 +-- ..._visual_2_class_budzik_calibration_amp.ini | 3 +- .../analysis_amp.ini | 2 +- .../ugm_engine_amp.ini | 34 ++++++++++++++++++ .../analysis_amp.ini | 19 ++++++++++ .../ugm_engine_amp.ini | 33 +++++++++++++++++ 7 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 obci/gui/ugm/resources/einstein.png create mode 100644 obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine_amp.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/analysis_amp.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine_amp.ini diff --git a/obci/gui/ugm/resources/einstein.png b/obci/gui/ugm/resources/einstein.png new file mode 100644 index 0000000000000000000000000000000000000000..9a6a6100f816a0b0491b8caf5633f174bb9d0232 GIT binary patch literal 171963 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

V6m1PvUI_tk691Iy`hY#C`|85g(h8{It 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

amP_J8+6w8urw2t5T@rtc0oubzQU>Z+H_) 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 Y32Nk&SND+?O9VWu%uZuVG5GlZ0aG@rHUIzs literal 0 HcmV?d00001 diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini index 6d4ca5da..ad0e0656 100644 --- a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini @@ -25,6 +25,7 @@ signal_saver = signal_saver signal_saver = signal_saver [peers.amplifier] +;path=drivers/eeg/amplifier_virtual.py path = drivers/eeg/cpp_amplifiers/amplifier_tmsi.py config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini @@ -34,7 +35,7 @@ config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/config_s [peers.analysis] path = interfaces/bci/p300_MD/p300_master_peer.py -config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/analysis_amp.ini [peers.analysis.config_sources] amplifier = amplifier @@ -76,7 +77,7 @@ amplifier = amplifier [peers.ugm_engine] path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py -config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine_amp.ini [peers.ugm_engine.config_sources] logic = diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_amp.ini index ffce96b0..17e58582 100644 --- a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_amp.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_amp.ini @@ -13,6 +13,7 @@ signal_saver = signal_saver [peers.amplifier] path = drivers/eeg/cpp_amplifiers/amplifier_tmsi.py +;path=drivers/eeg/amplifier_virtual.py config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini [peers.config_server] @@ -77,7 +78,7 @@ amplifier = amplifier [peers.ugm_engine] path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py -config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine.ini +config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine_amp.ini [peers.ugm_engine.config_sources] logic = diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini index bb4eb6c1..2cd0a21e 100644 --- a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/analysis_amp.ini @@ -8,7 +8,7 @@ [local_params] experiment_uuid = console_log_level = info -channels_for_classification = O1;O2;Pz;Cz +channels_for_classification = O1;O2;Pz;Cz;M1;M2 montage_channels=M1;M2 log_dir = ~/.obci/logs offline_learning = 0 diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine_amp.ini new file mode 100644 index 00000000..90969b5d --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine_amp.ini @@ -0,0 +1,34 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +blink_duration=0.15 +target_image_path=obci.gui.ugm.resources.einstein.png +ugm_config = budzik_visual_p300_2class +blink_ugm_id_start = 1001 +modalities = visual +running_on_start = 0 +haptic_duration = 0.8 +blink_ugm_key = font_color +blink_count_type = random +sentry_log_level = error +blink_ugm_value = #E42525 +log_dir = ~/.obci/logs +blink_count_min = 5 +blink_count_max = 10 +global_target_proba = 0.3 +console_log_level = info +file_log_level = debug +mx_log_level = info +experiment_uuid = +active_field_ids = 0;1 +blink_max_break = 0.22 +blink_min_break = 0.18 +blink_id_count = 2 +blink_ugm_id_count = 2 +blink_ugm_type = singletextimageoddball diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/analysis_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/analysis_amp.ini new file mode 100644 index 00000000..9c5482cd --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/analysis_amp.ini @@ -0,0 +1,19 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +channels_for_classification = O1;O2;Pz;Cz;M1;M2 +montage_channels=M1;M2 +log_dir = ~/.obci/logs +offline_learning = 0 +mx_log_level = info +file_log_level = debug +downsample_to = 24 +sentry_log_level = error +calibration_field_index = diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine_amp.ini new file mode 100644 index 00000000..d0c69fe4 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine_amp.ini @@ -0,0 +1,33 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +blink_duration=0.15 +target_image_path=obci.gui.ugm.resources.einstein.png +ugm_config = budzik_visual_p300_2class +blink_ugm_id_start = 1001 +modalities = visual +running_on_start = 0 +haptic_duration = 0.8 +blink_ugm_key = font_color +sentry_log_level = error +blink_ugm_value = #E42525 +log_dir = ~/.obci/logs +blink_count_min = 64 +blink_count_max = 80 +global_target_proba = 0.3 +console_log_level = info +file_log_level = debug +mx_log_level = info +experiment_uuid = +active_field_ids = 0;1 +blink_max_break = 0.22 +blink_min_break = 0.18 +blink_id_count = 2 +blink_ugm_id_count = 2 +blink_ugm_type = singletextimageoddball From e831b2490dbd8d8e0a0729eb03ce85dc1d128144 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Fri, 3 Jun 2016 01:42:23 +0200 Subject: [PATCH 22/28] fixing calibration length --- .../logic.ini | 2 +- .../ugm_engine_amp.ini | 6 +++--- .../P300_visual_2_class_budzik_configs/ugm_engine_amp.ini | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini index 64550eab..aa53a7c0 100644 --- a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini @@ -12,5 +12,5 @@ console_log_level = info sentry_log_level = error mx_log_level = info file_log_level = debug -trials_count = 4 +trials_count = 6 log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine_amp.ini index 90969b5d..2ccbd9d3 100644 --- a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine_amp.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/ugm_engine_amp.ini @@ -19,9 +19,9 @@ blink_count_type = random sentry_log_level = error blink_ugm_value = #E42525 log_dir = ~/.obci/logs -blink_count_min = 5 -blink_count_max = 10 -global_target_proba = 0.3 +blink_count_min = 10 +blink_count_max = 15 +global_target_proba = 0.4 console_log_level = info file_log_level = debug mx_log_level = info diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine_amp.ini index d0c69fe4..118157e8 100644 --- a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine_amp.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/ugm_engine_amp.ini @@ -20,7 +20,7 @@ blink_ugm_value = #E42525 log_dir = ~/.obci/logs blink_count_min = 64 blink_count_max = 80 -global_target_proba = 0.3 +global_target_proba = 0.4 console_log_level = info file_log_level = debug mx_log_level = info From 2c7a0f39835102af712a6b42f49adeaccb5e0fa5 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 7 Jun 2016 16:55:01 +0200 Subject: [PATCH 23/28] haptic scenarios --- obci/control/gui/presets/budzik.ini | 10 ++ .../P300_haptic_2_class_budzik_amp.ini | 98 +++++++++++++++++++ ..._haptic_2_class_budzik_calibration_amp.ini | 89 +++++++++++++++++ .../amplifier.ini | 17 ++++ .../amplifier_cap.ini | 17 ++++ .../analysis.ini | 18 ++++ .../analysis_amp.ini | 19 ++++ .../blink_catcher.ini | 14 +++ .../config_server.ini | 14 +++ .../feedback.ini | 15 +++ .../info_saver.ini | 14 +++ .../logic.ini | 16 +++ .../mx.ini | 14 +++ .../signal_saver.ini | 15 +++ .../tag_saver.ini | 14 +++ .../ugm_engine.ini | 32 ++++++ .../ugm_engine_amp.ini | 34 +++++++ .../ugm_server.ini | 14 +++ .../amplifier.ini | 17 ++++ .../analysis.ini | 17 ++++ .../analysis_amp.ini | 19 ++++ .../blink_catcher.ini | 14 +++ .../config_server.ini | 14 +++ .../feedback.ini | 15 +++ .../info_saver.ini | 14 +++ .../logic.ini | 19 ++++ .../P300_haptic_2_class_budzik_configs/mx.ini | 14 +++ .../signal_saver.ini | 15 +++ .../switch.ini | 14 +++ .../switch_backup.ini | 15 +++ .../tag_saver.ini | 14 +++ .../ugm_engine.ini | 31 ++++++ .../ugm_engine_amp.ini | 33 +++++++ .../ugm_server.ini | 14 +++ 34 files changed, 743 insertions(+) create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_amp.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_amp.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier_cap.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/analysis.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/analysis_amp.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/blink_catcher.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/config_server.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/feedback.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/info_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/logic.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/mx.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/signal_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/tag_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_engine.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_engine_amp.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_server.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/amplifier.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/analysis.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/analysis_amp.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/blink_catcher.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/config_server.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/feedback.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/info_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/logic.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/mx.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/signal_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/switch.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/switch_backup.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/tag_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_engine.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_engine_amp.ini create mode 100644 obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_server.ini diff --git a/obci/control/gui/presets/budzik.ini b/obci/control/gui/presets/budzik.ini index 6366536c..0b0f6cf2 100644 --- a/obci/control/gui/presets/budzik.ini +++ b/obci/control/gui/presets/budzik.ini @@ -94,4 +94,14 @@ launch_file=scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_a public_params= category=Prototypes P300 visual bci +[P300 haptic 2 classes bci] +info=run interactive p300 2 class bci with haptic stim +launch_file=scenarios/budzik/prototypes/P300_haptic_2_class_budzik_amp.ini +public_params= +category=Prototypes P300 haptic bci +[P300 haptic 2 classes bci calibration] +info=run 2 class bci calibration with haptic stim +launch_file=scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_amp.ini +public_params= +category=Prototypes P300 haptic bci diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_amp.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_amp.ini new file mode 100644 index 00000000..28604353 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_amp.ini @@ -0,0 +1,98 @@ +[peers] +scenario_dir = + +[peers.feedback] +path = logic/feedback/logic_decision_feedback_budzik_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/feedback.ini + +[peers.feedback.config_sources] +ugm_engine = ugm_engine +analysis = analysis + +[peers.feedback.launch_dependencies] +ugm_server = ugm_server +ugm_engine = ugm_engine +analysis = analysis + +[peers.tag_saver] +path = acquisition/tag_saver_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/tag_saver.ini + +[peers.tag_saver.config_sources] +signal_saver = signal_saver + +[peers.tag_saver.launch_dependencies] +signal_saver = signal_saver + +[peers.amplifier] +;path=drivers/eeg/amplifier_virtual.py +path = drivers/eeg/cpp_amplifiers/amplifier_tmsi.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier_cap.ini + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/config_server.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/analysis_amp.ini + +[peers.analysis.config_sources] +amplifier = amplifier + +[peers.analysis.launch_dependencies] +amplifier = amplifier + +[peers.ugm_server] +path = gui/ugm/ugm_server_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_server.ini + +[peers.ugm_server.config_sources] +ugm_engine = ugm_engine + +[peers.ugm_server.launch_dependencies] +ugm_engine = ugm_engine + +[peers.info_saver] +path = acquisition/info_saver_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/info_saver.ini + +[peers.info_saver.config_sources] +signal_saver = signal_saver +amplifier = amplifier + +[peers.info_saver.launch_dependencies] +signal_saver = signal_saver +amplifier = amplifier + +[peers.signal_saver] +path = acquisition/signal_saver_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/signal_saver.ini + +[peers.signal_saver.config_sources] +amplifier = amplifier + +[peers.signal_saver.launch_dependencies] +amplifier = amplifier + +[peers.ugm_engine] +path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_engine_amp.ini + +[peers.ugm_engine.config_sources] +logic = + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/mx.ini + +[peers.blink_catcher] +path = utils/blink_catcher_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/blink_catcher.ini + +[peers.blink_catcher.config_sources] +ugm_engine = ugm_engine + +[peers.blink_catcher.launch_dependencies] +ugm_engine = ugm_engine + diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_amp.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_amp.ini new file mode 100644 index 00000000..cb9c10c1 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_amp.ini @@ -0,0 +1,89 @@ +[peers] +scenario_dir = + +[peers.tag_saver] +path = acquisition/tag_saver_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/tag_saver.ini + +[peers.tag_saver.config_sources] +signal_saver = signal_saver + +[peers.tag_saver.launch_dependencies] +signal_saver = signal_saver + +[peers.amplifier] +;path = drivers/eeg/cpp_amplifiers/amplifier_tmsi.py +path=drivers/eeg/amplifier_virtual.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier_cap.ini + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/config_server.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/analysis_amp.ini + +[peers.analysis.config_sources] +amplifier = amplifier + +[peers.analysis.launch_dependencies] +amplifier = amplifier + +[peers.ugm_server] +path = gui/ugm/ugm_server_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_server.ini + +[peers.ugm_server.config_sources] +ugm_engine = ugm_engine + +[peers.ugm_server.launch_dependencies] +ugm_engine = ugm_engine + +[peers.info_saver] +path = acquisition/info_saver_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/info_saver.ini + +[peers.info_saver.config_sources] +signal_saver = signal_saver +amplifier = amplifier + +[peers.info_saver.launch_dependencies] +signal_saver = signal_saver +amplifier = amplifier + +[peers.logic] +path = interfaces/bci/p300_MD/logic_p300_calibration_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/logic.ini + +[peers.logic.config_sources] +ugm_engine = ugm_engine +analysis = analysis + +[peers.logic.launch_dependencies] +signal_saver = signal_saver +ugm_server = ugm_server +ugm_engine = ugm_engine +analysis = analysis + +[peers.signal_saver] +path = acquisition/signal_saver_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/signal_saver.ini + +[peers.signal_saver.config_sources] +amplifier = amplifier + +[peers.signal_saver.launch_dependencies] +amplifier = amplifier + +[peers.ugm_engine] +path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_engine_amp.ini + +[peers.ugm_engine.config_sources] +logic = + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/mx.ini + diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier.ini new file mode 100644 index 00000000..2fa97c86 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier.ini @@ -0,0 +1,17 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +active_channels = 0;1;2;3;4;5;6;7;8;Saw;Driver_Saw +channel_names = PO7;O1;Oz;O2;PO8;PO3;PO4;Pz;Cz;AmpSaw;DriverSaw +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sampling_rate = 512 +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier_cap.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier_cap.ini new file mode 100644 index 00000000..80e35ee7 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier_cap.ini @@ -0,0 +1,17 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +active_channels=0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22 +channel_names=Fp1;Fpz;Fp2;F7;F3;Fz;F4;F8;M1;T7;C3;Cz;C4;T8;M2;P7;P3;Pz;P4;P8;O1;Oz;O2 +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sampling_rate = 512 +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/analysis.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/analysis.ini new file mode 100644 index 00000000..7cb4bad7 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/analysis.ini @@ -0,0 +1,18 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +channels_for_classification = C3;C4;Pz;Cz +log_dir = ~/.obci/logs +offline_learning = 0 +mx_log_level = info +file_log_level = debug +downsample_to = 24 +sentry_log_level = error +calibration_field_index = 0 diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/analysis_amp.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/analysis_amp.ini new file mode 100644 index 00000000..74ecc552 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/analysis_amp.ini @@ -0,0 +1,19 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +channels_for_classification = C3;C4;Pz;Cz +montage_channels=M1;M2 +log_dir = ~/.obci/logs +offline_learning = 0 +mx_log_level = info +file_log_level = debug +downsample_to = 24 +sentry_log_level = error +calibration_field_index = 0 diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/blink_catcher.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/blink_catcher.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/blink_catcher.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/config_server.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/config_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/config_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/feedback.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/feedback.ini new file mode 100644 index 00000000..a26604ca --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/feedback.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +hello_message = Start BCI, wysłuchaj pytania i skup się na odpowiedzi +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/info_saver.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/info_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/info_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/logic.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/logic.ini new file mode 100644 index 00000000..aa53a7c0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/logic.ini @@ -0,0 +1,16 @@ + +[config_sources] + +[launch_dependencies] +signal_saver = + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +trials_count = 6 +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/mx.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/mx.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/mx.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/signal_saver.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/signal_saver.ini new file mode 100644 index 00000000..cd1348d6 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/signal_saver.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +save_file_name = test2 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/tag_saver.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/tag_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/tag_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_engine.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_engine.ini new file mode 100644 index 00000000..33f187ae --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_engine.ini @@ -0,0 +1,32 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +ugm_config = budzik_visual_p300_2class +blink_ugm_id_start = 1001 +modalities = visual +running_on_start = 0 +haptic_duration = 0.8 +blink_ugm_key = font_color +blink_count_type = random +sentry_log_level = error +blink_ugm_value = #E42525 +log_dir = ~/.obci/logs +blink_count_min = 5 +blink_count_max = 10 +global_target_proba = 0.3 +console_log_level = info +file_log_level = debug +mx_log_level = info +experiment_uuid = +active_field_ids = 0;1 +blink_max_break = 0.22 +blink_min_break = 0.18 +blink_id_count = 2 +blink_ugm_id_count = 2 +blink_ugm_type = singletextimageoddball diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_engine_amp.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_engine_amp.ini new file mode 100644 index 00000000..fdc26e1f --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_engine_amp.ini @@ -0,0 +1,34 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +blink_duration=0.15 +target_image_path=obci.gui.ugm.resources.einstein.png +ugm_config = budzik_visual_p300_2class +blink_ugm_id_start = 1001 +modalities = haptic +running_on_start = 0 +haptic_duration = 0.15 +blink_ugm_key = font_color +blink_count_type = random +sentry_log_level = error +blink_ugm_value = #E42525 +log_dir = ~/.obci/logs +blink_count_min = 10 +blink_count_max = 15 +global_target_proba = 1 +console_log_level = info +file_log_level = debug +mx_log_level = info +experiment_uuid = +active_field_ids = 0;1 +blink_max_break = 0.22 +blink_min_break = 0.18 +blink_id_count = 2 +blink_ugm_id_count = 2 +blink_ugm_type = singletextimageoddball diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_server.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/ugm_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/amplifier.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/amplifier.ini new file mode 100644 index 00000000..2fa97c86 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/amplifier.ini @@ -0,0 +1,17 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +active_channels = 0;1;2;3;4;5;6;7;8;Saw;Driver_Saw +channel_names = PO7;O1;Oz;O2;PO8;PO3;PO4;Pz;Cz;AmpSaw;DriverSaw +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sampling_rate = 512 +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/analysis.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/analysis.ini new file mode 100644 index 00000000..9489a15b --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/analysis.ini @@ -0,0 +1,17 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +channels_for_classification = O1;O2;Pz;Cz +log_dir = ~/.obci/logs +offline_learning = 0 +mx_log_level = info +file_log_level = debug +downsample_to = 24 +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/analysis_amp.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/analysis_amp.ini new file mode 100644 index 00000000..1ae308ea --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/analysis_amp.ini @@ -0,0 +1,19 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +channels_for_classification = C3;C4;Pz;Cz +montage_channels=M1;M2 +log_dir = ~/.obci/logs +offline_learning = 0 +mx_log_level = info +file_log_level = debug +downsample_to = 24 +sentry_log_level = error +calibration_field_index = diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/blink_catcher.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/blink_catcher.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/blink_catcher.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/config_server.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/config_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/config_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/feedback.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/feedback.ini new file mode 100644 index 00000000..139b81a8 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/feedback.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +hello_message=Start BCI, wysłuchaj pytania i skup się na odpowiedzi +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/info_saver.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/info_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/info_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/logic.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/logic.ini new file mode 100644 index 00000000..02bf4bc9 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/logic.ini @@ -0,0 +1,19 @@ + +[config_sources] + +[launch_dependencies] +signal_saver = + +[external_params] + +[local_params] +experiment_uuid = +active_field_ids = 0;2;5 +dec_count = 3 +logic_decision_config = obci.logic.configs.config_maze_rovio.Config +robot_ip = 192.168.1.18 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/mx.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/mx.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/mx.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/signal_saver.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/signal_saver.ini new file mode 100644 index 00000000..cd1348d6 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/signal_saver.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +save_file_name = test2 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/switch.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/switch.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/switch.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/switch_backup.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/switch_backup.ini new file mode 100644 index 00000000..81e49483 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/switch_backup.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +sentry_log_level = error +mx_log_level = info +file_log_level = debug +finish_saving = 1 diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/tag_saver.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/tag_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/tag_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_engine.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_engine.ini new file mode 100644 index 00000000..47bb60fc --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_engine.ini @@ -0,0 +1,31 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +ugm_config = budzik_visual_p300_2class +blink_ugm_id_start = 1001 +modalities = visual +running_on_start = 0 +haptic_duration = 0.8 +blink_ugm_key = font_color +sentry_log_level = error +blink_ugm_value = #E42525 +log_dir = ~/.obci/logs +blink_count_min = 64 +blink_count_max = 80 +global_target_proba = 0.3 +console_log_level = info +file_log_level = debug +mx_log_level = info +experiment_uuid = +active_field_ids = 0;1 +blink_max_break = 0.22 +blink_min_break = 0.18 +blink_id_count = 2 +blink_ugm_id_count = 2 +blink_ugm_type = singletextimageoddball diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_engine_amp.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_engine_amp.ini new file mode 100644 index 00000000..6dcfb346 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_engine_amp.ini @@ -0,0 +1,33 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +blink_duration=0.15 +target_image_path=obci.gui.ugm.resources.einstein.png +ugm_config = budzik_visual_p300_2class +blink_ugm_id_start = 1001 +modalities = haptic +running_on_start = 0 +haptic_duration = 0.15 +blink_ugm_key = font_color +sentry_log_level = error +blink_ugm_value = #E42525 +log_dir = ~/.obci/logs +blink_count_min = 64 +blink_count_max = 80 +global_target_proba = 1 +console_log_level = info +file_log_level = debug +mx_log_level = info +experiment_uuid = +active_field_ids = 0;1 +blink_max_break = 0.22 +blink_min_break = 0.18 +blink_id_count = 2 +blink_ugm_id_count = 2 +blink_ugm_type = singletextimageoddball diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_server.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/ugm_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error From 32e61b4b517e2c982ac4f4437d89c382adf8cc19 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Tue, 7 Jun 2016 17:29:30 +0200 Subject: [PATCH 24/28] 8 class visual p300 --- obci/control/gui/presets/budzik.ini | 7 ++ .../ugm/configs/budzik_visual_p300_8class.ugm | Bin 0 -> 2915 bytes .../bci/p300_MD/tests/synthetic_generator.py | 29 ++++-- .../P300_visual_2_class_budzik_amp.ini | 14 +-- .../logic.ini | 2 +- .../prototypes/p300_visual_8_classes.ini | 97 ++++++++++++++++++ .../amplifier.ini | 17 +++ .../analysis.ini | 19 ++++ .../blink_catcher.ini | 14 +++ .../config_server.ini | 14 +++ .../feedback.ini | 15 +++ .../info_saver.ini | 14 +++ .../p300_visual_8_classes_configs/mx.ini | 14 +++ .../signal_saver.ini | 15 +++ .../tag_saver.ini | 14 +++ .../ugm_engine.ini | 29 ++++++ .../ugm_server.ini | 14 +++ 17 files changed, 309 insertions(+), 19 deletions(-) create mode 100644 obci/gui/ugm/configs/budzik_visual_p300_8class.ugm create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/amplifier.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/analysis.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/blink_catcher.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/config_server.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/feedback.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/info_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/mx.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/signal_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/tag_saver.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_engine.ini create mode 100644 obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_server.ini diff --git a/obci/control/gui/presets/budzik.ini b/obci/control/gui/presets/budzik.ini index 6366536c..2ccc8747 100644 --- a/obci/control/gui/presets/budzik.ini +++ b/obci/control/gui/presets/budzik.ini @@ -88,6 +88,13 @@ launch_file=scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini public_params= category=Prototypes P300 visual bci +[P300 visual 8 classes bci] +info=run interactive p300 8 class bci with faces as highlight +launch_file=scenarios/budzik/prototypes/p300_visual_8_classes.ini +public_params= +category=Prototypes P300 visual bci + + [P300 visual 2 classes bci calibration] info=run 2 class bci calibration with faces as highlight launch_file=scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_amp.ini diff --git a/obci/gui/ugm/configs/budzik_visual_p300_8class.ugm b/obci/gui/ugm/configs/budzik_visual_p300_8class.ugm new file mode 100644 index 0000000000000000000000000000000000000000..062cf6f12c41d071014477d7d8ab21308e0db37a GIT binary patch literal 2915 zcmb8w`FB%A6bJCqv`H5bOQ2LiMbS2*BKxL@MG>(CH%39N@im!d9=_z|=1mH%RB*+8 z-}imr759Aw7k1De{n1_i1nwju@7`xPV0wB^Pv_-(@BPeVvbMGxY6f9QOf;>uQ(7f- ze@;TJXvj;)(srMOkZ4xYPAgqnsv}GFqCV+5ZXUuSGJaC=1rZU=xt^_UH=9sy-X3tX z+H#OVk&OLX-cWwkq!~Rc;myR*&a81ubEyX!5k3DL4gBdN8aT z4vG!J@R*{FAFODvEs8WbLJZsc_EL6KH0EVeTiLWDVWeshb&l-SFiK&szm|Gc*yNV- zpUom-Id(cLQ_!lg`e>})788xVIBdeR2P7QQmBN&8Qf9Tx!=WlF>a<*?VT@=lKGbVv zY^NW_is6gx9+^jKH#;uwW?eW;k@Uk&`Vpe3_@0vWIJBPGX@1~H5y?o;v(gfdS{B0I zmUec*c+pZJ?2~zICz1c?N-M{RS~~^p@!vzD^*_&*FgpC{Vue9C7SD#A!6J#Ar4>vN zHEeNzBQsq`s9Md^^kURFa;M|I_Tn5EIp0y=ybND zGw6=aR8fWd=5NuyHqJlTvrL1tap&g56yL6sFl>w(tJqeYY#yJh2>&Unq`qX)0+>ez zJr!s9BSdHE#98LY)C5K0PQxlp>~vFM0SP(-gSIiTg}&ICxHy7|Ey5~iaj`BYwwQ{= zao@oo#wO2l37g;+*uQ~$9pL~Bf_LyKus@1(;;}0C&MxVu^eap zlZjr0vtAq&y~IC?-L%Z5rc8q5U50tv8LZ{Q_TXZa!6va-io;3<+e=~7999|F-^v%q z{$Xv?>k5RuJP7MlU^CTVvvgwDq~s_plwqepot763+580=aKdliShTH%+&$VE+~3^MRZ*? z(HrQfH=2~2sOZgQ(W!6?5x5nH{F{m1hC|*S6urY2y_1%?%apmB#5YcWvS`#9vSd`9N=Fj^#Km>VNmL$pw!1E{}U4Q zDF*$_q&{O(pX2&yCiMjt`jSh1#iYKbQU`w9ePdet7Nx!mN^Phnwb4BOJ(c>QEHwju zBm@401MFr}n{a^5L8&c4sjVh|8wuKuLBBAm9Y)IkrfiS587Tj@*=g4CUTVkR4mGr3 GP22+%n6Tjh literal 0 HcmV?d00001 diff --git a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py index 820d7add..e3f1d3df 100644 --- a/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py +++ b/obci/interfaces/bci/p300_MD/tests/synthetic_generator.py @@ -99,16 +99,17 @@ def load_meta(self): def send_nontarget_blink(self,): - with self.lock: - self.logger.info('sending distractor blink') + #~ with self.lock: + #~ self.logger.info('sending distractor blink') choice = random.choice(tuple(set(self.fields)-set([self.focus]))) b = variables_pb2.Blink() timestamp = self.time self.time_of_blink=timestamp b.timestamp=timestamp b.index=choice + self.conn.send_message(message = b.SerializeToString(), - type = types.BLINK_MESSAGE, flush=True) + type = types.BLINK_MESSAGE, flush=True) def send_target_blink(self, timestamp=None): b = variables_pb2.Blink() @@ -117,6 +118,7 @@ def send_target_blink(self, timestamp=None): self.time_of_blink=timestamp b.timestamp=timestamp b.index=self.focus + self.conn.send_message(message = b.SerializeToString(), type = types.BLINK_MESSAGE, flush=True) @@ -143,6 +145,7 @@ def send_isi(self): s.channels.extend(signal[:, ind].tolist()) s.timestamp = self.time self.time+=1.0/self.sampling_rate + self.conn.send_message(message = sv.SerializeToString(), type = types.AMPLIFIER_SIGNAL_MESSAGE, flush=True) if (self.time-self.time_of_blink)>self.isi: @@ -156,9 +159,9 @@ def send_isi(self): def send_target(self): - with self.lock: - self.logger.info('Sending target, focus: {}'.format(self.focus)) - self.sent_targets+=1 + #~ with self.lock: + #~ self.logger.info('Sending target, focus: {}'.format(self.focus)) + self.sent_targets+=1 length_window = int(self.window*self.sampling_rate) length_packet_aligned = length_window - length_window % self.samples_per_packet gauss_w = gaussian(length_packet_aligned, length_packet_aligned/10) @@ -188,8 +191,9 @@ def send_target(self): s.channels.extend(signal[:,i*self.samples_per_packet+sn].tolist()) s.timestamp = self.time self.time+=1.0/self.sampling_rate + #~ with self.lock: self.conn.send_message(message = sv.SerializeToString(), - type = types.AMPLIFIER_SIGNAL_MESSAGE, flush=True) + type = types.AMPLIFIER_SIGNAL_MESSAGE, flush=True) if (self.time-self.time_of_blink)>self.isi: self.send_nontarget_blink() left_to_sleep = sleeping_time-(time.time()-t0) @@ -198,14 +202,16 @@ def send_target(self): @log_crash def run(self): + #~ with self.lock: time.sleep(self.delay) for i in xrange(self.test_trials_n): for k in xrange(len(self.fields)-1): - + #~ with self.lock: self.send_isi() + #~ with self.lock: self.send_target() - with self.lock: - self.save_statistics() + + self.save_statistics() sys.exit(0) def save_statistics(self): @@ -245,7 +251,8 @@ def handle_message(self, mxmsg): self.sent_targets = 0 if self.learning == 0: self.focus=random.choice(self.fields) - self.no_response() + with self.lock: + self.no_response() diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini index ad0e0656..45835934 100644 --- a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_amp.ini @@ -86,13 +86,13 @@ logic = path = multiplexer-install/bin/mxcontrol config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/mx.ini -[peers.blink_catcher] -path = utils/blink_catcher_peer.py -config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/blink_catcher.ini +;[peers.blink_catcher] +;path = utils/blink_catcher_peer.py +;config = scenarios/budzik/prototypes/P300_visual_2_class_budzik_configs/blink_catcher.ini -[peers.blink_catcher.config_sources] -ugm_engine = ugm_engine +;[peers.blink_catcher.config_sources] +;ugm_engine = ugm_engine -[peers.blink_catcher.launch_dependencies] -ugm_engine = ugm_engine +;[peers.blink_catcher.launch_dependencies] +;ugm_engine = ugm_engine diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini index aa53a7c0..cd28fc51 100644 --- a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/logic.ini @@ -12,5 +12,5 @@ console_log_level = info sentry_log_level = error mx_log_level = info file_log_level = debug -trials_count = 6 +trials_count = 12 log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes.ini new file mode 100644 index 00000000..0e62c654 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes.ini @@ -0,0 +1,97 @@ +[peers] +scenario_dir = + +[peers.feedback] +path = logic/feedback/logic_decision_feedback_budzik_peer.py +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/feedback.ini + +[peers.feedback.config_sources] +ugm_engine = ugm_engine +analysis = analysis + +[peers.feedback.launch_dependencies] +ugm_server = ugm_server +ugm_engine = ugm_engine +analysis = analysis + +[peers.tag_saver] +path = acquisition/tag_saver_peer.py +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/tag_saver.ini + +[peers.tag_saver.config_sources] +signal_saver = signal_saver + +[peers.tag_saver.launch_dependencies] +signal_saver = signal_saver + +[peers.amplifier] +path = drivers/eeg/cpp_amplifiers/amplifier_tmsi.py +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/amplifier.ini + +[peers.config_server] +path = control/peer/config_server.py +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/config_server.ini + +[peers.analysis] +path = interfaces/bci/p300_MD/p300_master_peer.py +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/analysis.ini + +[peers.analysis.config_sources] +amplifier = amplifier + +[peers.analysis.launch_dependencies] +amplifier = amplifier + +[peers.ugm_server] +path = gui/ugm/ugm_server_peer.py +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_server.ini + +[peers.ugm_server.config_sources] +ugm_engine = ugm_engine + +[peers.ugm_server.launch_dependencies] +ugm_engine = ugm_engine + +[peers.info_saver] +path = acquisition/info_saver_peer.py +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/info_saver.ini + +[peers.info_saver.config_sources] +signal_saver = signal_saver +amplifier = amplifier + +[peers.info_saver.launch_dependencies] +signal_saver = signal_saver +amplifier = amplifier + +[peers.signal_saver] +path = acquisition/signal_saver_peer.py +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/signal_saver.ini + +[peers.signal_saver.config_sources] +amplifier = amplifier + +[peers.signal_saver.launch_dependencies] +amplifier = amplifier + +[peers.ugm_engine] +path = gui/ugm/blinking/ugm_modal_blinking_engine_peer.py +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_engine.ini + +[peers.ugm_engine.config_sources] +logic = + +[peers.mx] +path = multiplexer-install/bin/mxcontrol +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/mx.ini + +[peers.blink_catcher] +path = utils/blink_catcher_peer.py +config = scenarios/budzik/prototypes/p300_visual_8_classes_configs/blink_catcher.ini + +[peers.blink_catcher.config_sources] +ugm_engine = ugm_engine + +[peers.blink_catcher.launch_dependencies] +ugm_engine = ugm_engine + diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/amplifier.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/amplifier.ini new file mode 100644 index 00000000..6c894dcd --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/amplifier.ini @@ -0,0 +1,17 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +active_channels = 0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22 +channel_names = Fp1;Fpz;Fp2;F7;F3;Fz;F4;F8;M1;T7;C3;Cz;C4;T8;M2;P7;P3;Pz;P4;P8;O1;Oz;O2 +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sampling_rate = 512 +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/analysis.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/analysis.ini new file mode 100644 index 00000000..59199363 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/analysis.ini @@ -0,0 +1,19 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +decision_stop = 2 +console_log_level = info +channels_for_classification = O1;O2;Pz;Cz;M1;M2 +montage_channels = M1;M2 +log_dir = ~/.obci/logs +offline_learning = 0 +mx_log_level = info +file_log_level = debug +downsample_to = 24 +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/blink_catcher.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/blink_catcher.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/blink_catcher.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/config_server.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/config_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/config_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/feedback.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/feedback.ini new file mode 100644 index 00000000..a26604ca --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/feedback.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +hello_message = Start BCI, wysłuchaj pytania i skup się na odpowiedzi +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/info_saver.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/info_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/info_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/mx.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/mx.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/mx.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/signal_saver.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/signal_saver.ini new file mode 100644 index 00000000..cd1348d6 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/signal_saver.ini @@ -0,0 +1,15 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +save_file_name = test2 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/tag_saver.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/tag_saver.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/tag_saver.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_engine.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_engine.ini new file mode 100644 index 00000000..e2f4c0ba --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_engine.ini @@ -0,0 +1,29 @@ + +[config_sources] +logic = + +[launch_dependencies] + +[external_params] + +[local_params] +ugm_config = budzik_visual_p300_8class +blink_ugm_id_start = 1001 +experiment_uuid = +blink_ugm_key = font_color +target_image_path = obci.gui.ugm.resources.einstein.png +blink_max_break = 0.22 +modalities = visual +blink_min_break = 0.18 +blink_ugm_type = singletextimageoddball +blink_count_max = 80 +blink_ugm_value = #E42525 +running_on_start = 0 +blink_duration = 0.15 +console_log_level = info +sentry_log_level = error +mx_log_level = info +file_log_level = debug +blink_count_min = 64 +haptic_duration = 0.8 +log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_server.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_server.ini new file mode 100644 index 00000000..cb6640e0 --- /dev/null +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_server.ini @@ -0,0 +1,14 @@ + +[config_sources] + +[launch_dependencies] + +[external_params] + +[local_params] +experiment_uuid = +console_log_level = info +log_dir = ~/.obci/logs +mx_log_level = info +file_log_level = debug +sentry_log_level = error From 5bfdc3f181f30faac405d2f374025cf47ddb180d Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Wed, 8 Jun 2016 01:03:21 +0200 Subject: [PATCH 25/28] text correction for haptic stim --- .../P300_haptic_2_class_budzik_calibration_configs/logic.ini | 1 + .../prototypes/P300_haptic_2_class_budzik_configs/feedback.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/logic.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/logic.ini index aa53a7c0..0f502fb6 100644 --- a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/logic.ini +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/logic.ini @@ -7,6 +7,7 @@ signal_saver = [external_params] [local_params] +hi_text=Witamy. Zliczaj wibracje po lewej experiment_uuid = console_log_level = info sentry_log_level = error diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/feedback.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/feedback.ini index 139b81a8..35142b5d 100644 --- a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/feedback.ini +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_configs/feedback.ini @@ -6,7 +6,7 @@ [external_params] [local_params] -hello_message=Start BCI, wysłuchaj pytania i skup się na odpowiedzi +hello_message=Start BCI, wysłuchaj pytania i skup się na odpowiedzi lewo - TAK, prawo - NIE experiment_uuid = console_log_level = info log_dir = ~/.obci/logs From cd82a82f0bb06c7f332098b33ecc3e77d1dc3d22 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Wed, 8 Jun 2016 15:42:43 +0200 Subject: [PATCH 26/28] little fixes, more logging in analysis --- obci/interfaces/bci/p300_MD/helper_functions.py | 12 +++++++----- obci/interfaces/bci/p300_MD/p300_master_peer.ini | 1 + obci/interfaces/bci/p300_MD/p300_master_peer.py | 8 +++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/obci/interfaces/bci/p300_MD/helper_functions.py b/obci/interfaces/bci/p300_MD/helper_functions.py index f5d6db49..b6b72c64 100644 --- a/obci/interfaces/bci/p300_MD/helper_functions.py +++ b/obci/interfaces/bci/p300_MD/helper_functions.py @@ -107,15 +107,17 @@ def evoked_pair_plot_smart_tags(tags1, tags2, chnames=['O1', 'O2', 'Pz', 'PO7', ev1, std1 = evoked_from_smart_tags(tags1, chnames, start_offset) ev2, std2 = evoked_from_smart_tags(tags2, chnames, start_offset) Fs = float(tags1[0].get_param('sampling_frequency')) - time = np.linspace(0+start_offset, ev1.shape[1]/Fs+start_offset, ev1.shape[1]) + time1 = np.linspace(0+start_offset, ev1.shape[1]/Fs+start_offset, ev1.shape[1]) + time2 = np.linspace(0+start_offset, ev2.shape[1]/Fs+start_offset, ev2.shape[1]) fig = pb.figure() for nr, i in enumerate(chnames): ax = fig.add_subplot( (len(chnames)+1)/2, 2, nr+1) - ax.plot(time, ev1[nr], 'r',label = labels[0]+' N:{}'.format(len(tags1))) - ax.fill_between(time, ev1[nr]-std1[nr], ev1[nr]+std1[nr], + + ax.plot(time1, ev1[nr], 'r',label = labels[0]+' N:{}'.format(len(tags1))) + ax.fill_between(time1, ev1[nr]-std1[nr], ev1[nr]+std1[nr], color = 'red', alpha=0.3, ) - ax.plot(time, ev2[nr], 'b', label = labels[1]+' N:{}'.format(len(tags2))) - ax.fill_between(time, ev2[nr]-std2[nr], ev2[nr]+std2[nr], + ax.plot(time2, ev2[nr], 'b', label = labels[1]+' N:{}'.format(len(tags2))) + ax.fill_between(time2, ev2[nr]-std2[nr], ev2[nr]+std2[nr], color = 'blue', alpha=0.3) ax.set_title(i) diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.ini b/obci/interfaces/bci/p300_MD/p300_master_peer.ini index 8fe5fc01..b747c359 100644 --- a/obci/interfaces/bci/p300_MD/p300_master_peer.ini +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.ini @@ -1,4 +1,5 @@ [local_params] +decision_stop_threshold=0.4 wisdom_path=~/classifier.dump channels_for_classification=O1;O2;Pz montage_channels=Cz diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.py b/obci/interfaces/bci/p300_MD/p300_master_peer.py index 614b8b1e..e2c71768 100644 --- a/obci/interfaces/bci/p300_MD/p300_master_peer.py +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.py @@ -115,10 +115,13 @@ def add_result(self, blink, probabilities): ) last_single = [blink.index, probabilities['targetSingle']] last_mean = [blink.index, probabilities['targetCMean']] + self.logger.info('Last single dec: {}, proba: {:.2f}'.format( + last_single[0], + last_single[1])) self.logger.info('Last mean dec: {}, proba: {:.2f}'.format( last_mean[0], last_mean[1])) - if last_mean[1]>0.5: + if last_mean[1]>self.desision_stop_proba_thr: self.decision_buffor.append(blink.index) # enough of the same decisions condition @@ -179,6 +182,9 @@ def identify_blink(self, blink): @log_crash def init_params(self): self.logger.info('Initialasing parameters') + #configdence (probability) level for internal decision to be counted to decision stop: + self.desision_stop_proba_thr = self.config.get_param( + 'decision_stop_threshold').split(';') #available channels self.channel_names = self.config.get_param( 'channel_names').split(';') From 056521d482b40f670843c690dcf00b3675ebb672 Mon Sep 17 00:00:00 2001 From: Budzik BCI lap Date: Wed, 15 Mar 2017 16:43:34 +0100 Subject: [PATCH 27/28] Fix for not stopping decision by max dec --- .../bci/p300_MD/p300_master_peer.py | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/obci/interfaces/bci/p300_MD/p300_master_peer.py b/obci/interfaces/bci/p300_MD/p300_master_peer.py index e2c71768..0f16cb72 100644 --- a/obci/interfaces/bci/p300_MD/p300_master_peer.py +++ b/obci/interfaces/bci/p300_MD/p300_master_peer.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 # -*- coding: utf-8 -*- # # OpenBCI - framework for Brain-Computer Interfaces based on EEG signal @@ -42,6 +42,7 @@ import os.path import pickle import pylab as pb +import time from obci.gui.ugm import ugm_helper class P300MasterPeer(AnalysisMaster): @@ -61,6 +62,8 @@ def _reset_buffors(self): self.features = defaultdict(list) #final decision buffor self.decision_buffor = deque(maxlen = self.decision_stop) + self.hold_time = 1.0 + self.last_dec_timestamp = 0.0 def _prepare_chunk(self, chunk): ''' @@ -96,7 +99,8 @@ def _prepare_chunk(self, chunk): def _send_decision(self, decision): self.conn.send_message(message = str(decision), type = types.DECISION_MESSAGE, flush=True) self._reset_buffors() - + self.last_dec_timestamp = time.time() + def add_result(self, blink, probabilities): ''' Sends decision if some last decisions from cumulative mean @@ -121,12 +125,16 @@ def add_result(self, blink, probabilities): self.logger.info('Last mean dec: {}, proba: {:.2f}'.format( last_mean[0], last_mean[1])) + + if last_mean[1]>self.desision_stop_proba_thr: self.decision_buffor.append(blink.index) # enough of the same decisions condition one_decision = (len(set(self.decision_buffor)) == 1) buffor_full = (len(self.decision_buffor) == self.decision_stop) + + self.logger.info("Decision buffor:", self.decision_buffor) if one_decision and buffor_full: decision = self.decision_buffor[-1] self.logger.info('Decision by decision_stop {}'.format(decision)) @@ -183,8 +191,8 @@ def identify_blink(self, blink): def init_params(self): self.logger.info('Initialasing parameters') #configdence (probability) level for internal decision to be counted to decision stop: - self.desision_stop_proba_thr = self.config.get_param( - 'decision_stop_threshold').split(';') + self.desision_stop_proba_thr = float(self.config.get_param( + 'decision_stop_threshold').split(';')[0]) #available channels self.channel_names = self.config.get_param( 'channel_names').split(';') @@ -318,19 +326,20 @@ def classify(self, classifier, chunk, blink): or None if classification could not be performed """ - if blink.index in self.ignored_blink_ids: - return None - chunk_ready = self._prepare_chunk(chunk) - self.features[blink.index].append(chunk_ready) - probabilities = {} - probabilities['targetSingle'] = classifier.classify( - chunk_ready - ) - chunk_mean = np.array(self.features[blink.index]).mean(axis=0) - probabilities['targetCMean'] = classifier.classify( - chunk_mean - ) - return probabilities + if time.time()-self.last_dec_timestamp>self.hold_time: + if blink.index in self.ignored_blink_ids: + return None + chunk_ready = self._prepare_chunk(chunk) + self.features[blink.index].append(chunk_ready) + probabilities = {} + probabilities['targetSingle'] = classifier.classify( + chunk_ready + ) + chunk_mean = np.array(self.features[blink.index]).mean(axis=0) + probabilities['targetCMean'] = classifier.classify( + chunk_mean + ) + return probabilities def learn(self, classifier, chunk, target): """Learn that given chunk represents given target. From 8f548f3af8c6e9236bc035000dff0a8a51dd0542 Mon Sep 17 00:00:00 2001 From: mdovgialo Date: Wed, 15 Mar 2017 17:42:15 +0100 Subject: [PATCH 28/28] P300 visual bci now uses budzik cap setup --- .../haptics/haptics_stim_test_logic_peer.py | 19 +++++++++++++++++++ .../amplifier_cap.ini | 4 ++-- .../amplifier_cap.ini | 4 ++-- .../amplifier.ini | 4 ++-- .../analysis.ini | 1 + .../ugm_engine.ini | 1 + 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/obci/devices/haptics/haptics_stim_test_logic_peer.py b/obci/devices/haptics/haptics_stim_test_logic_peer.py index ec3bb2f8..e946d61b 100644 --- a/obci/devices/haptics/haptics_stim_test_logic_peer.py +++ b/obci/devices/haptics/haptics_stim_test_logic_peer.py @@ -68,6 +68,25 @@ def generate_test_messages(self): self.logger.info('RUNNING! S2') time.sleep(4) + msg = variables_pb2.Variable() + msg.key = 'S' + msg.value = '3:0.5' + self.conn.send_message(message=msg.SerializeToString(), + type=types.HAPTIC_CONTROL_MESSAGE, + flush=True) + self.logger.info('RUNNING! S3') + time.sleep(4) + + + msg = variables_pb2.Variable() + msg.key = 'S' + msg.value = '4:0.5' + self.conn.send_message(message=msg.SerializeToString(), + type=types.HAPTIC_CONTROL_MESSAGE, + flush=True) + self.logger.info('RUNNING! S4') + time.sleep(4) + msg = variables_pb2.Variable() msg.key = 'S' msg.value = '1,2:1.5,2.5' diff --git a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier_cap.ini b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier_cap.ini index 80e35ee7..8d098d22 100644 --- a/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier_cap.ini +++ b/obci/scenarios/budzik/prototypes/P300_haptic_2_class_budzik_calibration_configs/amplifier_cap.ini @@ -6,8 +6,8 @@ [external_params] [local_params] -active_channels=0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22 -channel_names=Fp1;Fpz;Fp2;F7;F3;Fz;F4;F8;M1;T7;C3;Cz;C4;T8;M2;P7;P3;Pz;P4;P8;O1;Oz;O2 +channel_names=Fp1;Fpz;Fp2;F7;F3;Fz;F4;F8;M1;T3;C3;Cz;C4;T4;M2;T5;P3;Pz;P4;T6;O1;Oz;O2;l_reka;p_reka;l_noga;eog;haptic1;haptic2;phones +active_channels=0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22;24;25;26;27;28;29;30 experiment_uuid = console_log_level = info log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini index 80e35ee7..8d098d22 100644 --- a/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini +++ b/obci/scenarios/budzik/prototypes/P300_visual_2_class_budzik_calibration_configs/amplifier_cap.ini @@ -6,8 +6,8 @@ [external_params] [local_params] -active_channels=0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22 -channel_names=Fp1;Fpz;Fp2;F7;F3;Fz;F4;F8;M1;T7;C3;Cz;C4;T8;M2;P7;P3;Pz;P4;P8;O1;Oz;O2 +channel_names=Fp1;Fpz;Fp2;F7;F3;Fz;F4;F8;M1;T3;C3;Cz;C4;T4;M2;T5;P3;Pz;P4;T6;O1;Oz;O2;l_reka;p_reka;l_noga;eog;haptic1;haptic2;phones +active_channels=0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22;24;25;26;27;28;29;30 experiment_uuid = console_log_level = info log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/amplifier.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/amplifier.ini index 6c894dcd..8d098d22 100644 --- a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/amplifier.ini +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/amplifier.ini @@ -6,8 +6,8 @@ [external_params] [local_params] -active_channels = 0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22 -channel_names = Fp1;Fpz;Fp2;F7;F3;Fz;F4;F8;M1;T7;C3;Cz;C4;T8;M2;P7;P3;Pz;P4;P8;O1;Oz;O2 +channel_names=Fp1;Fpz;Fp2;F7;F3;Fz;F4;F8;M1;T3;C3;Cz;C4;T4;M2;T5;P3;Pz;P4;T6;O1;Oz;O2;l_reka;p_reka;l_noga;eog;haptic1;haptic2;phones +active_channels=0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22;24;25;26;27;28;29;30 experiment_uuid = console_log_level = info log_dir = ~/.obci/logs diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/analysis.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/analysis.ini index 59199363..985bb7a7 100644 --- a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/analysis.ini +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/analysis.ini @@ -17,3 +17,4 @@ mx_log_level = info file_log_level = debug downsample_to = 24 sentry_log_level = error +decision_stop_threshold=0.6 diff --git a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_engine.ini b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_engine.ini index e2f4c0ba..5d613aa4 100644 --- a/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_engine.ini +++ b/obci/scenarios/budzik/prototypes/p300_visual_8_classes_configs/ugm_engine.ini @@ -27,3 +27,4 @@ file_log_level = debug blink_count_min = 64 haptic_duration = 0.8 log_dir = ~/.obci/logs +global_target_proba = 0.7