Skip to content

Commit

Permalink
OCM: handle mechanism-control spec for randomization_control_signal
Browse files Browse the repository at this point in the history
randomization_control_signal would fail to generate if control is
specified in the parameter value of a mechanism, as in
pnl.TransferMechanism(function=pnl.Linear(slope=(1.0, pnl.CONTROL))),
instead of in the control_signals argument of
pnl.OptimizationControlMechanism.__init__
  • Loading branch information
kmantel committed Dec 14, 2021
1 parent 18b67cd commit e41f205
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,7 @@
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, RANDOMIZATION_DIMENSION
GridSearch, OBJECTIVE_FUNCTION, SEARCH_SPACE
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 \
Expand Down Expand Up @@ -1714,23 +1714,6 @@ def _instantiate_control_signals(self, context):
# control_signals_from_composition = self.agent_rep._get_control_signals_for_composition()
# self.output_ports.extend(control_signals_from_composition)
# MODIFIED 11/21/21 END

if self.num_estimates:

randomization_seed_mod_values = SampleSpec(start=1,stop=self.num_estimates,step=1)

# FIX: 11/3/21 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 Components with variables to be randomized across estimates
# and construct ControlSignal to modify their seeds over estimates
if self.random_variables is ALL:
self.random_variables = self.agent_rep.random_variables
self.output_ports.append(ControlSignal(name=RANDOMIZATION_CONTROL_SIGNAL,
modulates=[param.parameters.seed._port
for param in self.random_variables],
allocation_samples=randomization_seed_mod_values))

control_signals = []
for i, spec in list(enumerate(self.output_ports)):
control_signal = self._instantiate_control_signal(spec, context=context)
Expand All @@ -1742,6 +1725,11 @@ def _instantiate_control_signals(self, context):
# MODIFIED 11/20/21 END
self.output_ports[i] = control_signal

self._create_randomization_control_signal(
context,
set_control_signal_index=False
)

self.defaults.value = np.tile(
control_signal.parameters.variable.default_value,
(len(self.output_ports), 1)
Expand Down Expand Up @@ -1785,27 +1773,13 @@ 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 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].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} ControlSignal ({num_seeds})."
randomization_control_signal_index = self.control_signals.names.index(RANDOMIZATION_CONTROL_SIGNAL)

# Assign parameters to function (OptimizationFunction) that rely on OptimizationControlMechanism
self.function.reset(**{
DEFAULT_VARIABLE: self.parameters.control_allocation._get(context),
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),
RANDOMIZATION_DIMENSION: randomization_control_signal_index
})

if isinstance(self.agent_rep, type):
Expand Down Expand Up @@ -1969,6 +1943,65 @@ def evaluate_agent_rep(self, control_allocation, context=None, return_results=Fa
context=context
)

def _create_randomization_control_signal(
self,
context,
set_control_signal_index=True
):
if self.num_estimates:
# must be SampleSpec in allocation_samples arg
randomization_seed_mod_values = SampleSpec(start=1, stop=self.num_estimates, step=1)

# FIX: 11/3/21 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 Components with variables to be randomized across estimates
# and construct ControlSignal to modify their seeds over estimates
if self.random_variables is ALL:
self.random_variables = self.agent_rep.random_variables

randomization_control_signal = ControlSignal(
name=RANDOMIZATION_CONTROL_SIGNAL,
modulates=[
param.parameters.seed._port
for param in self.random_variables
],
allocation_samples=randomization_seed_mod_values
)
randomization_control_signal_index = len(self.output_ports)
randomization_control_signal._variable_spec = (
OWNER_VALUE, randomization_control_signal_index
)
randomization_control_signal = self._instantiate_control_signal(
randomization_control_signal, context
)
self.output_ports.append(randomization_control_signal)

# Otherwise, assert that num_estimates and number of seeds generated by randomization_control_signal are equal
num_seeds = self.control_signals[RANDOMIZATION_CONTROL_SIGNAL].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} ControlSignal ({num_seeds})."

function_search_space = self.function.parameters.search_space._get(context)
if randomization_control_signal_index >= len(function_search_space):
# TODO: check here if search_space has an item for each
# control_signal? or is allowing it through for future
# checks the right way?

# search_space must be a SampleIterator
function_search_space.append(SampleIterator(randomization_seed_mod_values))

# workaround for fact that self.function.reset call in
# _instantiate_attributes_after_function expects to use
# old/unset values when running _update_default_variable,
# which calls self.agent_rep.evaluate and is brittle.
if set_control_signal_index:
self.function.parameters.randomization_dimension._set(
randomization_control_signal_index, context
)

def _get_evaluate_input_struct_type(self, ctx):
# We construct input from optimization function input
return ctx.get_input_struct_type(self.function)
Expand Down
14 changes: 13 additions & 1 deletion psyneulink/core/compositions/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -2530,7 +2530,7 @@ def input_function(env, result):
from psyneulink.core.components.functions.nonstateful.transferfunctions import Identity
from psyneulink.core.components.mechanisms.mechanism import Mechanism_Base, MechanismError, MechanismList
from psyneulink.core.components.mechanisms.modulatory.control.controlmechanism import ControlMechanism
from psyneulink.core.components.mechanisms.modulatory.control.optimizationcontrolmechanism import AGENT_REP
from psyneulink.core.components.mechanisms.modulatory.control.optimizationcontrolmechanism import AGENT_REP, RANDOMIZATION_CONTROL_SIGNAL
from psyneulink.core.components.mechanisms.modulatory.learning.learningmechanism import \
LearningMechanism, ACTIVATION_INPUT_INDEX, ACTIVATION_OUTPUT_INDEX, ERROR_SIGNAL, ERROR_SIGNAL_INDEX
from psyneulink.core.components.mechanisms.modulatory.modulatorymechanism import ModulatoryMechanism_Base
Expand Down Expand Up @@ -7474,6 +7474,18 @@ def add_controller(self, controller:ControlMechanism, context=None):
for node in self.nodes:
self._instantiate_deferred_init_control(node, context)

if RANDOMIZATION_CONTROL_SIGNAL not in self.controller.output_ports.names:
try:
self.controller._create_randomization_control_signal(context)
except AttributeError:
# ControlMechanism does not use RANDOMIZATION_CONTROL_SIGNAL
pass
else:
self.controller.function.parameters.randomization_dimension._set(
self.controller.output_ports.names.index(RANDOMIZATION_CONTROL_SIGNAL),
context
)

# ACTIVATE FOR COMPOSITION -----------------------------------------------------

self.node_ordering.append(controller)
Expand Down
2 changes: 1 addition & 1 deletion tests/composition/test_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ def test_state_input_ports_for_two_input_nodes(self):
"[pnl.ControlSignal(modulates=('slope', a), allocation_samples=[1, 2])]",
]
)
@pytest.mark.parametrize('ocm_num_estimates', [None, 1])
@pytest.mark.parametrize('ocm_num_estimates', [None, 1, 2])
@pytest.mark.parametrize(
'slope, intercept',
[
Expand Down

0 comments on commit e41f205

Please sign in to comment.