diff --git a/psyneulink/components/component.py b/psyneulink/components/component.py index b97273a5e5f..160f60a2f6f 100644 --- a/psyneulink/components/component.py +++ b/psyneulink/components/component.py @@ -358,7 +358,8 @@ class `UserList ` and their current `LogCondition`. diff --git a/psyneulink/components/functions/function.py b/psyneulink/components/functions/function.py index de40e95c690..717c55ab233 100644 --- a/psyneulink/components/functions/function.py +++ b/psyneulink/components/functions/function.py @@ -192,7 +192,8 @@ EXPONENTIAL_DIST_FUNCTION, EXPONENTIAL_FUNCTION, EXPONENTS, FHN_INTEGRATOR_FUNCTION, FULL_CONNECTIVITY_MATRIX, \ FUNCTION, FUNCTION_OUTPUT_TYPE, FUNCTION_OUTPUT_TYPE_CONVERSION, FUNCTION_PARAMS, GAIN, GAMMA_DIST_FUNCTION, \ HEBBIAN_FUNCTION, HIGH, HOLLOW_MATRIX, IDENTITY_MATRIX, INCREMENT, INITIALIZER, \ - INITIALIZING, INPUT_STATES, INTEGRATOR_FUNCTION, INTEGRATOR_FUNCTION_TYPE, INTERCEPT, LEARNING_FUNCTION_TYPE, \ + INITIALIZING, INPUT_STATES, INTEGRATOR_FUNCTION, INTEGRATOR_FUNCTION_TYPE, INTERCEPT, \ + LEARNING, LEARNING_FUNCTION_TYPE, \ LEARNING_RATE, LINEAR_COMBINATION_FUNCTION, LINEAR_FUNCTION, LINEAR_MATRIX_FUNCTION, LOGISTIC_FUNCTION, LOW, MATRIX, \ MATRIX_KEYWORD_NAMES, MATRIX_KEYWORD_VALUES, MAX_INDICATOR, MAX_VAL, NOISE, NORMAL_DIST_FUNCTION, \ OBJECTIVE_FUNCTION_TYPE, OFFSET, OPERATION, ORNSTEIN_UHLENBECK_INTEGRATOR_FUNCTION, OUTPUT_STATES, OUTPUT_TYPE, \ @@ -1557,14 +1558,14 @@ def _validate_params(self, request_set, target_set=None, context=None): if WEIGHTS in target_set and target_set[WEIGHTS] is not None: target_set[WEIGHTS] = np.atleast_2d(target_set[WEIGHTS]).reshape(-1, 1) - if EXECUTING in context: + if any(c in context for c in {EXECUTING, LEARNING}): if len(target_set[WEIGHTS]) != len(self.instance_defaults.variable): raise FunctionError("Number of weights ({0}) is not equal to number of items in variable ({1})". format(len(target_set[WEIGHTS]), len(self.instance_defaults.variable.shape))) if EXPONENTS in target_set and target_set[EXPONENTS] is not None: target_set[EXPONENTS] = np.atleast_2d(target_set[EXPONENTS]).reshape(-1, 1) - if EXECUTING in context: + if (c in context for c in {EXECUTING, LEARNING}): if len(target_set[EXPONENTS]) != len(self.instance_defaults.variable): raise FunctionError("Number of exponents ({0}) does not equal number of items in variable ({1})". format(len(target_set[EXPONENTS]), len(self.instance_defaults.variable.shape))) @@ -1578,7 +1579,7 @@ def _validate_params(self, request_set, target_set=None, context=None): else: raise FunctionError("{} param of {} ({}) must be a scalar or an np.ndarray". format(SCALE, self.name, scale)) - if EXECUTING in context: + if (c in context for c in {EXECUTING, LEARNING}): if (isinstance(scale, np.ndarray) and (scale.size != self.instance_defaults.variable.size or scale.shape != self.instance_defaults.variable.shape)): @@ -1596,7 +1597,7 @@ def _validate_params(self, request_set, target_set=None, context=None): else: raise FunctionError("{} param of {} ({}) must be a scalar or an np.ndarray". format(OFFSET, self.name, offset)) - if EXECUTING in context: + if (c in context for c in {EXECUTING, LEARNING}): if (isinstance(offset, np.ndarray) and (offset.size != self.instance_defaults.variable.size or offset.shape != self.instance_defaults.variable.shape)): @@ -1996,14 +1997,14 @@ def _validate_params(self, request_set, target_set=None, context=None): if WEIGHTS in target_set and target_set[WEIGHTS] is not None: target_set[WEIGHTS] = np.atleast_2d(target_set[WEIGHTS]).reshape(-1, 1) - if EXECUTING in context: + if (c in context for c in {EXECUTING, LEARNING}): if len(target_set[WEIGHTS]) != len(self.instance_defaults.variable): raise FunctionError("Number of weights ({0}) is not equal to number of items in variable ({1})". format(len(target_set[WEIGHTS]), len(self.instance_defaults.variable.shape))) if EXPONENTS in target_set and target_set[EXPONENTS] is not None: target_set[EXPONENTS] = np.atleast_2d(target_set[EXPONENTS]).reshape(-1, 1) - if EXECUTING in context: + if (c in context for c in {EXECUTING, LEARNING}): if len(target_set[EXPONENTS]) != len(self.instance_defaults.variable): raise FunctionError("Number of exponents ({0}) does not equal number of items in variable ({1})". format(len(target_set[EXPONENTS]), len(self.instance_defaults.variable.shape))) @@ -2017,7 +2018,7 @@ def _validate_params(self, request_set, target_set=None, context=None): else: raise FunctionError("{} param of {} ({}) must be a scalar or an np.ndarray". format(SCALE, self.name, scale)) - if EXECUTING in context: + if (c in context for c in {EXECUTING, LEARNING}): if (isinstance(scale, np.ndarray) and (scale.size != self.instance_defaults.variable.size or scale.shape != self.instance_defaults.variable.shape)): @@ -2035,7 +2036,7 @@ def _validate_params(self, request_set, target_set=None, context=None): else: raise FunctionError("{} param of {} ({}) must be a scalar or an np.ndarray". format(OFFSET, self.name, offset)) - if EXECUTING in context: + if (c in context for c in {EXECUTING, LEARNING}): if (isinstance(offset, np.ndarray) and (offset.size != self.instance_defaults.variable.size or offset.shape != self.instance_defaults.variable.shape)): diff --git a/psyneulink/components/mechanisms/mechanism.py b/psyneulink/components/mechanisms/mechanism.py index be584be6c25..a6065ead21a 100644 --- a/psyneulink/components/mechanisms/mechanism.py +++ b/psyneulink/components/mechanisms/mechanism.py @@ -787,7 +787,7 @@ class `UserList ` -- a dictionary with the items that can be logged in a Component's `log `; the key for each entry is the name of a Component, and the value is it current `LogCondition`. .. - * `set_log_conditions ` -- used to assign the LogCondition for one or more Components. Components can be - specified by their names, a reference to the Component object, in a tuple that specifies the `LogCondition` to - assign to that Component, or in a list with a `LogCondition` to be applied to multiple items at once. + * `set_log_conditions ` -- used to assign the LogCondition for one or more Components. + Components can be specified by their names, a reference to the Component object, in a tuple that specifies the + `LogCondition` to assign to that Component, or in a list with a `LogCondition` to be applied to multiple items + at once. .. * `log_values ` -- used to the `value ` of one or more Components in the Log programmatically ("manually"). Components can be specified by their names or references to the objects. @@ -108,12 +109,21 @@ ~~~~~~~~~~~~~ Configuring a Component to be logged is done using a `LogCondition`, that specifies the conditions under which its -`value ` should be entered in its Log. These can be specified in the `set_log_conditions ` -method of a Log, or directly by specifying a LogCondition for the value a Component's `logPref ` item -of its `prefs ` attribute. The former is easier, and allows multiple Components to be specied at -once, while the latter affords more control over the specification (see `Preferences`). LogConditions are treated as -binary "flags", and can be combined to permit logging under more than one condition, using bitwise operators on -LogConditions (e.g., LogCondition.EXECUTION | LogCondition.LEARNING). +`value ` should be entered in its Log. These can be specified in the `set_log_conditions +` method of a Log, or directly by specifying a LogCondition for the value a Component's +`logPref ` item of its `prefs ` attribute. The former is easier, and allows +multiple Components to be specied at once, while the latter affords more control over the specification (see +`Preferences`). LogConditions are treated as binary "flags", and can be combined to permit logging under more than +one condition using bitwise operators on LogConditions. For convenience, they can also be referred to by their +names, and combined by specifying a list. For example, all of the following specify that the `value +` of ``my_mech`` be logged both during execution and learning:: + + >>> import psyneulink as pnl + >>> my_mech = pnl.TransferMechanism() + >>> my_mech.set_log_conditions('value', pnl.LogCondition.EXECUTION | pnl.LogCondition.LEARNING) + >>> my_mech.set_log_conditions('value', pnl.LogCondition.EXECUTION + pnl.LogCondition.LEARNING) + >>> my_mech.set_log_conditions('value', [pnl.EXECUTION, LEARNING]) + .. note:: Currently, the `VALIDATION` `LogCondition` is not implemented. @@ -128,8 +138,7 @@ COMMENT: FIX: THIS EXAMPLE CAN'T CURRENTLY BE EXECUTED AS IT PERMANENTLY SETS THE LogPref FOR ALL TransferMechanism COMMENT - >>> import psyneulink as pnl - >>> T = pnl.TransferMechanism( + >>> my_mech = pnl.TransferMechanism( ... prefs={pnl.LOG_PREF: pnl.PreferenceEntry(pnl.LogCondition.INITIALIZATION, pnl.PreferenceLevel.INSTANCE)}) .. hint:: @@ -401,9 +410,7 @@ class LogCondition(IntEnum): """Record all value assignments during any execution of the Component.""" PROCESSING = 1<<4 # 16 """Record all value assignments during processing phase of Composition execution.""" - # FIX: IMPLEMENT EXECUTION+LEARNING CONDITION - # LEARNING = 1<<5 # 32 - LEARNING = (1<<5) + EXECUTION # 40 + LEARNING = 1<<5 # 32 """Record all value assignments during learning phase of Composition execution.""" CONTROL = 1<<6 # 64 """Record all value assignments during control phase of Composition execution.""" @@ -425,6 +432,27 @@ class LogCondition(IntEnum): # def _log_level_max(cls): # return max([cls[i].value for i in list(cls.__members__) if cls[i] is not LogCondition.ALL_ASSIGNMENTS]) + @classmethod + def _get_condition_string(cls, condition, string=None): + """Return string with the names of all flags that are set in **condition**, prepended by **string**""" + if string: + string += ": " + else: + string = "" + flagged_items = [] + # If OFF or ALL_ASSIGNMENTS, just return that + if condition in (LogCondition.ALL_ASSIGNMENTS, LogCondition.OFF): + return condition.name + # Otherwise, append each flag's name to the string + for c in list(cls.__members__): + # Skip ALL_ASSIGNMENTS (handled above) + if c is LogCondition.ALL_ASSIGNMENTS.name: + continue + if LogCondition[c] & condition: + flagged_items.append(c) + string += ", ".join(flagged_items) + return string + LogEntry = namedtuple('LogEntry', 'time, context, value') @@ -694,11 +722,18 @@ def set_log_conditions(self, items, log_condition=LogCondition.EXECUTION): def assign_log_level(item, level): - try: - level = LogCondition[level] if isinstance(level, str) else level - except KeyError: - raise LogError("\'{}\' is not a value of {}". - format(level, LogCondition.__name__)) + # Handle multiple level assignments (as LogConditions or strings in a list) + if not isinstance(level, list): + level = [level] + levels = LogCondition.OFF + for l in level: + try: + l = LogCondition[l] if isinstance(l, str) else l + except KeyError: + raise LogError("\'{}\' is not a value of {}". + format(l, LogCondition.__name__)) + levels |= l + level = levels if not item in self.loggable_items: raise LogError("\'{0}\' is not a loggable item for {1} (try using \'{1}.log.add_entries()\')". @@ -720,20 +755,20 @@ def assign_log_level(item, level): for item in items: if isinstance(item, (str, Component)): - # self.add_entries(item) if isinstance(item, Component): item = item.name assign_log_level(item, log_condition) else: - # self.add_entries(item[0]) assign_log_level(item[0], item[1]) - def _log_value(self, value, context=None): + def _log_value(self, value, time=None, context=None): """Add LogEntry to an entry in the Log - Identifies the context in which the call is being made, which is assigned to the context field of the - `LogEntry`, along with the current time stamp and value itself - + If **value** is a LogEntry, it is assigned to the entry + If **context** is a LogCondition, it is used to determine whether the entry should be made; + **time** must be passed; the name of the LogCondition(s) specified are assigned to the context of LogEntry + Otherwise, uses string (or Component) passed in **context**, or searches stack (see note) to determine the + context, and uses that to determine the scheduler and, from that, the time; If value is None, uses owner's `value ` attribute. .. note:: @@ -746,57 +781,69 @@ def _log_value(self, value, context=None): from psyneulink.components.component import Component programmatic = False + if isinstance(value, LogEntry): + self.entries[self.owner.name] = value - if context is COMMAND_LINE: - # If _log_value is being called programmatically, - # flag for later and set context to None to get context from the stack - programmatic = True - context = None - - # Get context from the stack - if context is None: - curr_frame = inspect.currentframe() - prev_frame = inspect.getouterframes(curr_frame, 2) - i = 1 - # Search stack for first frame (most recent call) with a context specification - while context is None: - try: - context = inspect.getargvalues(prev_frame[i][0]).locals['context'] - except KeyError: - # Try earlier frame - i += 1 - except IndexError: - # Ran out of frames, so just set context to empty string - context = "" - else: - break - - # If context is a Component object, it must be during its initialization, so assign accordingly: - if isinstance(context, Component): - context = "{} of {}".format(INITIALIZING, context.name) - - # No context was specified in any frame - if context is None: - raise LogError("PROGRAM ERROR: No context specification found in any frame") - - if not isinstance(context, str): - raise LogError("PROGRAM ERROR: Unrecognized context specification ({})".format(context)) - - # Context is an empty string, but called programatically - if not context and programmatic: - context = COMMAND_LINE - # context = self.owner.prev_context + "FROM " + COMMAND_LINE - # context = self.owner.prev_context - - context_flags = _get_log_context(context) + else: - log_pref = self.owner.prefs.logPref if self.owner.prefs else None + if isinstance(context, LogCondition): + context_flags = context + context = LogCondition._get_condition_string(context) + if not time: + raise LogError("Use of LogCondition ({}) by {} to specify context requires specificatiom of time". + format(context.name, self.owner.name )) - # Log value if logging condition is satisfied or called for programmatically - if (log_pref and log_pref == context_flags) or context_flags & LogCondition.COMMAND_LINE: - # FIX: IMPLEMENT EXECUTION+LEARNING CONDITION - # if log_pref and log_pref | context_flags: - self.entries[self.owner.name] = LogEntry(self._get_time(context, context_flags), context, value) + # Get context + else: + if context is COMMAND_LINE: + # If _log_value is being called programmatically, + # flag for later and set context to None to get context from the stack + programmatic = True + context = None + + # Get context from the stack + if context is None: + curr_frame = inspect.currentframe() + prev_frame = inspect.getouterframes(curr_frame, 2) + i = 1 + # Search stack for first frame (most recent call) with a context specification + while context is None: + try: + context = inspect.getargvalues(prev_frame[i][0]).locals['context'] + except KeyError: + # Try earlier frame + i += 1 + except IndexError: + # Ran out of frames, so just set context to empty string + context = "" + else: + break + + # If context is a Component object, it must be during its initialization, so assign accordingly: + if isinstance(context, Component): + context = "{} of {}".format(INITIALIZING, context.name) + + # No context was specified in any frame + if context is None: + raise LogError("PROGRAM ERROR: No context specification found in any frame") + + if not isinstance(context, str): + raise LogError("PROGRAM ERROR: Unrecognized context specification ({})".format(context)) + + # Context is an empty string, but called programatically + if not context and programmatic: + context = COMMAND_LINE + # context = self.owner.prev_context + "FROM " + COMMAND_LINE + # context = self.owner.prev_context + + context_flags = _get_log_context(context) + + log_pref = self.owner.prefs.logPref if self.owner.prefs else None + + # Get time and log value if logging condition is satisfied or called for programmatically + if (log_pref and log_pref & context_flags) or context_flags & LogCondition.COMMAND_LINE: + time = time or self._get_time(context, context_flags) + self.entries[self.owner.name] = LogEntry(time, context, value) if context is not COMMAND_LINE: self.owner.prev_context = context @@ -985,12 +1032,14 @@ def print_entries(self, entries:tc.optional(tc.any(str, list, is_component))=ALL, width:int=120, display:tc.any(tc.enum(TIME, CONTEXT, VALUE, ALL), list)=ALL, + long_context=False ): """ print_entries( \ entries=ALL, \ width=120, \ display=None \ + long_context=Full \ ) Print summary of the Log's entries in a (human-readable) table format. @@ -1013,6 +1062,10 @@ def print_entries(self, specified items; the widths of the columns for the items is dynamically adjusted, based on how many are specified, allowing more information about one to be shown by omitting others (this is useful if the context strings are long and/or the values are arrays). + + long_context : bool : default False + specifies the use of the full context string in the display; this can be informative, but can also take up + more space in each line of the display. """ @@ -1032,6 +1085,7 @@ class options(IntEnum): if not isinstance(display, list): display = [display] + # Set option_flags for specified options option_flags = options.NONE if TIME in display: option_flags |= options.TIME @@ -1042,6 +1096,7 @@ class options(IntEnum): if ALL in display: option_flags = options.ALL + # Default widths full_width = width item_name_width = 15 time_width = 10 @@ -1052,7 +1107,20 @@ class options(IntEnum): value_spacer = " ".ljust(value_spacer_width) base_width = item_name_width - # FIX: COULD "ALGORITHMIZE" THIS: + # Set context_width based on long_context option (length of context string) or context flags + if option_flags & options.CONTEXT: + c_width = 0 + for entry in entries: + for datum in self.logged_entries[entry]: + if long_context: + context = datum.context + else: + context = LogCondition._get_condition_string(_get_log_context(datum.context)) + c_width = max(c_width, len(context)) + context_width = min(context_width, c_width) + + # Set other widths based on options: + # FIX: "ALGORITHMIZE" THIS: if option_flags == options.TIME: pass elif option_flags == options.CONTEXT: @@ -1067,7 +1135,7 @@ class options(IntEnum): context_width = full_width - value_width value_width = full_width - context_width elif option_flags == options.ALL: - pass + value_width = full_width - context_width else: raise LogError("PROGRAM ERROR: unrecognized state of option_flags: {}".format(option_flags)) @@ -1097,23 +1165,36 @@ class options(IntEnum): else: import numpy as np for i, item in enumerate(datum): + time, context, value = item - if len(context) > context_width: - context = context[:context_width-3] + "..." - value = str(value).replace('\n',',') - if len(value) > value_width: - value = value[:value_width-3].rstrip() + "..." - time_str = _time_string(time) + entry_name = self._alias_owner_name(entry_name) data_str = repr(entry_name).ljust(item_name_width, spacer) + if options.TIME & option_flags: + time_str = _time_string(time) data_str = data_str + time_str.ljust(time_width) + if options.CONTEXT & option_flags: - data_str = data_str + repr(context).ljust(context_width, spacer) + if long_context: + # Use context from LogEntry + context = repr(context) + else: + # Get names of LogCondition flag(s) from parse of context string + context = LogCondition._get_condition_string(_get_log_context(context)) + if len(context) > context_width: + context = context[:context_width-3] + "..." + data_str = data_str + context.ljust(context_width, spacer) + if options.VALUE & option_flags: + value = str(value).replace('\n',',') + if len(value) > value_width: + value = value[:value_width-3].rstrip() + "..." format_str = "{{:2.{0}}}".format(value_width) data_str = data_str + value_spacer + format_str.format(value).ljust(value_width) + print(data_str) + if len(datum) > 1: print("\n") @@ -1237,25 +1318,26 @@ def nparray(self, time_col = iter(time_values) for datum in self.logged_entries[entry]: if time_values: - # MODIFIED 12/14/17 OLD: - while datum.time != next(time_col,None): - row.append(None) - value = None if datum.value is None else np.array(datum.value).tolist() - row.append(value) - # # MODIFIED 12/14/17 NEW: - # for i in range(len(time_values)): - # time = next(time_col,None) - # if time is None: - # break - # if datum.time != time: - # row.append(None) - # continue - # value = None if datum.value is None else datum.value.tolist() - # row.append(value) - # break - # else: - # value = None if datum.value is None else datum.value.tolist() - # row.append(value) + # time_col = iter(time_values) + # # MODIFIED 12/14/17 OLD: + # while datum.time != next(time_col,None): + # row.append(None) + # value = None if datum.value is None else np.array(datum.value).tolist() + # row.append(value) + # MODIFIED 12/14/17 NEW: + for i in range(len(time_values)): + time = next(time_col,None) + if time is None: + break + if datum.time != time: + row.append(None) + continue + value = None if datum.value is None else datum.value.tolist() + row.append(value) + break + else: + value = None if datum.value is None else datum.value.tolist() + row.append(value) # MODIFIED 12/14/17 END if header: @@ -1392,10 +1474,12 @@ def loggable_items(self): for c in self.loggable_components: name = self._alias_owner_name(c.name) try: - log_pref = c.logPref.name + # log_pref_names = c.logPref.name + log_pref_names = LogCondition._get_condition_string(c.logPref) except: - log_pref = None - loggable_items[name] = log_pref + log_pref_names = None + # log_pref_names = LogCondition._get_condition_string(c.logPref) + loggable_items[name] = log_pref_names return loggable_items @property @@ -1437,3 +1521,14 @@ def logged_entries(self): # def save_log(self): # print("Saved") + +def _log_trials_and_runs(self, composition, curr_condition:tc.enum(LogCondition.TRIAL, LogCondition.RUN), context): + for mech in composition.mechanisms: + for component, condition in mech.loggable_components: + if condition is curr_condition: + value = LogEntry((composition.simple_time.run, + composition.simple_time.trial, + composition.simple_time.time_step), + context, + component.value) + component.log._log_value(value, context) diff --git a/tests/control/test_EVC.py b/tests/control/test_EVC.py index 12cc8fecf25..624f4a8a31d 100644 --- a/tests/control/test_EVC.py +++ b/tests/control/test_EVC.py @@ -6,7 +6,7 @@ from psyneulink.components.process import Process from psyneulink.components.projections.modulatory.controlprojection import ControlProjection from psyneulink.components.system import System -from psyneulink.globals.keywords import ALLOCATION_SAMPLES, IDENTITY_MATRIX, MEAN, RESULT, VARIANCE +from psyneulink.globals.keywords import ALLOCATION_SAMPLES, IDENTITY_MATRIX, MEAN, RESULT, VARIANCE, SLOPE, CONTROL from psyneulink.globals.preferences.componentpreferenceset import ComponentPreferenceSet, kpReportOutputPref, kpVerbosePref from psyneulink.globals.preferences.preferenceset import PreferenceEntry, PreferenceLevel from psyneulink.library.mechanisms.processing.integrator.ddm import DDM, DECISION_VARIABLE, PROBABILITY_UPPER_THRESHOLD, RESPONSE_TIME @@ -467,6 +467,8 @@ def test_outcome_function(**kwargs): 0.2645, 0.28289958, 0.98320731, 100., ] + Flanker_Rep.set_log_conditions((SLOPE, CONTROL)) + mySystem.run( num_trials=nTrials, inputs=stim_list_dict, @@ -479,6 +481,40 @@ def test_outcome_function(**kwargs): verbose=True, ) + # log_val = Flanker_Rep.log.nparray(SLOPE, header=False) + # trial_vals = [[1], [2], [3], [4], [5], + # [6], [7], [8], [9], [10], + # [11], [12], [13], [14], [15], + # [16], [17], [18], [19], [20], + # [21], [22], [23], [24], [25], + # [27], [28], [29], [30], [31], + # [32], [33], [34], [35], [36], + # [37], [38], [39], [40], [41], + # [42], [43], [44], [45], [46], + # [47], [48], [49], [50], [51], + # [53], [54], [55], [56], [57], + # [58], [59], [60], [61], [62], + # [63], [64], [65], [66], [67], + # [68], [69], [70], [71], [72], + # [73], [74], [75], [76], [77]] + # slope_vals = [[1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8], + # [1.0], [1.2], [1.4], [1.6], [1.8]] + # np.testing.assert_allclose(pytest.helpers.expand_np_nd(log_val[1][0:]), trial_vals, atol=1e-08, verbose=True) + # np.testing.assert_allclose(pytest.helpers.expand_np_nd(log_val[3][0:]), slope_vals, atol=1e-08, verbose=True) + def test_laming_validation_specify_control_signals(): # Mechanisms: