def _demo_decision_maker(): alp = ['T', 'H', 'I', 'S', 'I', 'S', 'D', 'E', 'M', 'O'] len_alp = len(alp) evidence_names = ['LM', 'ERP', 'FRP'] num_series = 10 conjugator = EvidenceFusion(evidence_names, len_dist=len_alp) decision_maker = DecisionMaker( min_num_inq=1, max_num_inq=10, state='', alphabet=alp) for idx_series in range(num_series): while True: # Generate random inquiries evidence_erp = np.abs(np.random.randn(len_alp)) evidence_erp[idx_series] += 1 evidence_frp = np.abs(np.random.randn(len_alp)) evidence_frp[idx_series] += 3 p = conjugator.update_and_fuse( {'ERP': evidence_erp, 'FRP': evidence_frp}) d, arg = decision_maker.decide(p) if d: break # Reset the conjugator before starting a new series for clear history conjugator.reset_history() print('State:{}'.format(decision_maker.state)) print('Displayed State: {}'.format(decision_maker.displayed_state))
def __init__(self, min_num_seq, max_num_seq, signal_model=None, fs=300, k=2, alp=None, evidence_names=['LM', 'ERP'], task_list=[('I_LOVE_COOKIES', 'I_LOVE_')], lmodel=None, is_txt_stim=True, device_name='LSL', device_channels=None, stimuli_timing=[1, .2], decision_threshold=0.8, backspace_prob=0.05, backspace_always_shown=False, filter_high=45, filter_low=2, filter_order=2, notch_filter_frequency=60): self.conjugator = EvidenceFusion(evidence_names, len_dist=len(alp)) seq_constants = [] if backspace_always_shown and BACKSPACE_CHAR in alp: seq_constants.append(BACKSPACE_CHAR) self.decision_maker = DecisionMaker( min_num_seq, max_num_seq, decision_threshold=decision_threshold, state=task_list[0][1], alphabet=alp, is_txt_stim=is_txt_stim, stimuli_timing=stimuli_timing, seq_constants=seq_constants) self.alp = alp # non-letter target labels include the fixation cross and calibration. self.nonletters = ['+', 'PLUS', 'calibration_trigger'] self.valid_targets = set(self.alp) self.signal_model = signal_model self.sampling_rate = fs self.downsample_rate = k self.filter_high = filter_high self.filter_low = filter_low self.filter_order = filter_order self.notch_filter_frequency = notch_filter_frequency self.mode = 'copy_phrase' self.task_list = task_list self.lmodel = lmodel self.channel_map = analysis_channels(device_channels, device_name) self.backspace_prob = backspace_prob
def setUp(self): """Set up decision maker object for testing """ self.decision_maker = DecisionMaker( 1, 3, state='', alphabet=list(string.ascii_uppercase) + ['<'] + [SPACE_CHAR], is_txt_stim=True, stimuli_timing=[1, .2], seq_constants=None) self.evidence_fusion = EvidenceFusion(list_name_evidence=['A', 'B'], len_dist=2)
def setUp(self): """Set up decision maker object for testing """ alphabet = list(string.ascii_uppercase) + ['<'] + [SPACE_CHAR] stopping_criteria = CriteriaEvaluator( continue_criteria=[MinIterationsCriteria(min_num_inq=1)], commit_criteria=[ MaxIterationsCriteria(max_num_inq=10), ProbThresholdCriteria(threshold=0.8) ]) stimuli_agent = NBestStimuliAgent(alphabet=alphabet, len_query=10) self.decision_maker = DecisionMaker( stimuli_agent=stimuli_agent, stopping_evaluator=stopping_criteria, state='', alphabet=alphabet, is_txt_stim=True, stimuli_timing=[1, .2], inq_constants=None) self.evidence_fusion = EvidenceFusion(list_name_evidence=['A', 'B'], len_dist=2)
class CopyPhraseWrapper: """Basic copy phrase task duty cycle wrapper. Given the phrases once operate() is called performs the task. Attr: min_num_seq: The minimum number of sequences to be displayed max_num_seq: The maximum number of sequences to be displayed model(pipeline): model trained using a calibration session of the same user. fs(int): sampling frequency k(int): down sampling rate alp(list[str]): symbol set of the task task_list(list[tuple(str,str)]): list[(phrases, initial_states)] for the copy phrase task is_txt_stim: Whether or not the stimuli are text objects conjugator(EvidenceFusion): fuses evidences in the task decision_maker(DecisionMaker): mastermind of the task mode(str): mode of thet task (should be copy phrase) d(binary): decision flag sti(list(tuple)): stimuli for the display decision_threshold: Minimum likelihood value required for a decision backspace_prob(float): default language model probability for the backspace character. backspace_always_shown(bool): whether or not the backspace should always be presented. """ def __init__(self, min_num_seq, max_num_seq, signal_model=None, fs=300, k=2, alp=None, evidence_names=['LM', 'ERP'], task_list=[('I_LOVE_COOKIES', 'I_LOVE_')], lmodel=None, is_txt_stim=True, device_name='LSL', device_channels=None, stimuli_timing=[1, .2], decision_threshold=0.8, backspace_prob=0.05, backspace_always_shown=False, filter_high=45, filter_low=2, filter_order=2, notch_filter_frequency=60): self.conjugator = EvidenceFusion(evidence_names, len_dist=len(alp)) seq_constants = [] if backspace_always_shown and BACKSPACE_CHAR in alp: seq_constants.append(BACKSPACE_CHAR) self.decision_maker = DecisionMaker( min_num_seq, max_num_seq, decision_threshold=decision_threshold, state=task_list[0][1], alphabet=alp, is_txt_stim=is_txt_stim, stimuli_timing=stimuli_timing, seq_constants=seq_constants) self.alp = alp # non-letter target labels include the fixation cross and calibration. self.nonletters = ['+', 'PLUS', 'calibration_trigger'] self.valid_targets = set(self.alp) self.signal_model = signal_model self.sampling_rate = fs self.downsample_rate = k self.filter_high = filter_high self.filter_low = filter_low self.filter_order = filter_order self.notch_filter_frequency = notch_filter_frequency self.mode = 'copy_phrase' self.task_list = task_list self.lmodel = lmodel self.channel_map = analysis_channels(device_channels, device_name) self.backspace_prob = backspace_prob def evaluate_sequence(self, raw_data, triggers, target_info, window_length): """Once data is collected, infers meaning from the data. Args: raw_data(ndarray[float]): C x L eeg data where C is number of channels and L is the signal length triggers(list[tuple(str,float)]): triggers e.g. ('A', 1) as letter and flash time for the letter target_info(list[str]): target information about the stimuli window_length(int): The length of the time between stimuli presentation """ letters, times, target_info = self.letter_info(triggers, target_info) # Remove 60hz noise with a notch filter notch_filter_data = notch.notch_filter( raw_data, self.sampling_rate, frequency_to_remove=self.notch_filter_frequency) # bandpass filter from 2-45hz filtered_data = bandpass.butter_bandpass_filter( notch_filter_data, self.filter_low, self.filter_high, self.sampling_rate, order=self.filter_order) # downsample data = downsample.downsample(filtered_data, factor=self.downsample_rate) x, _, _, _ = trial_reshaper(target_info, times, data, fs=self.sampling_rate, k=self.downsample_rate, mode=self.mode, channel_map=self.channel_map, trial_length=window_length) lik_r = inference(x, letters, self.signal_model, self.alp) prob = self.conjugator.update_and_fuse({'ERP': lik_r}) decision, sti = self.decision_maker.decide(prob) return decision, sti def letter_info( self, triggers: List[Tuple[str, float]], target_info: List[str] ) -> Tuple[List[str], List[float], List[str]]: """ Filters out non-letters and separates timings from letters. Parameters: ----------- triggers: triggers e.g. [['A', 0.5], ...] as letter and flash time for the letter target_info: target information about the stimuli; ex. ['nontarget', 'nontarget', ...] Returns: -------- (letters, times, target_info) """ letters = [] times = [] target_types = [] for i, (letter, stamp) in enumerate(triggers): if letter not in self.nonletters: letters.append(letter) times.append(stamp) target_types.append(target_info[i]) # Raise an error if the stimuli includes unexpected terms if not set(letters).issubset(self.valid_targets): invalid = set(letters).difference(self.valid_targets) raise Exception( f'unexpected letters received in copy phrase: {invalid}') return letters, times, target_types def initialize_epoch(self): """If a decision is made initializes the next epoch.""" try: # First, reset the history for this new epoch self.conjugator.reset_history() # If there is no language model specified, mock the LM prior # TODO: is the probability domain correct? ERP evidence is in # the log domain; LM by default returns negative log domain. if not self.lmodel: # mock probabilities to be equally likely for all letters. overrides = {BACKSPACE_CHAR: self.backspace_prob} prior = equally_probable(self.alp, overrides) # Else, let's query the lmodel for priors else: # Get the displayed state # TODO: for oclm this should be a list of (sym, prob) update = self.decision_maker.displayed_state # update the lmodel and get back the priors lm_prior = self.lmodel.state_update(update) # normalize to probability domain if needed if getattr(self.lmodel, 'normalized', False): lm_letter_prior = lm_prior['letter'] else: lm_letter_prior = norm_domain(lm_prior['letter']) if BACKSPACE_CHAR in self.alp: # Append backspace if missing. sym = (BACKSPACE_CHAR, self.backspace_prob) lm_letter_prior = sym_appended(lm_letter_prior, sym) # convert to format needed for evidence fusion; # probability value only in alphabet order. # TODO: ensure that probabilities still add to 1.0 prior = [ prior_prob for alp_letter in self.alp for prior_sym, prior_prob in lm_letter_prior if alp_letter == prior_sym ] # Try fusing the lmodel evidence try: prob_dist = self.conjugator.update_and_fuse( {'LM': np.array(prior)}) except Exception as lm_exception: print("Error updating language model!") raise lm_exception # Get decision maker to give us back some decisions and stimuli is_accepted, sti = self.decision_maker.decide(prob_dist) except Exception as init_exception: print("Error in initialize_epoch: %s" % (init_exception)) raise init_exception return is_accepted, sti
def __init__(self, min_num_inq, max_num_inq, signal_model=None, fs=300, k=2, alp=None, evidence_names=['LM', 'ERP'], task_list=[('I_LOVE_COOKIES', 'I_LOVE_')], lmodel=None, is_txt_stim=True, device_name='LSL', device_channels=None, stimuli_timing=[1, .2], decision_threshold=0.8, backspace_prob=0.05, backspace_always_shown=False, filter_high=45, filter_low=2, filter_order=2, notch_filter_frequency=60): self.conjugator = EvidenceFusion(evidence_names, len_dist=len(alp)) inq_constants = [] if backspace_always_shown and BACKSPACE_CHAR in alp: inq_constants.append(BACKSPACE_CHAR) # Stimuli Selection Module stopping_criteria = CriteriaEvaluator( continue_criteria=[MinIterationsCriteria(min_num_inq)], commit_criteria=[ MaxIterationsCriteria(max_num_inq), ProbThresholdCriteria(decision_threshold) ]) # TODO: Parametrize len_query in the future releases! stimuli_agent = NBestStimuliAgent(alphabet=alp, len_query=10) self.decision_maker = DecisionMaker( stimuli_agent=stimuli_agent, stopping_evaluator=stopping_criteria, state=task_list[0][1], alphabet=alp, is_txt_stim=is_txt_stim, stimuli_timing=stimuli_timing, inq_constants=inq_constants) self.alp = alp # non-letter target labels include the fixation cross and calibration. self.nonletters = ['+', 'PLUS', 'calibration_trigger'] self.valid_targets = set(self.alp) self.signal_model = signal_model self.sampling_rate = fs self.downsample_rate = k self.filter_high = filter_high self.filter_low = filter_low self.filter_order = filter_order self.notch_filter_frequency = notch_filter_frequency self.mode = 'copy_phrase' self.task_list = task_list self.lmodel = lmodel self.channel_map = analysis_channels(device_channels, device_name) self.backspace_prob = backspace_prob
class TestDecisionMaker(unittest.TestCase): """Test for decision maker class """ def setUp(self): """Set up decision maker object for testing """ self.decision_maker = DecisionMaker( 1, 3, state='', alphabet=list(string.ascii_uppercase) + ['<'] + [SPACE_CHAR], is_txt_stim=True, stimuli_timing=[1, .2], seq_constants=None) self.evidence_fusion = EvidenceFusion(list_name_evidence=['A', 'B'], len_dist=2) def tearDown(self): """Reset decision maker and evidence fusion at the end of each test. """ self.decision_maker.reset() self.evidence_fusion.reset_history() def test_evidence_fusion_init(self): self.assertEqual(self.evidence_fusion.evidence_history, { 'A': [], 'B': [] }) self.assertEqual(self.evidence_fusion.likelihood[0], [0.5]) self.assertEqual(self.evidence_fusion.likelihood[1], [0.5]) def test_reset_history(self): self.evidence_fusion.reset_history() self.assertEqual(self.evidence_fusion.evidence_history, { 'A': [], 'B': [] }) self.assertEqual(self.evidence_fusion.likelihood[0], [0.5]) self.assertEqual(self.evidence_fusion.likelihood[1], [0.5]) def test_update_and_fuse_with_float_evidence(self): dict_evidence = {'A': [0.5], 'B': [0.5]} self.evidence_fusion.update_and_fuse(dict_evidence) self.assertEqual(self.evidence_fusion.evidence_history['A'][0], dict_evidence['A']) self.assertEqual(self.evidence_fusion.evidence_history['B'][0], dict_evidence['B']) self.assertEqual(self.evidence_fusion.likelihood[0], [[0.5]]) self.assertEqual(self.evidence_fusion.likelihood[1], [[0.5]]) def test_update_and_fuse_with_inf_evidence(self): dict_evidence = {'A': [0.5], 'B': [math.inf]} self.evidence_fusion.update_and_fuse(dict_evidence) self.assertEqual(self.evidence_fusion.evidence_history['A'][0], dict_evidence['A']) self.assertEqual(self.evidence_fusion.evidence_history['B'][0], dict_evidence['B']) self.assertEqual(self.evidence_fusion.likelihood[0], [1.0]) self.assertEqual(self.evidence_fusion.likelihood[1], [0.0]) def test_save_history(self): history = self.evidence_fusion.save_history() self.assertEqual(0, history) def test_decision_maker_init(self): """Test initialization""" self.assertEqual(self.decision_maker.min_num_seq, 1) self.assertEqual(self.decision_maker.max_num_seq, 3) self.assertEqual(self.decision_maker.state, '') self.assertEqual(self.decision_maker.displayed_state, '') def test_decide_without_commit(self): """ Test decide method with case of no commit using a fake probability distribution """ probability_distribution = np.ones(len( self.decision_maker.alphabet)) / 8 decision, chosen_stimuli = self.decision_maker.decide( probability_distribution) self.assertTrue( np.all(self.decision_maker.list_epoch[-1]['list_distribution'][-1] == probability_distribution)) self.assertFalse(decision) self.decision_maker.do_epoch() self.assertEqual(self.decision_maker.sequence_counter, 0) def test_decide_with_commit(self): """Test decide method with case of commit""" probability_distribution = np.ones(len(self.decision_maker.alphabet)) self.decision_maker.sequence_counter = self.decision_maker.min_num_seq decision, chosen_stimuli = self.decision_maker.decide( probability_distribution) self.assertTrue(decision) self.assertEqual(chosen_stimuli, None) def test_update_with_letter(self): """Test update method with letter being the new state""" old_displayed_state = self.decision_maker.displayed_state old_state = self.decision_maker.state new_state = 'E' self.decision_maker.update(state=new_state) self.assertEqual(self.decision_maker.state, old_state + new_state) self.assertEqual(self.decision_maker.displayed_state, old_displayed_state + 'E') def test_update_with_backspace(self): """Test update method with backspace being the new state""" old_displayed_state = self.decision_maker.displayed_state old_state = self.decision_maker.state new_state = '<' self.decision_maker.update(state=new_state) self.assertEqual(self.decision_maker.state, old_state + new_state) self.assertEqual(self.decision_maker.displayed_state, old_displayed_state[0:-1]) self.assertLess(len(self.decision_maker.displayed_state), len(self.decision_maker.state)) def test_reset(self): """Test reset of decision maker state""" self.decision_maker.reset() self.assertEqual(self.decision_maker.state, '') self.assertEqual(self.decision_maker.displayed_state, '') self.assertEqual(self.decision_maker.time, 0) self.assertEqual(self.decision_maker.sequence_counter, 0) def test_form_display_state(self): """Test form display state method with a dummy state""" self.decision_maker.update(state='ABC<.E') self.decision_maker.form_display_state(self.decision_maker.state) self.assertEqual(self.decision_maker.displayed_state, 'ABE') self.decision_maker.reset() def test_do_epoch(self): """Test do_epoch method""" probability_distribution = np.ones(len( self.decision_maker.alphabet)) / 8 decision, chosen_stimuli = self.decision_maker.decide( probability_distribution) self.decision_maker.do_epoch() self.assertEqual(self.decision_maker.sequence_counter, 0) def test_decide_state_update(self): """Tests decide state update method""" probability_distribution = np.ones(len(self.decision_maker.alphabet)) self.decision_maker.list_epoch[-1]['list_distribution'].append( probability_distribution) decision = self.decision_maker.decide_state_update() expected = 'A' # expect to commit to first letter in sequence, due to uniform probability self.assertEqual(decision, 'A') def test_schedule_sequence(self): """Test sequence scheduling. Should return new stimuli list, at random.""" probability_distribution = np.ones(len(self.decision_maker.alphabet)) old_counter = self.decision_maker.sequence_counter self.decision_maker.list_epoch[-1]['list_distribution'].append( probability_distribution) stimuli = self.decision_maker.schedule_sequence() self.assertEqual(self.decision_maker.state, '.') self.assertEqual(stimuli[0], self.decision_maker.list_epoch[-1]['list_sti'][-1]) self.assertLess(old_counter, self.decision_maker.sequence_counter) def test_prepare_stimuli(self): """Test that stimuli are prepared as expected""" probability_distribution = np.ones(len(self.decision_maker.alphabet)) self.decision_maker.list_epoch[-1]['list_distribution'].append( probability_distribution) stimuli = self.decision_maker.prepare_stimuli() self.assertEqual(11, len(stimuli[0][0])) for i in range(1, len(stimuli[0][0])): self.assertIn(stimuli[0][0][i], self.decision_maker.alphabet) self.assertEqual(stimuli[1][0][0:2], self.decision_maker.stimuli_timing)
class TestDecisionMaker(unittest.TestCase): """Test for decision maker class """ def setUp(self): """Set up decision maker object for testing """ alphabet = list(string.ascii_uppercase) + ['<'] + [SPACE_CHAR] stopping_criteria = CriteriaEvaluator( continue_criteria=[MinIterationsCriteria(min_num_inq=1)], commit_criteria=[ MaxIterationsCriteria(max_num_inq=10), ProbThresholdCriteria(threshold=0.8) ]) stimuli_agent = NBestStimuliAgent(alphabet=alphabet, len_query=10) self.decision_maker = DecisionMaker( stimuli_agent=stimuli_agent, stopping_evaluator=stopping_criteria, state='', alphabet=alphabet, is_txt_stim=True, stimuli_timing=[1, .2], inq_constants=None) self.evidence_fusion = EvidenceFusion(list_name_evidence=['A', 'B'], len_dist=2) def tearDown(self): """Reset decision maker and evidence fusion at the end of each test. """ self.decision_maker.reset() self.evidence_fusion.reset_history() def test_evidence_fusion_init(self): self.assertEqual(self.evidence_fusion.evidence_history, { 'A': [], 'B': [] }) self.assertEqual(self.evidence_fusion.likelihood[0], [0.5]) self.assertEqual(self.evidence_fusion.likelihood[1], [0.5]) def test_reset_history(self): self.evidence_fusion.reset_history() self.assertEqual(self.evidence_fusion.evidence_history, { 'A': [], 'B': [] }) self.assertEqual(self.evidence_fusion.likelihood[0], [0.5]) self.assertEqual(self.evidence_fusion.likelihood[1], [0.5]) def test_update_and_fuse_with_float_evidence(self): dict_evidence = {'A': [0.5], 'B': [0.5]} self.evidence_fusion.update_and_fuse(dict_evidence) self.assertEqual(self.evidence_fusion.evidence_history['A'][0], dict_evidence['A']) self.assertEqual(self.evidence_fusion.evidence_history['B'][0], dict_evidence['B']) self.assertEqual(self.evidence_fusion.likelihood[0], [[0.5]]) self.assertEqual(self.evidence_fusion.likelihood[1], [[0.5]]) def test_update_and_fuse_with_inf_evidence(self): dict_evidence = {'A': [0.5], 'B': [math.inf]} self.evidence_fusion.update_and_fuse(dict_evidence) self.assertEqual(self.evidence_fusion.evidence_history['A'][0], dict_evidence['A']) self.assertEqual(self.evidence_fusion.evidence_history['B'][0], dict_evidence['B']) self.assertEqual(self.evidence_fusion.likelihood[0], [1.0]) self.assertEqual(self.evidence_fusion.likelihood[1], [0.0]) def test_save_history(self): history = self.evidence_fusion.save_history() self.assertEqual(0, history) def test_decision_maker_init(self): """Test initialization""" # TODO: Update that test part # self.assertEqual(self.decision_maker.min_num_inq, 1) # self.assertEqual(self.decision_maker.max_num_inq, 3) self.assertEqual(self.decision_maker.state, '') self.assertEqual(self.decision_maker.displayed_state, '') def test_update_with_letter(self): """Test update method with letter being the new state""" old_displayed_state = self.decision_maker.displayed_state old_state = self.decision_maker.state new_state = 'E' self.decision_maker.update(state=new_state) self.assertEqual(self.decision_maker.state, old_state + new_state) self.assertEqual(self.decision_maker.displayed_state, old_displayed_state + 'E') def test_update_with_backspace(self): """Test update method with backspace being the new state""" old_displayed_state = self.decision_maker.displayed_state old_state = self.decision_maker.state new_state = '<' self.decision_maker.update(state=new_state) self.assertEqual(self.decision_maker.state, old_state + new_state) self.assertEqual(self.decision_maker.displayed_state, old_displayed_state[0:-1]) self.assertLess(len(self.decision_maker.displayed_state), len(self.decision_maker.state)) def test_reset(self): """Test reset of decision maker state""" self.decision_maker.reset() self.assertEqual(self.decision_maker.state, '') self.assertEqual(self.decision_maker.displayed_state, '') self.assertEqual(self.decision_maker.time, 0) self.assertEqual(self.decision_maker.inquiry_counter, 0) def test_form_display_state(self): """Test form display state method with a dummy state""" self.decision_maker.update(state='ABC<.E') self.decision_maker.form_display_state(self.decision_maker.state) self.assertEqual(self.decision_maker.displayed_state, 'ABE') self.decision_maker.reset() def test_do_series(self): """Test do_series method""" probability_distribution = np.ones(len( self.decision_maker.alphabet)) / 8 decision, chosen_stimuli = self.decision_maker.decide( probability_distribution) self.decision_maker.do_series() self.assertEqual(self.decision_maker.inquiry_counter, 0) def test_decide_state_update(self): """Tests decide state update method""" probability_distribution = np.ones(len(self.decision_maker.alphabet)) self.decision_maker.list_series[-1]['list_distribution'].append( probability_distribution) decision = self.decision_maker.decide_state_update() expected = 'A' # expect to commit to first letter in inquiry, due to uniform probability self.assertEqual(decision, 'A') def test_schedule_inquiry(self): """Test inquiry scheduling. Should return new stimuli list, at random.""" probability_distribution = np.ones(len(self.decision_maker.alphabet)) old_counter = self.decision_maker.inquiry_counter self.decision_maker.list_series[-1]['list_distribution'].append( probability_distribution) stimuli = self.decision_maker.schedule_inquiry() self.assertEqual(self.decision_maker.state, '.') self.assertEqual(stimuli[0], self.decision_maker.list_series[-1]['list_sti'][-1]) self.assertLess(old_counter, self.decision_maker.inquiry_counter) def test_prepare_stimuli(self): """Test that stimuli are prepared as expected""" probability_distribution = np.ones(len(self.decision_maker.alphabet)) self.decision_maker.list_series[-1]['list_distribution'].append( probability_distribution) stimuli = self.decision_maker.prepare_stimuli() self.assertEqual(self.decision_maker.stimuli_agent.len_query + 1, len(stimuli[0][0])) for i in range(1, len(stimuli[0][0])): self.assertIn(stimuli[0][0][i], self.decision_maker.alphabet) self.assertEqual(stimuli[1][0][0:2], self.decision_maker.stimuli_timing)