• 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

79.66
/psyneulink/core/components/ports/port.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
#  *********************************************  Port ********************************************************
10

11
"""
12
Contents
13
--------
14

15
  * `Port_Overview`
16
  * `Port_Creation`
17
      - `Port_Specification`
18
      - `Port_Projections`
19
      - `Port_Deferred_Initialization`
20
  * `Port_Structure`
21
      - `Port_Strucure_Owner`
22
      - `Port_Structure_Projections`
23
      - `Port_Structure_Variable_Function_Value`
24
      - `Port_Modulation`
25
  * `Port_Execution`
26
  * `Port_Examples`
27
  * `Port_Class_Reference`
28

29

30
.. _Port_Overview:
31

32
Overview
33
--------
34

35
A Port provides an interface to one or more `Projections <Projection>`, and receives the `value(s) <Projection>`
36
provided by them.  The value of a Port can be modulated by a `ModulatoryProjection <ModulatoryProjection>`. There are
37
three primary types of Ports (InputPorts, ParameterPorts and OutputPorts) as well as one subtype (ModulatorySignal,
38
used to send ModulatoryProjections), as summarized in the table below:
39

40
.. _Port_types_Table:
41

42
.. table:: **Port Types and Associated Projection Types**
43
   :align: left
44

45
   +-------------------+----------------------+-----------------------+-----------------+------------------------------+
46
   | *Port Type*       | *Owner*              |      *Description*    | *Modulated by*  |       *Specification*        |
47
   +===================+======================+=======================+=================+==============================+
48
   | `InputPort`       |  `Mechanism          |receives input from    |`ControlSignal`  |`InputPort` constructor;      |
49
   |                   |  <Mechanism>`        |`MappingProjection`    |or `GatingSignal`|`Mechanism <Mechanism>`       |
50
   |                   |                      |                       |                 |constructor or its            |
51
   |                   |                      |                       |                 |`add_ports` method            |
52
   +-------------------+----------------------+-----------------------+-----------------+------------------------------+
53
   |`ParameterPort`    |  `Mechanism          |represents parameter   |`ControlSignal`  |Implicitly whenever a         |
54
   |                   |  <Mechanism>` or     |value for a `Component |and/or           |parameter value is            |
55
   |                   |  `Projection         |<Component>`           |`LearningSignal` |`specified                    |
56
   |                   |  <Projection>`       |or its `function       |                 |<ParameterPort_Specification>`|
57
   |                   |                      |<Component.function>`  |                 |                              |
58
   +-------------------+----------------------+-----------------------+-----------------+------------------------------+
59
   | `OutputPort`      |  `Mechanism          |provides output to     |`ControlSignal`  |`OutputPort` constructor;     |
60
   |                   |  <Mechanism>`        |`MappingProjection`    |or `GatingSignal`|`Mechanism <Mechanism>`       |
61
   |                   |                      |                       |                 |constructor or its            |
62
   |                   |                      |                       |                 |`add_ports` method            |
63
   +-------------------+----------------------+-----------------------+-----------------+------------------------------+
64
   |`ModulatorySignal  |`ModulatoryMechanism  |provides value for     |                 |`ModulatoryMechanism          |
65
   |<ModulatorySignal>`|<ModulatoryMechanism>`|`ModulatoryProjection  |                 |<ModulatoryMechanism>`        |
66
   |                   |                      |<ModulatoryProjection>`|                 |constructor; tuple in Port    |
67
   |                   |                      |                       |                 |or parameter specification    |
68
   +-------------------+----------------------+-----------------------+-----------------+------------------------------+
69

70
COMMENT:
71

72
* `InputPort`:
73
    used by a Mechanism to receive input from `MappingProjections <MappingProjection>`;
74
    its value can be modulated by a `ControlSignal` or a `GatingSignal`.
75

76
* `ParameterPort`:
77
    * used by a Mechanism to represent the value of one of its parameters, or a parameter of its
78
      `function <Mechanism_Base.function>`, that can be modulated by a `ControlSignal`;
79
    * used by a `MappingProjection` to represent the value of its `matrix <MappingProjection.MappingProjection.matrix>`
80
      parameter, that can be modulated by a `LearningSignal`.
81

82
* `OutputPort`:
83
    used by a Mechanism to send its value to any efferent projections.  For
84
    `ProcessingMechanisms <ProcessingMechanism>` these are `PathwayProjections <PathwayProjection>`, most commonly
85
    `MappingProjections <MappingProjection>`.  For `ModulatoryMechanisms <ModulatoryMechanism>`, these are
86
    `ModulatoryProjections <ModulatoryProjection>` as described below. The `value <OutputPort.value>` of an
87
    OutputPort can be modulated by a `ControlSignal` or a `GatingSignal`.
88

89
* `ModulatorySignal <ModulatorySignal>`:
90
    a subclass of `OutputPort` used by `ModulatoryMechanisms <ModulatoryMechanism>` to modulate the value of the primary
91
    types of Ports listed above.  There are three types of ModulatorySignals:
92

93
    * `LearningSignal`, used by a `LearningMechanism` to modulate the *MATRIX* ParameterPort of a `MappingProjection`;
94
    * `ControlSignal`, used by a `ControlMechanism <ControlMechanism>` to modulate the `ParameterPort` of a `Mechanism
95
      <Mechanism>`;
96
    * `GatingSignal`, used by a `GatingMechanism` to modulate the `InputPort` or `OutputPort` of a `Mechanism
97
       <Mechanism>`.
98
    Modulation is discussed further `below <Port_Modulation>`, and described in detail under
99
    `ModulatorySignals <ModulatorySignal_Modulation>`.
100

101
COMMENT
102

103
.. _Port_Creation:
104

105
Creating a Port
106
----------------
107

108
In general, Ports are created automatically by the objects to which they belong (their `owner <Port_Strucure_Owner>`),
109
or by specifying the Port in the constructor for its owner.  For example, unless otherwise specified, when a
110
`Mechanism <Mechanism>` is created it creates a default `InputPort` and `OutputPort` for itself, and whenever any
111
Component is created, it automatically creates a `ParameterPort` for each of its `configurable parameters
112
<Component_Structural_Attributes>` and those of its `function <Component_Function>`. Ports are also created in
113
response to explicit specifications.  For example, InputPorts and OutputPorts can be specified in the constructor for
114
a Mechanism (see `Mechanism_Port_Specification`); and ParameterPorts are specified in effect when the value of a
115
parameter for any Component or its `function <Component.function>` is specified in the constructor for that Component
116
or function.  InputPorts and OutputPorts (but *not* ParameterPorts) can also be created directly using their
117
constructors, and then assigned to a Mechanism using the Mechanism's `add_ports <Mechanism_Base.add_ports>` method;
118
however, this should be done with caution as the Port must be compatible with other attributes of its owner (such as
119
its OutputPorts) and its `function <Mechanism_Base.function>` (for example, see `note <Mechanism_Add_InputPorts_Note>`
120
regarding InputPorts). Parameter Ports **cannot** on their own; they are always and only created when the Component
121
to which a parameter belongs is created.
122

123
COMMENT:
124
    IMPLEMENTATION NOTE:
125
    If the constructor for a Port is called programmatically other than on the command line (e.g., within a method)
126
    the **context** argument must be specified (by convention, as ContextFlags.METHOD); otherwise, it is assumed that
127
    it is being created on the command line.  This is taken care of when it is created automatically (e.g., as part
128
    of the construction of a Mechanism or Projection) by the _instantiate_port method that specifies a context
129
    when it calls the relevant Port constructor methods.
130
COMMENT
131

132
.. _Port_Specification:
133

134
*Specifying a Port*
135
~~~~~~~~~~~~~~~~~~~~
136

137
A Port can be specified using any of the following:
138

139
    * existing **Port** object;
140
    ..
141
    * name of a **Port subclass** (`InputPort`, `ParameterPort`, or `OutputPort`) -- creates a default Port of the
142
      specified type, using a default value for the Port that is determined by the context in which it is specified.
143
    ..
144
    * **value** -- creates a default Port using the specified value as its default `value <Port_Base.value>`.
145

146
    .. _Port_Specification_Dictionary:
147

148
    * **Port specification dictionary** -- can use the following: *KEY*:<value> entries, in addition to those
149
      specific to the Port's type (see documentation for each Port type):
150

151
      * *PORT_TYPE*:<Port type>
152
          specifies type of Port to create (necessary if it cannot be determined from
153
          the context of the other entries or in which it is being created).
154
      ..
155
      * *NAME*:<str>
156
          the string is used as the name of the Port.
157
      ..
158
      * *VALUE*:<value>
159
          the value is used as the default value of the Port.
160

161
      A Port specification dictionary can also be used to specify one or more `Projections <Projection>' to or from
162
      the Port, including `ModulatoryProjection(s) <ModulatoryProjection>` used to modify the `value
163
      <Port_Base.value>` of the Port.  The type of Projection(s) created depend on the type of Port specified and
164
      context of the specification (see `examples <Port_Specification_Dictionary_Examples>`).  This can be done using
165
      any of the following entries, each of which can contain any of the forms used to `specify a Projection
166
      <Projection_Specification>`:
167

168
      * *PROJECTIONS*:List[<`projection specification <Projection_Specification>`>,...]
169
          the list must contain one or more `Projection specifications <Projection_Specification>` to or from
170
          the Port, and/or `ModulatorySignals <ModulatorySignal>` from which it should receive projections (see
171
          `Port_Projections` below).
172

173
      .. _Port_Port_Name_Entry:
174

175
      * *<str>*:List[<`projection specification <Projection_Specification>`>,...]
176
          this must be the only entry in the dictionary, and the string cannot be a PsyNeuLink
177
          keyword;  it is used as the name of the Port, and the list must contain one or more `Projection
178
          specifications <Projection_Specification>`.
179

180
      .. _Port_MECHANISM_PORTS_Entries:
181

182
      * *MECHANISM*:Mechanism
183
          this can be used to specify one or more Projections to or from the specified Mechanism.  If the entry appears
184
          without any accompanying Port specification entries (see below), the Projection is assumed to be a
185
          `MappingProjection` to the Mechanism's `primary InputPort <InputPort_Primary>` or from its `primary
186
          OutputPort <OutputPort_Primary>`, depending upon the type of Mechanism and context of specification.  It
187
          can also be accompanied by one or more Port specification entries described below, to create one or more
188
          Projections to/from those specific Ports (see `examples <Port_Port_Name_Entry_Example>`).
189
      ..
190
      * <PORTS_KEYWORD>:List[<str or Port.name>,...]
191
         this must accompany a *MECHANISM* entry (described above), and is used to specify its Port(s) by name.
192
         Each entry must use one of the following keywords as its key, and there can be no more than one of each:
193
            - *INPUT_PORTS*
194
            - *OUTPUT_PORTS*
195
            - *PARAMETER_PORTS*
196
            - *LEARNING_SIGNAL*
197
            - *CONTROL_SIGNAL*
198
            - *GATING_SIGNAL*.
199
         Each entry must contain a list Ports of the specified type, all of which belong to the Mechanism specified in
200
         the *MECHANISM* entry;  each item in the list must be the name of one the Mechanism's Ports, or a
201
         `ProjectionTuple <Port_ProjectionTuple>` the first item of which is the name of a Port. The types of
202
         Ports that can be specified in this manner depends on the type of the Mechanism and context of the
203
         specification (see `examples <Port_Port_Name_Entry_Example>`).
204

205
    * **Port, Mechanism, or list of these** -- creates a default Port with Projection(s) to/from the specified
206
      Ports;  the type of Port being created determines the type and directionality of the Projection(s) and,
207
      if Mechanism(s) are specified, which of their primary Ports are used (see Port subclasses for specifics).
208

209
   .. _Port_Tuple_Specification:
210

211
    * **Tuple specifications** -- these are convenience formats that can be used to compactly specify a Port
212
      by specifying other Components with which it should be connected by Projection(s). Different Ports support
213
      different forms, but all support the following two forms:
214

215
      .. _Port_2_Item_Tuple:
216

217
      * **2-item tuple:** *(<Port name or list of Port names>, <Mechanism>)* -- 1st item is the name of a Port or
218
        list of them, and the 2nd item is the Mechanism to which they belong; a Projection is created to or from each
219
        of the Ports specified.  The type of Projection depends on the type of Port being created, and the type of
220
        Ports specified in the tuple  (see `Projection_Table`).  For example, if the Port being created is an
221
        InputPort, and the Ports specified in the tuple are OutputPorts, then `MappingProjections
222
        <MappingProjection>` are used; if `ModulatorySignals <ModulatorySignal>` are specified, then the corresponding
223
        type of `ModulatoryProjections <ModulatoryProjection>` are created.  See Port subclasses for additional
224
        details and compatibility requirements.
225
      |
226
      .. _Port_ProjectionTuple:
227
      * `ProjectionTuple <Projection_ProjectionTuple>` -- a 4-item tuple that specifies one or more `Projections
228
        <Projection>` to or from other Port(s), along with a weight and/or exponent for each.
229

230
.. _Port_Projections:
231

232
*Projections*
233
~~~~~~~~~~~~~
234

235
When a Port is created, it can be assigned one or more `Projections <Projection>`, in either the **projections**
236
argument of its constructor, or a *PROJECTIONS* entry of a `Port specification dictionary
237
<Port_Specification_Dictionary>` (or a dictionary assigned to the **params** argument of the Port's constructor).
238
The following types of Projections can be specified for each type of Port:
239

240
    .. _Port_Projections_Table:
241

242
    .. table:: **Specifiable Projections for Port Types**
243
        :align: left
244

245
        +------------------+-------------------------------+-------------------------------------+
246
        | *Port Type*      | *PROJECTIONS* specification   | *Assigned to Attribute*             |
247
        +==================+===============================+=====================================+
248
        |`InputPort`       | `PathwayProjection(s)         | `path_afferents                     |
249
        |                  | <PathwayProjection>`          | <Port_Base.path_afferents>`         |
250
        |                  |                               |                                     |
251
        |                  | `ControlProjection(s)         | `mod_afferents                      |
252
        |                  | <ControlProjection>`          | <Port_Base.mod_afferents>`          |
253
        |                  |                               |                                     |
254
        |                  | `GatingProjection(s)          | `mod_afferents                      |
255
        |                  | <GatingProjection>`           | <Port_Base.mod_afferents>`          |
256
        +------------------+-------------------------------+-------------------------------------+
257
        |`ParameterPort`   | `ControlProjection(s)         | `mod_afferents                      |
258
        |                  | <ControlProjection>`          | <ParameterPort.mod_afferents>`      |
259
        +------------------+-------------------------------+-------------------------------------+
260
        |`OutputPort`      | `PathwayProjection(s)         | `efferents                          |
261
        |                  | <PathwayProjection>`          | <Port_Base.efferents>`              |
262
        |                  |                               |                                     |
263
        |                  | `ControlProjection(s)         | `mod_afferents                      |
264
        |                  | <ControlProjection>`          | <Port_Base.mod_afferents>`          |
265
        |                  |                               |                                     |
266
        |                  | `GatingProjection(s)          | `mod_afferents                      |
267
        |                  | <GatingProjection>`           | <Port_Base.mod_afferents>`          |
268
        +------------------+-------------------------------+-------------------------------------+
269
        |`ModulatorySignal`|  `ModulatoryProjection(s)     | `efferents                          |
270
        |                  |  <ModulatoryProjection>`      | <ModulatorySignal.efferents>`       |
271
        +------------------+-------------------------------+-------------------------------------+
272

273
Projections must be specified in a list.  Each entry must be either a `specification for a projection
274
<Projection_Specification>`, or for a `sender <Projection_Base.sender>` or `receiver <Projection_Base.receiver>` of
275
one, in which case the appropriate type of Projection is created.  A sender or receiver can be specified as a `Port
276
<Port>` or a `Mechanism <Mechanism>`. If a Mechanism is specified, its primary `InputPort <InputPort_Primary>` or
277
`OutputPort <OutputPort_Primary>`  is used, as appropriate.  When a sender or receiver is used to specify the
278
Projection, the type of Projection created is inferred from the Port and the type of sender or receiver specified,
279
as illustrated in the `examples <Port_Projections_Examples>` below.  Note that the Port must be `assigned to an
280
owner <Port_Creation>` in order to be functional, irrespective of whether any `Projections <Projection>` have been
281
assigned to it.
282

283

284
.. _Port_Deferred_Initialization:
285

286
*Deferred Initialization*
287
~~~~~~~~~~~~~~~~~~~~~~~~~
288

289
If a Port is created on its own, and its `owner <Port_Strucure_Owner>` Mechanism is specified, it is assigned to that
290
Mechanism; if its owner not specified, then its initialization is `deferred <Port_Deferred_Initialization>`.
291
Its initialization is completed automatically when it is assigned to an owner `Mechanism <Mechanism>` using the
292
owner's `add_ports <Mechanism_Base.add_ports>` method.  If the Port is not assigned to an owner, it will not be
293
functional (i.e., used during the execution of `Mechanisms <Mechanism_Execution>` and/or `Compositions
294
<Composition_Execution>`, irrespective of whether it has any `Projections <Projection>` assigned to it.
295

296

297
.. _Port_Structure:
298

299
Structure
300
---------
301

302
.. _Port_Strucure_Owner:
303

304
*Owner*
305
~~~~~~~
306

307
Every Port has an `owner <Port_Base.owner>`.  For `InputPorts <InputPort>` and `OutputPorts <OutputPort>`, the
308
owner must be a `Mechanism <Mechanism>`.  For `ParameterPorts <ParameterPort>` it can be a Mechanism or a
309
`PathwayProjection <PathwayProjection>`. For `ModulatorySignals <ModulatorySignal>`, it must be a `ModulatoryMechanism
310
<ModulatoryMechanism>`. When a Port is created as part of another Component, its `owner <Port_Base.owner>` is
311
assigned automatically to that Component.  It is also assigned automatically when the Port is assigned to a
312
`Mechanism <Mechanism>` using that Mechanism's `add_ports <Mechanism_Base.add_ports>` method.  Otherwise, it must be
313
specified explicitly in the **owner** argument of the constructor for the Port (in which case it is immediately
314
assigned to the specified Mechanism).  If the **owner** argument is not specified, the Port's initialization is
315
`deferred <Port_Deferred_Initialization>` until it has been assigned to an owner using the owner's `add_ports
316
<Mechanism_Base.add_ports>` method.
317

318
.. _Port_Structure_Projections:
319

320
*Projections*
321
~~~~~~~~~~~~~
322

323
Every Port has attributes that lists the `Projections <Projection>` it sends and/or receives.  These depend on the
324
type of Port, listed below (and shown in the `table <Port_Projections_Table>`):
325

326
.. table::  Port Projection Attributes
327
   :align: left
328

329
   ============================================ ============================================================
330
   *Attribute*                                  *Projection Type and Port(s)*
331
   ============================================ ============================================================
332
   `path_afferents <Port_Base.path_afferents>`  `MappingProjections <MappingProjection>` to `InputPort`
333
   `mod_afferents <Port_Base.mod_afferents>`    `ModulatoryProjections <ModulatoryProjection>` to any Port
334
   `efferents <Port_Base.efferents>`            `MappingProjections <MappingProjection>` from `OutputPort`
335
   ============================================ ============================================================
336

337
In addition to these attributes, all of the Projections sent and received by a Port are listed in its `projections
338
<Port_Base.projections>` attribute.
339

340

341
.. _Port_Structure_Variable_Function_Value:
342

343
*Variable, Function and Value*
344
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
345

346
In addition, like all PsyNeuLink Components, it also has the three following core attributes:
347

348
    * `variable <Port_Base.variable>`:  for an `InputPort` and `ParameterPort`,
349
      the value of this is determined by the value(s) of the Projection(s) that it receives (and that are listed in
350
      its `path_afferents <Port_Base.path_afferents>` attribute).  For an `OutputPort`, it is the item of the owner
351
      Mechanism's `value <Mechanism_Base.value>` to which the OutputPort is assigned (specified by the OutputPort's
352
      `index <OutputPort_Index>` attribute.
353
    ..
354
    * `function <Port_Base.function>`:  for an `InputPort` this combines the values of the Projections that the
355
      Port receives (the default is `LinearCombination` that sums the values), under the potential influence of a
356
      `GatingSignal`;  for a `ParameterPort`, it determines the value of the associated parameter, under the potential
357
      influence of a `ControlSignal` (for a `Mechanism <Mechanism>`) or a `LearningSignal` (for a `MappingProjection`);
358
      for an OutputPort, it conveys the result  of the Mechanism's function to its `output_values
359
      <Mechanism_Base.output_values>` attribute, under the potential influence of a `GatingSignal`.  See
360
      `ModulatorySignals <ModulatorySignal_Structure>` and the `ModulatoryMechanism <ModulatoryMechanism>` associated
361
      with each type for a description of how they can be used to modulate the `function <Port_Base.function>` of a
362
      Port.
363
    ..
364
    * `value <Port_Base.value>`:  for an `InputPort` this is the combined value of the `PathwayProjections` it
365
      receives;  for a `ParameterPort`, this represents the value of the parameter that will be used by the Port's
366
      owner or its `function <Component.function>`; for an `OutputPort`, it is the item of the  owner Mechanism's
367
      `value <Mechanisms.value>` to which the OutputPort is assigned, possibly modified by its `assign
368
      <OutputPort_Assign>` attribute and/or a `GatingSignal`, and used as the `value <Projection_Base.value>` of
369
      the Projections listed in its `efferents <OutputPort.path_efferents>` attribute.
370

371
.. _Port_Modulation:
372

373
*Modulation*
374
~~~~~~~~~~~~
375

376
Every type of Port has a `mod_afferents <Port_Base.mod_afferents>` attribute, that lists the `ModulatoryProjections
377
<ModulatoryProjection>` it receives.  Each ModulatoryProjection comes from a `ModulatorySignal <ModulatorySignal>`
378
that specifies how it should modulate the Port's `value <Port_Base.value>` when the Port is updated (see
379
`ModulatorySignal_Modulation` and `ModulatorySignal_Anatomy_Figure`).  In most cases, a ModulatorySignal uses the
380
Port's `function <Port_Base.function>` to modulate its `value <Port_Base.value>`.  The function of every Port
381
assigns one of its parameters as its *ADDITIVE_PARAM* and another as its *MULTIPLICATIVE_PARAM*. The
382
`modulation <ModulatorySignal.modulation>` attribute of a ModulatorySignal determines which of these to modify when the
383
Port uses it `function <Port_Base.function>` to calculate its `value  <Port_Base.value>`.  However, the
384
ModulatorySignal can also be configured to override the Port's `value <Port_Base.value>` (i.e., assign it directly),
385
or to disable modulation, using either the keyword *OVERRIDE* or *DSIABLE*, respectively, to specify the value for its
386
`modulation <ModulatorySignal.modulation>` attribute (see `ModulatorySignal_Modulation` for a more detailed discussion).
387

388
.. _Port_Execution:
389

390
Execution
391
---------
392

393
Ports cannot be executed.  They are updated when the Component to which they belong is executed.  InputPorts and
394
ParameterPorts belonging to a Mechanism are updated before the Mechanism's function is called.  OutputPorts are
395
updated after the Mechanism's function is called.  When a Port is updated, it executes any Projections that project
396
to it (listed in its `all_afferents <Port_Base.all_afferents>` attribute.  It uses the values it receives from any
397
`PathWayProjections` (listed in its `path_afferents` attribute) as the variable for its
398
`function <Port_Base.function>`. It then executes all of the ModulatoryProjections it receives.  Different
399
ModulatorySignals may call for different forms of modulation (see `ModulatorySignal_Modulation`).  Accordingly,
400
it separately sums the values specified by any ModulatorySignals for the *MULTIPLICATIVE_PARAM* of its
401
`function <Port_Base.function>`, and similarly for the *ADDITIVE_PARAM*.  It then applies the summed value for each
402
to the corresponding parameter of its `function <Port_Base.function>`.  If any of the ModulatorySignals specifies
403
*OVERRIDE*, then the value of that ModulatorySignal is used as the Port's `value <Port_Base.value>`. Finally,
404
the Port calls its `function <Port_Base.function>` to determine its `value <Port_Base.value>`.
405

406
.. note::
407
   The change in the value of a `Port <Port>` does not occur until the Mechanism to which the Port belongs is next
408
   executed; This conforms to a "lazy evaluation" protocol (see `Lazy Evaluation <Component_Lazy_Updating>` for an
409
   explanation of "lazy" updating).
410

411
.. _Port_Examples:
412

413
Examples
414
========
415

416
.. _Port_Constructor_Examples:
417

418
Usually, Ports are created automatically by the Mechanism to which they belong.  For example, creating a
419
TransferMechanism::
420

421
    my_mech = pnl.TransferMechanism()
422

423
automatically creates an InputPort, ParameterPorts for its parameters, including the `slope <Linear.slope>` and
424
`intercept <Linear.intercept>` parameters of its `Linear` `Function <Function>` (its default `function
425
<Mechanism_Base.function>`), and an OutputPort (named *RESULT*)::
426

427
    print(my_mech.input_ports)
428
    > [(InputPort InputPort-0)]
429
    print(my_mech.parameter_ports)
430
    > [(ParameterPort intercept), (ParameterPort slope), (ParameterPort noise), (ParameterPort integration_rate)]
431
    print(my_mech.output_ports)
432
    > [(OutputPort RESULT)]
433

434
.. _Port_Constructor_Argument_Examples:
435

436
*Using the* **input_ports** *argument of a Mechanism constructor.*
437

438
When Ports are specified explicitly, it is usually in an argument of the constructor for the Mechanism to which they
439
belong.  For example, the following specifies that ``my_mech`` should have an InputPort named 'MY INPUT`::
440

441
    my_mech = pnl.TransferMechanism(input_ports=['MY INPUT'])
442
    print(my_mech.input_ports)
443
    > [(InputPort 'MY INPUT')]
444

445
The InputPort was specified by a string (for its name) in the **input_ports** argument.  It can also be specified in
446
a variety of other ways, as described `above <Port_Specification>` and illustrated in the examples below.
447
Note that when one or more Ports is specified in the argument of a Mechanism's constructor, it replaces any defaults
448
Ports created by the Mechanism when none are specified (see `note <Mechanism_Default_Port_Suppression_Note>`.
449

450
.. _port_value_Spec_Example:
451

452
For example, the following specifies the InputPort by a value to use as its `variable <InputPort.variable>` attribute::
453

454
    my_mech = pnl.TransferMechanism(input_ports=[[0,0])
455

456
The value is also used to format the InputPort's `value <InputPort.value>`, as well as the first (and, in this case,
457
only) item of the Mechanism's `variable <Mechanism_Base.variable>` (i.e., the one to which the InputPort is
458
assigned), as show below::
459

460
    print(my_mech.input_ports[0].variable)
461
    > [0 0]
462
    print (my_mech.input_port.value)
463
    > [ 0.  0.]
464
    print (my_mech.variable)
465
    > [[0 0]]
466

467
Note that in the first print port, the InputPort was referenced as the first one in the `input_ports
468
<Mechanism_Base.input_ports>` attribute of ``my_mech``;  the second print port references it directly,
469
as the `primary InputPort <Input_port.primary>` of ``my_mech``, using its `input_port <Mechanism_Base.input_port>`
470
attribute (note the singular).
471

472
.. _Port_Multiple_InputSates_Example:
473

474
*Multiple InputPorts*
475

476
The **input_ports** argument can also be used to create more than one InputPort::
477

478
    my_mech = pnl.TransferMechanism(input_ports=['MY FIRST INPUT', 'MY SECOND INPUT'])
479
    print(my_mech.input_ports)
480
    > [(InputPort MY FIRST INPUT), (InputPort MY SECOND INPUT)]
481

482
Here, the print statement uses the `input_ports <Mechanism_Base.input_ports>` attribute, since there is now more
483
than one InputPort.  OutputPorts can be specified in a similar way, using the **output_ports** argument.
484

485
    .. note::
486
        Although InputPorts and OutputPorts can be specified in a Mechanism's constructor, ParameterPorts cannot;
487
        those are created automatically when the Mechanism is created, for each of its `user configurable parameters
488
        <Component_User_Params>`  and those of its `function <Mechanism_Base.function>`.  However, the `value
489
        <ParameterPort.value>` can be specified when the Mechanism is created, or its `function
490
        <Mechanism_Base.function>` is assigned, and can be accessed and subsequently modified, as described under
491
        `ParameterPort_Specification>`.
492

493
.. _Port_Standard_OutputPorts_Example:
494

495
*OutputPorts*
496

497
The following example specifies two OutputPorts for ``my_mech``, using its `Standard OutputPorts
498
<OutputPort_Standard>`::
499

500
    my_mech = pnl.TransferMechanism(output_ports=['RESULT', 'MEAN'])
501

502
As with InputPorts, specification of OutputPorts in the **output_ports** argument suppresses the creation of any
503
default OutputPorts that would have been created if no OutputPorts were specified (see `note
504
<Mechanism_Default_Port_Suppression_Note>` above).  For example, TransferMechanisms create a *RESULT* OutputPort
505
by default, that contains the result of their `function <OutputPort.function>`.  This default behavior is suppressed
506
by any specifications in its **output_ports** argument.  Therefore, to retain a *RESULT* OutputPort,
507
it must be included in the **output_ports** argument along with any others that are specified, as in the example
508
above.  If the name of a specified OutputPort matches the name of a Standard OutputPort <OutputPort_Standard>` for
509
the type of Mechanism, then that is used (as is the case for both of the OutputPorts specified for the
510
`TransferMechanism` in the example above); otherwise, a new OutputPort is created.
511

512
.. _Port_Specification_Dictionary_Examples:
513

514
*Port specification dictionary*
515

516
Ports can be specified in greater detail using a `Port specification dictionary
517
<Port_Specification_Dictionary>`. In the example below, this is used to specify the variable and name of an
518
InputPort::
519

520
    my_mech = pnl.TransferMechanism(input_ports=[{PORT_TYPE: InputPort,
521
                                                   NAME: 'MY INPUT',
522
                                                   VARIABLE: [0,0]})
523

524
The *PORT_TYPE* entry is included here for completeness, but is not actually needed when the Port specification
525
dicationary is used in **input_ports** or **output_ports** argument of a Mechanism, since the Port's type
526
is clearly determined by the context of the specification;  however, where that is not clear, then the *PORT_TYPE*
527
entry must be included.
528

529
.. _Port_Projections_Examples:
530

531
*Projections*
532

533
A Port specification dictionary can also be used to specify projections to or from the Port, also in
534
a number of different ways.  The most straightforward is to include them in a *PROJECTIONS* entry.  For example, the
535
following specifies that the InputPort of ``my_mech`` receive two Projections,  one from ``source_mech_1`` and another
536
from ``source_mech_2``, and that its OutputPort send one to ``destination_mech``::
537

538
    source_mech_1 = pnl.TransferMechanism(name='SOURCE_1')
539
    source_mech_2 = pnl.TransferMechanism(name='SOURCE_2')
540
    destination_mech = pnl.TransferMechanism(name='DEST')
541
    my_mech = pnl.TransferMechanism(name='MY_MECH',
542
                                    input_ports=[{pnl.NAME: 'MY INPUT',
543
                                                   pnl.PROJECTIONS:[source_mech_1, source_mech_2]}],
544
                                    output_ports=[{pnl.NAME: 'RESULT',
545
                                                    pnl.PROJECTIONS:[destination_mech]}])
546

547
    # Print names of the Projections:
548
    for projection in my_mech.input_port.path_afferents:
549
        print(projection.name)
550
    > MappingProjection from SOURCE_1[RESULT] to MY_MECH[MY INPUT]
551
    > MappingProjection from SOURCE_2[RESULT] to MY_MECH[MY INPUT]
552
    for projection in my_mech.output_port.efferents:
553
        print(projection.name)
554
    > MappingProjection from MY_MECH[RESULT] to DEST[InputPort]
555

556

557
A *PROJECTIONS* entry can contain any of the forms used to `specify a Projection <Projection_Specification>`.
558
Here, Mechanisms are used, which creates Projections from the `primary InputPort <InputPort_Primary>` of
559
``source_mech``, and to the `primary OutputPort <OutputPort_Primary>` of ``destination_mech``.  Note that
560
MappingProjections are created, since the Projections specified are between InputPorts and OutputPorts.
561
`ModulatoryProjections` can also be specified in a similar way.  The following creates a `GatingMechanism`, and
562
specifies that the InputPort of ``my_mech`` should receive a `GatingProjection` from it::
563

564
    my_gating_mech = pnl.GatingMechanism()
565
    my_mech = pnl.TransferMechanism(name='MY_MECH',
566
                                    input_ports=[{pnl.NAME: 'MY INPUT',
567
                                                   pnl.PROJECTIONS:[my_gating_mech]}])
568

569

570
.. _Port_Modulatory_Projections_Examples:
571

572
Conversely, ModulatoryProjections can also be specified from a Mechanism to one or more Ports that it modulates.  In
573
the following example, a `ControlMechanism` is created that sends `ControlProjections <ControlProjection>` to the
574
`drift_rate <DriftDiffusionAnalytical.drift_rate>` and `threshold <DriftDiffusionAnalytical.threshold>`
575
ParameterPorts of a `DDM` Mechanism::
576

577
    my_mech = pnl.DDM(name='MY DDM')
578
    my_ctl_mech = pnl.ControlMechanism(control_signals=[{pnl.NAME: 'MY DDM DRIFT RATE AND THREHOLD CONTROL SIGNAL',
579
                                                         pnl.PROJECTIONS: [my_mech.parameter_ports[pnl.DRIFT_RATE],
580
                                                                           my_mech.parameter_ports[pnl.THRESHOLD]]}])
581
    # Print ControlSignals and their ControlProjections
582
    for control_signal in my_ctl_mech.control_signals:
583
        print(control_signal.name)
584
        for control_projection in control_signal.efferents:
585
            print("\t{}: {}".format(control_projection.receiver.owner.name, control_projection.receiver))
586
    > MY DDM DRIFT RATE AND THRESHOLD CONTROL SIGNAL
587
    >     MY DDM: (ParameterPort drift_rate)
588
    >     MY DDM: (ParameterPort threshold)
589

590
Note that a ControlMechanism uses a **control_signals** argument in place of an **output_ports** argument (since it
591
uses `ControlSignal <ControlSignals>` for its `OutputPorts <OutputPort>`.  Note also that, for specifying Projections
592
of a ControlSignal (i.e., its ControlProjections), the keyword *CONTROL* can be used in place of the more generic
593
*PROJECTIONS* keyword (as shown in the example below).
594

595
In the example above, both ControlProjections are assigned to a single ControlSignal.  However, they could each be
596
assigned to their own by specifying them in separate items of the **control_signals** argument::
597

598
    my_mech = pnl.DDM(name='MY DDM')
599
    my_ctl_mech = pnl.ControlMechanism(control_signals=[{pnl.NAME: 'DRIFT RATE CONTROL SIGNAL',
600
                                                         pnl.CONTROL: [my_mech.parameter_ports[pnl.DRIFT_RATE]]},
601
                                                        {pnl.NAME: 'THRESHOLD RATE CONTROL SIGNAL',
602
                                                         pnl.CONTROL: [my_mech.parameter_ports[pnl.THRESHOLD]]}])
603
    # Print ControlSignals and their ControlProjections...
604
    > DRIFT RATE CONTROL SIGNAL
605
    >     MY DDM: (ParameterPort drift_rate)
606
    > THRESHOLD RATE CONTROL SIGNAL
607
    >     MY DDM: (ParameterPort threshold)
608

609
Specifying Projections in a Port specification dictionary affords flexibility -- for example, naming the Port
610
and/or specifying other attributes.  However, if this is not necessary, the Projections can be used to specify
611
Ports directly.  For example, the following, which is much simpler, produces the same result as the previous
612
example (sans the custom name; though as the printout below shows, the default names are usually pretty clear)::
613

614
    my_ctl_mech = pnl.ControlMechanism(control_signals=[my_mech.parameter_ports[pnl.DRIFT_RATE],
615
                                                        my_mech.parameter_ports[pnl.THRESHOLD]])
616
    # Print ControlSignals and their ControlProjections...
617
    > MY DDM drift_rate ControlSignal
618
    >    MY DDM: (ParameterPort drift_rate)
619
    > MY DDM threshold ControlSignal
620
    >    MY DDM: (ParameterPort threshold)
621

622
.. _Port_Port_Name_Entry_Example:
623

624
*Convenience formats*
625

626
There are two convenience formats for specifying Ports and their Projections in a Port specification
627
dictionary.  The `first <Port_Port_Name_Entry>` is to use the name of the Port as the key for its entry,
628
and then a list of , as in the following example::
629

630
    source_mech_1 = pnl.TransferMechanism()
631
    source_mech_2 = pnl.TransferMechanism()
632
    destination_mech = pnl.TransferMechanism()
633
    my_mech_C = pnl.TransferMechanism(input_ports=[{'MY INPUT':[source_mech_1, source_mech_2]}],
634
                                      output_ports=[{'RESULT':[destination_mech]}])
635

636
This produces the same result as the first example under `Port specification dictionary <Port_Projections_Examples>`
637
above, but it is simpler and easier to read.
638

639
The second convenience format is used to specify one or more Projections to/from the Ports of a single Mechanism
640
by their name.  It uses the keyword *MECHANISM* to specify the Mechanism, coupled with a Port-specific entry to
641
specify Projections to its Ports.  This can be useful when a Mechanism must send Projections to several Ports
642
of another Mechanism, such as a ControlMechanism that sends ControlProjections to several parameters of a
643
given Mechanism, as in the following example::
644

645
    my_mech = pnl.DDM(name='MY DDM')
646
    my_ctl_mech = pnl.ControlMechanism(control_signals=[{pnl.MECHANISM: my_mech,
647
                                                         pnl.PARAMETER_PORTS: [pnl.DRIFT_RATE, pnl.THRESHOLD]}])
648

649
This produces the same result as the `earlier example <Port_Modulatory_Projections_Examples>` of ControlProjections,
650
once again in a simpler and easier to read form.  However, it be used only to specify Projections for a Port to or
651
from the Ports of a single Mechanism;  Projections involving other Mechanisms must be assigned to other Ports.
652

653
.. _Port_Create_Port_Examples:
654

655
*Create and then assign a port*
656

657
Finally, a Port can be created directly using its constructor, and then assigned to a Mechanism.
658
The following creates an InputPort ``my_input_port`` with a `MappingProjection` to it from the
659
`primary OutputPort <OutputPort_Primary>` of ``mech_A`` and assigns it to ``mech_B``::
660

661
    mech_A = pnl.TransferMechanism()
662
    my_input_port = pnl.InputPort(name='MY INPUTPORT',
663
                                    projections=[mech_A])
664
    mech_B = pnl.TransferMechanism(input_ports=[my_input_port])
665
    print(mech_B.input_ports)
666
    > [(InputPort MY INPUTPORT)]
667

668
The InputPort ``my_input_port`` could also have been assigned to ``mech_B`` in one of two other ways:
669
by explicity adding it using ``mech_B``\\'s `add_ports <Mechanism_Base.add_ports>` method::
670

671
    mech_A = pnl.TransferMechanism()
672
    my_input_port = pnl.InputPort(name='MY INPUTPORT',
673
                                    projections=[mech_A])
674
    mech_B = pnl.TransferMechanism()
675
    mech_B.add_ports([my_input_port])
676

677
or by constructing it after ``mech_B`` and assigning ``mech_B`` as its owner::
678

679
    mech_A = pnl.TransferMechanism()
680
    mech_B = pnl.TransferMechanism()
681
    my_input_port = pnl.InputPort(name='MY INPUTPORT',
682
                                    owner=mech_B,
683
                                    projections=[mech_A])
684

685
Note that, in both cases, adding the InputPort to ``mech_B`` does not replace its the default InputPort generated
686
when it was created, as shown by printing the `input_ports <Mechanism_Base.input_ports>` for ``mech_B``::
687

688
    print(mech_B.input_ports)
689
    > [(InputPort InputPort-0), (InputPort MY INPUTPORT)]
690
    > [(InputPort InputPort-0), (InputPort MY INPUTPORT)]
691

692
As a consequence, ``my_input_port`` is  **not** the `primary InputPort <InputPort_Primary>` for ``mech_B`` (i.e.,
693
input_ports[0]), but rather its second InputPort (input_ports[1]). This is differs from specifying the InputPort
694
as part of the constructor for the Mechanism, which suppresses generation of the default InputPort,
695
as in the first example above (see `note <Mechanism_Default_Port_Suppression_Note>`).
696

697
COMMENT:
698

699
*** ??ADD THESE TO EXAMPLES, HERE OR IN Projection??
700

701
    def test_mapping_projection_with_mech_and_port_Name_specs(self):
702
         R1 = pnl.TransferMechanism(output_ports=['OUTPUT_1', 'OUTPUT_2'])
703
         R2 = pnl.TransferMechanism(default_variable=[[0],[0]],
704
                                    input_ports=['INPUT_1', 'INPUT_2'])
705
         T = pnl.TransferMechanism(input_ports=[{pnl.MECHANISM: R1,
706
                                                  pnl.OUTPUT_PORTS: ['OUTPUT_1', 'OUTPUT_2']}],
707
                                   output_ports=[{pnl.MECHANISM:R2,
708
                                                   pnl.INPUT_PORTS: ['INPUT_1', 'INPUT_2']}])
709

710
   def test_transfer_mech_input_ports_specification_dict_spec(self):
711
        R1 = TransferMechanism(output_ports=['FIRST', 'SECOND'])
712
        T = TransferMechanism(default_variable=[[0],[0]],
713
                                      input_ports=[{NAME: 'FROM DECISION',
714
                                                     PROJECTIONS: [R1.output_ports['FIRST']]},
715
                                                    {NAME: 'FROM RESPONSE_TIME',
716
                                                     PROJECTIONS: R1.output_ports['SECOND']}])
717

718
   def test_transfer_mech_input_ports_projection_in_specification_dict_spec(self):
719
        R1 = TransferMechanism(output_ports=['FIRST', 'SECOND'])
720
        T = TransferMechanism(input_ports=[{NAME: 'My InputPort with Two Projections',
721
                                             PROJECTIONS:[R1.output_ports['FIRST'],
722
                                                          R1.output_ports['SECOND']]}])
723

724
    def test_transfer_mech_input_ports_mech_output_port_in_specification_dict_spec(self):
725
        R1 = TransferMechanism(output_ports=['FIRST', 'SECOND'])
726
        T = TransferMechanism(input_ports=[{MECHANISM: R1,
727
                                             OUTPUT_PORTS: ['FIRST', 'SECOND']}])
728
        assert len(T.input_ports)==1
729
        for input_port in T.input_ports:
730
            for projection in input_port.path_afferents:
731
                assert projection.sender.owner is R1
732

733
creates a `GatingSignal` with
734
`GatingProjections <GatingProjection>` to ``mech_B`` and ``mech_C``, and assigns it to ``my_gating_mech``::
735

736
    my_gating_signal = pnl.GatingSignal(projections=[mech_B, mech_C])
737
    my_gating_mech = GatingMechanism(gating_signals=[my_gating_signal]
738

739
The `GatingMechanism` created will gate the `primary InputPorts <InputPort_Primary>` of ``mech_B`` and ``mech_C``.
740

741
The following creates
742

743
   def test_multiple_modulatory_projections_with_mech_and_port_Name_specs(self):
744

745
        M = pnl.DDM(name='MY DDM')
746
        C = pnl.ControlMechanism(control_signals=[{pnl.MECHANISM: M,
747
                                                   pnl.PARAMETER_PORTS: [pnl.DRIFT_RATE, pnl.THRESHOLD]}])
748
        G = pnl.GatingMechanism(gating_signals=[{pnl.MECHANISM: M,
749
                                                 pnl.OUTPUT_PORTS: [pnl.DECISION_VARIABLE, pnl.RESPONSE_TIME]}])
750

751

752
        M = pnl.DDM(name='MY DDM')
753
        C = pnl.ControlMechanism(control_signals=[{'DECISION_CONTROL':[M.parameter_ports[pnl.DRIFT_RATE],
754
                                                                       M.parameter_ports[pnl.THRESHOLD]]}])
755
        G = pnl.GatingMechanism(gating_signals=[{'DDM_OUTPUT_GATE':[M.output_ports[pnl.DECISION_VARIABLE],
756
                                                                    M.output_ports[pnl.RESPONSE_TIME]]}])
757

758
COMMENT
759

760
.. _Port_Class_Reference:
761

762
Class Reference
763
---------------
764

765
"""
766

767
import abc
1✔
768
import inspect
1✔
769
import itertools
1✔
770
import numbers
1✔
771
import sys
1✔
772
import types
1✔
773
import warnings
1✔
774
from collections import defaultdict
1✔
775
from collections.abc import Iterable
1✔
776

777
import numpy as np
1✔
778
from beartype import beartype
1✔
779

780
from psyneulink._typing import Optional, Union, Type
1✔
781

782
from psyneulink.core import llvm as pnlvm
1✔
783
from psyneulink.core.components.component import ComponentError, DefaultsFlexibility, component_keywords
1✔
784
from psyneulink.core.components.functions.function import \
1✔
785
    Function, get_param_value_for_keyword, is_function_type, RandomMatrix
786
from psyneulink.core.components.functions.nonstateful.transformfunctions import TransformFunction, LinearCombination
1✔
787
from psyneulink.core.components.functions.nonstateful.transferfunctions import Linear
1✔
788
from psyneulink.core.components.shellclasses import Mechanism, Projection, Port
1✔
789
from psyneulink.core.globals.context import ContextFlags, handle_external_context
1✔
790
from psyneulink.core.globals.keywords import \
1✔
791
    ADDITIVE, ADDITIVE_PARAM, AUTO_ASSIGN_MATRIX, CONTEXT, CONTROL_PROJECTION_PARAMS, CONTROL_SIGNAL_SPECS, \
792
    DEFAULT_INPUT, DEFAULT_VARIABLE, DEFERRED_INITIALIZATION, DISABLE, \
793
    EXPONENT, FEEDBACK, FUNCTION, FUNCTION_PARAMS, GATING_PROJECTION_PARAMS, GATING_SIGNAL_SPECS, INPUT_PORTS, \
794
    LEARNING_PROJECTION_PARAMS, LEARNING_SIGNAL_SPECS, \
795
    MATRIX, MECHANISM, MODULATORY_PROJECTION, MODULATORY_PROJECTIONS, MODULATORY_SIGNAL, \
796
    MULTIPLICATIVE, MULTIPLICATIVE_PARAM, \
797
    NAME, OUTPUT_PORTS, OVERRIDE, OWNER, \
798
    PARAMETER_PORTS, PARAMS, PATHWAY_PROJECTIONS, PREFS_ARG, \
799
    PROJECTION_DIRECTION, PROJECTIONS, PROJECTION_PARAMS, PROJECTION_TYPE, \
800
    RECEIVER, REFERENCE_VALUE, REFERENCE_VALUE_NAME, SENDER, STANDARD_OUTPUT_PORTS, \
801
    PORT, PORT_COMPONENT_CATEGORY, PORT_CONTEXT, Port_Name, port_params, PORT_PREFS, PORT_TYPE, port_value, \
802
    VALUE, VARIABLE, WEIGHT
803
from psyneulink.core.globals.parameters import Parameter, check_user_specified, copy_parameter_value
1✔
804
from psyneulink.core.globals.preferences.basepreferenceset import VERBOSE_PREF
1✔
805
from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel
1✔
806
from psyneulink.core.globals.registry import register_category
1✔
807
from psyneulink.core.globals.socket import ConnectionInfo
1✔
808
from psyneulink.core.globals.utilities import \
1✔
809
    ContentAddressableList, convert_all_elements_to_np_array, convert_to_np_array, get_args, is_numeric, is_value_spec, iscompatible, \
810
    MODULATION_OVERRIDE, try_extract_0d_array_item, type_match
811

812
__all__ = [
1✔
813
    'Port_Base', 'port_keywords', 'port_type_keywords', 'PortError', 'PortRegistry', 'PORT_SPEC'
814
]
815

816
port_keywords = component_keywords.copy()
1✔
817
port_keywords.update({MECHANISM,
1✔
818
                      PORT_TYPE,
819
                      port_value,
820
                      port_params,
821
                      PATHWAY_PROJECTIONS,
822
                      MODULATORY_PROJECTIONS,
823
                      PROJECTION_TYPE,
824
                      PROJECTION_PARAMS,
825
                      LEARNING_PROJECTION_PARAMS,
826
                      LEARNING_SIGNAL_SPECS,
827
                      CONTROL_PROJECTION_PARAMS,
828
                      CONTROL_SIGNAL_SPECS,
829
                      GATING_PROJECTION_PARAMS,
830
                      GATING_SIGNAL_SPECS})
831

832
port_type_keywords = {PORT_TYPE}
1✔
833

834
PORT_SPECIFIC_PARAMS = 'PORT_SPECIFIC_PARAMS'
1✔
835
PROJECTION_SPECIFIC_PARAMS = 'PROJECTION_SPECIFIC_PARAMS'
1✔
836

837

838
STANDARD_PORT_ARGS = {PORT_TYPE, OWNER, REFERENCE_VALUE, VARIABLE, NAME, PARAMS, PREFS_ARG}
1✔
839
PORT_SPEC = 'port_spec'
1✔
840

841

842
def _is_port_class(spec):
1✔
843
    if inspect.isclass(spec) and issubclass(spec, Port):
×
844
        return True
×
845
    return False
×
846

847

848
def match_modulation_to_value(modulatory_value, reference_value):
1✔
849
    modulatory_value = type_match(modulatory_value, type(reference_value))
1✔
850

851
    # mimics cast to float when default is a float, which previously
852
    # happened in type_match
853
    if isinstance(reference_value, np.ndarray) and reference_value.ndim == 0:
1✔
854
        modulatory_value = modulatory_value.reshape(reference_value.shape)
1✔
855

856
    return modulatory_value
1✔
857

858

859
# Note:  This is created only for assignment of default projection types for each Port subclass (see .__init__.py)
860
#        Individual portRegistries (used for naming) are created for each Mechanism
861
PortRegistry = {}
1✔
862

863
class PortError(ComponentError):
1✔
864
    pass
1✔
865

866

867
# DOCUMENT:  INSTANTIATION CREATES AN ATTIRBUTE ON THE OWNER MECHANISM WITH THE PORT'S NAME + VALUE_SUFFIX
868
#            THAT IS UPDATED BY THE PORT'S value setter METHOD (USED BY LOGGING OF MECHANISM ENTRIES)
869
class Port_Base(Port):
1✔
870
    """
871
    Port_Base(     \
872
        owner=None \
873
        )
874

875
    Base class for Port.
876

877
    The arguments below can be used in the constructor for any subclass of Port.
878
    See `Component <Component_Class_Reference>` for additional arguments and attributes.
879

880
    .. note::
881
       Port is an abstract class and should *never* be instantiated by a call to its constructor. It should be created
882
       by calling the constructor for a `subclass <Port_Subtypes>`, or by using any of the other methods for `specifying
883
       a Port <Port_Specification>`.
884

885
    .. technical_note::
886

887
        PortRegistry
888
        -------------
889
            Used by .__init__.py to assign default Projection types to each Port subclass
890

891
            .. note::
892
              All Ports that belong to a given owner are registered in the owner's _portRegistry, which maintains a
893
              dict for each Port type that it uses, a count for all instances of that type, and a dictionary of those
894
              instances;  **none** of these are registered in the PortRegistry This. is so that the same name can be
895
              used for instances of a Port type by different owners without adding index suffixes for that name across
896
              owners, while still indexing multiple uses of the same base name within an owner
897

898
    Arguments
899
    ---------
900

901
    owner : Mechanism : default None
902
        the Mechanism to which the Port belongs;  if it is not specified or determinable from the context in which
903
        the Port is created, the Port's initialization is `deferred <Port_Deferred_Initialization>`.
904

905
    Attributes
906
    ----------
907

908
    variable : number, list or np.ndarray
909
        the Port's input, provided as the `variable <Port_Base.variable>` to its `function <Port_Base.function>`.
910

911
    owner : Mechanism or Projection
912
        object to which the Port belongs (see `Port_Strucure_Owner` for additional details).
913

914
    base_value : number, list or np.ndarray
915
        value with which the Port was initialized.
916

917
    all_afferents : Optional[List[Projection]]
918
        list of all Projections received by the Port (i.e., for which it is a `receiver <Projection_Base.receiver>`.
919

920
    path_afferents : Optional[List[Projection]]
921
        list of all `PathwayProjections <PathwayProjection>` received by the Port (i.e., for which it is the
922
        receiver <Projection_Base.receiver>` (note:  only `InputPorts <InputPort>` have path_afferents;  the list is
923
        empty for other types of Ports).
924

925
    mod_afferents : Optional[List[GatingProjection]]
926
        list of all `ModulatoryProjections <ModulatoryProjection>` received by the Port.
927

928
    projections : List[Projection]
929
        list of all of the `Projections <Projection>` sent or received by the Port.
930

931
    efferents : Optional[List[Projection]]
932
        list of outgoing Projections from the Port (i.e., for which is a `sender <Projection_Base.sender>`;
933
        note:  only `OutputPorts <OutputPort>`, and members of its `ModulatoryProjection <ModulatoryProjection>`
934
        subclass (`LearningProjection`, `ControlProjection` and `GatingProjection`) have efferents;  the list is empty for
935
        InputPorts and ParameterPorts.
936

937
    function : TransferFunction : default determined by type
938
        used to determine the Port's `value <Port_Base.value>` from the `value <Projection_Base.value>` of the
939
        `Projection(s) <Projection>` it receives;  the parameters that the TransferFunction identifies as *ADDITIVE*
940
        and *MULTIPLICATIVE* are subject to modulation by a `ModulatorySignal <ModulatorySignal>`.
941

942
    value : number, list or np.ndarray
943
        current value of the Port.
944

945
    name : str
946
        the name of the Port. If the Port's `initialization has been deferred <Port_Deferred_Initialization>`, it is
947
        assigned a temporary name (indicating its deferred initialization status) until initialization is completed,
948
        at which time it is assigned its designated name.  If that is the name of an existing Port, it is appended
949
        with an indexed suffix, incremented for each Port with the same base name (see `Registry_Naming`). If the name
950
        is not  specified in the **name** argument of its constructor, a default name is assigned by the subclass
951
        (see subclass for details).
952

953
        .. _Port_Naming_Note:
954

955
        .. note::
956
            Unlike other PsyNeuLink Components, Ports names are "scoped" within a Mechanism, meaning that Ports with
957
            the same name are permitted in different Mechanisms.  However, they are *not* permitted in the same
958
            Mechanism: Ports within a Mechanism with the same base name are appended an index in the order of their
959
            creation).
960

961
    full_name : str
962
        the name of the Port with its owner if that is assigned: <owner.name>[<self.name>] if owner is not None;
963
        otherwise same as `name <Port.name>`.
964

965
    prefs : PreferenceSet or specification dict
966
        the `PreferenceSet` for the Port; if it is not specified in the **prefs** argument of the constructor,
967
        a default is assigned using `classPreferences` defined in __init__.py (see `Preferences` for details).
968

969
    """
970

971
    componentCategory = PORT_COMPONENT_CATEGORY
1✔
972
    className = PORT
1✔
973
    suffix = " " + className
1✔
974
    paramsType = None
1✔
975

976
    class Parameters(Port.Parameters):
1✔
977
        """
978
            Attributes
979
            ----------
980

981
                function
982
                    see `function <Port_Base.function>`
983

984
                    :default value: `Linear`
985
                    :type: `Function`
986

987
                projections
988
                    see `projections <Port_Base.projections>`
989

990
                    :default value: None
991
                    :type:
992

993
                require_projection_in_composition
994
                    specifies whether the InputPort requires a projection when instantiated in a Composition;
995
                    if so, but none exists, a warning is issued.
996

997
                    :default value: True
998
                    :type: ``bool``
999
                    :read only: True
1000
        """
1001
        function = Parameter(Linear, stateful=False, loggable=False)
1✔
1002
        projections = Parameter(
1✔
1003
            None,
1004
            structural=True,
1005
            stateful=False,
1006
            loggable=False
1007
        )
1008
        require_projection_in_composition = Parameter(True, stateful=False, loggable=False, read_only=True, pnl_internal=True)
1✔
1009

1010
    portAttributes = {FUNCTION, FUNCTION_PARAMS, PROJECTIONS}
1✔
1011

1012
    registry = PortRegistry
1✔
1013

1014
    classPreferenceLevel = PreferenceLevel.CATEGORY
1✔
1015

1016
    @check_user_specified
1✔
1017
    @beartype
1✔
1018
    @abc.abstractmethod
1✔
1019
    def __init__(self,
1✔
1020
                 owner: Union[Mechanism, Projection],
1021
                 variable=None,
1022
                 input_shapes=None,
1023
                 projections=None,
1024
                 function=None,
1025
                 params=None,
1026
                 name=None,
1027
                 prefs=None,
1028
                 context=None,
1029
                 **kwargs):
1030
        """Initialize subclass that computes and represents the value of a particular Port of a Mechanism
1031

1032
        This is used by subclasses to implement the InputPort(s), OutputPort(s), and ParameterPort(s) of a Mechanism.
1033

1034
        COMMENT: [OLD]
1035
        Arguments:
1036
            - owner (Mechanism):
1037
                 Mechanism with which Port is associated (default: NotImplemented)
1038
                 this argument is required, as can't instantiate a Port without an owning Mechanism
1039
            - variable (value): value of the Port:
1040
                must be list or tuple of numbers, or a number (in which case it will be converted to a single-item list)
1041
                must match input and output of Port's _update method, and any sending or receiving projections
1042
            - input_shapes (int or array/list of ints):
1043
                Sets variable to be array(s) of zeros, if **variable** is not specified as an argument;
1044
                if **variable** is specified, it takes precedence over the specification of **input_shapes**.
1045
            - params (dict):
1046
                + if absent, implements default Port determined by PROJECTION_TYPE param
1047
                + if dict, can have the following entries:
1048
                    + PROJECTIONS:<Projection object, Projection class, dict, or list of either or both>
1049
                        if absent, no projections will be created
1050
                        if dict, must contain entries specifying a projection:
1051
                            + PROJECTION_TYPE:<Projection class> - must be a subclass of Projection
1052
                            + PROJECTION_PARAMS:<dict> - must be dict of params for PROJECTION_TYPE
1053
            - name (str): string with name of Port (default: name of owner + suffix + instanceIndex)
1054
            - prefs (dict): dictionary containing preferences (default: Prefs.DEFAULTS)
1055
            - context (str)
1056
            - **kwargs (dict): dictionary of arguments using the following keywords for each of the above kwargs:
1057
                # port_params is not handled here like the others are
1058
                + port_params = params
1059
                + Port_Name = name
1060
                + PORT_PREFS = prefs
1061
                + PORT_CONTEXT = context
1062
                NOTES:
1063
                    * these are used for dictionary specification of a Port in param declarations
1064
                    * they take precedence over arguments specified directly in the call to __init__()
1065
        COMMENT
1066
        """
1067
        if kwargs:
1✔
1068
            try:
1✔
1069
                name = kwargs[Port_Name]
1✔
1070
            except (KeyError, NameError):
1✔
1071
                pass
1✔
1072
            try:
1✔
1073
                prefs = kwargs[PORT_PREFS]
1✔
1074
            except (KeyError, NameError):
1✔
1075
                pass
1✔
1076
            try:
1✔
1077
                context = kwargs[PORT_CONTEXT]
1✔
1078
            except (KeyError, NameError):
1✔
1079
                pass
1✔
1080

1081
        # Enforce that subclass must implement and _execute method
1082
        if not hasattr(self, '_execute'):
1✔
1083
            raise PortError("{}, as a subclass of {}, must implement an _execute() method".
1084
                             format(self.__class__.__name__, PORT))
1085

1086
        self.owner = owner
1✔
1087

1088
        # If name is not specified, assign default name
1089
        if name is not None and DEFERRED_INITIALIZATION in name:
1!
1090
            name = self._assign_default_port_Name()
×
1091

1092
        # Register Port with PortRegistry of owner (Mechanism to which the Port is being assigned)
1093
        register_category(entry=self,
1✔
1094
                          base_class=Port_Base,
1095
                          name=name,
1096
                          registry=owner._portRegistry,
1097
                          # sub_group_attr='owner',
1098
                          )
1099

1100
        # VALIDATE VARIABLE, PARAM_SPECS, AND INSTANTIATE self.function
1101
        super(Port_Base, self).__init__(
1✔
1102
            default_variable=variable,
1103
            input_shapes=input_shapes,
1104
            function=function,
1105
            projections=projections,
1106
            param_defaults=params,
1107
            name=name,
1108
            prefs=prefs,
1109
            **kwargs
1110
        )
1111

1112
        # IMPLEMENTATION NOTE:  MOVE TO COMPOSITION ONCE THAT IS IMPLEMENTED
1113
        # INSTANTIATE PROJECTIONS SPECIFIED IN projections ARG OR params[PROJECTIONS:<>]
1114
        if self.projections is not None:
1✔
1115
            self._instantiate_projections(self.projections, context=context)
1✔
1116
        else:
1117
            # No projections specified, so none will be created here
1118
            # IMPLEMENTATION NOTE:  This is where a default projection would be implemented
1119
            #                       if params = NotImplemented or there is no param[PROJECTIONS]
1120
            pass
1121

1122
        self.projections = self._get_all_projections()
1✔
1123

1124
        if context.source == ContextFlags.COMMAND_LINE:
1✔
1125
            owner.add_ports([self])
1✔
1126

1127
    def _validate_variable(self, variable, context=None):
1✔
1128
        """Validate variable and return validated variable
1129

1130
        Sets self.base_value = self.value = variable
1131
        Insures that it is a number of list or tuple of numbers
1132

1133
        This overrides the class method, to perform more detailed type checking
1134
        See explanation in class method.
1135
        Note:  this method (or the class version) is called only if the parameter_validation attribute is True
1136
        """
1137

1138
        variable = super(Port, self)._validate_variable(variable, context)
1✔
1139

1140
        return variable
1✔
1141

1142
    def _validate_params(self, request_set, target_set=None, context=None):
1✔
1143
        """validate projection specification(s)
1144

1145
        Call super (Component._validate_params()
1146
        Validate following params:
1147
            + PROJECTIONS:  <entry or list of entries>; each entry must be one of the following:
1148
                + Projection object
1149
                + Projection class
1150
                + specification dict, with the following entries:
1151
                    + PROJECTION_TYPE:<Projection class> - must be a subclass of Projection
1152
                    + PROJECTION_PARAMS:<dict> - must be dict of params for PROJECTION_TYPE
1153
            # IMPLEMENTATION NOTE: TBI - When learning projection is implemented
1154
            # + FUNCTION_PARAMS:  <dict>, every entry of which must be one of the following:
1155
            #     ParameterPort, projection, 2-item tuple or value
1156
        """
1157
        # FIX: PROJECTION_REFACTOR
1158
        #      SHOULD ADD CHECK THAT PROJECTION_TYPE IS CONSISTENT WITH TYPE SPECIFIED BY THE
1159
        #      RECEIVER/SENDER SOCKET SPECIFICATIONS OF CORRESPONDING PROJECTION TYPES (FOR API)
1160

1161
        if PROJECTIONS in request_set and request_set[PROJECTIONS] is not None:
1✔
1162
            # if projection specification is an object or class reference, needs to be wrapped in a list
1163
            #    to be consistent with defaults and for consistency of treatment below
1164
            projections = request_set[PROJECTIONS]
1✔
1165
            if not isinstance(projections, list):
1✔
1166
                projections = [projections]
1✔
1167
                request_set[PROJECTIONS] = projections
1✔
1168
        else:
1169
            # If no projections, ignore (none will be created)
1170
            projections = None
1✔
1171

1172
        super(Port, self)._validate_params(request_set, target_set, context=context)
1✔
1173

1174
        if projections:
1✔
1175
            # Validate projection specs in list
1176
            for projection in projections:
1✔
1177
                try:
1✔
1178
                    issubclass(projection, Projection)
1✔
1179
                except TypeError:
1✔
1180
                    if (isinstance(projection, Projection) or iscompatible(projection, dict)):
1✔
1181
                        continue
1✔
1182
                    else:
1183
                        if self.prefs.verbosePref:
1!
1184
                            print("{0} in {1} is not a projection, projection type, or specification dict; "
×
1185
                                  "{2} will be used to create default {3} for {4}".
1186
                                format(projection,
1187
                                       self.__class__.__name__,
1188
                                       target_set[PROJECTION_TYPE],
1189
                                       self.owner.name))
1190

1191
    def _instantiate_function(self, function, function_params=None, context=None):
1✔
1192

1193
        var_is_matrix = False
1✔
1194
        # If variable is a 2d array or matrix (e.g., for the MATRIX ParameterPort of a MappingProjection),
1195
        #     it needs to be embedded in a list so that it is properly handled by LinearCombination
1196
        #     (i.e., solo matrix is returned intact, rather than treated as arrays to be combined);
1197
        # Notes:
1198
        #     * this is not a problem when LinearCombination is called in port._update(), since that puts
1199
        #         projection values in a list before calling LinearCombination to combine them
1200
        #     * it is removed from the list below, after calling _instantiate_function
1201
        # FIX: UPDATE WITH MODULATION_MODS REMOVE THE FOLLOWING COMMENT:
1202
        #     * no change is made to PARAMETER_MODULATION_FUNCTION here (matrices may be multiplied or added)
1203
        #         (that is handled by the individual Port subclasses (e.g., ADD is enforced for MATRIX ParameterPort)
1204
        if (
1!
1205
            (
1206
                (inspect.isclass(function) and issubclass(function, LinearCombination))
1207
                or isinstance(function, LinearCombination)
1208
            )
1209
            and isinstance(self.defaults.variable, np.matrix)
1210
        ):
1211
            self.defaults.variable = [self.defaults.variable]
×
1212
            var_is_matrix = True
×
1213

1214
        super()._instantiate_function(function=function, function_params=function_params, context=context)
1✔
1215

1216
        # If it is a matrix, remove from list in which it was embedded after instantiating and evaluating function
1217
        if var_is_matrix:
1!
1218
            self.defaults.variable = self.defaults.variable[0]
×
1219

1220
    # FIX: PROJECTION_REFACTOR
1221
    #      - MOVE THESE TO Projection, WITH self (Port) AS ADDED ARG
1222
    #          BOTH _instantiate_projections_to_port AND _instantiate_projections_from_port
1223
    #          CAN USE self AS connectee PORT, since _parse_connection_specs USES SOCKET TO RESOLVE
1224
    #      - ALTERNATIVE: BREAK PORT FIELD OF ProjectionTuple INTO sender AND receiver FIELDS, THEN COMBINE
1225
    #          _instantiate_projections_to_port AND _instantiate_projections_to_port INTO ONE METHOD
1226
    #          MAKING CORRESPONDING ASSIGNMENTS TO send AND receiver FIELDS (WOULD BE CLEARER)
1227

1228
    def _instantiate_projections(self, projections, context=None):
1✔
1229
        """Implement any Projection(s) to/from Port specified in PROJECTIONS entry of params arg
1230

1231
        Must be implemented by subclasss, to handle interpretation of projection specification(s)
1232
        in a class-appropriate manner:
1233
            PathwayProjections:
1234
              InputPort: _instantiate_projections_to_port (.path_afferents)
1235
              ParameterPort: disallowed
1236
              OutputPort: _instantiate_projections_from_port (.efferents)
1237
              ModulatorySignal: disallowed
1238
            ModulatoryProjections:
1239
              InputPort, OutputPort and ParameterPort:  _instantiate_projections_to_port (mod_afferents)
1240
              ModulatorySignal: _instantiate_projections_from_port (.efferents)
1241
        """
1242

1243
        raise PortError("f{self.__class__.__name__} must implement _instantiate_projections (called for {self.name}).")
1244

1245
    # FIX: MOVE TO InputPort AND ParameterPort OR...
1246
    # IMPLEMENTATION NOTE:  MOVE TO COMPOSITION ONCE THAT IS IMPLEMENTED
1247
    def _instantiate_projections_to_port(self, projections, context=None):
1✔
1248
        """Instantiate projections to a Port and assign them to self.path_afferents
1249

1250
        Parses specifications in projection_list into ProjectionTuples
1251

1252
        For projection_spec in ProjectionTuple:
1253
            - if it is a Projection specifiction dicionatry, instantiate it
1254
            - assign self as receiver
1255
            - assign sender
1256
            - if deferred_init and sender is instantiated, complete initialization
1257
            - assign to path_afferents or mod_afferents
1258
            - if specs fail, instantiates a default Projection of type specified by self.projection_type
1259

1260
        Notes:
1261
            Calls _parse_connection_specs() to parse projection_list into a list of ProjectionTuples;
1262
                 _parse_connection_specs, in turn, calls _parse_projection_spec for each spec in projection_list,
1263
                 which returns either a Projection object or Projection specification dictionary for each spec;
1264
                 that is placed in projection_spec entry of ProjectionTuple (Port, weight, exponent, projection_spec).
1265
            When the Projection is instantiated, it assigns itself to
1266
               its receiver's .path_afferents attribute (in Projection_Base._instantiate_receiver) and
1267
               its sender's .efferents attribute (in Projection_Base._instantiate_sender);
1268
               so, need to test for prior assignment to avoid duplicates.
1269
        """
1270

1271
        from psyneulink.core.components.projections.pathway.pathwayprojection import PathwayProjection_Base
1✔
1272
        from psyneulink.core.components.projections.modulatory.modulatoryprojection import ModulatoryProjection_Base
1✔
1273
        from psyneulink.core.components.projections.projection import _parse_connection_specs
1✔
1274

1275

1276
        default_projection_type = self.projection_type
1✔
1277

1278
        # If specification is not a list, wrap it in one for consistency of treatment below
1279
        # (since specification can be a list, so easier to treat any as a list)
1280
        projection_list = projections
1✔
1281
        if not isinstance(projection_list, list):
1✔
1282
            projection_list = [projection_list]
1✔
1283

1284
        # return a list of the newly created projections
1285
        new_projections = []
1✔
1286

1287
        # Parse each Projection specification in projection_list using self as connectee_port:
1288
        # - calls _parse_projection_spec for each projection_spec in list
1289
        # - validates that Projection specification is compatible with its sender and self
1290
        # - returns ProjectionTuple with Projection specification dictionary for projection_spec
1291
        projection_tuples = _parse_connection_specs(self.__class__, self.owner, projection_list)
1✔
1292

1293
        # For Projection in each ProjectionTuple:
1294
        # - instantiate the Projection if necessary, and initialize if possible
1295
        # - insure its value is compatible with self.value FIX: ??and variable is compatible with sender's value
1296
        # - assign it to self.path_afferents or .mod_afferents
1297
        for connection in projection_tuples:
1✔
1298

1299
            # Get sender Port, weight, exponent and projection for each projection specification
1300
            #    note: weight and exponent for connection have been assigned to Projection in _parse_connection_specs
1301
            port, weight, exponent, projection_spec = connection
1✔
1302

1303
            # GET Projection --------------------------------------------------------
1304

1305
            # Projection object
1306
            if isinstance(projection_spec, Projection):
1✔
1307
                projection = projection_spec
1✔
1308
                projection_type = projection.__class__
1✔
1309

1310

1311
            # Projection specification dictionary:
1312
            elif isinstance(projection_spec, dict):
1✔
1313
                # Instantiate Projection
1314
                projection_spec[WEIGHT]=weight
1✔
1315
                projection_spec[EXPONENT]=exponent
1✔
1316
                projection_spec[FEEDBACK]
1✔
1317
                projection_type = projection_spec.pop(PROJECTION_TYPE, None) or default_projection_type
1✔
1318
                projection = projection_type(**projection_spec)
1✔
1319

1320

1321
            else:
1322
                raise PortError(
1323
                    f"PROGRAM ERROR: Unrecognized {Projection.__name__} specification ({projection_spec}) "
1324
                    f"returned from _parse_connection_specs for connection to {self.name} of {self.owner.name}.")
1325

1326
            # ASSIGN PARAMS
1327

1328
            # Deferred init
1329
            if projection.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
1330

1331
                proj_sender = projection._init_args[SENDER]
1✔
1332
                proj_receiver = projection._init_args[RECEIVER]
1✔
1333

1334
                # validate receiver
1335
                if proj_receiver is not None and proj_receiver != self:
1✔
1336
                    raise PortError(f"Projection ({projection_type.__name__}) "
1337
                                    f"assigned to '{self.name}' of '{self.owner.name}' "
1338
                                    f"already has a receiver ('{proj_receiver.owner.name}[{proj_receiver.name}]').")
1339
                projection._init_args[RECEIVER] = self
1✔
1340

1341
                # parse/validate sender
1342
                if proj_sender:
1✔
1343
                    # If the Projection already has Port as its sender,
1344
                    #    it must be the same as the one specified in the connection spec
1345
                    if isinstance(proj_sender, Port):
1✔
1346
                        if proj_sender == port:
1✔
1347
                            sender = port
1✔
1348
                        else:
1349
                            raise PortError(
1350
                                f"Projection assigned to '{self.name}' of '{self.owner.name}' from {port.name} "
1351
                                f"already has a sender ('{proj_sender.owner.name}[{proj_sender.name}]').")
1352
                    # If the Projection has a Mechanism specified as its sender:
1353
                    elif isinstance(port, Port):
1!
1354
                        #    Connection spec (port) is specified as a Port, so validate that
1355
                        #       Port belongs to proj_sender Mechanism and is of the correct type
1356
                        sender = _get_port_for_socket(owner=self.owner,
1✔
1357
                                                       mech=proj_sender,
1358
                                                       port_spec=port,
1359
                                                       port_types=port.__class__,
1360
                                                       projection_socket=SENDER)
1361
                    elif isinstance(proj_sender, Mechanism) and inspect.isclass(port) and issubclass(port, Port):
×
1362
                        #    Connection spec (port) is specified as Port type
1363
                        #    so try to get that Port type for the Mechanism
1364
                        sender = _get_port_for_socket(owner=self.owner,
×
1365
                                                       connectee_port_type=self.__class__,
1366
                                                       port_spec=proj_sender,
1367
                                                       port_types=port)
1368
                    else:
1369
                        sender = proj_sender
×
1370
                else:
1371
                    sender = port
1✔
1372
                projection._init_args[SENDER] = sender
1✔
1373

1374
                projection.sender = sender
1✔
1375
                projection.receiver = projection._init_args[RECEIVER]
1✔
1376
                projection.receiver.afferents_info[projection] = ConnectionInfo()
1✔
1377

1378
                # Construct and assign name
1379
                if isinstance(sender, Port):
1✔
1380
                    if sender.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
1381
                        sender_name = sender._init_args[NAME]
1✔
1382
                    else:
1383
                        sender_name = sender.name
1✔
1384
                    sender_name = sender_name or sender.__class__.__name__
1✔
1385
                elif inspect.isclass(sender) and issubclass(sender, Port):
1✔
1386
                    sender_name = sender.__name__
1✔
1387
                else:
1388
                    raise PortError(f"SENDER of {projection_type.__name__} to {self.name} of {self.owner.name} "
1389
                                    f"is neither a Port or Port class.")
1390
                projection._assign_default_projection_name(port=self,
1✔
1391
                                                           sender_name=sender_name,
1392
                                                           receiver_name=self.name)
1393

1394
                # If sender has been instantiated, try to complete initialization
1395
                # If not, assume it will be handled later (by Mechanism or Composition)
1396
                if isinstance(sender, Port) and sender.initialization_status == ContextFlags.INITIALIZED:
1✔
1397
                    projection._deferred_init(context=context)
1✔
1398

1399

1400
            # VALIDATE (if initialized)
1401

1402
            if projection.initialization_status == ContextFlags.INITIALIZED:
1✔
1403

1404
                # FIX: 10/3/17 - VERIFY THE FOLLOWING:
1405
                # IMPLEMENTATION NOTE:
1406
                #     Assume that validation of Projection's variable (i.e., compatibility with sender)
1407
                #         has already been handled by instantiation of the Projection and/or its sender
1408

1409
                # Validate value:
1410
                #    - check that output of projection's function (projection_spec.value) is compatible with
1411
                #        self.variable;  if it is not, raise exception:
1412
                #        the buck stops here; can't modify projection's function to accommodate the Port,
1413
                #        or there would be an unmanageable regress of reassigning projections,
1414
                #        requiring reassignment or modification of sender OutputPorts, etc.
1415

1416
                # PathwayProjection:
1417
                #    - check that projection's value is compatible with the Port's variable
1418
                if isinstance(projection, PathwayProjection_Base):
1✔
1419
                    if not iscompatible(projection.defaults.value, self.defaults.variable[0]):
1✔
1420
                    # if len(projection.value) != self.defaults.variable.shape[-1]:
1421
                        raise PortError("Output of function for {} ({}) is not compatible with value of {} ({}).".
1422
                                         format(projection.name, projection.value, self.name, self.defaults.value))
1423

1424
                # ModualatoryProjection:
1425
                #    - check that projection's value is compatible with value of the function param being modulated
1426
                elif isinstance(projection, ModulatoryProjection_Base):
1!
1427
                    mod_spec, mod_param_name, mod_param_value = self._get_modulated_param(projection, context=context)
1✔
1428
                    # Match the projection's value with the value of the function parameter
1429
                    mod_proj_spec_value = match_modulation_to_value(projection.defaults.value, mod_param_value)
1✔
1430
                    if (mod_param_value is not None
1✔
1431
                        and not iscompatible(mod_param_value, mod_proj_spec_value)):
1432
                        raise PortError(f"Output of function for {projection.name} ({projection.defaults.value}) "
1433
                                        f"is not compatible with value of {self.name} ({self.defaults.value}).")
1434

1435
            # ASSIGN TO PORT
1436

1437
            # Avoid duplicates, since instantiation of projection may have already called this method
1438
            #    and assigned Projection to self.path_afferents or mod_afferents lists
1439
            if self._check_for_duplicate_projections(projection):
1!
1440
                continue
×
1441

1442
            # reassign default variable shape to this port and its function
1443
            if isinstance(projection, PathwayProjection_Base) and projection not in self.path_afferents:
1✔
1444
                projs = self.path_afferents
1✔
1445
                variable = self.defaults.variable
1✔
1446
                projs.append(projection)
1✔
1447
                new_projections.append(projection)
1✔
1448
                if len(projs) > 1:
1✔
1449
                    # KDM 5/16/18: Why are we casting this to 2d? I expect this to make the InputPort variable
1450
                    # 2d, so its owner's 3d, but that does not appear to be happening.
1451
                    # Removing this cast can cause an AutoAssignMatrix to interpret the entire InputPort's variable
1452
                    # as its target - ex: two incoming projections -> [0, 0]; third sees socket_width of len 2, so
1453
                    # creates a projection with value length 2, so variable becomes [0, 0, 0, 0]
1454
                    if variable.ndim == 1:
1✔
1455
                        variable = np.atleast_2d(variable)
1✔
1456
                    self.defaults.variable = np.append(variable, np.atleast_2d(projection.defaults.value), axis=0)
1✔
1457

1458
                # assign identical default variable to function if it can be modified
1459
                if self.function._variable_shape_flexibility is DefaultsFlexibility.FLEXIBLE:
1✔
1460
                    self.function.defaults.variable = copy_parameter_value(self.defaults.variable)
1✔
1461
                elif (
1!
1462
                    self.function._variable_shape_flexibility is DefaultsFlexibility.INCREASE_DIMENSION
1463
                    and np.array([self.function.defaults.variable]).shape == self.defaults.variable.shape
1464
                ):
1465
                    self.function.defaults.variable = np.array([self.defaults.variable])
×
1466
                elif self.function.defaults.variable.shape != self.defaults.variable.shape:
1!
1467
                    from psyneulink.core.compositions.composition import Composition
×
1468
                    warnings.warn('A {} from {} is being added to an {} of {} ({}) that already receives other '
×
1469
                                  'Projections, but does not use a {}; unexpected results may occur when the {} '
1470
                                  'or {} to which it belongs is executed.'.
1471
                                  format(Projection.__name__, projection.sender.owner.name, self.__class__.__name__,
1472
                                         self.owner.name, self.name, TransformFunction.__name__, Mechanism.__name__,
1473
                                         Composition.__name__))
1474
                            # f'A {Projection.__name__} from {projection.sender.owner.name} is being added ' \
1475
                            #     f'to an {self.__class__.__name__} of {self.owner.name} ({self.name}) ' \
1476
                            #     f'that already receives other Projections, ' \
1477
                            #     f'but does not use a {TransformFunction.__name__}; ' \
1478
                            #     f'unexpected results may occur when the {Mechanism.__name__} ' \
1479
                            #     f'or {Composition.__name__} to which it belongs is executed.')
1480

1481
            elif isinstance(projection, ModulatoryProjection_Base) and projection not in self.mod_afferents:
1✔
1482
                self.mod_afferents.append(projection)
1✔
1483
                new_projections.append(projection)
1✔
1484

1485
            self.owner._projection_added(projection, context)
1✔
1486

1487

1488
        return new_projections
1✔
1489

1490
    # FIX: MOVE TO OutputPort or...
1491
    # IMPLEMENTATION NOTE:  MOVE TO COMPOSITION ONCE THAT IS IMPLEMENTED
1492
    def _instantiate_projection_from_port(self, projection_spec, receiver=None, feedback=False, context=None):
1✔
1493
        """Instantiate outgoing projection from a Port and assign it to self.efferents
1494

1495
        Instantiate Projections specified in projection_list and, for each:
1496
            - insure its self.value is compatible with the projection's function variable
1497
            - place it in self.efferents:
1498

1499
        Notes:
1500
            # LIST VERSION:
1501
            # If receivers is not specified, they must be assigned to projection specs in projection_list
1502
            # Calls _parse_connection_specs() to parse projection_list into a list of ProjectionTuples;
1503
            #    _parse_connection_specs, in turn, calls _parse_projection_spec for each spec in projection_list,
1504
            #    which returns either a Projection object or Projection specification dictionary for each spec;
1505
            #    that is placed in projection_spec entry of ProjectionTuple (Port, weight, exponent, projection_spec).
1506

1507
            Calls _parse_connection_specs() to parse projection into a ProjectionTuple;
1508
               _parse_connection_specs, in turn, calls _parse_projection_spec for the projection_spec,
1509
               which returns either a Projection object or Projection specification dictionary for the spec;
1510
               that is placed in projection_spec entry of ProjectionTuple (Port, weight, exponent, projection_spec).
1511

1512
            When the Projection is instantiated, it assigns itself to
1513
               its self.path_afferents or .mod_afferents attribute (in Projection_Base._instantiate_receiver) and
1514
               its sender's .efferents attribute (in Projection_Base._instantiate_sender);
1515
               so, need to test for prior assignment to avoid duplicates.
1516

1517
        Returns instantiated Projection
1518
        """
1519
        from psyneulink.core.components.projections.modulatory.modulatoryprojection import ModulatoryProjection_Base
1✔
1520
        from psyneulink.core.components.projections.pathway.pathwayprojection import PathwayProjection_Base
1✔
1521
        from psyneulink.core.components.projections.pathway.mappingprojection import MappingProjection
1✔
1522
        from psyneulink.core.components.projections.modulatory.gatingprojection import GatingProjection
1✔
1523
        from psyneulink.core.components.projections.projection import ProjectionTuple, _parse_connection_specs
1✔
1524

1525
        # FIX: 10/3/17 THIS NEEDS TO BE MADE SPECIFIC TO EFFERENT PROJECTIONS (I.E., FOR WHICH IT CAN BE A SENDER)
1526
        # default_projection_type = ProjectionRegistry[self.projection_type].subclass
1527
        default_projection_type = self.projection_type
1✔
1528

1529
        # projection_object = None # flags whether projection object has been instantiated; doesn't store object
1530
        # projection_type = None   # stores type of projection to instantiate
1531
        # projection_params = {}
1532

1533

1534
        # IMPLEMENTATION NOTE:  THE FOLLOWING IS WRITTEN AS A LOOP IN PREP FOR GENERALINZING METHOD
1535
        #                       TO HANDLE PROJECTION LIST (AS PER _instantiate_projections_to_port())
1536

1537
        # If projection_spec and/or receiver is not in a list, wrap it in one for consistency of treatment below
1538
        # (since specification can be a list, so easier to treat any as a list)
1539
        projection_list = projection_spec
1✔
1540
        if not isinstance(projection_list, list):
1!
1541
            projection_list = [projection_list]
1✔
1542

1543
        if not receiver:
1!
1544
            receiver_list = [None] * len(projection_list)
×
1545
        elif not isinstance(receiver, list):
1!
1546
            receiver_list = [receiver]
1✔
1547

1548
        # Parse Projection specification using self as connectee_port:
1549
        # - calls _parse_projection_spec for projection_spec;
1550
        # - validates that Projection specification is compatible with its receiver and self
1551
        # - returns ProjectionTuple with Projection specification dictionary for projection_spec
1552
        projection_tuples = _parse_connection_specs(self.__class__, self.owner, receiver_list)
1✔
1553

1554
        # For Projection in ProjectionTuple:
1555
        # - instantiate the Projection if necessary, and initialize if possible
1556
        # - insure its variable is compatible with self.value and its value is compatible with receiver's variable
1557
        # - assign it to self.path_efferents
1558

1559
        for connection, receiver in zip(projection_tuples, receiver_list):
1!
1560

1561
            # VALIDATE CONNECTION AND RECEIVER SPECS
1562

1563
            # Validate that Port to be connected to specified in receiver is same as any one specified in connection
1564
            def _get_receiver_port(spec):
1✔
1565
                """Get port specification from ProjectionTuple, which itself may be a ProjectionTuple"""
1566
                if isinstance(spec, (tuple, ProjectionTuple)):
1✔
1567
                    spec = _parse_connection_specs(connectee_port_type=self.__class__,
1✔
1568
                                                   owner=self.owner,
1569
                                                   connections=receiver)
1570
                    return _get_receiver_port(spec[0].port)
1✔
1571
                elif isinstance(spec, Projection):
1✔
1572
                    try:
1✔
1573
                        return spec.receiver
1✔
NEW
1574
                    except AttributeError:
×
NEW
1575
                        return spec._init_args[RECEIVER]
×
NEW
1576
                    except:
×
1577
                        raise PortError(f"Unrecognized specification of receiver for Projection "
1578
                                        f"from '{self.name}' of '{self.owner.name}'.")
1579
                # FIX: 11/25/17 -- NEEDS TO CHECK WHETHER PRIMARY SHOULD BE INPUT_PORT OR PARAMETER_PORT
1580
                elif isinstance(spec, Mechanism):
1✔
1581
                    return spec.input_port
1✔
1582
                return spec
1✔
1583
            receiver_port = _get_receiver_port(receiver)
1✔
1584
            connection_receiver_port = _get_receiver_port(connection)
1✔
1585
            if receiver_port != connection_receiver_port:
1✔
1586
                raise PortError("PROGRAM ERROR: Port specified as receiver ({}) should "
1587
                                 "be the same as the one specified in the connection {}.".
1588
                                 format(receiver_port, connection_receiver_port))
1589

1590
            if (not isinstance(connection, ProjectionTuple)
1✔
1591
                and receiver
1592
                and not isinstance(receiver, (Port, Mechanism))
1593
                and not (inspect.isclass(receiver) and issubclass(receiver, (Port, Mechanism)))):
1594
                raise PortError("Receiver ({}) of {} from {} must be a {}, {}, a class of one, or a {}".
1595
                                 format(receiver, projection_spec, self.name,
1596
                                        Port.__name__, Mechanism.__name__, ProjectionTuple.__name__))
1597

1598
            if isinstance(receiver, Mechanism):
1✔
1599
                from psyneulink.core.components.ports.inputport import InputPort
1✔
1600
                from psyneulink.core.components.ports.parameterport import ParameterPort
1✔
1601

1602
                # If receiver is a Mechanism and Projection is a MappingProjection,
1603
                #    use primary InputPort (and warn if verbose is set)
1604
                if isinstance(default_projection_type, (MappingProjection, GatingProjection)):
1!
1605
                    if self.owner.verbosePref:
×
1606
                        warnings.warn("Receiver {} of {} from {} is a {} and {} is a {}, "
×
1607
                                      "so its primary {} will be used".
1608
                                      format(receiver, projection_spec, self.name, Mechanism.__name__,
1609
                                             Projection.__name__, default_projection_type.__name__,
1610
                                             InputPort.__name__))
1611
                    receiver = receiver.input_port
×
1612

1613
                    raise PortError("Receiver {} of {} from {} is a {}, but the specified {} is a {} so "
1614
                                     "target {} can't be determined".
1615
                                     format(receiver, projection_spec, self.name, Mechanism.__name__,
1616
                                            Projection.__name__, default_projection_type.__name__,
1617
                                            ParameterPort.__name__))
1618

1619

1620
            # GET Projection --------------------------------------------------------
1621

1622
            # Get sender Port, weight, exponent and projection for each projection specification
1623
            #    note: weight and exponent for connection have been assigned to Projection in _parse_connection_specs
1624
            connection_receiver, weight, exponent, projection_spec = connection
1✔
1625

1626
            # Parse projection_spec and receiver specifications
1627
            #    - if one is assigned and the other is not, assign the one to the other
1628
            #    - if both are assigned, validate they are the same
1629
            #    - if projection_spec is None and receiver is specified, use the latter to construct default Projection
1630

1631
            # Projection object
1632
            if isinstance(projection_spec, Projection):
1✔
1633
                projection = projection_spec
1✔
1634
                projection_type = projection.__class__
1✔
1635

1636
                if projection.initialization_status == ContextFlags.DEFERRED_INIT:
1!
1637
                    projection._init_args[RECEIVER] = projection._init_args[RECEIVER] or receiver
1✔
1638
                    proj_recvr = projection._init_args[RECEIVER]
1✔
1639
                else:
1640
                    projection.receiver = projection.receiver or receiver
×
1641
                    proj_recvr = projection.receiver
×
1642
                projection._assign_default_projection_name(port=self,
1✔
1643
                                                           sender_name=self.name,
1644
                                                           receiver_name=proj_recvr.name)
1645

1646
            # Projection specification dictionary or None:
1647
            elif isinstance(projection_spec, (dict, None)):
1!
1648

1649
                # Instantiate Projection from specification dict
1650
                projection_type = projection_spec.pop(PROJECTION_TYPE, None) or default_projection_type
1✔
1651
                # If Projection was not specified, create default Projection specification dict
1652
                if not (projection_spec or len(projection_spec)):
1!
1653
                    projection_spec = {SENDER: self, RECEIVER: receiver_port}
1✔
1654
                projection = projection_type(**projection_spec)
1✔
1655
                try:
1✔
1656
                    projection.receiver = projection.receiver
1✔
1657
                except AttributeError:
×
1658
                    projection.receiver = receiver
×
1659
                proj_recvr = projection.receiver
1✔
1660

1661
            else:
1662
                rcvr_str = ""
×
1663
                if receiver:
×
1664
                    if isinstance(receiver, Port):
×
NEW
1665
                        rcvr_str = f" to {receiver.name}"
×
1666
                    else:
NEW
1667
                        rcvr_str = f" to {receiver.__name__}"
×
1668
                raise PortError(f"PROGRAM ERROR: Unrecognized {Projection.__name__} specification ({projection_spec}) "
1669
                                f"returned from _parse_connection_specs for connection from {self.name} of "
1670
                                f"{self.owner.name}{rcvr_str}.")
1671

1672
            # Validate that receiver and projection_spec receiver are now the same
1673
            receiver = proj_recvr or receiver  # If receiver was not specified, assign it receiver from projection_spec
1✔
1674
            if proj_recvr and receiver and proj_recvr is not receiver:
1✔
1675
                # Note: if proj_recvr is None, it will be assigned under handling of deferred_init below
1676
                raise PortError(f"Receiver ({proj_recvr}) specified for Projection ({projection.name}) "
1677
                                f"is not the same as the one specified in {ProjectionTuple.__name__} ({receiver}).")
1678

1679
            # ASSIGN REMAINING PARAMS
1680

1681
            # Deferred init
1682
            if projection.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
1683
                projection._init_args[SENDER] = self
1✔
1684
                if isinstance(receiver, Port) and receiver.initialization_status == ContextFlags.INITIALIZED:
1!
1685
                    projection._deferred_init(context=context)
1✔
1686

1687
            # VALIDATE (if initialized or being initialized)
1688

1689
            if projection.initialization_status & (ContextFlags.INITIALIZED | ContextFlags.INITIALIZING):
1!
1690

1691
                # If still being initialized, then assign sender and receiver as necessary
1692
                if projection.initialization_status == ContextFlags.INITIALIZING:
1!
1693
                    if not isinstance(projection.sender, Port):
×
1694
                        projection.sender = self
×
1695

1696
                    if not isinstance(projection.receiver, Port):
×
1697
                        projection.receiver = receiver_port
×
1698

1699
                    projection._assign_default_projection_name(
×
1700
                        port=self,
1701
                        sender_name=self.name,
1702
                        receiver_name=projection.receiver.name
1703
                    )
1704

1705
                # when this is called during initialization, doesn't make sense to validate here
1706
                # because the projection values are set later to the values they're being validated against here
1707
                else:
1708
                    # Validate variable
1709
                    #    - check that input to Projection is compatible with self.value
1710
                    if not iscompatible(self.defaults.value, projection.defaults.variable):
1✔
1711
                        raise PortError(f"Input to {projection.name} ({projection.defaults.variable}) "
1712
                                        f"is not compatible with the value ({self.defaults.value}) of "
1713
                                        f"the Port from which it is supposed to project ({self.name}).")
1714

1715
                    # Validate value:
1716
                    #    - check that output of projection's function (projection_spec.value) is compatible with
1717
                    #        variable of the Port to which it projects;  if it is not, raise exception:
1718
                    #        the buck stops here; can't modify projection's function to accommodate the Port,
1719
                    #        or there would be an unmanageable regress of reassigning projections,
1720
                    #        requiring reassignment or modification of sender OutputPorts, etc.
1721

1722
                    # PathwayProjection:
1723
                    #    - check that projection's value is compatible with the receiver's variable
1724
                    if isinstance(projection, PathwayProjection_Base):
1✔
1725
                        if not iscompatible(projection.value, receiver.socket_template):
1✔
1726
                            raise PortError(f"Output of {projection.name} ({projection.value}) "
1727
                                            f"is not compatible with the variable ({receiver.defaults.variable}) of "
1728
                                            f"the Port to which it is supposed to project ({receiver.name}).")
1729

1730
                    # ModualatoryProjection:
1731
                    #    - check that projection's value is compatible with value of the function param being modulated
1732
                    elif isinstance(projection, ModulatoryProjection_Base):
1!
1733
                        mod_spec, mod_param_name, mod_param_value = self._get_modulated_param(projection,
1✔
1734
                                                                                              receiver=receiver,
1735
                                                                                              context=context)
1736
                        # Match the projection's value with the value of the function parameter
1737
                        # should be defaults.value?
1738
                        try:
1✔
1739
                            mod_proj_spec_value = match_modulation_to_value(projection.value, mod_param_value)
1✔
1740
                        except TypeError as error:
×
1741
                            raise PortError(f"The value for {self.name} of {self.owner.name} ({projection.value}) does "
1742
                                            f"not match the format ({mod_param_value}) of the Parameter it modulates "
1743
                                            f"({receiver.owner.name}[{mod_param_name}]).")
1744
                        if (mod_param_value is not None
1✔
1745
                            and not iscompatible(mod_param_value, mod_proj_spec_value)):
1746
                            raise PortError(f"Output of {projection.name} ({mod_proj_spec_value}) is not compatible "
1747
                                            f"with the value of {receiver.name} ({mod_param_value}).")
1748

1749
            # ASSIGN TO PORT
1750

1751
            # Avoid duplicates, since instantiation of projection may have already called this method
1752
            #    and assigned Projection to self.efferents
1753
            if self._check_for_duplicate_projections(projection):
1!
1754
                continue
×
1755

1756
            # FIX: MODIFIED FEEDBACK - CHECK THAT THAT THIS IS STILL NEEDED (RE: ASSIGNMENT IN ModulatorySignal)
1757
            # FIX: 9/14/19 - NOTE:  IT *IS* NEEDED FOR CONTROLPROJECTIONS
1758
            #                       SPECIFIED FOR PARAMETER IN CONSTRUCTOR OF A MECHANISM
1759
            if isinstance(projection, ModulatoryProjection_Base):
1✔
1760
                self.owner.aux_components.append((projection, feedback))
1✔
1761
            return projection
1✔
1762

1763
    def remove_projection(self, projection, context=None):
1✔
1764
        if projection in self.afferents_info:
1✔
1765
            del self.afferents_info[projection]
1✔
1766
        if projection in self.projections:
1✔
1767
            self.projections.remove(projection)
1✔
1768
        try:
1✔
1769
            if projection in self.mod_afferents or projection in self.path_afferents:
1✔
1770
                self._remove_projection_to_port(projection, context=context)
1✔
1771
        except PortError:
1✔
1772
            pass
1✔
1773
        try:
1✔
1774
            if projection in self.efferents:
1✔
1775
                self._remove_projection_from_port(projection, context=context)
1✔
1776
        except PortError:
1✔
1777
            pass
1✔
1778

1779
    def _remove_projection_from_port(self, projection, context=None):
1✔
1780
        """Remove Projection entry from Port.efferents."""
1781
        del self.efferents[self.efferents.index(projection)]
1✔
1782

1783
    def _remove_projection_to_port(self, projection, context=None):
1✔
1784
        """
1785
        If projection is in mod_afferents, remove that projection from self.mod_afferents.
1786
        Else, Remove Projection entry from Port.path_afferents and reshape variable accordingly.
1787
        """
1788
        if projection in self.mod_afferents:
1✔
1789
            del self.mod_afferents[self.mod_afferents.index(projection)]
1✔
1790
        else:
1791
            # Do this first so that if it fails (i.e., miscalled for OutputPort)
1792
            #    no changes are made to the Port's or its function's variable
1793
            del self.path_afferents[self.path_afferents.index(projection)]
1✔
1794
            shape = list(self.defaults.variable.shape)
1✔
1795
            # Reduce outer dimension by one
1796
            # only if shape is already greater than 1 (ports keep
1797
            # default of [0] if no incoming projections)
1798
            shape[0] -= 1
1✔
1799
            if shape[0] > 0:
1✔
1800
                self.defaults.variable = np.resize(self.defaults.variable, shape)
1✔
1801
                self.function.defaults.variable = np.resize(self.function.defaults.variable, shape)
1✔
1802

1803
    def _get_primary_port(self, mechanism):
1✔
1804
        raise PortError("PROGRAM ERROR: {} does not implement _get_primary_port method".
1805
                         format(self.__class__.__name__))
1806

1807
    def _get_all_projections(self):
1✔
1808
        assert False, f"Subclass of Port ({self.__class__.__name__}) must implement '_get_all_projections()' method."
1809

1810
    def _get_all_afferents(self):
1✔
1811
        assert False, f"Subclass of Port ({self.__class__.__name__}) must implement '_get_all_afferents()' method."
1812

1813
    def _parse_port_specific_specs(self, owner, port_dict, port_specific_spec, context=None):
1✔
1814
        """Parse parameters in Port specification tuple specific to each subclass
1815

1816
        Called by _parse_port_spec()
1817
        port_dict contains standard args for Port constructor passed to _parse_port_spec
1818
        port_specific_spec is either a:
1819
            - tuple containing a specification for the Port and/or Projections to/from it
1820
            - a dict containing port-specific parameters to be processed
1821

1822
         Returns two values:
1823
         - port_spec:  specification for the Port;
1824
                          - can be None (this is usually the case when port_specific_spec
1825
                            is a tuple specifying a Projection that will be used to specify the port)
1826
                          - if a value is returned, that is used by _parse_port_spec in a recursive call to
1827
                            parse the specified value as the Port specification
1828
         - params: port-specific parameters that will be included in the PARAMS entry of the Port specification dict
1829
         """
1830
        raise PortError("PROGRAM ERROR: {} does not implement _parse_port_specific_specs method".
1831
                         format(self.__class__.__name__))
1832

1833
    def _update(self, params=None, context=None):
1✔
1834
        """Update each projection, combine them, and assign return result
1835

1836
        Assign any runtime_params specified for Port, its function, and any of its afferent projections;
1837
          - assumes that type-specific sub-dicts have been created for each Projection type for which there are params
1838
          - and that specifications for individual Projections have been put in their own PROJECTION-SPECIFIC sub-dict
1839
          - specifications for individual Projections are removed from that as used (for check by Mechanism at end)
1840
        Call _update for each projection in self.path_afferents (passing specified params)
1841
        Note: only update LearningSignals if context == LEARNING; otherwise, just get their value
1842
        Call self.function (default: LinearCombination function) to combine their values
1843
        Returns combined values of projections, modulated by any mod_afferents
1844
        """
1845
        from psyneulink.core.components.projections.projection import \
1✔
1846
            projection_param_keywords, projection_param_keyword_mapping
1847

1848
        # Skip execution and set value directly if function is identity_function and no runtime_params were passed
1849
        if (
1✔
1850
            len(self.all_afferents) == 0
1851
            and self.function._is_identity(context)
1852
            and not params
1853
        ):
1854
            variable = self._parse_function_variable(self._get_variable_from_projections(context))
1✔
1855
            self.parameters.variable._set(variable, context)
1✔
1856
            # FIX: below conversion really should not be happening ultimately, but it is
1857
            # in _validate_variable. Should be removed eventually
1858
            variable = convert_to_np_array(variable, 1)
1✔
1859
            self.parameters.value._set(variable, context)
1✔
1860
            self.most_recent_context = context
1✔
1861
            self.function.most_recent_context = context
1✔
1862
            return
1✔
1863

1864
        # GET RUNTIME PARAMS FOR PORT AND ITS PROJECTIONS ---------------------------------------------------------
1865

1866
        # params (ones passed from Mechanism that should be kept intact for other Ports):
1867
        #  - remove any found in PORT_SPECIFIC_PARAMS specific to this port
1868
        #      (Mechanism checks for errant ones after all ports have been executed)
1869
        #  - remove any found in PROJECTION_SPECIFIC for Projections to this port
1870
        #      (Mechanism checks for errant ones after all ports have been executed)
1871

1872
        # local_params (ones that will be passed to Port's execute method):
1873
        #  - copy of ones params outer dict
1874
        #  - ones for this Port from params PARAMS_SPECIFIC_DICT (override "general" ones from outer dict)
1875
        #  - mod_params (from _execute_afferent_projections)
1876

1877
        # projection_params (ones that will passed to _execute_afferent_projections() method):
1878
        #  - copy of ones from <PROJECTION_TYPE>_PROJECTION_PARAMS
1879
        #  - ones from PROJECTION_SPECIFIC_PARAMS relevant to Projections to this Port
1880
        #    (override more "general" ones specified for the type of Projection)
1881

1882
        # params = params or {}
1883
        params = defaultdict(lambda:{}, params or {})
1✔
1884

1885
        # FIX 5/8/20 [JDC]:
1886
        #    ADD IF STATEMENT HERE TO ASSIGN EMPTY DICTS TO local_params AND projection_params IF params is EMPTY
1887

1888
        # Move any params specified for Port's function in FUNCTION_PARAMS dict into runtime_port_params
1889
        # Do this on params so it holds for all subsequents ports processed
1890
        if FUNCTION_PARAMS in params:
1✔
1891
            params.update(params.pop(FUNCTION_PARAMS))
1✔
1892

1893
        # Copy all items in outer level of params to local_params (i.e., excluding its subdicts)
1894
        local_params = defaultdict(lambda:{}, {k:v for k,v in params.items() if not isinstance(v,dict)})
1✔
1895
        # Get rid of items in params specific to this Port
1896
        for entry in copy_parameter_value(params[PORT_SPECIFIC_PARAMS]):
1✔
1897
            if entry in {self, self.name}:
1✔
1898
                # Move param from params to local_params
1899
                local_params.update(params[PORT_SPECIFIC_PARAMS].pop(entry))
1✔
1900

1901
        # Put copy of all type-specific Projection dicts from params into local_params
1902
        # FIX: ON FIRST PASS ALSO CREATES THOSE DICTS IN params IF THEY DON'T ALREADY EXIST
1903
        projection_params = defaultdict(
1✔
1904
            lambda: {},
1905
            {
1906
                proj_type: copy_parameter_value(params[proj_type])
1907
                for proj_type in projection_param_keywords()
1908
            },
1909
        )
1910

1911
        for entry in copy_parameter_value(params[PROJECTION_SPECIFIC_PARAMS]):
1✔
1912
            if self.all_afferents and entry in self.all_afferents + [p.name for p in self.all_afferents]:
1✔
1913
                if isinstance(entry, str):
1✔
1914
                    projection_type = next(p for p in self.all_afferents if p.name ==entry).componentType
1✔
1915
                else:
1916
                    projection_type = entry.componentType
1✔
1917
                # Get key for type-specific dict in params in which to place it in local_params
1918
                projection_param_type = projection_param_keyword_mapping()[projection_type]
1✔
1919
                # Move from params into relevant type-specific dict in local_params
1920
                projection_params[projection_param_type].update(params[PROJECTION_SPECIFIC_PARAMS].pop(entry))
1✔
1921

1922
        # Note:  having removed all Port- and Projection-specific entries on params, no longer concerned with it;
1923
        #        now only care about local_params and projection_params
1924

1925
        # If either the Port's variable or value is specified in runtime_params, skip executing Projections
1926
        if local_params and any(var_or_val in local_params for var_or_val in {VARIABLE, VALUE}):
1✔
1927
            mod_params = {}
1✔
1928
        else:
1929
            # Otherwise, execute afferent Projections
1930
            mod_params = self._execute_afferent_projections(projection_params, context)
1✔
1931
            if mod_params == OVERRIDE:
1✔
1932
                return
1✔
1933
        local_params.update(mod_params)
1✔
1934

1935
        # EXECUTE PORT  -------------------------------------------------------------------------------------
1936

1937
        self._validate_and_assign_runtime_params(local_params, context=context)
1✔
1938
        variable = local_params.pop(VARIABLE, None)
1✔
1939
        self.execute(variable, context=context, runtime_params=local_params)
1✔
1940

1941
    def _execute_afferent_projections(self, projection_params, context):
1✔
1942
        """Execute all afferent Projections for Port
1943

1944
        Returns
1945
        -------
1946
        mod_params : dict or OVERRIDE
1947

1948
        """
1949
        from psyneulink.core.components.projections.modulatory.modulatoryprojection import ModulatoryProjection_Base
1✔
1950
        from psyneulink.core.components.projections.modulatory.learningprojection import LearningProjection
1✔
1951
        from psyneulink.library.components.projections.pathway.maskedmappingprojection import MaskedMappingProjection
1✔
1952
        from psyneulink.core.components.projections.projection import projection_param_keyword_mapping
1✔
1953

1954
        def set_projection_value(projection, value, context):
1✔
1955
            """Manually set Projection value"""
1956
            projection.parameters.value._set(value, context)
1✔
1957
            # KDM 8/14/19: a caveat about the dot notation/most_recent_context here!
1958
            # should these be manually set despite it not actually being executed?
1959
            # explicitly getting/setting based on context will be more clear
1960
            projection.most_recent_context = context
1✔
1961
            projection.function.most_recent_context = context
1✔
1962
            for pport in projection.parameter_ports:
1✔
1963
                pport.most_recent_context = context
1✔
1964
                pport.function.most_recent_context = context
1✔
1965

1966
        # EXECUTE AFFERENT PROJECTIONS ------------------------------------------------------------------------------
1967

1968
        modulatory_override = False
1✔
1969
        mod_proj_values = {}
1✔
1970

1971
        # For each projection: get its params, pass them to it, get the projection's value, and append to relevant list
1972
        for projection in self.all_afferents:
1✔
1973

1974
            if not self.afferents_info[projection].is_active_in_composition(context.composition):
1✔
1975
                continue
1✔
1976

1977
            if hasattr(projection, 'sender'):
1!
1978
                sender = projection.sender
1✔
1979
            else:
1980
                if self.verbosePref:
×
1981
                    warnings.warn(f"{projection.__class__.__name__} to {self.name} {self.__class__.__name__} "
×
1982
                                  f"of {self.owner.name} ignored [has no sender].")
1983
                continue
×
1984

1985
            # Get type-specific params that apply for type of current
1986
            projection_params_keyword = projection_param_keyword_mapping()[projection.componentType]
1✔
1987
            projection_type_params = copy_parameter_value(projection_params[projection_params_keyword])
1✔
1988

1989
            # Get Projection's variable and/or value if specified in runtime_port_params
1990
            projection_variable = projection_type_params.pop(VARIABLE, None)
1✔
1991
            projection_value = projection_type_params.pop(VALUE, None)
1✔
1992

1993
            # Projection value specified in runtime_port_params, so just assign its value
1994
            if projection_value:
1✔
1995
                set_projection_value(projection, projection_value, context)
1✔
1996

1997
            # ----------------------
1998

1999
            # Handle LearningProjection
2000
            #  - update LearningSignals only if context == LEARNING;  otherwise, assign zero for projection_value
2001
            # IMPLEMENTATION NOTE: done here rather than in its own method in order to exploit parsing of params above
2002
            elif (isinstance(projection, LearningProjection)
1✔
2003
                   and (ContextFlags.LEARNING not in context.execution_phase
2004
                        or not projection.receiver.owner.learnable)):
2005
                projection_value = projection.defaults.value * 0.0
1✔
2006
            # FIX: WHAT IS THE FOLLOWING IF CHECKING FOR?  THAT IT IS AN IDENTITY FUNCTION SO NO EXECUTION NEEDED?
2007
            elif (
1✔
2008
                # learning projections add extra behavior in _execute that invalidates identity function
2009
                not isinstance(projection, LearningProjection)
2010
                # masked mapping projections apply a mask separate from their function - consider replacing it
2011
                # with a masked linear matrix and removing this special class?
2012
                and not isinstance(projection, MaskedMappingProjection)
2013
                and projection.function._is_identity(context)
2014
                # has no parameter ports with afferents (these can modulate parameters and make it non-identity)
2015
                and len(list(itertools.chain.from_iterable([p.all_afferents for p in projection.parameter_ports]))) == 0
2016
                # matrix ParameterPort may be a non identity Accumulator integrator
2017
                and all(pport.function._is_identity(context) for pport in projection.parameter_ports)
2018
            ):
2019
                if projection_variable is None:
1✔
2020
                    projection_variable = projection.sender.parameters.value._get(context)
1✔
2021
                    # KDM 8/14/19: this fallback seems to always happen on the first execution
2022
                    # of the Projection's function (MatrixTransform). Unsure if this is intended or not
2023
                    if projection_variable is None:
1!
2024
                        projection_variable = projection.function.defaults.value
×
2025
                projection.parameters.variable._set(projection_variable, context)
1✔
2026
                projection_value = projection._parse_function_variable(projection_variable)
1✔
2027
                set_projection_value(projection, projection_value, context)
1✔
2028

2029
            # Actually execute Projection to get its value
2030
            else:
2031
                if projection_variable is None:
1✔
2032
                    projection_variable = projection.sender.parameters.value._get(context)
1✔
2033
                projection_value = projection.execute(variable=projection_variable,
1✔
2034
                                                      context=context,
2035
                                                      runtime_params=projection_type_params,
2036
                                                      )
2037

2038
            # If this is initialization run and projection initialization has been deferred, pass
2039
            try:
1✔
2040
                if projection.initialization_status == ContextFlags.DEFERRED_INIT:
1!
2041
                    continue
×
2042
            except AttributeError:
×
2043
                pass
×
2044

2045
            # # KDM 6/20/18: consider moving handling of Modulatory projections into separate method
2046
            # If it is a ModulatoryProjection, add its value to the list in the dict entry for the relevant mod_param
2047
            if isinstance(projection, ModulatoryProjection_Base):
1✔
2048
                # Get the meta_param to be modulated from modulation attribute of the  projection's ModulatorySignal
2049
                #    and get the function parameter to be modulated to type_match the projection value below
2050
                mod_spec, mod_param_name, mod_param_value = self._get_modulated_param(projection, context=context)
1✔
2051
                # If meta_param is DISABLE, ignore the ModulatoryProjection
2052
                if mod_spec == DISABLE:
1✔
2053
                    continue
1✔
2054
                if mod_spec == OVERRIDE:
1✔
2055
                    # If paramValidationPref is set, allow all projections to be processed
2056
                    #    to be sure there are no other conflicting OVERRIDES assigned
2057
                    if self.owner.paramValidationPref:
1!
2058
                        if modulatory_override:
1✔
2059
                            raise PortError(f"Illegal assignment of {MODULATION_OVERRIDE} to more than one "
2060
                                             f"{MODULATORY_SIGNAL} ({projection.name} and {modulatory_override[2]}).")
2061
                        modulatory_override = (MODULATION_OVERRIDE, projection_value, projection)
1✔
2062
                        continue
1✔
2063
                    # Otherwise, for efficiency, assign first OVERRIDE value encountered and return
2064
                    else:
2065
                        # FIX 5/8/20 [JDC]: SHOULD THIS USE set_projection_value()??
2066
                        self.parameters.value._set(match_modulation_to_value(projection_value, self.defaults.value), context)
×
2067
                        return OVERRIDE
×
2068
                else:
2069
                    try:
1✔
2070
                        mod_value = match_modulation_to_value(projection_value, mod_param_value)
1✔
2071
                    except ValueError:
1✔
2072
                        # if type_match fails, assume that the computation is
2073
                        # valid further down the line. This was implicitly true
2074
                        # before adding this catch block by manually setting the
2075
                        # modulated param value from None to a default
2076
                        mod_value = projection_value
1✔
2077

2078
                    if mod_param_name not in mod_proj_values.keys():
1✔
2079
                        mod_proj_values[mod_param_name]=[mod_value]
1✔
2080
                    else:
2081
                        mod_proj_values[mod_param_name].append(mod_value)
1✔
2082

2083
        # Handle ModulatoryProjection OVERRIDE
2084
        #    if there is one and it wasn't been handled above (i.e., if paramValidation is set)
2085
        if modulatory_override:
1✔
2086
            # KDM 6/20/18: consider defining exactly when and how type_match occurs, now it seems
2087
            # a bit handwavy just to make stuff work
2088
            # FIX 5/8/20 [JDC]: SHOULD THIS USE set_projection_value()??
2089
            self.parameters.value._set(match_modulation_to_value(modulatory_override[1], self.defaults.value), context)
1✔
2090
            return OVERRIDE
1✔
2091

2092
        # AGGREGATE ModulatoryProjection VALUES  -----------------------------------------------------------------------
2093

2094
        mod_params = {}
1✔
2095
        for mod_param_name, value_list in mod_proj_values.items():
1✔
2096
            param = getattr(self.function.parameters, mod_param_name)
1✔
2097
            # If the param has a single modulatory value, use that
2098
            if len(value_list)==1:
1✔
2099
                mod_val = value_list[0]
1✔
2100
            # If the param has multiple modulatory values, combine them
2101
            else:
2102
                mod_val = self._get_combined_mod_val(mod_param_name, value_list)
1✔
2103

2104
            # FIX: SHOULD THIS REALLY BE GETTING SET HERE??
2105
            # Set modulatory parameter's value
2106
            param._set(mod_val, context)
1✔
2107
            # Add mod_param and its value to port_params for Port's function
2108
            mod_params.update({mod_param_name: mod_val})
1✔
2109
        return mod_params
1✔
2110

2111
    def _execute(self, variable=None, context=None, runtime_params=None):
1✔
2112
        if variable is None:
1✔
2113
            if hasattr(self, DEFAULT_INPUT) and self.default_input == DEFAULT_VARIABLE:
1✔
2114
                return copy_parameter_value(self.defaults.variable)
1✔
2115

2116
            variable = self._get_variable_from_projections(context)
1✔
2117

2118
            # if the fallback is also None
2119
            # return None, so that this port is ignored
2120
            if variable is None:
1✔
2121
                return None
1✔
2122

2123
        return super()._execute(
1✔
2124
            variable,
2125
            context=context,
2126
            runtime_params=runtime_params,
2127
        )
2128

2129
    def _get_modulated_param(self, mod_proj, receiver=None, context=None):
1✔
2130
        """Return modulation specification from ModulatoryProjection, and name and value of param modulated."""
2131

2132
        from psyneulink.core.components.projections.modulatory.modulatoryprojection import ModulatoryProjection_Base
1✔
2133

2134
        receiver = receiver or self
1✔
2135

2136
        if not isinstance(mod_proj, ModulatoryProjection_Base):
1✔
2137
            raise PortError(f'Specification of {MODULATORY_PROJECTION} to {receiver.full_name} ({mod_proj}) '
2138
                                f'is not a {ModulatoryProjection_Base.__name__}')
2139

2140
        # Get modulation specification from the Projection sender's modulation attribute
2141
        mod_spec = mod_proj.sender.parameters.modulation._get(context)
1✔
2142

2143
        if mod_spec in {OVERRIDE, DISABLE}:
1✔
2144
            mod_param_name = mod_proj.receiver.name
1✔
2145
            mod_param_value = mod_proj.sender.parameters.value._get(context)
1✔
2146
        else:
2147
            mod_param = getattr(receiver.function.parameters, mod_spec)
1✔
2148
            try:
1✔
2149
                mod_param_name = mod_param.source.name
1✔
2150
            except:
×
2151
                mod_param_name = mod_param.name
×
2152

2153
            # Get the value of the modulated parameter
2154
            mod_param_value = getattr(receiver.function.parameters, mod_spec)._get(context)
1✔
2155

2156
        return mod_spec, mod_param_name, mod_param_value
1✔
2157

2158
    def _get_combined_mod_val(self, mod_param_name, values):
1✔
2159
        """Combine the modulatory values received by ModulatoryProjections to mod_param_name
2160
        Uses function specified by modulation_combination_function attribute of param,
2161
        or MULTIPLICATIVE if not specified
2162
        """
2163
        comb_fct = getattr(self.function.parameters, mod_param_name).modulation_combination_function or MULTIPLICATIVE
1✔
2164
        aliases = getattr(self.function.parameters, mod_param_name).aliases
1✔
2165

2166
        if comb_fct==MULTIPLICATIVE or any(mod_spec in aliases for mod_spec in {MULTIPLICATIVE, MULTIPLICATIVE_PARAM}):
1!
2167
            res = np.prod(np.array(values), axis=0)
1✔
2168
        elif comb_fct == ADDITIVE or any(mod_spec in aliases for mod_spec in {MULTIPLICATIVE, ADDITIVE_PARAM}):
×
2169
            res = np.sum(np.array(values), axis=0)
×
2170
        elif isinstance(comb_fct, is_function_type):
×
2171
            res = comb_fct(values)
×
2172
        else:
2173
            assert False, f'PROGRAM ERROR: modulation_combination_function not properly specified ' \
2174
                          f'for {mod_param_name} {Parameter.__name__} of {self.name}'
2175

2176
        return convert_all_elements_to_np_array(res)
1✔
2177

2178
    @abc.abstractmethod
1✔
2179
    def _get_variable_from_projections(self, context=None):
1✔
2180
        """
2181
            Return a variable to be used for self.execute when the variable passed in is None
2182
        """
2183
        pass
×
2184

2185
    def _get_value_label(self, labels_dict, all_ports, context=None):
1✔
2186
        subdicts = False
1✔
2187
        if labels_dict != {}:
1✔
2188
            if isinstance(list(labels_dict.values())[0], dict):
1!
2189
                subdicts = True
1✔
2190

2191
        if not subdicts:    # Labels are specified at the mechanism level - not individual ports
1✔
2192
            # label dict only applies to index 0 port
2193
            if all_ports.index(self) == 0:
1!
2194
                for label in labels_dict:
1!
2195
                    if np.allclose(labels_dict[label], self.parameters.value.get(context)):
×
2196
                        return label
×
2197
            # if this isn't the index 0 port OR a label was not found then just return the original value
2198
            return self.parameters.value.get(context)
1✔
2199

2200
        for port in labels_dict:
1!
2201
            if port is self:
1!
2202
                return self.find_label_value_match(port, labels_dict, context)
×
2203
            elif port == self.name:
1!
2204
                return self.find_label_value_match(self.name, labels_dict, context)
×
2205
            elif port == all_ports.index(self):
1!
2206
                return self.find_label_value_match(all_ports.index(self), labels_dict, context)
1✔
2207

2208
        return self.parameters.value.get(context)
×
2209

2210
    def find_label_value_match(self, key, labels_dict, context=None):
1✔
2211
        for label in labels_dict[key]:
1!
2212
            if np.allclose(labels_dict[key][label], self.parameters.value.get(context)):
1!
2213
                return label
1✔
2214
        return self.parameters.value.get(context)
×
2215

2216
    @property
1✔
2217
    def labeled_value(self):
1✔
2218
        return self.get_label()
1✔
2219

2220
    @property
1✔
2221
    def value_label(self):
1✔
2222
        """Alias of labeled_value"""
2223
        return self.labeled_value
×
2224

2225
    @property
1✔
2226
    def owner(self):
1✔
2227
        return self._owner
1✔
2228

2229
    @owner.setter
1✔
2230
    def owner(self, assignment):
1✔
2231
        self._owner = assignment
1✔
2232

2233
    @property
1✔
2234
    def all_afferents(self):
1✔
2235
        return self._get_all_afferents()
1✔
2236

2237
    @property
1✔
2238
    def afferents_info(self):
1✔
2239
        try:
1✔
2240
            return self._afferents_info
1✔
2241
        except AttributeError:
1✔
2242
            self._afferents_info = {}
1✔
2243
            return self._afferents_info
1✔
2244

2245
    # IMPLEMENTATION NOTE:
2246
    #  Every Port subtype has mod_afferents
2247
    #  path_afferents are specific to InputPorts
2248
    #  efferents are specific to OutputPorts
2249

2250
    @property
1✔
2251
    def mod_afferents(self):
1✔
2252
        try:
1✔
2253
            return self._mod_afferents
1✔
2254
        except:
1✔
2255
            self._mod_afferents = []
1✔
2256
            return self._mod_afferents
1✔
2257

2258
    @property
1✔
2259
    def path_afferents(self):
1✔
2260
        raise PortError(f"{self.__class__.__name__}s do not have 'path_afferents'; "
2261
                        f"(access attempted for {self.full_name}).")
2262

2263
    @path_afferents.setter
1✔
2264
    def path_afferents(self, value):
1✔
2265
        raise PortError(f"{self.__class__.__name__}s are not allowed to have 'path_afferents' "
2266
                             f"(assignment attempted for {self.full_name}).")
2267

2268
    @property
1✔
2269
    def efferents(self):
1✔
2270
        # assert False, f"{self.__class__.__name__} must implement 'efferents' property."
2271
        raise PortError(f"{self.__class__.__name__}s do not have 'efferents'; "
2272
                        f"(access attempted for {self.full_name}).")
2273

2274
    @efferents.setter
1✔
2275
    def efferents(self, proj):
1✔
2276
        # assert False, f"Illegal attempt to directly assign {repr('efferents')} attribute of {self.name}"
2277
        raise PortError(f"{self.__class__.__name__}s are not allowed to have 'efferents' "
2278
                             f"(assignment attempted for {self.full_name}).")
2279

2280
    def get_afferents(self, from_component=None):
1✔
2281
        return self._get_matching_projections(
×
2282
            from_component, self.all_afferents, filter_component_is_sender=True
2283
        )
2284

2285
    def get_efferents(self, to_component=None):
1✔
2286
        return self._get_matching_projections(
×
2287
            to_component, self.efferents, filter_component_is_sender=False
2288
        )
2289

2290
    @property
1✔
2291
    def full_name(self):
1✔
2292
        """Return name relative to owner as:  <owner.name>[<self.name>]"""
2293
        if hasattr(self, OWNER) and self.owner:
1✔
2294
            return f'{self.owner.name}[{self.name}]'
1✔
2295
        else:
2296
            return self.name
1✔
2297

2298
    def _assign_default_port_Name(self):
1✔
2299
        return False
1✔
2300

2301
    def has_modulation(self, composition) -> bool:
1✔
2302
        """Returns True if this Port has an active incoming modulatory
2303
        projection in **composition** or False if it does not.
2304
        """
2305
        for ma in self.mod_afferents:
1✔
2306
            if self.afferents_info[ma].is_active_in_composition(composition):
1✔
2307
                return True
1✔
2308

2309
        return False
1✔
2310

2311
    def _get_input_struct_type(self, ctx):
1✔
2312
        # Use function input type. The shape should be the same,
2313
        # however, some functions still need input shape workarounds.
2314
        func_input_type = ctx.get_input_struct_type(self.function)
1✔
2315

2316
        # Not all ports have path_afferents property.
2317
        len_path_afferents = len(self._get_all_afferents()) - len(self.mod_afferents)
1✔
2318

2319
        # Check that either all inputs or none are delivered by projections.
2320
        if len_path_afferents > 0:
1✔
2321
            assert len(func_input_type) == len_path_afferents, \
1✔
2322
                f"{self.name} shape mismatch: {func_input_type}\nport:\n\t{self.defaults.variable}" \
2323
                f"\n\tfunc: {self.function.defaults.variable}\npath_afferents: {len(self.path_afferents)}."
2324

2325
        if len(self.mod_afferents) == 0:
1✔
2326
            # Not need to wrap inputs of non-modulated ports inside mechanisms
2327
            # This makes sure the port input matches port data input and avoids a copy
2328
            return func_input_type
1✔
2329

2330
        input_types = [func_input_type]
1✔
2331
        # Add modulation
2332
        for mod in self.mod_afferents:
1✔
2333
            input_types.append(ctx.get_output_struct_type(mod))
1✔
2334
        return pnlvm.ir.LiteralStructType(input_types)
1✔
2335

2336
    def _gen_llvm_function_body(self, ctx, builder, params, state, arg_in, arg_out, *, tags:frozenset):
1✔
2337
        port_f = ctx.import_llvm_function(self.function)
1✔
2338

2339
        base_params, f_state = ctx.get_param_or_state_ptr(builder,
1✔
2340
                                                          self,
2341
                                                          "function",
2342
                                                          param_struct_ptr=params,
2343
                                                          state_struct_ptr=state)
2344

2345
        if any(a.sender.modulation != OVERRIDE for a in self.mod_afferents):
1✔
2346
            # Create a local copy of the function parameters only if
2347
            # there are modulating projections of type other than OVERRIDE.
2348
            # LLVM is not eliminating the redundant copy.
2349
            f_params = builder.alloca(port_f.args[0].type.pointee,
1✔
2350
                                      name="modulated_port_params")
2351
            builder.store(builder.load(base_params), f_params)
1✔
2352
        else:
2353
            f_params = base_params
1✔
2354

2355
        # FIXME: Handle and combine multiple afferents
2356
        assert len(self.mod_afferents) <= 1
1✔
2357

2358
        # Apply modulation
2359
        for idx, afferent in enumerate(self.mod_afferents):
1✔
2360
            # The first input is function data input
2361
            # Modulatory projections are ordered after that
2362

2363
            # Get the modulation value
2364
            f_mod_ptr = builder.gep(arg_in, [ctx.int32_ty(0),
1✔
2365
                                             ctx.int32_ty(idx + 1)])
2366

2367
            # Get name of the modulated parameter
2368
            if afferent.sender.modulation == MULTIPLICATIVE:
1✔
2369
                name = self.function.parameters.multiplicative_param.source.name
1✔
2370
            elif afferent.sender.modulation == ADDITIVE:
1✔
2371
                name = self.function.parameters.additive_param.source.name
1✔
2372
            elif afferent.sender.modulation == DISABLE:
1✔
2373
                name = None
1✔
2374
            elif afferent.sender.modulation == OVERRIDE:
1✔
2375
                assert f_mod_ptr.type == arg_out.type, \
1✔
2376
                    "Shape mismatch: Value of '{}' for '{}' ({}) " \
2377
                    "should match value of '{}'s '{}' ({})".format(afferent.sender.name,
2378
                                                                   afferent.sender.owner.name,
2379
                                                                   self.defaults.value,
2380
                                                                   self.owner.name,
2381
                                                                   self.name,
2382
                                                                   afferent.defaults.value)
2383
                # Directly store the value in the output array
2384
                builder.store(builder.load(f_mod_ptr), arg_out)
1✔
2385
                return builder
1✔
2386
            else:
2387
                assert False, "Unsupported modulation parameter: {}".format(afferent.sender.modulation)
2388

2389
            # Replace base param with the modulation value
2390
            if name is not None:
1✔
2391
                f_mod_param_ptr = pnlvm.helpers.get_param_ptr(builder,
1✔
2392
                                                              self.function,
2393
                                                              f_params, name)
2394
                if f_mod_param_ptr.type != f_mod_ptr.type:
1✔
2395
                    warnings.warn("Shape mismatch: Modulation vs. modulated parameter: {} vs. {}".format(
1✔
2396
                                  afferent.defaults.value,
2397
                                  getattr(self.function.parameters, name).get(None)),
2398
                                  pnlvm.PNLCompilerWarning)
2399
                    param_val = pnlvm.helpers.load_extract_scalar_array_one(builder, f_mod_ptr)
1✔
2400
                else:
2401
                    param_val = builder.load(f_mod_ptr)
1✔
2402
                builder.store(param_val, f_mod_param_ptr)
1✔
2403

2404
        # OutputPort returns 1D array even for scalar functions
2405
        if arg_out.type != port_f.args[3].type:
1✔
2406
            assert len(arg_out.type.pointee) == 1
1✔
2407
            arg_out = builder.gep(arg_out, [ctx.int32_ty(0), ctx.int32_ty(0)])
1✔
2408

2409
        # Extract the data part of input
2410
        if len(self.mod_afferents) == 0:
1✔
2411
            f_input = arg_in
1✔
2412
        else:
2413
            f_input = builder.gep(arg_in, [ctx.int32_ty(0), ctx.int32_ty(0)])
1✔
2414

2415
        builder.call(port_f, [f_params, f_state, f_input, arg_out])
1✔
2416
        return builder
1✔
2417

2418
    @staticmethod
1✔
2419
    # TODO: find out why UNSET is required to avoid many more deepcopies
2420
    # occurring causing slowdowns
2421
    @handle_external_context(source=ContextFlags.UNSET)
1✔
2422
    def _get_port_function_value(owner, function, variable, context=None):
1✔
2423
        """Execute the function of a Port and return its value
2424
        # FIX: CONSIDER INTEGRATING THIS INTO _EXECUTE FOR PORT?
2425

2426
        This is a stub, that a Port subclass can override to treat its function in a Port-specific manner.
2427
        Used primarily during validation, when the function may not have been fully instantiated yet
2428
        (e.g., InputPort must sometimes embed its variable in a list-- see InputPort._get_port_function_value).
2429
        """
2430
        return function.execute(variable, context=context)
1✔
2431

2432
    @property
1✔
2433
    def _dependent_components(self):
1✔
2434
        res = super()._dependent_components
1✔
2435
        try:
1✔
2436
            res.extend(self.all_afferents)
1✔
NEW
2437
        except AttributeError:
×
NEW
2438
            pass
×
2439
        return res
1✔
2440

2441

2442
def _instantiate_port_list(owner,
1✔
2443
                            port_list,              # list of Port specs, (port_spec, params) tuples, or None
2444
                            port_types,             # PortType subclass
2445
                            port_Param_identifier,  # used to specify port_type Port(s) in params[]
2446
                            reference_value,         # value(s) used as default for Port and to check compatibility
2447
                            reference_value_name,    # name of reference_value type (e.g. variable, output...)
2448
                            context=None):
2449
    """Instantiate and return a ContentAddressableList of Ports specified in port_list
2450

2451
    Arguments:
2452
    - port_type (class): Port class to be instantiated
2453
    - port_list (list): List of Port specifications (generally from owner.kw<port>),
2454
                             each item of which must be a:
2455
                                 string (used as name)
2456
                                 number (used as constraint value)
2457
                                 dict (key=name, value=reference_value or param dict)
2458
                         if None, instantiate a single default Port using reference_value as port_spec
2459
    - port_Param_identifier (str): used to identify set of Ports in params;  must be one of:
2460
        - INPUT_PORT
2461
        - OUTPUT_PORT
2462
        (note: this is not a list, even if port_types is, since it is about the attribute to which the
2463
               ports will be assigned)
2464
    - reference_value (2D np.array): set of 1D np.ndarrays used as default values and
2465
        for compatibility testing in instantiation of Port(s):
2466
        - INPUT_PORT: self.defaults.variable
2467
        - OUTPUT_PORT: self.value
2468
        ?? ** Note:
2469
        * this is ignored if param turns out to be a dict (entry value used instead)
2470
    - reference_value_name (str):  passed to Port._instantiate_port(), used in error messages
2471
    - context (str)
2472

2473
    If port_list is None:
2474
        - instantiate a default Port using reference_value,
2475
        - place as the single entry of the list returned.
2476
    Otherwise, if port_list is:
2477
        - a single value:
2478
            instantiate it (if necessary) and place as the single entry in an OrderedDict
2479
        - a list:
2480
            instantiate each item (if necessary) and place in a ContentAddressableList
2481
    In each case, generate a ContentAddressableList with one or more entries, assigning:
2482
        # the key for each entry the name of the Port if provided,
2483
        #     otherwise, use MECHANISM<port_type>Ports-n (incrementing n for each additional entry)
2484
        # the Port value for each entry to the corresponding item of the Mechanism's port_type Port's value
2485
        # the dict to self.<port_type>Ports
2486
        # self.<port_type>Port to self.<port_type>Ports[0] (the first entry of the dict)
2487
    Notes:
2488
        * if there is only one Port, but the value of the Mechanism's port_type has more than one item:
2489
            assign it to the sole Port, which is assumed to have a multi-item value
2490
        * if there is more than one Port:
2491
            the number of Ports must match length of Mechanisms port_type value or an exception is raised
2492
    """
2493

2494
    # If no Ports were passed in, instantiate a default port_type using reference_value
2495
    if not port_list:
1✔
2496
        # assign reference_value as single item in a list, to be used as port_spec below
2497
        port_list = copy_parameter_value(reference_value)
1✔
2498

2499
        # issue warning if in VERBOSE mode:
2500
        if owner.prefs.verbosePref:
1!
2501
            print(f"No {port_Param_identifier} specified for {owner.__class__.__name__}; "
×
2502
                  f"default will be created using {reference_value_name} "
2503
                  f"of function ({reference_value}) as its value.")
2504

2505
    # Ports should be either in a list, or possibly an np.array (from reference_value assignment above):
2506
    # KAM 6/21/18 modified to include tuple as an option for port_list
2507
    if not isinstance(port_list, (ContentAddressableList, list, np.ndarray, tuple)):
1✔
2508
        # This shouldn't happen, as items of port_list should be validated to be one of the above in _validate_params
2509
        raise PortError("PROGRAM ERROR: {} for {} is not a recognized \'{}\' specification for {}; "
2510
                         "it should have been converted to a list in Mechanism._validate_params)".
2511
                         format(port_list, owner.name, port_Param_identifier, owner.__class__.__name__))
2512

2513

2514
    # VALIDATE THAT NUMBER OF PORTS IS COMPATIBLE WITH NUMBER OF ITEMS IN reference_values
2515

2516
    num_ports = len(port_list)
1✔
2517
    # Check that reference_value is an indexable object, the items of which are the constraints for each Port
2518
    # Notes
2519
    # * generally, this will be a list or an np.ndarray (either >= 2D np.array or with a dtype=object)
2520
    # * for OutputPorts, this should correspond to its value
2521
    try:
1✔
2522
        # Ensure that reference_value is an indexible item (list, >=2D np.darray, or otherwise)
2523
        num_constraint_items = len(reference_value)
1✔
2524
    except:
×
2525
        raise PortError(f"PROGRAM ERROR: reference_value ({reference_value}) for {reference_value_name} of "
2526
                         f"{[s.__name__ for s in port_types]} must be an indexable object (e.g., list or np.ndarray).")
2527
    # If number of Ports does not equal the number of items in reference_value, raise exception
2528
    if num_ports != num_constraint_items:
1✔
2529
        if num_ports > num_constraint_items:
1✔
2530
            comparison_string = 'more'
1✔
2531
        else:
2532
            comparison_string = 'fewer'
1✔
2533
        raise PortError(f"There are {comparison_string} {port_Param_identifier}s specified ({num_ports}) "
2534
                         f"than the number of items ({num_constraint_items}) in the '{reference_value_name}' "
2535
                         f"of the function for '{repr(owner.name)}'.")
2536

2537
    # INSTANTIATE EACH PORT
2538

2539
    ports = ContentAddressableList(component_type=Port_Base,
1✔
2540
                                   name=owner.name + ' ContentAddressableList of ' + port_Param_identifier)
2541
    # For each port, pass port_spec and the corresponding item of reference_value to _instantiate_port
2542

2543
    if not isinstance(port_types, list):
1✔
2544
        port_types = [port_types] * len(port_list)
1✔
2545
    if len(port_types) != len(port_list):
1!
2546
        port_types = [port_types[0]] * len(port_list)
×
2547
    # for index, port_spec, port_type in enumerate(zip(port_list, port_types)):
2548
    for index, port_spec, port_type in zip(list(range(len(port_list))), port_list, port_types):
1✔
2549
        # # Get name of port, and use as index to assign to ports ContentAddressableList
2550
        # default_name = port_type._assign_default_port_Name(port_type)
2551
        # name = default_name or None
2552

2553
        port = _instantiate_port(port_type=port_type,
1✔
2554
                                   owner=owner,
2555
                                   reference_value=reference_value[index],
2556
                                   reference_value_name=reference_value_name,
2557
                                   port_spec=port_spec,
2558
                                   # name=name,
2559
                                   context=context)
2560
        # automatically generate any Projections to InputPort or ParameterPort
2561
        # (e.g. if InputPort was specified using the OutputPort of another Mechanism,
2562
        #       or a ParameterPort was specified using the ControlSignal of a ControlMechanism)
2563
        try:
1✔
2564
            for proj in port.path_afferents:
1✔
2565
                owner.aux_components.append(proj)
1✔
2566
        except PortError:
1✔
2567
            # OutputPort that has no path_afferents
2568
            pass
1✔
2569

2570
        # KDM 12/3/19: this depends on name setting for InputPorts that
2571
        # ensures there are no duplicates. If duplicates exist, ports
2572
        # will be overwritten
2573
        # be careful of:
2574
        #   test_rumelhart_semantic_network_sequential
2575
        #   test_mix_and_match_input_sources
2576
        ports[port.name] = port
1✔
2577

2578
    return ports
1✔
2579

2580
@beartype
1✔
2581
def _instantiate_port(port_type: Type[Port],  # Port's type
1✔
2582
                      owner: Union[Mechanism, Projection],  # Port's owner
2583
                      reference_value,  # constraint for Port's value and default for variable
2584
                      name: Optional[str] = None,  # port's name if specified
2585
                      variable=None,  # used as default value for port if specified
2586
                      params=None,  # port-specific params
2587
                      prefs=None,
2588
                      context=None,
2589
                      **port_spec):                        # captures *port_spec* arg and any other non-standard ones
2590
    """Instantiate a Port of specified type, with a value that is compatible with reference_value
2591

2592
    This is the interface between the various ways in which a port can be specified and the Port's constructor
2593
        (see list below, and `Port_Specification` in docstring above).
2594
    It calls _parse_port_spec to:
2595
        create a Port specification dictionary (the canonical form) that can be passed to the Port's constructor;
2596
        place any Port subclass-specific params in the params entry;
2597
        call _parse_port_specific_specs to parse and validate the values of those params
2598
    It checks that the Port's value is compatible with the reference value and/or any projection specifications
2599

2600
    # Constraint value must be a number or a list or tuple of numbers
2601
    # (since it is used as the variable for instantiating the requested port)
2602

2603
    If port_spec is a:
2604
    + Port class:
2605
        implement default using reference_value
2606
    + Port object:
2607
        check compatibility of value with reference_value
2608
        check owner is owner; if not, raise exception
2609
    + 2-item tuple:
2610
        assign first item to port_spec
2611
        assign second item to port_params{PROJECTIONS:<projection>}
2612
    + Projection object:
2613
        assign reference_value to value
2614
        assign projection to port_params{PROJECTIONS:<projection>}
2615
    + Projection class (or keyword string constant for one):
2616
        assign reference_value to value
2617
        assign projection class spec to port_params{PROJECTIONS:<projection>}
2618
    + specification dict for Port
2619
        check compatibility of port_value with reference_value
2620

2621
    Returns a Port or None
2622
    """
2623

2624
    # Parse reference value to get actual value (in case it is, itself, a specification dict)
2625
    from psyneulink.core.globals.utilities import is_numeric
1✔
2626
    if not is_numeric(reference_value):
1✔
2627
        reference_value_dict = _parse_port_spec(owner=owner,
1✔
2628
                                                 port_type=port_type,
2629
                                                 port_spec=reference_value,
2630
                                                 value=None,
2631
                                                 params=None)
2632
        # Its value is assigned to the VARIABLE entry (including if it was originally just a value)
2633
        reference_value = reference_value_dict[VARIABLE]
1✔
2634

2635
    parsed_port_spec = _parse_port_spec(port_type=port_type,
1✔
2636
                                        owner=owner,
2637
                                        reference_value=reference_value,
2638
                                        name=name,
2639
                                        variable=variable,
2640
                                        params=params,
2641
                                        prefs=prefs,
2642
                                        context=context,
2643
                                        **port_spec)
2644

2645
    # PORT SPECIFICATION IS A Port OBJECT ***************************************
2646
    # Validate and return
2647

2648
    # - check that its value attribute matches the reference_value
2649
    # - check that it doesn't already belong to another owner
2650
    # - if either fails, assign default Port
2651

2652
    if isinstance(parsed_port_spec, Port):
1✔
2653

2654
        port = parsed_port_spec
1✔
2655

2656
        # Port initialization was deferred (owner or reference_value was missing), so
2657
        #    assign owner, variable, and/or reference_value if they were not already specified
2658
        if port.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
2659
            if not port._init_args[OWNER]:
1!
2660
                port._init_args[OWNER] = owner
1✔
2661
            # If variable was not specified by user or Port's constructor:
2662
            if VARIABLE not in port._init_args or port._init_args[VARIABLE] is None:
1✔
2663
                # If call to _instantiate_port specified variable, use that
2664
                if variable is not None:
1✔
2665
                    port._init_args[VARIABLE] = variable
1✔
2666
                # Otherwise, use Port's owner's default variable as default if it has one
2667
                elif len(owner.defaults.variable):
1!
2668
                    port._init_args[VARIABLE] = owner.defaults.variable[0]
1✔
2669
                # If all else fails, use Port's own defaults.variable
2670
                else:
UNCOV
2671
                    port._init_args[VARIABLE] = port.defaults.variable
×
2672
            if not hasattr(port, REFERENCE_VALUE):
1!
2673
                if REFERENCE_VALUE in port._init_args and port._init_args[REFERENCE_VALUE] is not None:
1✔
2674
                    port.reference_value = port._init_args[REFERENCE_VALUE]
1✔
2675
                else:
2676
                    # port.reference_value = owner.defaults.variable[0]
2677
                    port.reference_value = port._init_args[VARIABLE]
1✔
2678
            port._deferred_init(context=context)
1✔
2679

2680
        # # FIX: 10/3/17 - CHECK THE FOLLOWING BY CALLING PORT-SPECIFIC METHOD?
2681
        # # FIX: DO THIS IN _parse_connection_specs?
2682
        # # *reference_value* arg should generally be a constraint for the value of the Port;  however,
2683
        # #     if port_spec is a Projection, and method is being called from:
2684
        # #         InputPort, reference_value should be the projection's value;
2685
        # #         ParameterPort, reference_value should be the projection's value;
2686
        # #         OutputPort, reference_value should be the projection's variable
2687
        # # variable:
2688
        # #   InputPort: set of projections it receives
2689
        # #   ParameterPort: value of its sender
2690
        # #   OutputPort: _parse_output_port_variable()
2691
        # # FIX: ----------------------------------------------------------
2692

2693
        # FIX: THIS SHOULD ONLY APPLY TO InputPort AND ParameterPort; WHAT ABOUT OutputPort?
2694
        # Port's assigned value is incompatible with its reference_value (presumably its owner Mechanism's variable)
2695
        reference_value = reference_value if reference_value is not None else port.reference_value
1✔
2696
        if not iscompatible(port.defaults.value, reference_value):
1✔
2697
            raise PortError("{}'s value attribute ({}) is incompatible with the {} ({}) of its owner ({})".
2698
                             format(port.name, port.defaults.value, REFERENCE_VALUE, reference_value, owner.name))
2699

2700
        # Port has already been assigned to an owner
2701
        if port.owner is not None and port.owner is not owner:
1✔
2702
            raise PortError("Port {} does not belong to the owner for which it is specified ({})".
2703
                             format(port.name, owner.name))
2704
        return port
1✔
2705

2706
    # PORT SPECIFICATION IS A Port specification dictionary ***************************************
2707
    #    so, call constructor to instantiate Port
2708

2709
    port_spec_dict = parsed_port_spec
1✔
2710

2711
    port_spec_dict.pop(VALUE, None)
1✔
2712

2713
    # FIX: 2/25/18  GET REFERENCE_VALUE FROM REFERENCE_DICT?
2714
    # Get reference_value
2715
    if port_spec_dict[REFERENCE_VALUE] is None:
1✔
2716
        port_spec_dict[REFERENCE_VALUE] = reference_value
1✔
2717
        if reference_value is None:
1✔
2718
            port_spec_dict[REFERENCE_VALUE] = port_spec_dict[VARIABLE]
1✔
2719

2720
    #  Convert reference_value to np.array to match port_variable (which, as output of function, will be an np.array)
2721
    if port_spec_dict[REFERENCE_VALUE] is not None:
1✔
2722
        port_spec_dict[REFERENCE_VALUE] = convert_to_np_array(port_spec_dict[REFERENCE_VALUE], 1)
1✔
2723

2724
    # INSTANTIATE PORT:
2725

2726
    # IMPLEMENTATION NOTE:
2727
    # - setting prefs=NotImplemented causes TYPE_DEFAULT_PREFERENCES to be assigned (from BasePreferenceSet)
2728
    # - alternative would be prefs=owner.prefs, causing port to inherit the prefs of its owner;
2729
    port_type = port_spec_dict.pop(PORT_TYPE, None)
1✔
2730
    if REFERENCE_VALUE_NAME in port_spec_dict:
1!
2731
        del port_spec_dict[REFERENCE_VALUE_NAME]
×
2732
    if port_spec_dict[PARAMS] and REFERENCE_VALUE_NAME in port_spec_dict[PARAMS]:
1!
2733
        del port_spec_dict[PARAMS][REFERENCE_VALUE_NAME]
×
2734

2735
    # Implement default Port
2736
    port = port_type(**port_spec_dict, context=context)
1✔
2737

2738
# FIX LOG: ADD NAME TO LIST OF MECHANISM'S VALUE ATTRIBUTES FOR USE BY LOGGING ENTRIES
2739
    # This is done here to register name with Mechanism's portValues[] list
2740
    # It must be consistent with value setter method in Port
2741
# FIX LOG: MOVE THIS TO MECHANISM PORT __init__ (WHERE IT CAN BE KEPT CONSISTENT WITH setter METHOD??
2742
#      OR MAYBE JUST REGISTER THE NAME, WITHOUT SETTING THE
2743
# FIX: 2/17/17:  COMMENTED THIS OUT SINCE IT CREATES AN ATTRIBUTE ON OWNER THAT IS NAMED <port.name.value>
2744
#                NOT SURE WHAT THE PURPOSE IS
2745
#     setattr(owner, port.name+'.value', port.value)
2746

2747
    port._validate_against_reference_value(reference_value)
1✔
2748

2749
    return port
1✔
2750

2751

2752
def _parse_port_type(owner, port_spec):
1✔
2753
    """Determine Port type for port_spec and return type
2754

2755
    Determine type from context and/or type of port_spec if the latter is not a `Port <Port>` or `Mechanism
2756
    <Mechanism>`.
2757
    """
2758

2759
    # Port class reference
2760
    if isinstance(port_spec, Port):
1!
2761
        return type(port_spec)
1✔
2762

2763
    # keyword for a Port or name of a standard_output_port or of Port itself
2764
    if isinstance(port_spec, str):
×
2765

2766
        # Port keyword
2767
        if port_spec in port_type_keywords:
×
2768
            return getattr(sys.modules['PsyNeuLink.Components.Ports.' + port_spec], port_spec)
×
2769

2770
        # Try as name of Port
2771
        for port_attr in [INPUT_PORTS, PARAMETER_PORTS, OUTPUT_PORTS]:
×
2772
            port_list = getattr(owner, port_attr)
×
2773
            try:
×
2774
                port = port_list[port_spec]
×
2775
                return port.__class__
×
2776
            except TypeError:
×
2777
                pass
×
2778

2779
        # standard_output_port
2780
        if hasattr(owner, STANDARD_OUTPUT_PORTS):
×
2781
            # check if string matches the name entry of a dict in standard_output_ports
2782
            # item = next((item for item in owner.standard_output_ports.names if port_spec is item), None)
2783
            # if item is not None:
2784
            #     # assign dict to owner's output_port list
2785
            #     return owner.standard_output_ports.get_dict(port_spec)
2786
            # from psyneulink.core.Components.Ports.OutputPort import StandardOutputPorts
2787
            if owner.standard_output_ports.get_port_dict(port_spec):
×
2788
                from psyneulink.core.components.Ports.OutputPort import OutputPort
×
2789
                return OutputPort
×
2790

2791
    # Port specification dict
2792
    if isinstance(port_spec, dict):
×
2793
        if PORT_TYPE in port_spec:
×
2794
            if not inspect.isclass(port_spec[PORT_TYPE]) and issubclass(port_spec[PORT_TYPE], Port):
×
2795
                raise PortError("PORT entry of port specification for {} ({})"
2796
                                 "is not a Port or type of Port".
2797
                                 format(owner.name, port_spec[PORT]))
2798
            return port_spec[PORT_TYPE]
×
2799

2800
    raise PortError("{} is not a legal Port specification for {}".format(port_spec, owner.name))
2801

2802

2803
PORT_SPEC_INDEX = 0
1✔
2804

2805
# FIX: CHANGE EXPECTATION OF *PROJECTIONS* ENTRY TO BE A SET OF TUPLES WITH THE WEIGHT AND EXPONENT FOR IT
2806
#          THESE CAN BE USED BY THE InputPort's LinearCombination Function
2807
#          (AKIN TO HOW THE MECHANISM'S FUNCTION COMBINES InputPort VALUES)
2808
#          THIS WOULD ALLOW FULLY GENEREAL (HIEARCHICALLY NESTED) ALGEBRAIC COMBINATION OF INPUT VALUES TO A MECHANISM
2809
@beartype
1✔
2810
def _parse_port_spec(port_type=None,
1✔
2811
                      owner=None,
2812
                      reference_value=None,
2813
                      name=None,
2814
                      variable=None,
2815
                      value=None,
2816
                      params=None,
2817
                      prefs=None,
2818
                      context=None,
2819
                      **port_spec):
2820
    """Parse Port specification and return either Port object or Port specification dictionary
2821

2822
    If port_spec is or resolves to a Port object, returns Port object.
2823
    Otherwise, return Port specification dictionary using any arguments provided as defaults
2824
    Warn if variable is assigned the default value, and verbosePref is set on owner.
2825
    *value* arg should generally be a constraint for the value of the Port;  however,
2826
        if port_spec is a Projection, and method is being called from:
2827
            InputPort, value should be the projection's value;
2828
            ParameterPort, value should be the projection's value;
2829
            OutputPort, value should be the projection's variable
2830

2831
    If a Port specification dictionary is specified in the *port_specs* argument,
2832
       its entries are moved to standard_args, replacing any that are there, and they are deleted from port_specs;
2833
       any remaining entries are passed to _parse_port_specific_specs and placed in params.
2834
    This gives precedence to values of standard args specified in a Port specification dictionary
2835
       (e.g., by the user) over any explicitly specified in the call to _instantiate_port.
2836
    The standard arguments (from standard_args and/or a Port specification dictonary in port_specs)
2837
        are placed assigned to port_dict, as defaults for the Port specification dictionary returned by this method.
2838
    Any item in *port_specs* OTHER THAN a Port specification dictionary is placed in port_spec_arg
2839
       is parsed and/or validated by this method.
2840
    Values in standard_args (i.e., specified in the call to _instantiate_port) are used to validate a port specified
2841
       in port_spec_arg;
2842
       - if the Port is an existing one, the standard_arg values are assigned to it;
2843
       - if port_spec_arg specifies a new Port, the values in standard_args are used as defaults;  any specified
2844
          in the port_spec_arg specification are used
2845
    Any arguments to _instantiate_ports that are not standard arguments (in standard_args) or a port_specs_arg
2846
       generate a warning and are ignored.
2847

2848
    Return either Port object or Port specification dictionary
2849
    """
2850
    from psyneulink.core.components.projections.projection \
1✔
2851
        import _is_projection_spec, _parse_projection_spec, _parse_connection_specs, ProjectionTuple
2852
    from psyneulink.core.components.ports.modulatorysignals.modulatorysignal import _is_modulatory_spec
1✔
2853
    from psyneulink.core.components.mechanisms.modulatory.modulatorymechanism import ModulatoryMechanism_Base
1✔
2854
    from psyneulink.core.components.projections.projection import _get_projection_value_shape
1✔
2855

2856
    value = copy_parameter_value(value)
1✔
2857

2858
    # Get all of the standard arguments passed from _instantiate_port (i.e., those other than port_spec) into a dict
2859
    standard_args = get_args(inspect.currentframe())
1✔
2860

2861
    PORT_SPEC_ARG = 'port_spec'
1✔
2862
    port_specification = None
1✔
2863
    port_specific_args = {}
1✔
2864

2865
    # If there is a port_specs arg passed from _instantiate_port:
2866
    if PORT_SPEC_ARG in port_spec:
1✔
2867

2868
        # If it is a Port specification dictionary
2869
        if isinstance(port_spec[PORT_SPEC_ARG], dict):
1✔
2870
            # If the Port specification has a Projection specification (instantiated or in deferred_init)
2871
            # (used to define its connection) then return port specified in the Projection.
2872
            # This can either the sender or the receiver of the specified Projection, depending on the caller:
2873
            # - if the sender is specified in the Projection, return port = receiver
2874
            #   (e.g., projection is a ControlProjections with its receiver (ParameterPort) specified,
2875
            #          so return port = sender -- i.e., ControlSignal to which ControlProjection should be connected)
2876
            # - if the receiver is specified in the Projection, return port = sender
2877
            #   (e.g., projection is from an OutputPort with its sender specified,
2878
            #          so return the port = receiver -- i.e., InputPort to which Projection should be connected)
2879
            # FIX: WHAT IF PORT SPECIFICATION DICT HAS OTHER SPECS, SUCH AS SIZE?
2880
            # FIX: POSSIBLY THIS SHOULD BE CALLED ONLY IF DICT CONTAINS *ONLY* A PROJECTION SPEC?
2881
            try:
1✔
2882
                projection = port_spec[PORT_SPEC_ARG][PROJECTIONS]
1✔
2883
                if isinstance(projection, list):
1✔
2884
                    assert len(port_spec[PORT_SPEC_ARG][PROJECTIONS])==1
1✔
2885
                    projection = port_spec[PORT_SPEC_ARG][PROJECTIONS][0]
1✔
2886
                if projection.initialization_status == ContextFlags.INITIALIZED:
1✔
2887
                    sender = projection.sender
1✔
2888
                    receiver = projection.receiver
×
2889
                elif projection.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
2890
                    sender = projection._init_args[SENDER]
1✔
2891
                    receiver = projection._init_args[RECEIVER]
1✔
2892
                else:
2893
                    assert False, f"PROGRAM ERROR: Projection ('{projection.name}') specified for" \
2894
                                  f" {port_type.__name__} of '{owner.name}' has unrcognized initialization_status: " \
2895
                                  f"{projection.initialization_status.name}"
2896
                # Check whether sender and/or receiver is an instantiated Port
2897
                sender_instantiated = isinstance(sender, Port)
1✔
2898
                if sender_instantiated:
1✔
2899
                    sender_instantiated = sender.initialization_status == ContextFlags.INITIALIZED
1✔
2900
                receiver_instantiated = isinstance(receiver, Port)
1✔
2901
                if receiver_instantiated:
1✔
2902
                    receiver_instantiated = receiver.initialization_status == ContextFlags.INITIALIZED
1✔
2903
                # Ensure that *either* sender or receiver is an instantiated Port, but *not both*
2904
                assert sender_instantiated ^ receiver_instantiated, \
1✔
2905
                    f"PROGRAM ERROR: Projection ('{projection.name}') specified for" \
2906
                    f" {port_type.__name__} of '{owner.name}' has both sender and receiver instantiated: " \
2907
                    f"sender: '{sender.name}'; receiver: '{receiver.name}'"
2908
                if sender_instantiated:
1✔
2909
                    port = projection.receiver
1✔
2910
                else:
2911
                    port = projection.sender
1✔
2912

2913
                if port.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
2914
                    port._init_args[PROJECTIONS] = projection
1✔
2915
                else:
2916
                    port._instantiate_projections_to_port(projections=projection, context=context)
1✔
2917
                return port
1✔
2918
            except:
1✔
2919
                pass
1✔
2920

2921
            # Use the value of any standard args specified in the Port specification dictionary
2922
            #    to replace those explicitly specified in the call to _instantiate_port (i.e., passed in standard_args)
2923
            #    (use copy so that items in port_spec dict are not deleted when called from _validate_params)
2924
            port_specific_args = copy_parameter_value(port_spec[PORT_SPEC_ARG])
1✔
2925
            standard_args.update({key: port_specific_args[key]
1✔
2926
                                  for key in port_specific_args
2927
                                  if key in standard_args and port_specific_args[key] is not None})
2928
            # Delete them from the Port specification dictionary, leaving only port-specific items there
2929
            for key in standard_args:
1✔
2930
                port_specific_args.pop(key, None)
1✔
2931

2932
            try:
1✔
2933
                spec = port_spec[PORT_SPEC_ARG]
1✔
2934
                port_tuple = [spec[PORT_SPEC_ARG], spec[WEIGHT], spec[EXPONENT]]
1✔
2935
                try:
1✔
2936
                    port_tuple.append(spec[PROJECTIONS])
1✔
2937
                except KeyError:
1✔
2938
                    pass
1✔
2939
                port_specification = tuple(port_tuple)
1✔
2940
            except KeyError:
1✔
2941
                pass
1✔
2942

2943
        else:
2944
            port_specification = try_extract_0d_array_item(port_spec[PORT_SPEC_ARG])
1✔
2945

2946
        # Delete the Port specification dictionary from port_spec
2947
        del port_spec[PORT_SPEC_ARG]
1✔
2948

2949
    if REFERENCE_VALUE_NAME in port_spec:
1✔
2950
        del port_spec[REFERENCE_VALUE_NAME]
1✔
2951

2952
    if port_spec:
1✔
2953
        if owner.verbosePref:
1!
2954
            print(f'Args other than standard args and port_spec were in _instantiate_port ({port_spec}).')
×
2955
        port_spec.update(port_specific_args)
1✔
2956
        port_specific_args = port_spec
1✔
2957

2958
    port_dict = standard_args
1✔
2959
    context = port_dict.pop(CONTEXT, None)
1✔
2960
    owner = port_dict[OWNER]
1✔
2961
    port_type = port_dict[PORT_TYPE]
1✔
2962
    reference_value = copy_parameter_value(port_dict[REFERENCE_VALUE])
1✔
2963
    variable = copy_parameter_value(port_dict[VARIABLE])
1✔
2964
    params = port_specific_args
1✔
2965

2966
    # Validate that port_type is a Port class
2967
    if isinstance(port_type, str):
1✔
2968
        try:
1✔
2969
            port_type = PortRegistry[port_type].subclass
1✔
2970
        except KeyError:
×
2971
            raise PortError("{} specified as a string (\'{}\') must be the name of a sublcass of {}".
2972
                             format(PORT_TYPE, port_type, Port.__name__))
2973
        port_dict[PORT_TYPE] = port_type
1✔
2974
    elif not inspect.isclass(port_type) or not issubclass(port_type, Port):
1✔
2975
        raise PortError("\'port_type\' arg ({}) must be a sublcass of {}".format(port_type,
2976
                                                                                   Port.__name__))
2977
    port_type_name = port_type.__name__
1✔
2978

2979
    proj_is_feedback = False
1✔
2980
    if isinstance(port_specification, tuple) and str(port_specification[1]) == FEEDBACK:
1!
2981
        port_specification = port_specification[0]
×
2982
        proj_is_feedback = True
×
2983

2984
    # EXISTING PORTS
2985

2986
    # Determine whether specified Port is one to be instantiated or to be connected with,
2987
    #    and validate that it is consistent with any standard_args specified in call to _instantiate_port
2988

2989
    # function; try to resolve to a value
2990
    if isinstance(port_specification, types.FunctionType):
1!
2991
        port_specification = port_specification()
×
2992

2993
    # RandomMatrix (used for Projection); try to resolve to a matrix
2994
    if isinstance(port_specification, RandomMatrix):
1!
2995
        rows = len(owner.sender.value)
×
2996
        cols = len(owner.receiver.value)
×
2997
        port_specification = port_specification(rows,cols)
×
2998

2999
    # ModulatorySpecification of some kind
3000
    if _is_modulatory_spec(port_specification):
1✔
3001
        # If it is a ModulatoryMechanism specification, get its ModulatorySignal class
3002
        # (so it is recognized by _is_projection_spec below (Mechanisms are not for secondary reasons)
3003
        if isinstance(port_specification, type) and issubclass(port_specification, ModulatoryMechanism_Base):
1✔
3004
            port_specification = port_specification.outputPortTypes
1✔
3005
            # IMPLEMENTATION NOTE:  The following is to accomodate GatingSignals on ControlMechanism
3006
            # FIX: TRY ELIMINATING SIMILAR HANDLING IN Projection (and OutputPort?)
3007
            # FIX: AND ANY OTHER PLACES WHERE LISTS ARE DEALT WITH
3008
            if isinstance(port_specification, list):
1!
3009
                # If modulatory projection is specified as a Mechanism that allows more than one type of OutputPort
3010
                #   (e.g., ModulatoryMechanism allows either ControlSignals or GatingSignals together with standard
3011
                #   OutputPorts) make sure that only one of these is appropriate for port to be modulated
3012
                #   (port_type.connectswith), otherwise it is ambiguous which to assign as Port_Specification
3013
                specs = [s for s in port_specification if s.__name__ in port_type.connectsWith]
×
3014
                try:
×
3015
                    port_specification, = specs
×
3016
                except ValueError:
×
3017
                    assert False, \
3018
                        f"PROGRAM ERROR:  More than one {Port.__name__} type found ({specs})" \
3019
                            f"that can be specificied as a modulatory {Projection.__name__} to {port_type}"
3020

3021
        projection = port_type
1✔
3022

3023
    # Port or Mechanism object specification:
3024
    if isinstance(port_specification, (Mechanism, Port)):
1✔
3025

3026
        projection = None
1✔
3027

3028
        # Mechanism object:
3029
        if isinstance(port_specification, Mechanism):
1✔
3030
            mech = port_specification
1✔
3031
            # Instantiating Port of specified Mechanism, so get primary Port of port_type
3032
            if mech is owner:
1!
3033
                port_specification = port_type._get_primary_port(port_type, mech)
×
3034
            # mech used to specify Port to be connected with:
3035
            else:
3036
                port_specification = mech
1✔
3037
                projection = port_type
1✔
3038

3039
        if port_specification.__class__ == port_type:
1✔
3040
            # Make sure that the specified Port belongs to the Mechanism passed in the owner arg
3041
            if port_specification.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
3042
                port_owner = port_specification._init_args[OWNER]
1✔
3043
            else:
3044
                port_owner = port_specification.owner
1✔
3045
            if owner is not None and port_owner is not None and port_owner is not owner:
1✔
3046
                try:
1✔
3047
                    new_port_specification = port_type._parse_self_port_type_spec(port_type,
1✔
3048
                                                                                     owner,
3049
                                                                                     port_specification,
3050
                                                                                     context)
3051
                    port_specification = _parse_port_spec(port_type=port_type,
1✔
3052
                                                          owner=owner,
3053
                                                          port_spec=new_port_specification,
3054
                                                          context=context)
3055
                    assert True
1✔
3056
                except AttributeError:
×
3057
                    raise PortError("Attempt to assign a {} ({}) to {} that belongs to another {} ({})".
3058
                                     format(Port.__name__, port_specification.name, owner.name,
3059
                                            Mechanism.__name__, port_owner.name))
3060
            return port_specification
1✔
3061

3062
        # Specification is a Port with which connectee can connect, so assume it is a Projection specification
3063
        elif port_specification.__class__.__name__ in port_type.connectsWith + port_type.modulators:
1✔
3064
            projection = port_type
1✔
3065

3066
        # Re-process with Projection specified
3067
        port_dict = _parse_port_spec(port_type=port_type,
1✔
3068
                                       owner=owner,
3069
                                       variable=variable,
3070
                                       value=value,
3071
                                       reference_value=reference_value,
3072
                                       params=params,
3073
                                       prefs=prefs,
3074
                                       context=context,
3075
                                       port_spec=ProjectionTuple(port=port_specification,
3076
                                                                  weight=None,
3077
                                                                  exponent=None,
3078
                                                                  projection=projection))
3079

3080
    # Projection specification (class, object, or matrix value (matrix keyword processed below):
3081
    elif _is_projection_spec(port_specification, include_matrix_spec=False):
1✔
3082

3083
        # FIX: 11/12/17 - HANDLE SITUATION IN WHICH projection_spec IS A MATRIX (AND SENDER IS SOMEHOW KNOWN)
3084
        # Parse to determine whether Projection's value is specified
3085
        projection_spec = _parse_projection_spec(port_specification, owner=owner, port_type=port_dict[PORT_TYPE])
1✔
3086

3087
        projection_value=None
1✔
3088
        sender=None
1✔
3089
        matrix=None
1✔
3090

3091
        # Projection has been instantiated
3092
        if isinstance(projection_spec, Projection):
1✔
3093
            if projection_spec.initialization_status == ContextFlags.INITIALIZED:
1!
3094
            # if projection_spec.initialization_status != ContextFlags.DEFERRED_INIT:
3095
                projection_value = projection_spec.value
×
3096
            # If deferred_init, need to get sender and matrix to determine value
3097
            else:
3098
                try:
1✔
3099
                    sender = projection_spec._init_args[SENDER]
1✔
3100
                    matrix = projection_spec._init_args[MATRIX]
1✔
3101
                except (KeyError, TypeError):
1✔
3102
                    pass
1✔
3103
        # Projection specification dict:
3104
        else:
3105
            # Need to get sender and matrix to determine value
3106
            sender = projection_spec[SENDER]
1✔
3107
            matrix = projection_spec[MATRIX]
1✔
3108

3109
        if sender is not None and matrix is not None and matrix is not AUTO_ASSIGN_MATRIX:
1✔
3110
            # Get sender of Projection to determine its value
3111
            from psyneulink.core.components.ports.outputport import OutputPort
1✔
3112
            sender = _get_port_for_socket(owner=owner,
1✔
3113
                                           connectee_port_type=port_type,
3114
                                           port_spec=sender,
3115
                                           port_types=[OutputPort])
3116
            projection_value = _get_projection_value_shape(sender, matrix)
1✔
3117

3118
        reference_value = port_dict[REFERENCE_VALUE]
1✔
3119
        # If Port's reference_value is not specified, but Projection's value is, use projection_spec's value
3120
        if reference_value is None and projection_value is not None:
1✔
3121
            port_dict[REFERENCE_VALUE] = projection_value
1✔
3122
        # If Port's reference_value has been specified, check for compatibility with projection_spec's value
3123
        elif (reference_value is not None and projection_value is not None
1✔
3124
            and not iscompatible(reference_value, projection_value)):
3125
            raise PortError("{} of {} ({}) is not compatible with {} of {} ({}) for {}".
3126
                             format(VALUE, Projection.__name__, projection_value, REFERENCE_VALUE,
3127
                                    port_dict[PORT_TYPE].__name__, reference_value, owner.name))
3128

3129
        # Move projection_spec to PROJECTIONS entry of params specification dict (for instantiation of Projection)
3130
        if port_dict[PARAMS] is None:
1!
3131
            port_dict[PARAMS] = {}
1✔
3132
        port_dict[PARAMS].update({PROJECTIONS:[port_specification]})
1✔
3133

3134
    # string (keyword or name specification)
3135
    elif isinstance(port_specification, str):
1✔
3136
        # Check if it is a keyword
3137
        spec = get_param_value_for_keyword(owner, port_specification)
1✔
3138
        # A value was returned, so use value of keyword as reference_value
3139
        if spec is not None:
1✔
3140
            port_dict[REFERENCE_VALUE] = spec
1✔
3141
            # NOTE: (7/26/17 CW) This warning below may not be appropriate, since this routine is run if the
3142
            # matrix parameter is specified as a keyword, which may be intentional.
3143
            if owner.prefs.verbosePref:
1!
3144
                print("{} not specified for {} of {};  reference value ({}) will be used".
×
3145
                      format(VARIABLE, port_type, owner.name, port_dict[REFERENCE_VALUE]))
3146
        # It is not a keyword, so treat string as the name for the port
3147
        else:
3148
            port_dict[NAME] = port_specification
1✔
3149

3150
    # # function; try to resolve to a value
3151
    # elif isinstance(Port_Specification, types.FunctionType):
3152
    #     port_dict[REFERENCE_VALUE] = get_param_value_for_function(owner, Port_Specification)
3153
    #     if port_dict[REFERENCE_VALUE] is None:
3154
    #         raise PortError("PROGRAM ERROR: port_spec for {} of {} is a function ({}), but failed to return a value".
3155
    #                          format(port_type_name, owner.name, Port_Specification))
3156

3157
    # FIX: THIS SHOULD REALLY BE PARSED IN A PORT-SPECIFIC WAY:
3158
    #      FOR InputPort: variable
3159
    #      FOR ParameterPort: default (base) parameter value
3160
    #      FOR OutputPort: index
3161
    #      FOR ModulatorySignal: default value of ModulatorySignal (e.g, allocation or gating policy)
3162
    # value, so use as variable of Port
3163
    elif is_value_spec(port_specification):
1✔
3164
        port_dict[REFERENCE_VALUE] = np.atleast_1d(port_specification)
1✔
3165

3166
    elif isinstance(port_specification, Iterable) or port_specification is None:
1✔
3167

3168
        # Standard port specification dict
3169
        # Warn if VARIABLE was not in dict
3170
        if ((VARIABLE not in port_dict or port_dict[VARIABLE] is None)
1!
3171
                and hasattr(owner, 'prefs') and owner.prefs.verbosePref):
3172
            print("{} missing from specification dict for {} of {};  "
×
3173
                  "will be inferred from context or the default ({}) will be used".
3174
                  format(VARIABLE, port_type, owner.name, port_dict))
3175

3176
        if isinstance(port_specification, (list, set)):
1✔
3177
            port_specific_specs = ProjectionTuple(port=port_specification,
1✔
3178
                                                  weight=None,
3179
                                                  exponent=None,
3180
                                                  projection=port_type)
3181

3182
        # Port specification is a tuple
3183
        elif isinstance(port_specification, tuple):
1✔
3184

3185
            # 1st item of tuple is a tuple (presumably a (Port name, Mechanism) tuple),
3186
            #    so parse to get specified Port (any projection spec should be included as 4th item of outer tuple)
3187
            if isinstance(port_specification[0],tuple):
1✔
3188
                proj_spec = _parse_connection_specs(connectee_port_type=port_type,
1✔
3189
                                                    owner=owner,
3190
                                                    connections=port_specification[0])
3191
                port_specification = (proj_spec[0].port,) + port_specification[1:]
1✔
3192

3193
            # Reassign tuple for handling by _parse_port_specific_specs
3194
            port_specific_specs = port_specification
1✔
3195

3196
        # Otherwise, just pass params to Port subclass
3197
        else:
3198
            port_specific_specs = params
1✔
3199

3200
        if port_specific_specs:
1✔
3201
            port_spec, params = port_type._parse_port_specific_specs(port_type,
1✔
3202
                                                                     owner=owner,
3203
                                                                     port_dict=port_dict,
3204
                                                                     port_specific_spec = port_specific_specs,
3205
                                                                     context=context)
3206
            # Port subclass returned a port_spec, so call _parse_port_spec to parse it
3207
            if port_spec is not None:
1✔
3208
                port_dict = _parse_port_spec(context=context, port_spec=port_spec, **standard_args)
1✔
3209

3210
            # Move PROJECTIONS entry to params
3211
            if PROJECTIONS in port_dict:
1!
3212
                if not isinstance(port_dict[PROJECTIONS], list):
×
3213
                    port_dict[PROJECTIONS] = [port_dict[PROJECTIONS]]
×
3214
                params[PROJECTIONS].append(port_dict[PROJECTIONS])
×
3215

3216
            # MECHANISM entry specifies Mechanism; <PORTS> entry has names of its Ports
3217
            #           MECHANISM: <Mechanism>, <PORTS>:[<Port.name>, ...]}
3218
            if MECHANISM in port_specific_args:
1✔
3219

3220
                if PROJECTIONS not in params:
1!
3221
                    if NAME in spec:
1✔
3222
                        # substitute into tuple spec
3223
                        params[PROJECTIONS] = (spec[NAME], params[MECHANISM])
1✔
3224
                    else:
3225
                        params[PROJECTIONS] = []
1✔
3226

3227
                mech = port_specific_args[MECHANISM]
1✔
3228
                if not isinstance(mech, Mechanism):
1!
3229
                    entry_name = ''
×
3230
                    from psyneulink.core.components.component import ParameterValue
×
3231
                    if hasattr(mech, 'name'):
×
3232
                        entry_name = f" ('{mech.name}')"
×
3233
                    elif isinstance(mech, ParameterValue):
×
3234
                        entry_name = f" ('{mech._parameter.name}' of '{mech._owner.name}')"
×
3235
                    raise PortError(f"The type of the {MECHANISM} entry{entry_name} in the specification dictionary "
3236
                                    f"for {port_type.__name__} of '{owner.name}' is {type(mech).__name__};  "
3237
                                    f"it must be a {Mechanism.__name__}.")
3238

3239
                # For Ports with which the one being specified can connect:
3240
                for PORTS in port_type.connectsWithAttribute:
1✔
3241

3242
                    if PORTS in port_specific_args:
1✔
3243
                        port_specs = port_specific_args[PORTS]
1✔
3244
                        port_specs = port_specs if isinstance(port_specs, list) else [port_specs]
1✔
3245
                        for port_spec in port_specs:
1✔
3246
                            # If Port is a tuple, get its first item as port
3247
                            port = port_spec[0] if isinstance(port_spec, tuple) else port_spec
1✔
3248
                            try:
1✔
3249
                                port_attr = getattr(mech, PORTS)
1✔
3250
                                port = port_attr[port]
1✔
3251
                            except:
×
3252
                                name = owner.name if 'unnamed' not in owner.name else 'a ' + owner.__class__.__name__
×
3253
                                raise PortError("Unrecognized name ({port}) for {PORTS} of {mech.name} "
3254
                                                "in specification of {port_type.__name__} for {name}.")
3255
                            # If port_spec was a tuple, put port back in as its first item and use as projection spec
3256
                            if isinstance(port_spec, tuple):
1!
3257
                                port = (port,) + port_spec[1:]
×
3258
                            params[PROJECTIONS].append(port)
1✔
3259
                        # Delete <PORTS> entry as it is not a parameter of a Port
3260
                        del port_specific_args[PORTS]
1✔
3261

3262
                # Delete MECHANISM entry as it is not a parameter of a Port
3263
                del port_specific_args[MECHANISM]
1✔
3264

3265
            # FIX: 11/4/17 - MAY STILL NEED WORK:
3266
            # FIX:   PROJECTIONS FROM UNRECOGNIZED KEY ENTRY MAY BE REDUNDANT OR CONFLICT WITH ONE ALREADY IN PARAMS
3267
            # FIX:   NEEDS TO BE BETTER COORDINATED WITH _parse_port_specific_specs
3268
            # FIX:   REGARDING WHAT IS IN port_specific_args VS params (see REF_VAL_NAME BRANCH)
3269
            # FIX:   ALSO, ??DOES PROJECTIONS ENTRY BELONG IN param OR port_dict?
3270
            # Check for single unrecognized key in params, used for {<Port_Name>:[<projection_spec>,...]} format
3271
            unrecognized_keys = [key for key in port_specific_args if key not in port_type.portAttributes]
1✔
3272
            if unrecognized_keys:
1✔
3273
                if len(unrecognized_keys)==1:
1✔
3274
                    key = unrecognized_keys[0]
1✔
3275
                    port_dict[NAME] = key
1✔
3276
                    # KDM 12/24/19: in some cases, params[PROJECTIONS] is
3277
                    # already parsed into a ProjectionTuple, and this assignment
3278
                    # will replace it with an unparsed ndarray which causes a
3279
                    # ProjectionError in _parse_connection_specs
3280
                    if (
1✔
3281
                        PROJECTIONS not in params
3282
                        or not any([
3283
                            isinstance(x, ProjectionTuple)
3284
                            for x in params[PROJECTIONS]
3285
                        ])
3286
                    ):
3287
                        params[PROJECTIONS] = port_specific_args[key]
1✔
3288
                        del port_specific_args[key]
1✔
3289
                else:
3290
                    raise PortError(f"There is more than one entry of the {port_type.__name__} "
3291
                                    f"specification dictionary for {owner.name} "
3292
                                    f"({', '.join([s for s in list(port_specific_args.keys())])}) "
3293
                                    f"that is not a keyword; there should be only one (used to name the Port, "
3294
                                    f"with a list of Projection specifications.")
3295

3296
            for param in port_type.portAttributes:
1✔
3297
                # KDM 12/24/19: below is meant to skip overwriting an already
3298
                # parsed ProjectionTuple just as in the section above
3299
                if param in port_specific_args:
1✔
3300
                    try:
1✔
3301
                        param_value_is_tuple = any([
1✔
3302
                            isinstance(x, ProjectionTuple)
3303
                            for x in params[param]
3304
                        ])
3305
                    except TypeError:
1✔
3306
                        param_value_is_tuple = isinstance(
1✔
3307
                            params[param],
3308
                            ProjectionTuple
3309
                        )
3310
                    except KeyError:
1✔
3311
                        # param may not be in port_specific_args, and in this
3312
                        # case use the default/previous behavior
3313
                        param_value_is_tuple = False
1✔
3314

3315
                    if param not in params or not param_value_is_tuple:
1✔
3316
                        params[param] = port_specific_args[param]
1✔
3317

3318
            if PROJECTIONS in params and params[PROJECTIONS] is not None:
1✔
3319
                #       (E.G., WEIGHTS AND EXPONENTS FOR InputPort AND INDEX FOR OutputPort)
3320
                # Get and parse projection specifications for the Port
3321
                params[PROJECTIONS] = _parse_connection_specs(port_type, owner, params[PROJECTIONS])
1✔
3322

3323
            # Update port_dict[PARAMS] with params
3324
            if port_dict[PARAMS] is None:
1✔
3325
                port_dict[PARAMS] = {}
1✔
3326
            port_dict[PARAMS].update(params)
1✔
3327

3328
    else:
3329
        # if owner.verbosePref:
3330
        #     warnings.warn("PROGRAM ERROR: port_spec for {} of {} is an unrecognized specification ({})".
3331
        #                  format(port_type_name, owner.name, port_spec))
3332
        # return
3333
        raise PortError("PROGRAM ERROR: port_spec for {} of {} is an unrecognized specification ({})".
3334
                         format(port_type_name, owner.name, port_specification))
3335

3336
    # If variable is none, use value:
3337
    if port_dict[VARIABLE] is None:
1✔
3338
        if port_dict[VALUE] is not None:
1✔
3339
            # TODO: be careful here - if the port spec has a function that
3340
            # changes the shape of its variable, this will be incorrect
3341
            port_dict[VARIABLE] = port_dict[VALUE]
1✔
3342
        else:
3343
            port_dict[VARIABLE] = port_dict[REFERENCE_VALUE]
1✔
3344

3345
    if is_numeric(port_dict[VARIABLE]):
1✔
3346
        port_dict[VARIABLE] = convert_all_elements_to_np_array(port_dict[VARIABLE])
1✔
3347

3348
    # get the Port's value from the spec function if it exists,
3349
    # otherwise we can assume there is a default function that does not
3350
    # affect the shape, so it matches variable
3351
    # FIX: JDC 2/21/18 PROBLEM IS THAT, IF IT IS AN InputPort, THEN EITHER _update MUST BE CALLED
3352
    # FIX:    OR VARIABLE MUST BE WRAPPED IN A LIST, ELSE LINEAR COMB MAY TREAT A 2D ARRAY
3353
    # FIX:    AS TWO ITEMS TO BE COMBINED RATHER THAN AS A 2D ARRAY
3354
    # KDM 6/7/18: below this can end up assigning to the port a variable of the same shape as a default function
3355
    #   (because when calling the function, _check_args is called and if given None, will fall back to instance or
3356
    #   class defaults)
3357
    try:
1✔
3358
        spec_function = port_dict[PARAMS][FUNCTION]
1✔
3359
        # if isinstance(spec_function, Function):
3360
        if isinstance(spec_function, (Function, types.FunctionType, types.MethodType)):
1✔
3361
            spec_function_value = port_type._get_port_function_value(owner, spec_function, port_dict[VARIABLE])
1✔
3362
        elif inspect.isclass(spec_function) and issubclass(spec_function, Function):
1✔
3363
            try:
1✔
3364
                spec_function = spec_function(**port_dict[PARAMS][FUNCTION_PARAMS])
1✔
3365
            except (KeyError, TypeError):
1✔
3366
                spec_function = spec_function()
1✔
3367
            spec_function_value = port_type._get_port_function_value(owner, spec_function, port_dict[VARIABLE])
1✔
3368
        else:
3369
            raise PortError('port_spec value for FUNCTION ({0}) must be a function, method, '
3370
                             'Function class or instance of one'.
3371
                             format(spec_function))
3372
    except (KeyError, TypeError):
1✔
3373
        spec_function_value = port_type._get_port_function_value(owner, None, port_dict[VARIABLE])
1✔
3374
        spec_function = port_type.class_defaults.function
1✔
3375

3376

3377
    # Assign value based on variable if not specified
3378
    if port_dict[VALUE] is None:
1✔
3379
        port_dict[VALUE] = spec_function_value
1✔
3380
    # Otherwise, make sure value returned by spec function is same as one specified for Port's value
3381
    # else:
3382
    #     if not np.asarray(port_dict[VALUE]).shape == np.asarray(spec_function_value).shape:
3383
    #         port_Name = port_dict[NAME] or 'unnamed'
3384
    #         raise PortError('port_spec value ({}) specified for {} {} of {} is not compatible with '
3385
    #                          'the value ({}) computed from the port_spec function ({})'.
3386
    #                          format(port_dict[VALUE], port_Name, port_type.__name__,
3387
    #                                 port_dict[OWNER].name, spec_function_value, spec_function))
3388

3389
    if port_dict[REFERENCE_VALUE] is not None and not iscompatible(port_dict[VALUE], port_dict[REFERENCE_VALUE]):
1✔
3390
        port_name = f"the {port_dict[NAME]}" if (NAME in port_dict and port_dict[NAME]) else f"a"
1✔
3391
        raise PortError(f"The value ({port_dict[VALUE]}) for {port_name} {port_type.__name__} of "
3392
                        f"{owner.name} does not match the reference_value ({port_dict[REFERENCE_VALUE]}) "
3393
                        f"used for it at construction.")
3394

3395
    if proj_is_feedback:
1!
3396
        port_dict['params']['projections'][0].projection[FEEDBACK]=True
×
3397

3398
    return port_dict
1✔
3399

3400

3401
# FIX: REPLACE mech_port_attribute WITH DETERMINATION FROM port_type
3402
# FIX:          ONCE PORT CONNECTION CHARACTERISTICS HAVE BEEN IMPLEMENTED IN REGISTRY
3403
@beartype
1✔
3404
def _get_port_for_socket(owner,
1✔
3405
                         connectee_port_type: Optional[Type[Port]] = None,
3406
                         port_spec=None,
3407
                         port_types: Optional[Union[list, Type[Port]]] = None,
3408
                         mech: Optional[Mechanism] = None,
3409
                         mech_port_attribute: Optional[Union[str, list]] = None,
3410
                         projection_socket: Optional[Union[str, set]] = None):
3411
    """Take some combination of Mechanism, port name (string), Projection, and projection_socket, and return
3412
    specified Port(s)
3413

3414
    If port_spec is:
3415
        Port name (str), then *mech* and *mech_port_attribute* args must be specified
3416
        Mechanism, then *port_type* must be specified; primary Port is returned
3417
        Projection, *projection_socket* arg must be specified;
3418
                    Projection must be instantiated or in deferred_init, with projection_socket attribute assigned
3419

3420
    IMPLEMENTATION NOTES:
3421
    Currently does not support Port specification dict (referenced Port must be instantiated)
3422
    Currently does not support Projection specification using class or Projection specification dict
3423
        (Projection must be instantiated, or in deferred_init status with projection_socket assigned)
3424

3425
    Returns a Port if it can be resolved, or list of allowed Port types if not.
3426
    """
3427
    from psyneulink.core.components.projections.projection import \
1✔
3428
        _is_projection_spec, _validate_connection_request, _parse_projection_spec
3429
    from psyneulink.core.globals.utilities import is_matrix
1✔
3430

3431
    # # If the mech_port_attribute specified has more than one item, get the primary one
3432
    # if isinstance(mech_port_attribute, list):
3433
    #     mech_port_attribute = mech_port_attribute[0]
3434

3435
    # port_types should be a list, and port_type its first (or only) item
3436
    if isinstance(port_types, list):
1✔
3437
        port_type = port_types[0]
1✔
3438
    else:
3439
        port_type = port_types
1✔
3440
        port_types = [port_types]
1✔
3441

3442
    port_type_names = ", ".join([s.__name__ for s in port_types])
1✔
3443

3444
    # Return Port itself if it is an instantiated Port
3445
    if isinstance(port_spec, Port):
1✔
3446
        return port_spec
1✔
3447

3448
    # Return port_type (Class) if port_spec is:
3449
    #    - an allowable Port type for the projection_socket
3450
    #    - a projection keyword (e.g., 'LEARNING' or 'CONTROL', and it is consistent with projection_socket
3451
    # Otherwise, return list of allowable Port types for projection_socket (if port_spec is a Projection type)
3452
    if _is_projection_spec(port_spec):
1✔
3453

3454
        # These specifications require that a particular Port be specified to assign its default Projection type
3455
        if ((is_matrix(port_spec) or (isinstance(port_spec, dict) and PROJECTION_TYPE not in port_spec))):
1!
3456
            for st in port_types:
×
3457
                try:
×
3458
                    proj_spec = _parse_projection_spec(port_spec, owner=owner, port_type=st)
×
3459
                    if isinstance(proj_spec, Projection):
×
3460
                        proj_type = proj_spec.__class__
×
3461
                    else:
3462
                        proj_type = proj_spec[PROJECTION_TYPE]
×
3463
                except:
×
3464
                    continue
×
3465
        else:
3466
            proj_spec = _parse_projection_spec(port_spec, owner=owner, port_type=port_type)
1✔
3467
            if isinstance(proj_spec, Projection):
1✔
3468
                proj_type = proj_spec.__class__
1✔
3469
            else:
3470
                proj_type = proj_spec[PROJECTION_TYPE]
1✔
3471

3472
        # Get Port type if it is appropriate for the specified socket of the Projection's type
3473
        s = next((s for s in port_types if
1✔
3474
                  s.__name__ in getattr(proj_type.sockets, projection_socket)),
3475
                 None)
3476
        # If there is a port_type for the projection_socket, try to get the actual Port and return it;
3477
        #   otherwise return first in the list of allowable Port types for that socket
3478
        if s:
1!
3479
            try:
1✔
3480
                # Return Port associated with projection_socket if proj_spec is an actual Projection
3481
                if proj_spec.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
3482
                    port = proj_spec._init_args[projection_socket]
1✔
3483
                    if port is None:
1✔
3484
                        raise AttributeError
3485
                    elif isinstance(port, Mechanism):
1✔
3486
                        # Mechanism specifiea as sender or receiver of Projection, so get corresponding primary Port
3487
                        port = port.output_port
1✔
3488
                    return port
1✔
3489
                else:
3490
                    port = getattr(proj_spec, projection_socket)
1✔
3491
                    return port
1✔
3492
            except AttributeError:
1✔
3493
                # Otherwise, return first port_type (s)
3494
                return s
1✔
3495

3496
        # FIX: 10/3/17 - ??IS THE FOLLOWING NECESSARY?  ??HOW IS IT DIFFERENT FROM ABOVE?
3497
        # Otherwise, get Port types that are allowable for that projection_socket
3498
        elif inspect.isclass(proj_type) and issubclass(proj_type, Projection):
×
3499
            projection_socket_port_Names = getattr(proj_type.sockets, projection_socket)
×
3500
            projection_socket_port_types = [PortRegistry[name].subclass for name in projection_socket_port_Names]
×
3501
            return projection_socket_port_types
×
3502
        else:
3503
            assert False
3504
            # return port_type
3505

3506
    # Get port by name
3507
    if isinstance(port_spec, str):
1✔
3508

3509
        if mech is None:
1✔
3510
            raise PortError("PROGRAM ERROR: A {} must be specified to specify its {} ({}) by name".
3511
                             format(Mechanism.__name__, Port.__name__, port_spec))
3512
        if mech_port_attribute is None:
1✔
3513
            raise PortError("PROGRAM ERROR: The attribute of {} that holds the requested Port ({}) must be specified".
3514
                             format(mech.name, port_spec))
3515
        for attr in mech_port_attribute:
1!
3516
            try:
1✔
3517
                portListAttribute = getattr(mech, attr)
1✔
3518
                port = portListAttribute[port_spec]
1✔
3519
            except AttributeError:
1✔
3520
                portListAttribute = None
×
3521
            except (KeyError, TypeError):
1✔
3522
                port = None
1✔
3523
            else:
3524
                break
1✔
3525
        if portListAttribute is None:
1✔
3526
            raise PortError("PROGRAM ERROR: {} attribute(s) not found on {}'s type ({})".
3527
                             format(mech_port_attribute, mech.name, mech.__class__.__name__))
3528
        if port is None:
1!
3529
            if len(mech_port_attribute)==1:
×
3530
                attr_name = mech_port_attribute[0] + " attribute"
×
3531
            else:
3532
                attr_name = " or ".join(f"{repr(attr)}" for (attr) in mech_port_attribute) + " attributes"
×
3533
            raise PortError(f"{mech.name} does not have a {Port.__name__} named \'{port_spec}\' in its {attr_name}.")
3534

3535
    # Get primary Port of specified type
3536
    elif isinstance(port_spec, Mechanism):
1!
3537

3538
        if port_type is None:
1✔
3539
            raise PortError("PROGRAM ERROR: The type of Port requested for {} must be specified "
3540
                             "to get its primary Port".format(port_spec.name))
3541
        try:
1✔
3542
            port = port_type._get_primary_port(port_type, port_spec)
1✔
3543
            # Primary Port for Mechanism specified in port_spec is not compatible
3544
            # with owner's Port for which a connection is being specified
3545
            if port.__class__.__name__ not in connectee_port_type.connectsWith:
1!
3546
                from psyneulink.core.components.projections.projection import ProjectionError
×
3547
                raise ProjectionError(f"Primary {port_type.__name__} of {port_spec.name} ({port.name}) cannot be "
3548
                                      f"used "
3549
                                      f"as a {projection_socket} of a {Projection.__name__} "
3550
                                      f"{PROJECTION_DIRECTION[projection_socket]} {connectee_port_type.__name__} of "
3551
                                      f"{owner.name}")
3552
        except PortError:
×
3553
            if mech_port_attribute:
×
3554
                try:
×
3555
                    port = getattr(port_spec, mech_port_attribute)[0]
×
3556
                except:
×
3557
                    raise PortError("{} does not seem to have an {} attribute"
3558
                                     .format(port_spec.name, mech_port_attribute))
3559
            for attr in mech_port_attribute:
×
3560
                try:
×
3561
                    port = getattr(port_spec, attr)[0]
×
3562
                except :
×
3563
                    port = None
×
3564
                else:
3565
                    break
×
3566
                if port is None:
×
3567
                    raise PortError("PROGRAM ERROR: {} attribute(s) not found on {}'s type ({})".
3568
                                     format(mech_port_attribute, mech.name, mech.__class__.__name__))
3569

3570
    # Get port from Projection specification (exclude matrix spec in test as it can't be used to determine the port)
3571
    elif _is_projection_spec(port_spec, include_matrix_spec=False):
×
3572
        _validate_connection_request(owner=owner,
×
3573
                                     connect_with_ports=port_type,
3574
                                     projection_spec=port_spec,
3575
                                     projection_socket=projection_socket)
3576
        if isinstance(port_spec, Projection):
×
3577
            port = port_spec.socket_assignments[projection_socket]
×
3578
            if port is None:
×
3579
                port = port_type
×
3580
        else:
3581
            return port_spec
×
3582

3583
    else:
3584
        if port_spec is None:
×
3585
            raise PortError("PROGRAM ERROR: Missing port specification for {}".format(owner.name))
3586
        else:
3587
            raise PortError("Unrecognized port specification: {} for {}".format(port_spec, owner.name))
3588

3589
    return port
1✔
3590

3591

3592
def _is_legal_port_spec_tuple(owner, port_spec, port_type_name=None):
1✔
3593

3594
    from psyneulink.core.components.projections.projection import _is_projection_spec
×
3595

3596
    port_type_name = port_type_name or PORT
×
3597

3598
    if len(port_spec) != 2:
×
3599
        raise PortError("Tuple provided as port_spec for {} of {} ({}) must have exactly two items".
3600
                         format(port_type_name, owner.name, port_spec))
3601
    if not (_is_projection_spec(port_spec[1]) or
×
3602
                # IMPLEMENTATION NOTE: Mechanism or Port allowed as 2nd item of tuple or
3603
                #                      string (parameter name) as 1st and Mechanism as 2nd
3604
                #                      to accommodate specification of param for ControlSignal
3605
                isinstance(port_spec[1], (Mechanism, Port))
3606
                           or (isinstance(port_spec[0], Mechanism) and
3607
                                       port_spec[1] in port_spec[0]._parameter_ports)):
3608
        raise PortError("2nd item of tuple in port_spec for {} of {} ({}) must be a specification "
3609
                         "for a Mechanism, Port, or Projection".
3610
                         format(port_type_name, owner.__class__.__name__, port_spec[1]))
3611

3612

3613
def _merge_param_dicts(source, specific, general, remove_specific=True, remove_general=False):
1✔
3614
    """Search source dict for specific and general dicts, merge specific with general, and return merged
3615

3616
    Used to merge a subset of dicts in runtime_params (that may have several dicts);
3617
        for example: only the MAPPING_PROJECTION_PARAMS (specific) with PROJECTION_PARAMS (general)
3618
    Allows dicts to be referenced by name (e.g., paramName) rather than by object
3619
    Searches source dict for specific and general dicts
3620
    - if both are found, merges them, with entries from specific overwriting any duplicates in general
3621
    - if only one is found, returns just that dict
3622
    - if neither are found, returns empty dict
3623
    remove_specific and remove_general args specify whether to remove those from source;
3624
        if specific and/or general are specified by name (i.e., as keys in source the value of which are subdicts),
3625
        then the entyr with the subdict is removed from source;  if they are specified as dicts,
3626
        then the corresponding entires  are removed from source.
3627

3628
    Arguments
3629
    _________
3630

3631
    source : dict
3632
        container dict (entries are dicts); search entries for specific and general dicts
3633

3634
    specific : dict or str)
3635
        if str, use as key to look for specific dict in source, and check that it is a dict
3636

3637
    general : dict or str
3638
        if str, use as key to look for general dict in source, and check that it is a dict
3639

3640
    Returns
3641
    -------
3642

3643
    merged: dict
3644
    """
3645

3646
    # Validate source as dict
3647
    if not source:
×
3648
        return {}
×
3649
    if not isinstance(source, dict):
×
3650
        raise PortError("merge_param_dicts: source {0} must be a dict".format(source))
3651

3652
    # Get specific and make sure it is a dict
3653
    if isinstance(specific, str):
×
3654
        try:
×
3655
            specific_name = specific
×
3656
            specific = source[specific]
×
3657
        except (KeyError, TypeError):
×
3658
            specific = {}
×
3659
    if not isinstance(specific, dict):
×
3660
        raise PortError("merge_param_dicts: specific {specific} must be dict or the name of one in {source}.")
3661

3662
    # Get general and make sure it is a dict
3663
    if isinstance(general, str):
×
3664
        try:
×
3665
            general_name = general
×
3666
            general = source[general]
×
3667
        except (KeyError, TypeError):
×
3668
            general = {}
×
3669
    if not isinstance(general, dict):
×
3670
        raise PortError("merge_param_dicts: general {general} must be dict or the name of one in {source}.")
3671

3672
    general.update(specific)
×
3673

3674
    if remove_specific:
×
3675
        try:
×
3676
            source.pop(specific_name, None)
×
3677
        except ValueError:
×
3678
            for entry in specific:
×
3679
                source.pop(entry, None)
×
3680
    if remove_general:
×
3681
        try:
×
3682
            source.pop(general_name, None)
×
3683
        except ValueError:
×
3684
            for entry in general:
×
3685
                source.pop(entry, None)
×
3686

3687
    return general
×
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