def _parse_plot(self, lineno, linesplit_space): """ @plot expr {mpl_prop} """ cmd = linesplit_space[0] if len(linesplit_space) == 1: # @plot raise ParseException(lineno, f"Invalid {cmd} command.") expr0, mpl_prop = ParseUtil.get_ending_dict(linesplit_space[1]) if mpl_prop is None: mpl_prop = dict() expr = expr0 all_stimulus_elements = self.parameters.get(kw.STIMULUS_ELEMENTS) all_behaviors = self.parameters.get(kw.BEHAVIORS) err = None if cmd == kw.VPLOT: expr, err = ParseUtil.parse_element_behavior( expr0, all_stimulus_elements, all_behaviors) elif cmd == kw.VSSPLOT: expr, err = ParseUtil.parse_element_element( expr0, all_stimulus_elements) elif cmd == kw.PPLOT: stimulus, behavior, err = ParseUtil.parse_stimulus_behavior( expr0, all_stimulus_elements, all_behaviors, self.variables) expr = (stimulus, behavior) elif cmd == kw.NPLOT: expr, err = ParseUtil.parse_chain(expr0, all_stimulus_elements, all_behaviors) if err: raise ParseException(lineno, err) return expr, mpl_prop, expr0
def _parse_behavior_cost(self, behavior_cost_str, variables, to_be_continued, is_appending): if not self.val[kw.BEHAVIORS]: return f"The parameter 'behaviors' must be assigned before the parameter '{kw.BEHAVIOR_COST}'." # Create and populate the struct with None values if not is_appending: self.val[kw.BEHAVIOR_COST] = dict() for e in self.val[kw.BEHAVIORS]: self.val[kw.BEHAVIOR_COST][e] = None self.val[kw.BEHAVIOR_COST][kw.DEFAULT] = None single_c, _ = ParseUtil.evaluate(behavior_cost_str, variables) if single_c is not None: if is_appending: return "A single value for '{}' cannot follow other values.".format( kw.BEHAVIOR_COST) elif to_be_continued: return "A single value for '{}' cannot be followed by other values.".format( kw.BEHAVIOR_COST) else: for key in self.val[kw.BEHAVIOR_COST]: self.val[kw.BEHAVIOR_COST][key] = single_c self.val[kw.BEHAVIOR_COST].pop(kw.DEFAULT) else: cs = ParseUtil.comma_split(behavior_cost_str) cs = [x.strip() for x in cs] for bc_str in cs: # bc_str is 'e:value' or 'default:value' if bc_str.count(':') != 1: return "Expected 'element:value' or 'default:value' in '{}', got '{}'.".format( kw.BEHAVIOR_COST, bc_str) b, c_str = bc_str.split(':') b = b.strip() c_str = c_str.strip() c, err = ParseUtil.evaluate(c_str, variables) if err: return f"Invalid value '{c_str}' for '{b}' in parameter '{kw.BEHAVIOR_COST}'." if b == kw.DEFAULT: if self.val[kw.BEHAVIOR_COST][kw.DEFAULT] is not None: return "Default value for '{}' can only be stated once.".format( kw.BEHAVIOR_COST) elif b not in self.val[kw.BEHAVIORS]: return f"Error in parameter '{kw.BEHAVIOR_COST}': '{b}' is an invalid behavior name." if self.val[kw.BEHAVIOR_COST][b] is not None: return "Duplicate of {} in '{}'.".format( b, kw.BEHAVIOR_COST) self.val[kw.BEHAVIOR_COST][b] = c if not to_be_continued: # Set the default value for non-set behaviors err = self._set_default_values(kw.BEHAVIOR_COST) if err: return err return None # No error
def _parse_stimulus_values(self, NAME, stimulus_values, variables, to_be_continued, is_appending): if not self.val[kw.STIMULUS_ELEMENTS]: return f"The parameter 'stimulus_elements' must be assigned before the parameter '{NAME}'." # Create and populate the struct with None values if not is_appending: self.val[NAME] = dict() for e in self.val[kw.STIMULUS_ELEMENTS]: self.val[NAME][e] = None self.val[NAME][kw.DEFAULT] = None single_w, _ = ParseUtil.evaluate(stimulus_values, variables) if single_w is not None: if is_appending: return "A single value for '{}' cannot follow other values.".format( NAME) elif to_be_continued: return "A single value for '{}' cannot be followed by other values.".format( NAME) else: for key in self.val[NAME]: self.val[NAME][key] = single_w self.val[NAME].pop(kw.DEFAULT) else: ws = ParseUtil.comma_split(stimulus_values) ws = [x.strip() for x in ws] for e_w_str in ws: # eb_w_str is 'e:value' or 'default:value' if e_w_str.count(':') != 1: return "Expected 'element:value' or 'default:value' in '{}', got '{}'.".format( NAME, e_w_str) e, w_str = e_w_str.split(':') e = e.strip() w_str = w_str.strip() w, err = ParseUtil.evaluate(w_str, variables) if err: return "Invalid value '{}' for '{}' in parameter '{}'.".format( w_str, e, NAME) if e == kw.DEFAULT: if self.val[NAME][kw.DEFAULT] is not None: return "Default value for '{}' can only be stated once.".format( NAME) elif e not in self.val[kw.STIMULUS_ELEMENTS]: return f"Error in parameter '{NAME}': '{e}' is an invalid stimulus element." if self.val[NAME][e] is not None: return "Duplicate of {} in '{}'.".format(e, NAME) self.val[NAME][e] = w if not to_be_continued: # Set the default value for non-set stimulus elements err = self._set_default_values(NAME) if err: return err return None # No error
def _parse_export(self, lineno, linesplit_space): """ @export expr @export expr filename @hexport @hexport filename """ cmd = linesplit_space[0] filename_param = self.parameters.get(kw.FILENAME) if cmd == kw.HEXPORT: if len(linesplit_space) == 1: # @hexport if len(filename_param) == 0: raise ParseException(lineno, f"Invalid {cmd} command.") else: filename = filename_param else: # @hexport filename filename = linesplit_space[1] return None, filename, None if len(linesplit_space) == 1: # @export raise ParseException(lineno, f"Invalid {cmd} command.") args = linesplit_space[1] expr0, filename = ParseUtil.split1_strip(args) expr = expr0 if filename is None: if len(filename_param) == 0: raise ParseException(lineno, f"No filename given to {cmd}.") else: filename = filename_param all_stimulus_elements = self.parameters.get(kw.STIMULUS_ELEMENTS) all_behaviors = self.parameters.get(kw.BEHAVIORS) err = None if cmd == kw.VEXPORT: expr, err = ParseUtil.parse_element_behavior( expr0, all_stimulus_elements, all_behaviors) elif cmd == kw.VSSEXPORT: expr, err = ParseUtil.parse_element_element( expr0, all_stimulus_elements) elif cmd == kw.PEXPORT: stimulus, behavior, err = ParseUtil.parse_stimulus_behavior( expr0, all_stimulus_elements, all_behaviors, self.variables) expr = (stimulus, behavior) elif cmd == kw.NEXPORT: expr, err = ParseUtil.parse_chain(expr0, all_stimulus_elements, all_behaviors) if err: raise ParseException(lineno, err) return expr, filename, expr0
def add_cs_varvals(self, cs_varvals, pv): """ Adds to self.values the variables and values in the specified comma-separated variable:value pairs ("var1:val1, var2:val2, ..."). """ err = "A @VARIABLES line should have the form '@VARIABLES var1:val1, var2:val2, ...'." varvals = ParseUtil.comma_split(cs_varvals) for varval in varvals: if varval.count(':') == 1 or varval.count('=') == 1: if varval.count('=') == 1: sep = '=' else: sep = ':' var, val_str = varval.split(sep) var = var.strip() val_str = val_str.strip() if var not in self.values: # Otherwise overwrite previous value var_err = is_valid_name(var, pv, kw) if var_err: return var_err val, val_err = self._is_valid_value(val_str) if val_err: return val_err else: self.values[var] = val else: return err return None
def is_met(self, variables, event_counter): ismet, err = ParseUtil.evaluate(self.cond, variables, event_counter, ParseUtil.STOP_COND) if err: raise ParseException(self.lineno, err) if type(ismet) is not bool: raise ParseException(self.lineno, f"Condition '{self.cond}' is not a boolean expression.") return ismet
def _parse_goto(self, goto_str, lineno, all_linelabels, global_variables): self.goto = list() err = f"Invalid condition '{goto_str}'. " lbls_and_probs = goto_str.split(',') lbls_and_probs = [lbl_and_prob.strip() for lbl_and_prob in lbls_and_probs] for lbl_and_prob in lbls_and_probs: if lbl_and_prob in all_linelabels: if len(lbls_and_probs) > 1: raise ParseException(lineno, f"Invalid condition '{goto_str}'.") self.goto.append([1, lbl_and_prob]) else: if '(' and ')' in lbl_and_prob: if lbl_and_prob.count('(') > 1 or lbl_and_prob.count(')') > 1: raise ParseException(lineno, err + "Too many parentheses.") lindex = lbl_and_prob.find('(') rindex = lbl_and_prob.find(')') if lindex > rindex: raise ParseException(lineno, err + "Mismatched parentheses.") lbl = lbl_and_prob[0:lindex] if lbl not in all_linelabels: raise ParseException(lineno, err + f"Unknown line label '{lbl}'.") prob_str = lbl_and_prob[(lindex + 1): rindex] isprob, prob = ParseUtil.is_prob(prob_str, global_variables) if not isprob: if prob is not None: raise ParseException(lineno, err + f"Expected a probability, got '{prob_str}'.") prob = prob_str for prob_lbl in self.goto: if prob_lbl[1] == lbl: raise ParseException(lineno, err + f"Label '{lbl}' duplicated.") self.goto.append([prob, lbl]) else: raise ParseException(lineno, err + f"Invalid line label '{goto_str}'.")
def _goto_if_met(self, variables): goto_prob_cumsum = list() cumsum = 0 for i in range(len(self.goto)): prob = self.goto[i][0] if type(prob) is str: self.goto[i][0], err = ParseUtil.evaluate(prob, variables=variables) if err: raise ParseException(self.lineno, err) cumsum += self.goto[i][0] goto_prob_cumsum.append(cumsum) if cumsum > 1: raise ParseException(self.lineno, f"Sum of probabilities is {cumsum}>1.") ind = ParseUtil.weighted_choice(goto_prob_cumsum) if ind is None: return None else: return self.goto[ind][1]
def _parse_legend(self, lineno, linesplit_space): if len(linesplit_space) == 1: # @legend mpl_prop = dict() elif len(linesplit_space) == 2: # @legend {mpl_prop} arg = linesplit_space[1] is_dict, mpl_prop = ParseUtil.is_dict(arg) if not is_dict: raise ParseException(lineno, f"Invalid @legend argument {arg}.") return mpl_prop
def _parse_subject(self, v_str, variables): err = f"Parameter {kw.SUBJECT} must be 'average', 'all', or a positive integer." if v_str.lower() in ('average', 'all'): self.val[kw.SUBJECT] = v_str.lower() else: v, interr = ParseUtil.parse_posint(v_str, variables) if interr: # Parsing error return err + " " + interr if v is None: # Parsing worked, but negative integer return err self.val[kw.SUBJECT] = v - 1 # Zero-based index internally return None
def _perform_action(self, lineno, action): """ Sets a variable (x:3) or count_reset(event). """ omit_learn = False # No action to perform if len(action) == 0: pass # Setting a variable elif action.count(':') == 1 or action.count('=') == 1: if action.count('=') == 1: sep = '=' else: sep = ':' var_name, value_str = ParseUtil.split1_strip(action, sep=sep) variables_join = Variables.join(self.global_variables, self.local_variables) value, err = ParseUtil.evaluate(value_str, variables_join) if err: raise ParseException(lineno, err) err = self.local_variables.set(var_name, value, self.parameters) if err: raise ParseException(lineno, err) # count_reset elif action.startswith("count_reset(") and action.endswith(")"): event = action[12:-1] self.event_counter.reset_count(event) # omit_learn elif action == "@omit_learn": omit_learn = True else: raise ParseException(lineno, "Internal error.") # Should have been caught during Pase.parse() return omit_learn
def __init__(self, phase_obj, lineno, label, after_label, all_linelabels, parameters, global_variables): self.lineno = lineno self.label = label self.parameters = parameters self.all_linelabels = all_linelabels self.is_help_line = False self.stimulus = None # A dict with an intensity for each element in stimulus_elememts self.action = None self.action, logic = ParseUtil.split1_strip(after_label, sep=PHASEDIV) if logic is None: raise ParseException(lineno, f"Missing separator '{PHASEDIV}' on phase line.") action_list = ParseUtil.comma_split_strip(self.action) first_element, _, _ = ParseUtil.parse_element_and_intensity(action_list[0], variables=None, safe_intensity_eval=True) self.is_help_line = (len(action_list) == 1) and (first_element not in parameters.get(STIMULUS_ELEMENTS)) if self.is_help_line: self.stimulus = None if action_list[0] != '': check_action(action_list[0], parameters, global_variables, lineno, all_linelabels) else: self.stimulus, err = ParseUtil.parse_elements_and_intensities(self.action, global_variables, safe_intensity_eval=True) if err: raise ParseException(lineno, err) for element in self.stimulus: if element not in parameters.get(STIMULUS_ELEMENTS): raise ParseException(lineno, f"Expected a stimulus element, got '{element}'.") self.action = None if len(logic) == 0: raise ParseException(lineno, f"Line with label '{label}' has no conditions.") self.conditions = PhaseLineConditions(phase_obj, lineno, self.is_help_line, logic, parameters, all_linelabels, global_variables)
def _parse_phases(self, v_str): if v_str == 'all': self.val[kw.PHASES] = v_str # list(all_phase_labels) else: phase_labels = ParseUtil.comma_split_strip(v_str) for phase_label in phase_labels: if len(phase_label) == 0: return "Expected comma-separated list of phase labels, found {}".format( phase_labels) # else: # if phase_label not in all_phase_labels: # return "Undefined phase label '{}'.".format(phase_label) self.val[kw.PHASES] = phase_labels return None
def _parse_figure(self, lineno, linesplit_space): """ @figure @figure title @figure {mpl_prop} @figure title {mpl_prop} """ title = self.parameters.get(kw.TITLE) if len(linesplit_space) == 1: # @figure mpl_prop = dict() elif len(linesplit_space) == 2: # @figure args title, mpl_prop = ParseUtil.get_ending_dict(linesplit_space[1]) if mpl_prop is None: mpl_prop = dict() return title, mpl_prop
def _parse_subplot(self, lineno, linesplit_space): """ @subplot @subplot subplotspec @subplot subplotspec title @subplot subplotspec {mpl_prop} @subplot subplotspec title {mpl_prop} """ title_param = self.parameters.get(kw.SUBPLOTTITLE) if len(linesplit_space) == 1: # @subplot subplotspec = '111' title = title_param mpl_prop = dict() elif len(linesplit_space) == 2: # @subplot ... args, mpl_prop = ParseUtil.get_ending_dict(linesplit_space[1]) if mpl_prop is None: mpl_prop = dict() subplotspec, title_line = ParseUtil.split1_strip(args) if title_line is None: # @subplot subplotspec title = title_param else: title = title_line return subplotspec, title, mpl_prop
def _parse_xscale(self, xscale, phases): if not self.val[kw.STIMULUS_ELEMENTS]: return f"The parameter 'stimulus_elements' must be assigned before the parameter '{kw.XSCALE}'." if not self.val[kw.BEHAVIORS] and self.val[kw.MECHANISM_NAME] != mn.RW: return f"The parameter 'behaviors' must be assigned before the parameter '{kw.XSCALE}'." if phases.is_phase_label(xscale): pass elif xscale == 'all': pass else: xscale, err = ParseUtil.parse_chain(xscale, self.val[kw.STIMULUS_ELEMENTS], self.val[kw.BEHAVIORS]) if err: return err self.val[kw.XSCALE] = xscale return None
def parse(self, parameters, global_variables): self.parameters = parameters self.global_variables = global_variables stimulus_elements = parameters.get(STIMULUS_ELEMENTS) behaviors = parameters.get(BEHAVIORS) phase_lines_afterlabel = list() linenos = list() # First iteration through lines: Create list of lines (and labels) for line_lineno in self.lines: line, lineno = line_lineno label, afterlabel = ParseUtil.split1_strip(line) if afterlabel is None: raise ParseException(lineno, "Phase line contains only label.") coincide_err = f"The phase line label '{label}' coincides with the name of a " if label in stimulus_elements: raise ParseException(lineno, coincide_err + "stimulus element.") elif label in behaviors: raise ParseException(lineno, coincide_err + "behavior.") if label in self.linelabels and not self.is_inherited: raise ParseException(lineno, f"Duplicate of phase line label '{label}'.") self.linelabels.append(label) phase_lines_afterlabel.append(afterlabel) linenos.append(lineno) if self.first_label is None: # Set self.first_label to the label of the first line self.first_label = label # Second iteration: Create PhaseLine objects and put in the dict self.phase_lines for label, after_label, lineno in zip(self.linelabels, phase_lines_afterlabel, linenos): self.phase_lines[label] = PhaseLine(self, lineno, label, after_label, self.linelabels, self.parameters, self.global_variables) if label == "new_trial": # Change self.first_label to the new_trial line self.first_label = label self.initialize_local_variables() self.event_counter = PhaseEventCounter(self.linelabels, self.parameters) self.subject_reset() self.is_parsed = True
def is_met(self, response, global_variables, local_variables, event_counter): variables_both = Variables.join(global_variables, local_variables) if self.condition is None: ismet = True elif self.condition_is_behavior: ismet = (self.condition == response) else: ismet, err = ParseUtil.evaluate(self.condition, variables_both, event_counter, ParseUtil.PHASE_LINE) if err: raise ParseException(self.lineno, err) if type(ismet) is not bool: raise ParseException(self.lineno, f"Condition '{self.condition}' is not a boolean expression.") if ismet: label = self._goto_if_met(variables_both) if label is None: # In "ROW1(0.1),ROW2(0.3)", goto_if_met returns None with prob. 0.6 ismet = False else: label = None return ismet, label
def check_action(action, parameters, global_variables, lineno, all_linelabels): if action.count(':') == 1 or action.count('=') == 1: if action.count('=') == 1: sep = '=' else: sep = ':' var_name, _ = ParseUtil.split1_strip(action, sep=sep) var_err = is_valid_name(var_name, parameters, kw) if var_err is not None: raise ParseException(lineno, var_err) if global_variables.contains(var_name): raise ParseException(lineno, "Cannot modify global variable inside a phase.") elif action.startswith("count_reset(") and action.endswith(")"): behaviors = parameters.get(BEHAVIORS) stimulus_elements = parameters.get(STIMULUS_ELEMENTS) event = action[12:-1] if event not in stimulus_elements and event not in behaviors and event not in all_linelabels: raise ParseException(lineno, f"Unknown event '{event}' in count_reset.") elif action == "@omit_learn": pass else: raise ParseException(lineno, f"Unknown action '{action}'.")
def check_compatibility_with_world(self, world): behaviors = self.parameters.get(kw.BEHAVIORS) # Check that stop condition does not depend on behavior for phase in world.phases: expr_vars = ParseUtil.variables_in_expr(phase.stop_condition.cond) for behavior in behaviors: if behavior in expr_vars: mech_name = self.parameters.get(kw.MECHANISM_NAME) err = f"Stop condition cannot depend on behavior in mechanism '{mech_name}'." lineno = phase.stop_condition.lineno return False, err, lineno # Check that phase line logics do not depend on behavior for phase in world.phases: for _, phase_line in phase.phase_lines.items(): for condition_obj in phase_line.conditions.conditions: if condition_obj.condition_is_behavior: mech_name = self.parameters.get(kw.MECHANISM_NAME) err = f"Phase line logic cannot depend on behavior in mechanism '{mech_name}'." lineno = condition_obj.lineno return False, err, lineno return True, None, None
def next_stimulus(self, response, ignore_response_increment=False, preceeding_help_lines=None, omit_learn=None): # if not self.is_parsed: # raise Exception("Internal error: Cannot call Phase.next_stimulus" + # " before Phase.parse().") if preceeding_help_lines is None: preceeding_help_lines = list() if not ignore_response_increment: # if not self.is_first_line: if response is not None: self.event_counter.increment_count(response) self.event_counter.increment_count_line(response) self.event_counter.set_last_response(response) if self.first_stimulus_presented: variables_both = Variables.join(self.global_variables, self.local_variables) if self.stop_condition.is_met(variables_both, self.event_counter): return None, None, preceeding_help_lines, None if self.is_first_line: # assert(response is None) rowlbl = self.first_label self.is_first_line = False omit_learn = True # Since response is None, there is no learning (updating of v-values) to do else: rowlbl, omit_learn = self.curr_lineobj.next_line(response, self.global_variables, self.local_variables, self.event_counter) self.prev_linelabel = self.curr_lineobj.label self._make_current_line(rowlbl) stimulus = self.phase_lines[rowlbl].stimulus if stimulus is not None: for element, intensity in stimulus.items(): if type(intensity) is str: # element[var] where var is a (local) variable variables_both = Variables.join(self.global_variables, self.local_variables) stimulus[element], err = ParseUtil.evaluate(intensity, variables=variables_both) if err: raise ParseException(self.phase_lines[rowlbl].lineno, err) if rowlbl != self.prev_linelabel: self.event_counter.reset_count_line() self.event_counter.line_label = rowlbl self.event_counter.increment_count(rowlbl) self.event_counter.increment_count_line(rowlbl) if stimulus is None: # Help line action = self.phase_lines[rowlbl].action lineno = self.phase_lines[rowlbl].lineno self._perform_action(lineno, action) preceeding_help_lines.append(rowlbl) next_stimulus_out = self.next_stimulus(response, ignore_response_increment=True, preceeding_help_lines=preceeding_help_lines, omit_learn=omit_learn) stimulus, rowlbl, preceeding_help_lines, omit_learn_help = next_stimulus_out omit_learn = (omit_learn or omit_learn_help) else: for stimulus_element in stimulus: self.event_counter.increment_count(stimulus_element) self.event_counter.increment_count_line(stimulus_element) self.first_stimulus_presented = True return stimulus, rowlbl, preceeding_help_lines, omit_learn
def _parse(self, lineno, is_help_line, condition_and_actions, logicpart_index, n_logicparts, parameters, all_linelabels, global_variables): ''' Args: condition_and_actions (str): Examples are "b=5: x:2, y=2, ROWLBL", "x:2, y:2, ROWLBL", "@break" "x=1: @break" "x:1" "@break, x:1" ''' self.condition = None ca_list = ParseUtil.comma_split_strip(condition_and_actions) found_condition = False any_rowlbl_prob = False found_rowlbl = False goto_list = list() goto_list_index = list() for i, ca in enumerate(ca_list): err = f"Invalid statement '{ca}'." n_colons = ca.count(':') if n_colons == 0: contains_condition = False action = ca elif n_colons == 1: before_colon, after_colon = ParseUtil.split1_strip(ca, ':') contains_condition = self._is_condition(before_colon, parameters) if contains_condition: if self.condition is not None: # Cannot have multiple conditions raise ParseException(lineno, f"Multiple conditions ('{self.condition}' and '{before_colon}') found in '{condition_and_actions}'.") self.condition = before_colon action = after_colon else: action = ca elif n_colons == 2: colon_inds = [m.start() for m in re.finditer(':', ca)] if colon_inds[1] - colon_inds[0] == 1: raise ParseException(lineno, err) before_first_colon, after_first_colon = ParseUtil.split1_strip(ca, ':') if not self._is_condition(before_first_colon, parameters): raise ParseException(lineno, err) if self.condition is not None: # Cannot have multiple conditions raise ParseException(lineno, f"Multiple conditions ('{self.condition}' and '{before_first_colon}') found in '{condition_and_actions}'.") contains_condition = True self.condition = before_first_colon action = after_first_colon else: raise ParseException(lineno, err) if contains_condition and found_rowlbl: raise ParseException(lineno, f"Found condition '{self.condition}' after row label '{','.join(goto_list)}'.") found_condition = found_condition or contains_condition is_rowlbl, is_rowlbl_prob = self._is_rowlbl(action, all_linelabels) any_rowlbl_prob = (any_rowlbl_prob or is_rowlbl_prob) if is_rowlbl: found_rowlbl = True goto_list.append(action) goto_list_index.append(i) else: check_action(action, parameters, global_variables, lineno, all_linelabels) if found_rowlbl: err = f"Row label(s) must be the last action(s). Found '{action}' after row-label." raise ParseException(lineno, err) if found_condition: # self.condition is not None: self.conditional_actions.append(action) else: self.unconditional_actions.append(action) is_last_action = (i == len(ca_list) - 1) if is_last_action and not is_rowlbl: raise ParseException(lineno, f"Last action must be a row label, found '{action}'.") if found_condition: self.condition_is_behavior = (self.condition in parameters.get(BEHAVIORS)) cond_depends_on_behavior, err = ParseUtil.depends_on(self.condition, parameters.get(BEHAVIORS)) if err is not None: raise ParseException(lineno, err) if cond_depends_on_behavior and is_help_line: raise ParseException(lineno, "Condition on help line cannot depend on response.") goto_str = ','.join(goto_list) # A deterministic ROWLBL cannot have elif/else continuation if (not found_condition) and found_rowlbl and not any_rowlbl_prob: if logicpart_index < n_logicparts - 1: err = f"The unconditional goto row label '{goto_str}' cannot be continued." raise ParseException(lineno, err) if len(goto_list) > 0: self._parse_goto(goto_str, lineno, all_linelabels, global_variables)
def str_set(self, prop, v_str, variables, phases, all_run_labels, to_be_continued, is_appending=False): """ Parses the specified value (as a string) of the specified parameter and sets the resulting value. The input variables is a Variables object. Returns error message if parsing failed. """ err = check_is_parameter_name(prop) if err: return err # all_phase_labels = phases.labels_set() if prop == kw.BEHAVIORS: return self._parse_behaviors(v_str, variables, is_appending) elif prop == kw.STIMULUS_ELEMENTS: return self._parse_stimulus_elements(v_str, variables, is_appending) elif prop == kw.MECHANISM_NAME: return self._parse_mechanism_name(v_str) elif prop in (kw.START_VSS, kw.ALPHA_VSS): return self._parse_alphastart_vss(prop, v_str, variables, to_be_continued, is_appending) elif prop in (kw.START_W, kw.ALPHA_W, kw.U, kw.LAMBDA): return self._parse_stimulus_values(prop, v_str, variables, to_be_continued, is_appending) elif prop in (kw.BETA, kw.MU, kw.START_V, kw.ALPHA_V): return self._parse_stimulus_response_values( prop, v_str, variables, to_be_continued, is_appending) # Float elif prop in (kw.DISCOUNT, kw.TRACE): v, err = ParseUtil.evaluate(v_str, variables) if err: return err if (v < 0) or (v > 1): return f"Parameter '{prop}' must be a number >=0 and <=1." self.val[prop] = v return None elif prop == kw.BEHAVIOR_COST: return self._parse_behavior_cost(v_str, variables, to_be_continued, is_appending) elif prop == kw.RESPONSE_REQUIREMENTS: return self._parse_response_requirements(v_str, to_be_continued, is_appending) # 'on' or 'off' elif prop in (kw.BIND_TRIALS, kw.CUMULATIVE): v_str_lower = v_str.lower() if v_str_lower not in ('on', 'off'): return "Parameter '{}' must be 'on' or 'off'.".format(prop) self.val[prop] = v_str_lower return None # Positive integer elif prop == kw.N_SUBJECTS: v, err = ParseUtil.parse_posint(v_str, variables) if err: return err if not v: return "Parameter {} must be a positive integer.".format( kw.N_SUBJECTS) self.val[kw.N_SUBJECTS] = v return None # Any nonempty (after strip) string elif prop in (kw.TITLE, kw.SUBPLOTTITLE): if to_be_continued: # Add the removed comma v_str = v_str + "," self.val[prop] = v_str return None # 'average', 'all' or 1-based index elif prop == kw.SUBJECT: return self._parse_subject(v_str, variables) # 'all' or s1->b1->s2->..., s=se1,se2,... elif prop == kw.XSCALE: return self._parse_xscale(v_str, phases) # 'subset' or 'exact' elif prop in (kw.MATCH, kw.XSCALE_MATCH): if v_str.lower() not in ('subset', 'exact'): return "Parameter {} must be 'subset' or 'exact'.".format(prop) self.val[prop] = v_str return None # 'all' or cs-list of phase labels elif prop == kw.PHASES: return self._parse_phases(v_str) # , all_phase_labels) # String (@run-labels) (for postprocessing) elif prop == kw.RUNLABEL: if v_str not in all_run_labels: return "Invalid @RUN-label {}".format(v_str) self.val[kw.RUNLABEL] = v_str return None # Valid path to writable file elif prop == kw.FILENAME: filename = v_str try: file = open(filename, 'w', newline='') except Exception as ex: return str(ex) finally: file.close() try: os.remove(filename) except FileNotFoundError: pass self.val[kw.FILENAME] = filename return None
def _is_valid_value(self, value_str): return ParseUtil.evaluate(value_str, self)
def parse(self): if len(self.lines) == 0: raise ParseException(1, "Script is empty.") prop = None curr_phase_label = None in_prop = False in_variables = False in_phase = False for lineno0, line_orig in enumerate(self.lines): line = line_orig lineno = lineno0 + 1 # Handle empty line if len(line) == 0: continue line_endswith_comma = line.endswith(',') if line_endswith_comma: line = line[:-1] line_parser = LineParser(line, self.variables) line_parser.parse() linesplit_colon = line_parser.linesplit_colon linesplit_equal = line_parser.linesplit_equal linesplit_space = line_parser.linesplit_space if in_prop or in_variables or in_phase: parse_this_line_done = True err = None if in_prop: in_prop = line_endswith_comma err = self.parameters.str_append(prop, line, self.variables, self.phases, self.all_run_labels, line_endswith_comma) elif in_variables: in_variables = line_endswith_comma err = self.variables.add_cs_varvals(line, self.parameters) elif in_phase: in_phase = line_parser.line_type is None if in_phase: self.phases.append_line(curr_phase_label, line, lineno) else: parse_this_line_done = False # Phase with label curr_phase_label is complete, parse it self.phases.parse_phase(curr_phase_label, self.parameters, self.variables) if err: raise ParseException(lineno, err) if parse_this_line_done: continue if line_parser.line_type == LineParser.PARAMETER: # Handle line that sets a parameter (e.g. "prop : val") prop = line_parser.param if len(linesplit_colon) == 1 and len(linesplit_equal) == 1: raise ParseException( lineno, f"Parameter '{prop}' is not specified.") first_colon_index = line_orig.find(':') first_equal_index = line_orig.find('=') if first_equal_index > 0 and first_colon_index > 0: if first_colon_index < first_equal_index: # u : a=1, b=2 possible_val = linesplit_colon[1].strip() else: # u = a:1, b:2 possible_val = linesplit_equal[1].strip() elif first_equal_index > 0 and first_colon_index < 0: # u = 2 possible_val = linesplit_equal[1].strip() elif first_colon_index > 0 and first_equal_index < 0: # u : 2 possible_val = linesplit_colon[1].strip() if len(possible_val) == 0: raise ParseException( lineno, f"Parameter '{prop}' is not specified.") if line_endswith_comma: if not self.parameters.may_end_with_comma(prop): raise ParseException( lineno, "Value for {} may not end by comma.".format(prop)) in_prop = self.parameters.is_csv(prop) err = self.parameters.str_set(prop, possible_val, self.variables, self.phases, self.all_run_labels, line_endswith_comma) if err: raise ParseException(lineno, err) continue elif line_parser.line_type == LineParser.VARIABLES: if len(linesplit_space) == 1: raise ParseException(lineno, "@VARIABLES not specified.") in_variables = line_endswith_comma cs_varvals = linesplit_space[1].strip() err = self.variables.add_cs_varvals(cs_varvals, self.parameters) if err: raise ParseException(lineno, err) else: continue elif line_parser.line_type == LineParser.PREV_DEFINED_VARIABLE: err = self.variables.add_cs_varvals(line.strip(), self.parameters) if err: raise ParseException(lineno, err) else: continue elif line_parser.line_type == LineParser.RUN: if len(linesplit_space) == 1: raise ParseException( lineno, "@RUN line must have the form '@RUN phases [runlabel:label]." ) after_run = linesplit_space[1].strip() run_label, run_phase_labels = self._parse_run( after_run, lineno) world = self.phases.make_world(run_phase_labels) run_parameters = copy.deepcopy( self.parameters) # Params may change betweeen runs mechanism_obj, err = run_parameters.make_mechanism_obj() if err: raise ParseException(lineno, err) n_subjects = run_parameters.get(kw.N_SUBJECTS) bind_trials = run_parameters.get(kw.BIND_TRIALS) is_ok, err, err_lineno = mechanism_obj.check_compatibility_with_world( world) if err: raise ParseException(err_lineno, err) run = Run(run_label, world, mechanism_obj, n_subjects, bind_trials) self.runs.add(run, run_label, lineno) continue elif line_parser.line_type == LineParser.PHASE: gen_err = "@PHASE line must have the form '@PHASE label stop:condition'." if len(linesplit_space) == 1: raise ParseException(lineno, gen_err) lbl_and_stopcond = linesplit_space[1].strip() curr_phase_label, stop_condition = ParseUtil.split1( lbl_and_stopcond) inherited_from = None if '(' in curr_phase_label and curr_phase_label.endswith(')'): lind = curr_phase_label.index('(') inherited_from = curr_phase_label[(lind + 1):-1] curr_phase_label = curr_phase_label[0:lind] if not self.phases.contains(inherited_from): raise ParseException( lineno, f"Invalid phase label '{inherited_from}'.") if self.phases.contains(curr_phase_label): raise ParseException( lineno, f"Redefinition of phase '{curr_phase_label}'.") if not curr_phase_label.isidentifier(): raise ParseException( lineno, f"Phase label '{curr_phase_label}' is not a valid identifier." ) if stop_condition is None: raise ParseException(lineno, gen_err) stop, condition = ParseUtil.split1_strip(stop_condition, ':') if stop != "stop" or condition is None or len(condition) == 0: raise ParseException( lineno, "Phase stop condition must have the form 'stop:condition'." ) in_phase = True if inherited_from: self.phases.inherit_from(inherited_from, curr_phase_label, condition, lineno) else: self.phases.add_phase(curr_phase_label, condition, lineno) continue elif line_parser.line_type == LineParser.FIGURE: figure_title, mpl_prop = self._parse_figure( lineno, linesplit_space) figure_cmd = FigureCmd(figure_title, mpl_prop) self.postcmds.add(figure_cmd) elif line_parser.line_type == LineParser.SUBPLOT: subplotspec, subplot_title, mpl_prop = self._parse_subplot( lineno, linesplit_space) subplot_cmd = SubplotCmd(subplotspec, subplot_title, mpl_prop) self.postcmds.add(subplot_cmd) elif line_parser.line_type == LineParser.PLOT: expr, mpl_prop, expr0 = self._parse_plot( lineno, linesplit_space) cmd = linesplit_space[0].lower() plot_parameters = copy.deepcopy( self.parameters) # Params may change betweeen plot self._evalparse(lineno, plot_parameters) plot_cmd = PlotCmd(cmd, expr, expr0, plot_parameters, mpl_prop) self.postcmds.add(plot_cmd) elif line_parser.line_type == LineParser.LEGEND: mpl_prop = self._parse_legend(lineno, linesplit_space) legend_cmd = LegendCmd(mpl_prop) self.postcmds.add(legend_cmd) elif line_parser.line_type == LineParser.EXPORT: expr, filename, expr0 = self._parse_export( lineno, linesplit_space) cmd = linesplit_space[0].lower() export_parameters = copy.deepcopy( self.parameters) # Params may change betweeen exports self._evalparse(lineno, export_parameters) export_parameters.val[ kw. FILENAME] = filename # If filename only given on export line export_cmd = ExportCmd(lineno, cmd, expr, expr0, export_parameters) self.postcmds.add(export_cmd) else: raise ParseException(lineno, f"Invalid expression '{line}'.")
def _parse_response_requirements(self, v_str, to_be_continued, is_appending): if not self.val[kw.STIMULUS_ELEMENTS]: return f"The parameter 'stimulus_elements' must be assigned before the parameter '{kw.RESPONSE_REQUIREMENTS}'." if not self.val[kw.BEHAVIORS]: return f"The parameter 'behaviors' must be assigned before the parameter '{kw.RESPONSE_REQUIREMENTS}'." if not is_appending: self.val[kw.RESPONSE_REQUIREMENTS] = dict() for b in self.val[kw.BEHAVIORS]: self.val[kw.RESPONSE_REQUIREMENTS][b] = None rrs = ParseUtil.comma_split_sq(v_str) for rr in rrs: if rr.count(':') != 1: return "Expected 'behavior:stimulus_element', got '{}'.".format( rr) b, s = rr.split(':') b = b.strip() s = s.strip() if len(b) == 0 or len(s) == 0: return "Expected 'behavior:stimulus_element', got '{}'.".format( rr) if b not in self.val[kw.BEHAVIORS]: return "Unknown behavior name '{}'.".format(b) if self.val[kw.RESPONSE_REQUIREMENTS][b] is not None: return "Duplication of behavior '{}' in {}.".format( b, kw.RESPONSE_REQUIREMENTS) if '[' in s or ']' in s: if s.count('[') != 1 or s.count( ']') != 1 or s[0] != '[' or s[-1] != ']': return "Malformed expression '{}'.".format(s) s = s[1:-1] # Strip the '['and the ']' es = s.split(',') for e in es: e = e.strip() if e not in self.val[kw.STIMULUS_ELEMENTS]: return "Unknown stimulus element '{}'.".format(e) self._response_requirements_add_element(b, e) else: if s not in self.val[kw.STIMULUS_ELEMENTS]: return "Unknown stimulus element '{}'.".format(s) self._response_requirements_add_element(b, s) if not to_be_continued: # For the unrestricted behaviors, add all stimulus elements for b in self.val[kw.RESPONSE_REQUIREMENTS]: if self.val[kw.RESPONSE_REQUIREMENTS][b] is None: self.val[kw.RESPONSE_REQUIREMENTS][b] = set( self.val[kw.STIMULUS_ELEMENTS]) # Check that each stimulus element has at least one feasible response stimulus_elements_in_rr = [] for stimulus_list in self.val[kw.RESPONSE_REQUIREMENTS].values(): stimulus_elements_in_rr.extend(stimulus_list) if set(stimulus_elements_in_rr) != set( self.val[kw.STIMULUS_ELEMENTS]): elements_without_response = set(self.val[ kw.STIMULUS_ELEMENTS]) - set(stimulus_elements_in_rr) elements_without_response = list(elements_without_response) elements_without_response.sort( ) # To make error message testable elements_without_response_str = make_readable_list_of_strings( elements_without_response) err = f"Invalid {kw.RESPONSE_REQUIREMENTS}: " if len(elements_without_response) == 1: return err + f"Stimulus element {elements_without_response_str} has no possible responses." else: return err + f"Stimulus elements {elements_without_response_str} have no possible responses." return None # No error
def _parse_alphastart_vss(self, NAME, vss_str, variables, to_be_continued, is_appending): """ Parse the string vss_str with a start_vss/alpha_vss specification. Example: "S1->S2: 1.23, S2->S1:3.45, default:1" sets the parameter to {('S1','S2'):1.23, ('S2','S1'):3.45, ('S1','S1'):1, ('S2','S2'):1} under the assumption that stimulus_elements = {'S1', 'S2'} """ if not self.val[kw.STIMULUS_ELEMENTS]: return f"The parameter 'stimulus_elements' must be assigned before the parameter '{NAME}'." # Create and populate the struct with None values if not is_appending: self.val[NAME] = dict() for e1 in self.val[kw.STIMULUS_ELEMENTS]: for e2 in self.val[kw.STIMULUS_ELEMENTS]: self.val[NAME][(e1, e2)] = None self.val[NAME][kw.DEFAULT] = None single_vss, _ = ParseUtil.evaluate(vss_str, variables) if single_vss is not None: if is_appending: return f"A single value for '{NAME}' cannot follow other values." elif to_be_continued: return f"A single value for '{NAME}' cannot be followed by other values." else: for key in self.val[NAME]: self.val[NAME][key] = single_vss self.val[NAME].pop(kw.DEFAULT) else: vs = ParseUtil.comma_split(vss_str) vs = [x.strip() for x in vs] for ee_str in vs: # eb_v_str is 'e1->e2:value' or 'default:value' if ee_str.count(':') != 1: return f"Expected 'x->y:value' or 'default:value' in '{NAME}', got '{ee_str}'." ee, v_str = ee_str.split(':') ee = ee.strip() v_str = v_str.strip() v, err = ParseUtil.evaluate(v_str, variables) if err: return f"Invalid value '{v_str}' for '{ee}' in parameter '{NAME}'." if ee == kw.DEFAULT: if self.val[NAME][kw.DEFAULT] is not None: return f"Default value for '{NAME}' can only be stated once." self.val[NAME][kw.DEFAULT] = v elif ee.count('->') == 1: e1, e2 = ee.split('->') if e1 not in self.val[kw.STIMULUS_ELEMENTS]: return f"Error in parameter '{NAME}': '{e1}' is an invalid stimulus element." if e2 not in self.val[kw.STIMULUS_ELEMENTS]: return f"Error in parameter '{NAME}': '{e2}' is an invalid stimulus element." if self.val[NAME][(e1, e2)] is not None: return f"Duplicate of {e1}->{e2} in '{NAME}'." self.val[NAME][(e1, e2)] = v else: return f"Invalid string '{ee}' in parameter '{NAME}'." if not to_be_continued: # Set the default value for non-set stimulus-stimulus pairs err = self._set_default_values(NAME) if err: return err return None # No error
def _parse_stimulus_response_values(self, NAME, sr_str, variables, to_be_continued, is_appending): """ Parse the string sr_str with a value for stimulus-response pairs. Example: "S1->R1: 1.23, S2->R1:3.45, default:1" sets the parameter to {('S1','R1'):1.23, ('S1','R2'):1, ('S2','R1'):3.45, ('S2','R2'):1} under the assumption that behaviors = {'R1', 'R2'} and stimulus_elements = {'S1', 'S2'} """ if not self.val[kw.STIMULUS_ELEMENTS]: return f"The parameter 'stimulus_elements' must be assigned before the parameter '{NAME}'." if not self.val[kw.BEHAVIORS]: return f"The parameter 'behaviors' must be assigned before the parameter '{NAME}'." # Create and populate the struct with None values if not is_appending: self.val[NAME] = dict() for e in self.val[kw.STIMULUS_ELEMENTS]: for b in self.val[kw.BEHAVIORS]: self.val[NAME][(e, b)] = None self.val[NAME][kw.DEFAULT] = None single_v, _ = ParseUtil.evaluate(sr_str, variables) if single_v is not None: if is_appending: return f"A single value for '{NAME}' cannot follow other values." elif to_be_continued: return f"A single value for '{NAME}' cannot be followed by other values." else: for key in self.val[NAME]: self.val[NAME][key] = single_v self.val[NAME].pop(kw.DEFAULT) else: vs = ParseUtil.comma_split(sr_str) vs = [x.strip() for x in vs] for eb_v_str in vs: # eb_v_str is 'e->b:value' or 'default:value' if eb_v_str.count(':') != 1: return f"Expected 'x->y:value' or 'default:value' in '{NAME}', got '{eb_v_str}'." eb, v_str = eb_v_str.split(':') eb = eb.strip() v_str = v_str.strip() v, err = ParseUtil.evaluate(v_str, variables) if err: return f"Invalid value '{v_str}' for '{eb}' in parameter '{NAME}'." if eb == kw.DEFAULT: if self.val[NAME][kw.DEFAULT] is not None: return f"Default value for '{NAME}' can only be stated once." self.val[NAME][kw.DEFAULT] = v elif eb.count('->') == 1: e, b = eb.split('->') if e not in self.val[kw.STIMULUS_ELEMENTS]: return f"Error in parameter '{NAME}': '{e}' is an invalid stimulus element." if b not in self.val[kw.BEHAVIORS]: return f"Error in parameter '{NAME}': '{b}' is an invalid behavior name." if self.val[NAME][(e, b)] is not None: return f"Duplicate of {e}->{b} in '{NAME}'." self.val[NAME][(e, b)] = v else: return f"Invalid string '{eb}' in parameter '{NAME}'." if not to_be_continued: # Set the default value for non-set stimulus-behavior pairs err = self._set_default_values(NAME) if err: return err return None # No error