• 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

89.38
/psyneulink/core/components/mechanisms/modulatory/control/optimizationcontrolmechanism.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

9
# **************************************  OptimizationControlMechanism *************************************************
10

11
# FIX: REWORK WITH REFERENCES TO `outcome <OptimizationControlMechanism.outcome>`
12
#      INTRODUCE SIMULATION INTO DISCUSSION OF COMPOSITION-BASED
13

14
"""
15

16
Contents
17
--------
18

19
  * `OptimizationControlMechanism_Overview`
20
      - `Expected Value of Control <OptimizationControlMechanism_EVC>`
21
      - `Agent Representation and Types of Optimization <OptimizationControlMechanism_Agent_Representation_Types>`
22
          - `"Model-Free" Optimization <OptimizationControlMechanism_Model_Free>`
23
          - `Model-Based Optimization <OptimizationControlMechanism_Model_Based>`
24
  * `OptimizationControlMechanism_Creation`
25
      - `Agent Rep <OptimizationControlMechanism_Agent_Rep_Arg>`
26
      - `State Features <OptimizationControlMechanism_State_Features_Arg>`
27
          - `agent_rep Composition <OptimizationControlMechanism_Agent_Rep_Composition>`
28
          - `agent_rep CompositionFunctionApproximator <OptimizationControlMechanism_Agent_Rep_CFA>`
29
      - `State Feature Functions <OptimizationControlMechanism_State_Feature_Function_Arg>`
30
      - `Outcome  <OptimizationControlMechanism_Outcome_Args>`
31
  * `OptimizationControlMechanism_Structure`
32
      - `Agent Representation <OptimizationControlMechanism_Agent_Rep>`
33
          - `State <OptimizationControlMechanism_State>`
34
      - `Input <OptimizationControlMechanism_Input>`
35
          - `state_input_ports <OptimizationControlMechanism_State_Input_Ports>`
36
          - `outcome_input_ports <OptimizationControlMechanism_Outcome>`
37
              - `objective_mechanism <OptimizationControlMechanism_ObjectiveMechanism>`
38
              - `monitor_for_control <OptimizationControlMechanism_Monitor_for_Control>`
39
      - `Function <OptimizationControlMechanism_Function>`
40
          - `OptimizationControlMechanism_Custom_Function`
41
          - `OptimizationControlMechanism_Search_Functions`
42
          - `OptimizationControlMechanism_Default_Function`
43
      - `Output <OptimizationControlMechanism_Output>`
44
          - `Randomization ControlSignal <OptimizationControlMechanism_Randomization_Control_Signal>`
45
  * `OptimizationControlMechanism_Execution`
46
      - `OptimizationControlMechanism_Execution_Timing`
47
      - `OptimizationControlMechanism_Optimization_Procedure`
48
      - `OptimizationControlMechanism_Estimation_Randomization`
49
  * `OptimizationControlMechanism_Class_Reference`
50

51

52
.. _OptimizationControlMechanism_Overview:
53

54
Overview
55
--------
56

57
An OptimizationControlMechanism is a `ControlMechanism <ControlMechanism>` that uses an `OptimizationFunction` to
58
optimize the performance of the `Composition` for which it is a `controller <Composition_Controller>`.  It does so
59
by using the `OptimizationFunction` (assigned as its `function <OptimizationControlMechanism.function>`) to execute
60
its `agent_rep <OptimizationControlMechanism.agent_rep>` -- a representation of the Composition to be optimized --
61
under different `control_allocations <ControlMechanism.control_allocation>`, and selecting the one that optimizes
62
its `net_outcome <ControlMechanism.net_outcome>`.  An OptimizationControlMechanism can be configured to implement
63
various forms of optimization, ranging from fully `model-based optimization <OptimizationControlMechanism_Model_Based>`
64
that uses the Composition itself as the  `agent_rep <OptimizationControlMechanism.agent_rep>` to simulate the
65
outcome for a given `state <OptimizationControlMechanism_State>` (i.e., a combination of the current input and a
66
particular `control_allocation <ControlMechanism.control_allocation>`), to fully `model-free optimization
67
<OptimizationControlMechanism_Model_Free>` by using a `CompositionFunctionApproximator` as the `agent_rep
68
<OptimizationControlMechanism.agent_rep>` that learns to  predict the outcomes for a state. Intermediate forms of
69
optimization can also be implemented, that use simpler Compositions to approximate the dynamics of the full
70
Composition. The outcome of executing the `agent_rep <OptimizationControlMechanism.agent_rep>` is used to compute a
71
`net_outcome <ControlMechanism.net_outcome>` for a given `state <OptimizationControlMechanism_State>`, that takes
72
into account the `costs <ControlMechanism_Costs_NetOutcome>` associated with the `control_allocation
73
<ControlMechanism.control_allocation>`, and is used to determine the optimal `control_allocations
74
<ControlMechanism.control_allocation>`.
75

76
.. _OptimizationControlMechanism_EVC:
77

78
**Expected Value of Control**
79

80
The `net_outcome <ControlMechanism.net_outcome>` of an OptimizationControlMechanism's `agent_rep
81
<OptimizationControlMechanism.agent_rep>` is computed -- for a given `state <OptimizationControlMechanism_State>`
82
(i.e., set of `state_feature_values <OptimizationControlMechanism.state_feature_values>` and a `control_allocation
83
<ControlMechanism.control_allocation>`) -- as the difference between the `outcome <ControlMechanism.outcome>` computed
84
by its `objective_mechanism <ControlMechanism.objective_mechanism>` and the aggregated `costs <ControlMechanism.costs>`
85
of its `control_signals <OptimizationControlMechanism.control_signals>` computed by its `combine_costs
86
<ControlMechanism.combine_costs>` function.  If the `outcome <ControlMechanism.outcome>` computed by the
87
`objective_mechanism <ControlMechanism.objective_mechanism>` is configured to measure the value of processing (e.g.,
88
reward received, time taken to respond, or a combination of these, etc.), and the `OptimizationFunction` assigned as
89
the OptimizationControlMechanism's `function <OptimizationControlMechanism.function>` is configured to find the
90
`control_allocation <ControlMechanism.control_allocation>` that maximizes its `net_outcome
91
<ControlMechanism.net_outcome>` (that is, the `outcome <ControlMechanism.outcome>` discounted by the
92
result of the `combine_costs <ControlMechanism.combine_costs>` function), then the OptimizationControlMechanism is
93
said to be maximizing the `Expected Value of Control (EVC) <https://www.ncbi.nlm.nih.gov/pubmed/23889930>`_.  That
94
is, it implements a cost-benefit analysis that weighs the `costs <ControlMechanism.costs>` of the ControlSignal
95
`values <ControlSignal.value>` associated with a `control_allocation <ControlMechanism.control_allocation>` against
96
the `outcome <ControlMechanism.outcome>` expected to result from it.  The costs are computed based on the
97
`cost_options <ControlSignal.cost_options>` specified for each of the OptimizationControlMechanism's `control_signals
98
<OptimizationControlMechanism.control_signals>` and its `combine_costs <ControlMechanism.combine_costs>` function.
99
The EVC is determined by its `compute_net_outcome <ControlMechanism.compute_net_outcome>` function (assigned to its
100
`net_outcome <ControlMechanism.net_outcome>` attribute), which is computed for a given `state
101
<OptimizationControlMechanism_State>` by the OptimizationControlMechanism's `evaluate_agent_rep
102
<OptimizationControlMechanism.evaluate_agent_rep>` method. In these respects, optimization of a Composition's
103
performance by its OptimizationControlMechanism -- as indexed by its `net_outcome <ControlMechanism.net_outcome>`
104
attribute -- implement a form of `Bounded Rationality <https://psycnet.apa.org/buy/2009-18254-003>`_,
105
also referred to as `Resource Rationality <https://www.cambridge.org/core/journals/behavioral-and-brain-sciences/article/abs/resourcerational-analysis-understanding-human-cognition-as-the-optimal-use-of-limited-computational-resources/586866D9AD1D1EA7A1EECE217D392F4A>`_,
106
in which the constraints imposed by the "bounds" or resources are reflected in the `costs` of the ControlSignals
107
(also see `Computational Rationality <https://onlinelibrary.wiley.com/doi/full/10.1111/tops.12086>`_ and `Toward a
108
Rational and Mechanistic Account of Mental Effort
109
<https://www.annualreviews.org/doi/abs/10.1146/annurev-neuro-072116-031526?casa_token=O2pFelbmqvsAAAAA:YKjdIbygP5cj_O7vAj4KjIvfHehHSh82xm44I5VS6TdTtTELtTypcBeET4BGdAy0U33BnDXBasfqcQ>`_).
110

111
COMMENT:
112
The table `below <OptimizationControlMechanism_Examples>` lists different
113
parameterizations of OptimizationControlMechanism that implement various models of EVC Optimization.
114
COMMENT
115

116
.. _OptimizationControlMechanism_Agent_Representation_Types:
117

118
**Agent Representation and Types of Optimization**
119

120
Much of the functionality described above is supported by a `ControlMechanism` (the parent class of an
121
OptimizationControlMechanism). The defining  characteristic of an OptimizationControlMechanism is its `agent
122
representation <OptimizationControlMechanism_Agent_Rep>`, that is used to determine the `net_outcome
123
<ControlMechanism.net_outcome>` for a given `state <OptimizationControlMechanism_State>`, and find the
124
`control_allocation <ControlMechanism.control_allocation>` that optimizes this.  The `agent_rep
125
<OptimizationControlMechanism.agent_rep>` can be the `Composition` to which the OptimizationControlMechanism
126
belongs (and controls), another (presumably simpler) one, or a `CompositionFunctionApproximator` that is used to
127
estimate the `net_outcome <ControlMechanism.net_outcome>` of the Composition of which the OptimizationControlMechanism
128
is the `controller <Composition.controller>`.  These different types of `agent representation
129
<OptimizationControlMechanism_Agent_Rep>` correspond closely to the distinction between *model-based* and
130
*model-free* optimization in the `machine learning
131
<https://www.google.com/books/edition/Reinforcement_Learning_second_edition/uWV0DwAAQBAJ?hl=en&gbpv=1&dq=Sutton,+R.+S.,+%26+Barto,+A.+G.+(2018).+Reinforcement+learning:+An+introduction.+MIT+press.&pg=PR7&printsec=frontcover>`_
132
and `cognitive neuroscience <https://www.nature.com/articles/nn1560>`_ literatures, as described below.
133

134
    .. figure:: _static/Optimization_fig.svg
135
       :scale: 50%
136
       :alt: OptimizationControlMechanism
137

138
       **Functional Anatomy of an OptimizationControlMechanism.** *Panel A:* Examples of use in fully model-based
139
       and model-free optimization.  Note that in the example of `model-based optimization
140
       <OptimizationControlMechanism_Model_Based>` (left), the OptimizationControlMechanism uses the entire
141
       `Composition` that it controls as its `agent_rep <OptimizationControlMechanism.agent_rep>`, whereas in
142
       the example of `model-free optimization <OptimizationControlMechanism_Model_Free>` (right) the
143
       the `agent_rep <OptimizationControlMechanism.agent_rep>` is a `CompositionFunctionApproximator`. The `agent_rep
144
       <OptimizationControlMechanism.agent_rep>` can also be another (presumably simpler) Composition that can be used
145
       to implement forms of optimization intermediate between fully model-based and model-free. *Panel B:* Flow of
146
       execution during optimization.  In both panels, faded items show process of adaptation when using a
147
       `CompositionFunctionApproximator` as the `agent_rep <OptimizationControlMechanism.agent_rep>`.
148
|
149
.. _OptimizationControlMechanism_Model_Based:
150

151
*Model-Based Optimization*
152

153
The fullest form of this is implemented by assigning the Composition for which the OptimizationControlMechanism is the
154
`controller <Composition.controller>`) as its a`agent_rep  <OptimizationControlMechanism.agent_rep>`
155
On each `TRIAL <TimeScale.TRIAL>`, that Composition *itself* is provided with either the most recent inputs
156
to the Composition, or ones predicted for the upcoming trial (as determined by the `state_feature_values
157
<OptimizationControlMechanism.state_feature_values>` of the OptimizationControlMechanism), and then used to simulate
158
processing on that trial in order to find the `control_allocation <ControlMechanism.control_allocation>` that yields
159
the best `net_outcome <ControlMechanism.net_outcome>` for that trial.  A different Composition can also be assigned as
160
the `agent_rep  <OptimizationControlMechanism.agent_rep>`, that approximates in simpler form the dynamics of processing
161
in the Composition for which the OptimizationControlMechanism is the `controller <Composition.controller>`,
162
implementing a more restricted form of model-based optimization.
163

164
.. _OptimizationControlMechanism_Model_Free:
165

166
*"Model-Free" Optimization*
167

168
    .. note::
169
       The term *model-free* is placed in apology quotes to reflect the fact that, while this term is
170
       used widely (e.g., in machine learning and cognitive science) to distinguish it from *model-based* forms of
171
       processing, model-free processing nevertheless relies on *some* form of model -- albeit usually a much simpler
172
       one -- for learning, planning and decision making.  In the context of a OptimizationControlMechanism, this is
173
       addressed by use of the term "agent_rep", and how it is implemented, as described below.
174

175
This clearest form of this uses a `CompositionFunctionApproximator`, that learns to predict the `net_outcome
176
<ControlMechanism.net_outcome>` for a given state (e.g., using reinforcement learning or other forms of
177
function approximation, such as a `RegressionCFA`).  In each `TRIAL <TimeScale.TRIAL>` the  `agent_rep
178
<OptimizationControlMechanism.agent_rep>` is used to search over `control_allocation
179
<ControlMechanism.control_allocation>`\\s, to find the one that yields the best predicted `net_outcome
180
<ControlMechanism.net_outcome>` of processing on the upcoming trial, based on the current or (expected)
181
`state_feature_values <OptimizationControlMechanism.state_feature_values>` for that trial.  The `agent_rep
182
<OptimizationControlMechanism.agent_rep>` is also given the chance to adapt in order to improve its prediction of
183
its `net_outcome <ControlMechanism.net_outcome>` based on the `state <OptimizationControlMechanism_State>` and
184
`net_outcome <ControlMechanism.net_outcome>` of the prior `TRIAL <TimeScale.TRIAL>`.  A Composition can also be
185
used to generate such predictions, permitting forms of optimization that are intermediate between the extreme
186
examples of model-based and model-free, as noted above.
187

188
.. _OptimizationControlMechanism_Creation:
189

190
Creating an OptimizationControlMechanism
191
----------------------------------------
192

193
The constructor has the same arguments as a `ControlMechanism <ControlMechanism>`, with the following
194
exceptions/additions, which are specific to the OptimizationControlMechanism:
195

196
.. _OptimizationControlMechanism_Agent_Rep_Arg:
197

198
* **agent_rep** -- specifies the `Composition` used by the OptimizationControlMechanism's `evaluate_agent_rep
199
  <OptimizationControlMechanism.evaluate_agent_rep>` method to calculate the predicted `net_outcome
200
  <ControlMechanism.net_outcome>` for a given `state <OptimizationControlMechanism_State>` (see `below
201
  <OptimizationControlMechanism_Agent_Rep>` for additional details). If it is not specified, then the
202
  `Composition` to which the OptimizationControlMechanism is assigned becomes its `agent_rep
203
  <OptimizationControlMechanism.agent_rep>`, and the OptimizationControlMechanism is assigned as that Composition's
204
  `controller <Composition.controller>`, implementing fully `model-based <OptimizationControlMechanism_Model_Based>`
205
  optimization.  If that Composition already has a `controller <Composition.controller>` specified,
206
  the OptimizationControlMechanism is disabled. If another Composition is specified, it must conform to the
207
  specifications for an `agent_rep <OptimizationControlMechanism.agent_rep>` as described `below
208
  <OptimizationControlMechanism_Agent_Rep>`.  The  `agent_rep <OptimizationControlMechanism.agent_rep>` can also be
209
  a `CompositionFunctionApproximator` for `model-free <OptimizationControlMechanism_Model_Free>` forms of
210
  optimization.  The type of Component assigned as the `agent_rep <OptimizationControlMechanism.agent_rep>` is
211
  identified in the OptimizationControlMechanism's `agent_rep_type <OptimizationControlMechanism.agent_rep_type>`
212
  attribute.
213

214
.. _OptimizationControlMechanism_State_Features_Arg:
215

216
* **state_features** -- specifies the sources of input to the OptimizationControlMechanism's `agent_rep
217
  <OptimizationControlMechanism.agent_rep>` which, together with a selected `control_allocation
218
  <ControlMechanism.control_allocation>`, are provided as input to it's `evaluate <Composition.evaluate>` method
219
  when that is executed to estimate or predict the Composition's `net_outcome <ControlMechanism.net_outcome>`.
220
  Those sources of input are used to construct the OptimizationControlMechanism's `state_input_ports
221
  <OptimizationControlMechanism.state_input_ports>`, one for each `external InputPort
222
  <Composition_Input_External_InputPorts>` of the `agent_rep <OptimizationControlMechanism.agent_rep>`. The input to
223
  each `state_input_port <OptimizationControlMechanism.state_input_ports>`, after being processed by it `function
224
  <InputPort.function>`, is assigned as the corresponding value of `state_feature_values
225
  <OptimizationControlMechanism.state_feature_values>`, the values of which provided as the input to the corresponding
226
  InputPorts of the `INPUT <NodeRole.INPUT>` `Nodes <Composition_Nodes>` of the agent_rep each time it is `evaluated
227
  <Composition.evaluate>`.  Accordingly, the specification requirements for **state_features** depend on whether the
228
  `agent_rep<OptimizationControlMechanism.agent_rep>` is a `Composition` or a `CompositionFunctionApproximator`,
229
  as described in each of the two sections below.
230

231
  |
232

233
  .. _OptimizationControlMechanism_Agent_Rep_Composition:
234

235
  **state_features** *for an agent_rep that is a* **Composition**
236

237
  |
238

239
  .. _OptimizationControlMechanism_State_Features_Automatic_Assignment:
240

241
  *Automatic assignment.*  By default, if **state_features**, **state_feature_default** and **state_feature_function**
242
  are not specified, the `state_input_ports <OptimizationControlMechanism.state_input_ports>` are configured to
243
  `shadow the inputs <InputPort_Shadow_Inputs>` of every `external InputPort <Composition_Input_External_InputPorts>`
244
  of the `agent_rep <OptimizationControlMechanism.agent_rep>` Composition;  as a result, each time `agent_rep
245
  <OptimizationControlMechanism.agent_rep>` is `evaluated <Composition.evaluate>`, it receives the same `external
246
  input <Composition_Execution_Inputs>`) it received during its last `TRIAL<TimeScale.TRIAL>` of execution.
247

248
  |
249

250
  .. _OptimizationControlMechanism_State_Features_Explicit_Specification:
251

252
  *Explicit specification.* Specifying the **state_features**, **state_feature_default** and/or
253
  **state_feature_function** arguments explicitly can be useful if: values need to be provided as input to the
254
  `agent_rep <OptimizationControlMechanism.agent_rep>` when it is evaluated other than its `external inputs
255
  <Composition_Execution_Inputs>`; to restrict evaluation to a subset of its inputs (while others are held constant);
256
  and/or to assign specific functions to one or more `state_input_ports
257
  <OptimizationControlMechanism.state_input_ports>` (see `below
258
  <OptimizationControlMechanism_State_Feature_Function_Arg>`) that allow them to process the inputs
259
  (e.g., modulate and/or integrate them) before they are assigned to `state_feature_values
260
  <OptimizationControlMechanism.state_feature_values>` and passed to the `agent_rep
261
  <OptimizationControlMechanism.agent_rep>`. Assignments can be made to **state_features** corresponding
262
  to any or all InputPorts of the `agent_rep <OptimizationControlMechanism.agent_rep>`\\'s `INPUT <NodeRole.INPUT>`
263
  `Nodes <Composition_Nodes>`, as described `below <OptimizationControlMechanism_State_Features_Specification>`.
264
  Any that are not specified are assigned the value specified for **state_feature_default** (*SHADOW_INPUTS* by
265
  default; see `state_feature_default <OptimizationControlMechanism.state_feature_default>` for additional details).
266
  A single assignment can be made for all **state_features**, or they can be specified individually for each `INPUT
267
  <NodeRole.INPUT>` `Nodes <Composition_Nodes>` InputPort, as descdribed below.
268

269
  .. _OptimizationControlMechanism_State_Features_Shapes:
270

271
      .. note::
272
         If **state_features** are specified explicitly, the `value <Component.value>`\\s of the specified Components
273
         must match the `input_shape <InputPort.input_shape>` of the corresponding InputPorts of the `agent_rep
274
         <OptimizationControlMechanism.agent_rep>`\\'s `INPUT <NodeRole.INPUT>` `Nodes <Composition_Nodes>`. Those
275
         InputPorts are listed in the `agent_rep <OptimizationControlMechanism.agent_rep>`\\'s
276
         `external_input_ports_of_all_input_nodes <Composition.external_input_ports_of_all_input_nodes>` attribute
277
         and, together with examples of their values, in the OptimizationControlMechanism's `state_feature_values
278
         <OptimizationControlMechanism.state_feature_values>` attribute. A failure to properly meet these requirements
279
         produces an error.
280

281
  .. _OptimizationControlMechanism_State_Features_Specification:
282

283
  The **state_features** argument can be specified using any of the following formats:
284

285
  .. _OptimizationControlMechanism_State_Feature_Single_Spec:
286

287
  * *Single specification* -- any of the indivdiual specifications described `below
288
    <OptimizationControlMechanism_State_Feature_Individual_Specs>` can be directly to **state_features**, that is
289
    then used to construct *all* of the `state_input_ports <OptimizationControlMechanism.state_input_ports>`, one
290
    for each `external InputPort <Composition_Input_External_InputPorts>` of the `agent_rep
291
    <OptimizationControlMechanism.agent_rep>`.
292

293
  .. _OptimizationControlMechanism_State_Feature_Input_Dict:
294

295
  * *Inputs dictionary* -- specifies state_features (entry values) for individual `InputPorts <InputPort>` and/or
296
    `INPUT <NodeRole.INPUT>` `Nodes <Composition_Nodes>` of the `agent_rep <OptimizationControlMechanism.agent_rep>`
297
    (entry keys). It must conform to the format used to `specify external inputs <Composition_Input_Dictionary>`
298
    to the `agent_rep <OptimizationControlMechanism.agent_rep>`, in which entries consist of a key specifying either
299
    an `INPUT <NodeRole.INPUT>` `Node <Composition_Nodes>` of the `agent_rep <OptimizationControlMechanism.agent_rep>`
300
    or one of their `external InputPorts <Composition_Input_External_InputPorts>`, and a value that is the source of
301
    the input that can be any of the forms of individual input specifications listed `below
302
    <OptimizationControlMechanism_State_Feature_Individual_Specs>`. The format required for the entries can be seen
303
    using either the `agent_rep <OptimizationControlMechanism.agent_rep>`
304
    `get_input_format <Composition.get_input_format>` method (for inputs to its `INPUT <NodeRole.INPUT>` <Nodes
305
    <Composition_Nodes>`) or its `external_input_ports_of_all_input_nodes
306
    <Composition.external_input_ports_of_all_input_nodes>` (for all of their `external InputPorts
307
    <Composition_Input_External_InputPorts>`). If a nested Composition is specified (that is, one that is an `INPUT
308
    <NodeRole.INPUT>` Node of `agent_rep <OptimizationControlMechanism.agent_rep>`), the state_feature assigned to it
309
    is used to construct the `state_input_ports <OptimizationControlMechanism.state_input_ports>` for *all* of the
310
    `external InputPorts <Composition_Input_External_InputPorts>` for that nested Composition, and any nested within
311
    it at all levels of nesting. If any `INPUT <NodeRole.INPUT>` Nodes or their InputPorts are not specified in the
312
    dictionary, `state_feature_default <OptimizationControlMechanism.state_feature_default>` is assigned as their
313
    state_feature specification (this includes cases in which some but not all `INPUT <NodeRole.INPUT>` Nodes of a
314
    nested Composition, or their InputPorts, are specified; any unspecified INPUT Nodes of the corresponding
315
    Compositions are assigned `state_feature_default <OptimizationControlMechanism.state_feature_default>` as their
316
    state_feature specification).
317

318
  .. _OptimizationControlMechanism_State_Feature_List_Inputs:
319

320
  * *List* -- a list of individual state_feature specifications, that can be any of the forms of individual
321
    input specifications listed `below <OptimizationControlMechanism_State_Feature_Individual_Specs>`. The items
322
    correspond to all of the `external InputPorts <Composition_Input_External_InputPorts>` of the `agent_rep
323
    <OptimizationControlMechanism.agent_rep>`, and must be specified in the order they are listed in the
324
    `agent_rep <OptimizationControlMechanism.agent_rep>`\\'s `external_input_ports_of_all_input_nodes
325
    <Composition.external_input_ports_of_all_input_nodes>` attribute. If the list is incomplete, the remaining
326
    InputPorts are assigned `state_feature_default <OptimizationControlMechanism.state_feature_default>`
327
    as their state_feature specification, which by default is *SHADOW_INPUTS* (see `below
328
    <OptimizationControlMechanism_SHADOW_INPUTS_State_Feature>`. Items can be included in the list that
329
    have not yet been added to the OptimizationControlMechanism's Composition or its `agent_rep
330
    <OptimizationControlMechanism.agent_rep>`. However, these must be added before the Composition is executed,
331
    and must appear in the list in the same position that the InputPorts to which they pertain are listed in
332
    the `agent_rep <OptimizationControlMechanism.agent_rep>`\\'s `external_input_ports_of_all_input_nodes
333
    <Composition.external_input_ports_of_all_input_nodes>` attribute, once construction of the `agent_rep
334
    <OptimizationControlMechanism.agent_rep>` is complete.
335

336
  .. _OptimizationControlMechanism_State_Feature_Set_Inputs:
337

338
  * *Set* -- a set of `INPUT <NodeRole.INPUT>` `Nodes <Composition_Nodes>` of the `agent_rep
339
    <OptimizationControlMechanism.agent_rep>` that are assigned *SHADOW_INPUTS* as their state_feature
340
    -- that is, that should receive the same inputs during evaluation as when the Composition of which
341
    the OptimizationControlMechanism is the `controller <Composition_Controller>` is fully executed
342
    (see `below <_OptimizationControlMechanism_SHADOW_INPUTS_State_Feature>`). The order of their specification
343
    does not matter;  however, any of the `agent_rep <OptimizationControlMechanism.agent_rep>`\\'s `INPUT
344
    <NodeRole.INPUT>` Nodes that are *not* included in the set are assigned `state_feature_default
345
    <OptimizationControlMechanism.state_feature_default>` as their state_feature specification.  Note that,
346
    since the default for `state_feature_default <OptimizationControlMechanism.state_feature_default>` is
347
    *SHADOW_INPUTS*, unless this is specified otherwise omitting items from a set has no effect (i.e., they
348
    too are assigned *SHADOW_INPUTS*);  for omitted items to be treated differently, `state_feature_default
349
    <OptimizationControlMechanism.state_feature_default>` must be specified; for example by assigning it
350
    ``None`` so that items omitted from the set are assigned their default input value (see `below
351
    <OptimizationControlMechanism_None_State_Feature>`.
352

353
  .. _OptimizationControlMechanism_State_Feature_Individual_Specs:
354

355
  * *Individual state_feature specifications* -- any of the specifications listed below can be used singly,
356
    or in a dict, list or set as described `above <OptimizationControlMechanism_State_Feature_Input_Dict>`,
357
    to configure `state_input_ports <OptimizationControlMechanism.state_input_ports>`.
358

359
    .. _OptimizationControlMechanism_None_State_Feature:
360

361
    * *None* -- no `state_input_port <OptimizationControlMechanism.state_input_ports>` is constructed for
362
      the corresponding `INPUT <NodeRole.INPUT>` `Node <Composition_Nodes>` InputPort, and its the value
363
      of its `default variable <Component.defaults>` is used as the input to that InputPort whenever the
364
      <OptimizationControlMechanism.agent_rep>` is `evaluated <Composition.evaluate>`, irrespective of its input when
365
      the `agent_rep <OptimizationControlMechanism.agent_rep>` was last executed.
366

367
    .. _OptimizationControlMechanism_Numeric_State_Feature:
368

369
    * *numeric value* -- create a `state_input_port <OptimizationControlMechanism.state_input_ports>` has
370
      no `afferent Projections <Mechanism_Base.afferents>`, and uses the specified value as the input to its
371
      `function <InputPort.function>`, the result of which is assigned to the corresponding value of
372
      `state_feature_values <OptimizationControlMechanism.state_feature_values>` and provided as the input to
373
      the corresponding `INPUT <NodeRole.INPUT>` `Node <Composition_Nodes>` InputPort each time the `agent_rep
374
      <OptimizationControlMechanism.agent_rep>`  is `evaluated <Composition.evaluate>`. The specified value must
375
      be compatible with the shape of all of the `external InputPorts <Composition_Input_External_InputPorts>`
376
      of the `agent_rep <OptimizationControlMechanism.agent_rep>` (see `note
377
      <OptimizationControlMechanism_State_Features_Shapes>` above).
378

379
    .. _OptimizationControlMechanism_SHADOW_INPUTS_State_Feature:
380

381
    * *SHADOW_INPUTS* -- create a `state_input_port <OptimizationControlMechanism.state_input_ports>` that `shadows
382
      the input <InputPort_Shadow_Inputs>` of the InputPort to which the specification is assigned; that is, each time
383
      `agent_rep <OptimizationControlMechanism.agent_rep>` is  `evaluated <Composition.evaluate>`, the state_input_port
384
      receives the same input that the corresponding `INPUT <NodeRole.INPUT>` `Node <Composition_Nodes>` InputPort
385
      received during the last `TRIAL <TimeScale.TRIAL>` of execution.
386

387
    .. _OptimizationControlMechanism_Input_Port_State_Feature:
388

389
    * *InputPort specification* -- create a `state_input_port <OptimizationControlMechanism.state_input_ports>` that
390
      `shadows <InputPort_Shadow_Inputs>` the input to the specified `InputPort`;  that is, each time `agent_rep
391
      <OptimizationControlMechanism.agent_rep>` is  `evaluated <Composition.evaluate>`, the state_input_port receives
392
      the same input that the specified InputPort received during the last `TRIAL <TimeScale.TRIAL>` in which the
393
      Composition for which the OptimizationControlMechanism is the `controller <Composition.controller>` was executed.
394
      The specification can be any form of `InputPort specification <InputPort_Specification>` for the `InpuPort`
395
      of any `Mechanism <Mechanism>` that is an `INPUT <NodeRole.INPUT>` `Node <Composition_Nodes>` in the Composition
396
      (not limited to the `agent_rep <OptimizationControlMechanism.agent_rep>`).  This includes an
397
      `InputPort specification dictionary <InputPort_Specification_Dictionary>`, that can be used to configure the
398
      corresponding `state_input_port <OptimizationControlMechanism.state_input_ports>`, if `Parameters <Parameter>`
399
      other than its `function <InputPort.function>` need to be specified (which can be done directly using a
400
      `2-item tuple <OptimizationControlMechanism_Tuple_State_Feature>` specification or the **state_feature_function**
401
      arg as described `below <OptimizationControlMechanism_State_Feature_Function_Arg>`), such as the InputPort's
402
      `name <InputPort.name>` or more than a single `afferent Projection <Mechanism_Base.afferents>`.
403

404
      .. _OptimizationControlMechanism_INPUT_Node_Specification:
405

406
      .. note::
407
         Only the `INPUT <NodeRole.INPUT>` `Nodes <Composition_Nodes>` of a `nested Composition <Composition_Nested>`
408
         can be shadowed.  Therefore, if the Composition that an OptimizationControlMechanism controls contains any
409
         nested Compositions, only its `INPUT <NodeRole.INPUT>` `Nodes <Composition_Nodes>` can be specified for
410
         shadowing in the **state_features** argument of the OptimizationControlMechanism's constructor.
411

412
      .. hint::
413
         Shadowing the input to a Node of a `nested Composition <Composition_Nested>` that is not an `INPUT
414
         <NodeRole.INPUT>` Node of that Composition can be accomplished in one or of two ways, by: a) assigning it
415
         `INPUT <NodeRole.INPUT>` as a `required NodeRole <Composition_Node_Role_Assignment>` where it is added to
416
         the nested Composition; and/or b) adding an additional Node to that Composition that shadows the desired one
417
         (this is allowed *within* the *same* Composition), and is assigned as an `OUTPUT <NodeRole.OUTPUT>` Node of
418
         that Composition, the `OutputPort` of which which can then be specified in the **state_features** argument of
419
         the OptimizationControlMechanism's constructor (see below).
420

421
      .. technical_note::
422
        The InputPorts specified as state_features are designated as `internal_only <InputPort.internal_only>` = `True`.
423

424
    .. _OptimizationControlMechanism_Output_Port_State_Feature:
425

426
    * *OutputPort specification* -- create a `state_input_port <OptimizationControlMechanism.state_input_ports>` that
427
      receives a `MappingProjection` from the specified `OutputPort`;  that is, each time `agent_rep
428
      <OptimizationControlMechanism.agent_rep>` is  `evaluated <Composition.evaluate>`, the state_input_port receives
429
      the `value <OutputPort.value>` of the specified OutputPort after the last `TRIAL <TimeScale.TRIAL>` in which the
430
      Composition for which the OptimizationControlMechanism is the `controller <Composition.controller>` was executed.
431
      The specification can be any form of `OutputPort specification <OutputPort_Specification>` for any `OutputPort`
432
      of a `Mechanism <Mechanism>` in the Composition (not limited to the `agent_rep
433
      <OptimizationControlMechanism.agent_rep>`.
434

435
    .. _OptimizationControlMechanism_Mechanism_State_Feature:
436

437
    * *Mechanism* -- create a `state_input_port <OptimizationControlMechanism.state_input_ports>` that `shadows
438
      <InputPort_Shadow_Inputs>` the input to the `primary InputPort <InputPort_Primary>` of the specified Mechanism
439
      (this is the same as explicitly specifying the Mechanism's  input_port, as described `above
440
      <OptimizationControlMechanism_Input_Port_State_Feature>`). If the Mechanism is in a `nested Composition
441
      <Composition_Nested>`, it must be an `INPUT <NodeRole.INPUT>` `Node <Composition_Nodes>` of that Composition
442
      (see `note <OptimizationControlMechanism_INPUT_Node_Specification>` above).  If the Mechanism's `OutputPort`
443
      needs to be used, it must be specified explicitly (as described `above
444
      <OptimizationControlMechanism_Output_Port_State_Feature>`).
445

446
      .. note::
447
         The use of a Mechanism to specify the shadowing of its `primary InputPort <InputPort_Primary>` is unique to
448
         its specification in the **state_features** argument of an OptimizationControlMechanism, and differs from the
449
         ordinary usage where it specifies a Projection from its `primary OutputPort <OutputPort_Primary>` (see
450
         `InputPort specification <InputPort_Projection_Source_Specification>`).  This difference extends to the use
451
         of a Mechanism in the *PROJECTIONS* entry of an `InputPort specification dictionary
452
         <InputPort_Specification_Dictionary>` used in the **state_features** argument, where there too
453
         it designates shadowing of its `primary InputPort <InputPort_Primary>` rather than a `Projection` from its
454
         `primary OutputPort <OutputPort_Primary>`.
455

456
    .. _OptimizationControlMechanism_Tuple_State_Feature:
457

458
    * *2-item tuple* -- the first item must be any of the forms of individual state_feature specifications
459
      described `above <OptimizationControlMechanism_State_Feature_Individual_Specs>`, and the second
460
      item must be a `Function`, that is assigned as the `function <InputPort.function>` of the corresponding
461
      `state_input_port <OptimizationControlMechanism.state_input_ports>`; this takes precedence over
462
      any other state_feature_function specifications (e.g., in an `InputPort specification dictionary
463
      <InputPort_Specification_Dictionary>` or the **state_feature_function** argument of the
464
      OptimizationControlMechanism's constructor; see `state_feature_function
465
      <OptimizationControlMechanism_State_Feature_Function_Arg>` for additional details).
466

467
  |
468

469
  .. _OptimizationControlMechanism_Agent_Rep_CFA:
470

471
  **state_features** *for an agent_rep that is a* **CompositionFunctionApproximator**
472

473
  |
474

475
  The **state_features** specify the **feature_values** argument to the `CompositionFunctionApproximator`\\'s
476
  `evaluate <CompositionFunctionApproximator.evaluate>` method. These cannot be determined automatically and so
477
  they *must be specified explicitly*, in a list, with the correct number of items in the same order and with
478
  the same shapes they are expected have in the array passed to the **feature_values** argument of the
479
  `evaluate <CompositionFunctionApproximator.evaluate>` method (see warning below).
480

481
      .. warning::
482
         The **state_features** for an `agent_rep <OptimizationControlMechanism.agent_rep>` that is a
483
         `CompositionFunctionApproximator` cannot be created automatically nor can they be validated;
484
         thus specifying the wrong number or invalid **state_features**, or specifying them in an incorrect
485
         order may produce errors that are unexpected or difficult to interpret.
486

487
  The list of specifications can contain any of the forms of specification used for an `agent_rep
488
  <OptimizationControlMechanism.agent_rep>` that is a Composition as described `above
489
  <OptimizationControlMechanism_State_Feature_Input_Dict>`, with the following exception: if a
490
  `Mechanism` is specified, its `primary OutputPort <OutputPort_Primary>` is used (rather than
491
  shadowing its primary InputPort), since that is more typical usage, and there are no assumptions
492
  made about the state features of a `CompositionFunctionApproximator` (as there are about a Composition
493
  as `agent_rep <OptimizationControlMechanism.agent_rep>`); if the input to the Mechanism *is* to be
494
  `shadowed <InputPort_Shadow_Inputs>`, then its InputPort must be specified explicitly (as described
495
  `above <OptimizationControlMechanism_Input_Port_State_Feature>`).
496

497
|
498

499
.. _OptimizationControlMechanism_State_Feature_Function_Arg:
500

501
* **state_feature_function** -- specifies a `function <InputPort.function>` to be used as the default
502
  function for `state_input_ports <OptimizationControlMechanism.state_input_ports>`. This is assigned as
503
  the `function <InputPort.function>` to any state_input_ports for which *no other* `Function` is specified --
504
  that is, in either an `InputPort specification dictionary <InputPort_Specification_Dictionary>` or a `2-item tuple
505
  <OptimizationControlMechanism_Tuple_State_Feature>` in the **state_features** argument (see `state_features
506
  <OptimizationControlMechanism_State_Features_Arg>`).  If either of the latter is specified, they override
507
  the specification in **state_feature_function**.  If **state_feature_function** is *not* specified, then
508
  `LinearCombination` (the standard default `Function` for an `InputPort`) is assigned to any `state_input_ports
509
  <OptimizationControlMechanism.state_input_ports>` that are not otherwise assigned a `Function`.
510
  Specifying functions for `state_input_ports <OptimizationControlMechanism.state_input_ports>` can be useful,
511
  for example to provide an average or integrated value of prior inputs to the `agent_rep
512
  <OptimizationControlMechanism.agent_rep>`\\'s `evaluate <Composition.evaluate>` method during the optimization
513
  process, or to use a generative model of the environment to provide those inputs.
514

515
    .. note::
516
       The value returned by a function assigned to the **state_feature_function** argument must preserve the
517
       shape of its input, and must also accommodate the shape of the inputs to all of the `state_input_ports
518
       <OptimizationControlMechanism.state_input_ports>` to which it is assigned (see `note
519
       <OptimizationControlMechanism_State_Features_Shapes>` above).
520

521
.. _OptimizationControlMechanism_Outcome_Args:
522

523
* **Outcome arguments** -- these specify the Components, the values of which are assigned to the `outcome
524
  <ControlMechanism.outcome>` attribute, and used to compute the `net_outcome <ControlMechanism.net_outcome>` for a
525
  given `control_allocation <ControlMechanism.control_allocation>` (see `OptimizationControlMechanism_Execution`).
526
  As with a ControlMechanism, these can be sepcified directly in the **monitor_for_control** argument, or through the
527
  use of `ObjectiveMechanism` specified in the  **objecctive_mechanism** argument (see
528
  ControlMechanism_Monitor_for_Control for additional details).  However, an OptimizationControlMechanism places some
529
  restrictions on the specification of these arguments that, as with specification of `state_features
530
  <OptimizationControlMechanism_State_Features_Arg>`, depend on the nature of the `agent_rep
531
  <OptimizationControlMechanism.agent_rep>`, as described below.
532

533
  * *agent_rep is a Composition* -- the items specified to be monitored for control must belong to the `agent_rep
534
    <OptimizationControlMechanism.agent_rep>`, since those are the only ones that will be executed when the
535
    `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` is called; an error will be generated
536
    identifying any Components that do not belong to the `agent_rep <OptimizationControlMechanism.agent_rep>`.
537

538
  * *agent_rep is a CompositionFunctionApproximator* -- the items specified to be monitored for control can be any
539
    within the Composition for which the OptimizationControlMechanism is the `controller <Composition_Controller>`;
540
    this is because their values during the last execution of the Composition are used to determine the `net_outcome
541
    <ControlMechanism.net_outcome>` that the `agent_rep <OptimizationControlMechanism.agent_rep>`\\'s
542
    `adapt <CompositionFunctionApproximator.adapt>` method -- if it has one -- seeks to predict.  Accordingly,
543
    the values of the items specified to be monitored control must match, in shape and order, the
544
    **net_outcome** of that `adapt <CompositionFunctionApproximator.adapt>` method.
545

546
* **Optimization arguments** -- these specify parameters that determine how the OptimizationControlMechanism's
547
  `function <OptimizationControlMechanism.function>` searches for and determines the optimal `control_allocation
548
  <ControlMechanism.control_allocation>` (see `OptimizationControlMechanism_Execution`); this includes specification
549
  of the `num_estimates <OptimizationControlMechanism.num_estimates>` and `num_trials_per_estimate
550
  <OptimizationControlMechanism.num_trials_per_estimate>` parameters, as well as the `random_variables
551
  <OptimizationControlMechanism.random_variables>`, `initial_seed <OptimizationControlMechanism.initial_seed>` and
552
  `same_seed_for_all_allocations <OptimizationControlMechanism.same_seed_for_all_allocations>` Parameters, which
553
  determine how the `net_outcome <ControlMechanism.net_outcome>` is estimated for a given `control_allocation
554
  <ControlMechanism.control_allocation>` (see `OptimizationControlMechanism_Estimation_Randomization` for additional
555
  details).
556

557
.. _OptimizationControlMechanism_Structure:
558

559
Structure
560
---------
561

562
An OptimizationControlMechanism conforms to the structure of a `ControlMechanism`, with the following exceptions
563
and additions.
564

565
.. _OptimizationControlMechanism_Agent_Rep:
566

567
*Agent Representation*
568
^^^^^^^^^^^^^^^^^^^^^^
569

570
The defining feature of an OptimizationControlMechanism is its agent representation, specified in the **agent_rep**
571
argument of its constructor, and assigned to its `agent_rep <OptimizationControlMechanism.agent_rep>` attribute.  This
572
designates a representation of the `Composition` (or parts of it) that the OptimizationControlMechanism uses to
573
evaluate sample `control_allocations <ControlMechanism.control_allocation>` in order to find one that optimizes the
574
the `net_outcome <ControlMechanism.net_outcome>` of the Composition when it is fully executed. The `agent_rep
575
<OptimizationControlMechanism.agent_rep>` can be the Composition itself for which the OptimizationControlMechanism is
576
the `controller <Composition_Controller>` (fully `model-based optimization <OptimizationControlMechanism_Model_Based>`,
577
or another one `model-free optimization <OptimizationControlMechanism_Model_Free>`), that is usually a simpler
578
Composition or a `CompositionFunctionApproximator`  used to estimate the `net_outcome <ControlMechanism.net_outcome>`
579
for the full Composition (see `above <OptimizationControlMechanism_Agent_Representation_Types>`).  The `evaluate
580
<Composition.evaluate>` method of the `agent_rep <OptimizationControlMechanism.agent_rep>` is assigned as the
581
`evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` method of the OptimizationControlMechanism.
582
If the `agent_rep <OptimizationControlMechanism.agent_rep>` is not the Composition for which the
583
OptimizationControlMechanism is the controller, then it must meet the following requirements:
584

585
* Its `evaluate <Composition.evaluate>` method must accept as its first four positional arguments:
586

587
  - values that correspond in shape to  the `state_feature_values
588
    <OptimizationControlMechanism.state_feature_values>` (inputs for estimate);
589
  - `control_allocation <ControlMechanism.control_allocation>` (the set of parameters for which estimates
590
    of `net_outcome <ControlMechanism.net_outcome>` are made);
591
  - `num_trials_per_estimate <OptimizationControlMechanism.num_trials_per_estimate>` (number of trials executed by
592
    agent_rep for each estimate).
593
..
594
* If it has an `adapt <CompositionFunctionApproximator.adapt>` method, that must accept as its first three
595
  arguments, in order:
596

597
  - values that correspond to the shape of the  `state_feature_values
598
    <OptimizationControlMechanism.state_feature_values>` (inputs that led to the net_come);
599
  - `control_allocation <ControlMechanism.control_allocation>` (set of parameters that led to the net_outcome);
600
  - `net_outcome <ControlMechanism.net_outcome>` (the net_outcome that resulted from the `state_feature_values
601
    <OptimizationControlMechanism.state_feature_values>` and `control_allocation
602
    <ControlMechanism.control_allocation>`) that must match the shape of `outcome <ControlMechanism.outcome>`.
603
  COMMENT:
604
  - `num_estimates <OptimizationControlMechanism.num_trials_per_estimate>` (number of estimates of `net_outcome
605
    <ControlMechanism.net_outcome>` made for each `control_allocation <ControlMechanism.control_allocation>`).
606
  COMMENT
607

608
.. _OptimizationControlMechanism_State:
609

610
*State*
611
~~~~~~~
612

613
The current state of the OptimizationControlMechanism -- or, more properly, of its `agent_rep
614
<OptimizationControlMechanism.agent_rep>` -- is determined by the OptimizationControlMechanism's current
615
`state_feature_values <OptimizationControlMechanism.state_feature_values>` (see `below
616
<OptimizationControlMechanism_State_Input_Ports>`) and `control_allocation <ControlMechanism.control_allocation>`.
617
These are used by the `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` method,
618
the results of which are combined with the `costs <ControlMechanism_Costs_NetOutcome>` associated with the
619
`control_allocation <ControlMechanism.control_allocation>`, to evaluate the `net_outcome
620
<ControlMechanism.net_outcome>` for that state. The current state is listed in the OptimizationControlMechanism's
621
`state <OptimizationControlMechanism.state>` attribute, and `state_dict <OptimizationControlMechanism.state_dict>`
622
contains the Components associated with each value of `state <OptimizationControlMechanism.state>`.
623

624
COMMENT:
625
          > Attributes that pertain to the state of the agent_rep for a given evaluation:
626
              state_feature_specs: a list of the sources for state_feature_values, one for each InputPort of each INPUT Node of the agent_rep
627
                                   (at all levels of nesting);  None for any sources not specified in **state_features**
628
                                   (corresponding InputPorts are are assigned their default input when agent_rep.evaluate() is executed).
629
              state_feature_values: a dict with entries for each item specified in **state_features** arg of constructor,
630
                                    in which the key of each entry is an InputPort of an INPUT Node of agent_rep (at any level of nesting)
631
                                    and the value is the current value of the corresponding state_input_port;  the dict is suitable for
632
                                    use as the **predicted_inputs** or **feature_values** arg of the agent_rep's evaluate() method
633
                                    (depending on whether the agent_rep is a Composition or a CFA);
634
                                    note:  there are not entries for InputPorts of INPUT Nodes that are not specified in **state_features**;
635
                                           those are assigned either their default input values (LINK XXX) or the shadowed input of
636
                                           the corresponding INPUT Node InputPort (LINK XXX), depending on how **state_features** was formatted;
637
                                           (see LINK XXX for details of formatting).
638
              state_features: a dict with entries corresponding to each item of state_feature_specs,
639
                              the keys of which are InputPorts of the INPUT Nodes of the agent_rep,
640
                              and values of which are the corresponding state_feature_specs
641
                              (i.e., sources of input for those InputPorts when evaluate() is called);
642
              control_allocation: a list of the current values of the OCM's control_signals that are used to modulate the Parameters
643
                                  specified for control when the agent_rep's evaluate() method is called;
644
              state: a list of the values of the current state, starting with state_feature_values and ending with
645
                     control_allocations
646
              state_dict: a dictionary with entries for each state_feature and ControlSignal, keys?? values??
647
                                   their source/destination, and their current values
648
          > Constituents of state specifications:
649
            - agent_rep_input_port:  an InputPort of an INPUT Node of the agent_rep,
650
                                     that will receive a value from state_feature_values passed to agent_rep.evaluate()
651
            - source: the source of the input to an agent_rep_input_port,
652
                      that sends a Projection to the corresponding state_input_port
653

654
          > Relationship of numeric spec to ignoring it (i.e. assigning it None):
655
               allows specification of value as input *just* for simulations (i.e., agent_rep_evaluate)
656
               and not normal execution of comp
657

658
COMMENT
659

660
.. _OptimizationControlMechanism_Input:
661

662
*Input*
663
^^^^^^^
664

665
An OptimizationControlMechanism has two types of `input_ports <Mechanism_Base.input_ports>`, corresponding to the two
666
forms of input it requires: `state_input_ports <OptimizationControlMechanism.state_input_ports>` that provide the values
667
of the Components specified as its `state_features <OptimizationControlMechanism_State_Features_Arg>`, and that are used
668
as inputs to the `agent_rep <OptimizationControlMechanism.agent_rep>` when its `evaluate <Composition.evaluate>` method
669
is used to execute it; and `outcome_input_ports <OptimizationControlMechanism.outcome_input_ports>` that provide the
670
outcome of executing the `agent_rep <OptimizationControlMechanism.agent_rep>`, that is used to compute the `net_outcome
671
<ControlMechanism.net_outcome>` for the `control_allocation <ControlMechanism.control_allocation>` under which the
672
execution occurred. Each of these is described below.
673

674
.. _OptimizationControlMechanism_State_Input_Ports:
675

676
*state_input_ports*
677
~~~~~~~~~~~~~~~~~~~
678

679
The `state_input_ports <OptimizationControlMechanism.state_input_ports>` receive `Projections <Projection>`
680
from the Components specified as the OptimizationControlMechanism's `state_features
681
<OptimizationControlMechanism_State_Features_Arg>`, the values of which are assigned as the `state_feature_values
682
<OptimizationControlMechanism.state_feature_values>`, and conveyed to the `agent_rep
683
<OptimizationControlMechanism.agent_rep>`\\'s `evaluate <Composition.evaluate>` method when it is `executed
684
<OptimizationControlMechanism_Execution>`.  The OptimizationControlMechanism has a `state_input_port
685
<OptimizationControlMechanism.state_input_ports>` for every specification in the **state_features** arg of its
686
constructor (see `above <OptimizationControlMechanism_State_Features_Arg>`).
687

688
COMMENT:
689
OLD
690
If the `agent_rep is a Composition <OptimizationControlMechanism_Agent_Rep_Composition>`, then the
691
OptimizationControlMechanism has a state_input_port for every specification in the **state_features** arg of its
692
constructor (see `above <OptimizationControlMechanism_State_Features_Arg>`). If the `agent_rep is a
693
CompositionFunctionApproximator <OptimizationControlMechanism_Agent_Rep_CFA>`, then the OptimizationControlMechanism
694
has a state_input_port that receives a Projection from each Component specified in the **state_features** arg of its
695
constructor.
696
COMMENT
697

698
COMMENT:
699
In either, case the the `values <InputPort.value>` of the
700
`state_input_ports <OptimizationControlMechanism.state_input_ports>` are assigned to the `state_feature_values
701
<OptimizationControlMechanism.state_feature_values>` attribute that is used, in turn, by the
702
OptimizationControlMechanism's `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` method to
703
estimate or predict the `net_outcome <ControlMechanism.net_outcome>` for a given `control_allocation
704
<ControlMechanism.control_allocation>` (see `OptimizationControlMechanism_Execution`).
705

706
State features can be of two types:
707

708
* *Input Features* -- these are values that either shadow the input received by an `InputPort` of a `Mechanisms
709
  <Mechanism>` in the `Composition` for which the OptimizationControlMechanism is a `controller
710
  <Composition.controller>` (irrespective of whether that is the OptimizationControlMechanism`s `agent_rep
711
  <OptimizationControlMechanism.agent_rep>`). They are implemented as `shadow InputPorts <InputPort_Shadow_Inputs>`
712
  (see `OptimizationControlMechanism_SHADOW_INPUTS_State_Feature` for specification) that receive a `Projection`
713
  from the same source as the Mechanism being shadowed.
714
..
715
* *Output Features* -- these are the `value <OutputPort.value>` of an `OutputPort` of a `Mechanism <Mechanism>` in
716
  the `Composition` for which the OptimizationControlMechanism is a `controller <Composition.controller>` (again,
717
  irrespective of whether it is the OptimizationControlMechanism`s `agent_rep
718
  <OptimizationControlMechanism.agent_rep>`); and each is assigned a
719
  `Projection` from the specified OutputPort(s) to the InputPort of the OptimizationControlMechanism for that feature.
720

721
The InputPorts assigned to the **state_features** are listed in the OptimizationControlMechanism's `state_input_port
722
<OptimizationControlMechanism's.state_input_port>` attribute, and their current `values <InputPort.value>` are
723
listed in its `state_feature_values <OptimizationControlMechanism.state_feature_values>` attribute.
724

725
The InputPorts assigned to the **state_features** are listed in the OptimizationControlMechanism's `state_input_port
726
<OptimizationControlMechanism's.state_input_port>` attribute, and their current `values <InputPort.value>` are
727
listed in its `state_feature_values <OptimizationControlMechanism.state_feature_values>` attribute.
728
COMMENT
729

730
.. _OptimizationControlMechanism_Outcome:
731

732
*outcome_input_ports*
733
~~~~~~~~~~~~~~~~~~~~~
734

735
The `outcome_input_ports <OptimizationControlMechanism.outcome_input_ports>` comprise either a single `OutputPort`
736
that receives a `Projection` from the OptimizationControlMechanism's `objective_mechanism
737
<OptimizationControlMechanism_ObjectiveMechanism>` if has one; or, if it does not, then an OutputPort for each
738
Component it `monitors <OptimizationControlMechanism_Monitor_for_Control>` to determine the `net_outcome
739
<ControlMechanism.net_outcome>` of executing its `agent_rep <OptimizationControlMechanism.agent_rep>` (see `outcome
740
arguments <OptimizationControlMechanism_Outcome_Args>` for how these are specified). The value(s) of the
741
`outcome_input_ports <OptimizationControlMechanism.outcome_input_ports>` are assigned to the
742
OptimizationControlMechanism's `outcome <ControlMechanism.outcome>` attribute.
743

744
.. _OptimizationControlMechanism_ObjectiveMechanism:
745

746
*objective_mechanism*
747

748
If an OptimizationControlMechanism has an `objective_mechanism <ControlMechanism.objective_mechanism>`, it is
749
assigned a single outcome_input_port, named *OUTCOME*, that receives a Projection from the objective_mechanism's
750
`OUTCOME OutputPort <ObjectiveMechanism_Output>`. The OptimizationControlMechanism's `objective_mechanism
751
<ControlMechanism.objective_mechanism>` is used to evaluate the outcome of executing its `agent_rep
752
<OptimizationControlMechanism.agent_rep>` for a given `state <OptimizationControlMechanism_State>`. This passes
753
the result to the OptimizationControlMechanism's *OUTCOME* InputPort, that is placed in its `outcome
754
<ControlMechanism.outcome>` attribute.
755

756
    .. note::
757
        An OptimizationControlMechanism's `objective_mechanism <ControlMechanism.objective_mechanism>` and the `function
758
        <ObjectiveMechanism.function>` of that Mechanism, are distinct from and should not be confused with the
759
        `objective_function <OptimizationFunction.objective_function>` parameter of the OptimizationControlMechanism's
760
        `function <OptimizationControlMechanism.function>`.  The `objective_mechanism
761
        <ControlMechanism.objective_mechanism>`\\'s `function <ObjectiveMechanism.function>` evaluates the `outcome
762
        <ControlMechanism.outcome>` of processing without taking into account the `costs <ControlMechanism.costs>` of
763
        the OptimizationControlMechanism's `control_signals <OptimizationControlMechanism.control_signals>`.  In
764
        contrast, its `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` method, which is assigned
765
        as the `objective_function` parameter of its `function <OptimizationControlMechanism.function>`, takes the
766
        `costs <ControlMechanism.costs>` of the OptimizationControlMechanism's `control_signals
767
        <OptimizationControlMechanism.control_signals>` into account when calculating the `net_outcome` that it
768
        returns as its result.
769

770
COMMENT:
771
ADD HINT HERE RE: USE OF CONCATENATION
772

773
the items specified by `monitor_for_control
774
<ControlMechanism.monitor_for_control>` are all assigned `MappingProjections <MappingProjection>` to a single
775
*OUTCOME* InputPort.  This is assigned `Concatenate` as it `function <InputPort.function>`, which concatenates the
776
`values <Projection_Base.value>` of its Projections into a single array (that is, it is automatically configured
777
to use the *CONCATENATE* option of a ControlMechanism's `outcome_input_ports_option
778
<ControlMechanism.outcome_input_ports_option>` Parameter). This ensures that the input to the
779
OptimizationControlMechanism's `function <OptimizationControlMechanism.function>` has the same format as when an
780
`objective_mechanism <ControlMechanism.objective_mechanism>` has been specified, as described below.
781
COMMENT
782

783
.. _OptimizationControlMechanism_Monitor_for_Control:
784

785
*monitor_for_control*
786

787
If an OptimizationControlMechanism is not assigned an `objective_mechanism <ControlMechanism.objective_mechanism>`,
788
then its `outcome_input_ports <OptimizationControlMechanism.outcome_input_ports>` are determined by its
789
`monitor_for_control <ControlMechanism.monitor_for_control>` and `outcome_input_ports_option
790
<ControlMechanism.outcome_input_ports_option>` attributes, specified in the corresponding arguments of its
791
constructor (see `Outcomes arguments <OptimizationControlMechanism_Outcome_Args>`). The value(s) of the specified
792
Components are assigned as the OptimizationControlMechanism's `outcome <ControlMechanism.outcome>` attribute,
793
which is used to compute the `net_outcome <ControlMechanism.net_outcome>` of executing its `agent_rep
794
<OptimizationControlMechanism.agent_rep>`.
795

796
    .. note::
797
       If a `Node <Composition_Nodes>` other than an `OUTPUT <NodeRole.OUTPUT>` of a `nested <Composition_Nested>`
798
       Composition is `specified to be monitored <ControlMechanism_Monitor_for_Control>`, it is assigned as a `PROBE
799
       <NodeRole.PROBE>` of that nested Composition. Although `PROBE <NodeRole.PROBE>` Nodes are generally treated
800
       like `OUTPUT <NodeRole.OUTPUT>` Nodes (since they project out of the Composition to which they belong), their
801
       `value <Mechanism_Base.value>` is not included in the `output_values <Composition.output_values>` or `results
802
       <Composition.results>` attributes of the Composition for which the OptimizationControlMechanism is the
803
       `controller <Composition.controller>`, unless that Composition's `include_probes_in_output
804
       <Composition.include_probes_in_output>` attribute is set to True (see Probes `Composition_Probes` for additional
805
       information).
806

807
.. _OptimizationControlMechanism_Function:
808

809
*Function*
810
^^^^^^^^^^
811

812
The `function <OptimizationControlMechanism.function>` of an OptimizationControlMechanism is used to find the
813
`control_allocation <ControlMechanism.control_allocation>` that optimizes the `net_outcome
814
<ControlMechanism.net_outcome>` for the current (or expected) `state <OptimizationControlMechanism_State>`.
815
It is generally an `OptimizationFunction`, which in turn has `objective_function
816
<OptimizationFunction.objective_function>`, `search_function <OptimizationFunction.search_function>` and
817
`search_termination_function <OptimizationFunction.search_termination_function>` methods, as well as a `search_space
818
<OptimizationFunction.search_space>` attribute.  The `objective_function <OptimizationFunction.objective_function>`
819
is automatically assigned the OptimizationControlMechanism's `evaluate_agent_rep
820
<OptimizationControlMechanism.evaluate_agent_rep>` method, that is used to evaluate each `control_allocation
821
<ControlMechanism.control_allocation>` sampled from the `search_space <OptimizationFunction.search_space>` by the
822
`search_function <OptimizationFunction.search_function>` until the `search_termination_function
823
<OptimizationFunction.search_termination_function>` returns `True`.  The `net_outcome <ControlMechanism.net_outcome>`
824
returned by the call to `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` is assigned to the
825
OptimizationControlMechanism's `optimal_net_outcome <OptimizationControlMechanism.optimal_net_outcome>` attribute,
826
and the `control_allocation  <ControlMechanism.control_allocation>` that yielded it is assigned to the
827
OptimizationControlMechanism's `optimal_control_allocation <OptimizationControlMechanism.optimal_control_allocation>`
828
attribute (see `OptimizationControlMechanism_Execution` for additional details).
829

830
.. _OptimizationControlMechanism_Custom_Function:
831

832
*Custom Function*
833
~~~~~~~~~~~~~~~~~
834

835
A custom function can be assigned as the OptimizationControlMechanism's `function
836
<OptimizationControlMechanism.function>`, however it must meet the following requirements:
837

838
  - It must accept as its first argument and return as its result an array with the same shape as the
839
    OptimizationControlMechanism's `control_allocation <ControlMechanism.control_allocation>`.
840
  ..
841
  - It must be able to execute the OptimizationControlMechanism's `evaluate_agent_rep
842
    <OptimizationControlMechanism.evaluate_agent_rep>` `num_estimates <OptimizationControlMechanism.num_estimates>`
843
    times, and aggregate the results in computing the `net_outcome <ControlMechanism.net_outcome>` for a given
844
    `control_allocation <ControlMechanism.control_allocation>` (see
845
    `OptimizationControlMechanism_Estimation_Randomization` for additional details).
846
  ..
847
  - It must implement a `reset` method that can accept as keyword arguments **objective_function**,
848
    **search_function**, **search_termination_function**, and **search_space**, and implement attributes
849
    with corresponding names.
850

851
.. _OptimizationControlMechanism_Search_Functions:
852

853
*Search Function, Search Space and Search Termination Function*
854
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
855

856
Subclasses of OptimizationControlMechanism may implement their own `search_function
857
<OptimizationControlMechanism.search_function>` and `search_termination_function
858
<OptimizationControlMechanism.search_termination_function>` methods, as well as a
859
`control_allocation_search_space <OptimizationControlMechanism.control_allocation_search_space>` attribute, that are
860
passed as parameters to the `OptimizationFunction` when it is constructed.  These can be specified in the
861
constructor for an `OptimizationFunction` assigned as the **function** argument in the
862
OptimizationControlMechanism's constructor, as long as they are compatible with the requirements of
863
the OptimizationFunction and OptimizationControlMechanism.  If they are not specified, then defaults specified
864
either by the OptimizationControlMechanism or the OptimizationFunction are used.
865

866
.. _OptimizationControlMechanism_Default_Function:
867

868
*Default Function: GridSearch*
869
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
870

871
If the **function** argument is not specified, the `GridSearch` `OptimizationFunction` is assigned as the default,
872
which evaluates the `net_outcome <ControlMechanism.net_outcome>` using the OptimizationControlMechanism's
873
`control_allocation_search_space <OptimizationControlMechanism.control_allocation_search_space>` as its
874
`search_space <OptimizationFunction.search_space>`, and returns the `control_allocation
875
<ControlMechanism.control_allocation>` that yields the greatest `net_outcome <ControlMechanism.net_outcome>`,
876
thus implementing a computation of `EVC <OptimizationControlMechanism_EVC>`.
877

878

879
.. _OptimizationControlMechanism_Output:
880

881
*Output*
882
^^^^^^^^
883

884
The output of OptimizationControlMechanism are its `control_signals <ControlMechanism.control_signals>` that implement
885
the `control_allocations <ControlMechanism.control_allocation>` it evaluates and optimizes. These their effects are
886
estimated over variation in the values of Components with random variables, then the OptimizationControlMechanism's
887
`control_signals <ControlMechanism.control_signals>` include an additional *RANDOMIZATION_CONTROL_SIGNAL* that
888
implements that variablity for the relevant Components, as described below.
889

890
.. _OptimizationControlMechanism_Randomization_Control_Signal:
891

892
*Randomization ControlSignal*
893
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
894

895
If `num_estimates <OptimizationControlMechanism.num_estimates>` is specified (that is, it is not None), and
896
`agent_rep <OptimizationControlMechanism.agent_rep>` has any `Components <Component>` with random variables
897
(that is, that call a randomization function) specified in the OptimizationControlMechanism's `random_variables
898
<OptimizationControlMechanism.random_variables>` attribute, then a `ControlSignal` is automatically added to the
899
OptimizationControlMechanism's `control_signals <OptimizationControlMechanism.control_signals>`, named
900
*RANDOMIZATION_CONTROL_SIGNAL*, that randomizes the values of the `random variables
901
<OptimizationControlMechanism.random_variables>` over estimates of its `net_outcome <ControlMechanism.net_outcome>`
902
for each `control_allocation <ControlMechanism.control_allocation>` If `num_estimates
903
<OptimizationControlMechanism.num_estimates>` is specified but `agent_rep <OptimizationControlMechanism.agent_rep>`
904
has not random variables, then a warning is issued and no *RANDOMIZATION_CONTROL_SIGNAL* is constructed. The
905
`initial_seed <OptimizationControlMechanism.initial_seed>` and `same_seed_for_all_allocations
906
<OptimizationControlMechanism.same_seed_for_all_allocations>` Parameters can also be used to further refine
907
randomization (see `OptimizationControlMechanism_Estimation_Randomization` for additional details).
908

909
.. technical_note::
910

911
    The *RANDOMIZATION_CONTROL_SIGNAL* ControlSignal sends a `ControlProjection` to the `ParameterPort` for the
912
    see `Parameter` of Components specified either in the OptimizationControlMechanism's `random_variables
913
    <OptimizationControlMechanism.random_variables>` attribute or that of the `agent_rep
914
    <OptimizationControlMechanism.agent_rep>` (see above). The *RANDOMIZATION_CONTROL_SIGNAL* is also included when
915
    constructing the `control_allocation_search_space <OptimizationFunction.control_allocation_search_space>` passed
916
    to the constructor for OptimizationControlMechanism's `function <OptimizationControlMechanism.function>`,
917
    as its **search_space** argument, along with the index of the *RANDOMIZATION_CONTROL_SIGNAL* as its
918
    **randomization_dimension** argument.
919

920
.. _OptimizationControlMechanism_Execution:
921

922
Execution
923
---------
924

925
When an OptimizationControlMechanism is executed, the `OptimizationFunction` assigned as it's `function
926
<OptimizationControlMechanism.function>` is used evaluate the effects of different `control_allocations
927
<ControlMechanism.control_allocation>` to find one that optimizes the `net_outcome <ControlMechanism.net_outcome>`;
928
that `control_allocation <ControlMechanism.control_allocation>` is then used when the Composition controlled by the
929
OptimizationControlMechanism is next executed.  The OptimizationFunction does this by either simulating performance
930
of the Composition or executing the CompositionFunctionApproximator that is its `agent_rep
931
<OptimizationControlMechanism.agent_rep>`.
932

933
.. _OptimizationControlMechanism_Execution_Timing:
934

935
*Timing of Execution*
936
^^^^^^^^^^^^^^^^^^^^^
937

938
When the OptimizationControlMechanism is executed is determined by the `controller_mode <Composition.controller_mode>`
939
of the Composition for which the OptimizationControlMechanism is the `controller <Composition_Controller>`:  if it is
940
set to *AFTER* (the default), the OptimizationControlMechanism is executed at the end of a `TRIAL <TimeScale.TRIAL>`,
941
after the Composition has executed, using `state_feature_value <OptimizationControlMechanism.state_feature_values>`
942
(including any inputs to the Composition) for that `TRIAL <TimeScale.TRIAL>`; if the `controller_mode
943
<Composition.controller_mode>` is *BEFORE*, then the OptimizationControlMechanism is executed before the Composition
944
that it controls, using `state_feature_value <OptimizationControlMechanism.state_feature_values>` (including any inputs
945
to the Composition) from the previous `TRIAL <TimeScale.TRIAL>`.
946

947
.. _OptimizationControlMechanism_Optimization_Procedure:
948

949
*Optimization Procedure*
950
^^^^^^^^^^^^^^^^^^^^^^^^
951

952
When an OptimizationControlMechanism is executed, it carries out the following steps to find a `control_allocation
953
<ControlMechanism.control_allocation>` that optmimzes performance of the Composition that it controls:
954

955
  .. _OptimizationControlMechanism_Adaptation:
956

957
  * *Adaptation* -- if the `agent_rep <OptimizationControlMechanism.agent_rep>` is a `CompositionFunctionApproximator`,
958
    its `adapt <CompositionFunctionApproximator.adapt>` method, allowing it to modify its parameters in order to better
959
    predict the `net_outcome <ControlMechanism.net_outcome>` for a given `state <OptimizationControlMechanism_State>`,
960
    based the state and `net_outcome <ControlMechanism.net_outcome>` of the previous `TRIAL <TimeScale.TRIAL>`.
961

962
  .. _OptimizationControlMechanism_Evaluation:
963

964
  * *Evaluation* -- the OptimizationControlMechanism's `function <OptimizationControlMechanism.function>` is
965
    called to find the `control_allocation <ControlMechanism.control_allocation>` that optimizes `net_outcome
966
    <ControlMechanism.net_outcome>` of its `agent_rep <OptimizationControlMechanism.agent_rep>` for the current
967
    `state <OptimizationControlMechanism_State>`. The way in which it searches for the best `control_allocation
968
    <ControlMechanism.control_allocation>` is determined by the type of `OptimizationFunction` assigned to `function
969
    <OptimizationControlMechanism.function>`, whereas the way that it evaluates each one is determined by the
970
    OptimizationControlMechanism's `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` method.
971
    More specifically, it carries out the following procedure:
972

973
    .. _OptimizationControlMechanism_Estimation:
974

975
    * *Estimation* - the `function <OptimizationControlMechanism.function>` selects a sample `control_allocation
976
      <ControlMechanism.control_allocation>` (using its `search_function <OptimizationFunction.search_function>`
977
      to select one from its `search_space <OptimizationFunction.search_space>`), and evaluates the `net_outcome
978
      <ControlMechanism.net_outcome>` for that `control_allocation <ControlMechanism.control_allocation>`.
979
      It does this by calling the OptimizationControlMechanism's `evaluate_agent_rep
980
      <OptimizationControlMechanism.evaluate_agent_rep>` method `num_estimates <OptimizationControlMechanism>`
981
      times, each of which uses the `state_feature_values <OptimizationControlMechanism.state_feature_values>`
982
      and `control_allocation <ControlMechanism.control_allocation>` as the input to the `agent_rep
983
      <OptimizationControlMechanism.agent_rep>`\\'s `evaluate <Composition.evaluate>` method, executing it for
984
      `num_trials_per_estimate <OptimizationControlMechanism.num_trials_per_estimate>` trials for each estimate.
985
      The `state_feature_values <OptimizationControlMechanism.state_feature_values>` and `control_allocation
986
      <ControlMechanism.control_allocation>` remain fixed for each estimate, but the random seeds of any Parameters
987
      that rely on randomization are varied, so that the values of those Parameters are randomly sampled for every
988
      estimate (see `OptimizationControlMechanism_Estimation_Randomization`).
989

990
    * *Aggregation* - the `function <OptimizationControlMechanism.function>`\\'s `aggregation_function
991
      <OptimizationFunction.aggregation_function>` is used to aggregate the `net_outcome
992
      <ControlMechanism.net_outcome>` over the all the estimates for a given `control_allocation
993
      <ControlMechanism.control_allocation>`, and the aggregated value is returned as the `outcome
994
      <ControlMechanism.outcome>` and used to the compute the `net_outcome <ControlMechanism.net_outcome>`
995
      for that `control_allocation <ControlMechanism.control_allocation>`.
996

997
    * *Termination* - the `function <OptimizationControlMechanism.function>` continues to evaluate samples of
998
      `control_allocations <ControlMechanism.control_allocation>` provided by its `search_function
999
      <OptimizationFunction.search_function>` until its `search_termination_function
1000
      <OptimizationFunction.search_termination_function>` returns `True`.
1001

1002
  .. _OptimizationControlMechanism_Control_Assignment:
1003

1004
  * *Assignment* - when the search completes, the `function <OptimizationControlMechanism.function>`
1005
    assigns the `control_allocation <ControlMechanism.control_allocation>` that yielded the optimal value of
1006
    `net_outcome <ControlMechanism.net_outcome>` to the OptimizationControlMechanism's `control_signals,
1007
    that compute their `values <ControlSignal.value>` which, in turn, are assigned to their `ControlProjections
1008
    <ControlProjection>` to `modulate the Parameters <ModulatorySignal_Modulation>` they control when the
1009
    Composition is next executed. That `control_allocation <ControlMechanism.control_allocation>` is also
1010
    assigned as the OptimizationControlMechanism's `optimal_control_allocation
1011
    <OptimizationControlMechanism.optimal_control_allocation>` attribute, and the 'net_outcome
1012
    <ControlMechanism.net_outcome>` that yield it is assigned to the OptimizationControlMechanism's
1013
    `optimal_net_outcome <OptimizationControlMechanism.optimal_net_outcome>` attribute.
1014

1015
.. _OptimizationControlMechanism_Estimation_Randomization:
1016

1017
*Randomization of Estimation*
1018
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1019

1020
If `num_estimates <OptimizationControlMechanism.num_estimates>` is specified (i.e., it is not None), then each
1021
`control_allocation <ControlMechanism.control_allocation>` is independently evaluated `num_estimates
1022
<OptimizationControlMechanism.num_estimates>` times (i.e., by that number of calls to the
1023
OptimizationControlMechanism's `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` method).
1024
The values of Components listed in the OptimizationControlMechanism's `random_variables
1025
<OptimizationControlMechanism.random_variables>` attribute are randomized over those estimates.  By default,
1026
this includes all Components in the `agent_rep <OptimizationControlMechanism.agent_rep>` with random variables
1027
(listed in its `random_variables <Composition.random_variables>` attribute).  However, if particular Components
1028
are specified in the **random_variables** argument of the OptimizationControlMechanism's constructor, then
1029
randomization is restricted to their values. Randomization over estimates can be further configured using the
1030
`initial_seed <OptimizationControlMechanism.initial_seed>` and `same_seed_for_all_allocations
1031
<OptimizationControlMechanism.same_seed_for_all_allocations>` attributes. The results of all the estimates for a
1032
given `control_allocation <ControlMechanism.control_allocation>` are aggregated by the `aggregation_function
1033
<OptimizationFunction.aggregation_function>` of the `OptimizationFunction` assigned to the
1034
OptimizationControlMechanism's `function <OptimizationControlMechanism>`, and used to compute the `net_outcome
1035
<ControlMechanism.net_outcome>` over the estimates for that `control_allocation <ControlMechanism.control_allocation>`
1036
(see `OptimizationControlMechanism_Execution` for additional details).
1037

1038
COMMENT:
1039
.. _OptimizationControlMechanism_Examples:
1040

1041
Examples
1042
--------
1043

1044
The table below lists `model-free <OptimizationControlMechanism_Model_Free>` and `model-based
1045
<ModelBasedOptimizationControlMechanism` subclasses of OptimizationControlMechanisms.
1046

1047
.. table:: **Model-Free and Model-Based OptimizationControlMechanisms**
1048

1049
   +-------------------------+----------------------+----------------------+---------------------+---------------------+------------------------------+
1050
   |                         |     *Model-Free*     |                           *Model-Based*                                                         |
1051
   +-------------------------+----------------------+----------------------+---------------------+---------------------+------------------------------+
1052
   |**Functions**            |`LVOCControlMechanism`| LVOMControlMechanism | MDPControlMechanism |`EVCControlMechanism`| ParameterEstimationMechanism |
1053
   +-------------------------+----------------------+----------------------+---------------------+---------------------+------------------------------+
1054
   |**learning_function**    |     `BayesGLM`       |        `pymc`        |    `BeliefUpdate`   |       *None*        |           `pymc`             |
1055
   +-------------------------+----------------------+----------------------+---------------------+---------------------+------------------------------+
1056
   |**function** *(primary)* |`GradientOptimization`|     `GridSearch`     |       `Sample`      |    `GridSearch`     |           `Sample`           |
1057
   +-------------------------+----------------------+----------------------+---------------------+---------------------+------------------------------+
1058
   |       *search_function* |  *follow_gradient*   |   *traverse_grid*    | *sample_from_dist*  |   *traverse_grid*   |      *sample_from_dist*      |
1059
   +-------------------------+----------------------+----------------------+---------------------+---------------------+------------------------------+
1060
   |    *objective_function* |    *compute_EVC*     |  *evaluate*,   |  *evaluate*,  |  *evaluate*,  |    *evaluate*,         |
1061
   |                         |                      |  *compute_EVC*       |  *compute_EVC*      |  *compute_EVC*      |    *compute_likelihood*      |
1062
   +-------------------------+----------------------+----------------------+---------------------+---------------------+------------------------------+
1063
   |             *execution* | *iterate w/in trial* |  *once per trial*    | *iterate w/in trial*| *iterate w/in trial*|     *iterate w/in trial*     |
1064
   +-------------------------+----------------------+----------------------+---------------------+---------------------+------------------------------+
1065

1066
The following models provide examples of implementing the OptimizationControlMechanisms in the table above:
1067

1068
`LVOCControlMechanism`\\:  `BustamanteStroopXORLVOCModel`
1069
`EVCControlMechanism`\\:  `UmemotoTaskSwitchingEVCModel`
1070
COMMENT
1071

1072

1073

1074
.. _OptimizationControlMechanism_Class_Reference:
1075

1076
Class Reference
1077
---------------
1078

1079
"""
1080
import ast
1✔
1081
import copy
1✔
1082
import warnings
1✔
1083
from collections.abc import Iterable
1✔
1084

1085
import numpy as np
1✔
1086
from beartype import beartype
1✔
1087

1088
from psyneulink._typing import Optional, Union, Callable
1✔
1089

1090
from psyneulink.core import llvm as pnlvm
1✔
1091
from psyneulink.core.components.component import DefaultsFlexibility, Component
1✔
1092
from psyneulink.core.components.functions.nonstateful.optimizationfunctions import \
1✔
1093
    GridSearch, OBJECTIVE_FUNCTION, SEARCH_SPACE, RANDOMIZATION_DIMENSION
1094
from psyneulink.core.components.functions.nonstateful.transferfunctions import CostFunctions
1✔
1095
from psyneulink.core.components.mechanisms.mechanism import Mechanism
1✔
1096
from psyneulink.core.components.mechanisms.modulatory.control.controlmechanism import \
1✔
1097
    ControlMechanism, ControlMechanismError
1098
from psyneulink.core.components.ports.inputport import InputPort, _parse_shadow_inputs
1✔
1099
from psyneulink.core.components.ports.modulatorysignals.controlsignal import ControlSignal
1✔
1100
from psyneulink.core.components.ports.outputport import OutputPort
1✔
1101
from psyneulink.core.components.ports.port import _parse_port_spec, _instantiate_port, Port
1✔
1102
from psyneulink.core.components.shellclasses import Function
1✔
1103
from psyneulink.core.globals.context import Context, ContextFlags
1✔
1104
from psyneulink.core.globals.context import handle_external_context
1✔
1105
from psyneulink.core.globals.defaults import defaultControlAllocation
1✔
1106
from psyneulink.core.globals.keywords import \
1✔
1107
    ALL, COMPOSITION, COMPOSITION_FUNCTION_APPROXIMATOR, CONCATENATE, DEFAULT_INPUT, DEFAULT_VARIABLE, EID_FROZEN, \
1108
    FUNCTION, INPUT_PORT, INTERNAL_ONLY, NAME, OPTIMIZATION_CONTROL_MECHANISM, NODE, OWNER_VALUE, PARAMS, PORT, PROJECTIONS, \
1109
    PROJECTIONS, SHADOW_INPUTS, VALUE, OVERRIDE
1110
from psyneulink.core.globals.parameters import Parameter, check_user_specified
1✔
1111
from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel
1✔
1112
from psyneulink.core.globals.registry import rename_instance_in_registry
1✔
1113
from psyneulink.core.globals.sampleiterator import SampleIterator, SampleSpec
1✔
1114
from psyneulink.core.globals.utilities import convert_to_list, convert_to_np_array, ContentAddressableList, is_numeric, object_has_single_value, try_extract_0d_array_item
1✔
1115
from psyneulink.core.llvm.debug import debug_env
1✔
1116

1117
__all__ = [
1✔
1118
    'OptimizationControlMechanism', 'OptimizationControlMechanismError',
1119
    'AGENT_REP', 'STATE_FEATURES', 'STATE_FEATURE_FUNCTION', 'RANDOMIZATION_CONTROL_SIGNAL', 'NUM_ESTIMATES',
1120
    'DEFERRED_STATE_INPUT_PORT_PREFIX', 'NUMERIC_STATE_INPUT_PORT_PREFIX', 'SHADOWED_INPUT_STATE_INPUT_PORT_PREFIX',
1121
    'INPUT_SOURCE_FOR_STATE_INPUT_PORT_PREFIX'
1122
]
1123

1124
# constructor arguments
1125
AGENT_REP = 'agent_rep'
1✔
1126
STATE_FEATURES = 'state_features'
1✔
1127
STATE_FEATURE_FUNCTION = 'state_feature_function'
1✔
1128
RANDOMIZATION_CONTROL_SIGNAL = 'RANDOMIZATION_CONTROL_SIGNAL'
1✔
1129
RANDOM_VARIABLES = 'random_variables'
1✔
1130
NUM_ESTIMATES = 'num_estimates'
1✔
1131

1132
# state_input_port names
1133
NUMERIC_STATE_INPUT_PORT_PREFIX = "NUMERIC INPUT FOR "
1✔
1134
INPUT_SOURCE_FOR_STATE_INPUT_PORT_PREFIX = "SOURCE OF INPUT FOR "
1✔
1135
SHADOWED_INPUT_STATE_INPUT_PORT_PREFIX = "SHADOWED INPUT OF "
1✔
1136
# SHADOWED_INPUT_STATE_INPUT_PORT_PREFIX = "Shadowed input of "
1137
DEFERRED_STATE_INPUT_PORT_PREFIX = 'DEFERRED INPUT NODE InputPort '
1✔
1138

1139
def _state_input_port_name(source_port_name, agent_rep_input_port_name):
1✔
1140
    return f"INPUT FROM {source_port_name} FOR {agent_rep_input_port_name}"
1✔
1141

1142
def _shadowed_state_input_port_name(shadowed_port_name, agent_rep_input_port_name):
1✔
1143
    return f"{SHADOWED_INPUT_STATE_INPUT_PORT_PREFIX}{shadowed_port_name} FOR {agent_rep_input_port_name}"
1✔
1144

1145
def _numeric_state_input_port_name(agent_rep_input_port_name):
1✔
1146
    return f"{NUMERIC_STATE_INPUT_PORT_PREFIX}{agent_rep_input_port_name}"
1✔
1147

1148
def _deferred_agent_rep_input_port_name(node_name, agent_rep_name):
1✔
1149
    # return f"{DEFERRED_STATE_INPUT_PORT_PREFIX}{node_name} OF {agent_rep_name}"
1150
    return f"{DEFERRED_STATE_INPUT_PORT_PREFIX}OF {agent_rep_name} ({node_name})"
1✔
1151

1152
def _deferred_state_feature_spec_msg(spec_str, comp_name):
1✔
1153
    return f"{spec_str} NOT (YET) IN {comp_name}"
1✔
1154

1155
def _not_specified_state_feature_spec_msg(spec_str, comp_name):
1✔
1156
    return f"NO SPECIFICATION (YET) FOR {spec_str} IN {comp_name}"
×
1157

1158
def _state_feature_values_getter(owning_component=None, context=None):
1✔
1159
    """Return dict {agent_rep INPUT Node InputPort: value} suitable for **predicted_inputs** arg of evaluate method.
1160
    Only include entries for sources specified in **state_features**, corresponding to OCM's state_input_ports;
1161
       default input will be assigned for all other INPUT Node InputPorts in composition._instantiate_input_dict().
1162
    """
1163

1164
    # FIX: REFACTOR TO DO VALIDATIONS ON INIT AND INITIAL RUN-TIME CHECK
1165
    #      AND SIMPLY RETURN VALUES (WHICH SHOULD ALL BE ASSIGNED BY RUN TIME) DURING EXECUTION / SIMULATIONS
1166

1167
    # If no state_input_ports return empty list
1168
    if (not owning_component.num_state_input_ports):
1✔
1169
        return {}
1✔
1170

1171
    # Get sources specified in **state_features**
1172
    specified_state_features = [spec for spec in owning_component.state_feature_specs if spec is not None]
1✔
1173
    # Get INPUT Node InputPorts for sources specified in **state_features**
1174
    specified_INPUT_Node_InputPorts = [port for port, spec
1✔
1175
                                       in zip(owning_component._specified_INPUT_Node_InputPorts_in_order,
1176
                                              owning_component.state_feature_specs)
1177
                                       if spec is not None]
1178
    num_agent_rep_input_ports = len(owning_component._get_agent_rep_input_receivers())
1✔
1179

1180
    assert len(specified_state_features) == \
1✔
1181
           len(specified_INPUT_Node_InputPorts) == \
1182
           owning_component.num_state_input_ports
1183

1184
    # Construct state_feature_values dict
1185
    state_feature_values = {}
1✔
1186
    for i in range(owning_component.num_state_input_ports):
1✔
1187
        key = specified_INPUT_Node_InputPorts[i]
1✔
1188
        state_input_port = owning_component.state_input_ports[i]
1✔
1189
        spec = specified_state_features[i]
1✔
1190

1191
        # Get key
1192
        if not isinstance(key, InputPort):
1✔
1193
            # INPUT Node InputPort is not fully or properly specified
1194
            key = _deferred_agent_rep_input_port_name((key or str(i - num_agent_rep_input_ports)),
1✔
1195
                                                  owning_component.agent_rep.name)
1196
        elif key not in owning_component._get_agent_rep_input_receivers():
1✔
1197
            # INPUT Node InputPort is not (yet) in agent_rep
1198
            key = _deferred_agent_rep_input_port_name(key.full_name, owning_component.agent_rep.name)
1✔
1199

1200
        # Get state_feature_value
1201
        if spec is None:
1!
1202
            # state_feature not specified; default input will be assigned in _instantiate_input_dict()
1203
            state_feature_value = _not_specified_state_feature_spec_msg((key if isinstance(key, str) else key.full_name),
×
1204
                                                                       owning_component.composition.name)
1205
        elif is_numeric(spec):
1✔
1206
            # if spec is numeric, use that
1207
            state_feature_value = state_input_port.function(spec)
1✔
1208
        elif (hasattr(owning_component, 'composition')
1✔
1209
              and not owning_component.composition._is_in_composition(spec)):
1210
            # spec is not in ocm.composition
1211
            state_feature_value = _deferred_state_feature_spec_msg(spec.full_name, owning_component.agent_rep.name)
1✔
1212
        elif (
1✔
1213
            state_input_port.parameters.value._has_value(context)
1214
            and state_input_port.parameters.value._get(context) is not None
1215
        ):
1216
            # if state_input_port returns a value, use that
1217
            state_feature_value = state_input_port.parameters.value._get(context)
1✔
1218
        else:
1219
            # otherwise use state_input_port's default input value
1220
            state_feature_value = state_input_port.default_input_shape
1✔
1221

1222
        state_feature_values[key] = state_feature_value
1✔
1223

1224
    return state_feature_values
1✔
1225

1226

1227
class OptimizationControlMechanismError(ControlMechanismError):
1✔
1228
    pass
1✔
1229

1230

1231
def _control_allocation_search_space_getter(owning_component=None, context=None):
1✔
1232
    search_space = owning_component.parameters.search_space._get(context)
1✔
1233
    if search_space is None:
1✔
1234
        return [c.parameters.allocation_samples._get(context, fallback_value=None) for c in owning_component.control_signals]
1✔
1235
    else:
1236
        return search_space
1✔
1237

1238

1239
class OptimizationControlMechanism(ControlMechanism):
1✔
1240
    """OptimizationControlMechanism(                    \
1241
        agent_rep=None,                                 \
1242
        state_features=None,                            \
1243
        state_feature_function=None,                    \
1244
        monitor_for_control=None,                       \
1245
        objective_mechanism=None,                       \
1246
        function=GridSearch,                            \
1247
        num_estimates=1,                                \
1248
        random_variables=ALL,                           \
1249
        initial_seed=None,                              \
1250
        same_seed_for_all_parameter_combinations=False  \
1251
        num_trials_per_estimate=None,                   \
1252
        search_function=None,                           \
1253
        search_termination_function=None,               \
1254
        search_space=None,                              \
1255
        control_signals=None,                           \
1256
        modulation=MULTIPLICATIVE,                      \
1257
        combine_costs=np.sum,                           \
1258
        compute_reconfiguration_cost=None,              \
1259
        compute_net_outcome=lambda x,y:x-y)
1260

1261
    Subclass of `ControlMechanism <ControlMechanism>` that adjusts its `ControlSignals <ControlSignal>` to optimize
1262
    performance of the `Composition` to which it belongs.  See `ControlMechanism <ControlMechanism_Class_Reference>`
1263
    for arguments not described here.
1264

1265
    Arguments
1266
    ---------
1267

1268
    state_features : Mechanism, InputPort, OutputPort, Projection, numeric value, dict, or list containing any of these
1269
        specifies the Components from which `state_input_ports <OptimizationControlMechanism.state_input_ports>`
1270
        receive their inputs, the `values <InputPort.value>` of which are assigned to `state_feature_values
1271
        <OptimizationControlMechanism.state_feature_values>` and provided as input to the `agent_rep
1272
        <OptimizationControlMechanism.agent_rep>'s `evaluate <Composition.evaluate>` method when it is executed.
1273
        See `state_features <OptimizationControlMechanism_State_Features_Arg>` for details of specification.
1274

1275
    state_feature_default : same as state_features : default None
1276
        specifies the default used if a state_feature is not otherwise specified for the `InputPort` of an
1277
        `INPUT <NodeRole.INPUT>` `Node <Composition_Nodes>` of `agent_rep <OptimizationControlMechanism.agent_rep>`.
1278
        (see `state_feature_default <OptimizationControlMechanism.state_feature_default>` and
1279
        `state_features <OptimizationControlMechanism_State_Features_Arg>` for additional details).
1280

1281
    state_feature_function : Function or function : default None
1282
        specifies the `function <InputPort.function>` to use as the default function for the `state_input_ports
1283
        <OptimizationControlMechanism.state_input_ports>` created for the corresponding **state_features** (see
1284
        `state_feature_function <OptimizationControlMechanism_State_Feature_Function_Arg>` for additional details).
1285

1286
    agent_rep : None or Composition  : default None or Composition to which OptimizationControlMechanism is assigned
1287
        specifies the `Composition` used by `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>`
1288
        to predict the `net_outcome <ControlMechanism.net_outcome>` for a given `state
1289
        <OptimizationControlMechanism_State>`.  If a Composition is specified, it must be suitably configured
1290
        (see `agent_rep <OptimizationControlMechanism_Agent_Rep_Arg>` for additional details).  It can also be a
1291
        `CompositionFunctionApproximator`, or subclass of one, used for `model-free
1292
        <OptimizationControlMechanism_Model_Free>` optimization. If **agent_rep** is not specified, the
1293
        OptimizationControlMechanism is placed in `deferred_init <Component_Deferred_Init>` status until it is assigned
1294
        as the `controller <Composition.controller>` of a Composition, at which time that Composition is assigned as
1295
        the `agent_rep <OptimizationControlMechanism.agent_rep>`.
1296

1297
    num_estimates : int : 1
1298
        specifies the number independent runs of `agent_rep <OptimizationControlMechanism.agent_rep>` randomized
1299
        over **random_variables** and used to estimate its `net_outcome <ControlMechanism.net_outcome>` for each
1300
        `control_allocation <ControlMechanism.control_allocation>` sampled (see `num_estimates
1301
        <OptimizationControlMechanism.num_estimates>` for additional information).
1302

1303
    random_variables : Parameter or list[Parameter] : default ALL
1304
        specifies the Components of `agent_rep <OptimizationControlMechanism.agent_rep>` with random variables to be
1305
        randomized over different estimates of each `control_allocation <ControlMechanism.control_allocation>`;  these
1306
        must be in the `agent_rep <OptimizationControlMechanism.agent_rep>` and have a `seed` `Parameter`. By default,
1307
        all such Components (listed in its `random_variables <Composition.random_variables>` attribute) are included
1308
        (see `random_variables <OptimizationControlMechanism.random_variables>` for additional information).
1309

1310
        .. note::
1311
           if **num_estimates** is specified but `agent_rep <OptimizationControlMechanism.agent_rep>` has no
1312
           `random variables <Composition.random_variables>`, a warning is generated and `num_estimates
1313
           <OptimizationControlMechanism.num_estimates>` is set to None.
1314

1315
    initial_seed : int : default None
1316
        specifies the seed used to initialize the random number generator at construction.
1317
        If it is not specified then then the seed is set to a random value (see `initial_seed
1318
        <OptimizationControlMechanism.initial_seed>` for additional information).
1319

1320
    same_seed_for_all_parameter_combinations :  bool : default False
1321
        specifies whether the random number generator is re-initialized to the same value when estimating each
1322
        `control_allocation <ControlMechanism.control_allocation>` (see `same_seed_for_all_parameter_combinations
1323
        <OptimizationControlMechanism.same_seed_for_all_allocations>` for additional information).
1324

1325
    num_trials_per_estimate : int : default None
1326
        specifies the number of trials to execute in each run of `agent_rep
1327
        <OptimizationControlMechanism.agent_rep>` by a call to `evaluate_agent_rep
1328
        <OptimizationControlMechanism.evaluate_agent_rep>` (see `num_trials_per_estimate
1329
        <OptimizationControlMechanism.num_trials_per_estimate>` for additional information).
1330

1331
    search_function : function or method
1332
        specifies the function assigned to `function <OptimizationControlMechanism.function>` as its
1333
        `search_function <OptimizationFunction.search_function>` parameter, unless that is specified in a
1334
        constructor for `function <OptimizationControlMechanism.function>`.  It must take as its arguments
1335
        an array with the same shape as `control_allocation <ControlMechanism.control_allocation>` and an integer
1336
        (indicating the iteration of the `optimization process <OptimizationFunction_Procedure>`), and return
1337
        an array with the same shape as `control_allocation <ControlMechanism.control_allocation>`.
1338

1339
    search_termination_function : function or method
1340
        specifies the function assigned to `function <OptimizationControlMechanism.function>` as its
1341
        `search_termination_function <OptimizationFunction.search_termination_function>` parameter, unless that is
1342
        specified in a constructor for `function <OptimizationControlMechanism.function>`.  It must take as its
1343
        arguments an array with the same shape as `control_allocation <ControlMechanism.control_allocation>` and two
1344
        integers (the first representing the `net_outcome <ControlMechanism.net_outcome>` for the current
1345
        `control_allocation <ControlMechanism.control_allocation>`, and the second the current iteration of the
1346
        `optimization process <OptimizationFunction_Procedure>`);  it must return `True` or `False`.
1347

1348
    search_space : iterable [list, tuple, ndarray, SampleSpec, or SampleIterator] | list, tuple, ndarray, SampleSpec, or SampleIterator
1349
        specifies the `search_space <OptimizationFunction.search_space>` parameter for `function
1350
        <OptimizationControlMechanism.function>`, unless that is specified in a constructor for `function
1351
        <OptimizationControlMechanism.function>`.  An element at index i should correspond to an element at index i in
1352
        `control_allocation <ControlMechanism.control_allocation>`. If
1353
        `control_allocation <ControlMechanism.control_allocation>` contains only one element, then search_space can be
1354
        specified as a single element without an enclosing iterable.
1355

1356
    function : OptimizationFunction, function or method
1357
        specifies the function used to optimize the `control_allocation <ControlMechanism.control_allocation>`;
1358
        must take as its sole argument an array with the same shape as `control_allocation
1359
        <ControlMechanism.control_allocation>`, and return a similar array (see `Function
1360
        <OptimizationControlMechanism_Function>` for additional details).
1361

1362
    Attributes
1363
    ----------
1364

1365
    agent_rep : Composition
1366
        determines the `Composition` used by the `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>`
1367
        method to predict the `net_outcome <ControlMechanism.net_outcome>` for a given `state
1368
        <OptimizationControlMechanism_State>`; see `Agent Representation <OptimizationControlMechanism_Agent_Rep>`
1369
        for additional details.
1370

1371
    agent_rep_type : None, COMPOSITION or COMPOSITION_FUNCTION_APPROXIMATOR
1372
        identifies whether the agent_rep is a `Composition`, a `CompositionFunctionApproximator` or
1373
        one of its subclasses, or it has not been assigned (None) (see `Agent Representation and Types
1374
        of Optimization <OptimizationControlMechanism_Agent_Representation_Types>` for additional details).
1375

1376
    state_features : Dict[Node:source]
1377
        dictionary in which keys are all `external InputPorts <Composition_Input_External_InputPorts>` for `agent_rep
1378
        <OptimizationControlMechanism.agent_rep>`, and values are the sources of their input specified in
1379
        **state_features**. These are provided as the inputs to `state_input_ports
1380
        <OptimizationControlMechanism.state_input_ports>`, the `values <InputPort.value>`
1381
        of which are assigned to `state_feature_values <OptimizationControlMechanism.state_feature_values>` and
1382
        provided to the `agent_rep <OptimizationControlMechanism.agent_rep>`\\'s `evaluate <Composition.evaluate>`
1383
        method when it is executed (see `state_features <OptimizationControlMechanism_State_Features_Arg>` and
1384
        `OptimizationControlMechanism_State_Input_Ports` for additional details).
1385

1386
    state_feature_default : Mechanism, InputPort, OutputPort, Projection, dict, SHADOW_INPUTS, numeric value
1387
        determines the default used if the state_feature (i.e. source) is not otherwise specified for the `InputPort` of
1388
        an `INPUT <NodeRole.INPUT>` `Node <Composition_Nodes>` of `agent_rep <OptimizationControlMechanism.agent_rep>`.
1389
        If it is None, then no corresponding `state_input_port <OptimizationControlMechanism.state_input_ports>`
1390
        is created for that InputPort, and its `default variable <Component.defaults>` is used as its input when the
1391
        `agent_rep <OptimizationControlMechanism.agent_rep>`\\'s `evaluate <Composition.evaluate>` method is executed
1392
        (see `state_features <OptimizationControlMechanism_State_Features_Arg>` for additional details).
1393

1394
    state_feature_values : 2d array
1395
        a dict containing the current values assigned as the input to the InputPorts of the `INPUT <NodeRole.INPUT>`
1396
        `Nodes <Composition_Nodes>` of the `agent_rep <OptimizationControlMechanism.agent_rep>` when its `evaluate
1397
        <Composition.evaluate>` method is executed.  For each such InputPort, if a `state_feature
1398
        <OptimizationControlMechanism_State_Features_Arg>` has been specified for it, then its value in
1399
        state_feature_values is the `value <InputPort.value>` of the corresponding `state_input_port
1400
        <OptimizationControlMechanism.state_input_ports>`.  There are no entries for InputPorts for which the
1401
        **state_features** specification is ``None`` or it has not been otherwise specified;  for those InputPorts,
1402
        their `default_variable <Component.default_variable>` is assigned directly as their input when `agent_rep
1403
        <OptimizationControlMechanism.agent_rep>` is `evaluated <Composition.evaluate>` (see
1404
        `OptimizationControlMechanism_State_Input_Ports` for additional details).
1405

1406
    state_feature_function : Function of function
1407
        determines the `function <InputPort.function>` used as the default function for
1408
        `state_input_ports <OptimizationControlMechanism.state_input_ports>` (see `state_feature_function
1409
        <OptimizationControlMechanism_State_Feature_Function_Arg>` for additional details).
1410

1411
    state_input_ports : ContentAddressableList
1412
        lists the OptimizationControlMechanism's `InputPorts <InputPort>` that receive `Projections <Projection>`
1413
        from the items specified in the **state_features** argument in the OptimizationControlMechanism's constructor,
1414
        or constructed automatically (see `state_features <OptimizationControlMechanism_State_Features_Arg>`), the
1415
        values of which are assigned to `state_feature_values <OptimizationControlMechanism.state_feature_values>`
1416
        and provided as input to the `agent_rep <OptimizationControlMechanism.agent_rep>'s `evaluate
1417
        <Composition.evaluate>` method (see `OptimizationControlMechanism_State_Input_Ports` for additional details).
1418

1419
    num_state_input_ports : int
1420
        contains the number of `state_input_ports <OptimizationControlMechanism.state_input_ports>`.
1421

1422
    outcome_input_ports : ContentAddressableList
1423
        lists the OptimizationControlMechanism's `OutputPorts <OutputPort>` that receive `Projections <Projection>`
1424
        from either its `objective_mechanism <ControlMechanism.objective_mechanism>` or the Components listed in
1425
        its `monitor_for_control <ControlMechanism.monitor_for_control>` attribute, the values of which are used
1426
        to compute the `net_outcome <ControlMechanism.net_outcome>` of executing the `agent_rep
1427
        <OptimizationControlMechanism.agent_rep>` in a given `OptimizationControlMechanism_State`
1428
        (see `objective_mechanism <OptimizationControlMechanism_ObjectiveMechanism>` and `outcome_input_ports
1429
        <OptimizationControlMechanism_Outcome>` for additional details).
1430

1431
    state : ndarray
1432
        lists the values of the current state -- a concatenation of the `state_feature_values
1433
        <OptimizationControlMechanism.state_feature_values>` and `control_allocation
1434
        <ControlMechanism.control_allocation>` following the last execution of `agent_rep
1435
        <OptimizationControlMechanism.agent_rep>`.
1436

1437
    state_dict : Dict[(Port, Mechanism, Composition, index)):value]
1438
        dictionary containing information about the Components corresponding to the values in `state
1439
        <OptimizationControlMechanism.state>`.  Keys are (`Port`, `Mechanism`, `Composition`, index) tuples,
1440
        identifying the source of the value for each item at the corresponding index in
1441
        `state <OptimizationControlMechanism.state>`, and values are its value in `state
1442
        <OptimizationControlMechanism.state>`. The initial entries are for the OptimizationControlMechanism's
1443
        `state features <OptimizationControlMechanism.state_features>`, that are the sources of its
1444
        `state_feature_values <OptimizationControlMechanism.state_feature_values>`;  they are followed
1445
        by entries for the parameters modulated by the OptimizationControlMechanism's `control_signals
1446
        <OptimizationControlMechanism_Output>` with the corresponding `control_allocation
1447
        <ControlMechanism.control_allocation>` values.
1448

1449
    num_estimates : int
1450
        determines the number independent runs of `agent_rep <OptimizationControlMechanism.agent_rep>` (i.e., calls to
1451
        `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>`) used to estimate the `net_outcome
1452
        <ControlMechanism.net_outcome>` of each `control_allocation <ControlMechanism.control_allocation>` evaluated
1453
        by the OptimizationControlMechanism's `function <OptimizationControlMechanism.function>` (i.e.,
1454
        that are specified by its `search_space <OptimizationFunction.search_space>`); see
1455
        `OptimizationControlMechanism_Estimation_Randomization` for additional details.
1456

1457
    random_variables : Parameter or List[Parameter]
1458
        list of the `Parameters <Parameter>` in `agent_rep <OptimizationControlMechanism.agent_rep>` with random
1459
        variables (that is, ones that call a randomization function) that are randomized over estimates for a given
1460
        `control_allocation <ControlMechanism.control_allocation>`;  by default, all Components in the `agent_rep
1461
        <OptimizationControlMechanism.agent_rep>` with random variables are included (listed in its `random_variables
1462
        <Composition.random_variables>` attribute);  see `OptimizationControlMechanism_Estimation_Randomization`
1463
        for additional details.
1464

1465
    initial_seed : int or None
1466
        determines the seed used to initialize the random number generator at construction.
1467
        If it is not specified then then the seed is set to a random value, and different runs of a
1468
        Composition containing the OptimizationControlMechanism will yield different results, which should be roughly
1469
        comparable if the estimation process is stable.  If **initial_seed** is specified, then running the Composition
1470
        should yield identical results for the estimation process, which can be useful for debugging.
1471

1472
    same_seed_for_all_allocations :  bool
1473
        determines whether the random number generator used to select seeds for each estimate of the `agent_rep
1474
        <OptimizationControlMechanism.agent_rep>`\\'s `net_outcome <ControlMechanism.net_outcome>` is re-initialized
1475
        to the same value for each `control_allocation <ControlMechanism.control_allocation>` evaluated.
1476
        If same_seed_for_all_allocations is True, then any differences in the estimates made of `net_outcome
1477
        <ControlMechanism.net_outcome>` for each `control_allocation <ControlMechanism.control_allocation>` will
1478
        reflect exclusively the influence of the different control_allocations on the execution of the `agent_rep
1479
        <OptimizationControlMechanism.agent_rep>`, and *not* any variability intrinsic to the execution of
1480
        the Composition itself (e.g., any of its Components). This can be confirmed by identical results for repeated
1481
        executions of the OptimizationControlMechanism's `evaluate_agent_rep
1482
        <OptimizationControlMechanism.evaluate_agent_rep>` method for the same `control_allocation
1483
        <ControlMechanism.control_allocation>`. If same_seed_for_all_allocations is False, then each time a
1484
        `control_allocation <ControlMechanism.control_allocation>` is estimated, it will use a different set of seeds.
1485
        This can be confirmed by differing results for repeated executions of the OptimizationControlMechanism's
1486
        `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` method with the same `control_allocation
1487
        <ControlMechanism.control_allocation>`). Small differences in results suggest
1488
        stability of the estimation process across `control_allocations <ControlMechanism.control_allocation>`, while
1489
        substantial differences indicate instability, which may be helped by increasing `num_estimates
1490
        <OptimizationControlMechanism.num_estimates>`.
1491

1492
    num_trials_per_estimate : int or None
1493
        imposes an exact number of trials to execute in each run of `agent_rep <OptimizationControlMechanism.agent_rep>`
1494
        used to evaluate its `net_outcome <ControlMechanism.net_outcome>` by a call to the
1495
        OptimizationControlMechanism's `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` method.
1496
        If it is None (the default), then either the number of **inputs** or the value specified for **num_trials** in
1497
        the Composition's `run <Composition.run>` method used to determine the number of trials executed (see
1498
        `number of trials <Composition_Execution_Num_Trials>` for additional information).
1499

1500
    function : OptimizationFunction, function or method
1501
        takes current `control_allocation <ControlMechanism.control_allocation>` (as initializer),
1502
        uses its `search_function <OptimizationFunction.search_function>` to select samples of `control_allocation
1503
        <ControlMechanism.control_allocation>` from its `search_space <OptimizationFunction.search_space>`,
1504
        evaluates these using its `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` method by
1505
        calling it `num_estimates <OptimizationControlMechanism.num_estimates>` times to estimate its `net_outcome
1506
        `net_outcome <ControlMechanism.net_outcome>` for a given `control_allocation
1507
        <ControlMechanism.control_allocation>`, and returns the one that yields the optimal `net_outcome
1508
        <ControlMechanism.net_outcome>` (see `Function <OptimizationControlMechanism_Function>` for additional details).
1509

1510
    evaluate_agent_rep : function or method
1511
        returns the `net_outcome(s) <ControlMechanism.net_outcome>` for a given `state
1512
        <OptimizationControlMechanism_State>` (i.e., combination of `state_feature_values
1513
        <OptimizationControlMechanism.state_feature_values>` and `control_allocation
1514
        <ControlMechanism.control_allocation>`). It is assigned as the `objective_function
1515
        <OptimizationFunction.objective_function>` parameter of `function
1516
        <OptimizationControlMechanism.function>`, and calls the `evaluate` method of the OptimizationControlMechanism's
1517
        `agent_rep <OptimizationControlMechanism.agent_rep>` with the current `state_feature_values
1518
        <OptimizationControlMechanism.state_feature_values>` and a specified `control_allocation
1519
        <ControlMechanism.control_allocation>`, which runs of the `agent_rep
1520
        <OptimizationControlMechanism.agent_rep>` for `num_trials_per_estimate
1521
        <OptimizationControlMechanism.num_trials_per_estimate>` trials. It returns an array containing the
1522
        `net_outcome <ControlMechanism.net_outcome>` of the run and, if the **return_results** argument is True,
1523
        an array containing the `results <Composition.results>` of the run. This method is `num_estimates
1524
        <OptimizationControlMechanism>` times by the OptimizationControlMechanism's `function
1525
        <OptimizationControlMechanism.function>`, which aggregates the `net_outcome <ControlMechanism.net_outcome>`
1526
        over those in evaluating a given `control_allocation <ControlMechanism.control_allocation>`
1527
        (see `OptimizationControlMechanism_Function` for additional details).
1528

1529
    search_function : function or method
1530
        `search_function <OptimizationFunction.search_function>` assigned to `function
1531
        <OptimizationControlMechanism.function>`; used to select samples of `control_allocation
1532
        <ControlMechanism.control_allocation>` to evaluate by `evaluate_agent_rep
1533
        <OptimizationControlMechanism.evaluate_agent_rep>`.
1534

1535
    search_termination_function : function or method
1536
        `search_termination_function <OptimizationFunction.search_termination_function>` assigned to
1537
        `function <OptimizationControlMechanism.function>`;  determines when to terminate the
1538
        `optimization process <OptimizationFunction_Procedure>`.
1539

1540
    control_signals : ContentAddressableList[ControlSignal]
1541
        list of the `ControlSignals <ControlSignal>` for the OptimizationControlMechanism for the Parameters being
1542
        optimized by the OptimizationControlMechanism, including any inherited from the `Composition` for which it is
1543
        the `controller <Composition.controller>` (this is the same as ControlMechanism's `output_ports
1544
        <Mechanism_Base.output_ports>` attribute). Each sends a `ControlProjection` to the `ParameterPort` for the
1545
        Parameter it controls when evaluating a `control_allocation <ControlMechanism.control_allocation>`. If
1546
        `num_estimates <OptimizationControlMechanism.num_estimates>` is specified (that is, it is not None), a
1547
        `ControlSignal` is added to control_signals, named *RANDOMIZATION_CONTROL_SIGNAL*, that is used to randomize
1548
        estimates of `outcome <ControlMechanism.outcome>` for a given `control_allocation
1549
        <ControlMechanism.control_allocation>` (see `OptimizationControlMechanism_Estimation_Randomization` for
1550
        details.)
1551

1552
    control_allocation_search_space : list of SampleIterators
1553
        `search_space <OptimizationFunction.search_space>` assigned by default to the
1554
        OptimizationControlMechanism's `function <OptimizationControlMechanism.function>`, that determines the
1555
        samples of `control_allocation <ControlMechanism.control_allocation>` evaluated by the `evaluate_agent_rep
1556
        <OptimizationControlMechanism.evaluate_agent_rep>` method.  This is a property that, unless overridden,
1557
        returns a list of the `SampleIterators <SampleIterator>` generated from the `allocation_samples
1558
        <ControlSignal.allocation_samples>` specifications for each of the OptimizationControlMechanism's
1559
        `control_signals <OptimizationControlMechanism.control_signals>`, and includes the
1560
        *RANDOMIZATION_CONTROL_SIGNAL* used to randomize estimates of each `control_allocation
1561
        <ControlMechanism.control_allocation>` (see `note <OptimizationControlMechanism_Randomization_Control_Signal>`
1562
        above).
1563

1564
    optimal_control_allocation : 1d array
1565
        the `control_allocation <ControlMechanism.control_allocation>` that yielded the optimal
1566
        `net_outcome <ControlMechanism.net_outcome>` in call to `evaluate_agent_rep <OptimizationControlMechanism>`.
1567

1568
    optimal_net_outcome : float
1569
        the `net_outcome <ControlMechanism.net_outcome>` for the `optimal_control_allocation
1570
        <OptimizationControlMechanism>` returned by call to `evaluate_agent_rep <OptimizationControlMechanism>`.
1571

1572
    saved_samples : list
1573
        contains all values of `control_allocation <ControlMechanism.control_allocation>` sampled by `function
1574
        <OptimizationControlMechanism.function>` if its `save_samples <OptimizationFunction.save_samples>` parameter
1575
        is `True`;  otherwise list is empty.
1576

1577
    saved_values : list
1578
        contains values of `net_outcome <ControlMechanism.net_outcome>` associated with all samples of
1579
        `control_allocation <ControlMechanism.control_allocation>` evaluated by by `function
1580
        <OptimizationControlMechanism.function>` if its `save_values <OptimizationFunction.save_samples>` parameter
1581
        is `True`;  otherwise list is empty.
1582

1583
    search_statefulness : bool : True
1584
        if True (the default), calls to `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>`
1585
        by the OptimizationControlMechanism's `function <OptimizationControlMechanism.function>` for each
1586
        `control_allocation <ControlMechanism.control_allocation>` will run as simulations in their own
1587
        `execution contexts <Composition_Execution_Context>`.  If *search_statefulness* is False, calls for each
1588
        `control_allocation <ControlMechanism.control_allocation>` will not be executed as independent simulations;
1589
        rather, all will be run in the same (original) execution context.
1590

1591
    value : 2d np.array
1592
        the `optimal_control_allocation <OptimizationControlMechanism.optimal_control_allocation>` returned by the
1593
        `OptimizationFunction <OptimizationControlMechanism.function>` assigned as the OptimizationControlMechanism's
1594
        `function <OptimizationControlMechanism.function>`, which is the `net_outcome
1595
        <ControlMechanism.net_outcome>` of the `agent_rep <OptimizationControlMechanism.agent_rep>`.
1596

1597
        .. technical_note::
1598
           This uses only the first value returned by the `OptimizationFunction <OptimizationControlMechanism.function>`
1599
           which also may return the value associated with the `optimal_control_allocation
1600
           <OptimizationControlMechanism.optimal_control_allocation>` as well as the full set of `control_allocations
1601
           <ControlMechanism.control_allocation>` and corresponding values (if the *save_samples* and/or *save_values*
1602
           arguments of the OptimizationControlMechanism's constructor are `True`);  these are stored in the
1603
           OptimizationControlMechanism's `saved_samples <OptimizationControlMechanism.saved_samples>` and
1604
           `saved_values <OptimizationControlMechanism.saved_values>` attributes, respectively.
1605

1606
    return_results: bool : False
1607
        if True, the complete simulation results are returned when invoking
1608
        `evaluate_agent_rep <OptimizationControlMechanism.evaluate_agent_rep>` calls. This is nescessary when using a
1609
        ParameterEstimationCompostion for parameter estimation via data fitting.
1610

1611
    """
1612

1613
    componentType = OPTIMIZATION_CONTROL_MECHANISM
1✔
1614
    # initMethod = INIT_FULL_EXECUTE_METHOD
1615
    # initMethod = INIT_EXECUTE_METHOD_ONLY
1616

1617
    classPreferenceLevel = PreferenceLevel.SUBTYPE
1✔
1618
    # classPreferenceLevel = PreferenceLevel.TYPE
1619
    # Any preferences specified below will override those specified in TYPE_DEFAULT_PREFERENCES
1620
    # Note: only need to specify setting;  level will be assigned to Type automatically
1621
    # classPreferences = {
1622
    #     PREFERENCE_SET_NAME: 'DefaultControlMechanismCustomClassPreferences',
1623
    #     PREFERENCE_KEYWORD<pref>: <setting>...}
1624

1625
    class Parameters(ControlMechanism.Parameters):
1✔
1626
        """
1627
            Attributes
1628
            ----------
1629

1630
                agent_rep
1631
                    see `agent_rep <OptimizationControlMechanism_Agent_Rep>`
1632

1633
                    :default value: None
1634
                    :type:
1635

1636
                comp_execution_mode
1637
                    see `comp_execution_mode <OptimizationControlMechanism.comp_execution_mode>`
1638

1639
                    :default value: `PYTHON`
1640
                    :type: ``str``
1641

1642
                control_allocation_search_space
1643
                    see `control_allocation_search_space <OptimizationControlMechanism.control_allocation_search_space>`
1644

1645
                    :default value: None
1646
                    :type:
1647

1648
                function
1649
                    see `function <OptimizationControlMechanism_Function>`
1650

1651
                    :default value: None
1652
                    :type:
1653

1654
                input_ports
1655
                    see `input_ports <Mechanism_Base.input_ports>`
1656

1657
                    :default value: ["{name: OUTCOME, params: {internal_only: True}}"]
1658
                    :type: ``list``
1659
                    :read only: True
1660

1661
                num_estimates
1662
                    see `num_estimates <OptimizationControlMechanism.num_estimates>`
1663

1664
                    :default value: None
1665
                    :type:
1666

1667
                num_trials_per_estimate
1668
                    see `num_trials_per_estimate <OptimizationControlMechanism.num_trials_per_estimate>`
1669

1670
                    :default value: None
1671
                    :type:
1672

1673
                outcome_input_ports_option
1674
                    see `outcome_input_ports_option <OptimizationControlMechanism.outcome_input_ports_option>`
1675

1676
                    :default value: None
1677
                    :type:
1678

1679
                saved_samples
1680
                    see `saved_samples <OptimizationControlMechanism.saved_samples>`
1681

1682
                    :default value: None
1683
                    :type:
1684

1685
                saved_values
1686
                    see `saved_values <OptimizationControlMechanism.saved_values>`
1687

1688
                    :default value: None
1689
                    :type:
1690

1691
                search_function
1692
                    see `search_function <OptimizationControlMechanism.search_function>`
1693

1694
                    :default value: None
1695
                    :type:
1696

1697
                search_statefulness
1698
                    see `search_statefulness <OptimizationControlMechanism.search_statefulness>`
1699

1700
                    :default value: True
1701
                    :type: ``bool``
1702

1703
                search_termination_function
1704
                    see `search_termination_function <OptimizationControlMechanism.search_termination_function>`
1705

1706
                    :default value: None
1707
                    :type:
1708

1709
                state_feature_specs
1710
                    This is for internal use only, including population of the state_features property
1711
                    (see `state_features <OptimizationControlMechanism.state_features>`)
1712

1713
                    :default value: SHADOW_INPUTS
1714
                    :type: ``dict``
1715

1716
                state_feature_values
1717
                    Returns values of `state_input_ports <OptimizationControlMechanism.state_input_ports>`
1718
                    used as inputs to simulation (see `state_feature_values
1719
                    <OptimizationControlMechanism.state_features>`)
1720

1721
                    :default value: SHADOW_INPUTS
1722
                    :type: ``dict``
1723

1724
                state_feature_default_spec
1725
                    This is a shell parameter to validate its assignment and explicity user specification of None
1726
                    to override Parameter default;  its .spec attribute is assigned to the user-facing
1727
                    self.state_feature_default (see `state_feature_default <Optimization.state_feature_default>`).
1728

1729
                    :default value: SHADOW_INPUTS
1730
                    :type:
1731

1732
                state_feature_function
1733
                    see `state_feature_function <OptimizationControlMechanism_Feature_Function>`
1734

1735
                    :default value: None
1736
                    :type:
1737

1738
                state_input_ports
1739
                    see `state_input_ports <OptimizationControlMechanism.state_input_ports>`
1740

1741
                    :default value: None
1742
                    :type:  ``list``
1743
        """
1744
        agent_rep = Parameter(None, stateful=False, loggable=False, pnl_internal=True, structural=True)
1✔
1745
        state_input_ports = Parameter(None, reference=True, stateful=False, loggable=False, read_only=True)
1✔
1746
        state_feature_specs = Parameter(SHADOW_INPUTS, stateful=False, loggable=False, read_only=True,
1✔
1747
                                        structural=True, parse_spec=True)
1748
        state_feature_default_spec = Parameter(SHADOW_INPUTS, stateful=False, loggable=False, read_only=True,
1✔
1749
                                               structural=True)
1750
        state_feature_function = Parameter(None, reference=True, stateful=False, loggable=False)
1✔
1751
        state_feature_values = Parameter(None, getter=_state_feature_values_getter,
1✔
1752
                                         user=False, pnl_internal=True, read_only=True)
1753
        outcome_input_ports_option = Parameter(CONCATENATE, stateful=False, loggable=False, structural=True)
1✔
1754
        function = Parameter(GridSearch, stateful=False, loggable=False)
1✔
1755
        search_function = Parameter(None, stateful=False, loggable=False)
1✔
1756
        search_space = Parameter(None, read_only=True)
1✔
1757
        search_termination_function = Parameter(None, stateful=False, loggable=False)
1✔
1758
        comp_execution_mode = Parameter('Python', stateful=False, loggable=False, pnl_internal=True)
1✔
1759
        search_statefulness = Parameter(True, stateful=False, loggable=False)
1✔
1760

1761
        # FIX: Should any of these be stateful?
1762
        random_variables = ALL
1✔
1763
        initial_seed = None
1✔
1764
        same_seed_for_all_allocations = False
1✔
1765
        num_estimates = None
1✔
1766
        num_trials_per_estimate = None
1✔
1767

1768
        # search_space = None
1769
        control_allocation_search_space = Parameter(
1✔
1770
            None,
1771
            read_only=True,
1772
            getter=_control_allocation_search_space_getter,
1773
            dependencies='search_space'
1774
        )
1775

1776
        saved_samples = None
1✔
1777
        saved_values = None
1✔
1778

1779
        def _validate_state_feature_default_spec(self, state_feature_default):
1✔
1780
            if not (isinstance(state_feature_default, (InputPort, OutputPort, Mechanism))
1!
1781
                    or state_feature_default in {SHADOW_INPUTS}
1782
                    or is_numeric(state_feature_default)
1783
                    or state_feature_default is None):
1784
                return f"must be an InputPort, OutputPort, Mechanism, Composition, SHADOW_INPUTS or a list or array " \
×
1785
                       f"with a shape appropriate for all of the INPUT Nodes or InputPorts to which it will be applied."
1786

1787
    @handle_external_context()
1✔
1788
    @check_user_specified
1✔
1789
    @beartype
1✔
1790
    def __init__(self,
1✔
1791
                 agent_rep=None,
1792
                 state_features: Optional[Union[str, Iterable, InputPort, OutputPort, Mechanism]] = SHADOW_INPUTS,
1793
                 # state_feature_default=None,
1794
                 state_feature_default: Optional[Union[str, Iterable, InputPort, OutputPort, Mechanism]] = SHADOW_INPUTS,
1795
                 state_feature_function: Optional[Union[dict, Callable]]=None,
1796
                 function=None,
1797
                 num_estimates=None,
1798
                 random_variables=None,
1799
                 initial_seed=None,
1800
                 same_seed_for_all_allocations=None,
1801
                 num_trials_per_estimate=None,
1802
                 search_function: Optional[Callable]=None,
1803
                 search_termination_function: Optional[Callable]=None,
1804
                 search_statefulness=None,
1805
                 return_results: bool = False,
1806
                 data=None,
1807
                 context=None,
1808
                 **kwargs):
1809
        """Implement OptimizationControlMechanism"""
1810

1811
        self.return_results = return_results
1✔
1812

1813
        # Legacy warnings and conversions
1814
        for k in kwargs.copy():
1✔
1815
            if k == 'features':
1!
1816
                if state_features:
×
1817
                    warnings.warn(f"Both 'features' and '{STATE_FEATURES}' were specified in the constructor "
×
1818
                                  f"for an {self.__class__.__name__}. Note: 'features' has been deprecated; "
1819
                                  f"'{STATE_FEATURES}' ({state_features}) will be used.")
1820
                else:
1821
                    warnings.warn(f"'features' was specified in the constructor for an {self.__class__.__name__}; "
×
1822
                                  f"Note: 'features' has been deprecated; please use '{STATE_FEATURES}' in the future.")
1823
                    state_features = kwargs['features']
×
1824
                kwargs.pop('features')
×
1825
                continue
×
1826
            if k == 'feature_function':
1!
1827
                if state_feature_function:
×
1828
                    warnings.warn(f"Both 'feature_function' and 'state_feature_function' were specified in the "
×
1829
                                  f"constructor for an {self.__class__.__name__}. Note: 'feature_function' has been "
1830
                                  f"deprecated; 'state_feature_function' ({state_feature_function}) will be used.")
1831
                else:
1832
                    warnings.warn(f"'feature_function' was specified in the constructor for an"
×
1833
                                  f"{self.__class__.__name__}; Note: 'feature_function' has been deprecated; "
1834
                                  f"please use 'state_feature_function' in the future.")
1835
                    state_feature_function = kwargs['feature_function']
×
1836
                kwargs.pop('feature_function')
×
1837
                continue
×
1838

1839
        function = function or GridSearch
1✔
1840

1841
        # If agent_rep hasn't been specified, put into deferred init
1842
        if agent_rep is None:
1✔
1843
            if context.source==ContextFlags.COMMAND_LINE:
1✔
1844
                # Temporarily name InputPort
1845
                self._assign_deferred_init_name(self.__class__.__name__)
1✔
1846
                # Store args for deferred initialization
1847
                self._store_deferred_init_args(**locals())
1✔
1848
                self._init_args['state_feature_specs'] = state_features
1✔
1849
                self._init_args['state_feature_default_spec'] = state_feature_default
1✔
1850

1851
                # Flag for deferred initialization
1852
                self.initialization_status = ContextFlags.DEFERRED_INIT
1✔
1853
                return
1✔
1854
            # If constructor is called internally (i.e., for controller of Composition),
1855
            # agent_rep needs to be specified
1856
            else:
1857
                assert False, f"PROGRAM ERROR: 'agent_rep' arg should have been specified " \
1858
                              f"in internal call to constructor for {self.name}."
1859

1860
        # If agent_rep is a Composition, but there are more state_features than INPUT Nodes,
1861
        #     defer initialization until they are added
1862
        elif agent_rep.componentCategory=='Composition':
1✔
1863
            from psyneulink.core.compositions.composition import NodeRole
1✔
1864
            if (state_features
1✔
1865
                    and len(convert_to_list(state_features)) > len(agent_rep.get_nodes_by_role(NodeRole.INPUT))):
1866
                # Temporarily name InputPort
1867
                self._assign_deferred_init_name(self.__class__.__name__)
1✔
1868
                # Store args for deferred initialization
1869
                self._store_deferred_init_args(**locals())
1✔
1870
                self._init_args['state_feature_specs'] = state_features
1✔
1871
                self._init_args['state_feature_default_spec'] = state_feature_default
1✔
1872
                # Flag for deferred initialization
1873
                self.initialization_status = ContextFlags.DEFERRED_INIT
1✔
1874
                return
1✔
1875

1876
        super().__init__(
1✔
1877
            agent_rep=agent_rep,
1878
            state_feature_specs=state_features,
1879
            state_feature_default_spec=state_feature_default,
1880
            state_feature_function=state_feature_function,
1881
            function=function,
1882
            num_estimates=num_estimates,
1883
            num_trials_per_estimate = num_trials_per_estimate,
1884
            random_variables=random_variables,
1885
            initial_seed=initial_seed,
1886
            same_seed_for_all_allocations=same_seed_for_all_allocations,
1887
            search_statefulness=search_statefulness,
1888
            search_function=search_function,
1889
            search_termination_function=search_termination_function,
1890
            **kwargs
1891
        )
1892

1893
    def _validate_params(self, request_set, target_set=None, context=None):
1✔
1894
        """Insure that specification of ObjectiveMechanism has projections to it"""
1895

1896
        super()._validate_params(request_set=request_set, target_set=target_set, context=context)
1✔
1897

1898
        from psyneulink.core.compositions.composition import Composition
1✔
1899
        if request_set[AGENT_REP] is None:
1✔
1900
            raise OptimizationControlMechanismError(f"The '{AGENT_REP}' arg of an {self.__class__.__name__} must "
1901
                                                    f"be specified and be a {Composition.__name__}")
1902

1903
        elif not (isinstance(request_set[AGENT_REP], Composition)
1✔
1904
                  or (isinstance(request_set[AGENT_REP], type) and issubclass(request_set[AGENT_REP], Composition))):
1905
            raise OptimizationControlMechanismError(f"The '{AGENT_REP}' arg of an {self.__class__.__name__} "
1906
                                                    f"must be either a {Composition.__name__} or a sublcass of one")
1907

1908
        elif request_set[STATE_FEATURE_FUNCTION]:
1✔
1909
            state_feats = request_set.pop(STATE_FEATURES, None)
1✔
1910
            state_feat_fcts = request_set.pop(STATE_FEATURE_FUNCTION, None)
1✔
1911
            # If no or only one item is specified in state_features, only one state_function is allowed
1912
            if ((not state_feats or len(convert_to_list(state_feats))==1)
1✔
1913
                    and len(convert_to_list(state_feat_fcts))!=1):
1914
                raise OptimizationControlMechanismError(f"Only one function is allowed to be specified for "
1915
                                                        f"the '{STATE_FEATURE_FUNCTION}' arg of {self.name} "
1916
                                                        f"if either no only one items is specified for its "
1917
                                                        f"'{STATE_FEATURES}' arg.")
1918
            if len(convert_to_list(state_feat_fcts))>1 and not isinstance(state_feat_fcts, dict):
1✔
1919
                raise OptimizationControlMechanismError(f"The '{STATE_FEATURES}' arg of {self.name} contains more "
1920
                                                        f"than one item, so its '{STATE_FEATURE_FUNCTION}' arg "
1921
                                                        f"must be either only a single function (applied to all "
1922
                                                        f"{STATE_FEATURES}) or a dict with entries of the form "
1923
                                                        f"<state_feature>:<function>.")
1924
            if len(convert_to_list(state_feat_fcts))>1:
1!
1925
                invalid_fct_specs = [fct_spec for fct_spec in state_feat_fcts if fct_spec not in state_feats]
×
1926
                if invalid_fct_specs:
×
1927
                    raise OptimizationControlMechanismError(f"The following entries of the dict specified for "
1928
                                                            f"'{STATE_FEATURE_FUNCTION} of {self.name} have keys that "
1929
                                                            f"do not match any InputPorts specified in its "
1930
                                                            f"{STATE_FEATURES} arg: {invalid_fct_specs}.")
1931

1932
        if self.random_variables is not ALL:
1!
1933
            invalid_params = [param.name for param in self.random_variables
×
1934
                              if param not in self.agent_rep.random_variables]
1935
            if invalid_params:
×
1936
                raise OptimizationControlMechanismError(f"The following Parameters were specified for the "
1937
                                                        f"{RANDOM_VARIABLES} arg of {self.name} that are do"
1938
                                                        f"randomizable (i.e., they do not have a 'seed' attribute: "
1939
                                                        f"{invalid_params}.")
1940

1941
    # FIX: CONSIDER GETTING RID OF THIS METHOD ENTIRELY, AND LETTING state_input_ports
1942
    #      BE HANDLED ENTIRELY BY _update_state_input_ports_for_controller
1943
    def _instantiate_input_ports(self, context=None):
1✔
1944
        """Instantiate InputPorts for state_features (with state_feature_function if specified).
1945

1946
        This instantiates the OptimizationControlMechanism's `state_input_ports`;
1947
             these are used to provide input to the agent_rep when its evaluate method is called
1948
        The OptimizationControlMechanism's outcome_input_ports are instantiated by
1949
            ControlMechanism._instantiate_input_ports in the call to super().
1950

1951
        InputPorts are constructed for **state_features** by calling _parse_state_feature_specs()
1952
            with them and **state_feature_function** arguments of the OptimizationControlMechanism constructor.
1953
        The constructed state_input_ports  are passed to ControlMechanism_instantiate_input_ports(),
1954
             which appends them to the InputPort(s) that receive input from the **objective_mechanism* (if specified)
1955
             or **monitor_for_control** ports (if **objective_mechanism** is not specified).
1956
        Also ensures that:
1957
             - every state_input_port has only a single Projection;
1958
             - every outcome_input_ports receive Projections from within the agent_rep if it is a Composition.
1959

1960
        If no **state_features** are specified in the constructor, assign ones for INPUT Nodes of owner.
1961
          - warn for use of CompositionFunctionApproximator as agent_rep;
1962
          - ignore here for Composition as agent_rep
1963
            (handled in _update_state_input_ports_for_controller).
1964

1965
        See `state_features <OptimizationControlMechanism_State_Features_Arg>` and
1966
        `OptimizationControlMechanism_State_Input_Ports` for additional details.
1967
        """
1968

1969
        # FIX: 11/3/21 :
1970
        #    ADD CHECK IN _parse_state_feature_specs() THAT IF A NODE RATHER THAN InputPort IS SPECIFIED,
1971
        #    ITS PRIMARY IS USED (SEE SCRATCH PAD FOR EXAMPLES)
1972
        if self.state_feature_specs is None:
1!
1973
            # If agent_rep is CompositionFunctionApproximator, warn if no state_features specified.
1974
            # Note: if agent rep is Composition, state_input_ports and any state_feature_function specified
1975
            #       are assigned in _update_state_input_ports_for_controller.
1976
            if self.agent_rep_type == COMPOSITION_FUNCTION_APPROXIMATOR:
×
1977
                warnings.warn(f"No '{STATE_FEATURES}' specified for use with `agent_rep' of {self.name}")
×
1978

1979
        # Implement any specified state_features
1980
        state_input_ports_specs = self._parse_state_feature_specs(context)
1✔
1981
        # Note:
1982
        #   if state_features were specified and agent_rep is a CompositionFunctionApproximator,
1983
        #   assume they are OK (no way to check their validity for agent_rep.evaluate() method, and skip assignment
1984

1985
        # Pass state_input_ports_sepcs to ControlMechanism for instantiation and addition to OCM's input_ports
1986
        super()._instantiate_input_ports(state_input_ports_specs, context=context)
1✔
1987

1988
        # Assign to self.state_input_ports attribute
1989
        start = self.num_outcome_input_ports # FIX: 11/3/21 NEED TO MODIFY IF OUTCOME InputPorts ARE MOVED
1✔
1990
        stop = start + len(state_input_ports_specs) if state_input_ports_specs else 0
1✔
1991
        self.parameters.state_input_ports.set(ContentAddressableList(component_type=InputPort,
1✔
1992
                                                                     list=self.input_ports[start:stop]),
1993
                                              override=True)
1994

1995
        # Ensure that every state_input_port has no more than one afferent projection
1996
        # FIX: NEED TO MODIFY IF OUTCOME InputPorts ARE MOVED
1997
        for i in range(self.num_outcome_input_ports, self.num_state_input_ports):
1✔
1998
            port = self.input_ports[i]
1✔
1999
            if len(port.path_afferents) > 1:
1✔
2000
                raise OptimizationControlMechanismError(f"Invalid {type(port).__name__} on {self.name}. "
2001
                                                        f"{port.name} should receive exactly one projection, "
2002
                                                        f"but it receives {len(port.path_afferents)} projections.")
2003

2004
    def _get_agent_rep_input_receivers(self, comp=None, type=PORT, comp_as_node=False):
1✔
2005
        if not self.agent_rep_type or self.agent_rep_type == COMPOSITION_FUNCTION_APPROXIMATOR:
1✔
2006
            return [None]
1✔
2007
        comp = comp or self.agent_rep
1✔
2008
        return comp._get_input_receivers(comp=comp, type=type, comp_as_node=comp_as_node)
1✔
2009

2010
    def _get_specs_not_in_agent_rep(self, state_feature_specs):
1✔
2011
        from psyneulink.core.compositions.composition import Composition
1✔
2012
        agent_rep_nodes = self.agent_rep._get_all_nodes()
1✔
2013
        return [spec for spec in state_feature_specs
1✔
2014
                if ((isinstance(spec, (Mechanism, Composition))
2015
                     and spec not in agent_rep_nodes)
2016
                    or (isinstance(spec, Port)
2017
                        and spec.owner not in agent_rep_nodes))]
2018

2019
    def _validate_input_nodes(self, nodes, enforce=None):
1✔
2020
        """Check that nodes are INPUT Nodes of agent_rep
2021
        INPUT Nodes are those at the top level of agent_rep as well as those of any Compositions nested within it
2022
            that are themselves INPUT Nodes of their enclosing Composition.
2023
        Raise exception for non-INPUT Nodes if **enforce** is specified; otherwise just issue warning.
2024
        """
2025
        from psyneulink.core.compositions.composition import Composition
1✔
2026
        agent_rep_input_nodes = self._get_agent_rep_input_receivers(type=NODE,comp_as_node=ALL)
1✔
2027
        agent_rep_input_ports = self._get_agent_rep_input_receivers(type=PORT)
1✔
2028
        agent_rep_all_nodes = self.agent_rep._get_all_nodes()
1✔
2029
        non_input_node_specs = [node for node in nodes
1✔
2030
                                if ((isinstance(node, (Mechanism, Composition)) and node not in agent_rep_input_nodes)
2031
                                    or (isinstance(node, Port) and (not isinstance(node, InputPort)
2032
                                                                    or node not in agent_rep_input_ports)))]
2033
        non_agent_rep_node_specs = [node for node in nodes
1✔
2034
                                    if ((isinstance(node, (Mechanism, Composition)) and node not in agent_rep_all_nodes) or
2035
                                        (isinstance(node, Port) and node.owner not in agent_rep_all_nodes))]
2036

2037
        # Deal with Nodes that are in agent_rep but not INPUT Nodes
2038
        if non_input_node_specs:
1✔
2039
            items = ', '.join([n._name for n in non_input_node_specs])
1✔
2040
            if len(non_input_node_specs) == 1:
1✔
2041
                items_str = f"contains an item ({items}) that is not an INPUT Node"
1✔
2042
            else:
2043
                items_str = f"contains items ({items}) that are not INPUT Nodes"
1✔
2044
            message = f"The '{STATE_FEATURES}' specified for '{self.name}' {items_str} " \
1✔
2045
                      f"within its {AGENT_REP} ('{self.agent_rep.name}'); only INPUT Nodes can be in a set " \
2046
                      f"or used as keys in a dict used to specify '{STATE_FEATURES}'."
2047
            if enforce:
1✔
2048
                raise OptimizationControlMechanismError(message)
2049
            else:
2050
                warnings.warn(message)
1✔
2051

2052
        # Deal with Nodes that are not in agent_rep
2053
        if non_agent_rep_node_specs:
1✔
2054
            items = ', '.join([n._name for n in non_agent_rep_node_specs])
1✔
2055
            singular = len(non_agent_rep_node_specs) == 1
1✔
2056
            if singular:
1✔
2057
                items_str = f"contains an item ({items}) that is"
1✔
2058
            else:
2059
                items_str = f"contains items ({items}) that are"
1✔
2060
            message = f"The '{STATE_FEATURES}' specified for '{self.name}' {items_str} not in its {AGENT_REP} " \
1✔
2061
                      f"('{self.agent_rep.name}'). Executing '{self.agent_rep.name}' before " \
2062
                      f"{'it is' if singular else 'they are'} added will generate an error ."
2063
            if enforce:
1✔
2064
                raise OptimizationControlMechanismError(message)
2065
            else:
2066
                warnings.warn(message)
1✔
2067

2068
    # FIX: 1/29/22 - REFACTOR TO SUPPORT InportPort SPECIFICATION DICT FOR MULT. PROJS. TO STATE_INPUT_PORT
2069
    def _parse_state_feature_specs(self, context=None):
1✔
2070
        """Parse entries of state_features specifications used to construct state_input_ports.
2071

2072
        Called from _instantiate_input_ports()
2073

2074
        Parse **state_features** arg of constructor for OptimizationControlMechanism, assigned to state_feature_specs.
2075

2076
        state_feature_specs lists sources of inputs to *all* INPUT Nodes of agent_rep, at all levels of nesting; there
2077
          is one entry for every INPUT Node in agent_rep, and every INPUT Node of any nested Composition that is
2078
          itself an INPUT Node at any level of nesting.
2079

2080
        Construct a state_input_port for every entry in state_feature_specs that is not None:
2081
          the value of those state_input_ports comprise the state_feature_values attribute, and are provided as the
2082
              input to the INPUT Nodes of agent_rep when its evaluate() method is executed (as the **predicted_inputs**
2083
              argument if agent_rep is a Composition, and the **feature_values** argument if it is a
2084
              CompositionFunctionApproximator); for INPUT;
2085
          for None entries in state_feature_specs, the corresponding INPUT Nodes are provided their
2086
              default_external_input_shape as their input when agent_rep.evaluate() executes.
2087

2088
        Projection(s) to state_input_ports from sources specified in state_feature_specs can be direct,
2089
            or indirect by way of a CIM if the source is in a nested Composition.
2090

2091
        Handle four formats:
2092

2093
        - dict {INPUT Node: source or None, INPUT Node or InputPort: source or None...}:
2094
            - every key must be an INPUT Node of agent_rep or an INPUT Node of a nested Composition within it that is
2095
                itself an INPUT Node of its enclosing Composition, or the external InputPort of one, at any level of
2096
                nesting;
2097
            - if a Mechanism is specified as a key, construct a state_input_port for each of its external InputPorts,
2098
                and assign the value of the dict entry as the source for all of them;
2099
            - if a Composition is specified as a key, construct a state_input_port for each external InputPort of each
2100
                of its INPUT Nodes, and those of any Compositions nested within it at all levels of nesting,
2101
                and assign the the value of the dict entry as the source for all of them;
2102
            - for INPUT Nodes not specified or assigned None as their value, assign corresponding entries in
2103
                state_feature_specs as state_feature_default
2104
            - if only one or some of the INPUT Nodes of a nested Composition are specified,
2105
                for the remaining ones assign the corresponding entries in state_feature_specs as state_feature_default
2106
            - if None is specified, don't construct a state_input_port
2107
        - list [source, None, source...]: specifies source specs for INPUT Node external InputPorts:
2108
            - must be listed in same order as *expanded* list of agent_rep INPUT Node external InputPorts to which they
2109
              correspond (i.e., nested Compositions that are INPUT Nodes replaced by their INPUT Nodes,
2110
              for all levels of nesting);
2111
            - if there are fewer sources listed than INPUT Node external InputPorts, assign state_feature_default to
2112
                the entries in state_feature_specs corresponding to the remaining INPUT Node external InputPorts
2113
            - if there more sources listed than INPUT Nodes, leave the excess ones, and label them as
2114
               'EXPECT <specified INPUT Node InputPort name>' for later resolution (see below).
2115

2116
        - set {INPUT Node, Input Node...}: specifies INPUT Nodes to be shadowed
2117
            - every item must be an INPUT Node of agent_rep or an INPUT Node of a nested Composition within it that
2118
                is itself an INPUT Node of its enclosing Composition, at any level of nesting;
2119
            - if a Composition is specified, construct a state_input_port for each of its INPUT Node extenal InputPorts,
2120
                and those of any Compositions nested within it at all levels of nesting, each of which shadows the
2121
                input of the corresponding INPUT Node (see _InputPort_Shadow_Inputs).
2122
            - if only one or some of the INPUT Nodes of a nested Composition are specified, use state_feature_default.
2123

2124
        IMPLEMENTATION NOTE: this is a legacy format for consistency with generic specification of shadowing inputs
2125
        - SHADOW_INPUTS dict {"SHADOW_INPUTS":[shadowable input, None, shadowable input...]}:
2126
            - all items must be a Mechanism (or one of its external InputPorts) that is an INPUT Node of agent_rep or
2127
                 of a nested Composition within it that is itself an INPUT Node;
2128
            - must be listed in same order as *expanded* list of agent_rep INPUT Nodes to which they correspond
2129
                (see list format above);
2130
            - construct a state_input_port for each non-None spec, and assign it a Projection that shadows the spec.
2131
                (see _InputPort_Shadow_Inputs).
2132

2133
        If shadowing is specified for an INPUT Node InputPort, set INTERNAL_ONLY to True in entry of params dict in
2134
            specification dictionary for corresponding state_input_port (so that inputs to Composition are not
2135
            required if the specified source is itself an INPUT Node).
2136

2137
        If an INPUT Node (or one of its external InputPorts) is specified that is not (yet) in agent_rep,
2138
            and/or a source is specified that is not yet in self.composition, warn and defer creating a
2139
            state_input_port;  final check is made, and error(s) generated for unresolved specifications at run time.
2140

2141
        Assign functions specified in **state_feature_function** to InputPorts for all state_features
2142

2143
        Return list of InputPort specification dictionaries for state_input_ports
2144
        """
2145

2146
        from psyneulink.core.compositions.composition import Composition, NodeRole
1✔
2147
        # Agent rep's input Nodes and their names
2148
        agent_rep_input_ports = self._get_agent_rep_input_receivers(type=PORT)
1✔
2149
        self._specified_INPUT_Node_InputPorts_in_order = []
1✔
2150

2151
        # List of assigned state_feature_function (vs. user provided specs)
2152
        self._state_feature_functions = []
1✔
2153

2154
        # VALIDATION AND WARNINGS -----------------------------------------------------------------------------------
2155

2156
        # Only list spec allowed if agent_rep is a CompositionFunctionApproximator
2157
        if self.agent_rep_type == COMPOSITION_FUNCTION_APPROXIMATOR and not isinstance(self.state_feature_specs, list):
1✔
2158
            agent_rep_name = f" ({self.agent_rep.name})" if not isinstance(self.agent_rep, type) else ''
1✔
2159
            raise OptimizationControlMechanismError(
2160
                f"The {AGENT_REP} specified for {self.name}{agent_rep_name} is a {COMPOSITION_FUNCTION_APPROXIMATOR}, "
2161
                f"so its '{STATE_FEATURES}' argument must be a list, not a {type(self.state_feature_specs).__name__} "
2162
                f"({self.state_feature_specs}).")
2163

2164
        # agent_rep has not yet been (fully) constructed
2165
        if not agent_rep_input_ports and self.agent_rep_type is COMPOSITION:
1✔
2166
            # FIX: 3/18/22 - ADD TESTS FOR THESE (in test_deferred_init or test_partial_deferred_init()?)
2167
            if (isinstance(self.state_feature_specs, set)
1✔
2168
                    or isinstance(self.state_feature_specs, dict) and SHADOW_INPUTS not in self.state_feature_specs):
2169
                # Dict and set specs reference Nodes that are not yet in agent_rep
2170
                warnings.warn(f"Nodes are specified in the {STATE_FEATURES}' arg for '{self.name}' that are not "
1✔
2171
                              f"(yet) in its its {AGENT_REP} ('{self.agent_rep.name}'). They must all be assigned "
2172
                              f"to it before the Composition is executed'.  It is generally safer to assign all "
2173
                              f"Nodes to the {AGENT_REP} of a controller before specifying its '{STATE_FEATURES}'.")
2174
            else:
2175
                # List and SHADOW_INPUTS specs are dangerous before agent_rep has been fully constructed
2176
                warnings.warn(f"The '{STATE_FEATURES}' arg for '{self.name}' has been specified before any Nodes have "
1✔
2177
                              f"been assigned to its {AGENT_REP} ('{self.agent_rep.name}').  Their order must be the "
2178
                              f"same as the order of the corresponding INPUT Nodes for '{self.agent_rep.name}' once "
2179
                              f"they are added, or unexpected results may occur.  It is safer to assign all Nodes to "
2180
                              f"the {AGENT_REP} of a controller before specifying its '{STATE_FEATURES}'.")
2181

2182
        # HELPER METHODS ------------------------------------------------------------------------------------------
2183

2184
        def expand_nested_input_comp_to_input_nodes(comp):
1✔
2185
            input_nodes = []
×
2186
            for node in comp.get_nodes_by_role(NodeRole.INPUT):
×
2187
                if isinstance(node, Composition):
×
2188
                    input_nodes.extend(expand_nested_input_comp_to_input_nodes(node))
×
2189
                else:
2190
                    input_nodes.append(node)
×
2191
            return input_nodes
×
2192

2193
        def get_port_for_mech_spec(spec:Union[Port,Mechanism]):
1✔
2194
            """Return port for Mechanism specified as state_feature
2195
            This is used to override the standard interpretation of a Mechanism in an InputPort specification:
2196
               - return Primary InputPort of Mechanism (to be shadowed) if agent_rep is Composition
2197
               - return Primary OutputPort of Mechanism (standard behavior) if agent_rep is a CFA
2198
            """
2199
            # assert isinstance(mech, Mechanism), \
2200
            #     f"PROGRAM ERROR: {mech} should be Mechanism in call to get_port_for_mech_spec() for '{self.name}'"
2201
            if isinstance(spec, Port):
1!
2202
                return spec
×
2203
            if self.agent_rep_type == COMPOSITION:
1✔
2204
                # FIX: 11/29/21: MOVE THIS TO _parse_shadow_inputs
2205
                #      (ADD ARG TO THAT FOR DOING SO, OR RESTRICT TO InputPorts IN GENERAL)
2206
                if len(spec.input_ports)!=1:
1✔
2207
                    raise OptimizationControlMechanismError(
2208
                        f"A Mechanism ({spec.name}) is specified to be shadowed in the '{STATE_FEATURES}' arg "
2209
                        f"for '{self.name}', but it has more than one {InputPort.__name__}; a specific one of its "
2210
                        f"{InputPort.__name__}s must be specified to be shadowed.")
2211
                return spec.input_port
1✔
2212
            else:
2213
                # agent_rep is a CFA
2214
                return spec.output_port
1✔
2215

2216
        # PARSE SPECS  ------------------------------------------------------------------------------------------
2217
        # Generate parallel lists of state feature specs (for sources of inputs)
2218
        #                            and INPUT Nodes to which they (if specified in dict or set format)
2219
        def _parse_specs(state_feature_specs, specified_input_ports=None, spec_type="list"):
1✔
2220
            """Validate and parse INPUT Node specs assigned to construct state_feature_specs
2221
            Validate number and identity of specs relative to agent_rep INPUT Nodes.
2222
            Assign spec for every INPUT Mechanism (nested) within agent_rep (i.e., for all nested Compositions)
2223
                as entries in state_feature_specs
2224
            Return names for use as state_input_port_names in main body of method
2225
            """
2226

2227
            parsed_feature_specs = []
1✔
2228
            num_user_specs = len(state_feature_specs)
1✔
2229
            num_specified_ports = len(specified_input_ports)
1✔
2230
            num_agent_rep_input_ports = len(agent_rep_input_ports)
1✔
2231
            # Total number of specs to be parsed:
2232
            self._num_state_feature_specs = max(num_user_specs, num_agent_rep_input_ports)
1✔
2233

2234
            assert num_user_specs == num_specified_ports, f"ALERT: num state_feature_specs != num ports in _parse_spec()"
1✔
2235
            # Note: there may be more state_feature_specs (i.e., ones for unspecified input_ports)
2236
            #       than num_specified_ports
2237

2238
            if self.agent_rep_type == COMPOSITION:
1✔
2239

2240
                # FIX: 3/18/22 - THESE SEEM DUPLICATIVE OF _validate_state_features;  JUST CALL THAT HERE?
2241
                #               ALSO, WARNING IS TRIGGERED IF MECHANIMS RATHER THAN ITS INPUT_PORTS ARE SPEC'D
2242
                #              AT THE LEAST, MOVE TO THEIR OWN VALIDATION HELPER METHOD
2243
                # Too FEW specs for number of agent_rep receivers
2244
                if len(self.state_feature_specs) < num_agent_rep_input_ports:
1✔
2245
                    warnings.warn(f"There are fewer '{STATE_FEATURES}' specified for '{self.name}' than the number "
1✔
2246
                                  f"of {InputPort.__name__}'s for all of the INPUT Nodes of its {AGENT_REP} "
2247
                                  f"('{self.agent_rep.name}'); the remaining inputs will be assigned default values "
2248
                                  f"when '{self.agent_rep.name}`s 'evaluate' method is executed. If this is not the "
2249
                                  f"desired behavior, use its get_inputs_format() method to see the format for its "
2250
                                  f"inputs.")
2251

2252
                # Too MANY specs for number of agent_rep receivers
2253
                if num_user_specs > num_agent_rep_input_ports:
1✔
2254
                    # specs_not_in_agent_rep = [f"'{spec.name if isinstance(spec, Mechanism) else spec.owner.name}'"
2255
                    #                           for spec in self._get_specs_not_in_agent_rep(state_feature_specs)]
2256
                    specs_not_in_agent_rep = \
1✔
2257
                        [f"'{spec.name if isinstance(spec,(Mechanism, Composition)) else spec.owner.name}'"
2258
                         for spec in self._get_specs_not_in_agent_rep(user_specs)]
2259

2260
                    if specs_not_in_agent_rep:
1✔
2261
                        spec_type = ", ".join(specs_not_in_agent_rep)
1✔
2262
                        warnings.warn(
1✔
2263
                            f"The '{STATE_FEATURES}' specified for {self.name} is associated with a number of "
2264
                            f"{InputPort.__name__}s ({len(state_feature_specs)}) that is greater than for the "
2265
                            f"{InputPort.__name__}s of the INPUT Nodes ({num_agent_rep_input_ports}) for the "
2266
                            f"Composition assigned as its {AGENT_REP} ('{self.agent_rep.name}'), which includes "
2267
                            f"the following that are not (yet) in '{self.agent_rep.name}': {spec_type}. Executing "
2268
                            f"{self.name} before the additional item(s) are added as (part of) INPUT Nodes will "
2269
                            f"generate an error.")
2270
                    else:
2271
                        warnings.warn(
1✔
2272
                            f"The '{STATE_FEATURES}' specified for {self.name} is associated with a number of "
2273
                            f"{InputPort.__name__}s ({len(state_feature_specs)}) that is greater than for the "
2274
                            f"{InputPort.__name__}s of the INPUT Nodes ({num_agent_rep_input_ports}) for the "
2275
                            f"Composition assigned as its {AGENT_REP} ('{self.agent_rep.name}'). Executing "
2276
                            f"{self.name} before the additional item(s) are added as (part of) INPUT Nodes will "
2277
                            f"generate an error.")
2278

2279
            # Nested Compositions not allowed to be specified in a list spec
2280
            nested_comps = [node for node in state_feature_specs if isinstance(node, Composition)]
1✔
2281
            if nested_comps:
1✔
2282
                comp_names = ", ".join([f"'{n.name}'" for n in nested_comps])
1✔
2283
                raise OptimizationControlMechanismError(
2284
                    f"The '{STATE_FEATURES}' argument for '{self.name}' includes one or more Compositions "
2285
                    f"({comp_names}) in the {spec_type} specified for its '{STATE_FEATURES}' argument; these must be "
2286
                    f"replaced by direct references to the Mechanisms (or their InputPorts) within them to be "
2287
                    f"shadowed.")
2288

2289
            state_input_port_names = []
1✔
2290
            for i in range(self._num_state_feature_specs):
1✔
2291

2292
                state_input_port_name = None
1✔
2293
                state_feature_fct = None
1✔
2294

2295
                # FIX: CONSOLIDATE THIS WITH PARSING OF SPEC BELOW
2296
                # AGENT_REP INPUT NODE InputPort
2297
                #    Assign it's name to be used in state_features
2298
                # (and specs for CFA and any Nodes not yet in agent_rep)
2299
                if self.agent_rep_type == COMPOSITION:
1✔
2300
                    if i < num_agent_rep_input_ports:
1✔
2301
                        # spec is for Input{ort of INPUT Node already in agent_rep
2302
                        #    so get agent_rep_input_port and its name (spec will be parsed and assigned below)
2303
                        agent_rep_input_port = agent_rep_input_ports[i]
1✔
2304
                        agent_rep_input_port_name = agent_rep_input_port.full_name
1✔
2305
                    else:
2306
                        # spec is for deferred NODE InputPort (i.e., not (yet) in agent_rep)
2307
                        #    so get specified value for spec, for later parsing and assignment (once Node is known)
2308
                        agent_rep_input_port = specified_input_ports[i]
1✔
2309
                        # - assign "DEFERRED n" as node name
2310
                        agent_rep_input_port_name = \
1✔
2311
                            _deferred_agent_rep_input_port_name(str(i - num_agent_rep_input_ports),
2312
                                                                self.agent_rep.name)
2313
                # For CompositionFunctionApproximator, assign spec as agent_rep_input_port
2314
                else:
2315
                    spec = state_feature_specs[i]
1✔
2316
                    agent_rep_input_port = spec
1✔
2317
                    agent_rep_input_port_name = spec.full_name if isinstance(spec, Port) else spec.name
1✔
2318
                    # Assign state_input_port_name here as won't get done below (i can't be < num_user_specs for CFA)
2319
                    state_input_port_name = f"FEATURE {i} FOR {self.agent_rep.name}"
1✔
2320

2321
                # SPEC and state_input_port_name
2322
                # Parse and assign user specifications (note: may be for INPUT Node InputPorts not yet inagent_rep)
2323
                if i < num_user_specs:               # i.e., if num_agent_rep_input_ports < num_user_specs)
1!
2324
                    spec = state_feature_specs[i]
1✔
2325
                    # Unpack tuple
2326

2327
                    if isinstance(spec, tuple):
1✔
2328
                        state_feature_fct = spec[1]
1✔
2329
                        spec = spec[0]
1✔
2330

2331
                    # Assign spec and state_input_port name
2332
                    if is_numeric(spec):
1✔
2333
                        state_input_port_name = _numeric_state_input_port_name(agent_rep_input_port_name)
1✔
2334

2335
                    elif isinstance(spec, (InputPort, Mechanism)):
1✔
2336
                        spec_name = spec.full_name if isinstance(spec, InputPort) else spec.input_port.full_name
1✔
2337
                        state_input_port_name = _shadowed_state_input_port_name(spec_name,
1✔
2338
                                                                                agent_rep_input_port_name)
2339
                    elif isinstance(spec, OutputPort):
1✔
2340
                        state_input_port_name = _state_input_port_name(spec.full_name,
1✔
2341
                                                                       agent_rep_input_port_name)
2342
                    elif isinstance(spec, Composition):
1✔
2343
                        assert False, f"Composition spec ({spec}) made it to _parse_specs for {self.name}."
2344

2345
                    elif spec == SHADOW_INPUTS:
1✔
2346
                        # Shadow the specified agent_rep_input_port (Name assigned where shadow input is parsed)
2347
                        spec = agent_rep_input_port
1✔
2348
                        state_input_port_name = _shadowed_state_input_port_name(agent_rep_input_port_name,
1✔
2349
                                                                                agent_rep_input_port_name)
2350

2351
                    elif isinstance(spec, dict):
1✔
2352
                        state_input_port_name = spec[NAME] if NAME in spec else f"INPUT FOR {agent_rep_input_port_name}"
1✔
2353
                        # tuple specification of function (assigned above) overrides dictionary specification
2354
                        if state_feature_fct is None:
1✔
2355
                            if FUNCTION in spec:
1✔
2356
                                state_feature_fct = spec[FUNCTION]
1✔
2357
                            elif PARAMS in spec and FUNCTION in spec[PARAMS]:
1!
2358
                                state_feature_fct = spec[PARAMS][FUNCTION]
1✔
2359

2360
                    elif spec is not None:
1✔
2361
                        assert False, f"PROGRAM ERROR: unrecognized form of state_feature specification for {self.name}"
2362

2363
                # Fewer specifications than number of INPUT Nodes, so assign state_feature_default to the rest
2364
                else:
2365
                    # Note: state_input_port name assigned above
2366
                    spec = self.state_feature_default
×
2367

2368
                parsed_feature_specs.append(spec)
1✔
2369
                self._state_feature_functions.append(state_feature_fct)
1✔
2370
                self._specified_INPUT_Node_InputPorts_in_order.append(agent_rep_input_port)
1✔
2371
                state_input_port_names.append(state_input_port_name)
1✔
2372

2373
            if not any(self._state_feature_functions):
1✔
2374
                self._state_feature_functions = None
1✔
2375
            self.parameters.state_feature_specs.set(parsed_feature_specs, override=True)
1✔
2376
            return state_input_port_names or []
1✔
2377

2378
        # END OF PARSE SPECS  -----------------------------------------------------------------------------------
2379

2380
        user_specs = self.parameters.state_feature_specs.spec
1✔
2381
        self.state_feature_default = self.parameters.state_feature_default_spec.spec
1✔
2382

2383
        # SINGLE ITEM spec, SO APPLY TO ALL agent_rep_input_ports
2384
        if (user_specs is None
1✔
2385
                or isinstance(user_specs, (str, tuple, InputPort, OutputPort, Mechanism, Composition))
2386
                or (is_numeric(user_specs) and object_has_single_value(user_specs))):
2387
            specs = [user_specs] * len(agent_rep_input_ports)
1✔
2388
            # OK to assign here (rather than in _parse_secs()) since spec is intended for *all* state_input_ports
2389
            self.parameters.state_feature_specs.set(specs, override=True)
1✔
2390
            state_input_port_names = _parse_specs(state_feature_specs=specs,
1✔
2391
                                                  specified_input_ports=agent_rep_input_ports,
2392
                                                  spec_type='list')
2393

2394
        # LIST OR SHADOW_INPUTS DICT: source specs
2395
        # Source specs but not INPUT Nodes specified; spec is either:
2396
        # - list:  [spec, spec...]
2397
        # - SHADOW_INPUTS dict (with list spec as its only entry): {SHADOW_INPUTS: {[spec, spec...]}}
2398
        # Treat specs as sources of input to INPUT Nodes of agent_rep (in corresponding order):
2399
        # Call _parse_specs to construct a regular dict using INPUT Nodes as keys and specs as values
2400
        elif isinstance(user_specs, (list, np.ndarray)) or (isinstance(user_specs, dict) and SHADOW_INPUTS in user_specs):
1✔
2401
            if isinstance(user_specs, (list, np.ndarray)):
1✔
2402
                num_missing_specs = len(agent_rep_input_ports) - len(self.state_feature_specs)
1✔
2403
                specs = list(user_specs) + [self.state_feature_default] * num_missing_specs
1✔
2404
                spec_type = 'list'
1✔
2405
            else:
2406
                # SHADOW_INPUTS spec:
2407
                if isinstance(user_specs[SHADOW_INPUTS], set):
1✔
2408
                    # Set not allowed as SHADOW_INPUTS spec; catch here to provide context-relevant error message
2409
                    raise OptimizationControlMechanismError(
2410
                        f"The '{STATE_FEATURES}' argument for '{self.name}' uses a set in a '{SHADOW_INPUTS.upper()}' "
2411
                        f"dict;  this must be a single item or list of specifications in the order of the INPUT Nodes"
2412
                        f"of its '{AGENT_REP}' ({self.agent_rep.name}) to which they correspond." )
2413
                # FIX: 3/18/22 - ?DOES THIS NEED TO BE DONE HERE, OR CAN IT BE DONE IN A RELEVANT _validate METHOD?
2414
                # All specifications in list specified for SHADOW_INPUTS must be shadowable
2415
                #     (i.e., either an INPUT Node or the InputPort of one) or None;
2416
                #     note: if spec is not in agent_rep, might be added later,
2417
                #           so defer dealing with that until runtime.
2418
                bad_specs = [spec for spec in user_specs[SHADOW_INPUTS]
1✔
2419
                             if (    # spec is not an InputPort
2420
                                     ((isinstance(spec, Port) and not isinstance(spec, InputPort))
2421
                                      # spec is an InputPort of a Node in agent_rep but not one of its INPUT Nodes
2422
                                      or (isinstance(spec, InputPort)
2423
                                          and spec.owner in self.agent_rep._get_all_nodes()
2424
                                          and spec.owner not in self._get_agent_rep_input_receivers(type=NODE,
2425
                                                                                                    comp_as_node=ALL))
2426
                                      )
2427
                                     and spec is not None)]
2428
                if bad_specs:
1✔
2429
                    bad_spec_names = [f"'{item.owner.name}'" if hasattr(item, 'owner')
1✔
2430
                                      else f"'{item.name}'" for item in bad_specs]
2431
                    raise OptimizationControlMechanismError(
2432
                        f"The '{STATE_FEATURES}' argument for '{self.name}' has one or more items in the list "
2433
                        f"specified for '{SHADOW_INPUTS.upper()}' ({', '.join([name for name in bad_spec_names])}) "
2434
                        f"that are not (part of) any INPUT Nodes of its '{AGENT_REP}' ('{self.agent_rep.name}')." )
2435

2436
                specs = user_specs[SHADOW_INPUTS]
1✔
2437
                spec_type = f"{SHADOW_INPUTS.upper()} dict"
1✔
2438

2439
            specified_input_ports = agent_rep_input_ports + [None] * (len(specs) - len(agent_rep_input_ports))
1✔
2440
            state_input_port_names = _parse_specs(state_feature_specs=specs,
1✔
2441
                                                  specified_input_ports=specified_input_ports,
2442
                                                  spec_type=spec_type)
2443

2444
        # FIX: 2/25/22 - ?ITEMS IN set ARE SHADOWED, BUT UNSPECIFIED ITEMS IN SET AND DICT ARE ASSIGNED DEFAULT VALUES
2445
        # SET OR DICT: specification by INPUT Nodes
2446
        # INPUT Nodes of agent_rep specified in either a:
2447
        # - set, without source specs: {node, node...}
2448
        # - dict, with source specs for each: {node: spec, node: spec...}
2449
        # Call _parse_specs to convert to or flesh out dict with INPUT Nodes as keys and any specs as values
2450
        #  adding any unspecified INPUT Nodes of agent_rep and assiging default values for any unspecified values
2451
        elif isinstance(user_specs, (set, dict)):
1✔
2452

2453
            # FIX: 3/4/22 REINSTATE & MOVE TO ABOVE??
2454
            self._validate_input_nodes(set(user_specs))
1✔
2455

2456
            #  FIX: MOVE TO _validate_input_nodes
2457
            # Validate user_specs
2458
            internal_input_port_specs = [f"'{spec.full_name}'" for spec in user_specs
1✔
2459
                                         if isinstance(spec, InputPort) and spec.internal_only]
2460
            if internal_input_port_specs:
1✔
2461
                raise OptimizationControlMechanismError(
2462
                    f"The following {InputPort.__name__}s specified in the '{STATE_FEATURES}' arg for {self.name} "
2463
                    f"do not receive any external inputs and thus cannot be assigned '{STATE_FEATURES}': "
2464
                    f"{', '.join(internal_input_port_specs)}.")
2465
            non_input_port_specs = [f"'{spec.full_name}'" for spec in user_specs
1✔
2466
                                    if isinstance(spec, Port) and not isinstance(spec, InputPort)]
2467
            if non_input_port_specs:
1✔
2468
                raise OptimizationControlMechanismError(
2469
                    f"The following {Port.__name__}s specified in the '{STATE_FEATURES}' arg for {self.name} "
2470
                    f"are not {InputPort.__name__} and thus cannot be assigned '{STATE_FEATURES}': "
2471
                    f"{', '.join(non_input_port_specs)}.")
2472

2473
            expanded_specified_ports = []
1✔
2474
            expanded_dict_with_ports = {}
1✔
2475
            dict_format = isinstance(user_specs, dict)
1✔
2476

2477
            # Expand any Nodes specified in user_specs set or keys of dict to corresponding InputPorts
2478
            for spec in user_specs:
1✔
2479
                # Expand any specified Compositions into corresponding InputPorts for all INPUT Nodes (including nested)
2480
                if isinstance(spec, Composition):
1✔
2481
                    ports = self._get_agent_rep_input_receivers(spec)
1✔
2482
                # Expand any specified Mechanisms into corresponding InputPorts except any internal ones
2483
                elif isinstance(spec, Mechanism):
1✔
2484
                    ports = [input_port for input_port in spec.input_ports if not input_port.internal_only]
1✔
2485
                else:
2486
                    ports = [spec]
1✔
2487
                expanded_specified_ports.extend(ports)
1✔
2488
                if dict_format:
1✔
2489
                    # Assign values specified in user_specs dict to corresponding InputPorts
2490
                    expanded_dict_with_ports.update({port:user_specs[spec] for port in ports})
1✔
2491

2492
            # # Get specified ports in order of agent_rep INPUT Nodes, with None assigned to any unspecified InputPorts
2493
            all_specified_ports = [port if port in expanded_specified_ports
1✔
2494
                                   else None for port in agent_rep_input_ports]
2495
            # Get any not found anywhere (including nested) in agent_rep, which are placed at the end of list
2496
            all_specified_ports.extend([port for port in expanded_specified_ports if port not in agent_rep_input_ports])
1✔
2497

2498
            if isinstance(user_specs, set):
1✔
2499
                # Just pass ports;  _parse_specs assigns shadowing projections for them and None to all unspecified ones
2500
                specs = all_specified_ports
1✔
2501
            else:
2502
                # Pass values from user_spec dict to be parsed;
2503
                #    corresponding ports are safely in all_specified_ports
2504
                #    unspecified ports are assigned state_feature_default per requirements of list format
2505
                specs = [expanded_dict_with_ports[port] if port is not None and port in all_specified_ports
1✔
2506
                         else self.state_feature_default for port in all_specified_ports]
2507

2508
            state_input_port_names = _parse_specs(state_feature_specs=specs,
1✔
2509
                                                  specified_input_ports=list(all_specified_ports),
2510
                                                  spec_type='dict')
2511

2512
        else:
2513
            assert False, f"PROGRAM ERROR: Unanticipated type specified for '{STATE_FEATURES}' arg of '{self.name}: " \
2514
                          f"'{user_specs}' ({type(user_specs)})."
2515

2516
        # CONSTRUCT InputPort SPECS -----------------------------------------------------------------------------
2517

2518
        state_input_port_specs = []
1✔
2519

2520
        for i in range(self._num_state_feature_specs):
1✔
2521
            # Note: state_feature_specs have now been parsed (user's specs are in parameters.state_feature_specs.spec);
2522
            #       state_feature_specs correspond to all InputPorts of agent_rep's INPUT Nodes (including nested ones)
2523
            spec = self.state_feature_specs[i]
1✔
2524

2525
            if spec is None:
1✔
2526
                continue
1✔
2527

2528
            # FIX: 3/22/22 - COULD/SHOULD ANY OF THE FOLLOWING BE DONE IN _parse_specs():
2529

2530
            if isinstance(self.state_feature_specs[i], dict):
1✔
2531
                # If spec is an InputPort specification dict:
2532
                # - if a Mechanism is specified as the source, specify for shadowing:
2533
                #    - add SHADOW_INPUTS entry to specify shadowing of Mechanism's primary InputPort
2534
                #    - process dict through _parse_shadow_inputs(),
2535
                #      (preserves spec as dict in case other parameters (except function, handled below) are specified)
2536
                #    - replace self.state_feature_specs[i] with the InputPort (required by state_feature_values)
2537
                # - if a function is specified, clear from dict
2538
                #    - it has already been assigned to self._state_feature_functions in _parse_spec()
2539
                #    - it will be properly assigned to InputPort specification dict in _assign_state_feature_function
2540
                def _validate_entries(spec=None, source=None):
1✔
2541
                    if spec and source:
1!
2542
                        if (SHADOW_INPUTS in spec) or (PARAMS in spec and SHADOW_INPUTS in spec[PARAMS]):
1!
2543
                            error_msg = "both PROJECTIONS and SHADOW_INPUTS cannot specified"
×
2544
                        elif len(convert_to_list(source)) != 1:
1!
2545
                            error_msg = "PROJECTIONS entry has more than one source"
×
2546
                        return
1✔
2547
                    else:
2548
                        error_msg = f"missing a 'PROJECTIONS' entry specifying the source of the input."
×
2549
                    raise OptimizationControlMechanismError(f"Error in InputPort specification dictionary used in '"
2550
                                                            f"{STATE_FEATURES}' arg of '{self.name}': {error_msg}.")
2551
                # Handle shadowing of Mechanism as source
2552
                if PROJECTIONS in spec:
1✔
2553
                    source = get_port_for_mech_spec(spec.pop(PROJECTIONS))
1✔
2554
                    if spec:  # No need to check if nothing left in dict
1✔
2555
                        _validate_entries(spec, source)
1✔
2556
                    spec[SHADOW_INPUTS] = source
1✔
2557
                elif PARAMS in spec and PROJECTIONS in spec[PARAMS]:
1!
2558
                    source = get_port_for_mech_spec(spec[PARAMS].pop(PROJECTIONS))
1✔
2559
                    _validate_entries(spec, source)
1✔
2560
                    spec[PARAMS][SHADOW_INPUTS] = source
1✔
2561
                else:
2562
                    _validate_entries()
×
2563

2564
                # Clear FUNCTION entry
2565
                if self._state_feature_functions[i]:
1!
2566
                    spec.pop(FUNCTION, None)
1✔
2567
                    if PARAMS in spec:
1✔
2568
                        spec[PARAMS].pop(FUNCTION, None)
1✔
2569

2570
                spec = _parse_shadow_inputs(self, spec)[0] # _parse_shadow_inputs returns a list, so get item
1✔
2571
                self.state_feature_specs[i] = source
1✔
2572

2573
            if is_numeric(spec):
1✔
2574
                # Construct InputPort specification dict that configures it to use its InputPort.default_variable
2575
                #    as its input, and assigns the spec's value to the VALUE entry,
2576
                #    which assigns it as the value of the InputPort.default_variable
2577
                spec_val = copy.copy(spec)
1✔
2578
                spec = {VALUE: spec_val,
1✔
2579
                        PARAMS: {DEFAULT_INPUT: DEFAULT_VARIABLE}}
2580

2581
            if isinstance(spec, Mechanism):
1✔
2582
                # Replace with primary InputPort to be shadowed
2583
                spec = get_port_for_mech_spec(spec)
1✔
2584
                self.state_feature_specs[i] = spec
1✔
2585

2586
            # Get InputPort specification dictionary for state_input_port and update its entries
2587
            parsed_spec = _parse_port_spec(owner=self, port_type=InputPort, port_spec=spec,
1✔
2588
                                           context=Context(string='OptimizationControlMechanism._parse_specs'))
2589
            parsed_spec[NAME] = state_input_port_names[i]
1✔
2590
            if parsed_spec[PARAMS] and SHADOW_INPUTS in parsed_spec[PARAMS]:
1✔
2591
                # Composition._update_shadow_projections will take care of PROJECTIONS specification
2592
                parsed_spec[PARAMS][INTERNAL_ONLY]=True,
1✔
2593
                parsed_spec[PARAMS][PROJECTIONS]=None
1✔
2594
            # Assign function for state_input_port if specified---------------------------------------------------
2595
            parsed_spec = self._assign_state_feature_function(parsed_spec, i)
1✔
2596
            parsed_spec = [parsed_spec] # so that extend works below
1✔
2597
            state_input_port_specs.extend(parsed_spec)
1✔
2598

2599
        return state_input_port_specs
1✔
2600

2601
    def _assign_state_feature_function(self, specification_dict, idx=None):
1✔
2602
        """Assign any specified state_feature_function to corresponding state_input_ports
2603
        idx is index into self._state_feature_functions; if None, use self.state_feature_function specified by user
2604
        Specification in InputPort specification dictionary or **state_features** tuple
2605
            takes precedence over **state_feature_function** specification.
2606
        Assignment of function to dict specs handled above, so skip here
2607
        Return state_input_port_dicts with FUNCTION entries added as appropriate.
2608
        """
2609

2610
        # Note: state_feature_function has been validated in _validate_params
2611
        default_function = self.state_feature_function  # User specified in constructor
1✔
2612
        try:
1✔
2613
            if self._state_feature_functions:
1✔
2614
                assert len(self._state_feature_functions) == self._num_state_feature_specs, \
1✔
2615
                    f"PROGRAM ERROR: Length of _state_feature_functions for {self.name} should be same " \
2616
                    f"as number of state_input_port_dicts passed to _assign_state_feature_function"
2617
            state_feature_functions = self._state_feature_functions
1✔
2618
        except AttributeError:
×
2619
            # state_features assigned automatically in _update_state_input_ports_for_controller,
2620
            #    so _state_feature_functions (for individual state_features) not created
2621
            state_feature_functions = None
×
2622
        fct = state_feature_functions[idx] if state_feature_functions else None
1✔
2623
        if fct:
1✔
2624
            specification_dict[FUNCTION] = self._parse_state_feature_function(fct)
1✔
2625
        elif default_function and FUNCTION not in specification_dict[PARAMS]:
1✔
2626
            # Assign **state_feature_function** (aka default_function) if specified and no other has been specified
2627
            specification_dict[FUNCTION] = self._parse_state_feature_function(default_function)
1✔
2628
        return specification_dict
1✔
2629

2630
    def _parse_state_feature_function(self, feature_function):
1✔
2631
        if isinstance(feature_function, Function):
1✔
2632
            return copy.deepcopy(feature_function)
1✔
2633
        else:
2634
            return feature_function
1✔
2635

2636
    def _update_state_input_ports_for_controller(self, context=None):
1✔
2637
        """Check and update state_input_ports at run time if agent_rep is a Composition
2638

2639
        If no agent_rep has been specified or it is a CompositionFunctionApproximator, return
2640
            (note: validation of state_features specified for CompositionFunctionApproximator optimization
2641
            is up to the CompositionFunctionApproximator)
2642

2643
        If agent_rep is a Composition:
2644
           - if  has any new INPUT Node InputPorts:
2645
               - construct state_input_ports for them
2646
               - add to _specified_INPUT_Node_InputPorts_in_order
2647
           - call _validate_state_features()
2648
           - call _update_state_input_port_names()
2649
        """
2650

2651
        # Don't instantiate unless being called by Composition.run()
2652
        # This avoids error messages if called prematurely (i.e., before construction of Composition is complete)
2653
        if context.flags & ContextFlags.PROCESSING:
1!
2654
            return
×
2655

2656
        # Don't bother for agent_rep that is not a Composition, since state_input_ports specified can be validated
2657
        #    or assigned by default for a CompositionApproximator, and so are either up to its implementation or to
2658
        #    do the validation and/or default assignment (this contrasts with agent_rep that is a Composition, for
2659
        #    which there must be a state_input_port for every InputPort of every INPUT node of the agent_rep.
2660
        if self.agent_rep_type != COMPOSITION:
1!
2661
            return
×
2662

2663
        from psyneulink.core.compositions.composition import Composition
1✔
2664
        num_agent_rep_input_ports = len(self.agent_rep_input_ports)
1✔
2665
        num_state_feature_specs = len(self.state_feature_specs)
1✔
2666

2667
        if num_state_feature_specs < num_agent_rep_input_ports:
1✔
2668
            # agent_rep is Composition, but state_input_ports are missing for some agent_rep INPUT Node InputPorts
2669
            #   so construct a state_input_port for each missing one, using state_feature_default;
2670
            #   note: assumes INPUT Nodes added are at the end of the list in self.agent_rep_input_ports
2671
            # FIX: 3/24/22 - REFACTOR THIS TO CALL _parse_state_feature_specs?
2672
            state_input_ports = []
1✔
2673
            local_context = Context(source=ContextFlags.METHOD)
1✔
2674
            default = self.state_feature_default
1✔
2675
            new_agent_rep_input_ports = self.agent_rep_input_ports[self.num_state_input_ports:]
1✔
2676
            for input_port in new_agent_rep_input_ports:
1✔
2677
                # Instantiate state_input_port for each agent_rep INPUT Node InputPort not already specified:
2678
                params = {INTERNAL_ONLY:True,
1✔
2679
                          PARAMS: {}}
2680
                if default is None:
1✔
2681
                    continue
1✔
2682
                if default == SHADOW_INPUTS:
1!
2683
                    params[SHADOW_INPUTS] = input_port
1✔
2684
                    input_port_name = _shadowed_state_input_port_name(input_port.full_name, input_port.full_name)
1✔
2685
                    self.state_feature_specs.append(input_port)
1✔
2686
                elif is_numeric(default):
×
2687
                    params[VALUE]: default
×
2688
                    input_port_name = _numeric_state_input_port_name(input_port.full_name)
×
2689
                    self.state_feature_specs.append(default)
×
2690
                elif isinstance(default, (Port, Mechanism, Composition)):
×
2691
                    params[PROJECTIONS]: default
×
2692
                    self.state_feature_specs.append(default)
×
2693
                if self.state_feature_function:
1✔
2694
                    # Use **state_feature_function** if specified by user in constructor
2695
                    params = self._assign_state_feature_function(params)
1✔
2696
                state_input_port = _instantiate_port(name=input_port_name,
1✔
2697
                                                     port_type=InputPort,
2698
                                                     owner=self,
2699
                                                     reference_value=input_port.value,
2700
                                                     params=params,
2701
                                                     context=local_context)
2702

2703
                state_input_ports.append(state_input_port)
1✔
2704
                # FIX: 3/24/22 - MAKE THIS A PROPERTY? (OR NEED IT REMAIN STABLE FOR LOOPS?)
2705
                self._num_state_feature_specs += 1
1✔
2706

2707
            self.add_ports(state_input_ports,
1✔
2708
                                 update_variable=False,
2709
                                 context=local_context)
2710

2711
            # Assign OptimizationControlMechanism attributes
2712
            self.state_input_ports.extend(state_input_ports)
1✔
2713

2714
        # IMPLEMENTATION NOTE: Can't just assign agent_rep_input_ports to _specified_INPUT_Node_InputPorts_in_order
2715
        #                      below since there may be specifications in _specified_INPUT_Node_InputPorts_in_order
2716
        #                      for agent_rep INPUT Node InputPorts that have not yet been added to Composition
2717
        #                      (i.e., they are deferred)
2718
        # Update _specified_INPUT_Node_InputPorts_in_order with any new agent_rep_input_ports
2719
        for i in range(num_agent_rep_input_ports):
1✔
2720
            if i < len(self._specified_INPUT_Node_InputPorts_in_order):
1✔
2721
                # Replace existing ones (in case any deferred ones are "placemarked" with None)
2722
                self._specified_INPUT_Node_InputPorts_in_order[i] = self.agent_rep_input_ports[i]
1✔
2723
            else:
2724
                # Add any that have been added to Composition
2725
                self._specified_INPUT_Node_InputPorts_in_order.append(self.agent_rep_input_ports[i])
1✔
2726

2727
        if context._execution_phase == ContextFlags.PREPARING:
1✔
2728
            # Restrict validation until run time, when the Composition is expected to be fully constructed
2729
            self._validate_state_features(context)
1✔
2730

2731
        self._update_state_input_port_names(context)
1✔
2732

2733
    def _update_state_input_port_names(self, context=None):
1✔
2734
        """Update names of state_input_port for any newly instantiated INPUT Node InputPorts
2735

2736
        If its instantiation has NOT been DEFERRED, assert that:
2737
            - corresponding agent_rep INPUT Node InputPort is in Composition
2738
            - state_input_port either has path_afferents or it is for a numeric spec
2739

2740
        If it's instantiation HAS been DEFERRED, for any newly added agent_rep INPUT Node InputPorts:
2741
            - add agent_rep INPUT Node InputPort to _specified_INPUT_Node_InputPorts_in_order
2742
            - if state_input_port:
2743
                - HAS path_afferents, get source and generate new name
2744
                - does NOT have path_afferents, assert it is for a numeric spec and generate new name
2745
            - assign new name
2746
        """
2747

2748
        num_agent_rep_input_ports = len(self.agent_rep_input_ports)
1✔
2749
        for i, state_input_port in enumerate(self.state_input_ports):
1✔
2750

2751
            if context and context.flags & ContextFlags.PREPARING:
1✔
2752
                # By run time, state_input_port should either have path_afferents assigned or be for a numeric spec
2753
                assert state_input_port.path_afferents or NUMERIC_STATE_INPUT_PORT_PREFIX in state_input_port.name, \
1✔
2754
                    f"PROGRAM ERROR: state_input_port instantiated for '{self.name}' ({state_input_port.name}) " \
2755
                    f"with a specification in '{STATE_FEATURES}' ({self.parameters.state_feature_specs.spec[i]}) " \
2756
                    f"that is not numeric but has not been assigned any path_afferents."
2757

2758
            if DEFERRED_STATE_INPUT_PORT_PREFIX not in state_input_port.name:
1✔
2759
                # state_input_port should be associated with existing agent_rep INPUT Node InputPort
2760
                assert i < num_agent_rep_input_ports, \
1✔
2761
                    f"PROGRAM ERROR: state_input_port instantiated for '{self.name}' ({state_input_port.name}) " \
2762
                    f"but there is no corresponding INPUT Node in '{AGENT_REP}'."
2763
                continue
×
2764

2765
            if i >= num_agent_rep_input_ports:
1✔
2766
                # No more new agent_rep INPUT Node InputPorts
2767
                break
1✔
2768

2769
            # Add new agent_rep INPUT Node InputPorts
2770
            self._specified_INPUT_Node_InputPorts_in_order[i] = self.agent_rep_input_ports[i]
1✔
2771
            agent_rep_input_port_name = self.agent_rep_input_ports[i].full_name
1✔
2772

2773
            if state_input_port.path_afferents:
1✔
2774
                # Non-numeric spec, so get source and change name accordingly
2775
                source_input_port_name = self.state_feature_specs[i].full_name
1✔
2776
                if 'INPUT FROM' in state_input_port.name:
1!
2777
                    new_name = _state_input_port_name(source_input_port_name, agent_rep_input_port_name)
×
2778
                elif SHADOWED_INPUT_STATE_INPUT_PORT_PREFIX in state_input_port.name:
1!
2779
                    new_name = _shadowed_state_input_port_name(source_input_port_name, agent_rep_input_port_name)
1✔
2780
            elif NUMERIC_STATE_INPUT_PORT_PREFIX in state_input_port.name:
1!
2781
                # Numeric spec, so change name accordingly
2782
                new_name = _numeric_state_input_port_name(agent_rep_input_port_name)
1✔
2783
            else:
2784
                # Non-numeric but path_afferents haven't yet been assigned (will get tested again at run time)
2785
                continue
×
2786

2787
            # Change name of state_input_port
2788
            state_input_port.name = rename_instance_in_registry(registry=self._portRegistry,
1✔
2789
                                                                category=INPUT_PORT,
2790
                                                                new_name= new_name,
2791
                                                                component=state_input_port)
2792

2793
    def _validate_state_features(self, context):
1✔
2794
        """Validate that state_features are legal and consistent with agent_rep.
2795

2796
        Called by _update_state_input_ports_for_controller,
2797
        - after new Nodes have been added to Composition
2798
        - and/or in run() as final check before execution.
2799

2800
        Ensure that:
2801
        - the number of state_feature_specs equals the number of external InputPorts of INPUT Nodes of agent_rep;
2802
        - if state_feature_specs are specified as a user dict, keys are valid INPUT Nodes of agent_rep;
2803
        - all InputPorts shadowed by specified state_input_ports are in agent_rep or one of its nested Compositions;
2804
        - any Projections received from output_ports are from Nodes in agent_rep or its nested Compositions;
2805
        - all InputPorts shadowed by state_input_ports reference INPUT Nodes of agent_rep or Compositions nested in it;
2806
        - state_features are compatible with input format for agent_rep Composition
2807
        """
2808

2809
        # FIX: 3/4/22 - DO user_specs HAVE TO BE RE-VALIDATED? ?IS IT BECAUSE THEY MAY REFER TO NEWLY ADDED NODES?
2810
        from psyneulink.core.compositions.composition import \
1✔
2811
            Composition, CompositionInterfaceMechanism, CompositionError, RunError, NodeRole
2812

2813
        comp = self.agent_rep
1✔
2814
        user_specs = self.parameters.state_feature_specs.spec
1✔
2815

2816
        if isinstance(user_specs, dict) and SHADOW_INPUTS in user_specs:
1✔
2817
            state_feature_specs = user_specs[SHADOW_INPUTS]
1✔
2818
        else:
2819
            state_feature_specs = user_specs
1✔
2820
        if isinstance(state_feature_specs, list):
1✔
2821
            # Convert list to dict (assuming list is in order of InputPorts of INPUT Nodes)
2822
            input_ports = comp.external_input_ports_of_all_input_nodes
1✔
2823
            if len(state_feature_specs) > len(input_ports):
1✔
2824
                nodes_not_in_agent_rep = [f"'{spec.name if isinstance(spec, Mechanism) else spec.owner.name}'"
1✔
2825
                                          for spec in self._get_specs_not_in_agent_rep(state_feature_specs)]
2826
                missing_nodes_str = (f", that includes the following: {', '.join(nodes_not_in_agent_rep)} "
1✔
2827
                                     f"missing from {self.agent_rep.name}"
2828
                                     if nodes_not_in_agent_rep else '')
2829
                raise OptimizationControlMechanismError(
2830
                    f"The number of '{STATE_FEATURES}' specified for {self.name} ({len(state_feature_specs)}) "
2831
                    f"is more than the number of INPUT Nodes ({len(input_ports)}) of the Composition assigned "
2832
                    f"as its {AGENT_REP} ('{self.agent_rep.name}'){missing_nodes_str}.")
2833
            input_dict = {}
1✔
2834
            for i, spec in enumerate(state_feature_specs):
1✔
2835
                input_dict[input_ports[i]] = spec
1✔
2836
            state_features = state_feature_specs
1✔
2837
        elif isinstance(state_feature_specs, (set, dict)):
1✔
2838
            # If set or dict is specified, check that items of set or keys of dict are legal INPUT nodes:
2839
            self._validate_input_nodes(set(state_feature_specs), enforce=True)
1✔
2840
            if isinstance(state_feature_specs, dict):
1✔
2841
                # If dict is specified, get values for checks below
2842
                state_features = list(state_feature_specs.values())
1✔
2843
            else:
2844
                state_features = list(state_feature_specs)
1✔
2845

2846
        # Include agent rep in error messages if it is not the same as self.composition
2847
        self_has_state_features_str = f"'{self.name}' has '{STATE_FEATURES}' specified "
1✔
2848
        agent_rep_str = ('' if self.agent_rep == self.composition
1✔
2849
                         else f"both its `{AGENT_REP}` ('{self.agent_rep.name}') as well as ")
2850
        not_in_comps_str = f"that are missing from {agent_rep_str}'{self.composition.name}' and any " \
1✔
2851
                           f"{Composition.componentCategory}s nested within it."
2852

2853
        # Ensure that all InputPorts shadowed by specified state_input_ports
2854
        #    are in agent_rep or one of its nested Compositions
2855
        invalid_state_features = [input_port for input_port in self.state_input_ports
1✔
2856
                                  if (input_port.shadow_inputs
2857
                                      and not (input_port.shadow_inputs.owner in
2858
                                               list(comp.nodes) + [n[0] for n in comp._get_nested_nodes()])
2859
                                      and (not [input_port.shadow_inputs.owner.composition is x for x in
2860
                                                comp._get_nested_compositions()
2861
                                                if isinstance(input_port.shadow_inputs.owner,
2862
                                                              CompositionInterfaceMechanism)]))]
2863
        # Ensure any Projections received from output_ports are from Nodes in agent_rep or its nested Compositions
2864
        for input_port in self.state_input_ports:
1✔
2865
            if input_port.shadow_inputs:
1✔
2866
                continue
1✔
2867
            try:
1✔
2868
                all(comp._get_source(p) for p in input_port.path_afferents)
1✔
2869
            except CompositionError:
1✔
2870
                invalid_state_features.append(input_port)
1✔
2871
        if any(invalid_state_features):
1✔
2872
            raise OptimizationControlMechanismError(
2873
                self_has_state_features_str + f"({[d.name for d in invalid_state_features]}) " + not_in_comps_str)
2874

2875
        # Ensure that all InputPorts shadowed by specified state_input_ports
2876
        #    reference INPUT Nodes of agent_rep or of a nested Composition
2877
        invalid_state_features = [input_port for input_port in self.state_input_ports
1✔
2878
                                  if (input_port.shadow_inputs
2879
                                      and not (input_port.shadow_inputs.owner
2880
                                               in self.agent_rep_input_ports)
2881
                                      and (isinstance(input_port.shadow_inputs.owner,
2882
                                                      CompositionInterfaceMechanism)
2883
                                           and not (input_port.shadow_inputs.owner.composition in
2884
                                                    [nested_comp for nested_comp in comp._get_nested_compositions()
2885
                                                     if nested_comp in comp.get_nodes_by_role(NodeRole.INPUT)])))]
2886
        if any(invalid_state_features):
1✔
2887
            raise OptimizationControlMechanismError(
2888
                self_has_state_features_str + f"({[d.name for d in invalid_state_features]}) " + not_in_comps_str)
2889

2890
        # # FOLLOWING IS FOR DEBUGGING: (TO SEE CODING ERRORS DIRECTLY AND BREAK WHERE THEY OCCUR) -----------------------
2891
        # print("****** DEBUGGING CODE STILL IN OCM -- REMOVE FOR PROPER TESTING ************")
2892
        # inputs_dict, num_inputs = self.agent_rep._parse_input_dict(self.parameters.state_feature_values._get(context))
2893
        # #  END DEBUGGING ---------------------------------------------------------------------
2894

2895
        # Ensure state_features are compatible with input format for agent_rep Composition
2896
        try:
1✔
2897
            # Call this to check for errors in constructing inputs dict
2898
            self.agent_rep._parse_input_dict(self.parameters.state_feature_values._get(context))
1✔
2899
        except (RunError, CompositionError) as error:
1✔
2900
            raise OptimizationControlMechanismError(
2901
                f"The '{STATE_FEATURES}' argument has been specified for '{self.name}' that is using a "
2902
                f"{Composition.componentType} ('{self.agent_rep.name}') as its agent_rep, but some of the "
2903
                f"specifications are not compatible with the inputs required by its 'agent_rep': '{error.error_value}' "
2904
                f"Use the get_inputs_format() method of '{self.agent_rep.name}' to see the required format, or "
2905
                f"remove the specification of '{STATE_FEATURES}' from the constructor for {self.name} "
2906
                f"to have them automatically assigned.") from error
NEW
2907
        except KeyError:   # This occurs if a Node is illegal for a reason other than above,
×
NEW
2908
            pass           # and will issue the corresponding error message.
×
2909
        except:  # Legal Node specifications, but incorrect for input to agent_rep
×
2910
            specs = [f.full_name if hasattr(f, 'full_name') else (f.name if isinstance(f, Component) else f)
×
2911
                     for f in state_features]
2912
            raise OptimizationControlMechanismError(
2913
                f"The '{STATE_FEATURES}' argument has been specified for '{self.name}' that is using a "
2914
                f"{Composition.componentType} ('{self.agent_rep.name}') as its agent_rep, but the "
2915
                f"'{STATE_FEATURES}' ({specs}) specified are not compatible with the inputs required by 'agent_rep' "
2916
                f"when it is executed. Use its get_inputs_format() method to see the required format, "
2917
                f"or remove the specification of '{STATE_FEATURES}' from the constructor for {self.name} "
2918
                f"to have them automatically assigned.")
2919

2920
    def _validate_monitor_for_control(self, nodes):
1✔
2921
        # Ensure all of the Components being monitored for control are in the agent_rep if it is Composition
2922
        if self.agent_rep_type == COMPOSITION:
1!
2923
            try:
1✔
2924
                super()._validate_monitor_for_control(self.agent_rep._get_all_nodes())
1✔
2925
            except ControlMechanismError as e:
1✔
2926
                raise OptimizationControlMechanismError(f"{self.name} has 'outcome_ouput_ports' that receive "
2927
                                                        f"Projections from the following Components that do not belong "
2928
                                                        f"to its {AGENT_REP} ({self.agent_rep.name}): {e.data}.")
2929

2930
    def _instantiate_output_ports(self, context=None):
1✔
2931
        """Assign CostFunctions.DEFAULTS as default for cost_option of ControlSignals.
2932
        """
2933
        super()._instantiate_output_ports(context)
1✔
2934

2935
        for control_signal in self.control_signals:
1✔
2936
            if control_signal.cost_options is None:
1!
2937
                control_signal.cost_options = CostFunctions.DEFAULTS
×
2938
                control_signal._instantiate_cost_attributes(context)
×
2939

2940
    def _instantiate_control_signals(self, context):
1✔
2941
        super()._instantiate_control_signals(context)
1✔
2942
        self._create_randomization_control_signal(context)
1✔
2943

2944
    def _set_mechanism_value(self, context):
1✔
2945
        """Set Mechanism's value from control_allocation.
2946
        OCM uses optimal_control_allocation (returned by its _execute() method), which is isomorphic to
2947
        self.control_allocation, as its value.
2948
        This is needed because the OCM's:
2949
            - function (an OptimizationFunction) can return additional information (e.g., GridSearch)
2950
            - _execute() method processes the value returned by the OptimizationFunction (to incorporate costs)
2951
        """
2952
        control_allocation = self.parameters.control_allocation._get(context)
1✔
2953
        self.defaults.value = np.array(control_allocation)
1✔
2954
        self.parameters.value._set(copy.deepcopy(self.defaults.value), context)
1✔
2955
        return control_allocation
1✔
2956

2957
    def _create_randomization_control_signal(self, context):
1✔
2958
        num_estimates = self.parameters.num_estimates._get(context)
1✔
2959
        num_estimates = try_extract_0d_array_item(num_estimates)
1✔
2960

2961
        if num_estimates:
1✔
2962
            # must be SampleSpec in allocation_samples arg
2963
            randomization_seed_mod_values = SampleSpec(start=1, stop=num_estimates, step=1)
1✔
2964

2965
            # FIX: 11/3/21 noise PARAM OF TransferMechanism IS MARKED AS SEED WHEN ASSIGNED A DISTRIBUTION FUNCTION,
2966
            #                BUT IT HAS NO PARAMETER PORT BECAUSE THAT PRESUMABLY IS FOR THE INTEGRATOR FUNCTION,
2967
            #                BUT THAT IS NOT FOUND BY model.all_dependent_parameters
2968
            # FIX: 1/4/22:  CHECK IF THIS WORKS WITH CFA
2969
            #               (i.e., DOES IT [make sense to HAVE A random_variables ATTRIBUTE?)
2970
            # Get Components with variables to be randomized across estimates
2971
            #   and construct ControlSignal to modify their seeds over estimates
2972
            if self.random_variables is ALL:
1!
2973
                self.random_variables = self.agent_rep.random_variables
1✔
2974

2975
            if not self.random_variables:
1✔
2976
                warnings.warn(f"'{self.name}' has '{NUM_ESTIMATES} = {num_estimates}' specified, "
1✔
2977
                              f"but its '{AGENT_REP}' ('{self.agent_rep.name}') has no random variables: "
2978
                              f"'{RANDOMIZATION_CONTROL_SIGNAL}' will not be created, and num_estimates set to None.")
2979
                self.parameters.num_estimates._set(None, context)
1✔
2980
                return
1✔
2981

2982
            randomization_control_signal = ControlSignal(name=RANDOMIZATION_CONTROL_SIGNAL,
1✔
2983
                                                         modulates=[param.parameters.seed.port
2984
                                                                    for param in self.random_variables],
2985
                                                         allocation_samples=randomization_seed_mod_values,
2986
                                                         modulation=OVERRIDE,
2987
                                                         cost_options=CostFunctions.NONE,
2988
                                                         # FIXME: Hack that Jan found to prevent some LLVM runtime errors
2989
                                                         default_allocation=np.array([num_estimates]))
2990
            randomization_control_signal = self._instantiate_control_signal(randomization_control_signal, context)
1✔
2991
            randomization_control_signal_index = len(self.output_ports)
1✔
2992
            randomization_control_signal._variable_spec = (OWNER_VALUE, randomization_control_signal_index)
1✔
2993

2994
            self.output_ports.append(randomization_control_signal)
1✔
2995

2996
            # Otherwise, assert that num_estimates and number of seeds generated by randomization_control_signal are equal
2997
            num_seeds = self.control_signals[RANDOMIZATION_CONTROL_SIGNAL].parameters.allocation_samples._get(context).num
1✔
2998
            assert num_estimates == num_seeds, \
1✔
2999
                    f"PROGRAM ERROR:  The value of the {NUM_ESTIMATES} Parameter of {self.name}" \
3000
                    f"({num_estimates}) is not equal to the number of estimates that will be generated by " \
3001
                    f"its {RANDOMIZATION_CONTROL_SIGNAL} ControlSignal ({num_seeds})."
3002

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

3009
                # search_space must be a SampleIterator
3010
                function_search_space.append(SampleIterator(randomization_seed_mod_values))
1✔
3011

3012
            self.defaults.value = np.insert(self.defaults.value, len(self.defaults.value), self.defaults.value[0], 0)
1✔
3013
            self.parameters.value._set(copy.deepcopy(self.defaults.value), context)
1✔
3014

3015
    def _instantiate_function(self, function, function_params=None, context=None):
1✔
3016
        # this indicates a significant peculiarity of OCM, in that its function
3017
        # corresponds to its value (control_allocation) rather than anything to
3018
        # do with its variable. see _instantiate_attributes_after_function
3019

3020
        # Workaround this issue here, and explicitly allow the function's
3021
        # default variable to be modified
3022
        if isinstance(function, Function):
1✔
3023
            function._variable_shape_flexibility = DefaultsFlexibility.FLEXIBLE
1✔
3024

3025
        super()._instantiate_function(function, function_params, context)
1✔
3026

3027
    def _instantiate_attributes_after_function(self, context=None):
1✔
3028
        """Instantiate OptimizationControlMechanism's OptimizationFunction attributes"""
3029

3030
        super()._instantiate_attributes_after_function(context=context)
1✔
3031

3032
        search_space = self.parameters.search_space._get(context)
1✔
3033
        if type(search_space) == np.ndarray:
1✔
3034
            search_space = search_space.tolist()
1✔
3035
        if search_space:
1✔
3036
            corrected_search_space = []
1✔
3037
            try:
1✔
3038
                if type(search_space) == SampleIterator:
1✔
3039
                    corrected_search_space.append(search_space)
1✔
3040
                elif type(search_space) == SampleSpec:
1✔
3041
                    corrected_search_space.append(SampleIterator(search_space))
1✔
3042
                else:
3043
                    for i in self.parameters.search_space._get(context):
1✔
3044
                        if not type(i) == SampleIterator:
1✔
3045
                            corrected_search_space.append(SampleIterator(specification=i))
1✔
3046
                            continue
1✔
3047
                        corrected_search_space.append(i)
1✔
3048
            except AssertionError:
1✔
3049
                corrected_search_space = [SampleIterator(specification=search_space)]
1✔
3050
            self.parameters.search_space._set(corrected_search_space, context)
1✔
3051

3052
        try:
1✔
3053
            randomization_control_signal_index = self.control_signals.names.index(RANDOMIZATION_CONTROL_SIGNAL)
1✔
3054
        except ValueError:
1✔
3055
            randomization_control_signal_index = None
1✔
3056

3057
        # Assign parameters to function (OptimizationFunction) that rely on OptimizationControlMechanism
3058
        # NOTE: as in this call, randomization_dimension must be set
3059
        # after search_space to avoid IndexError when getting
3060
        # num_estimates of function
3061
        self.function.reset(**{
1✔
3062
            DEFAULT_VARIABLE: self.parameters.value._get(context),
3063
            OBJECTIVE_FUNCTION: self.evaluate_agent_rep,
3064
            # SEARCH_FUNCTION: self.search_function,
3065
            # SEARCH_TERMINATION_FUNCTION: self.search_termination_function,
3066
            SEARCH_SPACE: self.parameters.control_allocation_search_space._get(context),
3067
            RANDOMIZATION_DIMENSION: randomization_control_signal_index
3068
        })
3069

3070
        if isinstance(self.agent_rep, type):
1✔
3071
            self.agent_rep = self.agent_rep()
1✔
3072

3073
        if self.agent_rep_type == COMPOSITION_FUNCTION_APPROXIMATOR:
1✔
3074
            self._initialize_composition_function_approximator(context)
1✔
3075

3076
    def _execute(self, variable=None, context=None, runtime_params=None)->np.ndarray:
1✔
3077
        """Return control_allocation that optimizes net_outcome of agent_rep.evaluate().
3078
        """
3079

3080
        if self.is_initializing:
1✔
3081
            return np.asarray([defaultControlAllocation])
1✔
3082

3083
        # Assign default control_allocation if it is not yet specified (presumably first trial)
3084
        control_allocation = self.parameters.control_allocation._get(context)
1✔
3085
        if control_allocation is None:
1!
3086
            control_allocation = convert_to_np_array([c.defaults.variable for c in self.control_signals])
×
3087

3088
        # Give the agent_rep a chance to adapt based on last trial's state_feature_values and control_allocation
3089
        if hasattr(self.agent_rep, "adapt"):
1✔
3090
            # KAM 4/11/19 switched from a try/except to hasattr because in the case where we don't
3091
            # have an adapt method, we also don't need to call the net_outcome getter
3092
            net_outcome = self.parameters.net_outcome._get(context)
1✔
3093

3094
            self.agent_rep.adapt(self.parameters.state_feature_values._get(context),
1✔
3095
                                 control_allocation,
3096
                                 net_outcome,
3097
                                 context=context)
3098

3099
        # freeze the values of current context, because they can be changed in between simulations,
3100
        # and the simulations must start from the exact spot
3101
        frozen_context = self._get_frozen_context(context)
1✔
3102

3103
        alt_controller = None
1✔
3104
        if self.agent_rep.controller is None:
1✔
3105
            try:
1✔
3106
                alt_controller = context.composition.controller
1✔
3107
            except AttributeError:
×
3108
                pass
×
3109

3110
        self.agent_rep._initialize_as_agent_rep(
1✔
3111
            frozen_context, base_context=context, alt_controller=alt_controller
3112
        )
3113

3114
        # Get control_allocation that optimizes net_outcome using OptimizationControlMechanism's function
3115
        # IMPLEMENTATION NOTE: skip ControlMechanism._execute since it is a stub method that returns input_values
3116
        optimal_control_allocation, optimal_net_outcome, saved_samples, saved_values = \
1✔
3117
                                                super(ControlMechanism,self)._execute(
3118
                                                    variable=control_allocation,
3119
                                                    num_estimates=self.parameters.num_estimates._get(context),
3120
                                                    context=context,
3121
                                                    runtime_params=runtime_params
3122
                                                )
3123

3124
        # clean up frozen values after execution
3125
        self.agent_rep._clean_up_as_agent_rep(frozen_context, alt_controller=alt_controller)
1✔
3126

3127
        if self.function.save_samples:
1!
3128
            self.saved_samples = saved_samples
×
3129
        if self.function.save_values:
1✔
3130
            self.saved_values = saved_values
1✔
3131

3132
        self.optimal_control_allocation = optimal_control_allocation
1✔
3133
        self.optimal_net_outcome = optimal_net_outcome
1✔
3134
        optimal_control_allocation = np.array(optimal_control_allocation).reshape((len(self.defaults.value), 1))
1✔
3135

3136
        # Return optimal control_allocation formatted as 2d array
3137
        return optimal_control_allocation
1✔
3138

3139
    def _get_frozen_context(self, context=None):
1✔
3140
        return Context(execution_id=f'{context.execution_id}{EID_FROZEN}')
1✔
3141

3142
    def _set_up_simulation(
1✔
3143
        self,
3144
        base_context=Context(execution_id=None),
3145
        control_allocation=None,
3146
        alt_controller=None
3147
    ):
3148
        sim_context = copy.copy(base_context)
1✔
3149
        sim_context.execution_id = self.get_next_sim_id(base_context, control_allocation)
1✔
3150

3151
        try:
1✔
3152
            self.parameters.simulation_ids._get(base_context).append(sim_context.execution_id)
1✔
3153
        except AttributeError:
×
3154
            self.parameters.simulation_ids._set([sim_context.execution_id], base_context)
×
3155

3156
        self.agent_rep._initialize_as_agent_rep(
1✔
3157
            sim_context,
3158
            base_context=self._get_frozen_context(base_context),
3159
            alt_controller=alt_controller
3160
        )
3161

3162
        return sim_context
1✔
3163

3164
    def _tear_down_simulation(self, sim_context, alt_controller=None):
1✔
3165
        if not self.agent_rep.parameters.retain_old_simulation_data._get():
1✔
3166
            self.agent_rep._clean_up_as_agent_rep(sim_context, alt_controller=alt_controller)
1✔
3167

3168
    def evaluate_agent_rep(self, control_allocation, context=None):
1✔
3169
        """Call `evaluate <Composition.evaluate>` method of `agent_rep <OptimizationControlMechanism.agent_rep>`
3170

3171
        Assigned as the `objective_function <OptimizationFunction.objective_function>` for the
3172
        OptimizationControlMechanism's `function <OptimizationControlMechanism.function>`.
3173

3174
        Evaluates `agent_rep <OptimizationControlMechanism.agent_rep>` by calling its `evaluate <Composition.evaluate>`
3175
        method, which executes its `agent_rep <OptimizationControlMechanism.agent_rep>` using the current
3176
        `state_feature_values <OptimizationControlMechanism.state_feature_values>` as the input and the specified
3177
        **control_allocation**.
3178

3179
        If the `agent_rep <OptimizationControlMechanism.agent_rep>` is a `Composition`, each execution is a call to
3180
        its `run <Composition.run>` method that uses the `num_trials_per_estimate
3181
        <OptimizationControlMechanism.num_trials_per_estimate>` as its **num_trials** argument, and the same
3182
        `state_feature_values <OptimizationControlMechanism.state_feature_values>` and **control_allocation**
3183
        but a different randomly chosen seed for the random number generator for each run.  It then returns an array of
3184
        length **number_estimates** containing the `net_outcome <ControlMechanism.net_outcome>` of each execution
3185
        and, if **return_results** is True, also an array with the `results <Composition.results>` of each run.
3186

3187
        COMMENT:
3188
        FIX: THIS SHOULD BE REFACTORED TO BE HANDLED THE SAME AS A Composition AS agent_rep
3189
        COMMENT
3190
        If the `agent_rep <OptimizationControlMechanism.agent_rep>` is a CompositionFunctionApproximator,
3191
        then `num_estimates <OptimizationControlMechanism.num_estimates>` is passed to it to handle execution and
3192
        estimation as determined by its implementation, and returns a single estimated net_outcome.
3193

3194

3195
        (See `evaluate <Composition.evaluate>` for additional details.)
3196
        """
3197

3198
        # agent_rep is a Composition (since runs_simulations = True)
3199
        if self.agent_rep.runs_simulations:
1✔
3200
            alt_controller = None
1✔
3201
            if self.agent_rep.controller is None:
1!
3202
                try:
×
3203
                    alt_controller = context.composition.controller
×
3204
                except AttributeError:
×
3205
                    pass
×
3206
            # KDM 5/20/19: crudely using default here because it is a stateless parameter
3207
            # and there is a bug in setting parameter values on init, see TODO note above
3208
            # call to self._instantiate_defaults around component.py:1115
3209
            if self.defaults.search_statefulness:
1✔
3210
                new_context = self._set_up_simulation(context, control_allocation, alt_controller)
1✔
3211
            else:
3212
                new_context = context
1✔
3213

3214
            old_composition = context.composition
1✔
3215
            context.composition = self.agent_rep
1✔
3216

3217
            # We shouldn't get this far if execution mode is not Python
3218
            assert self.parameters.comp_execution_mode._get(context) == "Python"
1✔
3219
            exec_mode = pnlvm.ExecutionMode.Python
1✔
3220

3221
            predicted_input = self.parameters.state_feature_values._get(context)
1✔
3222
            ret_val = self.agent_rep.evaluate(predicted_input,
1✔
3223
                                              control_allocation,
3224
                                              self.parameters.num_trials_per_estimate._get(context),
3225
                                              base_context=context,
3226
                                              context=new_context,
3227
                                              execution_mode=exec_mode,
3228
                                              return_results=self.return_results)
3229
            context.composition = old_composition
1✔
3230
            if self.defaults.search_statefulness:
1✔
3231
                self._tear_down_simulation(new_context, alt_controller)
1✔
3232

3233
            # FIX: THIS SHOULD BE REFACTORED TO BE HANDLED THE SAME AS A Composition AS agent_rep
3234
            # If results of the simulation should be returned then, do so. agent_rep's evaluate method will
3235
            # return a tuple in this case in which the first element is the outcome as usual and the second
3236
            # is the results of the composition run.
3237
            if self.return_results:
1✔
3238
                return ret_val[0], ret_val[1]
1✔
3239
            else:
3240
                return ret_val
1✔
3241

3242
        # FIX: 11/3/21 - ??REFACTOR CompositionFunctionApproximator TO NOT TAKE num_estimates
3243
        #                (i.e., LET OptimzationFunction._grid_evaluate HANDLE IT)
3244
        # agent_rep is a CompositionFunctionApproximator (since runs_simuluations = False)
3245
        else:
3246
            return self.agent_rep.evaluate(self.parameters.state_feature_values._get(context),
1✔
3247
                                           control_allocation,
3248
                                           self.parameters.num_estimates._get(context),
3249
                                           self.parameters.num_trials_per_estimate._get(context),
3250
                                           context=context
3251
                                           )
3252

3253
    def _apply_control_allocation(self, control_allocation, runtime_params, context):
1✔
3254
        """Update values to `control_signals <ControlMechanism.control_signals>`
3255
        based on specified `control_allocation <ControlMechanism.control_allocation>`
3256
        """
3257
        # IMPLEMENTATION NOTE:
3258
        #  Need to set value of ControlMechanism (rather than variables of ControlSignals)
3259
        #  since OutputPort uses _output_port_variable_getter() to parse its variable_spec
3260
        #  rather than assigning a value directly to its variable.
3261
        #  Need to assign OCM's value to control_allocation, since the value of its function includes other info
3262
        #  (see `function <OptimizationControlMechanism.optimization_>`)
3263
        self.parameters.value._set(control_allocation, context)
1✔
3264
        self._update_output_ports(runtime_params, context)
1✔
3265

3266
    def _get_evaluate_output_struct_type(self, ctx, tags):
1✔
3267
        if "evaluate_type_all_results" in tags:
1✔
3268
            return ctx.get_output_struct_type(self.agent_rep)
1✔
3269
        assert "evaluate_type_objective" in tags, "Unknown evaluate type: {}".format(tags)
1✔
3270
        # Returns a scalar that is the predicted net_outcome
3271
        return ctx.float_ty
1✔
3272

3273
    def _get_evaluate_alloc_struct_type(self, ctx):
1✔
3274
        return pnlvm.ir.ArrayType(ctx.float_ty,
1✔
3275
                                  len(self.parameters.control_allocation_search_space.get()))
3276

3277
    def _gen_llvm_net_outcome_function(self, *, ctx, tags=frozenset()):
1✔
3278
        assert "net_outcome" in tags
1✔
3279
        args = [ctx.get_param_struct_type(self).as_pointer(),
1✔
3280
                ctx.get_state_struct_type(self).as_pointer(),
3281
                ctx.float_ty.as_pointer(),
3282
                self._get_evaluate_output_struct_type(ctx, tags=tags).as_pointer()]
3283

3284
        builder = ctx.create_llvm_function(args, self, str(self) + "_net_outcome")
1✔
3285
        llvm_func = builder.function
1✔
3286
        for p in llvm_func.args:
1✔
3287
            p.attributes.add('nonnull')
1✔
3288
        _, state, objective_ptr, arg_out = llvm_func.args
1✔
3289

3290
        op_states = pnlvm.helpers.get_state_ptr(builder, self, state,
1✔
3291
                                                "output_ports", None)
3292

3293
        # calculate cost function
3294
        total_cost_ptr = builder.alloca(ctx.float_ty, name="total_cost")
1✔
3295
        builder.store(total_cost_ptr.type.pointee(-0.0), total_cost_ptr)
1✔
3296

3297
        for i, op in enumerate(self.output_ports):
1✔
3298
            # FIXME Issue #2712: Use port total cost here
3299
            port_cost_ptr = builder.alloca(ctx.float_ty, name="port_{}_total_cost".format(i))
1✔
3300
            builder.store(port_cost_ptr.type.pointee(-0.0), port_cost_ptr)
1✔
3301

3302
            op_i_state = builder.gep(op_states, [ctx.int32_ty(0),
1✔
3303
                                                 ctx.int32_ty(i)])
3304

3305
            # Python uses alias-ed Parameters on Signals, but here we must get
3306
            # the function state values.
3307
            op_func = op.function
1✔
3308
            op_func_state = pnlvm.helpers.get_state_ptr(builder, op, op_i_state, "function")
1✔
3309

3310
            costs = [(CostFunctions.INTENSITY, op_func.parameters.intensity_cost),
1✔
3311
                     (CostFunctions.ADJUSTMENT, op_func.parameters.adjustment_cost),
3312
                     (CostFunctions.DURATION, op_func.parameters.duration_cost)]
3313

3314
            for (flag, param) in costs:
1✔
3315

3316
                # The check for enablement is structural and has to be done in Python.
3317
                # If a cost function is not enabled the cost parameter is None
3318
                if flag in op.parameters.cost_options.get():
1✔
3319
                    cost_ptr = pnlvm.helpers.get_state_ptr(builder, op_func, op_func_state, param.name)
1✔
3320
                    cost = pnlvm.helpers.load_extract_scalar_array_one(builder, cost_ptr)
1✔
3321
                    port_cost = builder.load(port_cost_ptr)
1✔
3322

3323
                    # FIXME Issue #2712: This assume addition as port cost combination function
3324
                    port_cost = builder.fadd(port_cost, cost)
1✔
3325
                    builder.store(port_cost, port_cost_ptr)
1✔
3326

3327

3328
            port_cost = builder.load(port_cost_ptr)
1✔
3329

3330
            # simplified version of combination fmax(cost, 0)
3331
            ltz = builder.fcmp_ordered("<", port_cost, port_cost.type(0))
1✔
3332
            port_cost = builder.select(ltz, port_cost.type(0), port_cost)
1✔
3333

3334
            # combine is not a PNL function
3335
            assert self.combine_costs is np.sum
1✔
3336
            total_cost = builder.load(total_cost_ptr)
1✔
3337
            total_cost = builder.fadd(total_cost, port_cost)
1✔
3338
            builder.store(total_cost, total_cost_ptr)
1✔
3339

3340
        # compute net_outcome
3341
        objective_val = builder.load(objective_ptr)
1✔
3342
        net_outcome = builder.fsub(objective_val, builder.load(total_cost_ptr))
1✔
3343
        builder.store(net_outcome, arg_out)
1✔
3344

3345
        builder.ret_void()
1✔
3346
        return llvm_func
1✔
3347

3348
    def _gen_llvm_evaluate_alloc_range_function(self, *, ctx:pnlvm.LLVMBuilderContext, tags=frozenset()):
1✔
3349
        assert "evaluate" in tags
1✔
3350
        assert "alloc_range" in tags
1✔
3351
        evaluate_f = ctx.import_llvm_function(self, tags=tags - {"alloc_range"})
1✔
3352

3353
        args = [*evaluate_f.type.pointee.args[:2],
1✔
3354
                ctx.int32_ty, ctx.int32_ty,
3355
                *evaluate_f.type.pointee.args[3:]]
3356
        builder = ctx.create_llvm_function(args, self, str(self) + "_evaluate_range")
1✔
3357
        llvm_func = builder.function
1✔
3358

3359
        params, state, start, stop, arg_out, arg_in, data, num_inputs = llvm_func.args
1✔
3360
        for p in llvm_func.args:
1✔
3361
            if isinstance(p.type, (pnlvm.ir.PointerType)):
1✔
3362
                p.attributes.add('nonnull')
1✔
3363

3364
        nodes_params = pnlvm.helpers.get_param_ptr(builder, self.composition,
1✔
3365
                                                   params, "nodes")
3366
        controller_idx = self.composition._get_node_index(self)
1✔
3367
        controller_params = builder.gep(nodes_params,
1✔
3368
                                        [ctx.int32_ty(0), ctx.int32_ty(controller_idx)])
3369
        num_trials_per_estimate_ptr = ctx.get_param_or_state_ptr(builder,
1✔
3370
                                                                 self,
3371
                                                                 "num_trials_per_estimate",
3372
                                                                 param_struct_ptr=controller_params)
3373
        func_params = pnlvm.helpers.get_param_ptr(builder, self,
1✔
3374
                                                  controller_params, "function")
3375
        search_space = pnlvm.helpers.get_param_ptr(builder, self.function,
1✔
3376
                                                   func_params, "search_space")
3377

3378
        allocation = builder.alloca(evaluate_f.args[2].type.pointee, name="allocation")
1✔
3379
        with pnlvm.helpers.for_loop(builder, start, stop, stop.type(1), "alloc_loop") as (b, idx):
1✔
3380

3381
            if "evaluate_type_objective" in tags:
1✔
3382
                out_idx = idx
1✔
3383
            elif "evaluate_type_all_results" in tags:
1✔
3384
                num_trials_per_estimate = builder.load(num_trials_per_estimate_ptr)
1✔
3385
                out_idx = builder.mul(idx, builder.trunc(num_trials_per_estimate, idx.type))
1✔
3386
            else:
3387
                assert False, "Evaluation type not detected in tags, or unknown: {}".format(tags)
3388

3389
            func_out = b.gep(arg_out, [out_idx])
1✔
3390
            pnlvm.helpers.create_sample(b, allocation, search_space, idx)
1✔
3391

3392
            b.call(evaluate_f, [params, state, allocation, func_out, arg_in, data, num_inputs])
1✔
3393

3394
        builder.ret_void()
1✔
3395
        return llvm_func
1✔
3396

3397
    def _gen_llvm_evaluate_function(self, *, ctx:pnlvm.LLVMBuilderContext, tags=frozenset()):
1✔
3398
        assert "evaluate" in tags
1✔
3399
        args = [ctx.get_param_struct_type(self.agent_rep).as_pointer(),
1✔
3400
                ctx.get_state_struct_type(self.agent_rep).as_pointer(),
3401
                self._get_evaluate_alloc_struct_type(ctx).as_pointer(),
3402
                self._get_evaluate_output_struct_type(ctx, tags=tags).as_pointer(),
3403
                ctx.get_input_struct_type(self.agent_rep).as_pointer(),
3404
                ctx.get_data_struct_type(self.agent_rep).as_pointer(),
3405
                ctx.int32_ty.as_pointer()]
3406

3407
        builder = ctx.create_llvm_function(args, self, str(self) + "_evaluate")
1✔
3408
        llvm_func = builder.function
1✔
3409
        for p in llvm_func.args:
1✔
3410
            p.attributes.add('nonnull')
1✔
3411

3412
        comp_params, base_comp_state, allocation_sample, arg_out, comp_input, base_comp_data, num_inputs = llvm_func.args
1✔
3413

3414
        if "const_params" in debug_env:
1!
3415
            comp_params = builder.alloca(comp_params.type.pointee, name="const_params_loc")
×
3416
            const_params = comp_params.type.pointee(self.agent_rep._get_param_initializer(None))
×
3417
            builder.store(const_params, comp_params)
×
3418

3419
        # Create a simulation copy of composition state
3420
        comp_state = builder.alloca(base_comp_state.type.pointee, name="state_copy")
1✔
3421
        if "const_state" in debug_env:
1!
3422
            const_state = self.agent_rep._get_state_initializer(None)
×
3423
            builder.store(comp_state.type.pointee(const_state), comp_state)
×
3424
        else:
3425
            builder = pnlvm.helpers.memcpy(builder, comp_state, base_comp_state)
1✔
3426

3427
        # Create a simulation copy of composition data
3428
        comp_data = builder.alloca(base_comp_data.type.pointee, name="data_copy")
1✔
3429
        if "const_data" in debug_env:
1!
3430
            const_data = self.agent_rep._get_data_initializer(None)
×
3431
            builder.store(comp_data.type.pointee(const_data), comp_data)
×
3432
        else:
3433
            builder = pnlvm.helpers.memcpy(builder, comp_data, base_comp_data)
1✔
3434

3435
        # Evaluate is called on composition controller
3436
        assert self.composition.controller is self
1✔
3437
        assert self.composition is self.agent_rep
1✔
3438
        nodes_states = pnlvm.helpers.get_state_ptr(builder, self.composition,
1✔
3439
                                                   comp_state, "nodes")
3440
        nodes_params = pnlvm.helpers.get_param_ptr(builder, self.composition,
1✔
3441
                                                   comp_params, "nodes")
3442

3443
        controller_idx = self.composition._get_node_index(self)
1✔
3444
        controller_state = builder.gep(nodes_states, [ctx.int32_ty(0),
1✔
3445
                                                      ctx.int32_ty(controller_idx)])
3446
        controller_params = builder.gep(nodes_params, [ctx.int32_ty(0),
1✔
3447
                                                       ctx.int32_ty(controller_idx)])
3448

3449
        # Apply allocation sample to simulation data
3450
        assert len(self.output_ports) == len(allocation_sample.type.pointee)
1✔
3451
        controller_out = builder.gep(comp_data, [ctx.int32_ty(0), ctx.int32_ty(0),
1✔
3452
                                                 ctx.int32_ty(controller_idx)])
3453
        all_op_params, all_op_states = ctx.get_param_or_state_ptr(builder,
1✔
3454
                                                                  self,
3455
                                                                  "output_ports",
3456
                                                                  param_struct_ptr=controller_params,
3457
                                                                  state_struct_ptr=controller_state)
3458
        for i, op in enumerate(self.output_ports):
1✔
3459
            op_idx = ctx.int32_ty(i)
1✔
3460

3461
            op_f = ctx.import_llvm_function(op, tags=frozenset({"simulation"}))
1✔
3462
            op_state = builder.gep(all_op_states, [ctx.int32_ty(0), op_idx])
1✔
3463
            op_params = builder.gep(all_op_params, [ctx.int32_ty(0), op_idx])
1✔
3464
            op_in = builder.alloca(op_f.args[2].type.pointee)
1✔
3465
            op_out = builder.gep(controller_out, [ctx.int32_ty(0), op_idx])
1✔
3466

3467
            # FIXME: Allocation samples are generated as scalars but
3468
            #        output ports consume 1d arrays
3469
            sample_ptr = builder.gep(allocation_sample, [ctx.int32_ty(0), op_idx])
1✔
3470
            sample_dst = builder.gep(op_in, [ctx.int32_ty(0), ctx.int32_ty(0)])
1✔
3471

3472
            builder.store(builder.load(sample_ptr), sample_dst)
1✔
3473
            builder.call(op_f, [op_params, op_state, op_in, op_out])
1✔
3474

3475

3476
        # Get simulation function
3477
        agent_tags = {"run", "simulation"}
1✔
3478
        if "evaluate_type_all_results" in tags:
1✔
3479
            agent_tags.add("simulation_results")
1✔
3480
        sim_f = ctx.import_llvm_function(self.agent_rep, tags=frozenset(agent_tags))
1✔
3481

3482
        if "const_input" in debug_env:
1!
3483
            comp_input = builder.alloca(sim_f.args[3].type.pointee, name="sim_input")
×
3484
            if not debug_env["const_input"]:
×
3485
                input_init = [[os.defaults.variable.tolist()] for os in self.agent_rep.input_CIM.input_ports]
×
3486
                print("Setting default input: ", input_init)
×
3487
            else:
3488
                input_init = ast.literal_eval(debug_env["const_input"])
×
3489
                print("Setting user input in evaluate: ", input_init)
×
3490

3491
            builder.store(comp_input.type.pointee(input_init), comp_input)
×
3492

3493

3494
        # Determine simulation counts
3495
        num_trials_per_estimate_ptr = ctx.get_param_or_state_ptr(builder,
1✔
3496
                                                                 self,
3497
                                                                 "num_trials_per_estimate",
3498
                                                                 param_struct_ptr=controller_params)
3499

3500
        num_trials_per_estimate = builder.load(num_trials_per_estimate_ptr, "num_trials_per_estimate")
1✔
3501

3502
        # if num_trials_per_estimate is 0, run 1 trial
3503
        num_trials_param_is_zero = builder.icmp_unsigned("==",
1✔
3504
                                                         num_trials_per_estimate,
3505
                                                         num_trials_per_estimate.type(0))
3506
        num_trials_per_estimate_fixed = builder.select(num_trials_param_is_zero,
1✔
3507
                                                       num_trials_per_estimate.type(1),
3508
                                                       num_trials_per_estimate,
3509
                                                       "corrected_num_trials_per_estimate")
3510

3511
        num_trials = builder.alloca(sim_f.args[5].type.pointee, name="num_sim_trials")
1✔
3512
        builder.store(builder.trunc(num_trials_per_estimate_fixed, num_trials.type.pointee), num_trials)
1✔
3513

3514
        # Simulations don't store output unless we run parameter fitting
3515
        if 'evaluate_type_objective' in tags:
1✔
3516
            comp_output = sim_f.args[4].type(None)
1✔
3517
        elif 'evaluate_type_all_results' in tags:
1✔
3518
            comp_output = arg_out
1✔
3519
        else:
3520
            assert False, "Evaluation type not detected in tags, or unknown: {}".format(tags)
3521

3522
        builder.call(sim_f, [comp_state, comp_params, comp_data, comp_input, comp_output, num_trials, num_inputs])
1✔
3523

3524
        if "evaluate_type_objective" in tags:
1✔
3525
            # Extract objective mechanism value
3526

3527
            assert self.objective_mechanism, \
1✔
3528
                "objective_mechanism on OptimizationControlMechanism cannot be None in 'evaluate_type_objective'"
3529

3530
            obj_idx = self.agent_rep._get_node_index(self.objective_mechanism)
1✔
3531
            # Mechanisms' results are stored in the first substructure
3532
            objective_op_ptr = builder.gep(comp_data, [ctx.int32_ty(0),
1✔
3533
                                                       ctx.int32_ty(0),
3534
                                                       ctx.int32_ty(obj_idx)])
3535
            # Objective mech output shape should be 1 single element 2d array
3536
            objective_val_ptr = builder.gep(objective_op_ptr,
1✔
3537
                                            [ctx.int32_ty(0), ctx.int32_ty(0), ctx.int32_ty(0)],
3538
                                            "obj_val_ptr")
3539

3540
            # Apply total cost to objective value
3541
            net_outcome_f = ctx.import_llvm_function(self, tags=tags.union({"net_outcome"}))
1✔
3542
            builder.call(net_outcome_f, [controller_params, controller_state, objective_val_ptr, arg_out])
1✔
3543
        elif "evaluate_type_all_results" in tags:
1✔
3544
            pass
1✔
3545
        else:
3546
            assert False, "Evaluation type not detected in tags, or unknown: {}".format(tags)
3547

3548
        builder.ret_void()
1✔
3549

3550
        return llvm_func
1✔
3551

3552
    def _gen_llvm_function(self, *, ctx:pnlvm.LLVMBuilderContext, tags:frozenset):
1✔
3553
        if "net_outcome" in tags:
1✔
3554
            return self._gen_llvm_net_outcome_function(ctx=ctx, tags=tags)
1✔
3555
        if "evaluate" in tags and "alloc_range" in tags:
1✔
3556
            return self._gen_llvm_evaluate_alloc_range_function(ctx=ctx, tags=tags)
1✔
3557
        if "evaluate" in tags:
1✔
3558
            return self._gen_llvm_evaluate_function(ctx=ctx, tags=tags)
1✔
3559

3560
        is_comp = not isinstance(self.agent_rep, Function)
1✔
3561
        if is_comp:
1!
3562
            extra_args = [ctx.get_param_struct_type(self.agent_rep).as_pointer(),
1✔
3563
                          ctx.get_state_struct_type(self.agent_rep).as_pointer(),
3564
                          ctx.get_data_struct_type(self.agent_rep).as_pointer()]
3565
        else:
3566
            extra_args = []
×
3567

3568
        f = super()._gen_llvm_function(ctx=ctx, extra_args=extra_args, tags=tags)
1✔
3569
        if is_comp:
1!
3570
            for a in f.args[-len(extra_args):]:
1✔
3571
                a.attributes.add('nonnull')
1✔
3572

3573
        return f
1✔
3574

3575
    def _gen_llvm_invoke_function(self, ctx, builder, function, params, context,
1✔
3576
                                  variable, out, *, tags:frozenset):
3577
        fun = ctx.import_llvm_function(function)
1✔
3578

3579
        # The function returns (sample_optimal, value_optimal),
3580
        # but the value of mechanism is only 'sample_optimal'
3581
        # so we cannot reuse the space provided and need to explicitly copy
3582
        # the results later.
3583
        fun_out = builder.alloca(fun.args[3].type.pointee, name="func_out")
1✔
3584
        value = builder.gep(fun_out, [ctx.int32_ty(0), ctx.int32_ty(0)])
1✔
3585

3586
        args = [params, context, variable, fun_out]
1✔
3587
        # If we're calling compiled version of Composition.evaluate,
3588
        # we need to pass extra arguments
3589
        if len(fun.args) > 4:
1!
3590
            args += builder.function.args[-3:]
1✔
3591
        builder.call(fun, args)
1✔
3592

3593

3594
        # The mechanism also converts the value to array of arrays
3595
        # e.g. [3 x double] -> [3 x [1 x double]]
3596
        assert len(value.type.pointee) == len(out.type.pointee)
1✔
3597
        assert value.type.pointee.element == out.type.pointee.element.element
1✔
3598
        with pnlvm.helpers.array_ptr_loop(builder, out, id='mech_value_copy') as (b, idx):
1✔
3599
            src = b.gep(value, [ctx.int32_ty(0), idx])
1✔
3600
            dst = b.gep(out, [ctx.int32_ty(0), idx, ctx.int32_ty(0)])
1✔
3601
            b.store(b.load(src), dst)
1✔
3602

3603
        return out, builder
1✔
3604

3605
    @property
1✔
3606
    def agent_rep_type(self):
1✔
3607
        from psyneulink.core.compositions.compositionfunctionapproximator import CompositionFunctionApproximator
1✔
3608
        if (isinstance(self.agent_rep, CompositionFunctionApproximator)
1✔
3609
                or self.agent_rep.componentCategory is COMPOSITION_FUNCTION_APPROXIMATOR):
3610
            return COMPOSITION_FUNCTION_APPROXIMATOR
1✔
3611
        elif self.agent_rep.componentCategory=='Composition':
1!
3612
            return COMPOSITION
1✔
3613
        else:
3614
            return None
×
3615

3616
    @property
1✔
3617
    def agent_rep_input_ports(self):
1✔
3618
        return self._get_agent_rep_input_receivers(type=PORT)
1✔
3619

3620
    @property
1✔
3621
    def num_state_input_ports(self):
1✔
3622
        try:
1✔
3623
            return len(self.state_input_ports)
1✔
3624
        except:
1✔
3625
            return 0
1✔
3626

3627
    # @property
3628
    # def _num_state_feature_specs(self):
3629
    #     return len(self.state_feature_specs)
3630

3631
    @property
1✔
3632
    def state_features(self):
1✔
3633
        """Return {InputPort name: source name} for all state_features.
3634
        If state_feature_spec is numeric for a Node, assign its value as the source
3635
        If existing INPUT Node is not specified in state_feature_specs, assign state_feature_default as source
3636
        If an InputPort is referenced in state_feature_specs that is not yet in agent_rep,
3637
            assign "DEFERRED INPUT NODE <InputPort name> OF <agent_rep>" as key for the entry;
3638
            (it should be resolved by runtime, or an error is generated).
3639
        If a state_feature_spec is referenced that is not yet in ocm.composition,
3640
            assign "<InputPort name> NOT (YET) IN <agent_rep>" as the value of the entry;
3641
            (it should be resolved by runtime, or an error is generated).
3642
        """
3643

3644
        self._update_state_input_port_names()
1✔
3645

3646
        agent_rep_input_ports = self.agent_rep.external_input_ports_of_all_input_nodes
1✔
3647
        state_features_dict = {}
1✔
3648
        state_input_port_num = 0
1✔
3649

3650
        # Process all state_feature_specs, that may include ones for INPUT Nodes not (yet) in agent_rep
3651
        for i in range(self._num_state_feature_specs):
1✔
3652
            spec = self.state_feature_specs[i]
1✔
3653
            input_port = self._specified_INPUT_Node_InputPorts_in_order[i]
1✔
3654
            # Get key for state_features dict
3655
            if input_port in agent_rep_input_ports:
1✔
3656
                # Specified InputPort belongs to an INPUT Node already in agent_rep, so use as key
3657
                key = input_port.full_name
1✔
3658
            else:
3659
                # Specified InputPort is not (yet) in agent_rep
3660
                input_port_name = (f"{input_port.full_name}" if input_port
1✔
3661
                                   else f"{str(i - len(agent_rep_input_ports))}")
3662
                key = _deferred_agent_rep_input_port_name(input_port_name, self.agent_rep.name)
1✔
3663

3664
            # Get source for state_features dict
3665
            if spec is None:
1✔
3666
                # no state_input_port has been constructed, so assign source as None
3667
                source = None
1✔
3668
            else:
3669
                if is_numeric(spec):
1✔
3670
                    # assign numeric spec as source
3671
                    source = spec
1✔
3672
                    # increment state_port_num, since one is implemented for numeric spec
3673
                else:
3674
                    # spec is a Component, so get distal source of Projection to state_input_port
3675
                    #    (spec if it is an OutputPort; or ??input_CIM.output_port if it spec for shadowing an input??
3676
                    state_input_port = self.state_input_ports[state_input_port_num]
1✔
3677
                    if self.composition._is_in_composition(spec):
1✔
3678
                        source = spec.full_name
1✔
3679
                    else:
3680
                        source = _deferred_state_feature_spec_msg(spec.full_name, self.composition.name)
1✔
3681
                state_input_port_num += 1
1✔
3682

3683
            state_features_dict[key] = source
1✔
3684

3685
        return state_features_dict
1✔
3686

3687
    @property
1✔
3688
    def state(self):
1✔
3689
        """Array that is concatenation of state_feature_values and control_allocations"""
3690
        # Use self.state_feature_values Parameter if state_features specified; else use state_input_port values
3691
        return list(self.state_feature_values.values()) + list(self.control_allocation)
1✔
3692

3693
    @property
1✔
3694
    def state_distal_sources_and_destinations_dict(self):
1✔
3695
        """Return dict with (Port, Node, Composition, index) tuples as keys and corresponding state[index] as values.
3696
        Initial entries are for sources of the state_feature_values (i.e., distal afferents for state_input_ports)
3697
        and subsequent entries are for destination parameters modulated by the OptimizationControlMechanism's
3698
        ControlSignals (i.e., distal efferents of its ControlProjections).
3699
        Note: the index is required, since a state_input_port may have more than one afferent Projection
3700
              (that is, a state_feature_value may be determined by Projections from more than one source),
3701
              and a ControlSignal may have more than one ControlProjection (that is, a given element of the
3702
              control_allocation may apply to more than one Parameter).  However, for state_input_ports that shadow
3703
              a Node[InputPort], only that Node[InputPort] is listed in state_dict even if the Node[InputPort] being
3704
              shadowed has more than one afferent Projection (this is because it is the value of the Node[InputPort]
3705
              (after it has processed the value of its afferent Projections) that determines the input to the
3706
              state_input_port.
3707
        """
3708
        sources_and_destinations = self.state_feature_sources
1✔
3709
        sources_and_destinations.update(self.control_signal_destinations)
1✔
3710
        return sources_and_destinations
1✔
3711

3712
    @property
1✔
3713
    def state_feature_sources(self):
1✔
3714
        """Dict with {InputPort: source} for all INPUT Nodes of agent_rep, and sources in **state_feature_specs**.
3715
        Used by state_distal_sources_and_destinations_dict()
3716
        """
3717
        state_dict = {}
1✔
3718
        # FIX: 3/4/22 - THIS NEEDS TO HANDLE BOTH state_input_ports BUT ALSO state_feature_values FOR WHICH THERE ARE NO INPUTPORTS
3719
        specified_state_features = [spec for spec in self.state_feature_specs if spec is not None]
1✔
3720
        for state_index, port in enumerate(self.state_input_ports):
1✔
3721
            if port.path_afferents:
1✔
3722
                get_info_method = self.composition._get_source
1✔
3723
                # MODIFIED 1/8/22: ONLY ONE PROJECTION PER STATE FEATURE
3724
                if port.shadow_inputs:
1✔
3725
                    port = port.shadow_inputs
1✔
3726
                    if port.owner in self.composition.nodes:
1!
3727
                        composition = self.composition
×
3728
                    else:
3729
                        composition = port.path_afferents[0].sender.owner.composition
1✔
3730
                    get_info_method = composition._get_destination
1✔
3731
                source_port, node, comp = get_info_method(port.path_afferents[0])
1✔
3732
            else:
3733
                if port.default_input is DEFAULT_VARIABLE:
1!
3734
                    source_port = DEFAULT_VARIABLE
1✔
3735
                    node = None
1✔
3736
                    comp = None
1✔
3737
                else:
3738
                    source_port = specified_state_features[state_index]
×
3739
                    node = None
×
3740
                    comp = None
×
3741
            state_dict.update({(source_port, node, comp, state_index):self.state[state_index]})
1✔
3742
        return state_dict
1✔
3743

3744
    @property
1✔
3745
    def control_signal_destinations(self):
1✔
3746
        state_dict = {}
1✔
3747
        state_index = self.num_state_input_ports
1✔
3748
        # Get recipients of control_allocations values of state:
3749
        for ctl_index, control_signal in enumerate(self.control_signals):
1✔
3750
            for projection in control_signal.efferents:
1✔
3751
                port, node, comp = self.composition._get_destination(projection)
1✔
3752
                state_dict.update({(port, node, comp, state_index + ctl_index):self.state[state_index + ctl_index]})
1✔
3753
        return state_dict
1✔
3754

3755
    @property
1✔
3756
    def _model_spec_parameter_blacklist(self):
1✔
3757
        # default_variable is hidden in constructor arguments,
3758
        # and anyway assigning it is problematic because it is modified
3759
        # several times when creating input ports, and assigning function that
3760
        # fits the control allocation
3761
        return super()._model_spec_parameter_blacklist.union({
1✔
3762
            'variable',
3763
        })
3764

3765
    # ******************************************************************************************************************
3766
    # FIX:  THE FOLLOWING IS SPECIFIC TO CompositionFunctionApproximator AS agent_rep
3767
    # ******************************************************************************************************************
3768

3769
    def _initialize_composition_function_approximator(self, context):
1✔
3770
        """Initialize CompositionFunctionApproximator"""
3771

3772
        # CompositionFunctionApproximator needs to have access to control_signals to:
3773
        # - to construct control_allocation_search_space from their allocation_samples attributes
3774
        # - compute their values and costs for samples of control_allocations from control_allocation_search_space
3775
        self.agent_rep.initialize(features_array=np.array(self.defaults.variable[1:]),
1✔
3776
                                  control_signals = self.control_signals,
3777
                                  context=context)
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