Skip to content

Commit

Permalink
fix/lccontrolmechanism_objectivemechanism (#3134)
Browse files Browse the repository at this point in the history
* • composition.py
  - suppress warning about adding CONTROL_OBJECTIVE NodeRole if ObjectiveMechanism is already associated with a ControlMechanism

• lccontrolmechanism.py
  - _instantiate_objective_mechanism():  implement default ObjectiveMechanism with CombineMeans
  - _instantiate_control_signals:
    complete implementation of ALL

* • test_control_mechanism.py
  test_lc_control_monitored_and_modulated_mechanisms_composition(): augmented to test implementation of ObjectiveMechanism
  • Loading branch information
jdcpni authored Nov 28, 2024
1 parent f91e2a1 commit 8d2fafc
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 14 additions & 5 deletions psyneulink/core/compositions/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
psyneulink.core.components.mechanisms.processing.processingmechanism
begins an import cycle.
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
Expand All @@ -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, \
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

# ******************************************************************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ControlMechanism.objective_mechanism>` attribute. The
`value <OutputPort.value>` of the ObjectiveMechanism's *OUTCOME* `OutputPort` must be a scalar (that is used as the
`value <OutputPort.value>` of the ObjectiveMechanism's *OUTCOME* `OutputPort` must be a scalar, that is used as the
input to the LCControlMechanism's `function <LCControlMechanism.function>` to drive its `phasic response
<LCControlMechanism_Modes_Of_Operation>`. An ObjectiveMechanism can also be constructed automatically, by specifying
**objective_mechanism** as True; that is assigned a `CombineMeans` Function as its `function
Expand All @@ -82,17 +82,17 @@
<Function_Modulatory_Params>` of the Mechanism's `function <Mechanism_Base.function>`. Therefore, any Mechanism
specified for control by an LCControlMechanism must be either a `ProcessingMechanism`, or a Mechanism that uses as its
`function <Mechanism_Base.function>` a class of `Function <Function>` that implements a `multiplicative_param
<Function_Modulatory_Params>`. The **modulate_mechanisms** argument must be either a list of such Mechanisms, or
a `Composition` (to modulate all of the `ProcessingMechanisms <ProcessingMechanism>` 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
<Function_Modulatory_Params>`. 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 <ProcessingMechanism>` in a Composition -- see below).
A `ControlProjection` is automatically created that projects from the LCControlMechanism to the
`ParameterPort` for the `multiplicative_param <Function_Modulatory_Params>` 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 <LCControlMechanism.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 <ControlMechanism.objective_mechanism>` of another `ControlMechanism`. Note that only the
as the `objective_mechanism <ControlMechanism.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 <Composition.add_node>` method) after all
Expand All @@ -119,7 +119,7 @@
ObjectiveMechanism
^^^^^^^^^^^^^^^^^^
If an ObjectiveMechanism is `automatically created <LCControlMechanism_ObjectiveMechanism_Creation> for an
If an ObjectiveMechanism is `automatically created <LCControlMechanism_ObjectiveMechanism_Creation>` for an
LCControlMechanism, it receives its inputs from the `OutputPort(s) <OutputPort>` specified the
**monitor_for_control** argument of the LCControlMechanism constructor, or the **montiored_output_ports** argument
of the LCControlMechanism's `ObjectiveMechanism <ControlMechanism_ObjectiveMechanism>`. By default, the
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -394,8 +398,10 @@ class LCControlMechanism(ControlMechanism):
modulated_mechanisms : List[`Mechanism <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 <Mechanism_Base.function>` that implements a `multiplicative_param
<Function_Modulatory_Params>`; alternatively the keyword *ALL* can be used to specify all of the
`ProcessingMechanisms <ProcessingMechanism>` in the Composition(s) to which the LCControlMechanism belongs.
<Function_Modulatory_Params>`; alternatively a `Composition` can be specified, in which case the
LCControlMechanism will modulate all of the suitable `ProcessingMechanisms <ProcessingMechanism>` in the
Composition that have been added to it up to the point at which the LCControlMechanism is constructed (see
`Mechanisms to Modulate <LCControlMechanism_Modulated_Mechanisms>` for additional information).
initial_w_FitzHughNagumo : float : default 0.0
sets `initial_w <initial_w.FitzHughNagumoIntegrator>` on the LCControlMechanism's `FitzHughNagumoIntegrator
Expand Down Expand Up @@ -517,6 +523,12 @@ class LCControlMechanism(ControlMechanism):
<ObjectiveMechanism.function>` to parametrize the contribution made to its output by each of the values that
it monitors (see `ObjectiveMechanism Function <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 <Mechanism_Base.input_port>`, that drives its phasic response
(see `LCControlMechanism_ObjectiveMechanism` for additional details).
function : FitzHughNagumoIntegrator
takes the LCControlMechanism's `input <LCControlMechanism_Input>` and generates its response
<LCControlMechanism_Output>` under
Expand Down Expand Up @@ -544,9 +556,10 @@ class LCControlMechanism(ControlMechanism):
<LCControlMechanism.modulated_mechanisms>` attribute.
modulated_mechanisms : List[Mechanism]
list of `Mechanisms <Mechanism>` modulated by the LCControlMechanism.
list of `Mechanisms <Mechanism>` modulated by the LCControlMechanism (see `Mechanisms to Modulate
<LCControlMechanism_Modulated_Mechanisms>` for additional information).
initial_w_FitzHughNagumo : float : default 0.0
initial_w_FitzHughNagumo : float : default 0.0
sets `initial_w <initial_w.FitzHughNagumoIntegrator>` on the LCControlMechanism's `FitzHughNagumoIntegrator
<FitzHughNagumoIntegrator>` function
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 <Mechanism_Base>` 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 <Mechanism_Base>` that has a multiplicative_param.
"""

super()._validate_params(request_set=request_set,
target_set=target_set,
context=context)
Expand All @@ -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)
Expand All @@ -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:
Expand Down
Loading

0 comments on commit 8d2fafc

Please sign in to comment.