From 1fdb95da128ed07b656b79abdd8e55e845b1f29b Mon Sep 17 00:00:00 2001 From: jdcpni Date: Mon, 3 Jan 2022 11:09:16 -0500 Subject: [PATCH] Feat/control signal/add control arg (#2272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * • optimizationcontrolmechanism.py: added feature_input_ports attribute and num_feature_input_ports property * • optimizationcontrolmechanism.py: added feature_input_ports attribute and num_feature_input_ports property • parameterestimationcomposition.py: fixed misplacement of its Parameters() attribute * • optimizationcontrolmechanism.py: added feature_input_ports attribute and num_feature_input_ports property • parameterestimationcomposition.py: fixed misplacement of its Parameters() attribute • optimizationfunctions.py: made num_estimates a Parameter * - modified test_mode_based_num_estimates * - * - * • optimizationcontrolmechanism.py: - _instantiate_control_signals: random_seeds -> random_seed_mod_values * • composition.py - _add_controller: modifying to instantiate feature_input_ports if none are specified * • composition.py: - add_controller: now adds feature_input_ports for Compostion INPUT nodes if not state_features not specified * - * • composition.py - _add_controller: modifying to instantiate feature_input_ports if none are specified * • composition.py: - add_controller: assign simulation_input_ports * - * • optimizationcontrolmechanism.py: - feature_input_ports -> state_input_ports - _instantiate_input_ports(): state_features only allowed to specifying state_input_ports if agent_rep is a CompositionFunctionApproximator (i.e., model-free optimization) • composition.py: - add_controller: adds state_input_ports to shadow INPUT Nodes of Composition if controller.agent_rep is Composition (model-based optimziation) or state_features have not been specified (for model-free optimizaton) * - * • optimizationcontrolmechanism.py: _instantiate_input_ports: reinstate allowance of state_features specification if agent_rep is a Composition (i.e., model-based optimization) as long as they are all INPUT Nodes of agent_rep * - * - * • optimizationcontrolmechanism.py - _gen_llvm_evaluate_function: num_estimates -> num_estimates_per_trial * - * • optimizationcontrolmechanism.py - _gen_llvm_evaluate_function: num_estimates -> num_trial_per_estimate * • optimizationcontrolmechanism.py - _gen_llvm_evaluate_function: num_estimates -> num_trials_per_estimate * - * - * - * - * • composition.py - __init__: moved controller instantiation until after nodes, projections and pathways * • composition.py - __init__: restored add_controller position * llvm/struct generation: Make sure num_estimats per trial is always integer Signed-off-by: Jan Vesely * - * • composition.py: - _update_controller: added - add_controller and _analyze_graph(): call _update_controller * - * • composition.py _update_controller: fixed to loop through all input_ports of comp INPUT nodes * • test_control.py - test_agent_rep_assignement_as_controller_and_replacement: updated to test that shadowing projections to state_input_ports are properly added and deleted * • optimizationfunctions.py: - _function: refactored to put use aggregation_function at end - _grid_evaluate: still needs to return all_samples * - * • composition.py - added call to _update_controller to add_node - moved test for projections to controller.state_input_ports to run() * - * • composition.py: moved calls to _update_controller to _complete_init_of_partially_initialized_nodes moved _update_controller to ocm._update_state_input_ports • optimizationcontrolmechanism.py: added _update_state_input_ports [**still needed work**] * • composition.py: moved calls to _update_controller to _complete_init_of_partially_initialized_nodes moved _update_controller to ocm._update_state_input_ports _instantiate_controller_shadow_projections [still needs to be implemented] • optimizationcontrolmechanism.py: added _update_state_input_ports [**still needed work**] * • composition.py added needs_update_controller * - * • composition.py: - implemented self.needs_update_controller - moved implementation of controlsignal projections from add_controller to _instantiate_control_projections that is called in _complete_init_of_partially_initialized_nodes Note: still need to set self.needs_update_controller to False after instantiating state_input_ports and projections to them * - * - * - * - * - * - * - * - * • Passing all test_control tests except test_mode_based_num_estimates * • Passing all test_control tests * - * • optimizationcontrolmechanism.py - _update_state_input_ports_for_controller: handle nested input nodes * - * • optimizationcontrolmechanism.py _update_state_input_ports_for_controller: fixed bug with > 1 INPUT node in Composition * • test_show_graph.py: passes all tests * - * • test_report.py: passing all tests * • Passes all tests! * - * - * • composition.py: reorganize with #region and #enregions * • composition.py: reorganize with #region and #enregions * • controlmechanism.py, optimizationcontrolmechanism.py: - _instantiate_monitor_for_control_input_ports -> _parse_monitor_control_input_ports - refactored to support allow_probes option on ocm * - * - * - * • controlmechanism.py, optimizationcontrolmechanism.py: - _instantiate_monitor_for_control_input_ports -> _parse_monitor_control_input_ports - refactored to support allow_probes option on ocm * • controlmechanism.py, optimizationcontrolmechanism.py: - _instantiate_monitor_for_control_input_ports -> _parse_monitor_control_input_ports - refactored to support allow_probes option on ocm * - * • composition.py: __init__: move controller to after add_nodes and add_linear_pathway * - * - test_control: only test_hanging_control_spec_outer_controller not passing * - * - * - * - * - * - * • composition.py: _instantiate_control_projections: weird requirement for double-call to controller._instantiate_control_signal * • test_paremtercomposition.py: restored parameter spec that causes crash ('threshold',Decision2) * ª Attempt to fix problem with partially overlapping local and ocm control specs - composition.py - _get_control_signals_for_composition: (see 11/20/21) - added (but commented out change) to "if node.controller" to "if not node.controller" - changed append to extend - _instantiation_control_projection: - got rid of try and except double-call to controller._instantiate_control_signals - outdented call to self.controller._activate_projections_for_composition at end - controlmechanism.py: - _check_for_duplicates: add warning and return duplicates - optimizationcontrolmechanism._instantiate_control_signals: - add call to self.agent_rep._get_control_signals_for_composition() to get local control specs (on mechs in comp) - eliminate duplicates with control_signal specs on OCM - instantiate local + ocm control_signals - parameterestimationcomposition.py - added context to various calls * see later commit * see later commit * see later commit * see later commit * - This branch passes all tests except: - test_parameterestimationcomposition - test_composition/test_partially_overlapping_control_specs (ADDED IN THIS COMMINT) - All relevant changes to this branch are marked as "11/21/21." However, most are commented out as they break other things. - The tests above both involve local control specifications (on mechanism within a nested comp) and on the OCM for the outer composition, some of which are for the same nested mechs - Both tests fail with: "AttributeError: 'NoneType' object has no attribute '_get_by_time_scale'" (in component.py LINE 3276) This may be due to a problem with context setting, since the error is because the modulation Parameter of the ControlProjection is returning "None" rather than "multiplicative_param" (when called with get(context)), whereas "multiplicative_param" is returned with a call to get() (i.e., with no context specified) - Most of test_partially_overlapping_control_specs is passed if changes marked "11/21/21 NEW" in optimizationcontrolmechanism.py (LINE 1390) are implemented, but it does not properly route ControlProjections through parameter_CIMS (see last assert in test). Furthermore, test_parameterestimationcompsition fails with the mod param error, even though the model has similar structure (i.e., outer composition -- in this case a ParameterEstimationComposition) with an OCM that is given control specs that overlap with ones in a nested composition. - There are also several other things in composition I found puzzling and tried modifying, but that cuased failures: - _get_control_signals_for_composition(): - seems "if node.controller" should be "if **not** node.controller" (emphasis added just for comment) - "append" should be "extend" - _instantiate_control_projection(): - call to self.controller._activate_projections_for_composition (at end of method) should not be indented * - small mods; don't impact anything relevant to prior commit message * - small mods; don't impact anything relevant to prior commit message * - small mods; don't impact anything relevant to prior commit message * - finished adding formatting regions to composition.py * - * • composition.py: - rename _check_projection_initialization_status -> _check_controller_initialization_status - add _check_nodes_initialization_status(context=context) (and calls it with _check_controller_initialization_status) * • show_graph.py: addressed bug associated with ocm.allow_direct_probe * • show_graph.py: addressed bug associated with ocm.allow_direct_probe * - * Composition: add_controller: set METHOD as context source early * - * • composition.py retore append of control_signals in _instantiate_control_projections() * • composition.py restore append of control_signals in _instantiate_control_projections() • test_composition.py: add test_partially_overlapping_local_and_control_mech_control_specs_in_unnested_and_nested_comp * • test_partially_overlapping_local_and_control_mech_control_specs_in_unnested_and_nested_comp(): - added clear_registry() to allow names to be reused in both runs of test * • composition.py docstring: added projections entry to list of attributes - add_controller: added call to _add_node_aux_components() for controller * • composition.py _add_node_aux_components(): added deletion of item from aux_components if instantiated * • composition.py - comment out _add_node_aux_components() (causing new failures) - move _instantiate_control_projections to be with _instantiate_control_projections, after self.add_node(self.controller.objective_mechanism (to be more orderly) * - * - confirm that it passes all tests exception test_composition/test_partially_overlapping... (with addition of _add_aux_components in add_controller commented out) * • composition.py: some more fixed to add_controller that now fail only one test: - test_agent_rep_assignement_as_controller_and_replacement * • Passes *all* current tests * • composition.py: - add_controller: few more minor mods; still passes all tests * - * - * - * • controlmechanism.py: - __init__: resrict specification to only one of control, modulatory_signals, or control_signals (synonyms) * - * • composition.py: in progress fix of bug in instantiating shadow projections for ocm.state_input_ports * • composition.py: - _get_original_senders(): added support for nested composition needs to be checked for more than one level needs to be refactored to be recursive * • optimizationcontrolmechanism.py - _update_state_input_ports_for_controller: fix invalid_state_features to allow input_CIM of nested comp in agent_rep * - * • composition.py - _get_original_senders: made recursive * • test_show_graph.py: update for fixes * - * • tests: passes all in test_show_graph.py and test_report.py * Passes all tests * - comment clean-up * • composition.py - add_controller and _get_nested_node_CIM_port: added support for forced assignment of NodeRole.OUTPUT for nodes specified in OCM.monitor_for_control, but referenced 'allow_probes' attribute still needs to be implemented * • composition.py, optimizationcontrolmechanism.py: allow_probes fully implemented * • show_graph.py: fixed bug causing extra projections to OCM * • composition.py: - _update_shadow_projections(): fix handling of deep nesting * • optimizationcontrolmechanism.py: add agent_rep_type property * • optimizationcontrolmechanism.py: - state_feature_function -> state_feature_functions * • optimizationcontrolmechanism.py: - _validate_params: validate state_feature_functions - _update_state_input_ports_for_controller: implement assignment of state_feature_functions * - * - * • Passes all tests except test_json with 'model_with_control' * - * • composition.py - add_projection: delete instantiation of shadow projections (handled by _update_shadow_projections) * • composition.py - add_projection: delete instantiation of shadow projections (handled by _update_shadow_projections) - remove calls to _update_shadows_dict * • composition.py - add_projection: delete instantiation of shadow projections (handled by _update_shadow_projections) - remove calls to _update_shadows_dict * - * • test_two_origins_two_input_ports: crashes on failure of C->B to update * - * • composition.py - added property shadowing_dict that has shadowing ports as keys and the ports they shadow as values - refactored _update_shadowing_projections to use shadowing_dict * • optimizationcontrolmechanism.py - _update_state_input_ports: modified validations for nested nodes; still failing some tests * • optimizationcontrolmechanism.py - _update_state_input_ports: more careful and informative validation that state_input_ports are in comp or nested comp and are INPUT nodes thereof; passes all tests except test_two_origins_two_input_ports as before * • composition.py _get_invalid_aux_components(): defer all shadow projections until _update_shadow_projections * • composition.py _get_invalid_aux_components(): bug fix in test for shadow projections * Port: _remove_projection_to_port: don't reduce variable below length 1 even ports with no incoming projections have variable at least length 1 * • composition.py add_node(): marked (but haven't removed) code block instantiating shadow_projections that seems now to be redundant with _update_shadow_projection * • show_graph.py - _assign_cim_components: supress showing projections not in composition * • composition.py: _analyze_graph(): add extra call to _determine_node_roles after _update_shadow_projections _run(): moved block of code at beginning initializing scheduler to after _complete_init_of_partially_initialized_nodes and _analyze_graph() • show_graph.py - add test to all loops on projections: "if proj in composition.projection" * • show_graph.py - add show_projections_not_in_composition option for debugging * • composition.py _update_shadow_projections(): delete unused shadow projections and corresponding ports * • composition.py _update_shadow_projections(): fix bug in deletion of unused shadow projections and ports • test_show_graph: tests failing, need mods to accomodate changes * • composition.py: _analyze_graph(): add extra call to _determine_node_roles after _update_shadow_projections _run(): moved block of code at beginning initializing scheduler to after _complete_init_of_partially_initialized_nodes and _analyze_graph() • show_graph.py - add test to all loops on projections: "if proj in composition.projection" * • show_graph.py fixes; now passes all show_graph tests * - * • composition.py _update_shadow_projections: raise error for attempt to shadow INTERNAL Node of nested comp * - * - * • test_composition.py implemented test_shadow_nested_nodes that tests shadowing of nested nodes * - * - * - * - * • optimizationcontrolmechanism.py: docstring mods * • composition.py: - add allow_probes and exclude_probes_from_output * • composition.py: - docstring mods re: allow_probes • optimizationcontrolmechanism.py: - allow_probes: eliminate DIRECT setting - remove _parse_monitor_for_control_input_ports (no longer needed without allow_probes=DIRECT) * • composition.py: - change "exclude_probes_from_output" -> "include_probes_in_output" * • composition.py: - docstring mods re: allow_probes and include_probes_in_output * • composition.py: - docstring mods re: allow_probes and include_probes_in_output * • controlmechanism.py: - add allow_probes handling (moved from OCM) • optimizationcontrolmechanism.py: - move allow_probes to controlmechanism.py • composition.py: - refactor handling of allow_probes to permit for any ControlMechanism • objectivemechanism.py: - add modulatory_mechanism attribute * • controlmechanism.py: - add allow_probes handling (moved from OCM) • optimizationcontrolmechanism.py: - move allow_probes to controlmechanism.py • composition.py: - refactor handling of allow_probes to permit for any ControlMechanism - add _handle_allow_probes_for_control() to reconcile setting on Composition and ControlMechanism • objectivemechanism.py: - add modulatory_mechanism attribute * • composition.py add assignment of learning_mechanism to objective_mechanism.modulatory_mechanism for add_learning methods * • docstring mods * - * - * • optimizationcontrolmechanism.py: docstring revs * - * - * • test_composition.py: - add test_unnested_PROBE - add test_nested_PROBES TBD: test include_probes_in_output * - * • composition.py - add_node(): support tuple with required_role * - * • composition.py: - _determine_node_roles: fix bug in which nested comp was prevented from being an OUTPUT Node if, in addition to Nodes that qualifed as OUTPUT, it also had nodes that projected to Nodes in an outer comp (making it look like it was INTERNAL) * - * • composition.py: - add_node(): enforce include_probes_in_output = True for nested Compositions - execute(): - replace return of output_value with get_output_value() * - * • CompositionInterfaceMechanism.rst: - correct path ref • compositioninterfacemechanism.py: - docstring fixes * • optimizationcontrolmechanism.py: - docstring edits * - * - * • controlmechanism.py: allow CONTROL as alias for PROJECTIONS in ControlSignal spec * • controlsignal.py: allow CONTROL as alias for PROJECTIONS in ControlSignal spec * • controlsignal.py: replaces modulates with control as arg (though still allow modulates) * • test_projection_specifications.py: - add test_control_signal_projections_arg * - * • gatingsignal.py: - __init__: replace modulates arg with gate * - * - * - * - * • CONTROL and GATE in place of PROJECTIONS Co-authored-by: jdcpni Co-authored-by: Jan Vesely Co-authored-by: Katherine Mantel --- .../modulatory/control/controlmechanism.py | 34 +++-- .../control/gating/gatingmechanism.py | 66 ++++++++- .../control/optimizationcontrolmechanism.py | 30 ++-- .../ports/modulatorysignals/controlsignal.py | 93 +++++++++---- .../ports/modulatorysignals/gatingsignal.py | 53 +++++++- .../modulatorysignals/modulatorysignal.py | 2 +- psyneulink/core/components/ports/port.py | 19 ++- psyneulink/core/globals/parameters.py | 13 +- tests/composition/test_composition.py | 11 +- tests/composition/test_control.py | 36 ++--- tests/mechanisms/test_control_mechanism.py | 5 +- tests/naming/test_naming.py | 7 +- .../test_projection_specifications.py | 128 +++++++++++++++--- 13 files changed, 375 insertions(+), 122 deletions(-) diff --git a/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py b/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py index f18b3c95132..27743dabdea 100644 --- a/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py +++ b/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py @@ -1210,6 +1210,9 @@ 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 @@ -1231,19 +1234,28 @@ def is_2tuple(o): MECHANISM: output_ports[i][1] } # handle dict of form {PROJECTIONS: <2 item tuple>, : , ...} - elif ( - isinstance(output_ports[i], dict) - and 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 + 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 return output_ports + # MODIFIED 1/2/22 END def _validate_input_ports(self, input_ports): if input_ports is None: diff --git a/psyneulink/core/components/mechanisms/modulatory/control/gating/gatingmechanism.py b/psyneulink/core/components/mechanisms/modulatory/control/gating/gatingmechanism.py index dbf03790b30..ed919e5bb67 100644 --- a/psyneulink/core/components/mechanisms/modulatory/control/gating/gatingmechanism.py +++ b/psyneulink/core/components/mechanisms/modulatory/control/gating/gatingmechanism.py @@ -187,7 +187,7 @@ from psyneulink.core.components.ports.modulatorysignals.gatingsignal import GatingSignal from psyneulink.core.globals.defaults import defaultGatingAllocation from psyneulink.core.globals.keywords import \ - GATING, GATING_PROJECTION, GATING_SIGNAL, GATING_SIGNALS, \ + GATE, GATING, GATING_PROJECTION, GATING_SIGNAL, GATING_SIGNALS, \ INIT_EXECUTE_METHOD_ONLY, MONITOR_FOR_CONTROL, PROJECTION_TYPE from psyneulink.core.globals.parameters import Parameter from psyneulink.core.globals.preferences.basepreferenceset import is_pref_set @@ -399,6 +399,13 @@ class Parameters(ControlMechanism.Parameters): :default value: numpy.array([0.5]) :type: ``numpy.ndarray`` + output_ports + see `output_ports ` + + :default value: None + :type: + :read only: True + gating_allocation see `gating_allocation ` @@ -413,6 +420,63 @@ class Parameters(ControlMechanism.Parameters): pnl_internal=True ) + output_ports = Parameter( + None, + stateful=False, + loggable=False, + read_only=True, + structural=True, + parse_spec=True, + aliases=['control', 'control_signals', 'gate', 'gating_signal'], + 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>, : , ...} + 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, diff --git a/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py b/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py index 5c654b8b5b4..59a0b533cbc 100644 --- a/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py +++ b/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.py @@ -582,20 +582,16 @@ which is used to compute the `net_outcome ` of executing its `agent_rep `. -COMMENT: - FIX: 1/1/22 .. note:: - , and the `allow_probes - ` attribute of the Composition for which the OptimizationControlMechanism is the - `controller `. The latter allows the values of the items listed in `monitor_for_control - ` to be `INPUT ` or `INTERNAL ` `Nodes - ` of a `nested Composition ` to be monitored and included in the computation - of `outcome ` (ordinarily, those must be `OUTPUT ` Nodes of a nested - Composition). This can be thought of as providing access to "latent variables" of the Composition being evaluated; - that is, ones that do not contribute directly to the Composition's `results `. This - applies both to items that are monitored directly by the OptimizationControlMechanism or via its ObjectiveMechanism. -COMMENT - + If a `Node ` other than an `OUTPUT ` of a `nested ` + Composition is `specified to be monitored `, it is assigned as a `PROBE + ` of that nested Composition. Although `PROBE ` Nodes are generally treated + like `OUTPUT ` Nodes (since they project out of the Composition to which they belong), their + `value ` is not included in the `output_values ` or `results + ` attributes of the Composition for which the OptimizationControlMechanism is the + `controller `, unless that Composition's `include_probes_in_output + ` attribute is set to True (see `Composition_Probes` for additional + information). .. _OptimizationControlMechanism_Function: @@ -2322,14 +2318,6 @@ def _gen_llvm_output_port_parse_variable(self, ctx, builder, params, context, va builder.store(builder.load(val_ptr), dest_ptr) return oport_input - # Deprecated - this is now a Parameter - # @property - # def state_feature_values(self): - # if hasattr(self.agent_rep, 'model_based_optimizer') and self.agent_rep.model_based_optimizer is self: - # return self.agent_rep._get_predicted_input() - # else: - # return np.array(np.array(self.variable[1:]).tolist()) - @property def agent_rep_type(self): from psyneulink.core.compositions.compositionfunctionapproximator import CompositionFunctionApproximator diff --git a/psyneulink/core/components/ports/modulatorysignals/controlsignal.py b/psyneulink/core/components/ports/modulatorysignals/controlsignal.py index 5bc8b48516a..a2ed0953f72 100644 --- a/psyneulink/core/components/ports/modulatorysignals/controlsignal.py +++ b/psyneulink/core/components/ports/modulatorysignals/controlsignal.py @@ -64,7 +64,7 @@ `, the parameter(s) to be controlled must be specified. If other attributes of the ControlSignal need to be specified (e.g., one or more of its `cost functions `), then the Constructor for the ControlSignal can be used or a `port specification dictionary `, in which the parameter(s) to be -controlled in the **projections** argument or *PROJECTIONS* entry, respectively, using any of the forms below. +controlled are specified in the **control** argument or *CONTROL* entry, respectively, using any of the forms below. For convenience, the parameters can also be specified on their own in the **control_signals** argument of the ControlMechanism's constructor, in which case a default ControlSignal will be created for each. In all cases, any of the following can be use to specify the parameter(s) to be controlled: @@ -114,10 +114,10 @@ ~~~~~~~~~~~~~ When a ControlSignal is created, it can be assigned one or more `ControlProjections `, using either -the **projections** argument of its constructor, or in an entry of a dictionary assigned to the **params** argument -with the key *PROJECTIONS*. These will be assigned to its `efferents ` attribute. See +the **control** argument of its constructor, or in an entry of a dictionary assigned to the **params** argument +with the key *CONTROL*. These are assigned to its `efferents ` attribute. See `Port Projections ` for additional details concerning the specification of Projections when -creating a Port. +creating a Port, including `examples ` of ControlProjection specification. .. note:: Although a ControlSignal can be assigned more than one `ControlProjection`, all of those Projections will receive @@ -347,7 +347,7 @@ >>> from psyneulink import * >>> mech = ProcessingMechanism(name='my_mech') >>> ctl_mech_A = ControlMechanism(monitor_for_control=mech, - ... control_signals=ControlSignal(modulates=(INTERCEPT,mech), + ... control_signals=ControlSignal(control=(INTERCEPT,mech), ... cost_options=CostFunctions.INTENSITY)) >>> ctl_mech_B = ControlMechanism(monitor_for_control=mech, ... control_signals=ControlSignal(modulates=ctl_mech_A.control_signals[0], @@ -398,26 +398,28 @@ """ +import warnings + import numpy as np import typecheck as tc -import warnings # FIX: EVCControlMechanism IS IMPORTED HERE TO DEAL WITH COST FUNCTIONS THAT ARE DEFINED IN EVCControlMechanism # SHOULD THEY BE LIMITED TO EVC?? from psyneulink.core import llvm as pnlvm -from psyneulink.core.components.functions.nonstateful.combinationfunctions import Reduce from psyneulink.core.components.functions.function import is_function_type +from psyneulink.core.components.functions.nonstateful.combinationfunctions import Reduce +from psyneulink.core.components.functions.nonstateful.transferfunctions import Exponential, Linear, CostFunctions, \ + TransferWithCosts from psyneulink.core.components.functions.stateful.integratorfunctions import SimpleIntegrator -from psyneulink.core.components.functions.nonstateful.transferfunctions import Exponential, Linear, CostFunctions, TransferWithCosts from psyneulink.core.components.ports.modulatorysignals.modulatorysignal import ModulatorySignal from psyneulink.core.components.ports.outputport import _output_port_variable_getter from psyneulink.core.globals.context import ContextFlags from psyneulink.core.globals.defaults import defaultControlAllocation from psyneulink.core.globals.keywords import \ - ALLOCATION_SAMPLES, CONTROL_PROJECTION, CONTROL_SIGNAL, \ - INPUT_PORT, INPUT_PORTS, \ + ALLOCATION_SAMPLES, CONTROL, CONTROL_PROJECTION, CONTROL_SIGNAL, \ + INPUT_PORT, INPUT_PORTS, MODULATES, \ OUTPUT_PORT, OUTPUT_PORTS, OUTPUT_PORT_PARAMS, \ - PARAMETER_PORT, PARAMETER_PORTS, \ + PARAMETER_PORT, PARAMETER_PORTS, PROJECTIONS, \ RECEIVER, FUNCTION from psyneulink.core.globals.parameters import FunctionParameter, Parameter, get_validator_by_function from psyneulink.core.globals.preferences.basepreferenceset import is_pref_set @@ -457,7 +459,7 @@ class ControlSignal(ModulatorySignal): duration_cost_function=IntegratorFunction, \ combine_costs_function=Reduce(operation=SUM), \ allocation_samples=self.class_defaults.allocation_samples, \ - modulates=None, \ + control=None, \ projections=None) A subclass of `ModulatorySignal ` used by a `ControlMechanism ` to @@ -499,7 +501,7 @@ class ControlSignal(ModulatorySignal): specifies the values used by the ControlSignal's `owner ` to determine its `control_allocation ` (see `ControlSignal_Execution`). - modulates : list of Projection specifications + control : list of Projection specifications specifies the `ControlProjection(s) ` to be assigned to the ControlSignal, and that will be listed in its `efferents ` attribute (see `ControlSignal_Projections` for additional details). @@ -767,11 +769,11 @@ def _validate_allocation_samples(self, allocation_samples): pass portAttributes = ModulatorySignal.portAttributes | {ALLOCATION_SAMPLES, - COST_OPTIONS, - INTENSITY_COST_FUNCTION, - ADJUSTMENT_COST_FUNCTION, - DURATION_COST_FUNCTION, - COMBINE_COSTS_FUNCTION} + 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] @@ -802,7 +804,7 @@ def __init__(self, combine_costs_function:tc.optional(is_function_type)=None, allocation_samples=None, modulation:tc.optional(str)=None, - modulates=None, + control=None, params=None, name=None, prefs:is_pref_set=None, @@ -810,14 +812,36 @@ def __init__(self, try: if kwargs[FUNCTION] is not None: - raise TypeError( - f'{self.__class__.__name__} automatically creates a ' - 'TransferWithCosts function, and does not accept override. ' - 'TransferWithCosts uses the transfer_function parameter.' - ) + raise TypeError(f'{self.__class__.__name__} automatically creates a ' + 'TransferWithCosts function, and does not accept override. ' + 'TransferWithCosts uses the transfer_function parameter.') except KeyError: pass + # Deal with **modulates** if specified + if MODULATES in kwargs: + # Don't allow **control** and **modulates** to both be specified + if control: + raise ControlSignalError(f"Both 'control' and '{MODULATES}' arguments are specified in the " + f"constructor for '{name if name else self.__class__.__name__}; " + f"Should use just 'control'.") + # warnings.warn(f"The '{MODULATES}' argument (specified in the constructor for " + # f"'{name if name else self.__class__.__name__}') has been deprecated; " + # f"should use '{'control'}' going forward.") + + if PROJECTIONS in kwargs: + raise ControlSignalError(f"Both '{MODULATES}' and '{PROJECTIONS}' arguments are specified " + f"in the constructor for '{name if name else self.__class__.__name__}; " + f"Should use just '{PROJECTIONS}' (or 'control') ") + control = kwargs.pop(MODULATES) + + elif PROJECTIONS in kwargs: + # Don't allow **control** and **modulates** to both be specified + if control: + raise ControlSignalError(f"Both 'control' and '{PROJECTIONS}' arguments are specified " + f"in the constructor for '{name if name else self.__class__.__name__}; " + f"Must use just one or the other.") + # This is included in case ControlSignal was created by another Component (such as ControlProjection) # that specified ALLOCATION_SAMPLES in params if params and ALLOCATION_SAMPLES in params and params[ALLOCATION_SAMPLES] is not None: @@ -836,7 +860,7 @@ def __init__(self, size=size, transfer_function=transfer_function, modulation=modulation, - modulates=modulates, + modulates=control, cost_options=cost_options, intensity_cost_function=intensity_cost_function, adjustment_cost_function=adjustment_cost_function, @@ -1012,17 +1036,34 @@ def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec): """ from psyneulink.core.components.projections.projection import _parse_connection_specs - from psyneulink.core.globals.keywords import PROJECTIONS params_dict = {} port_spec = port_specific_spec 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 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 + 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 params_dict[PROJECTIONS] = _parse_connection_specs(connectee_port_type=self, owner=owner, connections=port_specific_spec) diff --git a/psyneulink/core/components/ports/modulatorysignals/gatingsignal.py b/psyneulink/core/components/ports/modulatorysignals/gatingsignal.py index 8cba981e069..ee47bb3bf11 100644 --- a/psyneulink/core/components/ports/modulatorysignals/gatingsignal.py +++ b/psyneulink/core/components/ports/modulatorysignals/gatingsignal.py @@ -251,7 +251,7 @@ from psyneulink.core.globals.defaults import defaultGatingAllocation from psyneulink.core.globals.keywords import \ GATE, GATING_PROJECTION, GATING_SIGNAL, INPUT_PORT, INPUT_PORTS, \ - OUTPUT_PORT, OUTPUT_PORTS, OUTPUT_PORT_PARAMS, PROJECTIONS, RECEIVER + MODULATES, OUTPUT_PORT, OUTPUT_PORTS, OUTPUT_PORT_PARAMS, PROJECTIONS, RECEIVER 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 @@ -297,6 +297,11 @@ class GatingSignal(ControlSignal): default_allocation : scalar, list or np.ndarray : defaultGatingAllocation specifies the template and default value used for `allocation `. + gate : list of Projection specifications + specifies the `GatingProjection(s) ` to be assigned to the GatingSignal, and that will be + listed in its `efferents ` attribute (see `GatingSignal_Projections` for additional + details). + function : Function or method : default Linear specifies the function used to determine the value of the GatingSignal from the value of its `owner `. @@ -419,7 +424,7 @@ def __init__(self, size=None, transfer_function=None, modulation:tc.optional(str)=None, - modulates=None, + gate=None, params=None, name=None, prefs:is_pref_set=None, @@ -430,13 +435,39 @@ def __init__(self, # Consider adding self to owner.output_ports here (and removing from GatingProjection._instantiate_sender) # (test for it, and create if necessary, as per OutputPorts in GatingProjection._instantiate_sender), + + # Deal with **modulates** if specified + if MODULATES in kwargs: + # Don't allow **control** and **modulates** to both be specified + if gate: + raise GatingSignalError(f"Both 'gate' and '{MODULATES}' arguments are specified in the " + f"constructor for '{name if name else self.__class__.__name__}; " + f"Should use just 'gate'.") + # warnings.warn(f"The '{MODULATES}' argument (specified in the constructor for " + # f"'{name if name else self.__class__.__name__}') has been deprecated; " + # f"should use '{'control'}' going forward.") + + if PROJECTIONS in kwargs: + raise GatingSignalError(f"Both '{MODULATES}' and '{PROJECTIONS}' arguments are specified " + f"in the constructor for '{name if name else self.__class__.__name__}; " + f"Should use just '{PROJECTIONS}' (or 'gate') ") + gate = kwargs.pop(MODULATES) + + elif PROJECTIONS in kwargs: + # Don't allow **control** and **modulates** to both be specified + if gate: + raise GatingSignalError(f"Both 'gate' and '{PROJECTIONS}' arguments are specified " + f"in the constructor for '{name if name else self.__class__.__name__}; " + f"Must use just one or the other.") + + # Validate sender (as variable) and params super().__init__(owner=owner, reference_value=reference_value, default_allocation=default_allocation, size=size, modulation=modulation, - modulates=modulates, + control=gate, params=params, name=name, prefs=prefs, @@ -458,11 +489,27 @@ def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec): 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) diff --git a/psyneulink/core/components/ports/modulatorysignals/modulatorysignal.py b/psyneulink/core/components/ports/modulatorysignals/modulatorysignal.py index 697ee0ef062..deb1e474258 100644 --- a/psyneulink/core/components/ports/modulatorysignals/modulatorysignal.py +++ b/psyneulink/core/components/ports/modulatorysignals/modulatorysignal.py @@ -410,7 +410,7 @@ from psyneulink.core.globals.context import ContextFlags from psyneulink.core.globals.defaults import defaultModulatoryAllocation from psyneulink.core.globals.keywords import \ - ADDITIVE_PARAM, DISABLE, MAYBE, MECHANISM, MODULATION, MODULATORY_SIGNAL, MULTIPLICATIVE_PARAM, \ + ADDITIVE_PARAM, CONTROL, DISABLE, MAYBE, MECHANISM, MODULATION, MODULATORY_SIGNAL, MULTIPLICATIVE_PARAM, \ OVERRIDE, PROJECTIONS, VARIABLE from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel diff --git a/psyneulink/core/components/ports/port.py b/psyneulink/core/components/ports/port.py index 811bcb49c77..c481dda8d3c 100644 --- a/psyneulink/core/components/ports/port.py +++ b/psyneulink/core/components/ports/port.py @@ -584,20 +584,23 @@ print(control_signal.name) for control_projection in control_signal.efferents: print("\t{}: {}".format(control_projection.receiver.owner.name, control_projection.receiver)) - > MY DDM DRIFT RATE AND THREHOLD CONTROL SIGNAL + > MY DDM DRIFT RATE AND THRESHOLD CONTROL SIGNAL > MY DDM: (ParameterPort drift_rate) > MY DDM: (ParameterPort threshold) Note that a ControlMechanism uses a **control_signals** argument in place of an **output_ports** argument (since it -uses `ControlSignal ` for its `OutputPorts `. In the example above, -both ControlProjections are assigned to a single ControlSignal. However, they could each be assigned to their own by -specifying them in separate itesm of the **control_signals** argument:: +uses `ControlSignal ` for its `OutputPorts `. Note also that, for specifying Projections +of a ControlSignal (i.e., its ControlProjections), the keyword *CONTROL* can be used in place of the more generic +*PROJECTIONS* keyword (as shown in the example below). + +In the example above, both ControlProjections are assigned to a single ControlSignal. However, they could each be +assigned to their own by specifying them in separate items of the **control_signals** argument:: my_mech = pnl.DDM(name='MY DDM') my_ctl_mech = pnl.ControlMechanism(control_signals=[{pnl.NAME: 'DRIFT RATE CONTROL SIGNAL', - pnl.PROJECTIONS: [my_mech.parameter_ports[pnl.DRIFT_RATE]]}, + pnl.CONTROL: [my_mech.parameter_ports[pnl.DRIFT_RATE]]}, {pnl.NAME: 'THRESHOLD RATE CONTROL SIGNAL', - pnl.PROJECTIONS: [my_mech.parameter_ports[pnl.THRESHOLD]]}]) + pnl.CONTROL: [my_mech.parameter_ports[pnl.THRESHOLD]]}]) # Print ControlSignals and their ControlProjections... > DRIFT RATE CONTROL SIGNAL > MY DDM: (ParameterPort drift_rate) @@ -785,7 +788,7 @@ def test_multiple_modulatory_projections_with_mech_and_port_Name_specs(self): from psyneulink.core.globals.context import ContextFlags, handle_external_context from psyneulink.core.globals.keywords import \ ADDITIVE, ADDITIVE_PARAM, AUTO_ASSIGN_MATRIX, \ - CONTEXT, CONTROL_PROJECTION_PARAMS, CONTROL_SIGNAL_SPECS, DEFERRED_INITIALIZATION, DISABLE, EXPONENT, \ + CONTEXT, CONTROL, CONTROL_PROJECTION_PARAMS, CONTROL_SIGNAL_SPECS, DEFERRED_INITIALIZATION, DISABLE, EXPONENT, \ FUNCTION, FUNCTION_PARAMS, GATING_PROJECTION_PARAMS, GATING_SIGNAL_SPECS, INPUT_PORTS, \ LEARNING_PROJECTION_PARAMS, LEARNING_SIGNAL_SPECS, \ MATRIX, MECHANISM, MODULATORY_PROJECTION, MODULATORY_PROJECTIONS, MODULATORY_SIGNAL, \ @@ -1020,6 +1023,7 @@ def __init__(self, This is used by subclasses to implement the InputPort(s), OutputPort(s), and ParameterPort(s) of a Mechanism. + COMMENT: [OLD] Arguments: - owner (Mechanism): Mechanism with which Port is associated (default: NotImplemented) @@ -1050,6 +1054,7 @@ def __init__(self, NOTES: * these are used for dictionary specification of a Port in param declarations * they take precedence over arguments specified directly in the call to __init__() + COMMENT """ if kwargs: try: diff --git a/psyneulink/core/globals/parameters.py b/psyneulink/core/globals/parameters.py index feabfa83a85..27ffe9efcf0 100644 --- a/psyneulink/core/globals/parameters.py +++ b/psyneulink/core/globals/parameters.py @@ -102,8 +102,10 @@ class Parameters(A.Parameters): - an instance of *B*.Parameters will be assigned to the parameters attribute of the class *B* and all instances of *B* - each attribute on *B*.Parameters becomes a parameter (instance of the Parameter class) - as with *p*, specifying only a value uses default values for the attributes of the Parameter - - as with *q*, specifying an explicit instance of the Parameter class allows you to modify the `Parameter attributes ` -- if you want assignments to parameter *p* to be validated, add a method _validate_p(value), that returns None if value is a valid assignment, or an error string if value is not a valid assignment + - as with *q*, specifying an explicit instance of the Parameter class allows you to modify the + `Parameter attributes ` +- if you want assignments to parameter *p* to be validated, add a method _validate_p(value), + that returns None if value is a valid assignment, or an error string if value is not a valid assignment - if you want all values set to *p* to be parsed beforehand, add a method _parse_p(value) that returns the parsed value - for example, convert to a numpy array or float @@ -291,17 +293,18 @@ def _recurrent_transfer_mechanism_matrix_setter(value, owning_component=None, co import copy import itertools import logging -import toposort import types import typing import weakref +import toposort -from psyneulink.core.rpc.graph_pb2 import Entry, ndArray from psyneulink.core.globals.context import Context, ContextError, ContextFlags, _get_time, handle_external_context from psyneulink.core.globals.context import time as time_object from psyneulink.core.globals.log import LogCondition, LogEntry, LogError -from psyneulink.core.globals.utilities import call_with_pruned_args, copy_iterable_with_shared, get_alias_property_getter, get_alias_property_setter, get_deepcopy_with_shared, unproxy_weakproxy, create_union_set +from psyneulink.core.globals.utilities import call_with_pruned_args, copy_iterable_with_shared, \ + get_alias_property_getter, get_alias_property_setter, get_deepcopy_with_shared, unproxy_weakproxy, create_union_set +from psyneulink.core.rpc.graph_pb2 import Entry, ndArray __all__ = [ 'Defaults', 'get_validator_by_function', 'Parameter', 'ParameterAlias', 'ParameterError', diff --git a/tests/composition/test_composition.py b/tests/composition/test_composition.py index fe6aec55741..8161ab97cac 100644 --- a/tests/composition/test_composition.py +++ b/tests/composition/test_composition.py @@ -31,7 +31,7 @@ from psyneulink.core.globals.context import Context from psyneulink.core.globals.keywords import \ ADDITIVE, ALLOCATION_SAMPLES, BEFORE, DEFAULT, DISABLE, INPUT_PORT, INTERCEPT, LEARNING_MECHANISMS, \ - LEARNED_PROJECTIONS, RANDOM_CONNECTIVITY_MATRIX, \ + LEARNED_PROJECTIONS, RANDOM_CONNECTIVITY_MATRIX, CONTROL, \ NAME, PROJECTIONS, RESULT, OBJECTIVE_MECHANISM, OUTPUT_MECHANISM, OVERRIDE, SLOPE, TARGET_MECHANISM, VARIANCE from psyneulink.core.scheduling.condition import AtTimeStep, AtTrial, Never, TimeInterval from psyneulink.core.scheduling.condition import EveryNCalls @@ -1264,7 +1264,8 @@ def test_composition_learning_pathway_dict_with_no_learning_fct_in_tuple_error(s class TestProperties: - def test_properties(self): + @pytest.mark.parametrize("control_spec", [CONTROL, PROJECTIONS]) + def test_properties(self, control_spec): Input = pnl.TransferMechanism(name='Input') Reward = pnl.TransferMechanism(output_ports=[pnl.RESULT, pnl.MEAN, pnl.VARIANCE], name='reward') @@ -1286,11 +1287,11 @@ def test_properties(self): Decision.output_ports[pnl.PROBABILITY_UPPER_THRESHOLD], Decision.output_ports[pnl.RESPONSE_TIME]], function=pnl.GridSearch(), - control_signals=[{PROJECTIONS: ("drift_rate", Decision), + control_signals=[{control_spec: ("drift_rate", Decision), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}, - {PROJECTIONS: ("threshold", Decision), + {control_spec: ("threshold", Decision), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}, - {PROJECTIONS: ("slope", Reward), + {control_spec: ("slope", Reward), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}])) assert len(comp.nodes) == len(comp.mechanisms) == 3 diff --git a/tests/composition/test_control.py b/tests/composition/test_control.py index 3df12dd9dd0..dc3713a3883 100644 --- a/tests/composition/test_control.py +++ b/tests/composition/test_control.py @@ -4,7 +4,7 @@ import pytest import psyneulink as pnl -from psyneulink.core.globals.keywords import ALLOCATION_SAMPLES, PROJECTIONS +from psyneulink.core.globals.keywords import ALLOCATION_SAMPLES, CONTROL, PROJECTIONS from psyneulink.core.globals.log import LogCondition from psyneulink.core.globals.sampleiterator import SampleIterator, SampleIteratorError, SampleSpec from psyneulink.core.globals.utilities import _SeededPhilox @@ -94,7 +94,8 @@ def test_redundant_control_spec_add_controller_in_comp_constructor_then_add_node assert ddm.parameter_ports['drift_rate'].mod_afferents[0].sender.owner == comp.controller assert comp.controller.control_signals[0].allocation_samples is None - def test_redundant_control_spec_add_controller_in_comp_constructor_then_add_node_with_alloc_samples_specified(self): + @pytest.mark.parametrize("control_spec", [CONTROL, PROJECTIONS]) + def test_redundant_control_spec_add_controller_in_comp_constructor_then_add_node_with_alloc_samples_specified(self,control_spec): # First create Composition with controller that has HAS control specification that includes allocation_samples, # then add Mechanism with control specification to Composition; # Control specification on controller should supercede one on Mechanism (which should be ignored) @@ -108,7 +109,7 @@ def test_redundant_control_spec_add_controller_in_comp_constructor_then_add_node "deactivated until 'DDM-0' is added to' Composition-0' in a compatible way." with pytest.warns(UserWarning, match=expected_warning): comp = pnl.Composition(controller=pnl.ControlMechanism(control_signals={ALLOCATION_SAMPLES:np.arange(0.2,1.01, 0.3), - PROJECTIONS:('drift_rate', ddm)})) + control_spec:('drift_rate', ddm)})) comp.add_node(ddm) assert comp.controller.control_signals[0].efferents[0].receiver == ddm.parameter_ports['drift_rate'] assert ddm.parameter_ports['drift_rate'].mod_afferents[0].sender.owner == comp.controller @@ -138,7 +139,8 @@ def test_objective_mechanism_spec_as_monitor_for_control_error(self): error_msg = error.value.error_value assert expected_error in error_msg - def test_deferred_init(self): + @pytest.mark.parametrize("control_spec", [CONTROL, PROJECTIONS]) + def test_deferred_init(self, control_spec): # Test to insure controller works the same regardless of whether it is added to a composition before or after # the nodes it connects to @@ -182,9 +184,9 @@ def test_deferred_init(self): Decision.output_ports[pnl.PROBABILITY_UPPER_THRESHOLD], (Decision.output_ports[pnl.RESPONSE_TIME], -1, 1)]), function=pnl.GridSearch(), - control_signals=[{PROJECTIONS: ("drift_rate", Decision), + control_signals=[{control_spec: ("drift_rate", Decision), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}, - {PROJECTIONS: ("threshold", Decision), + {control_spec: ("threshold", Decision), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}]) ) assert comp._controller_initialization_status == pnl.ContextFlags.DEFERRED_INIT @@ -263,7 +265,7 @@ def test_partial_deferred_init(self): pathways=[initial_node_a, initial_node_b], controller_mode=pnl.BEFORE) - member_node_control_signal = pnl.ControlSignal(projections=[(pnl.SLOPE, initial_node_a)], + member_node_control_signal = pnl.ControlSignal(control=[(pnl.SLOPE, initial_node_a)], variable=1.0, intensity_cost_function=pnl.Linear(slope=0.0), allocation_samples=pnl.SampleSpec(start=1.0, @@ -821,9 +823,9 @@ def test_lvoc(self): monitor=[m1, m2]), function=pnl.GridSearch(max_iterations=1), control_signals=[ - {PROJECTIONS: (pnl.SLOPE, m1), + {CONTROL: (pnl.SLOPE, m1), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}, - {PROJECTIONS: (pnl.SLOPE, m2), + {CONTROL: (pnl.SLOPE, m2), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}]) c.add_node(lvoc) input_dict = {m1: [[1], [1]], m2: [1]} @@ -845,9 +847,9 @@ def test_lvoc_both_predictors_specs(self): monitor=[m1, m2]), function=pnl.GridSearch(max_iterations=1), control_signals=[ - {PROJECTIONS: (pnl.SLOPE, m1), + {CONTROL: (pnl.SLOPE, m1), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}, - {PROJECTIONS: (pnl.SLOPE, m2), + {CONTROL: (pnl.SLOPE, m2), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}]) c.add_node(lvoc) input_dict = {m1: [[1], [1]], m2: [1]} @@ -1651,9 +1653,9 @@ def test_evc(self): Decision.output_ports[pnl.PROBABILITY_UPPER_THRESHOLD], (Decision.output_ports[pnl.RESPONSE_TIME], -1, 1)]), function=pnl.GridSearch(), - control_signals=[{PROJECTIONS: ("drift_rate", Decision), + control_signals=[{CONTROL: ("drift_rate", Decision), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}, - {PROJECTIONS: ("threshold", Decision), + {CONTROL: ("threshold", Decision), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3)}]) ) @@ -1940,11 +1942,11 @@ def test_laming_validation_specify_control_signals(self): function=pnl.GridSearch(), control_signals=[ { - PROJECTIONS: (pnl.DRIFT_RATE, Decision), + CONTROL: (pnl.DRIFT_RATE, Decision), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3) }, { - PROJECTIONS: (pnl.THRESHOLD, Decision), + CONTROL: (pnl.THRESHOLD, Decision), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3) } ], @@ -2078,11 +2080,11 @@ def test_stateful_mechanism_in_simulation(self): function=pnl.GridSearch(), control_signals=[ { - PROJECTIONS: (pnl.DRIFT_RATE, Decision), + CONTROL: (pnl.DRIFT_RATE, Decision), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3) }, { - PROJECTIONS: (pnl.THRESHOLD, Decision), + CONTROL: (pnl.THRESHOLD, Decision), ALLOCATION_SAMPLES: np.arange(0.1, 1.01, 0.3) } ], diff --git a/tests/mechanisms/test_control_mechanism.py b/tests/mechanisms/test_control_mechanism.py index 916dee927b7..f43fdaf04b4 100644 --- a/tests/mechanisms/test_control_mechanism.py +++ b/tests/mechanisms/test_control_mechanism.py @@ -257,11 +257,12 @@ def test_control_signal_default_allocation_specification(self): name='C3', default_variable=[10], default_allocation=[4], - control_signals=[pnl.ControlSignal(modulates=(pnl.SLOPE, m1)), # tests for assignment to default_allocation + # Test synonyms allowed for **control**: generic **modulates**, and even more generic **projections**): + control_signals=[pnl.ControlSignal(control=(pnl.SLOPE, m1)), # tests for assignment to default_allocation pnl.ControlSignal(default_allocation=5, # tests for override of default_allocation modulates=(pnl.SLOPE, m2)), pnl.ControlSignal(default_allocation=[6], # as above same but with array - modulates=(pnl.SLOPE, m3))]) + projections=(pnl.SLOPE, m3))]) comp = pnl.Composition() comp.add_nodes([m1,m2,m3]) comp.add_controller(c2) diff --git a/tests/naming/test_naming.py b/tests/naming/test_naming.py index fb44f6bc44b..2082b73c47e 100644 --- a/tests/naming/test_naming.py +++ b/tests/naming/test_naming.py @@ -155,7 +155,8 @@ def test_input_port_and_assigned_projection_names(self): # TEST 11 # Test that ControlSignals and ControlProjections are properly named - def test_control_signal_and_control_projection_names(self): + @pytest.mark.parametrize('control_spec', [pnl.CONTROL, pnl.PROJECTIONS]) + def test_control_signal_and_control_projection_names(self, control_spec): D1 = pnl.DDM(name='D1') D2 = pnl.DDM(name='D2') @@ -166,7 +167,7 @@ def test_control_signal_and_control_projection_names(self): assert C1.control_signals[0].efferents[0].name == 'ControlProjection for D1[drift_rate]' # ControlSignal with two ControlProjection to two parameters of same Mechanism - C2 = pnl.ControlMechanism(control_signals=[{pnl.PROJECTIONS:[D1.parameter_ports[ + C2 = pnl.ControlMechanism(control_signals=[{control_spec:[D1.parameter_ports[ psyneulink.core.components.functions.nonstateful.distributionfunctions.DRIFT_RATE], D1.parameter_ports[ psyneulink.core.globals.keywords.THRESHOLD]]}]) @@ -175,7 +176,7 @@ def test_control_signal_and_control_projection_names(self): assert C2.control_signals[0].efferents[1].name == 'ControlProjection for D1[threshold]' # ControlSignal with two ControlProjection to two parameters of different Mechanisms - C3 = pnl.ControlMechanism(control_signals=[{pnl.PROJECTIONS:[D1.parameter_ports[ + C3 = pnl.ControlMechanism(control_signals=[{control_spec:[D1.parameter_ports[ psyneulink.core.components.functions.nonstateful.distributionfunctions.DRIFT_RATE], D2.parameter_ports[ psyneulink.core.components.functions.nonstateful.distributionfunctions.DRIFT_RATE]]}]) diff --git a/tests/projections/test_projection_specifications.py b/tests/projections/test_projection_specifications.py index bd3dd78fb7b..52f39ff5ac0 100644 --- a/tests/projections/test_projection_specifications.py +++ b/tests/projections/test_projection_specifications.py @@ -1,16 +1,17 @@ -import psyneulink as pnl import numpy as np import pytest +import psyneulink as pnl import psyneulink.core.components.functions.nonstateful.distributionfunctions -import psyneulink.core.components.functions.stateful.integratorfunctions import psyneulink.core.components.functions.nonstateful.transferfunctions +import psyneulink.core.components.functions.stateful.integratorfunctions + class TestProjectionSpecificationFormats: def test_projection_specification_formats(self): """Test various matrix and Projection specifications - Also tests assignment of Projections to pathay of Composition using add_linear_processing_pathway: + Also tests assignment of Projections to pathway of Composition using add_linear_processing_pathway: - Projection explicitly specified in sequence (M1_M2_proj) - Projection pre-constructed and assigned to Mechanisms, but not specified in pathway(M2_M3_proj) - Projection specified in pathway that is duplicate one preconstructed and assigned to Mechanisms (M3_M4_proj) @@ -52,25 +53,112 @@ def test_projection_specification_formats(self): # assert np.allclose(c.results, [[-130.19166667, -152.53333333, -174.875]]) assert np.allclose(c.results, [[ -78.115, -91.52 , -104.925]]) - def test_multiple_modulatory_projection_specs(self): + @pytest.mark.parametrize('args', [ + (pnl.CONTROL, None), + (pnl.MODULATES, None), + (pnl.PROJECTIONS, None), + ('mod and ctl', '"Both \'control\' and \'modulates\' arguments are specified in ' + 'the constructor for \'ControlSignal; Should use just \'control\'."'), + ('proj and ctl', 'Both \'control\' and \'projections\' arguments are specified in the constructor for ' + '\'ControlSignal; Must use just one or the other.'), + ('proj and mod','"Both \'modulates\' and \'projections\' arguments are specified in the constructor for ' + '\'ControlSignal; Should use just \'projections\' (or \'control\') "') + ]) + def test_control_signal_projections_arg(self, args): + M = pnl.ProcessingMechanism() + control_specs = {pnl.CONTROL: {'control':(pnl.SLOPE, M)}, + pnl.MODULATES: {pnl.MODULATES:(pnl.SLOPE, M)}, + pnl.PROJECTIONS: {pnl.PROJECTIONS:(pnl.SLOPE, M)}, + 'mod and ctl': {'control':(pnl.SLOPE, M), + pnl.MODULATES:(pnl.SLOPE, M)}, + 'proj and ctl': {'control':(pnl.SLOPE, M), + pnl.PROJECTIONS:(pnl.SLOPE, M)}, + 'proj and mod': {pnl.MODULATES:(pnl.SLOPE, M), + pnl.PROJECTIONS:(pnl.SLOPE, M)} + } + if args[0] in {'mod and ctl', 'proj and ctl', 'proj and mod'}: + from psyneulink.core.components.ports.modulatorysignals.controlsignal import ControlSignalError + with pytest.raises(ControlSignalError) as err: + pnl.ControlSignal(**control_specs[args[0]]) + assert args[1] in str(err.value) + else: + ctl_sig = pnl.ControlSignal(**control_specs[args[0]]) + assert ctl_sig._init_args[pnl.PROJECTIONS][0][0] == pnl.SLOPE + assert ctl_sig._init_args[pnl.PROJECTIONS][0][1] is M + + @pytest.mark.parametrize('args', [ + (pnl.GATE, None), + (pnl.MODULATES, None), + (pnl.PROJECTIONS, None), + ('mod and gate', '"Both \'gate\' and \'modulates\' arguments are specified in the constructor for ' + '\'GatingSignal; Should use just \'gate\'."'), + ('proj and gate', 'Both \'gate\' and \'projections\' arguments are specified in the constructor for ' + '\'GatingSignal; Must use just one or the other.'), + ('proj and mod','"Both \'modulates\' and \'projections\' arguments are specified in the constructor for ' + '\'GatingSignal; Should use just \'projections\' (or \'gate\') "') + ]) + def test_gating_signal_projections_arg(self, args): + M = pnl.ProcessingMechanism() + gating_specs = {pnl.GATE: {'gate':(pnl.SLOPE, M)}, + pnl.MODULATES: {pnl.MODULATES:(pnl.SLOPE, M)}, + pnl.PROJECTIONS: {pnl.PROJECTIONS:(pnl.SLOPE, M)}, + 'mod and gate': {'gate':(pnl.SLOPE, M), + pnl.MODULATES:(pnl.SLOPE, M)}, + 'proj and gate': {'gate':(pnl.SLOPE, M), + pnl.PROJECTIONS:(pnl.SLOPE, M)}, + 'proj and mod': {pnl.MODULATES:(pnl.SLOPE, M), + pnl.PROJECTIONS:(pnl.SLOPE, M)} + } + if args[0] in {'mod and gate', 'proj and gate', 'proj and mod'}: + from psyneulink.core.components.ports.modulatorysignals.gatingsignal import GatingSignalError + with pytest.raises(GatingSignalError) as err: + pnl.GatingSignal(**gating_specs[args[0]]) + assert args[1] in str(err.value) + else: + gating_sig = pnl.GatingSignal(**gating_specs[args[0]]) + assert gating_sig._init_args[pnl.PROJECTIONS][0][0] == pnl.SLOPE + assert gating_sig._init_args[pnl.PROJECTIONS][0][1] is M + + @pytest.mark.parametrize("control_spec, gating_spec, extra_spec", + [ + [pnl.CONTROL, pnl.GATE, ''], + [pnl.PROJECTIONS, pnl.PROJECTIONS, ''], + [pnl.CONTROL, pnl.GATE, pnl.PROJECTIONS] + ] + ) + def test_multiple_modulatory_projection_specs(self, control_spec, gating_spec, extra_spec): M = pnl.DDM(name='MY DDM') - C = pnl.ControlMechanism(control_signals=[{pnl.PROJECTIONS: [M.parameter_ports[ - psyneulink.core.components.functions.nonstateful.distributionfunctions.DRIFT_RATE], - M.parameter_ports[ - psyneulink.core.globals.keywords.THRESHOLD]]}]) - G = pnl.GatingMechanism(gating_signals=[{pnl.PROJECTIONS: [M.output_ports[pnl.DECISION_VARIABLE], - M.output_ports[pnl.RESPONSE_TIME]]}]) - assert len(C.control_signals)==1 - assert len(C.control_signals[0].efferents)==2 - assert M.parameter_ports[ - psyneulink.core.components.functions.nonstateful.distributionfunctions.DRIFT_RATE].mod_afferents[0] == C.control_signals[0].efferents[0] - assert M.parameter_ports[ - psyneulink.core.globals.keywords.THRESHOLD].mod_afferents[0] == C.control_signals[0].efferents[1] - assert len(G.gating_signals)==1 - assert len(G.gating_signals[0].efferents)==2 - assert M.output_ports[pnl.DECISION_VARIABLE].mod_afferents[0]==G.gating_signals[0].efferents[0] - assert M.output_ports[pnl.RESPONSE_TIME].mod_afferents[0]==G.gating_signals[0].efferents[1] + ctl_sig_spec = {control_spec: [M.parameter_ports[pnl.DRIFT_RATE], + M.parameter_ports[pnl.THRESHOLD]]} + gating_sig_spec = {gating_spec: [M.output_ports[pnl.DECISION_VARIABLE], + M.output_ports[pnl.RESPONSE_TIME]]} + if extra_spec: + ctl_sig_spec.update({extra_spec:[M.parameter_ports[pnl.STARTING_POINT]]}) + gating_sig_spec.update({extra_spec:[M.output_ports[pnl.RESPONSE_TIME]]}) + ctl_err_msg = '"Both \'PROJECTIONS\' and \'CONTROL\' entries found in specification dict for ' \ + '\'ControlSignal\' of \'ControlMechanism-0\'. Must use only one or the other."' + with pytest.raises(pnl.ControlSignalError) as err: + pnl.ControlMechanism(control_signals=[ctl_sig_spec]) + assert ctl_err_msg == str(err.value) + gating_err_msg = '"Both \'PROJECTIONS\' and \'GATE\' entries found in specification dict for ' \ + '\'GatingSignal\' of \'GatingMechanism-0\'. Must use only one or the other."' + with pytest.raises(pnl.GatingSignalError) as err: + pnl.GatingMechanism(gating_signals=[gating_sig_spec]) + assert gating_err_msg == str(err.value) + else: + C = pnl.ControlMechanism(control_signals=[ctl_sig_spec]) + G = pnl.GatingMechanism(gating_signals=[gating_sig_spec]) + assert len(C.control_signals)==1 + assert len(C.control_signals[0].efferents)==2 + assert M.parameter_ports[ + psyneulink.core.components.functions.nonstateful.distributionfunctions.DRIFT_RATE].mod_afferents[0] == C.control_signals[0].efferents[0] + assert M.parameter_ports[ + psyneulink.core.globals.keywords.THRESHOLD].mod_afferents[0] == C.control_signals[0].efferents[1] + assert len(G.gating_signals)==1 + assert len(G.gating_signals[0].efferents)==2 + assert M.output_ports[pnl.DECISION_VARIABLE].mod_afferents[0]==G.gating_signals[0].efferents[0] + assert M.output_ports[pnl.RESPONSE_TIME].mod_afferents[0]==G.gating_signals[0].efferents[1] def test_multiple_modulatory_projections_with_port_Name(self):