diff --git a/Scripts/Models (Under Development)/Bustamante_Stroop_XOR_LVOC_Model.py b/Scripts/Models (Under Development)/Bustamante_Stroop_XOR_LVOC_Model.py index 73d09f8a431..db888ec5ebf 100644 --- a/Scripts/Models (Under Development)/Bustamante_Stroop_XOR_LVOC_Model.py +++ b/Scripts/Models (Under Development)/Bustamante_Stroop_XOR_LVOC_Model.py @@ -181,7 +181,7 @@ def print_weights(): print('ControlSignal variables: ', [sig.parameters.variable.get(i) for sig in lvoc.control_signals]) print('ControlSignal values: ', [sig.parameters.value.get(i) for sig in lvoc.control_signals]) # print('state_features: ', lvoc.get_feature_values(context=c)) - print('lvoc: ', lvoc.evaluation_function([sig.parameters.variable.get(i) for sig in lvoc.control_signals], context=i)) + print('lvoc: ', lvoc.evaluate_agent_rep([sig.parameters.variable.get(i) for sig in lvoc.control_signals], context=i)) # print('time: ', duration) print('--------------------') @@ -222,6 +222,6 @@ def print_weights(): # print('ControlSignal variables: ', [sig.parameters.variable.get(c) for sig in lvoc.control_signals]) # print('ControlSignal values: ', [sig.parameters.value.get(c) for sig in lvoc.control_signals]) # # print('state_features: ', lvoc.get_feature_values(context=c)) -# print('lvoc: ', lvoc.evaluation_function([sig.parameters.variable.get(c) for sig in lvoc.control_signals], context=c)) +# print('lvoc: ', lvoc.evaluate_agent_rep([sig.parameters.variable.get(c) for sig in lvoc.control_signals], context=c)) # print('time: ', duration) # print('--------------------') diff --git a/Scripts/Models (Under Development)/Bustamante_Stroop_XOR_LVOC_Model_VZ.py b/Scripts/Models (Under Development)/Bustamante_Stroop_XOR_LVOC_Model_VZ.py index 2a1fd9718b8..7d5cea9a171 100644 --- a/Scripts/Models (Under Development)/Bustamante_Stroop_XOR_LVOC_Model_VZ.py +++ b/Scripts/Models (Under Development)/Bustamante_Stroop_XOR_LVOC_Model_VZ.py @@ -232,7 +232,7 @@ def print_weights(): print('ControlSignal variables: ', [sig.parameters.variable.get(i) for sig in lvoc.control_signals]) print('ControlSignal values: ', [sig.parameters.value.get(i) for sig in lvoc.control_signals]) # print('state_features: ', lvoc.state_feature_values) - # print('lvoc: ', lvoc.evaluation_function([sig.parameters.variable.get(i) for sig in lvoc.control_signals], context=i)) + # print('lvoc: ', lvoc.evaluate_agent_rep([sig.parameters.variable.get(i) for sig in lvoc.control_signals], context=i)) # print('time: ', duration) print('--------------------') diff --git a/psyneulink/core/components/component.py b/psyneulink/core/components/component.py index 92ff63e5ebc..79e01d3625e 100644 --- a/psyneulink/core/components/component.py +++ b/psyneulink/core/components/component.py @@ -1697,16 +1697,8 @@ def _deferred_init(self, **kwargs): self._init_args.update(kwargs) # Complete initialization - # MODIFIED 10/27/18 OLD: super(self.__class__,self).__init__(**self._init_args) - # MODIFIED 10/27/18 NEW: FOLLOWING IS NEEDED TO HANDLE FUNCTION DEFERRED INIT (JDC) - # try: - # super(self.__class__,self).__init__(**self._init_args) - # except: - # self.__init__(**self._init_args) - # MODIFIED 10/27/18 END - # If name was assigned, "[DEFERRED INITIALIZATION]" was appended to it, so remove it if DEFERRED_INITIALIZATION in self.name: self.name = self.name.replace("[" + DEFERRED_INITIALIZATION + "]", "") diff --git a/psyneulink/core/components/functions/nonstateful/optimizationfunctions.py b/psyneulink/core/components/functions/nonstateful/optimizationfunctions.py index 045f3c07f95..464299f0748 100644 --- a/psyneulink/core/components/functions/nonstateful/optimizationfunctions.py +++ b/psyneulink/core/components/functions/nonstateful/optimizationfunctions.py @@ -53,13 +53,15 @@ from psyneulink.core.globals.utilities import call_with_pruned_args __all__ = ['OptimizationFunction', 'GradientOptimization', 'GridSearch', 'GaussianProcess', 'ParamEstimationFunction', - 'ASCENT', 'DESCENT', 'DIRECTION', 'MAXIMIZE', 'MINIMIZE', 'OBJECTIVE_FUNCTION', - 'SEARCH_FUNCTION', 'SEARCH_SPACE', 'SEARCH_TERMINATION_FUNCTION', 'SIMULATION_PROGRESS' + 'ASCENT', 'DESCENT', 'DIRECTION', 'MAXIMIZE', 'MINIMIZE', 'OBJECTIVE_FUNCTION', 'SEARCH_FUNCTION', + 'SEARCH_SPACE', 'RANDOMIZATION_DIMENSION', 'SEARCH_TERMINATION_FUNCTION', 'SIMULATION_PROGRESS' ] OBJECTIVE_FUNCTION = 'objective_function' +AGGREGATION_FUNCTION = 'aggregation_function' SEARCH_FUNCTION = 'search_function' SEARCH_SPACE = 'search_space' +RANDOMIZATION_DIMENSION = 'randomization_dimension' SEARCH_TERMINATION_FUNCTION = 'search_termination_function' DIRECTION = 'direction' SIMULATION_PROGRESS = 'simulation_progress' @@ -76,6 +78,7 @@ class OptimizationFunction(Function_Base): objective_function=lambda x:0, \ search_function=lambda x:x, \ search_space=[0], \ + randomization_dimension=None, \ search_termination_function=lambda x,y,z:True, \ save_samples=False, \ save_values=False, \ @@ -102,18 +105,22 @@ class OptimizationFunction(Function_Base): When `function ` is executed, it iterates over the following steps: - get sample from `search_space ` by calling `search_function - ` + `; .. - - compute value of `objective_function ` for the sample - by calling `objective_function `; + - estimate the value of `objective_function ` for the sample + by calling `objective_function ` the number of times + specified in its `num_estimates ` attribute; .. - - report value returned by `objective_function ` for the sample - by calling `report_value `; + - aggregate value of the estimates using `aggregation_function ` + (the default is to average the values; if `aggregation_function ` + is not specified, the entire list of estimates is returned); + .. + - report the aggregated value for the sample by calling `report_value `; .. - evaluate `search_termination_function `. - The current iteration numberris contained in `iteration `. Iteration continues until - all values of `search_space ` have been evaluated and/or + The current iteration number is contained in `iteration `. Iteration continues + until all values of `search_space ` have been evaluated and/or `search_termination_function ` returns `True`. The `function ` returns: @@ -167,7 +174,14 @@ class OptimizationFunction(Function_Base): `objective_function `. objective_function : function or method : default None - specifies function used to evaluate sample in each iteration of the `optimization process + specifies function used to make a single estimate for a sample, `num_estimates + ` of which are made for a given sample in each iteration of the + `optimization process `; if it is not specified, a default function is used + that simply returns the value passed as its `variable ` parameter (see `note + `). + + aggregation_function : function or method : default None + specifies function used to evaluate samples in each iteration of the `optimization process `; if it is not specified, a default function is used that simply returns the value passed as its `variable ` parameter (see `note `). @@ -189,6 +203,10 @@ class OptimizationFunction(Function_Base): executes exactly once using the value passed as its `variable ` parameter (see `note `). + randomization_dimension : int + specifies the index of `search_space ` containing the seeds for use in + randomization over each estimate of a sample (see `num_estimates `). + search_termination_function : function or method : None specifies function used to terminate iterations of the `optimization process `. It must return a boolean value, and it **must be specified** if the @@ -230,11 +248,24 @@ class OptimizationFunction(Function_Base): `objective_function ` in each iteration of the `optimization process `. The number of SampleIterators in the list determines the dimensionality of each sample: in each iteration of the `optimization process `, each - SampleIterator is called upon to provide the value for one of the dimensions of the sample.m`NotImplemented` + SampleIterator is called upon to provide the value for one of the dimensions of the sample if the `objective_function ` generates its own samples. If it is required and not specified, the optimization process executes exactly once using the value passed as its `variable ` parameter (see `note `). + randomization_dimension : int or None + the index of `search_space ` containing the seeds for use in randomization + over each estimate of a sample (see `num_estimates `); if num_estimates + is not specified, this is None, and only a single estimate is made for each sample. + + num_estimates : int or None + the number of independent estimates evaluated (i.e., calls made to the OptimizationFunction's + `objective_function ` for each sample, and aggregated over + by its `aggregation_function ` to determine the estimated value + for a given sample. This is determined from the `search_space ` by + accessing its `randomization_dimension ` and determining the + the length of (i.e., number of elements specified for) that dimension. + search_termination_function : function or method that returns a boolean value used to terminate iterations of the `optimization process `; if it is required and not specified, the optimization process executes exactly once (see `note `). @@ -283,6 +314,11 @@ class Parameters(Function_Base.Parameters): :default value: lambda x: 0 :type: ``types.FunctionType`` + randomization_dimension + see `randomization_dimension ` + :default value: None + :type: ``int`` + save_samples see `save_samples ` @@ -330,9 +366,11 @@ class Parameters(Function_Base.Parameters): variable = Parameter(np.array([0, 0, 0]), read_only=True, pnl_internal=True, constructor_argument='default_variable') objective_function = Parameter(lambda x: 0, stateful=False, loggable=False) + aggregation_function = Parameter(lambda x,n: sum(x) / n, stateful=False, loggable=False) search_function = Parameter(lambda x: x, stateful=False, loggable=False) search_termination_function = Parameter(lambda x, y, z: True, stateful=False, loggable=False) search_space = Parameter([SampleIterator([0])], stateful=False, loggable=False) + randomization_dimension = Parameter(None, stateful=False, loggable=False) save_samples = Parameter(False, pnl_internal=True) save_values = Parameter(False, pnl_internal=True) @@ -348,8 +386,10 @@ def __init__( self, default_variable=None, objective_function:tc.optional(is_function_type)=None, + aggregation_function:tc.optional(is_function_type)=None, search_function:tc.optional(is_function_type)=None, search_space=None, + randomization_dimension=None, search_termination_function:tc.optional(is_function_type)=None, save_samples:tc.optional(bool)=None, save_values:tc.optional(bool)=None, @@ -366,12 +406,17 @@ def __init__( if objective_function is None: self._unspecified_args.append(OBJECTIVE_FUNCTION) + if aggregation_function is None: + self._unspecified_args.append(AGGREGATION_FUNCTION) + if search_function is None: self._unspecified_args.append(SEARCH_FUNCTION) if search_termination_function is None: self._unspecified_args.append(SEARCH_TERMINATION_FUNCTION) + self.randomization_dimension = randomization_dimension + super().__init__( default_variable=default_variable, save_samples=save_samples, @@ -379,6 +424,7 @@ def __init__( max_iterations=max_iterations, search_space=search_space, objective_function=objective_function, + aggregation_function=aggregation_function, search_function=search_function, search_termination_function=search_termination_function, params=params, @@ -398,6 +444,12 @@ def _validate_params(self, request_set, target_set=None, context=None): format(repr(OBJECTIVE_FUNCTION), self.__class__.__name__, request_set[OBJECTIVE_FUNCTION].__name__)) + if AGGREGATION_FUNCTION in request_set and request_set[AGGREGATION_FUNCTION] is not None: + if not is_function_type(request_set[AGGREGATION_FUNCTION]): + raise OptimizationFunctionError("Specification of {} arg for {} ({}) must be a function or method". + format(repr(AGGREGATION_FUNCTION), self.__class__.__name__, + request_set[AGGREGATION_FUNCTION].__name__)) + if SEARCH_FUNCTION in request_set and request_set[SEARCH_FUNCTION] is not None: if not is_function_type(request_set[SEARCH_FUNCTION]): raise OptimizationFunctionError("Specification of {} arg for {} ({}) must be a function or method". @@ -436,9 +488,11 @@ def reset( self, default_variable=None, objective_function=None, + aggregation_function=None, search_function=None, search_termination_function=None, search_space=None, + randomization_dimension=None, context=None ): """Reset parameters of the OptimizationFunction @@ -456,6 +510,8 @@ def reset( request_set={ 'default_variable': default_variable, 'objective_function': objective_function, + 'aggregation_function': aggregation_function, + RANDOMIZATION_DIMENSION : randomization_dimension, 'search_function': search_function, 'search_termination_function': search_termination_function, 'search_space': search_space, @@ -468,6 +524,10 @@ def reset( self.parameters.objective_function._set(objective_function, context) if OBJECTIVE_FUNCTION in self._unspecified_args: del self._unspecified_args[self._unspecified_args.index(OBJECTIVE_FUNCTION)] + if aggregation_function is not None: + self.parameters.aggregation_function._set(aggregation_function, context) + if AGGREGATION_FUNCTION in self._unspecified_args: + del self._unspecified_args[self._unspecified_args.index(AGGREGATION_FUNCTION)] if search_function is not None: self.parameters.search_function._set(search_function, context) if SEARCH_FUNCTION in self._unspecified_args: @@ -480,6 +540,8 @@ def reset( self.parameters.search_space._set(search_space, context) if SEARCH_SPACE in self._unspecified_args: del self._unspecified_args[self._unspecified_args.index(SEARCH_SPACE)] + if randomization_dimension is not None: + self.parameters.randomization_dimension._set(randomization_dimension, context) def _function(self, variable=None, @@ -503,7 +565,6 @@ def _function(self, second list contains the values returned by `objective_function ` for all the samples in the order they were evaluated; otherwise it is empty. """ - if self._unspecified_args and self.initialization_status == ContextFlags.INITIALIZED: warnings.warn("The following arg(s) were not specified for {}: {} -- using default(s)". format(self.name, ', '.join(self._unspecified_args))) @@ -537,11 +598,11 @@ def _function(self, format(self.owner.name, repr(_progress_bar_char), _progress_bar_rate_str, _search_space_size)) _progress_bar_count = 0 # Iterate optimization process + while not call_with_pruned_args(self.search_termination_function, current_sample, current_value, iteration, context=context): - if _show_progress: increment_progress_bar = (_progress_bar_rate < 1) or not (_progress_bar_count % _progress_bar_rate) if increment_progress_bar: @@ -550,8 +611,14 @@ def _function(self, # Get next sample of sample new_sample = call_with_pruned_args(self.search_function, current_sample, iteration, context=context) - # Compute new value based on new sample - new_value = call_with_pruned_args(self.objective_function, new_sample, context=context) + + # Generate num_estimates of sample, then apply aggregation_function and return result + estimates = [] + num_estimates = self.num_estimates + for i in range(num_estimates): + estimate = call_with_pruned_args(self.objective_function, new_sample, context=context) + estimates.append(estimate) + new_value = self.aggregation_function(estimates, num_estimates) if self.aggregation_function else estimates self._report_value(new_value) iteration += 1 max_iterations = self.parameters.max_iterations._get(context) @@ -575,6 +642,12 @@ def _report_value(self, new_value): """Report value returned by `objective_function ` for sample.""" pass + @property + def num_estimates(self): + if self.randomization_dimension is None: + return 1 + else: + return self.search_space[self.randomization_dimension].num class GridBasedOptimizationFunction(OptimizationFunction): """Implement helper method for parallelizing instantiation for evaluating samples from search space.""" diff --git a/psyneulink/core/components/mechanisms/mechanism.py b/psyneulink/core/components/mechanisms/mechanism.py index 01a64288a94..c22da86fa42 100644 --- a/psyneulink/core/components/mechanisms/mechanism.py +++ b/psyneulink/core/components/mechanisms/mechanism.py @@ -1380,20 +1380,20 @@ class Mechanism_Base(Mechanism): projections : ContentAddressableList a list of all of the Mechanism's `Projections `, composed from the - `path_afferents ` of all of its `input_ports `, + `path_afferents ` of all of its `input_ports `, the `mod_afferents` of all of its `input_ports `, `parameter_ports `, and `output_ports `, and the `efferents ` of all of its `output_ports `. afferents : ContentAddressableList a list of all of the Mechanism's afferent `Projections `, composed from the - `path_afferents ` of all of its `input_ports `, + `path_afferents ` of all of its `input_ports `, and the `mod_afferents` of all of its `input_ports `, `parameter_ports `, and `output_ports `., path_afferents : ContentAddressableList a list of all of the Mechanism's afferent `PathwayProjections `, composed from the - `path_afferents ` attributes of all of its `input_ports + `path_afferents ` attributes of all of its `input_ports `. mod_afferents : ContentAddressableList diff --git a/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py b/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py index 4909beb7ae3..b098957d896 100644 --- a/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py +++ b/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py @@ -144,8 +144,8 @@ * **monitor_for_control** -- a list of `OutputPort specifications `. If the **objective_mechanism** argument is not specified (or is *False* or *None*) then, when the ControlMechanism is - added to a `Composition`, a `MappingProjection` is created for each OutputPort specified to the ControlMechanism's - *OUTCOME* `input_port `. If the **objective_mechanism** `argument + added to a `Composition`, a `MappingProjection` is created from each OutputPort specified to InputPorts + created on the ControlMechanism (see `ControlMechanism_Input` for details). If the **objective_mechanism** `argument ` is specified, then the OutputPorts specified in **monitor_for_control** are assigned to the `ObjectiveMechanism` rather than the ControlMechanism itself (see `ControlMechanism_ObjectiveMechanism` for details). @@ -342,17 +342,21 @@ *Input* ~~~~~~~ -By default, a ControlMechanism has a single (`primary `) `input_port -` that is named *OUTCOME*. If the ControlMechanism has an `objective_mechanism -`, then the *OUTCOME* `input_port ` receives a -single `MappingProjection` from the `objective_mechanism `\\'s *OUTCOME* -OutputPort (see `ControlMechanism_ObjectiveMechanism` for additional details). Otherwise, when the ControlMechanism is -added to a `Composition`, MappingProjections are created that project to the ControlMechanism's *OUTCOME* `input_port -` from each of the OutputPorts specified in the **monitor_for_control** `argument -` of its constructor. The `value ` of the -ControlMechanism's *OUTCOME* InputPort is assigned to its `outcome ` attribute), -and is used as the input to the ControlMechanism's `function ` to determine its -`control_allocation `. +By default, a ControlMechanism has a single `input_port ` named *OUTCOME*. If it has an +`objective_mechanism `, then the *OUTCOME* `input_port +` receives a single `MappingProjection` from the `objective_mechanism +`\\'s *OUTCOME* `OutputPort` (see `ControlMechanism_ObjectiveMechanism` for +additional details). If the ControlMechanism has no `objective_mechanism ` then, +when it is added to a `Composition`, MappingProjections are created from the items specified in `monitor_for_control +` directly to InputPorts on the ControlMechanism (see +`ControlMechanism_Monitor_for_Control` for additional details). The number of InputPorts created, and how the items +listed in `monitor_for_control ` project to them is deterimined by the +ControlMechanism's `outcome_input_ports_option `. All of the Inports +that receive Projections from those items, or the `objective_mechanism ` if +the ControlMechanism has one, are listed in its `outcome_input_ports ` attribute, +and their values in the `outcome ` attribute. The latter is used as the input to the +ControlMechanism's `function ` to determine its `control_allocation +`. .. _ControlMechanism_Function: @@ -410,7 +414,7 @@ that can be used to compute the `combined costs ` of its `control_signals `, a `reconfiguration_cost ` based on their change in value, and a `net_outcome ` (the `value ` of the ControlMechanism's -*OUTCOME* `input_port ` minus its `combined costs `), +*OUTCOME* `InputPort ` minus its `combined costs `), respectively (see `ControlMechanism_Costs_Computation` below for additional details). These methods are used by some subclasses of ControlMechanism (e.g., `OptimizationControlMechanism`) to compute their `control_allocation `. Each method is assigned a default function, but can be assigned a custom @@ -439,6 +443,7 @@ A ControlMechanism is executed using the same sequence of actions as any `Mechanism `, with the following additions. +# FIX: 11/3/21: MODIFY TO INCLUDE POSSIBLITY OF MULTIPLE OUTCOME_INPUT_PORTS The ControlMechanism's `function ` takes as its input the `value ` of its *OUTCOME* `input_port ` (also contained in `outcome `). It uses that to determine the `control_allocation `, which specifies the value @@ -572,6 +577,7 @@ from psyneulink.core import llvm as pnlvm from psyneulink.core.components.functions.function import Function_Base, is_function_type +from psyneulink.core.components.functions.nonstateful.combinationfunctions import Concatenate from psyneulink.core.components.functions.nonstateful.combinationfunctions import LinearCombination from psyneulink.core.components.mechanisms.mechanism import Mechanism, Mechanism_Base from psyneulink.core.components.mechanisms.modulatory.modulatorymechanism import ModulatoryMechanism_Base @@ -579,13 +585,14 @@ from psyneulink.core.components.ports.modulatorysignals.controlsignal import ControlSignal from psyneulink.core.components.ports.outputport import OutputPort from psyneulink.core.components.ports.parameterport import ParameterPort -from psyneulink.core.components.ports.port import Port, _parse_port_spec +from psyneulink.core.components.ports.port import Port, _parse_port_spec, PortError from psyneulink.core.globals.defaults import defaultControlAllocation from psyneulink.core.globals.keywords import \ - AUTO_ASSIGN_MATRIX, CONTROL, CONTROL_PROJECTION, CONTROL_SIGNAL, CONTROL_SIGNALS, \ - EID_SIMULATION, GATING_SIGNAL, INIT_EXECUTE_METHOD_ONLY, INTERNAL_ONLY, NAME, \ + AUTO_ASSIGN_MATRIX, COMBINE, CONTROL, CONTROL_PROJECTION, CONTROL_SIGNAL, CONTROL_SIGNALS, CONCATENATE, \ + EID_SIMULATION, FUNCTION, GATING_SIGNAL, INIT_EXECUTE_METHOD_ONLY, INTERNAL_ONLY, NAME, \ MECHANISM, MULTIPLICATIVE, MODULATORY_SIGNALS, MONITOR_FOR_CONTROL, MONITOR_FOR_MODULATION, \ - OBJECTIVE_MECHANISM, OUTCOME, OWNER_VALUE, PARAMS, PRODUCT, PROJECTION_TYPE, PROJECTIONS, PORT_TYPE, SIZE + OBJECTIVE_MECHANISM, OUTCOME, OWNER_VALUE, PARAMS, PORT_TYPE, PRODUCT, PROJECTION_TYPE, PROJECTIONS, \ + SEPARATE, SIZE from psyneulink.core.globals.parameters import Parameter from psyneulink.core.globals.preferences.basepreferenceset import is_pref_set from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel @@ -657,7 +664,7 @@ def validate_monitored_port_spec(owner, spec_list): if isinstance(spec, type) and issubclass(spec, Mechanism): raise ControlMechanismError( f"Mechanism class ({spec.__name__}) specified in '{MONITOR_FOR_CONTROL}' arg " - f"of {self.name}; it must be an instantiated {Mechanism.__name__} or " + f"of {owner.name}; it must be an instantiated {Mechanism.__name__} or " f"{OutputPort.__name__} of one." ) elif isinstance(spec, Port): @@ -689,8 +696,9 @@ def _control_mechanism_costs_getter(owning_component=None, context=None): return None def _outcome_getter(owning_component=None, context=None): + """Return array of values of outcome_input_ports""" try: - return owning_component.parameters.variable._get(context)[0] + return np.array([port.parameters.value._get(context) for port in owning_component.outcome_input_ports]) except TypeError: return None @@ -759,15 +767,16 @@ def _gen_llvm_function_body(self, ctx, builder, _1, _2, arg_in, arg_out, *, tags class ControlMechanism(ModulatoryMechanism_Base): """ - ControlMechanism( \ - monitor_for_control=None, \ - objective_mechanism=None, \ - function=Linear, \ - default_allocation=None, \ - control=None, \ - modulation=MULTIPLICATIVE, \ - combine_costs=np.sum, \ - compute_reconfiguration_cost=None, \ + ControlMechanism( \ + monitor_for_control=None, \ + objective_mechanism=None, \ + outcome_input_ports_option=SEPARATE \ + function=Linear, \ + default_allocation=None, \ + control=None, \ + modulation=MULTIPLICATIVE, \ + combine_costs=np.sum, \ + compute_reconfiguration_cost=None, \ compute_net_outcome=lambda x,y:x-y) Subclass of `ModulatoryMechanism ` that modulates the parameter(s) of one or more @@ -818,6 +827,12 @@ class ControlMechanism(ModulatoryMechanism_Base): OutputPorts specified in the ControlMechanism's **monitor_for_control** `argument `. + outcome_input_ports_option : COMBINE, CONCATENATE, SEPARATE : default SEPARATE + if **objective_mechanism** is not specified, this specifies whether `MappingProjections ` + from items specified in **monitor_for_control** are each assigned their own `InputPort` (*SEPARATE*) + or to a single *OUTCOME* InputPort (*CONCATENATE*, *COMBINE*); (see `outcome_input_ports_option + ` for additional details. + function : TransferFunction : default Linear(slope=1, intercept=0) specifies function used to combine values of monitored OutputPorts. @@ -852,15 +867,31 @@ class ControlMechanism(ModulatoryMechanism_Base): Attributes ---------- + monitor_for_control : List[OutputPort] + each item is an `OutputPort` monitored by the ControlMechanism or its `objective_mechanism + ` if that is specified (see `ControlMechanism_Monitor_for_Control`); + in the latter case, the list returned is ObjectiveMechanism's `monitor ` attribute. + objective_mechanism : ObjectiveMechanism `ObjectiveMechanism` that monitors and evaluates the values specified in the ControlMechanism's **objective_mechanism** argument, and transmits the result to the ControlMechanism's *OUTCOME* `input_port `. - monitor_for_control : List[OutputPort] - each item is an `OutputPort` monitored by the ControlMechanism or its `objective_mechanism - ` if that is specified (see `ControlMechanism_Monitor_for_Control`); - in the latter case, the list returned is ObjectiveMechanism's `monitor ` attribute. + outcome_input_ports_option : , SEPARATE, COMBINE, or CONCATENATE + determines how items specified in `monitor_for_control ` project to + the ControlMechanism if not `objective_mechanism ` is specified. If + *SEPARATE* is specified (the default), the `Projection` from each item specified in `monitor_for_control + ` is assigned its own `InputPort`. All of the InputPorts are assigned + to a list in the ControlMechanism's `outcome_input_ports ` attribute. + If *CONCATENATE* or *COMBINE* is specified, all of the projections are assigned to a single InputPort, named + *OUTCOME*. If *COMBINE* is specified, the *OUTCOME* InputPort is assigned `LinearCombination` as its + `function `, which sums the `values ` of the projections to it (all of + which must have the same dimension), to produce a single array (this is the default behavior for multiple + Projections to a single InputPort; see InputPort `function `). If *CONCATENATE* is + specified, the *OUTCOME* InputPort is assigned `Concatenate` as its `function `, which + concatenates the `values ` of its Projections into a single array of length equal to the sum + of their lengths (which need not be the same). In both cases, the *OUTCOME* InputPort is assigned as the only + item in the list of `outcome_input_ports `. monitored_output_ports_weights_and_exponents : List[Tuple(float, float)] each tuple in the list contains the weight and exponent associated with a corresponding OutputPort specified @@ -871,15 +902,25 @@ class ControlMechanism(ModulatoryMechanism_Base): contribution made to its output by each of the values that it monitors (see `ObjectiveMechanism Function `). + COMMENT: + # FIX 11/3/21 DELETED SINCE IT CAN NOW HAVE MANY input_port : InputPort the ControlMechanism's `primary InputPort `, named *OUTCOME*; this receives a `MappingProjection` from the *OUTCOME* `OutputPort ` of `objective_mechanism ` if that is specified; otherwise, it receives MappingProjections - from each of the OutputPorts specifed in `monitor_for_control ` + from each of the OutputPorts specified in `monitor_for_control ` (see `ControlMechanism_Input` for additional details). + COMMENT + + outcome_input_ports : ContentAddressableList + list of the ControlMechanism's `InputPorts ` that receive `Projections ` from + either is `objective_mechanism ` (in which case the list contains + only the ControlMechanism's *OUTCOME* `InputPort `), or the `OutputPorts ` + of the items listed in its `monitor_for_control ` attribute. outcome : 1d array - the `value ` of the ControlMechanism's *OUTCOME* `input_port `. + an array containing the `value ` of each of the ControlMechanism's `outcome_input_ports + `. function : TransferFunction : default Linear(slope=1, intercept=0) determines how the `value `\\s of the `OutputPorts ` specified in the @@ -1028,7 +1069,7 @@ class Parameters(ModulatoryMechanism_Base.Parameters): :type: input_ports - see `input_ports ` + see `input_ports ` :default value: [`OUTCOME`] :type: ``list`` @@ -1047,6 +1088,13 @@ class Parameters(ModulatoryMechanism_Base.Parameters): :type: ``list`` :read only: True + outcome_input_ports_option + see `outcome_input_ports_option ` + + :default value: SEPARATE + :type: ``str`` + :read only: True + net_outcome see `net_outcome ` @@ -1103,6 +1151,7 @@ class Parameters(ModulatoryMechanism_Base.Parameters): modulation = Parameter(MULTIPLICATIVE, pnl_internal=True) objective_mechanism = Parameter(None, stateful=False, loggable=False, structural=True) + outcome_input_ports_option = Parameter(SEPARATE, stateful=False, loggable=False, structural=True) input_ports = Parameter( [OUTCOME], @@ -1184,6 +1233,7 @@ def __init__(self, size=None, monitor_for_control:tc.optional(tc.any(is_iterable, Mechanism, OutputPort))=None, objective_mechanism=None, + outcome_input_ports_option:tc.optional(tc.enum(CONCATENATE, COMBINE, SEPARATE))=None, function=None, default_allocation:tc.optional(tc.any(int, float, list, np.ndarray))=None, control:tc.optional(tc.any(is_iterable, @@ -1231,6 +1281,7 @@ def __init__(self, name=name, function=function, monitor_for_control=monitor_for_control, + outcome_input_ports_option=outcome_input_ports_option, control=control, output_ports=control, objective_mechanism=objective_mechanism, @@ -1325,6 +1376,10 @@ def _instantiate_objective_mechanism(self, input_ports=None, context=None): # GET OutputPorts to Monitor (to specify as or add to ObjectiveMechanism's monitored_output_ports attribute + # FIX: 11/3/21: put OUTCOME InputPort at the end rather than the beginning + # Other input_ports are those passed into this method, that are presumed to be for other purposes + # (e.g., used by OptimizationControlMechanism for representing state_features as inputs) + # those are appended after the OUTCOME InputPort # FIX <- change to prepend when refactored other_input_ports = input_ports or [] monitored_output_ports = [] @@ -1386,6 +1441,11 @@ def _instantiate_objective_mechanism(self, input_ports=None, context=None): input_ports=input_ports, reference_value=input_port_value_sizes) + # Assign OUTCOME InputPort to ControlMechanism's list of outcome_input_ports (in this case, it is the only one) + self.outcome_input_ports.append(self.input_ports[OUTCOME]) + + # FIX: 11/3/21: ISN'T THIS DONE IN super()_instantiate_input_ports BASED ON OUTCOME InputPort specification? + # (or shouldn't it be?) PRESUMABLY THE ONES FOR other_input_ports ARE # INSTANTIATE MappingProjection from ObjectiveMechanism to ControlMechanism projection_from_objective = MappingProjection(sender=self.objective_mechanism, receiver=self.input_ports[OUTCOME], @@ -1423,35 +1483,84 @@ def _instantiate_input_ports(self, input_ports=None, context=None): If nothing is specified, a default OUTCOME InputPort is instantiated with no projections to it """ - input_ports = input_ports or [] - self.num_outcome_input_ports = 1 # the default (OUTCOME InputPort) + other_input_ports = input_ports or [] + + # FIX 11/3/21: THIS SHOULD BE MODIFIED TO BE A LIST, THAT CONTAINS REFERENCES TO THE OUTCOME InputPorts + self.outcome_input_ports = ContentAddressableList(component_type=OutputPort) # If ObjectiveMechanism is specified, instantiate it and OUTCOME InputPort that receives projection from it if self.objective_mechanism: # This instantiates an OUTCOME InputPort sized to match the ObjectiveMechanism's OUTCOME OutputPort self._instantiate_objective_mechanism(input_ports, context=context) - # If items to monitor are specified, instantiate InputPorts and projections to them from the specified senders + # If no ObjectiveMechanism is specified, but items to monitor are specified, elif self.monitor_for_control: - len_stim_input_ports = len(input_ports) - self.num_outcome_input_ports = len(self.monitor_for_control) - # Create one InputPort for each item in monitor_for_control - reference_value = [] - for sender in self.monitor_for_control: - reference_value.append(sender.value) - input_ports.append({PARAMS:{INTERNAL_ONLY:True}}) - super()._instantiate_input_ports(context=context, input_ports=input_ports, reference_value=reference_value) - - # FIX: MODIFY TO CONSTRUCT MAPPING PROJECTION FROM EACH MONITOR_FOR_CONTROL SPEC TO CORRESPONDING INPUTPORT - from psyneulink.core.components.projections.pathway.mappingprojection import MappingProjection - for i, sender in enumerate(convert_to_list(self.monitor_for_control)): - input_port = self.input_ports[len_stim_input_ports + i] - input_port.name = sender.name.upper() - self.aux_components.append(MappingProjection(sender=sender, receiver=input_port)) + + monitored_for_control_ports, \ + monitor_for_control_value_sizes = self._instantiate_montiored_for_control_input_ports(context) + + # Get sizes of input_ports passed in (that are presumably used for other purposes; + # e.g., ones used by OptimizationControlMechanism for state_features) + other_input_port_value_sizes = self._handle_arg_input_ports(other_input_ports)[0] + + # Construct full list of InputPort specifications and sizes + input_ports = monitored_for_control_ports + other_input_ports + input_port_value_sizes = monitor_for_control_value_sizes + other_input_port_value_sizes + super()._instantiate_input_ports(context=context, + input_ports=input_ports, + reference_value=input_port_value_sizes) + # FIX: 11/3/21 NEED TO MODIFY ONCE OUTCOME InputPorts ARE MOVED + self.outcome_input_ports.extend(self.input_ports[:len(monitored_for_control_ports)]) + # FIX: 11/3/21 DELETE ONCE THIS IS A PROPERTY # Nothing has been specified, so just instantiate the default OUTCOME InputPort else: super()._instantiate_input_ports(context=context) + self.outcome_input_ports.append(self.input_ports[OUTCOME]) + + def _instantiate_montiored_for_control_input_ports(self, context): + """Instantiate InputPorts for items specified in monitor_for_control. + + Return sender ports and their value sizes + """ + monitor_for_control_specs = self.monitor_for_control + option = self.outcome_input_ports_option + + # FIX: 11/3/21 - MOVE _parse_monitor_specs TO HERE FROM ObjectiveMechanism + from psyneulink.core.components.mechanisms.processing.objectivemechanism import _parse_monitor_specs + monitored_ports = _parse_monitor_specs(monitor_for_control_specs) + + port_value_sizes = self._handle_arg_input_ports(monitor_for_control_specs)[0] + + # Construct port specification to assign its name + if option == SEPARATE: + for i, monitored_port in enumerate(monitored_ports): + name = monitored_port.name + if isinstance(monitored_port, OutputPort): + name = f"{monitored_port.owner.name}[{name.upper()}]" + name = 'MONITOR ' + name + monitored_ports[i] = {PORT_TYPE: InputPort, name: monitored_port} + return monitored_ports, port_value_sizes + + if option == CONCATENATE: + function = Concatenate + + elif option == COMBINE: + function = LinearCombination + + else: + assert False, f"PROGRAM ERROR: Unrecognized option ({option}) passed to " \ + f"ControlMechanism._instantiate_montiored_for_control_input_ports() for {self.name}" + + port_value_sizes = [function().function(port_value_sizes)] + + outcome_input_port = {PORT_TYPE: InputPort, + NAME: 'OUTCOME', + FUNCTION: function, + # SIZE: len(self._handle_arg_input_ports(monitor_for_control_specs)[0]) + PROJECTIONS: monitored_ports} + return [outcome_input_port], port_value_sizes + def _instantiate_output_ports(self, context=None): @@ -1703,6 +1812,7 @@ def _remove_default_control_signal(self, type:tc.enum(CONTROL_SIGNAL, GATING_SIG and not ctl_sig_attribute[0].efferents): self.remove_ports(ctl_sig_attribute[0]) + # FIX: 11/3/21 SHOULDN'T THIS BE PUT ON COMPOSITION?? def _activate_projections_for_compositions(self, composition=None): """Activate eligible Projections to or from Nodes in Composition. If Projection is to or from a node NOT (yet) in the Composition, @@ -1710,33 +1820,40 @@ def _activate_projections_for_compositions(self, composition=None): """ dependent_projections = set() - if self.objective_mechanism and composition and self.objective_mechanism in composition.nodes: - # Safe to add this, as it is already in the ControlMechanism's aux_components - # and will therefore be added to the Composition along with the ControlMechanism - from psyneulink.core.compositions.composition import NodeRole - assert (self.objective_mechanism, NodeRole.CONTROL_OBJECTIVE) in self.aux_components, \ - f"PROGRAM ERROR: {OBJECTIVE_MECHANISM} for {self.name} not listed in its 'aux_components' attribute." - dependent_projections.add(self._objective_projection) - - for aff in self.objective_mechanism.afferents: - dependent_projections.add(aff) + if composition: + if self.objective_mechanism and self.objective_mechanism in composition.nodes: + # Safe to add this, as it is already in the ControlMechanism's aux_components + # and will therefore be added to the Composition along with the ControlMechanism + from psyneulink.core.compositions.composition import NodeRole + assert (self.objective_mechanism, NodeRole.CONTROL_OBJECTIVE) in self.aux_components, \ + f"PROGRAM ERROR: {OBJECTIVE_MECHANISM} for {self.name} not listed in its 'aux_components' attribute." + dependent_projections.add(self._objective_projection) + + for aff in self.objective_mechanism.afferents: + dependent_projections.add(aff) + else: + # FIX: NOTE: This will apply if controller has an objective_mechanism but it is not in the Composition + # FIX: 11/3/21: THIS NEEDS TO BE ADJUSTED IF OUTCOME InputPorts ARE MOVED ZZZ + # Add Projections to controller's OUTCOME InputPorts + for i in range(self.num_outcome_input_ports): + for proj in self.outcome_input_ports[i].path_afferents: + dependent_projections.add(proj) for ms in self.control_signals: for eff in ms.efferents: dependent_projections.add(eff) # ??ELIMINATE SYSTEM - # FIX: 9/15/19 - HOW IS THIS DIFFERENT THAN objective_mechanism's AFFERENTS ABOVE? - # assign any deferred init objective mech monitored OutputPort projections to this system + # FIX: 9/15/19 AND 11/3/21 - HOW IS THIS DIFFERENT THAN objective_mechanism's AFFERENTS ABOVE? + # assign any deferred init objective mech monitored OutputPort projections to this Composition if self.objective_mechanism and composition and self.objective_mechanism in composition.nodes: for output_port in self.objective_mechanism.monitored_output_ports: for eff in output_port.efferents: dependent_projections.add(eff) - # ??ELIMINATE SYSTEM - # FIX: 9/15/19 - HOW IS THIS DIFFERENT THAN control_signal's EFFERENTS ABOVE? - for eff in self.efferents: - dependent_projections.add(eff) + # # FIX: 9/15/19 AND 11/3/21 - HOW IS THIS DIFFERENT THAN control_signal's EFFERENTS ABOVE? + # for eff in self.efferents: + # dependent_projections.add(eff) if composition: deeply_nested_aux_components = composition._get_deeply_nested_aux_projections(self) @@ -1771,6 +1888,13 @@ def monitored_output_ports(self, value): except AttributeError: return None + @property + def num_outcome_input_ports(self): + try: + return len(self.outcome_input_ports) + except: + return 0 + @property def monitored_output_ports_weights_and_exponents(self): try: diff --git a/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py b/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py index 86c26c0ce80..217fdf1c4ae 100644 --- a/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py +++ b/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py @@ -11,15 +11,41 @@ """ +Contents +-------- + + * `OptimizationControlMechanism_Overview` + - `Expected Value of Control ` + - `Agent Representation and Types of Optimization ` + - `Model-Free" Optimization ` + - `Model-Based" Optimization ` + * `OptimizationControlMechanism_Creation` + - `State Features ` + - `State Feature Function ` + - `Agent Rep ` + * `OptimizationControlMechanism_Structure` + - `Input ` + - `ObjectiveMechanism ` + - `State Features ` + ` `State ` + - `Agent Representation ` + - `Function ` + - `Search Function, Search Space and Search Termination Function` + * `OptimizationControlMechanism_Execution` + * `OptimizationControlMechanism_Class_Reference` + + +.. _OptimizationControlMechanism_Overview: + Overview -------- An OptimizationControlMechanism is a `ControlMechanism ` that uses an `OptimizationFunction` to find an optimal `control_allocation ` for a given `state `. The `OptimizationFunction` uses the OptimizationControlMechanism's -`evaluation_function` ` to evaluate `control_allocation +`evaluate_agent_rep` ` method to evaluate `control_allocation ` samples, and then implements the one that yields the best predicted result. -The result returned by the `evaluation_function` ` is ordinarily +The result returned by the `evaluate_agent_rep` ` method is ordinarily the `net_outcome ` computed by the OptimizationControlMechanism for the `Composition` (or part of one) that it controls, and its `ObjectiveFunction` seeks to maximize this, which corresponds to maximizing the Expected Value of Control, as described below. @@ -31,7 +57,7 @@ The `net_outcome ` of an OptimizationControlMechanism, like any `ControlMechanism` is computed as the difference between the `outcome ` computed by its `objective_mechanism ` and the `costs ` of its `control_signals -` for a given `state ` (i.e., +` for a given `state ` (i.e., set of `state_feature_values ` and `control_allocation `. If the `outcome ` is configured to measure the value of processing (e.g., reward received, time taken to respond, or a combination of these, etc.), @@ -43,11 +69,11 @@ weighs the `costs ` of the ControlSignal `values ` specified by a `control_allocation ` against the `outcome ` expected to result from it. The costs are computed based on the `cost_options ` specified for -each of the OptimizationControlMechanism's `control_signals ` and its +each of the OptimizationControlMechanism's `control_signals ` and its `combine_costs ` function. The EVC is determined by its `compute_net_outcome ` function (assigned to its `net_outcome ` attribute), which is computed for a given `state ` by the -OptimizationControlMechanism's `evaluation_function `. +OptimizationControlMechanism's `evaluate_agent_rep ` method. COMMENT: The table `below ` lists different @@ -128,6 +154,8 @@ An OptimizationControlMechanism is created in the same was as any `ControlMechanism `. The following arguments of its constructor are specific to the OptimizationControlMechanism: +.. _OptimizationControlMechanism_State_Features_Arg: + * **state_features** -- takes the place of the standard **input_ports** argument in the constructor for a Mechanism`, and specifies the values used by the OptimizationControlMechanism, together with a `control_allocation `, to calculate a `net_outcome `. For @@ -148,7 +176,7 @@ the last trial of its execution is used to predict the `net_outcome ` for the upcoming trial. -.. _OptimizationControlMechanism_Feature_Function: +.. _OptimizationControlMechanism_Feature_Function_Arg: * **state_feature_function** -- specifies `function ` of the InputPort created for each item listed in **state_features**. By default, this is the identity function, that assigns the current value of the feature to the @@ -156,9 +184,11 @@ However, other functions can be assigned, for example to maintain a record of past values, integrate them over trials, and/or provide a generative model of the environment (for use in `model-based processing `. -.. -* **agent_rep** -- specifies the `Composition` used by the OptimizationControlMechanism's `evaluation_function - ` to calculate the predicted `net_outcome + +.. _OptimizationControlMechanism_Agent_Rep_Arg: + +* **agent_rep** -- specifies the `Composition` used by the OptimizationControlMechanism's `evaluate_agent_rep + ` method to calculate the predicted `net_outcome ` for a given `state ` (see `below ` for additional details). If it is not specified, then the `Composition` to which the OptimizationControlMechanism is assigned becomes its `agent_rep @@ -174,10 +204,24 @@ Structure --------- -In addition to the standard Components associated with a `ControlMechanism`, including a `Projection ` -to its *OUTCOME* InputPort from its `objective_mechanism `, and a -`function ` used to carry out the optimization process, it has several -other constiuents, as described below. +An OptimizationControl Mechanism follows the structure of a `ControlMechanism`, with the following exceptions +and additions. + +*Input* +^^^^^^^ + +While an OptimizationControlMechanism may be assigned a `objective_mechanism ` +like any ControlMechanism, the input it receives from this is handled in a more specialized manner (see +`OptimizationControlMechanism_ObjectiveMechanism` below). If it is not assigned an `objective_mechanism +`, then the items specified by `monitor_for_control ` +are all assigned `MappingProjections ` to a single *OUTCOME* InputPort. This is assigned +`Concatenate` as it `function `, which concatenates `values ` of its Projections +into a single array (that is, it is automatically configured to use the *CONCATENATE* option of a ControlMechanism's +`outcome_input_ports_option ` Parameter). This ensures that the input +to the OptimizationControlMechanism's `function ` has the same format as when +an `objective_mechanism ` has been specified, as described below. + +.. _OptimizationControlMechanism_Input: .. _OptimizationControlMechanism_ObjectiveMechanism: @@ -186,24 +230,26 @@ Like any `ControlMechanism`, an OptimizationControlMechanism may be assigned an `objective_mechanism ` that is used to evaluate the outcome of processing for a given trial (see -`ControlMechanism_Objective_ObjectiveMechanism). This passes the result to the OptimizationControlMechanism, which it -places in its `outcome ` attribute. This is used by its `compute_net_outcome -` function, together with the `costs ` of its -`control_signals `, to compute the `net_outcome ` of -processing for a given `state `, and that is returned by `evaluation` method of the -OptimizationControlMechanism's `agent_rep `. +`ControlMechanism_Objective_ObjectiveMechanism). This passes the result to the OptimizationControlMechanism's +*OUTCOME* InputPort, that is placed in its `outcome ` attribute. This is used by +its `compute_net_outcome ` function, together with the `costs +` of its `control_signals `, to compute the +`net_outcome ` of processing for a given `state `, +and that is returned by `evaluation` method of theOptimizationControlMechanism's `agent_rep +`. .. note:: - The `objective_mechanism ` is distinct from, and should not be - confused with the `objective_function ` parameter of the - OptimizationControlMechanism's `function `. The `objective_mechanism - ` evaluates the `outcome ` of processing + The `objective_mechanism ` and its `function ` + are distinct from, and should not be confused with the `objective_function + ` parameter of the OptimizationControlMechanism's `function + `. The `objective_mechanism `\\'s + `function ` evaluates the `outcome ` of processing without taking into account the `costs ` of the OptimizationControlMechanism's - `control_signals `. In contrast, its `evaluation_function - `, which is assigned as the - `objective_function` parameter of its `function `, takes the `costs - ` of the OptimizationControlMechanism's `control_signals ` - into account when calculating the `net_outcome` that it returns as its result. + `control_signals `. In contrast, its `evaluate_agent_rep + ` method, which is assigned as the `objective_function` + parameter of its `function `, takes the `costs ` + of the OptimizationControlMechanism's `control_signals ` into + account when calculating the `net_outcome` that it returns as its result. .. _OptimizationControlMechanism_State_Features: @@ -211,16 +257,16 @@ ^^^^^^^^^^^^^^^^ In addition to its `primary InputPort ` (which typically receives a projection from the -*OUTCOME* OutputPort of the `objective_mechanism `, -an OptimizationControlMechanism also has an `InputPort` for each of its state_features. By default, these are the current +*OUTCOME* OutputPort of the `objective_mechanism `, an +OptimizationControlMechanism also has an `InputPort` for each of its state_features. By default, these are the current `input ` for the Composition to which the OptimizationControlMechanism belongs. However, different values can be specified, as can a `state_feature_function ` that transforms these. For OptimizationControlMechanisms that implement `model-free ` optimization, its `state_feature_values -` are used by its `evaluation_function -` to predict the `net_outcome ` for a -given `control_allocation `. For OptimizationControlMechanisms that implement -fully `agent_rep-based ` optimization, the `state_feature_values +` are used by its `evaluate_agent_rep +` method to predict the `net_outcome ` +for a given `control_allocation `. For OptimizationControlMechanisms that +implement fully `agent_rep-based ` optimization, the `state_feature_values ` are used as the Composition's `input ` when it is executed to evaluate the `net_outcome ` for a given `control_allocation`. @@ -266,7 +312,7 @@ or another one (`model-free optimization `) that is used to estimate the `net_outcome ` for that Composition (see `above `). The `evaluate ` method of the -Composition is assigned as the `evaluation_function ` of the +Composition is assigned as the `evaluate_agent_rep ` method of the OptimizationControlMechanism. If the `agent_rep ` is not the Composition for which the OptimizationControlMechanism is the controller, then it must meet the following requirements: @@ -302,27 +348,32 @@ It is generally an `OptimizationFunction`, which in turn has `objective_function `, `search_function ` and `search_termination_function ` methods, as well as a `search_space -` attribute. The OptimizationControlMechanism's `evaluation_function -` is automatically assigned as the +` attribute. The OptimizationControlMechanism's `evaluate_agent_rep +` method is automatically assigned as the OptimizationFunction's `objective_function `, and is used to evaluate each `control_allocation ` sampled from the `search_space ` by the `search_function `search_function ` until the `search_termination_function ` returns `True`. Each -`control_allocation ` is evaluated `num_estimates +`control_allocation ` is independently evaluated `num_estimates ` times (i.e., by that number of calls to the -OptimizationControlMechanism's `evaluation_function `, the results of which are -aggregated by the OptimizationControlMechanism's `function ` in computing the -`net_outcome `. -A custom function can be assigned as the OptimizationControlMechanism's `function -`, however it must meet the following requirements: +OptimizationControlMechanism's `evaluate_agent_rep ` method. Randomization over +estimates can be configured using the OptimizationControlMechanism's `initial_seed +` and `same_seed_for_all_allocations +` Parameters; see `control_signals +` for additional information. The results of the independent +estimates are aggregated by the `aggregation_function ` of the +`OptimizationFunction` assigned to the OptimizationControlMechanism's `function `, +and used to compute the `net_outcome `. A custom function can be assigned as the OptimizationControlMechanism's +`function `, however it must meet the following requirements: .. _OptimizationControlMechanism_Custom_Function: - It must accept as its first argument and return as its result an array with the same shape as the OptimizationControlMechanism's `control_allocation `. .. - - It must execute the OptimizationControlMechanism's `evaluation_function ` - `num_estimates ` and aggregate the results in computing the + - It must execute the OptimizationControlMechanism's `evaluate_agent_rep ` + `num_estimates ` times, and aggregate the results in computing the `net_outcome `. .. - It must implement a `reset` method that can accept as keyword arguments **objective_function**, @@ -363,19 +414,21 @@ previous trial. .. * Calls `function ` to find the `control_allocation - ` that optimizes `net_outcome `. The - way in which it searches for the best `control_allocation ` is determined by - the type of `OptimizationFunction` assigned to `function `, whereas the way - that it evaluates each one is determined by the OptimizationControlMechanism's `evaluation_function - `. More specifically: + ` that optimizes `net_outcome `. The way + in which it searches for the best `control_allocation ` is determined by + the type of `OptimizationFunction` assigned to `function `, whereas the + way that it evaluates each one is determined by the OptimizationControlMechanism's `evaluate_agent_rep + ` method. More specifically: * The `function ` selects a sample `control_allocation ` (using its `search_function ` to select one from its `search_space `), and evaluates the predicted `net_outcome ` for that `control_allocation - ` using the OptimizationControlMechanism's `evaluation_function` - ` and the current `state_feature_values - `. + ` using the OptimizationControlMechanism's `evaluate_agent_rep` + ` method and the current `state_feature_values + ` as it input, by calling it `num_estimates + ` times and aggregating the `agent_rep `\\'s + `net_outcome ` over those. .. * It continues to evaluate the `net_outcome ` for `control_allocation ` samples until its `search_termination_function @@ -383,9 +436,9 @@ .. * Finally, it implements the `control_allocation ` that yielded the optimal `net_outcome `. This is used by the OptimizationControlMechanism's `control_signals - ` to compute their `values ` which, in turn, are used by - their `ControlProjections ` to modulate the parameters they control when the Composition is - next executed. + ` to compute their `values ` which, in turn, + are used by their `ControlProjections ` to modulate the parameters they control when the + Composition is next executed. COMMENT: .. _OptimizationControlMechanism_Examples: @@ -440,11 +493,12 @@ from psyneulink.core.components.component import DefaultsFlexibility from psyneulink.core.components.functions.function import is_function_type from psyneulink.core.components.functions.nonstateful.optimizationfunctions import \ - GridSearch, OBJECTIVE_FUNCTION, SEARCH_SPACE + GridSearch, OBJECTIVE_FUNCTION, SEARCH_SPACE, RANDOMIZATION_DIMENSION from psyneulink.core.components.functions.nonstateful.transferfunctions import CostFunctions from psyneulink.core.components.mechanisms.mechanism import Mechanism from psyneulink.core.components.mechanisms.modulatory.control.controlmechanism import ControlMechanism from psyneulink.core.components.ports.inputport import InputPort, _parse_shadow_inputs +from psyneulink.core.components.ports.modulatorysignals.controlsignal import ControlSignal from psyneulink.core.components.ports.outputport import OutputPort from psyneulink.core.components.ports.port import _parse_port_spec from psyneulink.core.components.shellclasses import Function @@ -452,7 +506,8 @@ from psyneulink.core.globals.context import handle_external_context from psyneulink.core.globals.defaults import defaultControlAllocation from psyneulink.core.globals.keywords import \ - DEFAULT_VARIABLE, EID_FROZEN, FUNCTION, INTERNAL_ONLY, OPTIMIZATION_CONTROL_MECHANISM, PARAMS, PROJECTIONS + CONCATENATE, DEFAULT_VARIABLE, EID_FROZEN, FUNCTION, INTERNAL_ONLY, \ + OPTIMIZATION_CONTROL_MECHANISM, OWNER_VALUE, PARAMS, PROJECTIONS from psyneulink.core.globals.parameters import Parameter from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel from psyneulink.core.globals.sampleiterator import SampleIterator, SampleSpec @@ -460,17 +515,16 @@ __all__ = [ 'OptimizationControlMechanism', 'OptimizationControlMechanismError', - 'AGENT_REP', 'STATE_FEATURES' + 'AGENT_REP', 'STATE_FEATURES', 'RANDOMIZATION_CONTROL_SIGNAL_NAME' ] AGENT_REP = 'agent_rep' STATE_FEATURES = 'state_features' - +RANDOMIZATION_CONTROL_SIGNAL_NAME = 'RANDOMIZATION_CONTROL_SIGNAL' def _parse_state_feature_values_from_variable(variable): return convert_to_np_array(np.array(variable[1:]).tolist()) - class OptimizationControlMechanismError(Exception): def __init__(self, error_value): self.error_value = error_value @@ -488,28 +542,31 @@ def _control_allocation_search_space_getter(owning_component=None, context=None) class OptimizationControlMechanism(ControlMechanism): - """OptimizationControlMechanism( \ - objective_mechanism=None, \ - monitor_for_control=None, \ - origin_objective_mechanism=False \ - terminal_objective_mechanism=False \ - state_features=None, \ - state_feature_function=None, \ - function=GridSearch, \ - agent_rep=None, \ - num_estimates=1, \ - num_trials_per_estimate=None, \ - search_function=None, \ - search_termination_function=None, \ - search_space=None, \ - control_signals=None, \ - modulation=MULTIPLICATIVE, \ - combine_costs=np.sum, \ - compute_reconfiguration_cost=None, \ + """OptimizationControlMechanism( \ + objective_mechanism=None, \ + monitor_for_control=None, \ + origin_objective_mechanism=False \ + terminal_objective_mechanism=False \ + state_features=None, \ + state_feature_function=None, \ + function=GridSearch, \ + agent_rep=None, \ + num_estimates=1, \ + initial_seed=None, \ + same_seed_for_all_parameter_combinations=False \ + num_trials_per_estimate=None, \ + search_function=None, \ + search_termination_function=None, \ + search_space=None, \ + control_signals=None, \ + modulation=MULTIPLICATIVE, \ + combine_costs=np.sum, \ + compute_reconfiguration_cost=None, \ compute_net_outcome=lambda x,y:x-y) Subclass of `ControlMechanism ` that adjusts its `ControlSignals ` to optimize - performance of the `Composition` to which it belongs. See parent class for additional arguments. + performance of the `Composition` to which it belongs. See `ControlMechanism ` + for arguments not described here. Arguments --------- @@ -525,7 +582,7 @@ class OptimizationControlMechanism(ControlMechanism): `. agent_rep : None : default Composition to which the OptimizationControlMechanism is assigned - specifies the `Composition` used by the `evalution_function ` + specifies the `Composition` used by `evaluate_agent_rep ` to predict the `net_outcome ` for a given `state `. If a Composition is specified, it must be suitably configured (see `above ` for additional details). If it is not specified, the @@ -535,13 +592,24 @@ class OptimizationControlMechanism(ControlMechanism): num_estimates : int : 1 specifies the number independent runs of `agent_rep ` used - to estimate the outcome for each `control_allocation ` sampled - (see `num_estimates ` for additional information). + to estimate its `net_outcome ` for each `control_allocation + ` sampled (see `num_estimates + ` for additional information). + + initial_seed : int : default None + specifies the seed used to initialize the random number generator at construction. + If it is not specified then then the seed is set to a random value on construction (see `initial_seed + ` for additional information). + + same_seed_for_all_parameter_combinations : bool : default False + specifies whether the random number generator is re-initialized to the same value when estimating each + `control_allocation ` (see `same_seed_for_all_parameter_combinations + ` for additional information). num_trials_per_estimate : int : default None specifies the number of trials to execute in each run of `agent_rep - ` by a call to `evaluation_function - ` (see `num_trials_per_estimate + ` by a call to `evaluate_agent_rep + ` (see `num_trials_per_estimate ` for additional information). search_function : function or method @@ -583,23 +651,51 @@ class OptimizationControlMechanism(ControlMechanism): ` (each of which is a 1d array). agent_rep : Composition - determines the `Composition` used by the `evalution_function ` - to predict the `net_outcome ` for a given `state + determines the `Composition` used by the `evaluate_agent_rep ` + method to predict the `net_outcome ` for a given `state ` (see `above `for additional details). num_estimates : int determines the number independent runs of `agent_rep ` (i.e., calls to - `evaluation_function `) used to estimate the net_outcome of - each `control_allocation ` evaluated by the OptimizationControlMechanism's - `function ` (i.e., that are specified by its `search_space - `). + `evaluate_agent_rep `) used to estimate the `net_outcome + ` of each `control_allocation ` evaluated + by the OptimizationControlMechanism's `function ` (i.e., + that are specified by its `search_space `). + # FIX: 11/3/21 ADD POINTER TO DESCRIPTINO OF RAONDIMZATION CONTROL SIGNAL + + initial_seed : int or None + determines the seed used to initialize the random number generator at construction. + If it is not specified then then the seed is set to a random value on construction, and different runs of a + Composition containing the OptimizationControlMechanism will yield different results, which should be roughly + comparable if the estimation process is stable. If **initial_seed** is specified, then running the Composition + should yield identical results for the estimation process, which can be useful for debugging. + + same_seed_for_all_allocations : bool + determines whether the random number generator used to select seeds for each estimate of the `agent_rep + `\\'s `net_outcome ` is re-initialized + to the same value for each `control_allocation ` evaluated. + If same_seed_for_all_allocations is True, then any differences in the estimates made of `net_outcome + ` for each `control_allocation ` will reflect + exclusively the influence of the different control_allocations on the execution of the `agent_rep + `, and *not* any variability intrinsic to the execution of + the Composition itself (e.g., any of its Components). This can be confirmed by identical results for repeated + executions of the OptimizationControlMechanism's `evaluate_agent_rep + ` method for the same `control_allocation + `. If same_seed_for_all_allocations is False, then each time a + `control_allocation ` is estimated, it will use a different set of seeds. + This can be confirmed by differing results for repeated executions of the OptimizationControlMechanism's + `evaluate_agent_rep ` method with the same `control_allocation + `). Small differences in results suggest + stability of the estimation process across `control_allocations `, while + substantial differences indicate instability, which may be helped by increasing `num_estimates + `. num_trials_per_estimate : int or None imposes an exact number of trials to execute in each run of `agent_rep ` used to evaluate its `net_outcome ` by a call to the - OptimizationControlMechanism's `evaluation_function `. If - it is None (the default), then either the number of **inputs** or the value specified for **num_trials** in + OptimizationControlMechanism's `evaluate_agent_rep ` method. + If it is None (the default), then either the number of **inputs** or the value specified for **num_trials** in the Composition's `run ` method used to determine the number of trials executed (see `Composition_Execution_Num_Trials` for additional information). @@ -607,13 +703,13 @@ class OptimizationControlMechanism(ControlMechanism): takes current `control_allocation ` (as initializer), uses its `search_function ` to select samples of `control_allocation ` from its `search_space `, - evaluates these using its `evaluation_function ` by calling - it `num_estimates ` times to estimate its `net_outcome + evaluates these using its `evaluate_agent_rep ` method by + calling it `num_estimates ` times to estimate its `net_outcome `net_outcome ` for a given `control_allocation `, and returns the one that yields the optimal `net_outcome ` (see `Function ` for additional details). - evaluation_function : function or method + evaluate_agent_rep : function or method returns the `net_outcome(s) ` for a given `state ` (i.e., combination of `state_feature_values ` and `control_allocation @@ -626,16 +722,18 @@ class OptimizationControlMechanism(ControlMechanism): ` for `num_trials_per_estimate ` trials. It returns an array containing the `net_outcome ` of the run and, if the **return_results** argument is True, - an array containing the `results ` of the run. This method should be run `num_estimates + an array containing the `results ` of the run. This method is `num_estimates ` times by the OptimizationControlMechanism's `function - `. + `, which aggregates the `net_outcome ` + over those in evaluating a given control_allocation ` + (see `OptimizationControlMechanism_Function` for additional details). COMMENT: search_function : function or method `search_function ` assigned to `function `; used to select samples of `control_allocation - ` to evaluate by `evaluation_function - `. + ` to evaluate by `evaluate_agent_rep + `. search_termination_function : function or method `search_termination_function ` assigned to @@ -643,14 +741,46 @@ class OptimizationControlMechanism(ControlMechanism): `optimization process `. COMMENT + control_signals : ContentAddressableList[ControlSignal] + list of the `ControlSignals ` for the OptimizationControlMechanism for the Parameters being + optimized by the OptimizationControlMechanism, including any inherited from the `Composition` for which it is + the `controller ` (this is the same as ControlMechanism's `output_ports + ` attribute. Each sends a `ControlProjection` to the `ParameterPort` for the + Parameter it controls when evaluating a `control_allocation `. + + .. technical_note:: + If `num_estimates ` is specified (that is, it is not None), + a `ControlSignal` is added to control_signals, named *RANDOMIZATION_CONTROL_SIGNAL*, to modulate the + seeds used to randomize each estimate of the `net_outcome ` for each run of + the `agent_rep ` (i.e., in each call to its `evaluate + ` method). That ControlSignal sends a `ControlProjection` to every `Parameter` of + every `Component` in `agent_rep ` that is labelled "seed", each of + which corresponds to a Parameter that uses a random number generator to assign its value (i.e., + as its `function `. This ControlSignal is used to change the seeds for all + Parameters that use random values at the start of each run of the `agent + ` used to estimate a given `control_allocation + ` of the other ControlSignals (i.e., the ones for the parameters + being optimized). The *RANDOMIZATION_CONTROL_SIGNAL* is included when constructing the + `control_allocation_search_space ` passed to the + OptimizationControlMechanism's `function ` as its + `search_space `, along with the index of + the *RANDOMIZATION_CONTROL_SIGNAL* as its `randomization_dimension <> + + + + The `initial_seed ` and + `same_seed_for_all_allocations ` + Parameters can be used to further refine this behavior. + control_allocation_search_space : list of SampleIterators `search_space ` assigned by default to `function `, that determines the samples of - `control_allocation ` evaluated by the `evaluation_function - `. This is a proprety that, unless overridden, + `control_allocation ` evaluated by the `evaluate_agent_rep + ` method. This is a proprety that, unless overridden, returns a list of the `SampleIterators ` generated from the `allocation_sample ` specifications for each of the OptimizationControlMechanism's - `control_signals `. + `control_signals `. + # FIX: 11/3/21 ADD MENTION OF RANDOMIZATION CONTROL SIGNAL AND RAND DIM PASSED TO OPTIMIZAITON FUNCTION saved_samples : list contains all values of `control_allocation ` sampled by `function @@ -664,7 +794,7 @@ class OptimizationControlMechanism(ControlMechanism): is `True`; otherwise list is empty. search_statefulness : bool : True - if set to False, an `OptimizationControlMechanism`\\ 's `evaluation_function` will never run simulations; the + if set to False, an `OptimizationControlMechanism`\\ 's `evaluate_agent_rep` will never run simulations; the evaluations will simply execute in the original `execution context <_Execution_Contexts>`. if set to True, `simulations ` will be created normally for each @@ -721,7 +851,7 @@ class Parameters(ControlMechanism.Parameters): :type: input_ports - see `input_ports ` + see `input_ports ` :default value: ["{name: OUTCOME, params: {internal_only: True}}"] :type: ``list`` @@ -769,8 +899,9 @@ class Parameters(ControlMechanism.Parameters): :default value: None :type: """ + outcome_input_ports_option = Parameter(CONCATENATE, stateful=False, loggable=False, structural=True) function = Parameter(GridSearch, stateful=False, loggable=False) - state_feature_function = Parameter(None, reference=True, stateful=False, loggable=False) + state_feature_function = Parameter(None, referdence=True, stateful=False, loggable=False) search_function = Parameter(None, stateful=False, loggable=False) search_space = Parameter(None, read_only=True) search_termination_function = Parameter(None, stateful=False, loggable=False) @@ -783,7 +914,10 @@ class Parameters(ControlMechanism.Parameters): user=False, pnl_internal=True) - num_estimates = 1 + # FIX: Should any of these be stateful? + initial_seed = None + same_seed_for_all_allocations = False + num_estimates = None num_trials_per_estimate = None # search_space = None @@ -800,6 +934,8 @@ def __init__(self, state_features: tc.optional(tc.optional(tc.any(Iterable, Mechanism, OutputPort, InputPort))) = None, state_feature_function: tc.optional(tc.optional(tc.any(is_function_type))) = None, num_estimates = None, + initial_seed=None, + same_seed_for_all_allocations=None, num_trials_per_estimate = None, search_function: tc.optional(tc.optional(tc.any(is_function_type))) = None, search_termination_function: tc.optional(tc.optional(tc.any(is_function_type))) = None, @@ -833,12 +969,10 @@ def __init__(self, state_feature_function = kwargs['feature_function'] kwargs.pop('feature_function') continue + self.state_features = convert_to_list(state_features) function = function or GridSearch - # FIX: MAKE SURE MOVE TO HERE FROM BELOW PASSES TESTS - self.state_features = convert_to_list(state_features) - # If agent_rep hasn't been specified, put into deferred init if agent_rep is None: if context.source==ContextFlags.COMMAND_LINE: @@ -858,11 +992,11 @@ def __init__(self, super().__init__( function=function, - # input_ports=state_features, - # state_features=state_features, state_feature_function=state_feature_function, num_estimates=num_estimates, num_trials_per_estimate = num_trials_per_estimate, + initial_seed=initial_seed, + same_seed_for_all_allocations=same_seed_for_all_allocations, search_statefulness=search_statefulness, search_function=search_function, search_termination_function=search_termination_function, @@ -913,6 +1047,23 @@ def _instantiate_input_ports(self, context=None): f"{port.name} should receive exactly one projection, " f"but it receives {len(port.path_afferents)} projections.") + # def _instantiate_montiored_for_control_input_ports(self, context): + # """Override ControlMechanism to return standard *single* OUTCOOME InputPort that concatenates its inputs""" + # + # monitor_for_control_specs = self.monitor_for_control + # # FIX: 11/3/21 - MOVE THIS BACK TO ControlMechanism ONCE IT HAS THE OPTION TO CONCATENATE OR COMBINE + # # MULTIPLE monitor_for_control InpuPorts + # # FIX: 11/3/21 - MOVE _parse_monitor_specs TO HERE FROM ObjectiveMechanism + # from psyneulink.core.components.mechanisms.processing.objectivemechanism import _parse_monitor_specs + # monitored_ports = _parse_monitor_specs(monitor_for_control_specs) + # outcome_input_port = {PORT_TYPE: InputPort, + # NAME: 'OUTCOME', + # FUNCTION: Concatenate, + # # SIZE: len(self._handle_arg_input_ports(monitor_for_control_specs)[0]) + # PROJECTIONS: monitored_ports} + # # port_value_size, _ = self._handle_arg_input_ports(outcome_input_port) + # return [outcome_input_port], [self._handle_arg_input_ports(monitor_for_control_specs)[0]] + def _instantiate_output_ports(self, context=None): """Assign CostFunctions.DEFAULTS as default for cost_option of ControlSignals. """ @@ -928,9 +1079,27 @@ def _instantiate_control_signals(self, context): Set size of control_allocation equal to number of modulatory_signals. Assign each modulatory_signal sequentially to corresponding item of control_allocation. """ - from psyneulink.core.globals.keywords import OWNER_VALUE - output_port_specs = list(enumerate(self.output_ports)) - for i, spec in output_port_specs: + + if self.num_estimates: + # Construct iterator for seeds used to randomize estimates + def random_integer_generator(): + rng = np.random.RandomState() + rng.seed(self.initial_seed) + return rng.random_integers(self.num_estimates) + random_seeds = SampleSpec(num=self.num_estimates, function=random_integer_generator) + + # FIX: noise PARAM OF TransferMechanism IS MARKED AS SEED WHEN ASSIGNED A DISTRIBUTION FUNCTION, + # BUT IT HAS NO PARAMETER PORT BECAUSE THAT PRESUMABLY IS FOR THE INTEGRATOR FUNCTION, + # BUT THAT IS NOT FOUND BY model.all_dependent_parameters + # Get ParameterPorts for seeds of parameters in agent_rep that use them (i.e., that return a random value) + seed_param_ports = [param._port for param in self.agent_rep.all_dependent_parameters('seed').keys()] + + # Construct ControlSignal to modify seeds over estimates + self.output_ports.append(ControlSignal(name=RANDOMIZATION_CONTROL_SIGNAL_NAME, + modulates=seed_param_ports, + allocation_samples=random_seeds)) + + for i, spec in enumerate(self.output_ports): control_signal = self._instantiate_control_signal(spec, context=context) control_signal._variable_spec = (OWNER_VALUE, i) self.output_ports[i] = control_signal @@ -974,13 +1143,27 @@ def _instantiate_attributes_after_function(self, context=None): corrected_search_space = [SampleIterator(specification=search_space)] self.parameters.search_space._set(corrected_search_space, context) + # If there is no randomization_control_signal, but num_estimates is 1 or None, + # pass None for randomization_control_signal_index (1 will be used by default by OptimizationFunction) + if RANDOMIZATION_CONTROL_SIGNAL_NAME not in self.control_signals and self.num_estimates in {1, None}: + randomization_control_signal_index = None + # Otherwise, assert that num_estimates and number of seeds generated by randomization_control_signal are equal + else: + num_seeds = self.control_signals[RANDOMIZATION_CONTROL_SIGNAL_NAME].allocation_samples.base.num + assert self.num_estimates == num_seeds, \ + f"PROGRAM ERROR: The value of the 'num_estimates' Parameter of {self.name}" \ + f"({self.num_estimates}) is not equal to the number of estimates that will be generated by " \ + f"its {RANDOMIZATION_CONTROL_SIGNAL_NAME} ControlSignal ({num_seeds})." + randomization_control_signal_index = self.control_signals.names.index(RANDOMIZATION_CONTROL_SIGNAL_NAME) + # Assign parameters to function (OptimizationFunction) that rely on OptimizationControlMechanism self.function.reset(**{ DEFAULT_VARIABLE: self.parameters.control_allocation._get(context), - OBJECTIVE_FUNCTION: self.evaluation_function, + OBJECTIVE_FUNCTION: self.evaluate_agent_rep, # SEARCH_FUNCTION: self.search_function, # SEARCH_TERMINATION_FUNCTION: self.search_termination_function, - SEARCH_SPACE: self.parameters.control_allocation_search_space._get(context) + SEARCH_SPACE: self.parameters.control_allocation_search_space._get(context), + RANDOMIZATION_DIMENSION: randomization_control_signal_index }) if isinstance(self.agent_rep, type): @@ -1027,8 +1210,9 @@ def _execute(self, variable=None, context=None, runtime_params=None): # IMPLEMENTATION NOTE: skip ControlMechanism._execute since it is a stub method that returns input_values optimal_control_allocation, optimal_net_outcome, saved_samples, saved_values = \ super(ControlMechanism,self)._execute(variable=control_allocation, + num_estimates=self.parameters.num_estimates._get(context), context=context, - runtime_params=runtime_params, + runtime_params=runtime_params ) # clean up frozen values after execution @@ -1063,7 +1247,7 @@ def _tear_down_simulation(self, sim_context=None): if not self.agent_rep.parameters.retain_old_simulation_data._get(): self.agent_rep._delete_contexts(sim_context, check_simulation_storage=True) - def evaluation_function(self, control_allocation, context=None, return_results=False): + def evaluate_agent_rep(self, control_allocation, context=None, return_results=False): """Call `evaluate ` method of `agent_rep ` Assigned as the `objective_function ` for the @@ -1129,6 +1313,8 @@ def evaluation_function(self, control_allocation, context=None, return_results=F else: return ret_val + # FIX: 11/3/21 - ??REFACTOR CompositionFunctionApproximator TO NOT TAKE num_estimates + # (i.e., LET OptimzationFunction._grid_evaluate HANDLE IT) # agent_rep is a CompositionFunctionApproximator (since runs_simuluations = False) else: return self.agent_rep.evaluate(self.parameters.state_feature_values._get(context), @@ -1325,7 +1511,10 @@ def _gen_llvm_evaluate_function(self, *, ctx:pnlvm.LLVMBuilderContext, builder.store(builder.load(src), dst) - # FIX: 11/3/21 ??MAY NEED TO BE REFACTORED TO USE num_trials_per_estimate?? AS DISTINCT FROM num_estimates + # FIX: 11/3/21 ??REFACTOR EITHER: + # - AROUND PASSING OF num_estimates IN CALL TO _execute + # - OR TO USE num_trials_per_estimate RATHER THAN num_estimates IF THAT IS WHAT IS INTENDED + # # Determine simulation counts num_estimates_ptr = pnlvm.helpers.get_param_ptr(builder, self, controller_params, @@ -1455,9 +1644,9 @@ def _parse_state_feature_specs(self, feature_input_ports, feature_function, cont parsed_features = [] - # FIX: 11/3/21: input_ports IS IGNORED; DELETE?? - if not isinstance(feature_input_ports, list): - input_ports = [feature_input_ports] + # # FIX: 11/3/21: input_ports IS IGNORED; DELETE?? + # if not isinstance(feature_input_ports, list): + # input_ports = [feature_input_ports] for spec in feature_input_ports: spec = _parse_port_spec(owner=self, port_type=InputPort, port_spec=spec) # returns InputPort dict diff --git a/psyneulink/core/components/mechanisms/processing/objectivemechanism.py b/psyneulink/core/components/mechanisms/processing/objectivemechanism.py index 11ca581612b..02b69d06ce6 100644 --- a/psyneulink/core/components/mechanisms/processing/objectivemechanism.py +++ b/psyneulink/core/components/mechanisms/processing/objectivemechanism.py @@ -833,6 +833,8 @@ def monitored_output_ports_weights_and_exponents(self): def monitored_output_ports_weights_and_exponents(self, weights_and_exponents_tuples): self.monitor_weights_and_exponents = weights_and_exponents_tuples +# FIX: 11/3/21 -- MOVE THIS TO ControlMechanism, AND INTEGRATE WITH ControlMechanism.validate_monitored_port_spec() +# OR MOVE THAT METHOD TO HERE?? def _parse_monitor_specs(monitor_specs): spec_tuple = namedtuple('SpecTuple', 'index spec') parsed_specs = [] diff --git a/psyneulink/core/components/ports/port.py b/psyneulink/core/components/ports/port.py index 3ecd5551004..299a520044c 100644 --- a/psyneulink/core/components/ports/port.py +++ b/psyneulink/core/components/ports/port.py @@ -1766,7 +1766,12 @@ def _get_receiver_port(spec): context=context) # Match the projection's value with the value of the function parameter # should be defaults.value? - mod_proj_spec_value = type_match(projection.value, type(mod_param_value)) + try: + mod_proj_spec_value = type_match(projection.value, type(mod_param_value)) + except TypeError as error: + raise PortError(f"The value for {self.name} of {self.owner.name} ({projection.value}) does " + f"not match the format ({mod_param_value}) of the Parameter it modulates " + f"({receiver.owner.name}[{mod_param_name}]).") if (mod_param_value is not None and not iscompatible(mod_param_value, mod_proj_spec_value)): raise PortError(f"Output of {projection.name} ({mod_proj_spec_value}) is not compatible " @@ -3260,8 +3265,10 @@ def _parse_port_spec(port_type=None, # port_dict[OWNER].name, spec_function_value, spec_function)) if port_dict[REFERENCE_VALUE] is not None and not iscompatible(port_dict[VALUE], port_dict[REFERENCE_VALUE]): - raise PortError("Port value ({}) does not match reference_value ({}) for {} of {})". - format(port_dict[VALUE], port_dict[REFERENCE_VALUE], port_type.__name__, owner.name)) + port_name = f"the {port_dict[NAME]}" if (NAME in port_dict and port_dict[NAME]) else f"an" + raise PortError(f"The value ({port_dict[VALUE]}) for {port_name} {port_type.__name__} of " + f"{owner.name} does not match the reference_value ({port_dict[REFERENCE_VALUE]}) " + f"used for it at construction.") return port_dict diff --git a/psyneulink/core/compositions/composition.py b/psyneulink/core/compositions/composition.py index 3ffd7e57cc4..3e923bc6450 100644 --- a/psyneulink/core/compositions/composition.py +++ b/psyneulink/core/compositions/composition.py @@ -402,10 +402,10 @@ - `Composition_Controller_Execution` -A Composition can be assigned a `controller `. This is a `ControlMechanism`, or a subclass of -one, that modulates the parameters of Components within the Composition (including Components of nested Compositions). -It typically does this based on the output of an `ObjectiveMechanism` that evaluates the value of other Mechanisms in -the Composition, and provides the result to the `controller `. +A Composition can be assigned a `controller `. This must be an `OptimizationControlMechanism`, +or a subclass of one, that modulates the parameters of Components within the Composition (including Components of +nested Compositions). It typically does this based on the output of an `ObjectiveMechanism` that evaluates the value +of other Mechanisms in the Composition, and provides the result to the `controller `. .. _Composition_Controller_Assignment: @@ -2370,8 +2370,9 @@ def input_function(env, result): from psyneulink.core import llvm as pnlvm from psyneulink.core.components.component import Component, ComponentsMeta -from psyneulink.core.components.functions.nonstateful.combinationfunctions import LinearCombination, PredictionErrorDeltaFunction from psyneulink.core.components.functions.function import is_function_type +from psyneulink.core.components.functions.nonstateful.combinationfunctions import LinearCombination, \ + PredictionErrorDeltaFunction from psyneulink.core.components.functions.nonstateful.learningfunctions import \ LearningFunction, Reinforcement, BackPropagation, TDLearning from psyneulink.core.components.functions.nonstateful.transferfunctions import Identity @@ -7140,6 +7141,7 @@ def add_controller(self, controller:ControlMechanism, context=None): if invalid_aux_components: self._controller_initialization_status = ContextFlags.DEFERRED_INIT + # FIX: 11/3/21: ISN'T THIS HANDLED IN HANDLING OF aux_components? if self.controller.objective_mechanism and self.controller.objective_mechanism not in invalid_aux_components: self.add_node(self.controller.objective_mechanism, required_roles=NodeRole.CONTROLLER_OBJECTIVE) @@ -7159,6 +7161,8 @@ def add_controller(self, controller:ControlMechanism, context=None): input_cims= [self.input_CIM] + nested_cims # For the rest of the controller's input_ports if they are marked as receiving SHADOW_INPUTS, # instantiate the shadowing Projection to them from the sender to the shadowed InputPort + # FIX: 11/3/21: BELOW NEEDS TO BE CORRECTED IF OUTCOME InputPort GETS MOVED + # ALSO, IF Non-OCM IS USED AS CONTROLLER, MAY HAVE MORE THAN ONE Inport FOR MONITORING for input_port in controller.input_ports[1:]: if hasattr(input_port, SHADOW_INPUTS) and input_port.shadow_inputs is not None: for proj in input_port.shadow_inputs.path_afferents: @@ -7189,7 +7193,7 @@ def add_controller(self, controller:ControlMechanism, context=None): if proj.sender.owner not in nested_cims: proj._activate_for_compositions(self) - # Check whether controller has input, and if not then disable + # Confirm that controller has input, and if not then disable it if not (isinstance(self.controller.input_ports, ContentAddressableList) and self.controller.input_ports): # If controller was enabled, warn that it has been disabled @@ -7260,14 +7264,17 @@ def _build_predicted_inputs_dict(self, predicted_input): # If this is not a good assumption, we need another way to look up the feature InputPorts # of the OCM and know which InputPort maps to which predicted_input value - if predicted_input is None: + no_predicted_input = (predicted_input is None or not len(predicted_input)) + if no_predicted_input: warnings.warn(f"{self.name}.evaluate() called without any inputs specified; default values will be used") + nested_nodes = dict(self._get_nested_nodes()) + # FIX: 11/3/21 NEED TO MODIFY WHEN OUTCOME InputPort IS MOVED shadow_inputs_start_index = self.controller.num_outcome_input_ports for j in range(len(self.controller.input_ports) - shadow_inputs_start_index): input_port = self.controller.input_ports[j + shadow_inputs_start_index] - if predicted_input is None: + if no_predicted_input: shadowed_input = input_port.defaults.value else: shadowed_input = predicted_input[j] diff --git a/psyneulink/core/compositions/parameterestimationcomposition.py b/psyneulink/core/compositions/parameterestimationcomposition.py index f4555d2b936..06a50d2e1b0 100644 --- a/psyneulink/core/compositions/parameterestimationcomposition.py +++ b/psyneulink/core/compositions/parameterestimationcomposition.py @@ -140,17 +140,13 @@ """ -import numpy as np - from psyneulink.core.components.mechanisms.modulatory.control.optimizationcontrolmechanism import \ OptimizationControlMechanism from psyneulink.core.components.mechanisms.processing.objectivemechanism import ObjectiveMechanism from psyneulink.core.components.ports.modulatorysignals.controlsignal import ControlSignal from psyneulink.core.compositions.composition import Composition -from psyneulink.core.globals.context import Context from psyneulink.core.globals.keywords import BEFORE -from psyneulink.core.globals.sampleiterator import SampleSpec -from psyneulink.core.globals.utilities import convert_to_list +from psyneulink.core.globals.parameters import Parameter __all__ = ['ParameterEstimationComposition'] @@ -161,13 +157,61 @@ 'controller_time_scale', 'controller_condition', 'retain_old_simulation_data'} -RANDOMIZATION_SEED_CONTROL_SIGNAL_NAME = 'RANDOMIZATION SEEDS' + class ParameterEstimationCompositionError(Exception): def __init__(self, error_value): self.error_value = error_value +def _initial_seed_getter(owning_component, context=None): + try: + return owning_component.controler.parameters.initial_seed._get(context) + except: + return None + +def _initial_seed_setter(value, owning_component, context=None): + owning_component.controler.parameters.initial_seed.set(value, context) + return value + +def _same_seed_for_all_parameter_combinations_getter(owning_component, context=None): + try: + return owning_component.controler.parameters.same_seed_for_all_allocations._get(context) + except: + return None + +def _same_seed_for_all_parameter_combinations_setter(value, owning_component, context=None): + owning_component.controler.parameters.same_seed_for_all_allocations.set(value, context) + return value + + +class Parameters(Composition.Parameters): + """ + Attributes + ---------- + + initial_seed + see `input_specification ` + + :default value: None + :type: ``int`` + + same_seed_for_all_parameter_combinations + see `input_specification ` + + :default value: False + :type: ``bool`` + + """ + # FIX: 11/32/21 CORRECT INITIAlIZATIONS? + initial_seed = Parameter(None, loggable=False, pnl_internal=True, + getter=_initial_seed_getter, + setter=_initial_seed_setter) + same_seed_for_all_parameter_combinations = Parameter(False, loggable=False, pnl_internal=True, + getter=_same_seed_for_all_parameter_combinations_getter, + setter=_same_seed_for_all_parameter_combinations_setter) + + class ParameterEstimationComposition(Composition): """ Composition( \ @@ -233,7 +277,9 @@ class ParameterEstimationComposition(Composition): num_estimates : int : default 1 specifies the number of estimates made for a each combination of `parameter ` - values (see `num_estimates ` for additional information). + values (see `num_estimates ` for additional information); + it is passed to the ParameterEstimationComposition's `controller ` to set its + `num_estimates ` Parameter. num_trials_per_estimate : int : default None specifies an exact number of trials to execute for each run of the `model @@ -242,15 +288,15 @@ class ParameterEstimationComposition(Composition): ` for additional information). initial_seed : int : default None - specifies the seed used to initialize the random number generator at construction. - If it is not specified then then the seed is set to a random value on construction (see `initial_seed - ` for additional information). + specifies the seed used to initialize the random number generator at construction; it is passed to the + ParameterEstimationComposition's `controller ` to set its `initial_seed + ` Parameter. same_seed_for_all_parameter_combinations : bool : default False specifies whether the random number generator is re-initialized to the same value when estimating each - combination of `parameter ` values (see - `same_seed_for_all_parameter_combinations - ` for additional information). + combination of `parameter ` values; it is passed to the + ParameterEstimationComposition's `controller ` to set its + `same_seed_for_all_allocations ` Parameter. Attributes @@ -264,27 +310,8 @@ class ParameterEstimationComposition(Composition): parameters : list[Parameters] determines the parameters of the `model ` used for - `ParameterEstimationComposition_Data_Fitting` or `ParameterEstimationComposition_Optimization`. - These are assigned to the **control** argument of the constructor for the ParameterEstimationComposition's - `OptimizationControlMechanism`, that is used to construct the `control_signals - ` used to modulate each parameter that is being fit. - - .. technical_note:: - A `ControlSignal` is added to the `control_signals ` of the - ParameterEstimationComposition's `OptimizationControlMechanism`, named - *RANDOMIZATION_SEED_CONTROL_SIGNAL_NAME*, to modulate the seeds used to randomize each estimate of the - `net_outcome ` for each run of the `model - ` (i.e., call to its `evaluate ` - method). That ControlSignal sends a `ControlProjection` to every `Parameter` of every `Component` in the - `model ` that is labelled "seed", each of which corresponds to a - Parameter that uses a random number generator to assign its value (i.e., as its `function - `. This ControlSignal is used to change the seeds for all Parameters that use - random values at the start of each run of the `model ` used to - estimate a given `control_allocation ` of the other ControlSignals - (i.e., the ones for the `parameters ` being fit). The - `initial_seed ` `same_seed_for_all_parameter_combinations - ` attributes can be used to further - refine this behavior. + `ParameterEstimationComposition_Data_Fitting` or `ParameterEstimationComposition_Optimization` + (see `control ` for additional details). parameter_ranges_or_priors : List[Union[Iterator, Function, ist or Value] determines the range of values evaluated for each `parameter `. @@ -337,8 +364,8 @@ class ParameterEstimationComposition(Composition): num_trials_per_estimate : int or None imposes an exact number of trials to be executed in each run of `model ` used to evaluate its `net_outcome ` by a call to its - OptimizationControlMechanism's `evaluation_function `. If - it is None (the default), then either the number of **inputs** or the value specified for **num_trials** in + OptimizationControlMechanism's `evaluate_agent_rep ` method. + If it is None (the default), then either the number of **inputs** or the value specified for **num_trials** in the ParameterEstimationComposition's `run ` method used to determine the number of trials executed (see `Composition_Execution_Num_Trials` for additional information). @@ -351,31 +378,18 @@ class ParameterEstimationComposition(Composition): *within* each fit. initial_seed : int or None - determines the seed used to initialize the random number generator at construction. - If it is not specified then then the seed is set to a random value on construction, and different runs of a - script containing the ParameterEstimationComposition will yield different results, which should be roughly - comparable if the estimation process is stable. If **initial_seed** is specified, then running the script - should yield identical results for the estimation process, which can be useful for debugging. - - same_seed_for_all_parameter_combinations : bool : default False - determines whether the random number generator used to select seeds for each estimate of the `model - `\\'s `net_outcome ` is - re-initialized to the same value for each combination of `parameter ` values - evaluated. If same_seed_for_all_parameter_combinations is True, then any differences in the estimates made - of `net_outcome ` for each combination of parameter values will reflect - exclusively the influence of the *parameters* on the execution of the `model - `, and *not* any variability intrinsic to the execution of - the Composition itself (e.g., any of its Components). This can be confirmed by identical results for repeated - executions of the OptimizationControlMechanism's `evaluation_function - ` with the same set of parameter values (i.e., - `control_allocation `). If *same_seed_for_all_parameter_combinations* is - False, then each time a combination of parameter values is estimated, it will use a different set of seeds. - This can be confirmed by differing results for repeated executions of the OptimizationControlMechanism's - `evaluation_function ` with the same set of parameter - values (`control_allocation `). Small differences in results suggest - stability of the estimation process across combinations of parameter values, while substantial differences - indicate instability, which may be helped by increasing `num_estimates - `. + contains the seed used to initialize the random number generator at construction, that is stored on the + ParameterEstimationComposition's `controller `, and setting it sets the value + of that Parameter (see `initial_seed ` for additional details). + + same_seed_for_all_parameter_combinations : bool + contains the setting for determining whether the random number generator used to select seeds for each + estimate of the `model `\\'s `net_outcome + ` is re-initialized to the same value for each combination of `parameter + ` values evaluated. Its values is stored on the + ParameterEstimationComposition's `controller `, and setting it sets the value + of that Parameter (see `same_seed_for_all_allocations + ` for additional details). optimized_parameter_values : list contains the values of the `parameters ` of the `model @@ -459,14 +473,18 @@ def _validate_params(self, args): kwargs = args.pop('kwargs') pec_name = f"{self.__class__.__name__} '{args.pop('name',None)}'" or f'a {self.__class__.__name__}' + # FIX: 11/3/21 - WRITE TESTS FOR THESE ERRORS IN test_parameterestimationcomposition.py + # Must specify either model or a COMPOSITION_SPECIFICATION_ARGS if not (args['model'] or [arg for arg in kwargs if arg in COMPOSITION_SPECIFICATION_ARGS]): + # if not ((args['model'] or args['nodes']) for arg in kwargs if arg in COMPOSITION_SPECIFICATION_ARGS): raise ParameterEstimationCompositionError(f"Must specify either 'model' or the " f"'nodes', 'pathways', and/or `projections` ars " f"in the constructor for {pec_name}.") # Can't specify both model and COMPOSITION_SPECIFICATION_ARGUMENTS - if (args['model'] and [arg for arg in kwargs if arg in COMPOSITION_SPECIFICATION_ARGS]): + # if (args['model'] and [arg for arg in kwargs if arg in COMPOSITION_SPECIFICATION_ARGS]): + if args['model'] and kwargs.pop('nodes',None): raise ParameterEstimationCompositionError(f"Can't specify both 'model' and the " f"'nodes', 'pathways', or 'projections' args " f"in the constructor for {pec_name}.") @@ -501,81 +519,69 @@ def _instantiate_ocm(self, same_seed_for_all_parameter_combinations ): - # Construct iterator for seeds used to randomize estimates - def random_integer_generator(): - rng = np.random.RandomState() - rng.seed(initial_seed) - return rng.random_integers(num_estimates) - random_seeds = SampleSpec(num=num_estimates, function=random_integer_generator) - - # FIX: noise PARAM OF TransferMechanism IS MARKED AS SEED WHEN ASSIGNED A DISTRIBUTION FUNCTION, - # BUT IT HAS NO PARAMETER PORT BECAUSE THAT PRESUMABLY IS FOR THE INTEGRATOR FUNCTION, - # BUT THAT IS NOT FOUND BY model.all_dependent_parameters - # Get ParameterPorts for seeds of parameters that use them (i.e., that return a random value) - seed_param_ports = [param._port for param in self.all_dependent_parameters('seed').keys()] - - # Construct ControlSignal to modify seeds over estimates - seed_control_signal = ControlSignal(name=RANDOMIZATION_SEED_CONTROL_SIGNAL_NAME, - modulates=seed_param_ports, - allocation_samples=random_seeds) - - # FIX: WHAT iS THIS DOING? - if data: - objective_function = objective_function(data) - - # Get ControlSignals for parameters to be searched + # # Parse **parameters** into ControlSignals specs control_signals = [] for param,allocation in parameters.items(): control_signals.append(ControlSignal(modulates=param, allocation_samples=allocation)) - # Add ControlSignal for seeds to end of list of parameters to be controlled by pem - convert_to_list(control_signals).append(seed_control_signal) - - return OptimizationControlMechanism(agent_rep=self, - control_signals=control_signals, - objective_mechanism=ObjectiveMechanism(monitor=outcome_variables, - function=objective_function), - function=optimization_function, - num_estimates=num_estimates, - num_trials_per_estimate=num_trials_per_estimate - ) - - def run(self): - # FIX: IF DATA WAS SPECIFIED, CHECK THAT INPUTS ARE APPROPRIATE FOR THOSE DATA. - # FIX: THESE ARE THE PARAMS THAT SHOULD PROBABLY BE PASSED TO THE model COMP FOR ITS RUN: - # inputs=None, - # initialize_cycle_values=None, - # reset_stateful_functions_to=None, - # reset_stateful_functions_when=Never(), - # skip_initialization=False, - # clamp_input=SOFT_CLAMP, - # runtime_params=None, - # call_before_time_step=None, - # call_after_time_step=None, - # call_before_pass=None, - # call_after_pass=None, - # call_before_trial=None, - # call_after_trial=None, - # termination_processing=None, - # scheduler=None, - # scheduling_mode: typing.Optional[SchedulingMode] = None, - # execution_mode:pnlvm.ExecutionMode = pnlvm.ExecutionMode.Python, - # default_absolute_time_unit: typing.Optional[pint.Quantity] = None, - # FIX: ADD DOCSTRING THAT EXPLAINS HOW TO RUN FOR DATA FITTING VS. OPTIMIZATION - pass - - def evaluate(self, - feature_values, - control_allocation, - num_estimates, - num_trials_per_estimate, - base_context=Context(execution_id=None), - context=None): - """Return `model ` predicted by `function for - **input**, using current set of `prediction_parameters `. - """ - # FIX: THE FOLLOWING MOSTLY NEEDS TO BE HANDLED BY OptimizationFunction.evaluation_function AND/OR grid_evaluate - # FIX: THIS NEEDS TO BE A DEQUE THAT TRACKS ALL THE CONTROL_SIGNAL VALUES OVER num_estimates FOR PARAM DISTRIB - # FIX: AUGMENT TO USE num_estimates and num_trials_per_estimate - # FIX: AUGMENT TO USE same_seed_for_all_parameter_combinations PARAMETER - return self.function(feature_values, control_allocation, context=context) + + # If objective_function has been specified, create and pass ObjectiveMechanism to ocm + objective_mechanism = ObjectiveMechanism(monitor=outcome_variables, + function=objective_function) if objective_function else None + + # FIX: NEED TO BE SURE CONSTRUCTOR FOR MLE optimization_function HAS data ATTRIBUTE + if data: + optimization_function.data = data + + return OptimizationControlMechanism( + agent_rep=self, + monitor_for_control=outcome_variables, + objective_mechanism=objective_mechanism, + function=optimization_function, + control_signals=control_signals, + num_estimates=num_estimates, + num_trials_per_estimate=num_trials_per_estimate, + initial_seed=initial_seed, + same_seed_for_all_allocations=same_seed_for_all_parameter_combinations + ) + + # def run(self): + # # FIX: IF DATA WAS SPECIFIED, CHECK THAT INPUTS ARE APPROPRIATE FOR THOSE DATA. + # # FIX: THESE ARE THE PARAMS THAT SHOULD PROBABLY BE PASSED TO THE model COMP FOR ITS RUN: + # # inputs=None, + # # initialize_cycle_values=None, + # # reset_stateful_functions_to=None, + # # reset_stateful_functions_when=Never(), + # # skip_initialization=False, + # # clamp_input=SOFT_CLAMP, + # # runtime_params=None, + # # call_before_time_step=None, + # # call_after_time_step=None, + # # call_before_pass=None, + # # call_after_pass=None, + # # call_before_trial=None, + # # call_after_trial=None, + # # termination_processing=None, + # # scheduler=None, + # # scheduling_mode: typing.Optional[SchedulingMode] = None, + # # execution_mode:pnlvm.ExecutionMode = pnlvm.ExecutionMode.Python, + # # default_absolute_time_unit: typing.Optional[pint.Quantity] = None, + # # FIX: ADD DOCSTRING THAT EXPLAINS HOW TO RUN FOR DATA FITTING VS. OPTIMIZATION + # pass + + # def evaluate(self, + # feature_values, + # control_allocation, + # num_estimates, + # num_trials_per_estimate, + # execution_mode=None, + # base_context=Context(execution_id=None), + # context=None): + # """Return `model ` predicted by `function for + # **input**, using current set of `prediction_parameters `. + # """ + # # FIX: THE FOLLOWING MOSTLY NEEDS TO BE HANDLED BY OptimizationFunction.evaluate_agent_rep AND/OR grid_evaluate + # # FIX: THIS NEEDS TO BE A DEQUE THAT TRACKS ALL THE CONTROL_SIGNAL VALUES OVER num_estimates FOR PARAM DISTRIB + # # FIX: AUGMENT TO USE num_estimates and num_trials_per_estimate + # # FIX: AUGMENT TO USE same_seed_for_all_parameter_combinations PARAMETER + # return self.function(feature_values, control_allocation, context=context) diff --git a/psyneulink/core/compositions/showgraph.py b/psyneulink/core/compositions/showgraph.py index 68866f3d29c..c345c6051cb 100644 --- a/psyneulink/core/compositions/showgraph.py +++ b/psyneulink/core/compositions/showgraph.py @@ -1628,7 +1628,8 @@ def find_rcvr_comp(r, c, l): arrowhead=ctl_proj_arrowhead ) - # If controller has objective_mechanism, assign its node and Projections + # If controller has objective_mechanism, assign its node and Projections, + # including one from ObjectiveMechanism to controller if controller.objective_mechanism: # get projection from ObjectiveMechanism to ControlMechanism objmech_ctlr_proj = controller.input_port.path_afferents[0] @@ -1735,6 +1736,55 @@ def find_rcvr_comp(r, c, l): g.edge(sndr_proj_label, objmech_proj_label, label=edge_label, color=proj_color, penwidth=proj_width) + # If controller has no objective_mechanism but does have outcome_input_ports, add Projetions from them + elif controller.num_outcome_input_ports: + # incoming edges (from monitored mechs directly to controller) + for input_port in controller.outcome_input_ports: + for projection in input_port.path_afferents: + if controller in active_items: + if self.active_color == BOLD: + proj_color = self.controller_color + else: + proj_color = self.active_color + proj_width = str(self.default_width + self.active_thicker_by) + composition.active_item_rendered = True + else: + proj_color = self.controller_color + proj_width = str(self.default_width) + if show_node_structure: + sndr_proj_label = self._get_graph_node_label(composition, + projection.sender.owner, + show_types, + show_dimensions) + if projection.sender.owner not in composition.nodes: + num_nesting_levels = self.num_nesting_levels or 0 + nested_comp = projection.sender.owner.composition + try: + nesting_depth = next((k for k, v in comp_hierarchy.items() if v == nested_comp)) + sender_visible = nesting_depth <= num_nesting_levels + except StopIteration: + sender_visible = False + else: + sender_visible = True + if sender_visible: + sndr_proj_label += ':' + controller._get_port_name(projection.sender) + ctlr_input_proj_label = ctlr_label + ':' + controller._get_port_name(input_port) + else: + sndr_proj_label = self._get_graph_node_label(composition, + projection.sender.owner, + show_types, + show_dimensions) + ctlr_input_proj_label = self._get_graph_node_label(composition, + controller, + show_types, + show_dimensions) + if show_projection_labels: + edge_label = projection.name + else: + edge_label = '' + g.edge(sndr_proj_label, ctlr_input_proj_label, label=edge_label, + color=proj_color, penwidth=proj_width) + # If controller has an agent_rep, assign its node and edges (not Projections per se) if hasattr(controller, 'agent_rep') and controller.agent_rep and show_controller==AGENT_REP : # get agent_rep diff --git a/psyneulink/core/globals/context.py b/psyneulink/core/globals/context.py index b0e12f754e8..ebf721da1d1 100644 --- a/psyneulink/core/globals/context.py +++ b/psyneulink/core/globals/context.py @@ -391,7 +391,9 @@ def composition(self, composition): # if isinstance(composition, Composition): if ( composition is None - or composition.__class__.__name__ in {'Composition', 'AutodiffComposition'} + or composition.__class__.__name__ in {'Composition', + 'AutodiffComposition', + 'ParameterEstimationComposition'} ): self._composition = composition else: diff --git a/psyneulink/core/globals/keywords.py b/psyneulink/core/globals/keywords.py index b7d7c83c53d..c79f2582541 100644 --- a/psyneulink/core/globals/keywords.py +++ b/psyneulink/core/globals/keywords.py @@ -32,7 +32,7 @@ 'BACKPROPAGATION_FUNCTION', 'BEFORE', 'BETA', 'BIAS', 'BOLD', 'BOTH', 'BOUNDS', 'BUFFER_FUNCTION', 'CHANGED', 'CLAMP_INPUT', 'COMBINATION_FUNCTION_TYPE', 'COMBINE', 'COMBINE_MEANS_FUNCTION', 'COMBINE_OUTCOME_AND_COST_FUNCTION', 'COMMAND_LINE', 'comparison_operators', 'COMPARATOR_MECHANISM', 'COMPONENT', - 'COMPONENT_INIT', 'COMPONENT_PREFERENCE_SET', 'COMPOSITION', 'COMPOSITION_INTERFACE_MECHANISM', + 'COMPONENT_INIT', 'COMPONENT_PREFERENCE_SET', 'COMPOSITION', 'COMPOSITION_INTERFACE_MECHANISM', 'CONCATENATE', 'CONCATENATE_FUNCTION', 'CONDITION', 'CONDITIONS', 'CONSTANT', 'ContentAddressableMemory_FUNCTION', 'CONTEXT', 'CONTROL', 'CONTROL_MECHANISM', 'CONTROL_PATHWAY', 'CONTROL_PROJECTION', 'CONTROL_PROJECTION_PARAMS', 'CONTROL_PROJECTIONS', 'CONTROL_SIGNAL', 'CONTROL_SIGNAL_SPECS', 'CONTROL_SIGNALS', 'CONTROLLED_PARAMS', @@ -105,10 +105,10 @@ 'RANDOM', 'RANDOM_CONNECTIVITY_MATRIX', 'RATE', 'RATIO', 'REARRANGE_FUNCTION', 'RECEIVER', 'RECEIVER_ARG', 'RECURRENT_TRANSFER_MECHANISM', 'REDUCE_FUNCTION', 'REFERENCE_VALUE', 'RESET', 'RESET_STATEFUL_FUNCTION_WHEN', 'RELU_FUNCTION', 'REST', 'RESULT', 'RESULT', 'ROLES', 'RL_FUNCTION', 'RUN', - 'SAMPLE', 'SAVE_ALL_VALUES_AND_POLICIES', 'SCALAR', 'SCALE', 'SCHEDULER', 'SELF', 'SENDER', 'SEPARATOR_BAR', - 'SHADOW_INPUT_NAME', 'SHADOW_INPUTS', 'SIMPLE', 'SIMPLE_INTEGRATOR_FUNCTION', 'SIMULATIONS', 'SINGLETON', 'SIZE', - 'SLOPE', 'SOFT_CLAMP', 'SOFTMAX_FUNCTION', 'SOURCE', 'SSE', 'STABILITY_FUNCTION', 'STANDARD_ARGS', - 'STANDARD_DEVIATION', 'STANDARD_OUTPUT_PORTS', 'SUBTRACTION', 'SUM', + 'SAMPLE', 'SAVE_ALL_VALUES_AND_POLICIES', 'SCALAR', 'SCALE', 'SCHEDULER', 'SELF', 'SENDER', 'SEPARATE', + 'SEPARATOR_BAR', 'SHADOW_INPUT_NAME', 'SHADOW_INPUTS', 'SIMPLE', 'SIMPLE_INTEGRATOR_FUNCTION', 'SIMULATIONS', + 'SINGLETON', 'SIZE', 'SLOPE', 'SOFT_CLAMP', 'SOFTMAX_FUNCTION', 'SOURCE', 'SSE', 'STABILITY_FUNCTION', + 'STANDARD_ARGS', 'STANDARD_DEVIATION', 'STANDARD_OUTPUT_PORTS', 'SUBTRACTION', 'SUM', 'TARGET', 'TARGET_MECHANISM', 'TARGET_LABELS_DICT', 'TERMINAL', 'TERMINATION_MEASURE', 'TERMINATION_THRESHOLD', 'TERMINATION_COMPARISION_OP', 'TERSE', 'THRESHOLD', 'TIME', 'TIME_STEP_SIZE', 'TIME_STEPS_DIM', 'TRAINING_SET', 'TRANSFER_FUNCTION_TYPE', 'TRANSFER_MECHANISM', 'TRANSFER_WITH_COSTS_FUNCTION', 'TRIAL', 'TRIALS_DIM', @@ -902,6 +902,8 @@ def _is_metric(metric): SINUSOID = 'sinusoid' COMBINE = 'combine' +CONCATENATE = 'concatenate' +SEPARATE = 'separate' SUM = 'sum' DIFFERENCE = DIFFERENCE # Defined above for DISTANCE_METRICS PRODUCT = 'product' diff --git a/tests/composition/test_control.py b/tests/composition/test_control.py index c813200f671..86e235b26f9 100644 --- a/tests/composition/test_control.py +++ b/tests/composition/test_control.py @@ -2168,7 +2168,8 @@ def test_model_based_num_estimates(self): state_features=[A.input_port], objective_mechanism=objective_mech, function=pnl.GridSearch(), - num_estimates=5, + # num_estimates=5, + num_estimates=None, control_signals=[control_signal]) comp.add_controller(ocm) diff --git a/tests/composition/test_parameterestimationcomposition.py b/tests/composition/test_parameterestimationcomposition.py new file mode 100644 index 00000000000..8f3b3841d42 --- /dev/null +++ b/tests/composition/test_parameterestimationcomposition.py @@ -0,0 +1,107 @@ +import logging + +import numpy as np +import pytest + +import psyneulink as pnl +from psyneulink.core.components.functions.nonstateful.combinationfunctions import \ + LinearCombination, Concatenate +from psyneulink.core.components.functions.nonstateful.distributionfunctions import DriftDiffusionAnalytical +from psyneulink.core.components.functions.nonstateful.optimizationfunctions import GridSearch +from psyneulink.core.components.projections.modulatory.controlprojection import ControlProjection +from psyneulink.library.components.mechanisms.processing.integrator.ddm import \ + DDM, DECISION_VARIABLE, RESPONSE_TIME, PROBABILITY_UPPER_THRESHOLD + +logger = logging.getLogger(__name__) + + +# All tests are set to run. If you need to skip certain tests, +# see http://doc.pytest.org/en/latest/skipping.html + +# Unit tests for ParameterEstimationComposition + +# objective_function = {None: 2, Concatenate: 2, LinearCombination: 1} +# expected + +pec_test_args = [(None, 2, True, False), + (None, 2, False, True), + (Concatenate, 2, True, False), + (LinearCombination, 1, True, False), + # (None, 2, True, True), <- USE TO TEST ERROR + # (None, 2, False, False), <- USE TO TEST ERROR + ] + +@pytest.mark.parametrize( + 'objective_function_arg, expected_input_len, model_spec, node_spec', + pec_test_args, + ids=[f"{x[0]}-{'model' if x[2] else None}-{'nodes' if x[3] else None})" for x in pec_test_args] +) +def test_parameter_estimation_composition(objective_function_arg, expected_input_len, model_spec, node_spec): + samples = np.arange(0.1, 1.01, 0.3) + Input = pnl.TransferMechanism(name='Input') + reward = pnl.TransferMechanism(output_ports=[pnl.RESULT, pnl.MEAN, pnl.VARIANCE], + name='reward', + # integrator_mode=True, + # noise=NormalDist # <- FIX 11/3/31: TEST ALLOCATION OF SEED FOR THIS WHEN WORKING + ) + Decision = DDM(function=DriftDiffusionAnalytical(drift_rate=(1.0, + ControlProjection(function=pnl.Linear, + control_signal_params={ + pnl.ALLOCATION_SAMPLES: samples, + })), + threshold=(1.0, + ControlProjection(function=pnl.Linear, + control_signal_params={ + pnl.ALLOCATION_SAMPLES: samples, + })), + noise=0.5, + starting_point=0, + t0=0.45), + output_ports=[DECISION_VARIABLE, + RESPONSE_TIME, + PROBABILITY_UPPER_THRESHOLD], + name='Decision') + Decision2 = DDM(function=DriftDiffusionAnalytical(drift_rate=1.0, + threshold=1.0, + noise=0.5, + starting_point=0, + t0=0.45), + output_ports=[DECISION_VARIABLE, + RESPONSE_TIME, + PROBABILITY_UPPER_THRESHOLD], + name='Decision') + + + comp = pnl.Composition(name="evc", retain_old_simulation_data=True) + comp.add_node(reward, required_roles=[pnl.NodeRole.OUTPUT]) + comp.add_node(Decision, required_roles=[pnl.NodeRole.OUTPUT]) + comp.add_node(Decision2, required_roles=[pnl.NodeRole.OUTPUT]) + task_execution_pathway = [Input, pnl.IDENTITY_MATRIX, Decision, Decision2] + comp.add_linear_processing_pathway(task_execution_pathway) + + pec = pnl.ParameterEstimationComposition(name='pec', + model = comp if model_spec else None, + nodes = comp if node_spec else None, + # data = [1,2,3], # For testing error + parameters={('drift_rate',Decision):[1,2], + ('threshold',Decision2):[1,2],}, + # parameters={('shrimp_boo',Decision):[1,2], # For testing error + # ('scripblat',Decision2):[1,2],}, # For testing error + outcome_variables=[Decision.output_ports[DECISION_VARIABLE], + Decision.output_ports[RESPONSE_TIME]], + objective_function=objective_function_arg, + optimization_function=GridSearch, + num_estimates=3, + # controller_mode=AFTER, # For testing error + # enable_controller=False # For testing error + ) + ctlr = pec.controller + if objective_function_arg: + assert ctlr.objective_mechanism # For objective_function specified + else: + assert not ctlr.objective_mechanism # For objective_function specified + assert len(ctlr.input_ports[pnl.OUTCOME].variable) == expected_input_len + assert len(ctlr.control_signals) == 3 + assert pnl.RANDOMIZATION_CONTROL_SIGNAL_NAME in ctlr.control_signals.names + assert ctlr.control_signals[pnl.RANDOMIZATION_CONTROL_SIGNAL_NAME].allocation_samples.base.num == 3 + # pec.run() diff --git a/tests/mechanisms/test_input_state_spec.py b/tests/mechanisms/test_input_state_spec.py index 16b38dbe54a..241ded93279 100644 --- a/tests/mechanisms/test_input_state_spec.py +++ b/tests/mechanisms/test_input_state_spec.py @@ -132,7 +132,7 @@ def test_mismatch_dim_input_ports_with_default_variable_error(self): default_variable=[[0], [0]], input_ports=[[[32],[24]],'HELLO'] ) - assert 'Port value' in str(error_text.value) and 'does not match reference_value' in str(error_text.value) + assert 'The value' in str(error_text.value) and 'does not match the reference_value' in str(error_text.value) # ------------------------------------------------------------------------------------------------ # TEST 3