• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PrincetonUniversity / PsyNeuLink / 15917088825

05 Jun 2025 04:18AM UTC coverage: 84.482% (+0.5%) from 84.017%
15917088825

push

github

web-flow
Merge pull request #3271 from PrincetonUniversity/devel

Devel

9909 of 12966 branches covered (76.42%)

Branch coverage included in aggregate %.

1708 of 1908 new or added lines in 54 files covered. (89.52%)

25 existing lines in 14 files now uncovered.

34484 of 39581 relevant lines covered (87.12%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.52
/psyneulink/core/components/mechanisms/modulatory/control/controlmechanism.py
1
# Princeton University licenses this file to You under the Apache License, Version 2.0 (the "License");
2
# you may not use this file except in compliance with the License.  You may obtain a copy of the License at:
3
#     http://www.apache.org/licenses/LICENSE-2.0
4
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
5
# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6
# See the License for the specific language governing permissions and limitations under the License.
7

8
# **************************************  ControlMechanism ************************************************
9

10
"""
11

12
Contents
13
--------
14

15
  * `ControlMechanism_Overview`
16
  * `ControlMechanism_Composition_Controller`
17
  * `ControlMechanism_Creation`
18
      - `ControlMechanism_Monitor_for_Control`
19
      - `ControlMechanism_ObjectiveMechanism`
20
      - `ControlMechanism_ControlSignals`
21
  * `ControlMechanism_Structure`
22
      - `ControlMechanism_Input`
23
      - `ControlMechanism_Function`
24
      - `ControlMechanism_Output`
25
      - `ControlMechanism_Costs_NetOutcome`
26
  * `ControlMechanism_Execution`
27
  * `ControlMechanism_Examples`
28
  * `ControlMechanism_Class_Reference`
29

30

31
.. _ControlMechanism_Overview:
32

33
Overview
34
--------
35

36
A ControlMechanism is a `ModulatoryMechanism` that `modulates the value(s) <ModulatorySignal_Modulation>` of one or
37
more `Ports <Port>` of other Mechanisms in the `Composition` to which it belongs. In general, a ControlMechanism is
38
used to modulate the `ParameterPort(s) <ParameterPort>` of one or more Mechanisms, that determine the value(s) of
39
the parameter(s) of the `function(s) <Mechanism_Base.function>` of those Mechanism(s). However, a ControlMechanism
40
can also be used to modulate the function of `InputPorts <InputPort>` and/or `OutputPort <OutputPorts>`,
41
much like a `GatingMechanism`.  A ControlMechanism's `function <ControlMechanism.function>` calculates a
42
`control_allocation <ControlMechanism.control_allocation>`: one or more values used as inputs for its `control_signals
43
<ControlMechanism.control_signals>`.  Its control_signals are `ControlSignal` OutputPorts that are used to modulate
44
the parameters of other Mechanisms' `function <Mechanism_Base.function>` (see `ControlSignal_Modulation` for a more
45
detailed description of how modulation operates).  A ControlMechanism can be configured to monitor the outputs of
46
other Mechanisms in order to determine its `control_allocation <ControlMechanism.control_allocation>`, by specifying
47
these in the **monitor_for_control** `argument <ControlMechanism_Monitor_for_Control_Argument>` of its constructor,
48
or in the **monitor** `argument <ObjectiveMechanism_Monitor>` of an ObjectiveMechanism` assigned to its
49
**objective_mechanism** `argument <ControlMechanism_Objective_Mechanism_Argument>` (see `ControlMechanism_Creation`
50
below).  A ControlMechanism can also be assigned as the `controller <Composition.controller>` of a `Composition`,
51
which has a special relation to that Composition: it generally executes either before or after all of the other
52
Mechanisms in that Composition (see `Composition_Controller_Execution`).  The OutputPorts monitored by the
53
ControlMechanism or its `objective_mechanism <ControlMechanism.objective_mechanism>`, and the parameters it modulates
54
can be listed using its `show <ControlMechanism.show>` method.
55

56
.. _ControlMechanism_Composition_Controller:
57

58
*ControlMechanisms and a Composition*
59
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
60

61
A ControlMechanism can be assigned to a `Composition` and executed just like any other Mechanism. It can also be
62
assigned as the `controller <Composition.controller>` of a `Composition`, that has a special relation
63
to the Composition: it is used to control all of the parameters that have been `specified for control
64
<ControlMechanism_ControlSignals>` in that Composition.  A ControlMechanism can be the `controller
65
<Composition.controller>` for only one Composition, and a Composition can have only one `controller
66
<Composition.controller>`.  When a ControlMechanism is assigned as the `controller <Composition.controller>` of a
67
Composition (either in the Composition's constructor, or using its `add_controller <Composition.add_controller>`
68
method, the ControlMechanism assumes control over all of the parameters that have been `specified for control
69
<ControlMechanism_ControlSignals>` for Components in the Composition.  The Composition's `controller
70
<Composition.controller>` is executed either before or after all of the other Components in the Composition are
71
executed, including any other ControlMechanisms that belong to it (see `Composition_Controller_Execution`).  A
72
ControlMechanism can be assigned as the `controller <Composition.controller>` for a Composition by specifying it in
73
the **controller** argument of the Composition's constructor, or by using the Composition's `add_controller
74
<Composition.add_controller>` method.  A Composition's `controller <Composition.controller>` and its associated
75
Components can be displayed using the Composition's `show_graph <ShowGraph.show_graph>` method with its
76
**show_control** argument assigned as `True`.
77

78

79
.. _ControlMechanism_Creation:
80

81
Creating a ControlMechanism
82
---------------------------
83

84
A ControlMechanism is created by calling its constructor.  When a ControlMechanism is created, the OutputPorts it
85
monitors and the Ports it modulates can be specified in the **montior_for_control** and **objective_mechanism**
86
arguments of its constructor, respectively.  Each can be specified in several ways, as described below. If neither of
87
those arguments is specified, then only the ControlMechanism is constructed, and its inputs and the parameters it
88
modulates must be specified in some other way.
89

90
.. _ControlMechanism_Monitor_for_Control:
91

92
*Specifying OutputPorts to be monitored*
93
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
94

95
A ControlMechanism can be configured to monitor the output of other Mechanisms either directly (by receiving direct
96
Projections from their OutputPorts), or by way of an `ObjectiveMechanism` that evaluates those outputs and passes the
97
result to the ControlMechanism (see `below <ControlMechanism_ObjectiveMechanism>` for more detailed description).
98
The following figures show an example of each:
99

100
+-------------------------------------------------------------------------+----------------------------------------------------------------------+
101
| .. figure:: _static/ControlMechanism_without_ObjectiveMechanism_fig.svg | .. figure:: _static/ControlMechanism_with_ObjectiveMechanism_fig.svg |
102
+-------------------------------------------------------------------------+----------------------------------------------------------------------+
103

104
COMMENT:
105
FIX: USE THIS IF MOVED TO SECTION AT END THAT CONSOLIDATES EXAMPLES
106
**ControlMechanism with and without ObjectiveMechanism**
107

108
+-------------------------------------------------------------------------+-------------------------------------------------------------------------+
109
| >>> mech_A = ProcessingMechanism(name='ProcessingMechanism A')          | .. figure:: _static/ControlMechanism_without_ObjectiveMechanism_fig.svg |
110
| >>> mech_B = ProcessingMechanism(name='ProcessingMechanism B')          |                                                                         |
111
| >>> ctl_mech = ControlMechanism(name='ControlMechanism',                |                                                                         |
112
| ...                             monitor_for_control=[mech_A,            |                                                                         |
113
| ...                                                  mech_B],           |                                                                         |
114
| ...                             control_signals=[(SLOPE,mech_A),        |                                                                         |
115
| ...                                              (SLOPE,mech_B)])       |                                                                         |
116
| >>> comp = Composition()                                                |                                                                         |
117
| >>> comp.add_linear_processing_pathway([mech_A,mech_B, ctl_mech])       |                                                                         |
118
| >>> comp.show_graph()                                                   |                                                                         |
119
+-------------------------------------------------------------------------+-------------------------------------------------------------------------+
120
| >>> mech_A = ProcessingMechanism(name='ProcessingMechanism A')          | .. figure:: _static/ControlMechanism_with_ObjectiveMechanism_fig.svg    |
121
| >>> mech_B = ProcessingMechanism(name='ProcessingMechanism B')          |                                                                         |
122
| >>> ctl_mech = ControlMechanism(name='ControlMechanism',                |                                                                         |
123
| ...                             monitor_for_control=[mech_A,            |                                                                         |
124
| ...                                                  mech_B],           |                                                                         |
125
| ...                             objective_mechanism=True,               |                                                                         |
126
| ...                             control_signals=[(SLOPE,mech_A),        |                                                                         |
127
| ...                                              (SLOPE,mech_B)])       |                                                                         |
128
| >>> comp = Composition()                                                |                                                                         |
129
| >>> comp.add_linear_processing_pathway([mech_A,mech_B, ctl_mech])       |                                                                         |
130
| >>> comp.show_graph()                                                   |                                                                         |
131
+-------------------------------------------------------------------------+-------------------------------------------------------------------------+
132
COMMENT
133

134
Note that, in the figures above, the `ControlProjections <ControlProjection>` are designated with square "arrowheads",
135
and the ControlMechanisms are shown as septagons to indicate that their ControlProjections create a feedback loop
136
(see `Composition_Cycles_and_Feedback`;  also, see `below <ControlMechanism_Add_Linear_Processing_Pathway>`
137
regarding specification of a ControlMechanism and associated ObjectiveMechanism in a Composition's
138
`add_linear_processing_pathway <Composition.add_linear_processing_pathway>` method).
139

140
Which configuration is used is determined by how the following arguments of the ControlMechanism's constructor are
141
specified (also see `ControlMechanism_Examples`):
142

143
  .. _ControlMechanism_Monitor_for_Control_Argument:
144

145
  * **monitor_for_control** -- a list of `OutputPort specifications <OutputPort_Specification>`.  If the
146
    **objective_mechanism** argument is not specified (or is *False* or *None*) then, when the ControlMechanism is
147
    added to a `Composition`, a `MappingProjection` is created from each OutputPort specified to InputPorts
148
    created on the ControlMechanism (see `ControlMechanism_Input` for details). If the **objective_mechanism** `argument
149
    <ControlMechanism_Objective_Mechanism_Argument>` is specified, then the OutputPorts specified in
150
    **monitor_for_control** are assigned to the `ObjectiveMechanism` rather than the ControlMechanism itself (see
151
    `ControlMechanism_ObjectiveMechanism` for details).
152

153
  .. _ControlMechanism_Objective_Mechanism_Argument:
154

155
  * **objective_mechanism** -- if this is specfied in any way other than **False** or **None** (the default),
156
    then an ObjectiveMechanism is created that projects to the ControlMechanism and, when added to a `Composition`,
157
    is assigned Projections from all of the OutputPorts specified either in the  **monitor_for_control** argument of
158
    the ControlMechanism's constructor, or the **monitor** `argument <ObjectiveMechanism_Monitor>` of the
159
    ObjectiveMechanism's constructor (see `ControlMechanism_ObjectiveMechanism` for details).  The
160
    **objective_mechanism** argument can be specified in any of the following ways:
161

162
    - *False or None* -- no ObjectiveMechanism is created and, when the ControlMechanism is added to a
163
      `Composition`, Projections from the OutputPorts specified in the ControlMechanism's **monitor_for_control**
164
      argument are sent directly to ControlMechanism (see specification of **monitor_for_control** `argument
165
      <ControlMechanism_Monitor_for_Control_Argument>`).
166

167
    - *True* -- an `ObjectiveMechanism` is created that projects to the ControlMechanism, and any OutputPorts
168
      specified in the ControlMechanism's **monitor_for_control** argument are assigned to ObjectiveMechanism's
169
      **monitor** `argument <ObjectiveMechanism_Monitor>` instead (see `ControlMechanism_ObjectiveMechanism` for
170
      additional details).
171

172
    - *a list of* `OutputPort specifications <ObjectiveMechanism_Monitor>`; an ObjectiveMechanism is created that
173
      projects to the ControlMechanism, and the list of OutputPorts specified, together with any specified in the
174
      ControlMechanism's **monitor_for_control** `argument <ControlMechanism_Monitor_for_Control_Argument>`, are
175
      assigned to the ObjectiveMechanism's **monitor** `argument <ObjectiveMechanism_Monitor>` (see
176
      `ControlMechanism_ObjectiveMechanism` for additional details).
177

178
    - *a constructor for an* `ObjectiveMechanism` -- the specified ObjectiveMechanism is created, adding any
179
      OutputPorts specified in the ControlMechanism's **monitor_for_control** `argument
180
      <ControlMechanism_Monitor_for_Control_Argument>` to any specified in the ObjectiveMechanism's **monitor**
181
      `argument <ObjectiveMechanism_Monitor>` .  This can be used to specify the `function
182
      <ObjectiveMechanism.function>` used by the ObjectiveMechanism to evaluate the OutputPorts monitored as well as
183
      how it weights those OutputPorts when they are evaluated  (see `below
184
      <ControlMechanism_ObjectiveMechanism_Function>` for additional details).
185

186
    - *an existing* `ObjectiveMechanism` -- for any OutputPorts specified in the ControlMechanism's
187
      **monitor_for_control** `argument <ControlMechanism_Monitor_for_Control_Argument>`, an InputPort is added to the
188
      ObjectiveMechanism, along with `MappingProjection` to it from the specified OutputPort.    This can be used to
189
      specify an ObjectiveMechanism with a custom `function <ObjectiveMechanism.function>` and weighting of the
190
      OutputPorts monitored (see `below <ControlMechanism_ObjectiveMechanism_Function>` for additional details).
191

192
  .. _ControlMechanism_Allow_Probes:
193

194
  * **allow_probes** -- this argument allows values of Components of a `nested Composition <Composition_Nested>` other
195
    than its `OUTPUT <NodeRole.OUTPUT>` `Nodes <Composition_Nodes>` to be specified in the **monitor_for_control**
196
    argument of the ControlMechanism's constructor or, if **objective_mechanism** is specified, in the
197
    **monitor** argument of the ObjectiveMechanism's constructor (see above).  If the Composition's `allow_probes
198
    <Composition.allow_probes>` attribute is False, it is set to *CONTROL*, and only a ControlMechanism can receive
199
    projections from `PROBE <NodeRole.PROBE>` Nodes of a nested Composition (the current one as well as any others in
200
    the same Composition);  if the Composition's `allow_probes <Composition.allow_probes>` attribute is True, then it
201
    is left that way, and any node within the Comopsition, including the ControlMechanism, can receive projections from
202
    `PROBE <NodeRole.PROBE>` Nodes (see `Probes <Composition_Probes>` for additional details).
203

204
The OutputPorts monitored by a ControlMechanism or its `objective_mechanism <ControlMechanism.objective_mechanism>`
205
are listed in the ControlMechanism's `monitor_for_control <ControlMechanism.monitor_for_control>` attribute
206
(and are the same as those listed in the `monitor <ObjectiveMechanism.monitor>` attribute of the
207
`objective_mechanism <ControlMechanism.objective_mechanism>`, if specified).
208

209
.. _ControlMechanism_Add_Linear_Processing_Pathway:
210

211
Note that the MappingProjections created by specification of a ControlMechanism's **monitor_for_control** `argument
212
<ControlMechanism_Monitor_for_Control_Argument>` or the **monitor** argument in the constructor for an
213
ObjectiveMechanism in the ControlMechanism's **objective_mechanism** `argument
214
<ControlMechanism_Objective_Mechanism_Argument>` supersede any MappingProjections that would otherwise be created for
215
them when included in the **pathway** argument of a Composition's `add_linear_processing_pathway
216
<Composition.add_linear_processing_pathway>` method.
217

218
.. _ControlMechanism_ObjectiveMechanism:
219

220
Objective Mechanism
221
^^^^^^^^^^^^^^^^^^^
222

223
COMMENT:
224
TBI FOR COMPOSITION
225
If the ControlMechanism is created automatically by a System (as its `controller <System.controller>`), then the
226
specification of OutputPorts to be monitored and parameters to be controlled are made on the System and/or the
227
Components themselves (see `System_Control_Specification`).  In either case, the Components needed to monitor the
228
specified OutputPorts (an `ObjectiveMechanism` and `Projections <Projection>` to it) and to control the specified
229
parameters (`ControlSignals <ControlSignal>` and corresponding `ControlProjections <ControlProjection>`) are created
230
automatically, as described below.
231
COMMENT
232

233
If an `ObjectiveMechanism` is specified for a ControlMechanism (in the **objective_mechanism** `argument
234
<ControlMechanism_Objective_Mechanism_Argument>` of its constructor; also see `ControlMechanism_Examples`),
235
it is assigned to the ControlMechanism's `objective_mechanism <ControlMechanism.objective_mechanism>` attribute,
236
and a `MappingProjection` is created automatically that projects from the ObjectiveMechanism's *OUTCOME*
237
`output_port <ObjectiveMechanism_Output>` to the *OUTCOME* `input_port <ControlMechanism_Input>` of the
238
ControlMechanism.
239

240
The `objective_mechanism <ControlMechanism.objective_mechanism>` is used to monitor the OutputPorts
241
specified in the **monitor_for_control** `argument <ControlMechanism_Monitor_for_Control_Argument>` of the
242
ControlMechanism's constructor, as well as any specified in the **monitor** `argument <ObjectiveMechanism_Monitor>` of
243
the ObjectiveMechanism's constructor.  Specifically, for each OutputPort specified in either place, an `input_port
244
<ObjectiveMechanism.input_ports>` is added to the ObjectiveMechanism.  OutputPorts to be monitored (and
245
corresponding `input_ports <ObjectiveMechanism.input_ports>`) can be added to the `objective_mechanism
246
<ControlMechanism.objective_mechanism>` later, by using its `add_to_monitor <ObjectiveMechanism.add_to_monitor>` method.
247
The set of OutputPorts monitored by the `objective_mechanism <ControlMechanism.objective_mechanism>` are listed in
248
its `monitor <ObjectiveMechanism>` attribute, as well as in the ControlMechanism's `monitor_for_control
249
<ControlMechanism.monitor_for_control>` attribute.
250

251
When the ControlMechanism is added to a `Composition`, the `objective_mechanism <ControlMechanism.objective_mechanism>`
252
is also automatically added, and MappingProjectons are created from each of the OutputPorts that it monitors to
253
its corresponding `input_ports <ObjectiveMechanism.input_ports>`.  When the Composition is run, the `value
254
<OutputPort.value>`\\(s) of the OutputPort(s) monitored are evaluated using the `objective_mechanism`\\'s `function
255
<ObjectiveMechanism.function>`, and the result is assigned to its *OUTCOME* `output_port
256
<ObjectiveMechanism_Output>`.  That `value <ObjectiveMechanism.value>` is then passed to the ControlMechanism's
257
*OUTCOME* `input_port <ControlMechanism_Input>`, which is used by the ControlMechanism's `function
258
<ControlMechanism.function>` to determine its `control_allocation <ControlMechanism.control_allocation>`.
259

260
.. _ControlMechanism_ObjectiveMechanism_Function:
261

262
If a default ObjectiveMechanism is created by the ControlMechanism (i.e., when *True* or a list of OutputPorts is
263
specified for the **objective_mechanism** `argument <ControlMechanism_Objective_Mechanism_Argument>` of the
264
constructor), then the ObjectiveMechanism is created with its standard default `function <ObjectiveMechanism.function>`
265
(`LinearCombination`), but using *PRODUCT* (rather than the default, *SUM*) as the value of the function's `operation
266
<LinearCombination.operation>` parameter. The result is that the `objective_mechanism
267
<ControlMechanism.objective_mechanism>` multiplies the `value <OutputPort.value>`\\s of the OutputPorts that it
268
monitors, which it passes to the ControlMechanism.  However, if the **objective_mechanism** is specified using either
269
a constructor for, or an existing ObjectiveMechanism, then the defaults for the `ObjectiveMechanism` class -- and any
270
attributes explicitly specified in its construction -- are used.  In that case, if the `LinearCombination` with
271
*PRODUCT* as its `operation <LinearCombination.operation>` parameter are still desired, this must be explicitly
272
specified.  This is illustrated in the following examples.
273

274
The following example specifies a `ControlMechanism` that automatically constructs its `objective_mechanism
275
<ControlMechanism.objective_mechanism>`::
276

277
    >>> from psyneulink import *
278
    >>> my_ctl_mech = ControlMechanism(objective_mechanism=True)
279
    >>> assert isinstance(my_ctl_mech.objective_mechanism.function, LinearCombination)
280
    >>> assert my_ctl_mech.objective_mechanism.function.operation == PRODUCT
281

282
Notice that `LinearCombination` was assigned as the `function <ObjectiveMechanism.function>` of the `objective_mechanism
283
<ControlMechanism.objective_mechanism>`, and *PRODUCT* as its `operation <LinearCombination.operation>` parameter.
284

285
By contrast, the following example explicitly specifies the **objective_mechanism** argument using a constructor for
286
an ObjectiveMechanism::
287

288
    >>> my_ctl_mech = ControlMechanism(objective_mechanism=ObjectiveMechanism())
289
    >>> assert isinstance(my_ctl_mech.objective_mechanism.function, LinearCombination)
290
    >>> assert my_ctl_mech.objective_mechanism.function.operation == SUM
291

292
In this case, the defaults for the ObjectiveMechanism's class are used for its `function <ObjectiveMechanism.function>`,
293
which is a `LinearCombination` function with *SUM* as its `operation <LinearCombination.operation>` parameter.
294

295
Specifying the ControlMechanism's `objective_mechanism <ControlMechanism.objective_mechanism>` with a constructor
296
also provides greater control over how ObjectiveMechanism evaluates the OutputPorts it monitors.  In addition to
297
specifying its `function <ObjectiveMechanism.function>`, the **monitor_weights_and_exponents** `argument
298
<ObjectiveMechanism_Monitor_Weights_and_Exponents>` can be used to parameterize the relative contribution made by the
299
monitored OutputPorts when they are evaluated by that `function <ObjectiveMechanism.function>` (see
300
`ControlMechanism_Examples`).
301

302
COMMENT:
303
TBI FOR COMPOSITION
304
When a ControlMechanism is created for or assigned as the `controller <Composition.controller>` of a `Composition` (see
305
`ControlMechanism_Composition_Controller`), any OutputPorts specified to be monitored by the System are assigned as
306
inputs to the ObjectiveMechanism.  This includes any specified in the **monitor_for_control** argument of the
307
System's constructor, as well as any specified in a MONITOR_FOR_CONTROL entry of a Mechanism `parameter specification
308
dictionary <ParameterPort_Specification>` (see `Mechanism_Constructor_Arguments` and `System_Control_Specification`).
309

310
FOR DEVELOPERS:
311
    If the ObjectiveMechanism has not yet been created, these are added to the **monitored_output_ports** of its
312
    constructor called by ControlMechanism._instantiate_objective_mechanism;  otherwise, they are created using the
313
    ObjectiveMechanism.add_to_monitor method.
314
COMMENT
315

316
.. _ControlMechanism_ControlSignals:
317

318
*Specifying Parameters to Control*
319
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
320

321
This can be specified in either of two ways (see `ControlSignal_Examples` in `ControlSignal`):
322

323
*With a ControlMechanism itself*
324

325
The parameters controlled by a ControlMechanism can be specified in the **control** argument of its constructor;
326
the argument must be a `specification for one more ControlSignals <ControlSignal_Specification>`.  The parameter to
327
be controlled must belong to a Component in the same `Composition` as the ControlMechanism when it is added to the
328
Composition, or an error will occur.
329

330
*With a Parameter to be controlled by the* `controller <Composition.controller>` *of a* `Composition`
331

332
Control can also be specified for a parameter where the `parameter itself is specified <ParameterPort_Specification>`,
333
by including the specification of a `ControlSignal`, `ControlProjection`, or the keyword `CONTROL` in a `tuple
334
specification <ParameterPort_Tuple_Specification>` for the parameter.  In this case, the specified parameter will be
335
assigned for control by the `controller <Composition.controller>` of any `Composition` to which its Component belongs,
336
when the Component is added to the Composition (see `ControlMechanism_Composition_Controller`).  Conversely, when
337
a ControlMechanism is assigned as the `controller <Composition.controller>` of a Composition, a `ControlSignal` is
338
created and assigned to the ControlMechanism for every parameter of any `Component <Component>` in the Composition
339
that has been `specified for control <ParameterPort_Modulatory_Specification>`.
340

341
In general, a `ControlSignal` is created for each parameter specified to be controlled by a ControlMechanism.  These
342
are a type of `OutputPort` that send a `ControlProjection` to the `ParameterPort` of the parameter to be
343
controlled. All of the ControlSignals for a ControlMechanism are listed in its `control_signals
344
<ControlMechanism.control_signals>` attribute, and all of its ControlProjections are listed in
345
its `control_projections <ControlMechanism.control_projections>` attribute (see `ControlMechanism_Examples`).
346

347
.. _ControlMechanism_Structure:
348

349
Structure
350
---------
351

352
.. _ControlMechanism_Input:
353

354
*Input*
355
~~~~~~~
356

357
By default, a ControlMechanism has a single `input_port <Mechanism_Base.input_port>` named *OUTCOME*. If it has an
358
`objective_mechanism <ControlMechanism.objective_mechanism>`, then the *OUTCOME* `input_port
359
<ControlMechanism.outcome_input_ports>` receives a single `MappingProjection` from the `objective_mechanism
360
<ControlMechanism.objective_mechanism>`\\'s *OUTCOME* `OutputPort <OutputPort>` (see
361
`ControlMechanism_ObjectiveMechanism` for additional details). If the ControlMechanism has no `objective_mechanism
362
<ControlMechanism.objective_mechanism>` then, when it is added to a `Composition`, MappingProjections are created
363
from the items specified in `monitor_for_control <ControlMechanism.monitor_for_control>` directly to InputPorts on
364
the ControlMechanism (see `ControlMechanism_Monitor_for_Control` for additional details). The number of InputPorts
365
created, and how the items listed in `monitor_for_control <ControlMechanism.monitor_for_control>` project to them is
366
deterimined by the ControlMechanism's `outcome_input_ports_option <ControlMechanism.outcome_input_ports_option>`.
367
All of the Inports that receive Projections from those items, or the `objective_mechanism
368
<ControlMechanism.objective_mechanism>` if the ControlMechanism has one, are listed in its `outcome_input_ports
369
<ControlMechanism.outcome_input_ports>` attribute, and their values in the `outcome <ControlMechanism.outcome>`
370
attribute.  The latter is used as the input to the ControlMechanism's `function <ControlMechanism.function>` to
371
determine its `control_allocation <ControlMechanism.control_allocation>`.
372

373
.. _ControlMechanism_Function:
374

375
*Function*
376
~~~~~~~~~~
377

378
A ControlMechanism's `function <ControlMechanism.function>` uses its `outcome <ControlMechanism.outcome>`
379
attribute (the `value <InputPort.value>` of its *OUTCOME* `InputPort`) to generate one or more values used
380
for ControlMechanism's `control_allocation <ControlMechanism.control_allocation>`.  By default, its `function
381
<ControlMechanism.function>` is assigned the `Identity` Function, which takes a single value as its input and returns
382
it as its output.  That is then used as the ControlSignal's `control_allocation <ControlMechanism.control_allocation>`,
383
which in turn is assigned as the allocation for all of the ControlMechanism's `control_signals
384
<ControlMechanism.control_signals>`. That is, by default, the ControlMechanism's input is distributed as the
385
allocation to each of its `control_signals <ControlMechanism.control_signals>`. This same behavior also occurs for
386
any custom function assigned to a ControlMechanism that returns a 2d array with a single item in its outer dimension
387
(axis 0).  If a function is assigned that returns a 2d array with more than one item, and the number of those items
388
(i.e., the length of the 2d array) is the same as the number of `control_signals <ControlMechanism.control_signals>`,
389
then each item is assigned to a corresponding `ControlSignal` (in the order in which they are specified in the
390
**control_signals** argument of the ControlMechanism's constructor).  However, these default behaviors can be modified
391
by specifying that individual ControlSignals reference different items in the ControlMechanism's `value
392
<Mechanism_Base.value>` (see `OutputPort_Custom_Variable`).
393

394
.. _ControlMechanism_Output:
395

396
*Output*
397
~~~~~~~~
398

399
The OutputPorts of a ControlMechanism are `ControlSignals <ControlSignal>` (listed in its `control_signals
400
<ControlMechanism.control_signals>` attribute). It has a `ControlSignal` for each parameter specified in the
401
**control** argument of its constructor, that sends a `ControlProjection` to the `ParameterPort` for the
402
corresponding parameter.  The ControlSignals are listed in the `control_signals <ControlMechanism.control_signals>`
403
attribute;  since they are a type of `OutputPort`, they are also listed in the ControlMechanism's `output_ports
404
<Mechanism_Base.output_ports>` attribute. The parameters modulated by a ControlMechanism's ControlSignals can be
405
displayed using its `show <ControlMechanism.show>` method.
406

407
By default, each `ControlSignal` is assigned as its `allocation <ControlSignal.allocation>` the value of the
408
corresponding item of the ControlMechanism's `control_allocation <ControlMechanism.control_allocation>`;  however,
409
subtypes of ControlMechanism may assign allocations differently. The **default_allocation** argument of the
410
ControlMechanism's constructor can be used to specify a default allocation for ControlSignals that
411
have not been assigned their own `default_allocation <ControlSignal.default_allocation>`.
412

413
.. warning::
414
    the **default_variable** argument of the ControlMechanism's constructor is **not** used to specify a default
415
    allocation;  the **default_allocation** argument must be used for that purpose.  The **default_variable** argument
416
    should only be used to specify the format of the `value <ControlMechanism.value>` of the ControlMechanism's
417
    `input_port (see `Mechanism_InputPorts` for additional details), and its default input when it does not receive
418
    any Projections (see `default_variable <Component_Variable>` for additional details).
419

420
.. note::
421
   While specifying **default_allocation** will take effect the first time the ControlMechanism is executed,
422
   a value specified for **default_variable** will not take effect (i.e., in determining `control_allocation
423
   <ControlMechanism.control_allocation>` or `value <ControlSignal.value>`\\s of the ControlSignals) until after
424
   the ControlMechahnism is executed, and thus will not influence the parameters it controls until the next round of
425
   execution (see `Composition_Timing` for a detailed description of the sequence of events during a Composition's
426
   execution).
427

428
COMMENT:
429
    The `value <ControlSignal.value>` of a ControlSignal is not necessarily the same as its `allocation.
430
    <ControlSignal.allocation>`  The former is the result of the ControlSignal's `function <ControlSignal.function>`,
431
    which is executed when the ControlSignal is updated;  the latter is the value assigned to the ControlSignal's
432
COMMENT
433

434
The `allocation <ControlSignal.allocation>` is used by each ControlSignal to determine its `intensity
435
<ControlSignal.intensity>`, which is then assigned to the `value <ControlProjection.value>`
436
of the ControlSignal's `ControlProjection`.   The `value <ControlProjection.value>` of the ControlProjection
437
is used by the `ParameterPort` to which it projects to modify the value of the parameter it controls (see
438
`ControlSignal_Modulation` for a description of how a ControlSignal modulates the value of a parameter).
439

440
.. _ControlMechanism_Costs_NetOutcome:
441

442
*Costs and Net Outcome*
443
~~~~~~~~~~~~~~~~~~~~~~~
444

445
A ControlMechanism's `control_signals <ControlMechanism.control_signals>` are each associated with a set of `costs
446
<ControlSignal_Costs>`, that are computed individually by each `ControlSignal` when they are `executed
447
<ControlSignal_Execution>` by the ControlMechanism.  The costs last computed by the `control_signals
448
<ControlMechanism>` are assigned to the ControlMechanism's `costs <ControlSignal.costs>` attribute.  A ControlMechanism
449
also has a set of methods -- `combine_costs <ControlMechanism.combine_costs>`, `compute_reconfiguration_cost
450
<ControlMechanism.compute_reconfiguration_cost>`, and `compute_net_outcome <ControlMechanism.compute_net_outcome>` --
451
that can be used to compute the `combined costs <ControlMechanism.combined_costs>` of its `control_signals
452
<ControlMechanism.control_signals>`, a `reconfiguration_cost <ControlSignal.reconfiguration_cost>` based on their change
453
in value, and a `net_outcome <ControlMechanism.net_outcome>` (the `value <InputPort.value>` of the ControlMechanism's
454
*OUTCOME* `InputPort <ControlMechanism_Input>` minus its `combined costs <ControlMechanism.combined_costs>`),
455
respectively (see `ControlMechanism_Costs_Computation` below for additional details). These methods are used by some
456
subclasses of ControlMechanism (e.g., `OptimizationControlMechanism`) to compute their `control_allocation
457
<ControlMechanism.control_allocation>`.  Each method is assigned a default function, but can be assigned a custom
458
functions in a corrsponding argument of the ControlMechanism's constructor (see links to attributes for details).
459

460
.. _ControlMechanism_Reconfiguration_Cost:
461

462
*Reconfiguration Cost*
463

464
A ControlMechanism's `reconfiguration_cost <ControlMechanism.reconfiguration_cost>` is distinct from the
465
costs of the ControlMechanism's `ControlSignals <ControlSignal>`, and in particular it is not the same as their
466
`adjustment_cost <ControlSignal.adjustment_cost>`.  The latter, if specified by a ControlSignal, is computed
467
individually by that ControlSignal using its `adjustment_cost_function <ControlSignal.adjustment_cost_function>`, based
468
on the change in its `intensity <ControlSignal.intensity>` from its last execution. In contrast, a ControlMechanism's
469
`reconfiguration_cost  <ControlMechanism.reconfiguration_cost>` is computed by its `compute_reconfiguration_cost
470
<ControlMechanism.compute_reconfiguration_cost>` function, based on the change in its `control_allocation
471
ControlMechanism.control_allocation>` from the last execution, that will be applied to *all* of its
472
`control_signals <ControlMechanism.control_signals>`. By default, `compute_reconfiguration_cost
473
<ControlMechanism.compute_reconfiguration_cost>` is assigned as the `Distance` function with the `EUCLIDEAN` metric).
474

475
.. _ControlMechanism_Execution:
476

477
Execution
478
---------
479

480
A ControlMechanism is executed using the same sequence of actions as any `Mechanism <Mechanism_Execution>`, with the
481
following additions.
482

483
# FIX: 11/3/21: MODIFY TO INCLUDE POSSIBLITY OF MULTIPLE OUTCOME_INPUT_PORTS
484
The ControlMechanism's `function <ControlMechanism.function>` takes as its input the `value <InputPort.value>` of
485
its *OUTCOME* `input_port <ControlMechanism.input_port>` (also contained in `outcome <ControlSignal.outcome>`).
486
It uses that to determine the `control_allocation <ControlMechanism.control_allocation>`, which specifies the value
487
assigned to the `allocation <ControlSignal.allocation>` of each of its `ControlSignals <ControlSignal>`.  Each
488
ControlSignal uses that value to calculate its `intensity <ControlSignal.intensity>`, as well as its `cost
489
<ControlSignal.cost>.  The `intensity <ControlSignal.intensity>`is used by its `ControlProjection(s)
490
<ControlProjection>` to modulate the value of the ParameterPort(s) for the parameter(s) it controls.  Note that
491
the modulated value of the parameter may not be used until the subsequent `TRIAL <TimeScale.TRIAL>` of execution,
492
if the ControlMechansim is not executed until after the Component to which the paramter belongs is executed
493
(see `note <ModulatoryMechanism_Lazy_Evaluation_Note>`).
494

495
.. _ControlMechanism_Costs_Computation:
496

497
*Computation of Costs and Net_Outcome*
498
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
499

500
Once the ControlMechanism's `function <ControlMechanism.function>` has executed, if `compute_reconfiguration_cost
501
<ControlMechanism.compute_reconfiguration_cost>` has been specified, then it is used to compute the
502
`reconfiguration_cost <ControlMechanism.reconfiguration_cost>` for its `control_allocation
503
<ControlMechanism.control_allocation>` (see `above <ControlMechanism_Reconfiguration_Cost>`. After that, each
504
of the ControlMechanism's `control_signals <ControlMechanism.control_signals>` calculates its `cost
505
<ControlSignal.cost>`, based on its `intensity  <ControlSignal.intensity>`.  The ControlMechanism then combines these
506
with the `reconfiguration_cost <ControlMechanism.reconfiguration_cost>` using its `combine_costs
507
<ControlMechanism.combine_costs>` function, and the result is assigned to the `costs <ControlMechanism.costs>`
508
attribute.  Finally, the ControlMechanism uses this, together with its `outcome <ControlMechanism.outcome>` attribute,
509
to compute a `net_outcome <ControlMechanism.net_outcome>` using its `compute_net_outcome
510
<ControlMechanism.compute_net_outcome>` function.  This is used by some subclasses of ControlMechanism
511
(e.g., `OptimizationControlMechanism`) to  compute its `control_allocation <ControlMechanism.control_allocation>`
512
for the next `TRIAL <TimeScale.TRIAL>` of execution.
513

514
.. _ControlMechanism_Controller_Execution:
515

516
*Execution as Controller of a Composition*
517
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
518

519
if a ControlMechanism is assigned as the `controller of a `Composition <ControlMechanism_Composition_Controller>`,
520
then it is executed either before or after all of the other  `Mechanisms <Mechanism>` executed in a `TRIAL
521
<TimeScale.TRIAL>` for that Composition, depending on the value assigned to the Composition's `controller_mode
522
<Composition.controller_mode>` attribute (see `Composition_Controller_Execution`).  If a ControlMechanism is added to
523
a Composition for which it is not a `controller <Composition.controller>`, then it executes in the same way as any
524
`Mechanism <Mechanism>`, based on its place in the Composition's `graph <Composition.graph>`.  Because
525
`ControlProjections <ControlProjection>` are likely to introduce cycles (recurrent connection loops) in the
526
graph, the effects of a ControlMechanism and its projections will generally not be applied in the first `TRIAL
527
<TimeScale.TRIAL>` (see `Composition_Cycles_and_Feedback` for configuring the initialization of feedback
528
loops in a Composition; also see `Scheduler` for a description of additional ways in which a ControlMechanism and its
529
dependents can be scheduled to execute).
530

531
.. _ControlMechanism_Examples:
532

533
Examples
534
--------
535

536
The examples below focus on the specification of the `objective_mechanism <ControlMechanism.objective_mechanism>`
537
for a ControlMechanism.  See `Control Signal Examples <ControlSignal_Examples>` for examples of how to specify the
538
ControlSignals for a ControlMechanism.
539

540
The following example creates a ControlMechanism by specifying its **objective_mechanism** using a constructor
541
that specifies the OutputPorts to be monitored by its `objective_mechanism <ControlMechanism.objective_mechanism>`
542
and the function used to evaluate these::
543

544
    >>> my_mech_A = ProcessingMechanism(name="Mech A")
545
    >>> my_DDM = DDM(name="My DDM")
546
    >>> my_mech_B = ProcessingMechanism(function=Logistic,
547
    ...                                 name="Mech B")
548

549
    >>> my_control_mech = ControlMechanism(
550
    ...                          objective_mechanism=ObjectiveMechanism(monitor=[(my_mech_A, 2, 1),
551
    ...                                                                           my_DDM.output_ports[RESPONSE_TIME]],
552
    ...                                                                 name="Objective Mechanism"),
553
    ...                          function=LinearCombination(operation=PRODUCT),
554
    ...                          control_signals=[(THRESHOLD, my_DDM),
555
    ...                                           (GAIN, my_mech_B)],
556
    ...                          name="My Control Mech")
557

558
This creates an ObjectiveMechanism for the ControlMechanism that monitors the `primary OutputPort <OutputPort_Primary>`
559
of ``my_mech_A`` and the *RESPONSE_TIME* OutputPort of ``my_DDM``;  its function first multiplies the former by ``2``,
560
then takes product of their values and passes the result as the input to the ControlMechanism.  The ControlMechanism's
561
`function <ControlMechanism.function>` uses this value to determine the allocation for its ControlSignals, that control
562
the value of the `threshold <DriftDiffusionAnalytical.threshold>` parameter of the `DriftDiffusionAnalytical` Function
563
for ``my_DDM`` and the  `gain <Logistic.gain>` parameter of the `Logistic` Function for ``my_transfer_mech_B``.
564

565
The following example specifies the same set of OutputPorts for the ObjectiveMechanism, by assigning them directly
566
to the **objective_mechanism** argument::
567

568
    >>> my_control_mech = ControlMechanism(
569
    ...                             objective_mechanism=[(my_mech_A, 2, 1),
570
    ...                                                  my_DDM.output_ports[RESPONSE_TIME]],
571
    ...                             control_signals=[(THRESHOLD, my_DDM),
572
    ...                                              (GAIN, my_mech_B)])
573

574
Note that, while this form is more succinct, it precludes specifying the ObjectiveMechanism's function.  Therefore,
575
the values of the monitored OutputPorts will be added (the default) rather than multiplied.
576

577
The ObjectiveMechanism can also be created on its own, and then referenced in the constructor for the ControlMechanism::
578

579
    >>> my_obj_mech = ObjectiveMechanism(monitored_output_ports=[(my_mech_A, 2, 1),
580
    ...                                                            my_DDM.output_ports[RESPONSE_TIME]],
581
    ...                                      function=LinearCombination(operation=PRODUCT))
582

583
    >>> my_control_mech = ControlMechanism(
584
    ...                        objective_mechanism=my_obj_mech,
585
    ...                        control_signals=[(THRESHOLD, my_DDM),
586
    ...                                         (GAIN, my_mech_B)])
587

588
Here, as in the first example, the constructor for the ObjectiveMechanism can be used to specify its function, as well
589
as the OutputPort that it monitors.
590

591
COMMENT:
592
FIX 8/27/19 [JDC]:  ADD TO COMPOSITION
593
See `System_Control_Examples` for examples of how a ControlMechanism, the OutputPorts its
594
`objective_mechanism <ControlSignal.objective_mechanism>`, and its `control_signals <ControlMechanism.control_signals>`
595
can be specified for a System.
596
COMMENT
597

598
.. _ControlMechanism_Class_Reference:
599

600
Class Reference
601
---------------
602

603
"""
604

605
import collections
1✔
606
import copy
1✔
607
import itertools
1✔
608
import threading
1✔
609
import uuid
1✔
610
import warnings
1✔
611

612
import numpy as np
1✔
613
from beartype import beartype
1✔
614

615
from psyneulink._typing import Optional, Union, Callable, Literal, Iterable
1✔
616

617
from psyneulink.core.components.functions.nonstateful.transferfunctions import Identity
1✔
618
from psyneulink.core.components.functions.nonstateful.transformfunctions import Concatenate
1✔
619
from psyneulink.core.components.functions.nonstateful.transformfunctions import LinearCombination
1✔
620
from psyneulink.core.components.mechanisms.mechanism import Mechanism, Mechanism_Base, MechanismError
1✔
621
from psyneulink.core.components.mechanisms.modulatory.modulatorymechanism import ModulatoryMechanism_Base
1✔
622
from psyneulink.core.components.ports.inputport import InputPort
1✔
623
from psyneulink.core.components.ports.modulatorysignals.controlsignal import ControlSignal
1✔
624
from psyneulink.core.components.ports.outputport import OutputPort
1✔
625
from psyneulink.core.components.ports.parameterport import ParameterPort
1✔
626
from psyneulink.core.components.ports.port import Port, _parse_port_spec
1✔
627
from psyneulink.core.globals.defaults import defaultControlAllocation
1✔
628
from psyneulink.core.globals.keywords import \
1✔
629
    AUTO_ASSIGN_MATRIX, COMBINE, CONTROL, CONTROL_PROJECTION, CONTROL_SIGNAL, CONTROL_SIGNALS, CONCATENATE, \
630
    EID_SIMULATION, FEEDBACK, FUNCTION, GATING_SIGNAL, INIT_EXECUTE_METHOD_ONLY, INTERNAL_ONLY, NAME, \
631
    MECHANISM, MULTIPLICATIVE, MODULATORY_SIGNALS, MONITOR_FOR_CONTROL, MONITOR_FOR_MODULATION, \
632
    OBJECTIVE_MECHANISM, OUTCOME, OWNER_VALUE, PARAMS, PORT_TYPE, PRODUCT, PROJECTION_TYPE, PROJECTIONS, \
633
    REFERENCE_VALUE, SEPARATE, INPUT_SHAPES, VALUE
634
from psyneulink.core.globals.parameters import Parameter, ParameterNoValueError, check_user_specified
1✔
635
from psyneulink.core.globals.context import Context, ContextFlags
1✔
636
from psyneulink.core.globals.preferences.basepreferenceset import ValidPrefSet
1✔
637
from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel
1✔
638
from psyneulink.core.globals.utilities import ContentAddressableList, convert_all_elements_to_np_array, convert_to_list, convert_to_np_array
1✔
639

640
__all__ = [
1✔
641
    'CONTROL_ALLOCATION', 'GATING_ALLOCATION', 'ControlMechanism', 'ControlMechanismError',
642
]
643

644
CONTROL_ALLOCATION = 'control_allocation'
1✔
645
GATING_ALLOCATION = 'gating_allocation'
1✔
646

647
MonitoredOutputPortTuple = collections.namedtuple("MonitoredOutputPortTuple", "output_port weight exponent matrix")
1✔
648

649
def _is_control_spec(spec):
1✔
650
    from psyneulink.core.components.projections.modulatory.controlprojection import ControlProjection
1✔
651
    if isinstance(spec, tuple):
1✔
652
        return any(_is_control_spec(item) for item in spec)
1✔
653
    if isinstance(spec, dict) and PROJECTION_TYPE in spec:
1✔
654
        return _is_control_spec(spec[PROJECTION_TYPE])
1✔
655
    elif isinstance(spec, (ControlMechanism,
1✔
656
                           ControlSignal,
657
                           ControlProjection)):
658
        return True
1✔
659
    elif isinstance(spec, type) and issubclass(spec, (ControlMechanism,
1✔
660
                                                      ControlSignal,
661
                                                      ControlProjection)):
662
        return True
1✔
663
    elif isinstance(spec, str) and spec in {CONTROL, CONTROL_PROJECTION, CONTROL_SIGNAL}:
1✔
664
        return True
1✔
665
    else:
666
        return False
1✔
667

668

669
class ControlMechanismError(MechanismError):
1✔
670
    def __init__(self, message, data=None):
1✔
671
        self.data = data
1✔
672
        return super().__init__(message)
1✔
673

674

675
def validate_monitored_port_spec(owner, spec_list):
1✔
676
    context = Context(string='ControlMechanism.validate_monitored_port_spec')
1✔
677
    for spec in spec_list:
1✔
678
        if isinstance(spec, MonitoredOutputPortTuple):
1!
679
            spec = spec.output_port
×
680
        elif isinstance(spec, tuple):
1✔
681
            spec = _parse_port_spec(owner=owner, port_type=InputPort, port_spec=spec, context=context)
1✔
682
            spec = spec['params'][PROJECTIONS][0][0]
1✔
683
        elif isinstance(spec, dict):
1!
684
            # If it is a dict, parse to validate that it is an InputPort specification dict
685
            #    (for InputPort of ObjectiveMechanism to be assigned to the monitored_output_port)
686
            spec = _parse_port_spec(owner=owner, port_type=InputPort, port_spec=spec, context=context)
×
687
            # Get the OutputPort, to validate that it is in the ControlMechanism's Composition (below);
688
            #    presumes that the monitored_output_port is the first in the list of projection_specs
689
            #    in the InputPort port specification dictionary returned from the parse,
690
            #    and that it is specified as a projection_spec (parsed into that in the call
691
            #    to _parse_connection_specs by _parse_port_spec)
692
            spec = spec[PROJECTIONS][0][0]
×
693

694
        if not isinstance(spec, (OutputPort, Mechanism)):
1!
695
            if isinstance(spec, type) and issubclass(spec, Mechanism):
×
696
                raise ControlMechanismError(
697
                    f"Mechanism class ({spec.__name__}) specified in '{MONITOR_FOR_CONTROL}' arg "
698
                    f"of {owner.name}; it must be an instantiated {Mechanism.__name__} or "
699
                    f"{OutputPort.__name__} of one."
700
                )
701
            elif isinstance(spec, Port):
×
702
                raise ControlMechanismError(
703
                    f"{spec.__class__.__name__} specified in '{MONITOR_FOR_CONTROL}' arg of {owner.name} "
704
                    f"({spec.name} of {spec.owner.name}); "
705
                    f"it must be an {OutputPort.__name__} or {Mechanism.__name__}."
706
                )
707
            else:
708
                raise ControlMechanismError(
709
                    f"Erroneous specification of '{MONITOR_FOR_CONTROL}' arg for {owner.name} ({spec}); "
710
                    f"it must be an {OutputPort.__name__} or a {Mechanism.__name__}."
711
                )
712

713
def _control_mechanism_costs_getter(owning_component=None, context=None):
1✔
714
    # NOTE: In cases where there is a reconfiguration_cost, that cost is not returned by this method
715
    try:
1✔
716
        costs = convert_all_elements_to_np_array([
1✔
717
            (
718
                c.compute_costs(c.parameters.value._get(context), context=context)
719
                if c.initialization_status != ContextFlags.DEFERRED_INIT else None
720
            )
721
            for c in owning_component.control_signals
722
            if hasattr(c, 'compute_costs')
723
        ])  # GatingSignals don't have cost fcts
724
        return costs
1✔
725

NEW
726
    except (ParameterNoValueError, TypeError):
×
UNCOV
727
        return None
×
728

729
def _outcome_getter(owning_component=None, context=None):
1✔
730
    """Return array of values of outcome_input_ports"""
731
    try:
1✔
732
        return np.array([port.parameters.value._get(context) for port in owning_component.outcome_input_ports],
1✔
733
                        dytpe=object)
734
    except (AttributeError, ParameterNoValueError, TypeError):
1✔
735
        return None
1✔
736

737
def _net_outcome_getter(owning_component=None, context=None):
1✔
738
    # NOTE: In cases where there is a reconfiguration_cost, that cost is not included in the net_outcome
739
    try:
1✔
740
        c = owning_component
1✔
741
        return c.compute_net_outcome(
1✔
742
            c.parameters.outcome._get(context),
743
            c.combine_costs()
744
        )
745
    except (ParameterNoValueError, TypeError):
1✔
746
        return np.array([0])
1✔
747

748
def _control_allocation_getter(owning_component=None, context=None):
1✔
749
    try:
1✔
750
        return convert_to_np_array([
1✔
751
            v.parameters.variable._get(context) for v in owning_component.control_signals
752
        ])
753
    except (TypeError, AttributeError):
1✔
754
        return owning_component.defaults.control_allocation
1✔
755

756

757
class ControlMechanism(ModulatoryMechanism_Base):
1✔
758
    """
759
    ControlMechanism(                        \
760
        monitor_for_control=None,            \
761
        objective_mechanism=None,            \
762
        allow_probes=False,                  \
763
        outcome_input_ports_option=SEPARATE  \
764
        function=Linear,                     \
765
        default_allocation=None,             \
766
        control=None,                        \
767
        modulation=MULTIPLICATIVE,           \
768
        combine_costs=np.sum,                \
769
        compute_reconfiguration_cost=None,   \
770
        compute_net_outcome=lambda x,y:x-y)
771

772
    Subclass of `ModulatoryMechanism <ModulatoryMechanism>` that modulates the parameter(s) of one or more
773
    `Component(s) <Component>`.  See `Mechanism <Mechanism_Class_Reference>` for additional arguments and attributes.
774

775
    COMMENT: FIX 5/8/20
776
        Description:
777
            Protocol for instantiating unassigned ControlProjections (i.e., w/o a sender specified):
778
               If sender is not specified for a ControlProjection (e.g., in a parameter specification tuple)
779
                   it is flagged for deferred_init() in its __init__ method
780
               If ControlMechanism is instantiated or assigned as the controller for a System:
781
                   the System calls its _get_monitored_output_ports() method which returns all of the OutputPorts
782
                       within the System that have been specified to be MONITORED_FOR_CONTROL, and then assigns
783
                       them (along with any specified in the **monitored_for_control** arg of the System's constructor)
784
                       to the `objective_mechanism` argument of the ControlMechanism's constructor;
785
                   the System calls its _get_control_signals_for_system() method which returns all of the parameters
786
                       that have been specified for control within the System, assigns them a ControlSignal
787
                       (with a ControlProjection to the ParameterPort for the parameter), and assigns the
788
                       ControlSignals (alogn with any specified in the **control** argument of the System's
789
                       constructor) to the **control** argument of the ControlMechanism's constructor
790

791
            OBJECTIVE_MECHANISM param determines which Ports will be monitored.
792
                specifies the OutputPorts of the terminal Mechanisms in the System to be monitored by ControlMechanism
793
                this specification overrides any in System.], but can be overridden by mechanism.params[
794
                ?? if MonitoredOutputPorts appears alone, it will be used to determine how Ports are assigned from
795
                    System.execution_graph by default
796
                if MonitoredOutputPortsOption is used, it applies to any Mechanisms specified in the list for which
797
                    no OutputPorts are listed; it is overridden for any Mechanism for which OutputPorts are
798
                    explicitly listed
799
                TBI: if it appears in a tuple with a Mechanism, or in the Mechamism's params list, it applies to
800
                    just that Mechanism
801
    COMMENT
802

803
    Arguments
804
    ---------
805

806
    monitor_for_control : List[OutputPort or Mechanism] : default None
807
        specifies the `OutputPorts <OutputPort>` to be monitored by the `ObjectiveMechanism`, if specified in the
808
        **objective_mechanism** argument (see `ControlMechanism_ObjectiveMechanism`), or directly by the
809
        ControlMechanism itself if an **objective_mechanism** is not specified.  If any specification is a Mechanism
810
        (rather than its OutputPort), its `primary OutputPort <OutputPort_Primary>` is used (see
811
        `ControlMechanism_Monitor_for_Control` for additional details).
812

813
    objective_mechanism : ObjectiveMechanism or List[OutputPort specification] : default None
814
        specifies either an `ObjectiveMechanism` to use for the ControlMechanism, or a list of the OutputPorts it
815
        should monitor; if a list of `OutputPort specifications <ObjectiveMechanism_Monitor>` is used,
816
        a default ObjectiveMechanism is created and the list is passed to its **monitor** argument, along with any
817
        OutputPorts specified in the ControlMechanism's **monitor_for_control** `argument
818
        <ControlMechanism_Monitor_for_Control_Argument>`.
819

820
    allow_probes : bool : default False
821
        specifies whether Components of a `nested Composition <Composition_Nested>` that are not OUTPUT
822
        <NodeRole.OUTPUT>` `Nodes <Composition_Nodes>` of that Composition can be specified as items in the
823
        ControlMechanism's  **monitor_for_control** argument or, if **objective_mechanism**
824
        is specified, in the ObjectiveMechanism's **monitor** argument (see `allow_probes
825
        <ControlMechanism_Allow_Probes>` for additional information).
826

827
    outcome_input_ports_option : COMBINE, CONCATENATE, SEPARATE : default SEPARATE
828
        if **objective_mechanism** is not specified, this specifies whether `MappingProjections <MappingProjection>`
829
        from items specified in **monitor_for_control** are each assigned their own `InputPort` (*SEPARATE*)
830
        or to a single *OUTCOME* InputPort (*CONCATENATE*, *COMBINE*); (see `outcome_input_ports_option
831
        <ControlMechanism.outcome_input_ports_option>` for additional details.
832

833
    function : TransferFunction : default Linear(slope=1, intercept=0)
834
        specifies function used to combine values of monitored OutputPorts.
835

836
    control : ControlSignal specification or list[ControlSignal specification, ...]
837
        specifies the parameters to be controlled by the ControlMechanism; a `ControlSignal` is created for each
838
        (see `ControlSignal_Specification` for details of specification).
839

840
    modulation : str : MULTIPLICATIVE
841
        specifies the default form of modulation used by the ControlMechanism's `ControlSignals <ControlSignal>`,
842
        unless they are `individually specified <ControlSignal_Specification>`.
843

844
    combine_costs : Function, function or method : default np.sum
845
        specifies function used to combine the `cost <ControlSignal.cost>` of the ControlMechanism's `control_signals
846
        <ControlMechanism.control_signals>`;  must take a list or 1d array of scalar values as its argument and
847
        return a list or array with a single scalar value.
848

849
    compute_reconfiguration_cost : Function, function or method : default None
850
        specifies function used to compute the ControlMechanism's `reconfiguration_cost
851
        <ControlMechanism.reconfiguration_cost>`; must take a list or 2d array containing two lists or 1d arrays,
852
        both with the same shape as the ControlMechanism's control_allocation attribute, and return a scalar value.
853

854
    compute_net_outcome : Function, function or method : default lambda outcome, cost: outcome-cost
855
        function used to combine the values of its `outcome <ControlMechanism.outcome>` and `costs
856
        <ControlMechanism.costs>` attributes;  must take two 1d arrays (outcome and cost) with scalar values as its
857
        arguments and return an array with a single scalar value.
858

859
    Attributes
860
    ----------
861

862
    monitor_for_control : List[OutputPort]
863
        each item is an `OutputPort` monitored by the ControlMechanism or its `objective_mechanism
864
        <ControlMechanism.objective_mechanism>` if that is specified (see `ControlMechanism_Monitor_for_Control`);
865
        in the latter case, the list returned is ObjectiveMechanism's `monitor <ObjectiveMechanism.monitor>` attribute.
866

867
    objective_mechanism : ObjectiveMechanism
868
        `ObjectiveMechanism` that monitors and evaluates the values specified in the ControlMechanism's
869
        **objective_mechanism** argument, and transmits the result to the ControlMechanism's *OUTCOME*
870
        `input_port <Mechanism_Base.input_port>`.
871

872
    allow_probes : bool
873
        indicates status of the `allow_probes <Composition.allow_probes>` attribute of the Composition
874
        to which the ControlMechanism belongs.  If False, items specified in the `monitor_for_control
875
        <ControlMechanism.monitor_for_control>` are all `OUTPUT <NodeRole.OUTPUT>` `Nodes <Composition_Nodes>`
876
        of that Composition.  If True, they may be `INPUT <NodeRole.INPUT>` or `INTERNAL <NodeRole.INTERNAL>`
877
        `Nodes <Composition_Nodes>` of `nested Composition <Composition_Nested>` (see `allow probes
878
        <ControlMechanism_Allow_Probes>` and `Composition_Probes` for additional information).
879

880
    outcome_input_ports_option : , SEPARATE, COMBINE, or CONCATENATE
881
        determines how items specified in `monitor_for_control <ControlMechanism.monitor_for_control>` project to
882
        the ControlMechanism if no `objective_mechanism <ControlMechanism.objective_mechanism>` is specified.  If
883
        *SEPARATE* is specified (the default), the `Projection` from each item specified in `monitor_for_control
884
        <ControlMechanism.monitor_for_control>` is assigned its own `InputPort`.  All of the InputPorts are assigned
885
        to a list in the ControlMechanism's `outcome_input_ports <ControlMechanism.outcome_input_ports>` attribute.
886
        If *CONCATENATE* or *COMBINE* is specified, all of the projections are assigned to a single InputPort, named
887
        *OUTCOME*.  If *COMBINE* is specified, the *OUTCOME* InputPort  is assigned `LinearCombination` as its
888
        `function <InputPort.function>`, which sums the `values <Projection.value>` of the projections to it (all of
889
        which must have the same dimension), to produce a single array (this is the default behavior for multiple
890
        Projections to a single InputPort;  see InputPort `function <InputPort.function>`). If *CONCATENATE* is
891
        specified, the *OUTCOME* InputPort is assigned `Concatenate` as its `function <InputPort.function>`, which
892
        concatenates the `values <Projection.value>` of its Projections into a single array of length equal to the sum
893
        of their lengths (which need not be the same).  In both cases, the *OUTCOME* InputPort is assigned as the only
894
        item in the list of `outcome_input_ports <ControlMechanism.outcome_input_ports>`.
895

896
    monitored_output_ports_weights_and_exponents : List[Tuple(float, float)]
897
        each tuple in the list contains the weight and exponent associated with a corresponding OutputPort specified
898
        in `monitor_for_control <ControlMechanism.monitor_for_control>`; if `objective_mechanism
899
        <ControlMechanism.objective_mechanism>` is specified, these are the same as those in the ObjectiveMechanism's
900
        `monitor_weights_and_exponents <ObjectiveMechanism.monitor_weights_and_exponents>` attribute,
901
        and are used by the ObjectiveMechanism's `function <ObjectiveMechanism.function>` to parametrize the
902
        contribution made to its output by each of the values that it monitors (see `ObjectiveMechanism Function
903
        <ObjectiveMechanism_Function>`).
904

905
    COMMENT:
906
    # FIX 11/3/21 DELETED SINCE IT CAN NOW HAVE MANY
907
    input_port : InputPort
908
        the ControlMechanism's `primary InputPort <InputPort_Primary>`, named *OUTCOME*;  this receives a
909
        `MappingProjection` from the *OUTCOME* `OutputPort <ObjectiveMechanism_Output>` of `objective_mechanism
910
        <ControlMechanism.objective_mechanism>` if that is specified; otherwise, it receives MappingProjections
911
        from each of the OutputPorts specified in `monitor_for_control <ControlMechanism.monitor_for_control>`
912
        (see `ControlMechanism_Input` for additional details).
913
    COMMENT
914

915
    outcome_input_ports : ContentAddressableList
916
        list of the ControlMechanism's `InputPorts <InputPort>` that receive `Projections <Projection>` from
917
        either is `objective_mechanism <ControlMechanism.objective_mechanism>` (in which case the list contains
918
        only the ControlMechanism's *OUTCOME* `InputPort <ControlMechanism_Input>`), or the `OutputPorts <OutputPort>`
919
        of the items listed in its `monitor_for_control <ControlMechanism.monitor_for_control>` attribute.
920

921
    outcome : 1d array
922
        an array containing the `value <InputPort.value>` of each of the ControlMechanism's `outcome_input_ports
923
        <ControlMechanism.outcome_input_ports>`.
924

925
    function : TransferFunction : default Linear(slope=1, intercept=0)
926
        determines how the `value <OuputPort.value>`\\s of the `OutputPorts <OutputPort>` specified in the
927
        **monitor_for_control** `argument <ControlMechanism_Monitor_for_Control_Argument>` of the ControlMechanism's
928
        constructor are used to generate its `control_allocation <ControlMechanism.control_allocation>`.
929

930
    default_allocation : number, list or 1d array
931
        determines the default_allocation of any `control_signals <ControlMechanism.control_signals>` for
932
        which the **default_allocation** is not specified in its constructor;  if it is None (not specified)
933
        then the ControlSignal's parameters.allocation.default_value is used. See documentation for
934
        **default_allocation** argument of ControlSignal constructor for additional details.
935

936
    control_allocation : 2d array
937
        each item is the value assigned as the `allocation <ControlSignal.allocation>` for the corresponding
938
        ControlSignal listed in the `control_signals <ControlMechanism.control_signals>` attribute (that is,
939
        it is a list of the values of the `variable <OutputPort.variable>` attributes of the ControlMechanism's
940
        `ControlSignals <ControlSignal>`).
941

942
    control_signals : ContentAddressableList[ControlSignal]
943
        list of the `ControlSignals <ControlSignal>` for the ControlMechanism, including any inherited from a
944
        `Composition` for which it is a `controller <Composition.controller>` (same as ControlMechanism's
945
        `output_ports <Mechanism_Base.output_ports>` attribute); each sends a `ControlProjection`
946
        to the `ParameterPort` for the parameter it controls.
947

948
    compute_reconfiguration_cost : Function, function or method
949
        function used to compute the ControlMechanism's `reconfiguration_cost  <ControlMechanism.reconfiguration_cost>`;
950
        result is a scalar value representing the difference — defined by the function — between the values of the
951
        ControlMechanism's current and last `control_alloction <ControlMechanism.control_allocation>`, that can be
952
        accessed by `reconfiguration_cost <ControlMechanism.reconfiguration_cost>` attribute.
953

954
    reconfiguration_cost : scalar
955
        result of `compute_reconfiguration_cost <ControlMechanism.compute_reconfiguration_cost>` function, that
956
        computes the difference between the values of the ControlMechanism's current and last `control_alloction
957
        <ControlMechanism.control_allocation>`; value is None and is ignored if `compute_reconfiguration_cost
958
        <ControlMechanism.compute_reconfiguration_cost>` has not been specified.
959

960
        .. note::
961
        A ControlMechanism's reconfiguration_cost is not the same as the `adjustment_cost
962
        <ControlSignal.adjustment_cost>` of its ControlSignals (see `Reconfiguration Cost
963
        <ControlMechanism_Reconfiguration_Cost>` for additional details).
964

965
    costs : list
966
        current costs for the ControlMechanism's `control_signals <ControlMechanism.control_signals>`, computed
967
        for each using its `compute_costs <ControlSignal.compute_costs>` method.
968

969
    combine_costs : Function, function or method
970
        function used to combine the `cost <ControlSignal.cost>` of its `control_signals
971
        <ControlMechanism.control_signals>`; result is an array with a scalar value that can be accessed by
972
        `combined_costs <ControlMechanism.combined_costs>`.
973

974
        .. note::
975
          This function is distinct from the `combine_costs_function <ControlSignal.combine_costs_function>` of a
976
          `ControlSignal`.  The latter combines the different `costs <ControlSignal_Costs>` for an individual
977
          ControlSignal to yield its overall `cost <ControlSignal.cost>`; the ControlMechanism's
978
          `combine_costs <ControlMechanism.combine_costs>` function combines those `cost <ControlSignal.cost>`\\s
979
          for its `control_signals <ControlMechanism.control_signals>`.
980

981
    combined_costs : 1d array
982
        result of the ControlMechanism's `combine_costs <ControlMechanism.combine_costs>` function.
983

984
    compute_net_outcome : Function, function or method
985
        function used to combine the values of its `outcome <ControlMechanism.outcome>` and `costs
986
        <ControlMechanism.costs>` attributes;  result is an array with a scalar value that can be accessed
987
        by the the `net_outcome <ControlMechanism.net_outcome>` attribute.
988

989
    net_outcome : 1d array
990
        result of the ControlMechanism's `compute_net_outcome <ControlMechanism.compute_net_outcome>` function.
991

992
    control_projections : List[ControlProjection]
993
        list of `ControlProjections <ControlProjection>` that project from the ControlMechanism's `control_signals
994
        <ControlMechanism.control_signals>`.
995

996
    modulation : str
997
        the default form of modulation used by the ControlMechanism's `ControlSignals <GatingSignal>`,
998
        unless they are `individually specified <ControlSignal_Specification>`.
999

1000
    """
1001

1002
    componentType = "ControlMechanism"
1✔
1003
    controlType = CONTROL # Used as key in specification dictionaries;  can be overridden by subclasses
1✔
1004

1005
    initMethod = INIT_EXECUTE_METHOD_ONLY
1✔
1006

1007
    outputPortTypes = ControlSignal
1✔
1008
    portListAttr = Mechanism_Base.portListAttr.copy()
1✔
1009
    portListAttr.update({ControlSignal:CONTROL_SIGNALS})
1✔
1010

1011
    classPreferenceLevel = PreferenceLevel.TYPE
1✔
1012
    # Any preferences specified below will override those specified in TYPE_DEFAULT_PREFERENCES
1013
    # Note: only need to specify setting;  level will be assigned to TYPE automatically
1014
    # classPreferences = {
1015
    #     PREFERENCE_SET_NAME: 'ControlMechanismClassPreferences',
1016
    #     PREFERENCE_KEYWORD<pref>: <setting>...}
1017

1018
    class Parameters(ModulatoryMechanism_Base.Parameters):
1✔
1019
        """
1020
            Attributes
1021
            ----------
1022

1023
                variable
1024
                    see `variable <ControlMechanism.variable>`
1025

1026
                    :default value: numpy.array([[1.]])
1027
                    :type: ``numpy.ndarray``
1028

1029
                value
1030
                    see `value <ControlMechanism.value>`
1031

1032
                    :default value: numpy.array([1.])
1033
                    :type: ``numpy.ndarray``
1034

1035
                combine_costs
1036
                    see `combine_costs <ControlMechanism.combine_costs>`
1037

1038
                    :default value: sum
1039
                    :type: ``types.FunctionType``
1040

1041
                compute_net_outcome
1042
                    see `compute_net_outcome <ControlMechanism.compute_net_outcome>`
1043

1044
                    :default value: lambda outcome, cost: outcome - cost
1045
                    :type: ``types.FunctionType``
1046

1047
                compute_reconfiguration_cost
1048
                    see `compute_reconfiguration_cost <ControlMechanism.compute_reconfiguration_cost>`
1049

1050
                    :default value: None
1051
                    :type:
1052

1053
                control_allocation
1054
                    see `control_allocation <ControlMechanism.control_signal_costs>`
1055

1056
                    :default value: None
1057
                    :type:
1058
                    :read only: True
1059

1060
                control_signal_costs
1061
                    see `control_signal_costs <ControlMechanism.control_signal_costs>`
1062

1063
                    :default value: None
1064
                    :type:
1065
                    :read only: True
1066

1067
                costs
1068
                    see `costs <ControlMechanism.costs>`
1069

1070
                    :default value: None
1071
                    :type:
1072
                    :read only: True
1073

1074
                input_ports
1075
                    see `input_ports <Mechanism_Base.input_ports>`
1076

1077
                    :default value: [`OUTCOME`]
1078
                    :type: ``list``
1079
                    :read only: True
1080

1081
                modulation
1082
                    see `modulation <ControlMechanism.modulation>`
1083

1084
                    :default value: `MULTIPLICATIVE_PARAM`
1085
                    :type: ``str``
1086

1087
                monitor_for_control
1088
                    see `monitor_for_control <ControlMechanism.monitor_for_control>`
1089

1090
                    :default value: [`OUTCOME`]
1091
                    :type: ``list``
1092
                    :read only: True
1093

1094
                outcome_input_ports_option
1095
                    see `outcome_input_ports_option <ControlMechanism.monitor_for_control>`
1096

1097
                    :default value: SEPARATE
1098
                    :type: ``str``
1099
                    :read only: True
1100

1101
                net_outcome
1102
                    see `net_outcome <ControlMechanism.net_outcome>`
1103

1104
                    :default value: None
1105
                    :type:
1106
                    :read only: True
1107

1108
                objective_mechanism
1109
                    see `objective_mechanism <ControlMechanism.objective_mechanism>`
1110

1111
                    :default value: None
1112
                    :type:
1113

1114
                outcome
1115
                    see `outcome <ControlMechanism.outcome>`
1116

1117
                    :default value: None
1118
                    :type:
1119
                    :read only: True
1120

1121
                outcome_input_ports
1122
                    see `outcome_input_ports <ControlMechanism.outcome_input_ports>`
1123

1124
                    :default value: None
1125
                    :type:  ``list``
1126

1127
                output_ports
1128
                    see `output_ports <Mechanism_Base.output_ports>`
1129

1130
                    :default value: None
1131
                    :type:
1132
                    :read only: True
1133

1134
                reconfiguration_cost
1135
                    see `reconfiguration_cost <ControlMechanism_Reconfiguration_Cost>`
1136

1137
                    :default value: None
1138
                    :type:
1139
                    :read only: True
1140

1141
        """
1142
        # This must be a list, as there may be more than one (e.g., one per control_signal)
1143
        variable = Parameter(np.array([[defaultControlAllocation]]), pnl_internal=True,
1✔
1144
                             constructor_argument='default_variable')
1145
        value = Parameter(np.array([defaultControlAllocation]), pnl_internal=True)
1✔
1146
        control_allocation = Parameter(np.array([defaultControlAllocation]),
1✔
1147
                                       read_only=True,
1148
                                       getter=_control_allocation_getter,
1149
                                       constructor_argument='default_allocation')
1150
        combine_costs = Parameter(np.sum, stateful=False, loggable=False)
1✔
1151
        costs = Parameter(None, read_only=True, getter=_control_mechanism_costs_getter)
1✔
1152
        control_signal_costs = Parameter(None, read_only=True, pnl_internal=True)
1✔
1153
        compute_reconfiguration_cost = Parameter(None, stateful=False, loggable=False)
1✔
1154
        reconfiguration_cost = Parameter(None, read_only=True)
1✔
1155
        outcome = Parameter(None, read_only=True, getter=_outcome_getter, pnl_internal=True)
1✔
1156
        compute_net_outcome = Parameter(lambda outcome, cost: outcome - cost, stateful=False, loggable=False)
1✔
1157
        net_outcome = Parameter(
1✔
1158
            None,
1159
            read_only=True,
1160
            getter=_net_outcome_getter,
1161
            pnl_internal=True
1162
        )
1163
        simulation_ids = Parameter([], user=False, pnl_internal=True)
1✔
1164
        modulation = Parameter(MULTIPLICATIVE, pnl_internal=True)
1✔
1165

1166
        objective_mechanism = Parameter(None, stateful=False, loggable=False, structural=True)
1✔
1167
        outcome_input_ports_option = Parameter(SEPARATE, stateful=False, loggable=False, structural=True)
1✔
1168
        outcome_input_ports = Parameter(None, reference=True, stateful=False, loggable=False, read_only=True)
1✔
1169

1170
        input_ports = Parameter(
1✔
1171
            [OUTCOME],
1172
            stateful=False,
1173
            loggable=False,
1174
            read_only=True,
1175
            structural=True,
1176
            parse_spec=True,
1177
        )
1178

1179
        monitor_for_control = Parameter(
1✔
1180
            [],
1181
            stateful=False,
1182
            loggable=False,
1183
            read_only=True,
1184
            structural=True,
1185
        )
1186

1187
        output_ports = Parameter(
1✔
1188
            None,
1189
            stateful=False,
1190
            loggable=False,
1191
            read_only=True,
1192
            structural=True,
1193
            parse_spec=True,
1194
            aliases=[CONTROL, CONTROL_SIGNALS],
1195
            constructor_argument=CONTROL
1196
        )
1197
        function = Parameter(Identity, stateful=False, loggable=False)
1✔
1198

1199
        def _parse_output_ports(self, output_ports):
1✔
1200
            def is_2tuple(o):
1✔
1201
                return isinstance(o, tuple) and len(o) == 2
1✔
1202

1203
            if output_ports is None:
1✔
1204
                return output_ports
1✔
1205

1206
            output_ports = convert_to_list(output_ports)
1✔
1207

1208
            for i in range(len(output_ports)):
1✔
1209
                # handle 2-item tuple
1210
                if is_2tuple(output_ports[i]):
1✔
1211

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

1217
                    output_ports[i] = {
1✔
1218
                        NAME: output_ports[i][0],
1219
                        MECHANISM: output_ports[i][1]
1220
                    }
1221
                # handle dict of form {PROJECTIONS: <2 item tuple>, <param1>: <value1>, ...}
1222
                elif isinstance(output_ports[i], dict):
1✔
1223
                    for PROJ_SPEC_KEYWORD in {PROJECTIONS, self._owner.controlType}:
1✔
1224
                        if (PROJ_SPEC_KEYWORD in output_ports[i] and is_2tuple(output_ports[i][PROJ_SPEC_KEYWORD])):
1✔
1225
                            tuple_spec = output_ports[i].pop(PROJ_SPEC_KEYWORD)
1✔
1226
                            output_ports[i].update({
1✔
1227
                                NAME: tuple_spec[0],
1228
                                MECHANISM: tuple_spec[1]})
1229
                    assert True
1✔
1230

1231
            return output_ports
1✔
1232

1233
        def _parse_control_allocation(self, control_allocation):
1✔
1234
            if (
1!
1235
                isinstance(control_allocation, (float, int))
1236
                or (
1237
                    isinstance(control_allocation, np.ndarray)
1238
                    and control_allocation.dtype.kind in 'iufc'  # numeric type
1239
                )
1240
            ):
1241
                control_allocation = np.atleast_1d(control_allocation)
1✔
1242

1243
            return control_allocation
1✔
1244

1245
        def _parse_monitor_for_control(self, monitor_for_control):
1✔
1246
            if monitor_for_control is not None:
1!
1247
                monitor_for_control = convert_to_list(monitor_for_control)
1✔
1248

1249
            return monitor_for_control
1✔
1250

1251
        def _validate_input_ports(self, input_ports):
1✔
1252
            if input_ports is None:
1!
1253
                return
×
1254

1255
        def _validate_output_ports(self, control):
1✔
1256
            if control is None:
1✔
1257
                return
1✔
1258

1259
            if not isinstance(control, list):
1!
1260
                return 'should have been converted to a list'
×
1261

1262
            port_types = self._owner.outputPortTypes
1✔
1263
            for ctl_spec in control:
1✔
1264
                ctl_spec = _parse_port_spec(port_type=port_types, owner=self._owner, port_spec=ctl_spec,
1✔
1265
                                            context=Context(string='ControlMechanism._validate_input_ports'))
1266
                if not (isinstance(ctl_spec, port_types)
1!
1267
                        or (isinstance(ctl_spec, dict) and ctl_spec[PORT_TYPE] == port_types)):
1268
                    return 'invalid port specification'
×
1269

1270
            # FIX 5/28/20:
1271
            # TODO: uncomment this method or remove this block entirely.
1272
            # This validation check was never being run due to an
1273
            # unintentionally suppressed exception. Why is the default
1274
            # specification ([OUTCOME]) invalid according to this
1275
            # method?
1276
            # validate_monitored_port_spec(self._owner, input_ports)
1277

1278
    @check_user_specified
1✔
1279
    @beartype
1✔
1280
    def __init__(self,
1✔
1281
                 default_variable=None,
1282
                 input_shapes=None,
1283
                 monitor_for_control: Optional[Union[Iterable, Mechanism, OutputPort]] = None,
1284
                 objective_mechanism=None,
1285
                 allow_probes: bool = False,
1286
                 outcome_input_ports_option: Optional[Literal['concatenate', 'combine', 'separate']] = None,
1287
                 function=None,
1288
                 default_allocation: Optional[Union[int, float, list, np.ndarray]] = None,
1289
                 control: Optional[Union[Iterable, ParameterPort, InputPort, OutputPort, ControlSignal]] = None,
1290
                 modulation: Optional[str] = None,
1291
                 combine_costs: Optional[Callable] = None,
1292
                 compute_reconfiguration_cost: Optional[Callable] = None,
1293
                 compute_net_outcome=None,
1294
                 params=None,
1295
                 name=None,
1296
                 prefs: Optional[ValidPrefSet] = None,
1297
                 **kwargs
1298
                 ):
1299

1300
        self.allow_probes = allow_probes
1✔
1301
        self._sim_counts = {}
1✔
1302

1303
        # For backward compatibility:
1304
        if kwargs:
1✔
1305
            if MONITOR_FOR_MODULATION in kwargs:
1✔
1306
                args = kwargs.pop(MONITOR_FOR_MODULATION)
1✔
1307
                if args:
1!
1308
                    try:
1✔
1309
                        monitor_for_control.extend(convert_to_list(args))
1✔
1310
                    except AttributeError:
1✔
1311
                        monitor_for_control = convert_to_list(args)
1✔
1312

1313
            # Only allow one of CONTROL, MODULATORY_SIGNALS OR CONTROL_SIGNALS to be specified
1314
            # These are synonyms, but allowing several to be specified and trying to combine the specifications
1315
            # can cause problems if different forms of specification are used to refer to the same Component(s)
1316
            control_specified = f"'{CONTROL}'" if control else ''
1✔
1317
            modulatory_signals_specified = ''
1✔
1318
            if MODULATORY_SIGNALS in kwargs:
1!
1319
                args = kwargs.pop(MODULATORY_SIGNALS)
×
1320
                if args:
×
1321
                    if control:
×
1322
                        modulatory_signals_specified = f"'{MODULATORY_SIGNALS}'"
×
1323
                        raise ControlMechanismError(f"Both {control_specified} and {modulatory_signals_specified} "
1324
                                                    f"arguments have been specified for {self.name}. "
1325
                                                    f"These are synonyms, but only one should be used to avoid "
1326
                                                    f"creating unnecessary and/or duplicated Components.")
1327
                    control = convert_to_list(args)
×
1328
            if CONTROL_SIGNALS in kwargs:
1✔
1329
                args = kwargs.pop(CONTROL_SIGNALS)
1✔
1330
                if args:
1✔
1331
                    if control:
1!
1332
                        if control_specified and modulatory_signals_specified:
×
1333
                            prev_spec = ", ".join([control_specified, modulatory_signals_specified])
×
1334
                        else:
1335
                            prev_spec = control_specified or modulatory_signals_specified
×
1336
                        raise ControlMechanismError(f"Both {prev_spec} and '{CONTROL_SIGNALS}' arguments "
1337
                                                    f"have been specified for {self.name}. "
1338
                                                    f"These are synonyms, but only one should be used to avoid "
1339
                                                    f"creating unnecessary and/or duplicated Components.")
1340
                    control = convert_to_list(args)
1✔
1341
            if 'default_control_allocation' in kwargs:
1✔
1342
                raise ControlMechanismError(f"'default_allocation' should be used in place of "
1343
                                            f"'default_control_allocation'.")
1344

1345
        super(ControlMechanism, self).__init__(
1✔
1346
            default_variable=default_variable,
1347
            input_shapes=input_shapes,
1348
            modulation=modulation,
1349
            params=params,
1350
            name=name,
1351
            function=function,
1352
            monitor_for_control=monitor_for_control,
1353
            outcome_input_ports_option=outcome_input_ports_option,
1354
            control=control,
1355
            objective_mechanism=objective_mechanism,
1356
            default_allocation=default_allocation,
1357
            combine_costs=combine_costs,
1358
            compute_net_outcome=compute_net_outcome,
1359
            compute_reconfiguration_cost=compute_reconfiguration_cost,
1360
            prefs=prefs,
1361
            **kwargs
1362
        )
1363

1364
    def _validate_params(self, request_set, target_set=None, context=None):
1✔
1365
        """Validate monitor_for_control, objective_mechanism, CONTROL_SIGNALS and GATING_SIGNALS
1366

1367
        """
1368
        from psyneulink.core.components.mechanisms.processing.objectivemechanism import ObjectiveMechanism
1✔
1369

1370
        super(ControlMechanism, self)._validate_params(request_set=request_set,
1✔
1371
                                                       target_set=target_set,
1372
                                                       context=context)
1373

1374
        monitor_for_control = self.defaults.monitor_for_control
1✔
1375
        if monitor_for_control is not None:
1!
1376
            for item in monitor_for_control:
1✔
1377
                if item is ObjectiveMechanism or isinstance(item, ObjectiveMechanism):
1✔
1378
                    raise ControlMechanismError(
1379
                        f"The '{MONITOR_FOR_CONTROL}' arg of '{self.name}' contains a specification for"
1380
                        f" an {ObjectiveMechanism.componentType} ({monitor_for_control}).  "
1381
                        f"This should be specified in its '{OBJECTIVE_MECHANISM}' argument."
1382
                    )
1383

1384
        if (OBJECTIVE_MECHANISM in target_set and
1✔
1385
                target_set[OBJECTIVE_MECHANISM] is not None
1386
                and target_set[OBJECTIVE_MECHANISM] is not False):
1387

1388
            if isinstance(target_set[OBJECTIVE_MECHANISM], list):
1✔
1389

1390
                obj_mech_spec_list = target_set[OBJECTIVE_MECHANISM]
1✔
1391

1392
                # Check if there is any ObjectiveMechanism is in the list;
1393
                #    incorrect but possibly forgivable mis-specification --
1394
                #    if an ObjectiveMechanism is specified, it should be "exposed" (i.e., not in a list)
1395
                if any(isinstance(spec, ObjectiveMechanism) for spec in obj_mech_spec_list):
1!
1396
                    # If an ObjectiveMechanism is the *only* item in the list, forgive the mis-spsecification and use it
1397
                    if len(obj_mech_spec_list)==1 and isinstance(obj_mech_spec_list[0], ObjectiveMechanism):
×
1398
                        if self.verbosePref:
×
1399
                            warnings.warn("Specification of {} arg for {} is an {} in a list; it will be used, "
×
1400
                                                        "but, for future reference, it should not be in a list".
1401
                                                        format(OBJECTIVE_MECHANISM,
1402
                                                               ObjectiveMechanism.__name__,
1403
                                                               self.name))
1404
                        target_set[OBJECTIVE_MECHANISM] = target_set[OBJECTIVE_MECHANISM][0]
×
1405
                    else:
1406
                        raise ControlMechanismError("Ambigusous specification of {} arg for {}; "
1407
                                                    " it is in a list with other items ({})".
1408
                                                    format(OBJECTIVE_MECHANISM, self.name, obj_mech_spec_list))
1409
                else:
1410
                    validate_monitored_port_spec(self, obj_mech_spec_list)
1✔
1411

1412
            if not isinstance(target_set[OBJECTIVE_MECHANISM], (ObjectiveMechanism, list, bool)):
1✔
1413
                raise ControlMechanismError(f"Specification of {OBJECTIVE_MECHANISM} arg for '{self.name}' "
1414
                                            f"({target_set[OBJECTIVE_MECHANISM].name}) must be an "
1415
                                            f"{ObjectiveMechanism.componentType} or a list of Mechanisms and/or "
1416
                                            f"OutputPorts to be monitored for control.")
1417

1418
    # FIX: 2/11/23 SHOULDN'T THIS BE PUT ON COMPOSITION NOW?
1419
    # IMPLEMENTATION NOTE:  THIS SHOULD BE MOVED TO COMPOSITION ONCE THAT IS IMPLEMENTED
1420
    def _instantiate_objective_mechanism(self, input_ports=None, context=None):
1✔
1421
        """
1422
        # FIX: ??THIS SHOULD BE IN OR MOVED TO ObjectiveMechanism
1423
        Assign InputPort to ObjectiveMechanism for each OutputPort to be monitored;
1424
            uses _instantiate_monitoring_input_port and _instantiate_control_mechanism_input_port to do so.
1425
            For each item in self.monitored_output_ports:
1426
            - if it is a OutputPort, call _instantiate_monitoring_input_port()
1427
            - if it is a Mechanism, call _instantiate_monitoring_input_port for relevant Mechanism_Base.output_ports
1428
                (determined by whether it is a `TERMINAL` Mechanism and/or MonitoredOutputPortsOption specification)
1429
            - each InputPort is assigned a name with the following format:
1430
                '<name of Mechanism that owns the monitoredOutputPort>_<name of monitoredOutputPort>_Monitor'
1431

1432
        Notes:
1433
        * self.monitored_output_ports is a list, each item of which is a Mechanism_Base.output_port from which a
1434
          Projection will be instantiated to a corresponding InputPort of the ControlMechanism
1435
        * self.input_ports is the usual ordered dict of ports,
1436
            each of which receives a Projection from a corresponding OutputPort in self.monitored_output_ports
1437
        """
1438
        from psyneulink.core.components.projections.pathway.mappingprojection import MappingProjection
1✔
1439
        from psyneulink.core.components.mechanisms.processing.objectivemechanism import \
1✔
1440
            ObjectiveMechanism, ObjectiveMechanismError
1441
        from psyneulink.core.components.ports.inputport import EXPONENT_INDEX, WEIGHT_INDEX
1✔
1442
        from psyneulink.core.components.functions.function import FunctionError
1✔
1443

1444
        # GET OutputPorts to Monitor (to specify as or add to ObjectiveMechanism's monitored_output_ports attribute
1445

1446
        # FIX: 11/3/21:  put OUTCOME InputPort at the end rather than the beginning
1447
        #                THEN SEE ALL INSTANCES IN COMMENTS OF: "NEED TO MODIFY ONCE OUTCOME InputPorts ARE MOVED"
1448
        # Other input_ports are those passed into this method, that are presumed to be for other purposes
1449
        #   (e.g., used by OptimizationControlMechanism for representing state_features as inputs)
1450
        #   those are appended after the OUTCOME InputPort # FIX <- change to prepend when refactored
1451
        other_input_ports = input_ports or []
1✔
1452
        monitored_output_ports = []
1✔
1453

1454
        monitor_for_control = self.monitor_for_control or []
1✔
1455
        if not isinstance(monitor_for_control, list):
1!
1456
            monitor_for_control = [monitor_for_control]
×
1457

1458
        # If objective_mechanism arg is used to specify OutputPorts to be monitored (legacy feature)
1459
        #    move them to monitor_for_control
1460
        if isinstance(self.objective_mechanism, list):
1✔
1461
            monitor_for_control.extend(self.objective_mechanism)
1✔
1462

1463
        # Add items in monitor_for_control to monitored_output_ports
1464
        for i, item in enumerate(monitor_for_control):
1✔
1465
            # If it is already in the list received from System, ignore
1466
            if item in monitored_output_ports:
1!
1467
                # NOTE: this can happen if ControlMechanisms is being constructed by System
1468
                #       which passed its monitor_for_control specification
1469
                continue
×
1470
            monitored_output_ports.extend([item])
1✔
1471

1472
        # INSTANTIATE ObjectiveMechanism
1473
        # If *objective_mechanism* argument is an ObjectiveMechanism, add monitored_output_ports to it
1474
        if isinstance(self.objective_mechanism, ObjectiveMechanism):
1✔
1475
            if monitored_output_ports:
1✔
1476
                self.objective_mechanism.add_to_monitor(monitor_specs=monitored_output_ports,
1✔
1477
                                                        context=context)
1478
        # Otherwise, instantiate ObjectiveMechanism with list of ports in monitored_output_ports
1479
        else:
1480
            try:
1✔
1481
                self.objective_mechanism = ObjectiveMechanism(monitor=monitored_output_ports,
1✔
1482
                                                               function=LinearCombination(operation=PRODUCT),
1483
                                                               name=self.name + '_ObjectiveMechanism')
1484
            except (ObjectiveMechanismError, FunctionError) as e:
×
1485
                raise ObjectiveMechanismError(f"Error creating {OBJECTIVE_MECHANISM} for {self.name}: {e}")
1486

1487
        self.objective_mechanism.control_mechanism = self
1✔
1488

1489
        # Print monitored_output_ports
1490
        if self.prefs.verbosePref:
1!
1491
            print("{0} monitoring:".format(self.name))
×
1492
            for port in self.monitored_output_ports:
×
1493
                weight = self.monitored_output_ports_weights_and_exponents[
×
1494
                                                         self.monitored_output_ports.index(port)][WEIGHT_INDEX]
1495
                exponent = self.monitored_output_ports_weights_and_exponents[
×
1496
                                                         self.monitored_output_ports.index(port)][EXPONENT_INDEX]
1497
                print(f"\t{weight} (exp: {weight}; wt: {exponent})")
×
1498

1499

1500
        # INSTANTIATE OUTCOME InputPort on ControlMechanism that receives projection from ObjectiveMechanism
1501

1502
        # Get size of ObjectiveMechanism's OUTCOME OutputPort, and then append sizes of other any InputPorts passed in
1503
        outcome_input_port_size = self.objective_mechanism.output_ports[OUTCOME].value.size
1✔
1504
        outcome_input_port = {INPUT_SHAPES:outcome_input_port_size,
1✔
1505
                              NAME:OUTCOME,
1506
                              PARAMS:{INTERNAL_ONLY:True}}
1507
        other_input_port_value_sizes, _  = self._handle_arg_input_ports(other_input_ports)
1✔
1508
        input_port_value_sizes = [outcome_input_port_size] + other_input_port_value_sizes
1✔
1509
        input_ports = [outcome_input_port] + other_input_ports
1✔
1510
        super()._instantiate_input_ports(context=context,
1✔
1511
                                         input_ports=input_ports,
1512
                                         reference_value=input_port_value_sizes)
1513

1514
        # Assign OUTCOME InputPort to ControlMechanism's list of outcome_input_ports (in this case, it is the only one)
1515
        self.outcome_input_ports.append(self.input_ports[OUTCOME])
1✔
1516

1517
        # FIX: 11/3/21: ISN'T THIS DONE IN super()_instantiate_input_ports BASED ON OUTCOME InputPort specification?
1518
        #               (or shouldn't it be?)  PRESUMABLY THE ONES FOR other_input_ports ARE
1519
        # INSTANTIATE MappingProjection from ObjectiveMechanism to ControlMechanism
1520
        projection_from_objective = MappingProjection(sender=self.objective_mechanism,
1✔
1521
                                                      receiver=self.input_ports[OUTCOME],
1522
                                                      matrix=AUTO_ASSIGN_MATRIX,
1523
                                                      context=context)
1524

1525
        # CONFIGURE FOR ASSIGNMENT TO COMPOSITION
1526

1527
        # Ensure that ObjectiveMechanism's input_ports are not assigned projections from a Composition's input_CIM
1528
        for input_port in self.objective_mechanism.input_ports:
1✔
1529
            input_port.internal_only = True
1✔
1530

1531
        # Flag ObjectiveMechanism and its Projection to ControlMechanism for inclusion in Composition
1532
        from psyneulink.core.compositions.composition import NodeRole
1✔
1533
        self.aux_components.append((self.objective_mechanism, NodeRole.CONTROL_OBJECTIVE))
1✔
1534
        self.aux_components.append(projection_from_objective)
1✔
1535

1536
        # ASSIGN ATTRIBUTES
1537

1538
        self._objective_projection = projection_from_objective
1✔
1539
        self.parameters.monitor_for_control._set(self.monitored_output_ports, context)
1✔
1540

1541
    def _instantiate_input_ports(self, input_ports=None, context=None):
1✔
1542
        """Instantiate input_ports for items being monitored and evaluated, and ObjectiveMechanism if specified
1543

1544
        If **objective_mechanism** is specified:
1545
          - instantiate ObjectiveMechanism, which also instantiates an OUTCOME InputPort
1546
            and a MappingProjection to it from the ObjectiveMechanisms OUTCOME OutputPort
1547

1548
        If **monitor_for_control** is specified:
1549
          - it is used to construct an InputPort from each sender specified in it,
1550
            and a corresponding MappingProjection from the sender to that InputPort;
1551
          - each InputPort is named using an uppercase version of the sender's name
1552

1553
        If nothing is specified, a default OUTCOME InputPort is instantiated with no projections to it
1554
        """
1555

1556
        other_input_ports = input_ports or []
1✔
1557

1558
        self.parameters.outcome_input_ports.set(ContentAddressableList(component_type=OutputPort),
1✔
1559
                                                override=True)
1560

1561
        # If ObjectiveMechanism is specified, instantiate it and OUTCOME InputPort that receives projection from it
1562
        if self.objective_mechanism:
1✔
1563
            # This instantiates an OUTCOME InputPort sized to match the ObjectiveMechanism's OUTCOME OutputPort
1564
            # Note: in this case, any items specified in monitor_for_control are passed to the **monitor** argument
1565
            #       of the objective_mechanism's constructor
1566
            self._instantiate_objective_mechanism(input_ports, context=context)
1✔
1567

1568
        # FIX: CONSOLIDATE THIS WITH SIMILAR HANDLING IN _instantiate_objective_mechanism AND ELSE BELOW
1569
        # If no ObjectiveMechanism is specified, but items to monitor are specified,
1570
        #    assign an outcome_input_port for each item specified
1571
        elif self.monitor_for_control:
1✔
1572

1573
            # Get outcome_input_port_specs without including specifications of Projections to them, as those need to
1574
            #     be constructed and specified as aux_components (below) for validation and activation by Composition
1575
            outcome_input_port_specs, outcome_value_sizes, projection_specs \
1✔
1576
                = self._parse_monitor_for_control_input_ports(context)
1577

1578
            # Get sizes of input_ports passed in (that are presumably used for other purposes;
1579
            #   e.g., ones used by OptimizationControlMechanism for simulated inputs or state_features)
1580
            other_input_port_value_sizes  = self._handle_arg_input_ports(other_input_ports)[0]
1✔
1581

1582
            # Construct full list of InputPort specifications and sizes
1583
            input_ports = outcome_input_port_specs + other_input_ports
1✔
1584
            input_port_value_sizes = outcome_value_sizes + other_input_port_value_sizes
1✔
1585
            super()._instantiate_input_ports(context=context,
1✔
1586
                                             input_ports=input_ports,
1587
                                             reference_value=input_port_value_sizes)
1588
            self.outcome_input_ports.extend(self.input_ports[:len(outcome_input_port_specs)])
1✔
1589

1590
            # Instantiate Projections to outcome_input_ports from items specified in monitor_for_control
1591
            #   (list of which were placed in self.aux_components by _parse_monitor_for_control_input_ports)
1592
            option = self.outcome_input_ports_option
1✔
1593
            from psyneulink.core.components.projections.pathway.mappingprojection import MappingProjection
1✔
1594
            from psyneulink.core.components.mechanisms.processing.objectivemechanism import _parse_monitor_specs
1✔
1595

1596
            for i in range(len(projection_specs)):
1✔
1597
                if option == SEPARATE:
1✔
1598
                    # Each outcome_input_port get its own Projection
1599
                    outcome_port_index = i
1✔
1600
                else:
1601
                    # The single outcome_input_port gets all the Projections
1602
                    outcome_port_index = 0
1✔
1603

1604
        # Nothing has been specified, so just instantiate the default OUTCOME InputPort with any input_ports passed in
1605
        else:
1606
            # Get name of default input_port (specified in self.input_ports) if it is not 'OUTCOME'
1607
            default_input_port_name = self.input_ports[0]
1✔
1608
            if default_input_port_name != OUTCOME:
1!
NEW
1609
                if isinstance(default_input_port_name, InputPort):
×
NEW
1610
                    default_input_port_name = default_input_port_name.name.strip(' [Deferred Init]')
×
1611
            other_input_port_value_sizes  = self._handle_arg_input_ports(other_input_ports)[0]
1✔
1612
            # Construct full list of InputPort specifications and sizes
1613
            default_input_port_shape = self._handle_input_shapes(self.input_shapes, self.variable).tolist()
1✔
1614
            input_port_value_sizes = convert_all_elements_to_np_array(default_input_port_shape
1✔
1615
                                                                      + other_input_port_value_sizes)
1616
            input_ports = self.input_ports + other_input_ports
1✔
1617
            super()._instantiate_input_ports(context=context,
1✔
1618
                                             input_ports=input_ports,
1619
                                             reference_value=input_port_value_sizes)
1620
            self.outcome_input_ports.append(self.input_ports[default_input_port_name])
1✔
1621

1622
    def _parse_monitor_for_control_input_ports(self, context):
1✔
1623
        """Get outcome_input_port specification dictionaries for items specified in monitor_for_control.
1624

1625
        Note:  leave Projections unspecified, as they need to be added to self.aux_components
1626
               for validation and activation by Composition
1627

1628
        Return port specification dictionaries (*without* Projections to them specified), their value sizes,
1629
        and monitored ports (to be used as Projection specifications by _instantiate_input_ports)
1630
        """
1631

1632
        # FIX: 11/3/21 - MOVE _parse_monitor_specs TO HERE FROM ObjectiveMechanism
1633
        from psyneulink.core.components.mechanisms.processing.objectivemechanism import _parse_monitor_specs
1✔
1634

1635
        monitored_ports = _parse_monitor_specs(self.monitor_for_control)
1✔
1636
        port_value_sizes = self._handle_arg_input_ports(self.monitor_for_control)[0]
1✔
1637
        outcome_input_ports_option = self.outcome_input_ports_option
1✔
1638
        outcome_input_port_specs = []
1✔
1639

1640
        # SEPARATE outcome_input_ports OPTION:
1641
        #     Assign separate outcome_input_ports for each item in monitored_ports
1642
        if outcome_input_ports_option == SEPARATE:
1✔
1643

1644
            # Construct port specification to assign its name
1645
            for monitored_port in monitored_ports:
1✔
1646
                # Parse port_spec first (e.g., in case it is a (spec, feedback) tuple)
1647
                port_spec = _parse_port_spec(InputPort, self, port_spec = monitored_port)
1✔
1648
                port = port_spec['params']['projections'][0][0]
1✔
1649
                name = port.name
1✔
1650
                if isinstance(port, OutputPort):
1!
1651
                    name = f"{port.owner.name}[{name.upper()}]"
1✔
1652
                name = 'MONITOR ' + name
1✔
1653
                port_spec[NAME] = name
1✔
1654
                outcome_input_port_specs.append(port_spec)
1✔
1655
            # Return list of outcome_input_port specifications (and their sizes) for each monitored item
1656

1657
        # SINGLE outcome_input_port OPTIONS:
1658
        #     Either combine or concatenate inputs from all items specified in monitor_for_control
1659
        #     as input to a single outcome_input_port
1660
        else:
1661

1662
            if outcome_input_ports_option == CONCATENATE:
1!
1663
                function = Concatenate
1✔
1664

1665
            elif outcome_input_ports_option == COMBINE:
×
1666
                function = LinearCombination
×
1667

1668
            else:
1669
                assert False, f"PROGRAM ERROR:  Unrecognized option ({outcome_input_ports_option}) passed " \
1670
                              f"to ControlMechanism._parse_monitor_for_control_input_ports() for {self.name}"
1671

1672
            # Needs to be a list to be combined with other input_ports in _instantiate_input_ports
1673
            port_value_sizes = [function().function(port_value_sizes)]
1✔
1674
            # Return single outcome_input_port specification
1675
            port_spec = _parse_port_spec(InputPort, self, port_spec=monitored_ports, name='OUTCOME')
1✔
1676
            port_spec[FUNCTION] = function
1✔
1677
            port_spec[VALUE] = port_value_sizes[0]
1✔
1678
            port_spec[REFERENCE_VALUE] = port_value_sizes[0]  # Get reference_value for just this port
1✔
1679
            outcome_input_port_specs.append(port_spec)
1✔
1680

1681
        return outcome_input_port_specs, port_value_sizes, monitored_ports
1✔
1682

1683
    def _validate_monitor_for_control(self, nodes):
1✔
1684
        """Ensure all of the Components being monitored for control are in the Composition being controlled
1685
        If monitor_for_control is specified as an ObjectiveMechanism, warn and move to objective_mecahnism arg
1686
        """
1687
        from psyneulink.core.components.ports.port import Port
1✔
1688
        invalid_outcome_specs = [item for item in self.monitor_for_control
1✔
1689
                                 if ((isinstance(item, Mechanism)
1690
                                      and item not in nodes)
1691
                                     or ((isinstance(item, Port)
1692
                                          and item.owner not in nodes)))]
1693
        if invalid_outcome_specs:
1✔
1694
            names = [item.name if isinstance(item, Mechanism) else item.owner.name
1✔
1695
                     for item in invalid_outcome_specs]
1696
            raise ControlMechanismError(f"{self.name} has 'outcome_ouput_ports' that receive "
1697
                                        f"Projections from the following Components that do not "
1698
                                        f"belong to the Composition it controls: {names}.",
1699
                                        names)
1700

1701
    def _instantiate_output_ports(self, context=None):
1✔
1702

1703
    # ---------------------------------------------------
1704
    # FIX 5/23/17: PROJECTIONS AND PARAMS SHOULD BE PASSED BY ASSIGNING TO PORT SPECIFICATION DICT
1705
    # FIX          UPDATE parse_port_spec TO ACCOMODATE (param, ControlSignal) TUPLE
1706
    # FIX          TRACK DOWN WHERE PARAMS ARE BEING HANDED OFF TO ControlProjection
1707
    # FIX                   AND MAKE SURE THEY ARE NOW ADDED TO ControlSignal SPECIFICATION DICT
1708
    # ---------------------------------------------------
1709

1710
        self._register_control_signal_type(context=None)
1✔
1711

1712
        if self.control:
1✔
1713
            self._instantiate_control_signals(context=context)
1✔
1714

1715
        super()._instantiate_output_ports(context=context)
1✔
1716

1717
    def _register_control_signal_type(self, context=None):
1✔
1718
        from psyneulink.core.globals.registry import register_category
1✔
1719
        from psyneulink.core.components.ports.port import Port_Base
1✔
1720

1721
        # Create registry for ControlSignals (to manage names)
1722
        register_category(entry=ControlSignal,
1✔
1723
                          base_class=Port_Base,
1724
                          registry=self._portRegistry,
1725
                          )
1726

1727
    def _instantiate_control_signals(self, context):
1✔
1728
        """Subclasses can override for class-specific implementation (see OptimizationControlMechanism for example)"""
1729

1730
        for i, control_signal in enumerate(self.output_ports):
1✔
1731
            self.control[i] = self._instantiate_control_signal(control_signal, context=context)
1✔
1732

1733
        # For functions, assume that if its value has:
1734
        # - one item, all control_signals should get it (i.e., the default: (OWNER_VALUE, 0));
1735
        # - same number of items as the number of control_signals;
1736
        #     assign each control_signal to the corresponding item of the function's value
1737
        # - a different number of items than number of control_signals,
1738
        #     leave things alone, and allow any errant indices for control_signals to be caught later.
1739
        control_allocation_len = len(self._set_mechanism_value(context))
1✔
1740

1741
        # Assign each ControlSignal's variable_spec to index of ControlMechanism's value
1742
        for i, control_signal in enumerate(self.control):
1✔
1743

1744
            # If number of control_signals is same as number of items in function's value,
1745
            #    assign each ControlSignal to the corresponding item of the function's value
1746
            if len(self.control) == control_allocation_len:
1✔
1747
                control_signal._variable_spec = (OWNER_VALUE, i)
1✔
1748

1749
            if not isinstance(control_signal.owner_value_index, int):
1✔
1750
                assert False, \
1751
                    f"PROGRAM ERROR: The \'owner_value_index\' attribute for {control_signal.name} " \
1752
                        f"of {self.name} ({control_signal.owner_value_index})is not an int."
1753

1754
    def _instantiate_control_signal(self,  control_signal, context=None):
1✔
1755
        """Parse and instantiate ControlSignal (or subclass relevant to ControlMechanism subclass)
1756

1757
        Temporarily assign variable to default allocation value to avoid chicken-and-egg problem:
1758
           value, output_ports and control_signals haven't been expanded yet to accomodate the new
1759
           ControlSignal; reassign control_signal.variable to actual OWNER_VALUE below,
1760
           once value has been expanded
1761
        """
1762

1763
        if self.output_ports is None:
1!
1764
            self.parameters.output_ports._set([], context)
×
1765

1766
        control_signal = self._instantiate_control_signal_type(control_signal, context)
1✔
1767
        control_signal.owner = self
1✔
1768

1769
        self._check_for_duplicates(control_signal, self.control_signals, context)
1✔
1770

1771
        # Update control_signal_costs to accommodate instantiated Projection
1772
        control_signal_costs = self.parameters.control_signal_costs._get(context)
1✔
1773
        try:
1✔
1774
            control_signal_costs = np.append(control_signal_costs, np.zeros((1, 1)), axis=0)
1✔
1775
        except (AttributeError, ValueError):
1✔
1776
            control_signal_costs = np.zeros((1, 1))
1✔
1777
        self.parameters.control_signal_costs._set(control_signal_costs, context)
1✔
1778

1779
        # UPDATE output_ports AND control_projections -------------------------------------------------------------
1780

1781
        # FIX: 9/14/19 - THIS SHOULD BE IMPLEMENTED
1782
        # TBI: For ControlMechanisms that accumulate, starting output must be equal to the initial "previous value"
1783
        # so that modulation that occurs BEFORE the control mechanism executes is computed appropriately
1784
        # if (isinstance(self.function, IntegratorFunction)):
1785
        #     control_signal._intensity = function.initializer
1786

1787
        return control_signal
1✔
1788

1789
    def _instantiate_control_signal_type(self, control_signal_spec, context):
1✔
1790
        """Instantiate actual ControlSignal, or subclass if overridden"""
1791
        from psyneulink.core.components.ports.port import _instantiate_port
1✔
1792
        from psyneulink.core.components.projections.projection import ProjectionError
1✔
1793

1794
        try:
1✔
1795
            # set the default by implicit shape defined by one of the allocation_samples if possible
1796
            try:
1✔
1797
                allocation_parameter_default = control_signal_spec._init_args['allocation_samples'][0]
1✔
1798
            except AttributeError:
1✔
1799
                allocation_parameter_default = control_signal_spec['allocation_samples'][0]
1✔
1800

1801
            # several tests depend on the default value being 1
1802
            # tests/composition/test_control.py::TestControlSpecification::test_deferred_init
1803
            # tests/composition/test_control.py::TestModelBasedOptimizationControlMechanisms::test_evc
1804
            # tests/composition/test_control.py::TestModelBasedOptimizationControlMechanisms::test_laming_validation_specify_control_signals
1805
            # tests/composition/test_control.py::TestModelBasedOptimizationControlMechanisms::test_stateful_mechanism_in_simulation
1806
            allocation_parameter_default = np.ones(np.asarray(allocation_parameter_default).shape)
1✔
1807
        except (KeyError, IndexError, TypeError):
1✔
1808
            # if control allocation is a single value specified from
1809
            # default_variable for example, it should be used here
1810
            # instead of the "global default" defaultControlAllocation
1811
            # FIX: JDC 6/9/23 CHANGE THIS TO self.defaults.control_allocation or self.control_allocation??
1812
            #       ALSO, CONSOLIDATE default_allocation and defaults.control_allocation?? (SEE ABOVE)
1813
            if len(self.defaults.control_allocation) == 1:
1!
1814
                allocation_parameter_default = copy.deepcopy(self.defaults.control_allocation)
1✔
1815
            else:
1816
                allocation_parameter_default = copy.deepcopy(defaultControlAllocation)
×
1817

1818
        control_signal = _instantiate_port(port_type=ControlSignal,
1✔
1819
                                           owner=self,
1820
                                           variable=allocation_parameter_default,
1821
                                           reference_value=allocation_parameter_default,
1822
                                           modulation=self.defaults.modulation,
1823
                                           port_spec=control_signal_spec,
1824
                                           context=context)
1825
        if not type(control_signal) in convert_to_list(self.outputPortTypes):
1✔
1826
            raise ProjectionError(f'{type(control_signal)} inappropriate for {self.name}')
1827
        return control_signal
1✔
1828

1829
    def _set_mechanism_value(self, context):
1✔
1830
        """Set Mechanism's value.
1831
        By default, use value returned by ControlMechanism's function.
1832
        Can be overridden by a subclass if it determines its value in some other way (see OCM for example).
1833
        Note: this is used to determine the number of ControlSignals
1834
        """
1835
        self.defaults.value = convert_all_elements_to_np_array(self.function.parameters.value._get(context))
1✔
1836
        self.parameters.value._set(copy.deepcopy(self.defaults.value), context)
1✔
1837
        return self.defaults.value
1✔
1838

1839
    def _check_for_duplicates(self, control_signal, control_signals, context):
1✔
1840
        """
1841
        Check that control_signal is not a duplicate of one already instantiated for the ControlMechanism
1842

1843
        Can happen if control of parameter is specified in constructor for a Mechanism
1844
            and also in the ControlMechanism's **control** arg
1845

1846
        control_signals arg passed in to allow override by subclasses
1847

1848
        Warn if control_signal shares any ControlProjections with others in control_signals.
1849
        Warn if control_signal is a duplicate of any in control_signals.
1850

1851
        Return True if control_signal is a duplicate
1852
        """
1853
        duplicates = []
1✔
1854
        for existing_ctl_sig in control_signals:
1✔
1855
            # OK if control_signal is one already assigned to ControlMechanism (i.e., let it get processed below);
1856
            # this can happen if it was in deferred_init status and initalized in call to _instantiate_port above.
1857
            if control_signal == existing_ctl_sig:
1✔
1858
                continue
1✔
1859

1860
            # Return if *all* projections from control_signal are identical to ones in an existing control_signal
1861
            for proj in control_signal.efferents:
1✔
1862
                if proj not in existing_ctl_sig.efferents:
1!
1863
                    # A Projection in control_signal is not in this existing one: it is different,
1864
                    #    so break and move on to next existing_mod_sig
1865
                    break
1✔
1866
                warnings.warn(f"{control_signal.name} for {self.name} duplicates another one specified "
×
1867
                              f"({existing_ctl_sig.name}); it will be ignored.")
1868
                return True
×
1869

1870
            # Warn if *any* projections from control_signal are identical to ones in an existing control_signal
1871
            projection_type = existing_ctl_sig.projection_type
1✔
1872
            if any(any(new_p.receiver == existing_p.receiver
1!
1873
                                for existing_p in existing_ctl_sig.efferents) for new_p in control_signal.efferents):
1874
                warnings.warn(f"Specification of {control_signal.name} for {self.name} "
×
1875
                              f"has one or more {projection_type.__name__}s redundant with ones already on "
1876
                              f"an existing {ControlSignal.__name__} ({existing_ctl_sig.name}).")
1877

1878
    def _remove_default_control_signal(self, type: Literal['ControlSignal', 'GatingSignal']):
1✔
1879
        if type == CONTROL_SIGNAL:
1!
1880
            ctl_sig_attribute = self.control_signals
1✔
1881
        elif type == GATING_SIGNAL:
×
1882
            ctl_sig_attribute = self.gating_signals
×
1883
        else:
1884
            assert False, \
1885
                f"PROGRAM ERROR:  bad 'type' arg ({type})passed to " \
1886
                f"{ControlMechanism.__name__}._remove_default_control_signal" \
1887
                f"(should have been caught by typecheck"
1888

1889
        if (len(ctl_sig_attribute) == 1
1✔
1890
                and ctl_sig_attribute[0].name == type + '-0'
1891
                and not ctl_sig_attribute[0].efferents):
1892
            self.remove_ports(ctl_sig_attribute[0])
1✔
1893

1894
    def show(self):
1✔
1895
        """Display the OutputPorts monitored by ControlMechanism's `objective_mechanism
1896
        <ControlMechanism.objective_mechanism>` and the parameters modulated by its `control_signals
1897
        <ControlMechanism.control_signals>`.
1898
        """
1899

1900
        print("\n---------------------------------------------------------")
×
1901

1902
        print("\n{0}".format(self.name))
×
1903
        print("\n\tMonitoring the following Mechanism OutputPorts:")
×
1904
        for port in self.objective_mechanism.input_ports:
×
1905
            for projection in port.path_afferents:
×
1906
                monitored_port = projection.sender
×
1907
                monitored_port_Mech = projection.sender.owner
×
1908
                # ContentAddressableList
1909
                monitored_port_index = self.monitored_output_ports.index(monitored_port)
×
1910

1911
                weight = self.monitored_output_ports_weights_and_exponents[monitored_port_index][0]
×
1912
                exponent = self.monitored_output_ports_weights_and_exponents[monitored_port_index][1]
×
1913

1914
                print("\t\t{0}: {1} (exp: {2}; wt: {3})".
×
1915
                      format(monitored_port_Mech.name, monitored_port.name, weight, exponent))
1916

1917
        try:
×
1918
            if self.control_signals:
×
1919
                print("\n\tControlling the following Mechanism parameters:".format(self.name))
×
1920
                # Sort for consistency of output:
1921
                port_Names_sorted = sorted(self.control_signals.names)
×
1922
                for port_Name in port_Names_sorted:
×
1923
                    for projection in self.control_signals[port_Name].efferents:
×
1924
                        print("\t\t{0}: {1}".format(projection.receiver.owner.name, projection.receiver.name))
×
1925
        except:
×
1926
            pass
×
1927

1928
        try:
×
1929
            if self.gating_signals:
×
1930
                print("\n\tGating the following Ports:".format(self.name))
×
1931
                # Sort for consistency of output:
1932
                port_Names_sorted = sorted(self.gating_signals.names)
×
1933
                for port_Name in port_Names_sorted:
×
1934
                    for projection in self.gating_signals[port_Name].efferents:
×
1935
                        print("\t\t{0}: {1}".format(projection.receiver.owner.name, projection.receiver.name))
×
1936
        except:
×
1937
            pass
×
1938

1939
        print("\n---------------------------------------------------------")
×
1940

1941
    def add_to_monitor(self, monitor_specs, context=None):
1✔
1942
        """Instantiate OutputPorts to be monitored by ControlMechanism's `objective_mechanism
1943
        <ControlMechanism.objective_mechanism>`.
1944

1945
        **monitored_output_ports** can be any of the following:
1946
            - `Mechanism <Mechanism>`;
1947
            - `OutputPort`;
1948
            - `tuple specification <InputPort_Tuple_Specification>`;
1949
            - `Port specification dictionary <InputPort_Specification_Dictionary>`;
1950
            - list with any of the above.
1951
        If any item is a Mechanism, its `primary OutputPort <OutputPort_Primary>` is used.
1952
        OutputPorts must belong to Mechanisms in the same `System` as the ControlMechanism.
1953
        """
1954
        output_ports = self.objective_mechanism.add_to_monitor(monitor_specs=monitor_specs, context=context)
×
1955

1956
    # FIX: 11/15/21 SHOULDN'T THIS BE PUT ON COMPOSITION??
1957
    def _activate_projections_for_compositions(self, composition=None, context=None):
1✔
1958
        """Activate eligible Projections to or from Nodes in Composition.
1959
        If Projection is to or from a node NOT (yet) in the Composition,
1960
        assign it the node's aux_components attribute but do not activate it.
1961
        """
1962
        dependent_projections = set()
1✔
1963

1964
        if composition:
1!
1965
            # Ensure that objective_mechanism has been included in the ControlMechanism's aux_components
1966
            #    and then add all Projections to and from the objective_mechanism to it
1967
            if self.objective_mechanism and self.objective_mechanism in composition.nodes:
1✔
1968
                # Safe to assert this, as it is already in the ControlMechanism's aux_components
1969
                #    and will therefore be added to the Composition along with the ControlMechanism
1970
                from psyneulink.core.compositions.composition import NodeRole
1✔
1971
                assert (self.objective_mechanism, NodeRole.CONTROL_OBJECTIVE) in self.aux_components, \
1✔
1972
                    f"PROGRAM ERROR:  {OBJECTIVE_MECHANISM} for {self.name} " \
1973
                    f"not listed in its 'aux_components' attribute."
1974
                dependent_projections.add(self._objective_projection)
1✔
1975
                # Add all Projections to and from objective_mechanism
1976
                for aff in self.objective_mechanism.afferents:
1✔
1977
                    dependent_projections.add(aff)
1✔
1978
                # for output_port in self.objective_mechanism.monitored_output_ports:
1979
                #     for eff in output_port.efferents:
1980
                #         dependent_projections.add(eff)
1981
                for eff in self.objective_mechanism.efferents:
1✔
1982
                    dependent_projections.add(eff)
1✔
1983
            else:
1984
                # FIX: 11/3/21: NEED TO MODIFY ONCE OUTCOME InputPorts ARE MOVED
1985
                # Add Projections to controller's OUTCOME InputPorts
1986
                # Note: this applies if ControlMechanism has an objective_mechanism that is not in the Composition
1987
                for i in range(self.num_outcome_input_ports):
1✔
1988
                    for proj in self.outcome_input_ports[i].path_afferents:
1✔
1989
                        dependent_projections.add(proj)
1✔
1990

1991
        # Warn if any efferents have been added to the ContolMechanism that are not ControlSignals
1992
        if len(self.control_projections) != len(self.efferents):
1!
1993
            warnings.warn(f"Projections from {self.name} have been added to {composition} that are not ControlSignals.")
×
1994
        for eff in self.efferents:
1✔
1995
            dependent_projections.add(eff)
1✔
1996

1997
        if composition:
1!
1998
            deeply_nested_aux_components = composition._get_deeply_nested_aux_projections(self)
1✔
1999
            dependent_projections -= set(deeply_nested_aux_components.values())
1✔
2000

2001
        for proj in dependent_projections:
1✔
2002
            proj._activate_for_compositions(composition)
1✔
2003

2004
        for proj in deeply_nested_aux_components.values():
1✔
2005
            composition.add_projection(proj, sender=proj.sender, receiver=proj.receiver, context=context)
1✔
2006

2007
        # Add any remaining afferent Projections that have been assigned and are from nodes in composition
2008
        remaining_projections = set(self.projections) - dependent_projections - set(self.composition.projections)
1✔
2009
        for proj in remaining_projections:
1✔
2010
            # Projection is afferent:
2011
            if proj in self.afferents:
1!
2012
                # Confirm sender is in composition
2013
                port, node, comp = composition._get_source(proj)
1✔
2014
            elif proj in self.efferents:
×
2015
                # Confirm receiver is in composition
2016
                port, node, comp = composition._get_destination(proj)
×
2017
            else:
2018
                assert False, f"PROGRAM ERROR: Attempt to activate Projection ('{proj.name}') in '{composition.name}'" \
2019
                              f" associated with its controller '{self.name}' that is neither an afferent nor " \
2020
                              f"efferent of '{self.name}' -- May be as yet unaccounted for condition."
2021
            if node in composition._get_all_nodes():
1!
2022
                proj._activate_for_compositions(composition)
1✔
2023

2024
    @property
1✔
2025
    def monitored_output_ports(self):
1✔
2026
        try:
1✔
2027
            return self.objective_mechanism.monitored_output_ports
1✔
2028
        except AttributeError:
×
2029
            return None
×
2030

2031
    @monitored_output_ports.setter
1✔
2032
    def monitored_output_ports(self, value):
1✔
2033
        try:
×
2034
            self.objective_mechanism._monitored_output_ports = value
×
2035
        except AttributeError:
×
2036
            return None
×
2037

2038
    @property
1✔
2039
    def monitored_output_ports_weights_and_exponents(self):
1✔
2040
        try:
×
2041
            return self.objective_mechanism.monitored_output_ports_weights_and_exponents
×
2042
        except:
×
2043
            return None
×
2044

2045
    @property
1✔
2046
    def num_outcome_input_ports(self):
1✔
2047
        try:
1✔
2048
            return len(self.outcome_input_ports)
1✔
2049
        except:
×
2050
            return 0
×
2051

2052
    @property
1✔
2053
    def control_signals(self):
1✔
2054
        """Get ControlSignals from OutputPorts"""
2055
        try:
1✔
2056
            return ContentAddressableList(component_type=ControlSignal,
1✔
2057
                                          list=[port for port in self.output_ports
2058
                                                if isinstance(port, (ControlSignal))])
2059
        except:
1✔
2060
            return []
1✔
2061

2062
    @property
1✔
2063
    def control_projections(self):
1✔
2064
        try:
1✔
2065
            return [projection for control_signal in self.control_signals for projection in control_signal.efferents]
1✔
2066
        except:
×
2067
            return None
×
2068

2069
    @property
1✔
2070
    def _sim_count_lock(self):
1✔
2071
        try:
1✔
2072
            return self.__sim_count_lock
1✔
2073
        except AttributeError:
1✔
2074
            self.__sim_count_lock = threading.Lock()
1✔
2075
            return self.__sim_count_lock
1✔
2076

2077
    def get_next_sim_id(self, context, allocation=None):
1✔
2078
        with self._sim_count_lock:
1✔
2079
            try:
1✔
2080
                sim_num = self._sim_counts[context.execution_id]
1✔
2081
                self._sim_counts[context.execution_id] += 1
1✔
2082
            except KeyError:
1✔
2083
                sim_num = 0
1✔
2084
                self._sim_counts[context.execution_id] = 1
1✔
2085

2086
        try:
1✔
2087
            # should always be called when a composition defined here,
2088
            # but just in case
2089
            composition = context.composition.name
1✔
2090
        except AttributeError:
×
2091
            composition = None
×
2092

2093
        # NOTE: anything in square brackets is removed from
2094
        # rich/console/reporting Report.report_output
2095
        return (
1✔
2096
            f'{context.execution_id}{EID_SIMULATION}'
2097
            f'{{simulator: {composition}, num: {sim_num},'
2098
            f' allocation: {allocation}, uuid: {uuid.uuid4()}}}'
2099
        )
2100

2101
    @property
1✔
2102
    def _dependent_components(self):
1✔
2103
        return list(itertools.chain(
1✔
2104
            super()._dependent_components,
2105
            # [self.objective_mechanism],
2106
            [self.objective_mechanism] if self.objective_mechanism else [],
2107
        ))
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc