Skip to content

Commit

Permalink
Misc/ocm/docs and tests (#2223)
Browse files Browse the repository at this point in the history
• composition.py
  - add calls to _validate_monitor_for_control()

• controlmechanism.py, 
  - add _validate_monitor_for_control()

• optimizationcontrolmechanism.py:  
  - add _validate_monitor_for_control() for agent_rep
  - major docsring revisions

• test_control:
  add test_args_specific_to_ocm:  tests use of state_features, monitor_for_control, objective_mechanism and allow_probe args
  • Loading branch information
jdcpni authored Dec 5, 2021
1 parent a1d4f2a commit 002ff21
Show file tree
Hide file tree
Showing 9 changed files with 726 additions and 442 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -630,9 +630,9 @@ def _is_control_spec(spec):
return False

class ControlMechanismError(Exception):
def __init__(self, error_value):
def __init__(self, error_value, data=None):
self.error_value = error_value

self.data = data

def validate_monitored_port_spec(owner, spec_list):
for spec in spec_list:
Expand Down Expand Up @@ -1620,6 +1620,22 @@ def _parse_monitor_for_control_input_ports(self, context):

return outcome_input_port_specs, port_value_sizes, monitored_ports

def _validate_monitor_for_control(self, nodes):
# Ensure all of the Components being monitored for control are in the Composition being controlled
from psyneulink.core.components.ports.port import Port
invalid_outcome_specs = [item for item in self.monitor_for_control
if ((isinstance(item, Mechanism)
and item not in nodes)
or ((isinstance(item, Port)
and item.owner not in nodes)))]
if invalid_outcome_specs:
names = [item.name if isinstance(item, Mechanism) else item.owner.name
for item in invalid_outcome_specs]
raise ControlMechanismError(f"{self.name} has 'outcome_ouput_ports' that receive "
f"Projections from the following Components that do not "
f"belong to the Composition it controls: {names}.",
names)

def _instantiate_output_ports(self, context=None):

# ---------------------------------------------------
Expand Down Expand Up @@ -1793,7 +1809,6 @@ def _check_for_duplicates(self, control_signal, control_signals, context):
f"has one or more {projection_type.__name__}s redundant with ones already on "
f"an existing {ControlSignal.__name__} ({existing_ctl_sig.name}).")


def show(self):
"""Display the OutputPorts monitored by ControlMechanism's `objective_mechanism
<ControlMechanism.objective_mechanism>` and the parameters modulated by its `control_signals
Expand Down Expand Up @@ -1894,7 +1909,8 @@ def _activate_projections_for_compositions(self, composition=None):
# 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."
f"PROGRAM ERROR: {OBJECTIVE_MECHANISM} for {self.name} " \
f"not listed in its 'aux_components' attribute."
dependent_projections.add(self._objective_projection)
# Add all Projections to and from objective_mechanism
for aff in self.objective_mechanism.afferents:
Expand Down

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions psyneulink/core/components/ports/inputport.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,21 @@
list. In other words, for each InputPort specified, a new one is created that receives exactly the same inputs
from the same `senders <Projection_Base.sender>` as the ones specified.
If an InputPort shadows another, its `shadow_inputs <InputPort.shadow_inputs>` attribute identifies the InputPort
that it shadows.
.. note::
Only InputPorts belonging to Mechanisms in the *same Composition*, or ones that are `INPUT <NodeRole.INPUT>`
`Nodes <Composition_Nodes>` of a `nested <Composition_Nested>` can be specified for shadowing.
.. hint::
If an InputPort needs to be shadowed that belongs to a Mechanism in a `nested <Composition_Nested>` that is
not an `INPUT <NodeRole.INPUT>` `Node <Composition_Nodes>` of that Composition, this can be accomplished as
follows: 1) add a Mechanism to the nested Composition with an InputPort that shadows the one to be
shadowed; 2) specify `OUTPUT <NodeRole.INPUT>` as a `required_role <Composition.add_node.required_roles>`
for that Mechanism; 3) use that Mechanism as the `InputPort specification <InputPort_Specification>`
for the shadowing InputPort.
.. _InputPort_Compatability_and_Constraints:
InputPort `variable <InputPort.variable>`: Compatibility and Constraints
Expand Down Expand Up @@ -659,6 +674,9 @@ class InputPort(Port_Base):
<Port.owner>` is an `INPUT` `Node <Composition_Nodes>` of that Composition; if `True`, external input is
*not* required or allowed.
shadow_inputs : InputPort
identifies the InputPort of another `Mechanism` that is being shadowed by this InputPort.
name : str
the name of the InputPort; if it is not specified in the **name** argument of the constructor, a default is
assigned by the InputPortRegistry of the Mechanism to which the InputPort belongs. Note that some Mechanisms
Expand Down
19 changes: 15 additions & 4 deletions psyneulink/core/compositions/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -3165,7 +3165,7 @@ class Composition(Composition_Base, metaclass=ComponentsMeta):

controller_time_scale: TimeScale[TIME_STEP, PASS, TRIAL, RUN] : default TRIAL
deterines the frequency at which the `controller <Composition.controller>` is executed, either before or
after the Composition as determined by `controller_mode <cComposition.ontroller_mode>` (see
after the Composition as determined by `controller_mode <Composition.ontroller_mode>` (see
`Composition_Controller_Execution` for additional details).

controller_condition : Condition
Expand Down Expand Up @@ -3909,6 +3909,12 @@ def _get_nested_compositions(self,
visited_compositions)
return nested_compositions

def _get_all_nodes(self):
"""Return all nodes, including those within nested Compositions at any level
Note: this is distinct from the _all_nodes propety, which returns all nodes at the top level
"""
return [k[0] for k in self._get_nested_nodes()] + list(self.nodes)

def _determine_origin_and_terminal_nodes_from_consideration_queue(self):
"""Assigns NodeRole.ORIGIN to all nodes in the first entry of the consideration queue and NodeRole.TERMINAL
to all nodes in the last entry of the consideration queue. The ObjectiveMechanism of a Composition's
Expand Down Expand Up @@ -4106,6 +4112,7 @@ def _complete_init_of_partially_initialized_nodes(self, context=None):
if hasattr(self.controller, 'state_input_ports'):
self.controller._update_state_input_ports_for_controller(context=context)
# self._instantiate_controller_shadow_projections(context=context)
self.controller._validate_monitor_for_control(self._get_all_nodes())
self._instantiate_control_projections(context=context)
# FIX: 11/15/21 - CAN'T SET TO FALSE HERE, AS THIS IS CALLED BY _analyze_graph() FROM add_node()
# BEFORE PROJECTIONS TO THE NODE HAS BEEN ADDED (AFTER CALL TO add_node())
Expand Down Expand Up @@ -5384,9 +5391,10 @@ def _get_correct_sender(comp, shadowed_projection):
def _get_sender_at_right_level(shadowed_proj):
"""Search back up hierarchy of nested Compositions for sender at same level as **input_port**"""
if not isinstance(shadowed_proj.sender.owner, CompositionInterfaceMechanism):
raise CompositionError(f"Attempt to shadow the input(s) to a node "
raise CompositionError(f"Attempt to shadow the input to a node "
f"({shadowed_proj.receiver.owner.name}) in a nested Composition "
f"(of {self.name}) is not currently supported.")
f"of {self.name} that is not an INPUT Node of that Composition is "
f"not currently supported.")
else:
# WANT THIS ONE'S SENDER
# item[0] item[1,0] item[1,1]
Expand Down Expand Up @@ -7182,6 +7190,7 @@ def add_controller(self, controller:ControlMechanism, context=None):
if self.controller.objective_mechanism:
# If controller has objective_mechanism, then add it and all associated Projections to Composition
if self.controller.objective_mechanism not in invalid_aux_components:
self.controller._validate_monitor_for_control(self._get_all_nodes())
self.add_node(self.controller.objective_mechanism, required_roles=NodeRole.CONTROLLER_OBJECTIVE)
else:
# Otherwise, if controller has any afferent inputs (from items in monitor_for_control), add them
Expand All @@ -7205,7 +7214,9 @@ def add_controller(self, controller:ControlMechanism, context=None):
keep_checking = True
# Otherwise, return usual error
else:
raise CompositionError(e.error_value)
error_msg = e.error_value + f" Try setting 'allow_probes' argument of " \
f"{self.controller.name} to True."
raise CompositionError(error_msg)
else:
assert False, f"PROGRAM ERROR: Unable to apply NodeRole.OUTPUT to {node} of {nested_comp} "\
f"specified in 'monitor_for_control' arg for {controller.name} of {self.name}"
Expand Down
14 changes: 6 additions & 8 deletions psyneulink/core/globals/keywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@
'MECHANISM_DEFAULTParams', 'MECHANISM_EXECUTED_LOG_ENTRY', 'MECHANISM_NAME', 'MECHANISM_PARAM_VALUE',
'MECHANISM_TYPE', 'MECHANISM_VALUE', 'MEDIAN', 'METRIC', 'MIN_VAL', 'MIN_ABS_VAL', 'MIN_ABS_INDICATOR',
'MODE', 'MODULATES','MODULATION', 'MODULATORY_PROJECTION', 'MODULATORY_SIGNAL', 'MODULATORY_SIGNALS',
'MONITOR', 'MONITOR_FOR_CONTROL', 'MONITOR_FOR_LEARNING', 'MONITOR_FOR_MODULATION', 'MODEL_FREE', 'MODEL_BASED',
'MODEL_SPEC_ID_GENERIC', 'MODEL_SPEC_ID_INPUT_PORTS', 'MODEL_SPEC_ID_OUTPUT_PORTS', 'MODEL_SPEC_ID_PSYNEULINK',
'MODEL_SPEC_ID_SENDER_MECH', 'MODEL_SPEC_ID_SENDER_PORT', 'MODEL_SPEC_ID_RECEIVER_MECH',
'MODEL_SPEC_ID_RECEIVER_PORT',
'MODEL_SPEC_ID_PARAMETER_SOURCE', 'MODEL_SPEC_ID_PARAMETER_VALUE', 'MODEL_SPEC_ID_TYPE', 'MSE',
'MULTIPLICATIVE', 'MULTIPLICATIVE_PARAM', 'MUTUAL_ENTROPY',
'MONITOR', 'MONITOR_FOR_CONTROL', 'MONITOR_FOR_LEARNING', 'MONITOR_FOR_MODULATION',
'MODEL_SPEC_ID_GENERIC', 'MODEL_SPEC_ID_INPUT_PORTS', 'MODEL_SPEC_ID_OUTPUT_PORTS',
'MODEL_SPEC_ID_PSYNEULINK', 'MODEL_SPEC_ID_SENDER_MECH', 'MODEL_SPEC_ID_SENDER_PORT',
'MODEL_SPEC_ID_RECEIVER_MECH', 'MODEL_SPEC_ID_RECEIVER_PORT','MODEL_SPEC_ID_PARAMETER_SOURCE',
'MODEL_SPEC_ID_PARAMETER_VALUE', 'MODEL_SPEC_ID_TYPE',
'MSE', 'MULTIPLICATIVE', 'MULTIPLICATIVE_PARAM', 'MUTUAL_ENTROPY',
'NAME', 'NESTED', 'NEWEST', 'NODE', 'NOISE', 'NORMAL_DIST_FUNCTION', 'NORMED_L0_SIMILARITY', 'NOT_EQUAL',
'NUM_EXECUTIONS_BEFORE_FINISHED',
'OBJECTIVE_FUNCTION_TYPE', 'OBJECTIVE_MECHANISM', 'OBJECTIVE_MECHANISM_OBJECT', 'OFF', 'OFFSET', 'OLDEST', 'ON',
Expand Down Expand Up @@ -776,8 +776,6 @@ def _is_metric(metric):
SAVE_ALL_VALUES_AND_POLICIES = 'save_all_values_and_policies'
EVC_SIMULATION = 'CONTROL SIMULATION'
ALLOCATION_SAMPLES = "allocation_samples"
MODEL_FREE = 'model_free'
MODEL_BASED = 'model_based'

# GatingMechanism
GATING_SIGNALS = 'gating_signals'
Expand Down
2 changes: 1 addition & 1 deletion psyneulink/library/compositions/regressioncfa.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ class RegressionCFA(CompositionFunctionApproximator):
regression_weights : 1d array
result returned by `update_weights <RegressionCFA.update_weights>, and used by
`evaluate <RegressionCFA.evaluate>` method together with `prediction_vector <RegressionCFA.prediction_vector>`
to generate predicted `net_outcome <OptimizationControlMechanism.net_outcome>`.
to generate predicted `net_outcome <ControlMechanism.net_outcome>`.
"""

Expand Down
5 changes: 3 additions & 2 deletions tests/composition/test_composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -5907,8 +5907,9 @@ def test_shadow_nested_nodes(self, condition):
with pytest.raises(CompositionError) as err:
O = ProcessingMechanism(name='O',input_ports=[B.input_port])
ocomp = Composition(nodes=[mcomp,O], name='OUTER COMP')
assert 'Attempt to shadow the input(s) to a node (B) in a nested Composition ' \
'(of OUTER COMP) is not currently supported.' in err.value.error_value
assert 'Attempt to shadow the input to a node (B) in a nested Composition of OUTER COMP ' \
'that is not an INPUT Node of that Composition is not currently supported.' \
in err.value.error_value

def test_monitor_input_ports(self):
comp = Composition(name='comp')
Expand Down
Loading

0 comments on commit 002ff21

Please sign in to comment.