Skip to content

Commit

Permalink
Refactor/control specs (#2276)
Browse files Browse the repository at this point in the history
* -

* • controlmechanism.py:
  - add controlType attribute

• gatingmechanism.py:
  - remove Parameters.output_port._parse_output_ports (handled by ControlMechanism)

* • controlmechanism.py:
  - add controlType attribute

• gatingmechanism.py:
  - remove Parameters.output_port._parse_output_ports (handled by ControlMechanism)

• gatingsignal.py:
  - remove _parse_port_specific_specs (handled by ControlSignal)

* • controlmechanism.py:
  - add controlType attribute

• gatingmechanism.py:
  - remove Parameters.output_port._parse_output_ports (handled by ControlMechanism)

• gatingsignal.py:
  - remove _parse_port_specific_specs (handled by ControlSignal)

* -

* • controlmechanism.py:
  - Parameters._parse_output_port(): remove error for dual spec (handled by ControlSignal)

* -

* -

* -

* -

Co-authored-by: jdcpni <pniintel55>
  • Loading branch information
jdcpni authored Jan 5, 2022
1 parent 491fede commit ab0e3c7
Show file tree
Hide file tree
Showing 9 changed files with 82 additions and 161 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,7 @@ class ControlMechanism(ModulatoryMechanism_Base):
"""

componentType = "ControlMechanism"
controlType = CONTROL # Used as key in specification dictionaries; can be overridden by subclasses

initMethod = INIT_EXECUTE_METHOD_ONLY

Expand Down Expand Up @@ -1210,9 +1211,6 @@ class Parameters(ModulatoryMechanism_Base.Parameters):
constructor_argument=CONTROL
)

# MODIFIED 1/2/22 OLD: - MUCH OF THIS SEEMS TO BE COVERED ELSEWHERE; COMMENTING OUT ONLY CAUSES PROBLEMS WITH
# test_control_signal_and_control_projection_names AND
# test_json_results_equivalence (stroop_conflict_monitoring_py)
def _parse_output_ports(self, output_ports):
def is_2tuple(o):
return isinstance(o, tuple) and len(o) == 2
Expand All @@ -1235,27 +1233,15 @@ def is_2tuple(o):
}
# handle dict of form {PROJECTIONS: <2 item tuple>, <param1>: <value1>, ...}
elif isinstance(output_ports[i], dict):
# Handle CONTROL as synonym of PROJECTIONS
if CONTROL in output_ports[i]:
# MODIFIED 1/3/22 NEW:
# CONTROL AND PROJECTIONS can't both be used
if PROJECTIONS in output_ports[i]:
raise ControlMechanismError(f"Both 'CONTROL' and 'PROJECTIONS' entries found in "
f"specification dict for {ControlSignal.__name__} of "
f"'{self.name}': ({output_ports[i]}).")
# MODIFIED 1/3/22 END
# Replace CONTROL with PROJECTIONS
output_ports[i][PROJECTIONS] = output_ports[i].pop(CONTROL)
if (PROJECTIONS in output_ports[i] and is_2tuple(output_ports[i][PROJECTIONS])):
full_spec_dict = {
NAME: output_ports[i][PROJECTIONS][0],
MECHANISM: output_ports[i][PROJECTIONS][1],
**{k: v for k, v in output_ports[i].items() if k != PROJECTIONS}
}
output_ports[i] = full_spec_dict
for PROJ_SPEC_KEYWORD in {PROJECTIONS, self._owner.controlType}:
if (PROJ_SPEC_KEYWORD in output_ports[i] and is_2tuple(output_ports[i][PROJ_SPEC_KEYWORD])):
tuple_spec = output_ports[i].pop(PROJ_SPEC_KEYWORD)
output_ports[i].update({
NAME: tuple_spec[0],
MECHANISM: tuple_spec[1]})
assert True

return output_ports
# MODIFIED 1/2/22 END

def _validate_input_ports(self, input_ports):
if input_ports is None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@
from psyneulink.core.globals.defaults import defaultGatingAllocation
from psyneulink.core.globals.keywords import \
CONTROL, CONTROL_SIGNALS, GATE, GATING_PROJECTION, GATING_SIGNAL, GATING_SIGNALS, \
INIT_EXECUTE_METHOD_ONLY, MONITOR_FOR_CONTROL, PORT_TYPE, PROJECTION_TYPE
INIT_EXECUTE_METHOD_ONLY, MONITOR_FOR_CONTROL, PORT_TYPE, PROJECTIONS, PROJECTION_TYPE
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
Expand Down Expand Up @@ -373,6 +373,7 @@ class GatingMechanism(ControlMechanism):
"""

componentType = "GatingMechanism"
controlType = GATE

initMethod = INIT_EXECUTE_METHOD_ONLY

Expand Down Expand Up @@ -432,52 +433,6 @@ class Parameters(ControlMechanism.Parameters):
constructor_argument='gate'
)

def _parse_output_ports(self, output_ports):
from psyneulink.core.globals.keywords import NAME, MECHANISM, PROJECTIONS
# # FIX: 1/2/22 - ANY WAY TO CALL SUPER TO CALL ControlMechanism.parameters.output_ports._parse_output_ports
# super().output_ports._parse_output_ports(output_ports)

def is_2tuple(o):
return isinstance(o, tuple) and len(o) == 2

if not isinstance(output_ports, list):
output_ports = [output_ports]

for i in range(len(output_ports)):
# handle 2-item tuple
if is_2tuple(output_ports[i]):

# this is an odd case that uses two names in the name entry
# unsure what it means
if isinstance(output_ports[i][0], list):
continue

output_ports[i] = {
NAME: output_ports[i][0],
MECHANISM: output_ports[i][1]
}
# handle dict of form {PROJECTIONS: <2 item tuple>, <param1>: <value1>, ...}
elif isinstance(output_ports[i], dict):
# Handle GATE as synonym of PROJECTIONS
if GATE in output_ports[i]:
# GATE AND PROJECTIONS can't both be used
if PROJECTIONS in output_ports[i]:
raise GatingMechanismError(f"Both 'PROJECTIONS' and 'GATE' entries found in "
f"specification dict for {GatingSignal.__name__} of "
f"'{self.name}': ({output_ports[i]}).")
# Replace GATE with PROJECTIONS
output_ports[i][PROJECTIONS] = output_ports[i].pop(GATE)
if (PROJECTIONS in output_ports[i] and is_2tuple(output_ports[i][PROJECTIONS])):
full_spec_dict = {
NAME: output_ports[i][PROJECTIONS][0],
MECHANISM: output_ports[i][PROJECTIONS][1],
**{k: v for k, v in output_ports[i].items() if k != PROJECTIONS}
}
output_ports[i] = full_spec_dict

return output_ports


@tc.typecheck
def __init__(self,
default_gating_allocation=None,
Expand Down Expand Up @@ -543,6 +498,11 @@ def _instantiate_control_signal_type(self, gating_signal_spec, context):
from psyneulink.core.components.projections.projection import ProjectionError

allocation_parameter_default = self.parameters.gating_allocation.default_value

# Handle controlType as synonym for PROJECTIONS:
if isinstance(gating_signal_spec, dict) and self.controlType in gating_signal_spec:
gating_signal_spec[PROJECTIONS] = gating_signal_spec.pop(self.controlType)

gating_signal = _instantiate_port(port_type=GatingSignal,
owner=self,
variable=self.default_allocation # User specified value
Expand All @@ -551,8 +511,10 @@ def _instantiate_control_signal_type(self, gating_signal_spec, context):
modulation=self.defaults.modulation,
port_spec=gating_signal_spec,
context=context)

if not type(gating_signal) in convert_to_list(self.outputPortTypes):
raise ProjectionError(f'{type(gating_signal)} inappropriate for {self.name}')

return gating_signal

def _check_for_duplicates(self, control_signal, control_signals, context):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,8 @@ def _create_randomization_control_signal(self, context, set_control_signal_index
# 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
# FIX: 1/4/22: CHECK IF THIS WORKS WITH CFA
# (i.e., DOES IT [make sense to HAVE A random_variables ATTRIBUTE?)
# Get Components with variables to be randomized across estimates
# and construct ControlSignal to modify their seeds over estimates
if self.random_variables is ALL:
Expand Down
75 changes: 38 additions & 37 deletions psyneulink/core/components/ports/modulatorysignals/controlsignal.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,29 @@ class ControlSignal(ModulatorySignal):
#region CLASS ATTRIBUTES

componentType = CONTROL_SIGNAL
componentName = 'ControlSignal'
errorType = ControlSignalError

paramsType = OUTPUT_PORT_PARAMS
portAttributes = ModulatorySignal.portAttributes | {ALLOCATION_SAMPLES,
COST_OPTIONS,
INTENSITY_COST_FUNCTION,
ADJUSTMENT_COST_FUNCTION,
DURATION_COST_FUNCTION,
COMBINE_COSTS_FUNCTION}

connectsWith = [PARAMETER_PORT, INPUT_PORT, OUTPUT_PORT]
connectsWithAttribute = [PARAMETER_PORTS, INPUT_PORTS, OUTPUT_PORTS]
projectionSocket = RECEIVER
modulators = []
projection_type = CONTROL_PROJECTION

classPreferenceLevel = PreferenceLevel.TYPE
# Any preferences specified below will override those specified in TYPE_DEFAULT_PREFERENCES
# Note: only need to specify setting; level will be assigned to TYPE automatically
# classPreferences = {
# PREFERENCE_SET_NAME: 'OutputPortCustomClassPreferences',
# PREFERENCE_KEYWORD<pref>: <setting>...}

class Parameters(ModulatorySignal.Parameters):
"""
Expand Down Expand Up @@ -768,26 +790,6 @@ def _validate_allocation_samples(self, allocation_samples):
# not iterable, so assume single value
pass

portAttributes = ModulatorySignal.portAttributes | {ALLOCATION_SAMPLES,
COST_OPTIONS,
INTENSITY_COST_FUNCTION,
ADJUSTMENT_COST_FUNCTION,
DURATION_COST_FUNCTION,
COMBINE_COSTS_FUNCTION}

connectsWith = [PARAMETER_PORT, INPUT_PORT, OUTPUT_PORT]
connectsWithAttribute = [PARAMETER_PORTS, INPUT_PORTS, OUTPUT_PORTS]
projectionSocket = RECEIVER
modulators = []
projection_type = CONTROL_PROJECTION

classPreferenceLevel = PreferenceLevel.TYPE
# Any preferences specified below will override those specified in TYPE_DEFAULT_PREFERENCES
# Note: only need to specify setting; level will be assigned to TYPE automatically
# classPreferences = {
# PREFERENCE_SET_NAME: 'OutputPortCustomClassPreferences',
# PREFERENCE_KEYWORD<pref>: <setting>...}

#endregion

@tc.typecheck
Expand Down Expand Up @@ -1033,37 +1035,36 @@ def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec):
[TBI:] (Mechanism, parameter name, weight, exponent, projection_specs)
Returns params dict with CONNECTIONS entries if any of these was specified.
"""

from psyneulink.core.components.projections.projection import _parse_connection_specs

params_dict = {}
port_spec = port_specific_spec
dual_spec_error = self.errorType(f"Both 'PROJECTIONS' and '{owner.controlType.upper()}' entries found in "
f"specification dict for '{port_dict['port_type'].__name__}' of "
f"'{owner.name}'. Must use only one or the other.")

if isinstance(port_specific_spec, dict):
# MODIFIED 1/2/22 NEW:
# Note: if CONTROL is specified alone, it is moved to PROJECTIONS in Port._parse_ort_spec()
if CONTROL in port_specific_spec and PROJECTIONS in port_specific_spec:
raise ControlSignalError(f"Both 'PROJECTIONS' and 'CONTROL' entries found in specification dict "
f"for '{port_dict['port_type'].__name__}' of '{owner.name}'. "
f"Must use only one or the other.")
# MODIFIED 1/2/22 END
if owner.controlType in port_specific_spec:
# owner.controlType *and* PROJECTIONS can't both be used
if PROJECTIONS in port_specific_spec:
raise dual_spec_error
# Move owner.controlType entry to PROJECTIONS
port_specific_spec[PROJECTIONS] = port_specific_spec.pop(owner.controlType)
return None, port_specific_spec

elif isinstance(port_specific_spec, tuple):

port_spec = None
# MODIFIED 1/2/22 NEW:
# Resolve CONTROL as synonym for PROJECTIONS:
if CONTROL in params_dict:
# CONTROL AND PROJECTIONS can't both be used
# Resolve owner.controlType as synonym for PROJECTIONS:
if owner.controlType in params_dict:
# owner.controlType *and* PROJECTIONS can't both be used
if PROJECTIONS in params_dict:
raise ControlSignalError(f"Both 'PROJECTIONS' and 'CONTROL' entries found in specification dict "
f"for '{port_dict['port_type'].__name__}' of '{owner.name}'. "
f"Must use only one or the other.")
# Move CONTROL to PROJECTIONS
params_dict[PROJECTIONS] = params_dict.pop(CONTROL)
# MODIFIED 1/2/22 END
raise dual_spec_error
# Move owner.controlType entry to PROJECTIONS
params_dict[PROJECTIONS] = params_dict.pop(owner.controlType)
params_dict[PROJECTIONS] = _parse_connection_specs(connectee_port_type=self,
owner=owner,
connections=port_specific_spec)
Expand Down
52 changes: 2 additions & 50 deletions psyneulink/core/components/ports/modulatorysignals/gatingsignal.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,9 @@ class GatingSignal(ControlSignal):

componentType = GATING_SIGNAL
componentName = 'GatingSignal'
paramsType = OUTPUT_PORT_PARAMS
errorType = GatingSignalError

paramsType = OUTPUT_PORT_PARAMS
portAttributes = ControlSignal.portAttributes | {GATE}

connectsWith = [INPUT_PORT, OUTPUT_PORT]
Expand Down Expand Up @@ -474,55 +475,6 @@ def __init__(self,
transfer_function=transfer_function,
**kwargs)

def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec):
"""Get connections specified in a ParameterPort specification tuple
Tuple specification can be:
(Port name, Mechanism)
[TBI:] (Mechanism, Port name, weight, exponent, projection_specs)
Returns params dict with CONNECTIONS entries if any of these was specified.
"""
from psyneulink.core.components.projections.projection import _parse_connection_specs

params_dict = {}
port_spec = port_specific_spec

# MODIFIED 1/2/22 NEW:
if isinstance(port_specific_spec, dict):
# Note: if GATE is specified alone, it is moved to PROJECTIONS in Port._parse_ort_spec()
if GATE in port_specific_spec and PROJECTIONS in port_specific_spec:
raise GatingSignalError(f"Both 'PROJECTIONS' and 'GATE' entries found in specification dict "
f"for '{port_dict['port_type'].__name__}' of '{owner.name}'. "
f"Must use only one or the other.")
return None, port_specific_spec
# MODIFIED 1/2/22 END

elif isinstance(port_specific_spec, tuple):
port_spec = None
# Resolve CONTROL as synonym for PROJECTIONS:
if GATE in params_dict:
# CONTROL AND PROJECTIONS can't both be used
if PROJECTIONS in params_dict:
raise GatingSignalError(f"Both 'PROJECTIONS' and 'GATE' entries found in specification dict "
f"for '{port_dict['port_type'].__name__}' of '{owner.name}'. "
f"Must use only one or the other.")
# Move GATE to PROJECTIONS
params_dict[PROJECTIONS] = params_dict.pop(GATE)
params_dict[PROJECTIONS] = _parse_connection_specs(connectee_port_type=self,
owner=owner,
connections=port_specific_spec)
elif port_specific_spec is not None:
raise GatingSignalError("PROGRAM ERROR: Expected tuple or dict for {}-specific params but, got: {}".
format(self.__class__.__name__, port_specific_spec))

if params_dict[PROJECTIONS] is None:
raise GatingSignalError("PROGRAM ERROR: No entry found in {} params dict for {} "
"with specification of {}, {} or GatingProjection(s) to it".
format(GATING_SIGNAL, INPUT_PORT, OUTPUT_PORT, owner.name))
return port_spec, params_dict

def _instantiate_cost_functions(self, context):
"""Override ControlSignal as GatingSignal has not cost functions"""
pass
Expand Down
6 changes: 6 additions & 0 deletions psyneulink/core/compositions/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -7718,6 +7718,7 @@ def add_controller(self, controller:ControlMechanism, context=None):
for node in self.nodes:
self._instantiate_deferred_init_control(node, context)

# MODIFIED 1/4/22 OLD:
if hasattr(self.controller, NUM_ESTIMATES) and self.controller.num_estimates:
if RANDOMIZATION_CONTROL_SIGNAL not in self.controller.output_ports.names:
try:
Expand All @@ -7730,6 +7731,11 @@ def add_controller(self, controller:ControlMechanism, context=None):
self.controller.output_ports.names.index(RANDOMIZATION_CONTROL_SIGNAL),
context
)
self.controller.function.parameters.randomization_dimension._set(
self.controller.output_ports.names.index(RANDOMIZATION_CONTROL_SIGNAL),
context
)
# MODIFIED 1/4/22 END

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

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ markers =
composition: PsyNeuLink Composition tests
llvm: Tests using LLVM runtime compiler
cuda: Tests using LLVM runtime compiler and CUDA GPGPU backend
control: Tests including control mechanism
control: Tests including control mechanism and/or control projection
projection
nested: Tests including nested compositions
function: Tests of Function classes
Expand Down
Loading

0 comments on commit ab0e3c7

Please sign in to comment.