diff --git a/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py b/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py index fb3003db09..72ec6ed322 100644 --- a/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py +++ b/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py @@ -1452,7 +1452,7 @@ def _instantiate_objective_mechanism(self, input_ports=None, context=None): if not isinstance(monitor_for_control, list): monitor_for_control = [monitor_for_control] - # If objective_mechanism is used to specify OutputPorts to be monitored (legacy feature) + # If objective_mechanism arg is used to specify OutputPorts to be monitored (legacy feature) # move them to monitor_for_control if isinstance(self.objective_mechanism, list): monitor_for_control.extend(self.objective_mechanism) diff --git a/psyneulink/core/compositions/composition.py b/psyneulink/core/compositions/composition.py index 1e7fd49fe5..6cc1a44d39 100644 --- a/psyneulink/core/compositions/composition.py +++ b/psyneulink/core/compositions/composition.py @@ -2912,7 +2912,7 @@ def input_function(env, result): from psyneulink.core.components.mechanisms.modulatory.modulatorymechanism import ModulatoryMechanism_Base from psyneulink.core.components.mechanisms.processing.compositioninterfacemechanism import CompositionInterfaceMechanism from psyneulink.core.components.mechanisms.processing.objectivemechanism import ObjectiveMechanism -from psyneulink.core.components.mechanisms.processing.processingmechanism import ProcessingMechanism +from psyneulink.core.components.mechanisms.processing.processingmechanism import ProcessingMechanism, ProcessingMechanism_Base from psyneulink.core.components.ports.inputport import InputPort, InputPortError from psyneulink.core.components.ports.modulatorysignals.controlsignal import ControlSignal from psyneulink.core.components.ports.modulatorysignals.learningsignal import LearningSignal @@ -2939,7 +2939,8 @@ def input_function(env, result): INPUT, INPUT_PORTS, INPUTS, INPUT_CIM_NAME, \ LEARNABLE, LEARNED_PROJECTIONS, LEARNING_FUNCTION, LEARNING_MECHANISM, LEARNING_MECHANISMS, LEARNING_PATHWAY, \ LEARNING_SIGNAL, Loss, \ - MATRIX, MAYBE, MODEL_SPEC_ID_METADATA, MONITOR, MONITOR_FOR_CONTROL, NAME, NESTED, NO_CLAMP, NODE, NODES, \ + MATRIX, MAYBE, MODEL_SPEC_ID_METADATA, MONITOR, MONITOR_FOR_CONTROL, MULTIPLICATIVE_PARAM, \ + NAME, NESTED, NO_CLAMP, NODE, NODES, \ OBJECTIVE_MECHANISM, ONLINE, ONLY, OUTCOME, OUTPUT, OUTPUT_CIM_NAME, OUTPUT_MECHANISM, OUTPUT_PORTS, OWNER_VALUE, \ PARAMETER, PARAMETER_CIM_NAME, PORT, \ PROCESSING_PATHWAY, PROJECTION, PROJECTIONS, PROJECTION_TYPE, PROJECTION_PARAMS, PULSE_CLAMP, RECEIVER, \ @@ -4368,8 +4369,7 @@ def add_node(self, node, required_roles=None, context=None): else: self._pre_existing_pathway_components[NODES].append(node) - # Aux components are being added by Composition, even if main Node being added was from COMMAND_LINE - # (this suppresses warnings pertaining to illegal or ill-advised direct addition of some components) + # Aux components are being added by Composition, even if main Node is being added was from COMMAND_LINE invalid_aux_components = self._add_node_aux_components(node, context=context) # Implement required_roles @@ -5114,7 +5114,7 @@ def _add_node_aux_components(self, node, context=None): # ignore these for now and try to activate them again during every call to _analyze_graph # and, at runtime, if there are still any invalid aux_components left, issue a warning projections = [] - # Add all "nodes" to the composition first (in case projections reference them) + # Add all Nodes to the Composition first (in case Projections reference them) for i, component in enumerate(node.aux_components): if isinstance(component, (Mechanism, Composition)): if isinstance(component, Composition): @@ -9801,6 +9801,15 @@ def get_controller(comp): return total_cost + def _get_modulable_mechanisms(self): + modulated_mechanisms = [] + for mech in [m for m in self.nodes if (isinstance(m, ProcessingMechanism_Base) and + not (isinstance(m, ObjectiveMechanism) + and self.get_roles_for_node(m) != NodeRole.CONTROL) + and hasattr(m.function, MULTIPLICATIVE_PARAM))]: + modulated_mechanisms.append(mech) + return modulated_mechanisms + # endregion CONTROL # ****************************************************************************************************************** diff --git a/psyneulink/library/components/mechanisms/modulatory/control/agt/lccontrolmechanism.py b/psyneulink/library/components/mechanisms/modulatory/control/agt/lccontrolmechanism.py index c4f085d053..1acc55e1e3 100644 --- a/psyneulink/library/components/mechanisms/modulatory/control/agt/lccontrolmechanism.py +++ b/psyneulink/library/components/mechanisms/modulatory/control/agt/lccontrolmechanism.py @@ -56,11 +56,11 @@ .. _LCControlMechanism_ObjectiveMechanism_Creation: *ObjectiveMechanism and Monitored OutputPorts* -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If the **objective_mechanism** argument is specified then, as with a standard ControlMechanism, the specified `ObjectiveMechanism` is assigned to its `objective_mechanism ` attribute. The -`value ` of the ObjectiveMechanism's *OUTCOME* `OutputPort` must be a scalar (that is used as the +`value ` of the ObjectiveMechanism's *OUTCOME* `OutputPort` must be a scalar, that is used as the input to the LCControlMechanism's `function ` to drive its `phasic response `. An ObjectiveMechanism can also be constructed automatically, by specifying **objective_mechanism** as True; that is assigned a `CombineMeans` Function as its `function @@ -82,17 +82,17 @@ ` of the Mechanism's `function `. Therefore, any Mechanism specified for control by an LCControlMechanism must be either a `ProcessingMechanism`, or a Mechanism that uses as its `function ` a class of `Function ` that implements a `multiplicative_param -`. The **modulate_mechanisms** argument must be either a list of such Mechanisms, or -a `Composition` (to modulate all of the `ProcessingMechanisms ` in a Composition -- see below). -see below). If a Mechanism specified in the **modulated_mechanisms** argument does not implement a multiplicative_param, -it is ignored. A `ControlProjection` is automatically created that projects from the LCControlMechanism to the +`. If a Mechanism specified in the **modulated_mechanisms** argument does not implement a +multiplicative_param, it is ignored. The **modulate_mechanisms** argument must be either a list of suitable Mechanisms, +or a `Composition` (to modulate all of the `ProcessingMechanisms ` in a Composition -- see below). +A `ControlProjection` is automatically created that projects from the LCControlMechanism to the `ParameterPort` for the `multiplicative_param ` of every Mechanism specified in the -**modulated_mechanisms** argument. The Mechanisms modulated by an LCControlMechanism are listed in its +**modulated_mechanisms** argument. The Mechanisms modulated by an LCControlMechanism are listed in its `modulated_mechanisms ` attribute). -If `Composition` is assigned as the value of **modulate_mechanisms**, then the LCControlMechanism will modulate all +If a `Composition` is assigned as the value of **modulate_mechanisms**, then the LCControlMechanism will modulate all of the `ProcessingMechanisms` in that Composition, with the exception of any `ObjectiveMechanism`\\s that are assigned -a the `objective_mechanism ` of another `ControlMechanism`. Note that only the +as the `objective_mechanism ` of another `ControlMechanism`. Note that only the Mechanisms that already belong to that Composition are included at the time the LCControlMechanism is constructed. Therefore, to include *all* Mechanisms in the Composition at the time it is run, the LCControlMechanism should be constructed and `added to the Composition using the Composition's `add_node ` method) after all @@ -119,7 +119,7 @@ ObjectiveMechanism ^^^^^^^^^^^^^^^^^^ -If an ObjectiveMechanism is `automatically created for an +If an ObjectiveMechanism is `automatically created ` for an LCControlMechanism, it receives its inputs from the `OutputPort(s) ` specified the **monitor_for_control** argument of the LCControlMechanism constructor, or the **montiored_output_ports** argument of the LCControlMechanism's `ObjectiveMechanism `. By default, the @@ -312,14 +312,18 @@ from psyneulink._typing import Optional, Union, Iterable from psyneulink.core import llvm as pnlvm +from psyneulink.core.components.functions import FunctionError +from psyneulink.core.components.functions.nonstateful.transformfunctions import CombineMeans from psyneulink.core.components.functions.stateful.integratorfunctions import FitzHughNagumoIntegrator -from psyneulink.core.components.mechanisms.modulatory.control.controlmechanism import ControlMechanism, ControlMechanismError -from psyneulink.core.components.mechanisms.processing.objectivemechanism import ObjectiveMechanism +from psyneulink.core.components.mechanisms.modulatory.control.controlmechanism import ( + ControlMechanism, ControlMechanismError) +from psyneulink.core.components.mechanisms.processing.objectivemechanism import ( + ObjectiveMechanism, ObjectiveMechanismError) from psyneulink.core.components.projections.modulatory.controlprojection import ControlProjection from psyneulink.core.components.shellclasses import Mechanism from psyneulink.core.components.ports.outputport import OutputPort -from psyneulink.core.globals.keywords import \ - INIT_EXECUTE_METHOD_ONLY, MULTIPLICATIVE_PARAM, NAME, OWNER_VALUE, PORT_TYPE, PROJECTIONS, VARIABLE +from psyneulink.core.globals.keywords import (ALL, INIT_EXECUTE_METHOD_ONLY, MULTIPLICATIVE_PARAM, + OBJECTIVE_MECHANISM, OWNER_VALUE, PROJECTIONS,VARIABLE, SUM) from psyneulink.core.globals.parameters import Parameter, ParameterAlias, check_user_specified from psyneulink.core.globals.preferences.basepreferenceset import ValidPrefSet from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel @@ -394,8 +398,10 @@ class LCControlMechanism(ControlMechanism): modulated_mechanisms : List[`Mechanism `] or *ALL* specifies the Mechanisms to be modulated by the LCControlMechanism. If it is a list, every item must be a Mechanism with a `function ` that implements a `multiplicative_param - `; alternatively the keyword *ALL* can be used to specify all of the - `ProcessingMechanisms ` in the Composition(s) to which the LCControlMechanism belongs. + `; alternatively a `Composition` can be specified, in which case the + LCControlMechanism will modulate all of the suitable `ProcessingMechanisms ` in the + Composition that have been added to it up to the point at which the LCControlMechanism is constructed (see + `Mechanisms to Modulate ` for additional information). initial_w_FitzHughNagumo : float : default 0.0 sets `initial_w ` on the LCControlMechanism's `FitzHughNagumoIntegrator @@ -517,6 +523,12 @@ class LCControlMechanism(ControlMechanism): ` to parametrize the contribution made to its output by each of the values that it monitors (see `ObjectiveMechanism Function `). + objective_mechanism : ObjectiveMechanism + `ObjectiveMechanism` that monitors and evaluates the values specified in the LCControlMechanism's + **objective_mechanism** argument, and transmits the result to the LCControlMechanism's *OUTCOME* + `input_port `, that drives its phasic response + (see `LCControlMechanism_ObjectiveMechanism` for additional details). + function : FitzHughNagumoIntegrator takes the LCControlMechanism's `input ` and generates its response ` under @@ -544,9 +556,10 @@ class LCControlMechanism(ControlMechanism): ` attribute. modulated_mechanisms : List[Mechanism] - list of `Mechanisms ` modulated by the LCControlMechanism. + list of `Mechanisms ` modulated by the LCControlMechanism (see `Mechanisms to Modulate + ` for additional information). - initial_w_FitzHughNagumo : float : default 0.0 + initial_w_FitzHughNagumo : float : default 0.0 sets `initial_w ` on the LCControlMechanism's `FitzHughNagumoIntegrator ` function @@ -718,7 +731,7 @@ class Parameters(ControlMechanism.Parameters): def __init__(self, default_variable=None, default_allocation: Optional[Union[int, float, list, np.ndarray]] = None, - objective_mechanism: Optional[Union[ObjectiveMechanism, list]] = None, + objective_mechanism: Optional[Union[ObjectiveMechanism, list, bool]] = True, monitor_for_control: Optional[Union[Iterable, Mechanism, OutputPort]] = None, modulated_mechanisms=None, modulation: Optional[str] = None, @@ -785,12 +798,9 @@ def __init__(self, def _validate_params(self, request_set, target_set=None, context=None): """Validate modulated_mechanisms argument. - - Validate that **modulated_mechanisms** is either a Composition or a list of eligible Mechanisms . - Eligible Mechanisms are ones with a `function ` that has a multiplicative_param. - + Validate that **modulated_mechanisms** is either a Composition or a list of eligible Mechanisms; + eligible Mechanisms are ones with a `function ` that has a multiplicative_param. """ - super()._validate_params(request_set=request_set, target_set=target_set, context=context) @@ -799,26 +809,38 @@ def _validate_params(self, request_set, target_set=None, context=None): spec = target_set[MODULATED_MECHANISMS] from psyneulink.core.compositions.composition import Composition - if isinstance(spec, Composition): + if isinstance(spec, Composition) or spec is ALL: pass else: if not isinstance(spec, list): spec = [spec] for mech in spec: if not isinstance(mech, Mechanism): - raise LCControlMechanismError("The specification of the {} argument for {} " - "contained an item ({}) that is not a Mechanism.". - format(repr(MODULATED_MECHANISMS), self.name, mech)) + raise LCControlMechanismError(f"The specification of the {repr(MODULATED_MECHANISMS)} " + f"argument for {self.name} contained an item ({mech}) " + f"that is not a Mechanism.") elif not hasattr(mech.function, MULTIPLICATIVE_PARAM): raise LCControlMechanismError(f"The specification of the {repr(MODULATED_MECHANISMS)} " f"argument for {self.name} contained a Mechanism ({mech}) " f"that does not have a {repr(MULTIPLICATIVE_PARAM)}.") + def _instantiate_objective_mechanism(self, input_ports=None, context=None): + """Instantiate ObjectiveMechanism with CombineMeans as its function then call super() + """ + # If objective_mechanism is specified, use it; otherwise, instantiate one + if not self.objective_mechanism or self.objective_mechanism is True: + try: + self.objective_mechanism = ObjectiveMechanism(function=CombineMeans(operation=SUM), + name=self.name + '_ObjectiveMechanism') + except (ObjectiveMechanismError, FunctionError) as e: + raise ObjectiveMechanismError(f"Error creating {OBJECTIVE_MECHANISM} for {self.name}: {e}") + super()._instantiate_objective_mechanism(input_ports=input_ports, context=context) + def _instantiate_output_ports(self, context=None): - """Override to insure that ControlSignals are instantiated even thought self.control is not specified - LCControlMechanism automatically assigns ControlSignals, so no control specification is needed in constructor - super._instantiate_output_ports does not call _instantiate_control_signals if self.control is not specified; - so, override is needed to insure _instantiate_control_signals is called + """Override to ensure that ControlSignals are instantiated even though self.control is not specified. + LCControlMechanism automatically assigns ControlSignals, so no control specification is needed in constructor; + super()._instantiate_output_ports does not call _instantiate_control_signals if self.control is not specified; + so, override is needed to ensure _instantiate_control_signals is called. """ self._register_control_signal_type(context=None) self._instantiate_control_signals(context=context) @@ -829,26 +851,19 @@ def _instantiate_output_ports(self, context=None): self.aux_components.extend(self.control_projections) def _instantiate_control_signals(self, context=None): - """Instantiate ControlSignals and assign ControlProjections to Mechanisms in self.modulated_mechanisms - - If **modulated_mechanisms** argument of constructor was specified as *ALL*, assign all ProcessingMechanisms + """Instantiate ControlSignals and assign ControlProjections to Mechanisms in self.modulated_mechanisms. + If **modulated_mechanisms** argument of constructor is specified as *ALL*, assign all ProcessingMechanisms in Compositions to which LCControlMechanism belongs to self.modulated_mechanisms. Instantiate ControlSignal with Projection to the ParameterPort for the multiplicative_param of every Mechanism listed in self.modulated_mechanisms. """ - from psyneulink.core.components.mechanisms.processing.processingmechanism import ProcessingMechanism_Base - # A Composition is specified for modulated_mechanisms, so assign all Processing Mechanisms in composition - # to its modulated_mechanisms attribute + # A Composition is specified for modulated_mechanisms, + # so assign all Processing Mechanisms in Composition to its modulated_mechanisms attribute from psyneulink.core.compositions.composition import Composition, NodeRole + # FIX: 11/27/24 - NEED TO HANDLE "ALL" HERE, BY DEFERRING UNTIL ADDED TO COMPOSITION if isinstance(self.modulated_mechanisms, Composition): - comp = self.modulated_mechanisms - self.modulated_mechanisms = [] - for mech in [m for m in comp.nodes if (isinstance(m, ProcessingMechanism_Base) and - not (isinstance(m, ObjectiveMechanism) - and comp.get_roles_for_node(m) != NodeRole.CONTROL) - and hasattr(m.function, MULTIPLICATIVE_PARAM))]: - self.modulated_mechanisms.append(mech) + self.modulated_mechanisms = self.modulated_mechanisms._get_modulable_mechanisms() # Get the name of the multiplicative_param of each Mechanism in self.modulated_mechanisms if self.modulated_mechanisms: diff --git a/tests/mechanisms/test_control_mechanism.py b/tests/mechanisms/test_control_mechanism.py index 85051f9d92..c20354c29f 100644 --- a/tests/mechanisms/test_control_mechanism.py +++ b/tests/mechanisms/test_control_mechanism.py @@ -93,22 +93,34 @@ def test_lc_control_mech_basic(self, benchmark, mech_mode): np.testing.assert_allclose(val, expected) @pytest.mark.composition - def test_lc_control_modulated_mechanisms_all(self): - - T_1 = pnl.TransferMechanism(name='T_1') - T_2 = pnl.TransferMechanism(name='T_2') - - # S = pnl.System(processes=[pnl.proc(T_1, T_2, LC)]) - C = pnl.Composition(pathways=[T_1, T_2]) + def test_lc_control_monitored_and_modulated_mechanisms_composition(self): + """Test default configuration of LCControlMechanism with monitored and modulated mechanisms in a Composition + Test that it implements an ObjectiveMechanism by default, that uses CombineMeans + Test that ObjectiveMechanism can monitor Mechanisms with values of different lengths, + and generate a scalar output. + Test that it modulates all of the ProcessingMechanisms in the Composition but not the ObjectiveMechanism + """ + + T_1 = pnl.TransferMechanism(name='T_1', input_shapes=2) + T_2 = pnl.TransferMechanism(name='T_2', input_shapes=3) + + C = pnl.Composition(pathways=[T_1, np.array([[1,2,3],[4,5,6]]), T_2]) LC = pnl.LCControlMechanism(monitor_for_control=[T_1, T_2], modulated_mechanisms=C) C.add_node(LC) - assert len(LC.control_signals)==1 assert len(LC.control_signals[0].efferents)==2 + assert LC.path_afferents[0].sender.owner == LC.objective_mechanism + assert isinstance(LC.objective_mechanism.function, pnl.CombineMeans) + assert len(LC.objective_mechanism.input_ports[0].value) == 2 + assert len(LC.objective_mechanism.input_ports[1].value) == 3 + assert len(LC.objective_mechanism.output_ports[pnl.OUTCOME].value) == 1 assert T_1.parameter_ports[pnl.SLOPE].mod_afferents[0] in LC.control_signals[0].efferents assert T_2.parameter_ports[pnl.SLOPE].mod_afferents[0] in LC.control_signals[0].efferents + result = C.run(inputs={T_1:[1,2]})#, T_2:[3,4,5] + assert LC.objective_mechanism.value == (np.mean(T_1.value) + np.mean(T_2.value)) + @pytest.mark.composition class TestControlMechanism: