• 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

69.33
/psyneulink/core/components/projections/projection.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
# **********************************************  Projection ***********************************************************
10

11
"""
12

13
Contents
14
--------
15
  * `Projection_Overview`
16
  * `Projection_Creation`
17
  * `Projection_Structure`
18
      - `Projection_Sender`
19
      - `Projection_Receiver`
20
  * `Projection_Execution`
21
  * `Projection_Class_Reference`
22

23
.. _Projection_Overview:
24

25
Overview
26
--------
27

28
Projections allow information to be passed between `Mechanisms <Mechanism>`.  A Projection takes its input from
29
its `sender <Projection_Base.sender>` and transmits that information to its `receiver <Projection_Base.receiver>`.  The
30
`sender <Projection_Base.sender>` and `receiver <Projection_Base.receiver>` of a Projection are always `Ports <Port>`:
31
the `sender <Projection_Base.sender>` is always the `OutputPort` of a `Mechanism <Mechanism>`; the `receiver
32
<Projection_Base.receiver>` depends upon the type of Projection.  There are two broad categories of Projections,
33
each of which has subtypes that differ in the type of information they transmit, how they do this, and the type of
34
`Port <Port>` to which they project (i.e., of their `receiver <Projection_Base.receiver>`):
35

36
* `PathwayProjection <PathwayProjection>`
37
    Used in conjunction with `ProcessingMechanisms <ProcessingMechanism>` to convey information along a processing
38
    `pathway <Process.pathway>`.  There is currently one on type of PathwayProjection:
39

40
  * `MappingProjection`
41
      takes the `value <OutputPort.value>` of an `OutputPort` of a `ProcessingMechanism <ProcessingMechanism>`
42
      converts it by convolving it with the MappingProjection's `matrix <MappingProjection.matrix>`
43
      parameter, and transmits the result to the `InputPort` of another ProcessingMechanism.  Typically,
44
      MappingProjections are used to connect Mechanisms in the `pathway` of a `Process`, though they can be use for
45
      other purposes as well (for example, to convey the output of an `ObjectiveMechanism` to a `ModulatoryMechanism
46
      <ModulatoryMechanism>`).
47

48
* `ModulatoryProjection <ModulatoryProjection>`
49
    takes the `value <OutputPort.value>` of a `ModulatorySignal <ModulatorySignal>` of a `ModulatoryMechanism
50
    <ProcessingMechanism>`, uses it to regulate modify the `value <Port_Base.value>` of an `InputPort`,
51
    `ParameterPort` or `OutputPort` of another Component.  ModulatorySignals are specialized types of `OutputPort`,
52
    that are used to specify how to modify the `value <Port_Base.value>` of the `Port <Port>` to which a
53
    ModulatoryProjection projects. There are three types of ModulatoryProjections, corresponding to the three types
54
    of ModulatoryMechanisms (and corresponding ModulatorySignals; see `figure <ModulatorySignal_Anatomy_Figure>`),
55
    that project to different types of `Ports <Port>`:
56

57
  * `LearningProjection`
58
      takes the `value <LearningSignal.value>` of a `LearningSignal` of a `LearningMechanism`, and transmits this
59
      to the `ParameterPort` of a `MappingProjection` that uses it to modify its `matrix <MappingProjection.matrix>`
60
      parameter. LearningProjections are used in the `learning Pathway(s) <Composition_Learning_Pathway>` of a
61
      `Composition`.
62
  ..
63
  * `ControlProjection`
64
      takes the `value <ControlSignal.value>` of a `ControlSignal` of a `ControlMechanism <ControlMechanism>`, and
65
      transmit this to the `ParameterPort of a `ProcessingMechanism <ProcessingMechanism>` that uses it to modify
66
      the parameter of the `Mechanism <Mechanism>` (or its `function <Mechanism_Base.function>`) for which it is
67
      responsible.
68
      COMMENT:
69
      ControlProjections are used in the `control Pathway(s) <Composition_Control_Pathway>` of a `Composition`.
70
      COMMENT
71
  ..
72
  * `GatingProjection`
73
      takes the `value <GatingSignal.value>` of a `GatingSignal` of a `GatingMechanism`, and transmits this to
74
      the `InputPort` or `OutputPort` of a `ProcessingMechanism <ProcessingMechanism>` that uses this to modify the
75
      Port's `value <Port_Base.value>`.  GatingProjections are a special subclass of ControlProjections.
76

77
.. _Projection_Creation:
78

79
Creating a Projection
80
---------------------
81

82
A Projection can be created on its own, by calling the constructor for the desired type of Projection.  More
83
commonly, however, Projections are either specified `in context <Projection_Specification>`, or are `created
84
automatically <Projection_Automatic_Creation>`, as described below.
85

86

87
.. _Projection_Specification:
88

89
*Specifying a Projection*
90
~~~~~~~~~~~~~~~~~~~~~~~~~
91

92
Projections can be specified in a number of places where they are required or permitted, for example in the
93
specification of a `pathway <Process.pathway>` for a `Process`, where the value of a parameter is specified
94
(e.g., to assign a `ControlProjection`) or where a `MappingProjection` is specified  (to assign it a
95
`LearningProjection <MappingProjection_Tuple_Specification>`).  Any of the following can be used to specify a
96
Projection in context:
97

98
  * **Constructor** -- used the same way in context as it is ordinarily.
99
  ..
100
  * **Projection object** -- must be a reference to a Projection that has already been created.
101
  ..
102
  * **Projection subclass** -- creates a default instance of the specified Projection type.  The assignment or creation
103
    of the Projection's `sender <Projection_Base.sender>` is handled in the same manner as described below for keyword
104
    specifications.
105
  ..
106
  * **Keyword** -- creates a default instance of the specified type, which can be any of the following:
107

108
      * *MAPPING_PROJECTION* -- if the `sender <MappingProjection.sender>` and/or its `receiver
109
        <MappingProjection.receiver>` cannot be inferred from the context in which this specification occurs, then
110
        its `initialization is deferred <MappingProjection_Deferred_Initialization>` until both of those have been
111
        determined (e.g., it is used in the specification of a `Pathway` for a `Composition`). For MappingProjections,
112
        a `matrix specification <MappingProjection_Matrix_Specification>` can also be used to specify the Projection
113
        (see **value** below).
114

115
      COMMENT:
116
      * *LEARNING_PROJECTION*  (or *LEARNING*) -- this can only be used in the specification of a `MappingProjection`
117
        (see `tuple <MappingProjection_Matrix_Specification>` format).  If the `receiver <MappingProjection.receiver>`
118
        of the MappingProjection projects to a `LearningMechanism` or a `ComparatorMechanism` that projects to one,
119
        then a `LearningSignal` is added to that LearningMechanism and assigned as the LearningProjection's `sender
120
        <LearningProjection.sender>`;  otherwise, a LearningMechanism is `automatically created
121
        <LearningMechanism_Creation>`, along with a LearningSignal that is assigned as the LearningProjection's `sender
122
        <LearningProjection.sender>`. See `LearningMechanism_Learning_Configurations` for additional details.
123
      COMMENT
124

125
      COMMENT:
126
      # FIX 5/8/20 [JDC] ELIMINATE SYSTEM:  IS IT TRUE THAT CONTROL SIGNALS ARE AUTOMATICALLY CREATED BY COMPOSITIONS?
127
      COMMENT
128
      * *CONTROL_PROJECTION* (or *CONTROL*) -- this can be used when specifying a parameter using the `tuple format
129
        <ParameterPort_Tuple_Specification>`, to create a default `ControlProjection` to the `ParameterPort` for that
130
        parameter.  If the `Component <Component>` to which the parameter belongs is part of a `Composition`, then a
131
        `ControlSignal` is added to the Composition's `controller <Composition.controller>` and assigned as the
132
        ControlProjection's `sender <ControlProjection.sender>`;  otherwise, the ControlProjection's `initialization
133
        is deferred <ControlProjection_Deferred_Initialization>` until the Mechanism is assigned to a Composition, at
134
        which time the ControlSignal is added to the Composition's `controller <Composition.controller>` and assigned
135
        as its the ControlProjection's `sender <ControlProjection.sender>`.  See `ControlMechanism_ControlSignals` for
136
        additional details.
137

138
      * *GATING_PROJECTION* (or *GATE*) -- this can be used when specifying an `InputPort
139
        <InputPort_Projection_Source_Specification>` or an `OutputPort <OutputPort_Projections>`, to create a
140
        default `GatingProjection` to the `Port <Port>`. If the GatingProjection's `sender <GatingProjection.sender>`
141
        cannot be inferred from the context in which this specification occurs, then its `initialization is deferred
142
        <GatingProjection_Deferred_Initialization>` until it can be determined (e.g., a `GatingMechanism` or
143
        `GatingSignal` is created to which it is assigned).
144
  ..
145
  * **value** -- creates a Projection of a type determined by the context of the specification, and using the
146
    specified value as the `value <Projection_Base.value>` of the Projection, which must be compatible with the
147
    `variable <Port_Base.variable>` attribute of its `receiver <Projection_Base.receiver>`.  If the Projection is a
148
    `MappingProjection`, the value is interpreted as a `matrix specification <MappingProjection_Matrix_Specification>`
149
    and assigned as the `matrix <MappingProjection.matrix>` parameter of the Projection;  it must be compatible with the
150
    `value <Port_Base.value>` attribute of its `sender <MappingProjection.sender>` and `variable <Port_Base.variable>`
151
    attribute of its `receiver <MappingProjection.receiver>`.
152
  ..
153
  * **Mechanism** -- creates a `MappingProjection` to either the `primary InputPort <InputPort_Primary>` or
154
    `primary OutputPort <OutputPort_Primary>`, depending on the type of Mechanism and context of the specification.
155
  ..
156
  * **Port** -- creates a `Projection` to or from the specified `Port`, depending on the type of Port and the
157
    context of the specification.
158

159
  .. _Projection_Specification_Dictionary:
160

161
  * **Projection specification dictionary** -- can contain an entry specifying the type of Projection, and/or entries
162
    specifying the value of parameters used to instantiate it. These should take the following form:
163

164
      * *PROJECTION_TYPE*: *<name of a Projection type>* --
165
        if this entry is absent, a default Projection will be created that is appropriate for the context
166
        (for example, a `MappingProjection` for an `InputPort`, a `LearningProjection` for the `matrix
167
        <MappingProjection.matrix>` parameter of a `MappingProjection`, and a `ControlProjection` for any other
168
        type of parameter.
169

170
      * *PROJECTION_PARAMS*: *Dict[Projection argument, argument value]* --
171
        the key for each entry of the dictionary must be the name of a Projection parameter, and its value the value
172
        of the parameter.  It can contain any of the standard parameters for instantiating a Projection (in particular
173
        its `sender <Projection_Sender>` and `receiver <Projection_Receiver>`, or ones specific to a particular type
174
        of Projection (see documentation for subclass).  If the `sender <Projection_Sender>` and/or
175
        `receiver <Projection_Receiver>` are not specified, their assignment and/or creation are handled in the same
176
        manner as described above for keyword specifications.
177

178
      COMMENT:
179
          WHAT ABOUT SPECIFICATION USING OutputPort/ModulatorySignal OR Mechanism? OR Matrix OR Matrix keyword
180
      COMMENT
181

182
      COMMENT:  ??IMPLEMENTED FOR PROJECTION PARAMS??
183
        Note that parameter
184
        values in the specification dictionary will be used to instantiate the Projection.  These can be overridden
185
        during execution by specifying `runtime parameters <Mechanism_Runtime_Params>` for the Projection,
186
        either when calling the `execute <Mechanism_Base.execute>` or `run <Mechanism_Base.run>`
187
        method for a Mechanism directly, or where it is specified in the `pathway` of a Process.
188
      COMMENT
189

190
  .. _Projection_ProjectionTuple:
191

192
  * **ProjectionTuple** -- a 4-item tuple used in the context of a `Port specification <Port_Specification>` to
193
    create a Projection between it and another `Port <Port>`. It must have at least the first three of the following
194
    items in order, and can include the fourth optional item:
195

196
     * **Port specification** -- specifies the `Port <Port_Specification>` to connect with (**not** the one being
197
       connected; that is determined from context)
198

199
     * **weight** -- must be a value specifying the `weight <Projection_Base.weight>` of the Projection;  it can be
200
       `None`, in which case it is ignored, but there must be a specification present;
201

202
     * **exponent** -- must be a value specifying the `exponent <Projection_Base.exponent>` of the Projection;  it
203
       can be `None`, in which case it is ignored, but there must be a specification present;
204

205
     * **Projection specification** -- this is optional but, if included, msut be a `Projection specification
206
       <Projection_Specification>`;  it can take any of the forms of a Projection specification described above for
207
       any Projection subclass; it can be used to provide additional specifications for the Projection, such as its
208
       `matrix <MappingProjection.matrix>` if it is a `MappingProjection`.
209

210
    .. note::
211
       A ProjectionTuple should not be confused with a `4-item InputPort specification tuple
212
       <InputPort_Tuple_Specification>`, which also contains weight and exponent items.  In a ProjectionTuple, those
213
       items specify the weight and/or exponent assigned to the *Projection* (see `Projection_Weight_Exponent`),
214
       whereas in an `InputPort specification tuple <InputPort_Weights_And_Exponents>` they specify the weight
215
       and/or exponent of the **InputPort**.
216

217
    Any (but not all) of the items can be `None`.  If the Port specification is `None`, then there must be a
218
    Projection specification (used to infer the Port to be connected with).  If the Projection specification is
219
    `None` or absent, the Port specification cannot be `None` (as it is then used to infer the type of Projection).
220
    If weight and/or exponent is `None`, it is ignored.  If both the Port and Projection are specified, they must
221
    be compatible  (see `examples <Port_Projections_Examples>` in Port).
222

223

224
.. _Projection_Automatic_Creation:
225

226
*Automatic creation*
227
~~~~~~~~~~~~~~~~~~~~
228

229
Under some circumstances Projections are created automatically. For example, a `Composition` automatically creates
230
a `MappingProjection` between adjacent `ProcessingMechanisms <ProcessingMechanism>` specified in the **pathways**
231
argument of its constructor (if none is specified) or in its `add_linear_processing_pathway
232
<Composition.add_linear_processing_pathway>` method;  and, similarly, `LearningProjections <LearningProjection>` are
233
automatically created when a `learning pathway <Composition_Learning_Pathway>` is added to a Composition.
234

235
.. _Projection_Deferred_Initialization:
236

237
*Deferred Initialization*
238
~~~~~~~~~~~~~~~~~~~~~~~~~
239

240
When a Projection is created, its full initialization is `deferred <Component_Deferred_Init>` until its `sender
241
<Projection_Base.sender>` and `receiver <Projection_Base.receiver>` have been fully specified.  This allows a
242
Projection to be created before its `sender <Projection_Base.sender>` and/or `receiver <Projection_Base.receiver>` have
243
been created (e.g., before them in a script), by calling its constructor without specifying its **sender** or
244
**receiver** arguments. However, for the Projection to be operational, initialization must be completed by calling
245
its `_deferred_init` method.  Under most conditions this occurs automatically (e.g., when the projection is assigned
246
to a type of Component that expects to be the `sender <Projection_Base.sender>` or `receiver <Projection_Base.receiver>`
247
for that type of Projection); these conditions are described in the section on *Deferred Initialization* for each type
248
of Projection.  Otherwise, the  Projection's `_deferred_init` method must be called explicitly, once the missing
249
attribute assignments have been made.
250

251

252
.. _Projection_Structure:
253

254
Structure
255
---------
256

257
In addition to its `function <Projection_Base.function>`, a Projection has two primary attributes: a `sender
258
<Projection_Base.sender>` and `receiver <Projection_Base.receiver>`.  The types of `Port(s) <Port>` that can be
259
assigned to these, and the attributes of those Ports to which Projections of each type are assigned, are
260
summarized in the following table, and described in greater detail in the subsections below.  In addition to the
261
Port attributes to which different types of Projections are assigned (shown in the table), all of the Projections
262
of a Port are listed in its `projections <Port_Base.projections>` attribute.
263

264
.. _Projection_Table:
265

266
.. table:: **Sender, Receiver and Attribute Assignments for Projection Types**
267
    :align: center
268

269
    +----------------------+---------------------------------------+--------------------------------------------------+
270
    |     Projection       |   sender                              |  receiver                                        |
271
    |                      |   *(attribute)*                       |  *(attribute)*                                   |
272
    +======================+=======================================+==================================================+
273
    | `MappingProjection`  | `OutputPort`                          | `InputPort`                                      |
274
    |                      | (`efferents <Port_Base.efferents>`)   | (`path_afferents <Port_Base.path_afferents>`)    |
275
    +----------------------+---------------------------------------+--------------------------------------------------+
276
    | `LearningProjection` | `LearningSignal`                      | `ParameterPort`                                  |
277
    |                      | (`efferents <Port_Base.efferents>`)   | (`mod_afferents <ParameterPort.mod_afferents>`)  |
278
    +----------------------+---------------------------------------+--------------------------------------------------+
279
    | `ControlProjection`  | `ControlSignal`                       | `InputPort`, `ParameterPort` or `OutputPort`     |
280
    |                      | (`efferents <Port_Base.efferents>`)   | (`mod_afferents <ParameterPort.mod_afferents>`)  |
281
    +----------------------+---------------------------------------+--------------------------------------------------+
282
    | `GatingProjection`   | `GatingSignal`                        | `InputPort` or `OutputPort`                      |
283
    |                      | (`efferents <Port_Base.efferents>`)   | (`mod_afferents <Port_Base.mod_afferents>`)      |
284
    +----------------------+---------------------------------------+--------------------------------------------------+
285

286
.. _Projection_Sender:
287

288
*Sender*
289
~~~~~~~~
290

291
This must be an `OutputPort` or a `ModulatorySignal <ModulatorySignal>` (a subclass of OutputPort specialized for
292
`ModulatoryProjections <ModulatoryProjection>`).  The Projection is assigned to the OutputPort or ModulatorySignal's
293
`efferents <Port_Base.efferents>` list and, for ModulatoryProjections, to the list of ModulatorySignals specific to
294
the `ModulatoryMechanism <ModulatoryMechanism>` from which it projects.  The OutputPort or ModulatorySignal's `value
295
<OutputPort.value>` is used as the `variable <Function.variable>` for Projection's `function
296
<Projection_Base.function>`.
297

298
A sender can be specified as:
299

300
  * an **OutputPort** or **ModulatorySignal**, as appropriate for the Projection's type, using any of the ways for
301
    `specifying an OutputPort <OutputPort_Specification>`.
302
  ..
303
  * a **Mechanism**;  for a `MappingProjection`, the Mechanism's `primary OutputPort <OutputPort_Primary>` is
304
    assigned as the `sender <Projection_Base.sender>`; for a `ModulatoryProjection <ModulatoryProjection>`, a
305
    `ModulatorySignal <ModulatorySignal>` of the appropriate type is created and assigned to the Mechanism.
306

307
If the `sender <Projection_Base.sender>` is not specified and it can't be determined from the context, or an OutputPort
308
specification is not associated with a Mechanism that can be determined from , then the initialization of the
309
Projection is `deferred <Projection_Deferred_Initialization>`.
310

311
.. _Projection_Receiver:
312

313
*Receiver*
314
~~~~~~~~~~
315

316
The `receiver <Projection_Base.receiver>` required by a Projection depends on its type, as listed below:
317

318
    * MappingProjection: `InputPort`
319
    * LearningProjection: `ParameterPort` (for the `matrix <MappingProjection>` of a `MappingProjection`)
320
    * ControlProjection: `ParameterPort`
321
    * GatingProjection: `InputPort` or OutputPort`
322

323
A `MappingProjection` (as a `PathwayProjection <PathwayProjection>`) is assigned to the `path_afferents
324
<Port_Base.path_afferents>` attribute of its `receiver <Projection_Base.receiver>`.  The ModulatoryProjections are
325
assigned to the `mod_afferents <Port.mod_afferents>` attribute of their `receiver <Projection_Base.receiver>`.
326

327
A `receiver <Projection_Base.receiver>` can be specified as:
328

329
  * an existing **Port**;
330
  ..
331
  * an existing **Mechanism** or **Projection**; which of these is permissible, and how a port is assigned to it, is
332
    determined by the type of Projection — see subclasses for details).
333
  ..
334
  * a **specification dictionary** (see subclasses for details).
335

336
.. _Projection_Weight_Exponent:
337

338
*Weight and Exponent*
339
~~~~~~~~~~~~~~~~~~~~~
340

341
Every Projection has a `weight <Projection_Base.weight>` and `exponent <Projection_Base.exponent>` attribute. These
342
are applied to its `value <Projection_Base.value>` before combining it with other Projections that project to the same
343
`Port`.  If both are specified, the `exponent <Projection_Base.exponent>` is applied before the `weight
344
<Projection_Base.weight>`.  These attributes determine both how the Projection's `value <Projection_Base.value>` is
345
combined with others to determine the `variable <Port_Base.variable>` of the Port to which they project.
346

347
.. note::
348
   The `weight <Projection_Base.weight>` and `exponent <Projection_Base.exponent>` attributes of a Projection are not
349
   the same as a Port's `weight <Port_Base.weight>` and `exponent <Port_Base.exponent>` attributes.  Also, they are
350
   not normalized: their aggregate effects contribute to the magnitude of the `variable <Port.variable>` to which
351
   they project.
352

353

354
*ParameterPorts and Parameters*
355
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
356

357
`ParameterPorts <ParameterPort>` provide the value for each parameter of a Projection and its `function
358
<Mechanism_Base.function>`.  ParameterPorts and their associated parameters are handled in the same way by
359
Projections as they are for Mechanisms (see `Mechanism_ParameterPorts` for details).  The ParameterPorts for a
360
Projection are listed in its `parameter_ports <Projection_Base.parameter_ports>` attribute.
361

362

363
.. _Projection_Execution:
364

365
Execution
366
---------
367

368
A Projection cannot be executed directly.  It is executed when the `Port <Port>` to which it projects (i.e., its
369
`receiver <Projection_Base.receiver>`) is updated;  that occurs when the Port's owner `Mechanism <Mechanism>` is
370
executed. When a Projection executes, it gets the value of its `sender <Projection_Base.sender>`, assigns this as the
371
`variable <Projection_Base.variable>` of its `function <Projection_Base.function>`, calls the `function
372
<Projection_Base.function>`, and provides the result as to its `receiver <Projection_Base.receiver>`.  The `function
373
<Projection_Base.function>` of a Projection converts the value received from its `sender <Projection_Base.sender>` to
374
a form suitable as input for its `receiver <Projection_Base.receiver>`.
375

376
COMMENT:
377
*** ADD EXAMPLES
378

379
GET FROM Scratch Pad
380

381
for example, if a ProjectionTuple is used in the context of an
382
    `InputPort specification
383
    <InputPort_Specification>` to specify a MappingProjection to it from an `OutputPort` that is specified
384
    in the first item of the tuple, and a Projection specification is included in the fourth, its sender (and/or the
385
    sending dimensions of its `matrix <MappingProjection.matrix>` parameter) must be compatible with the specified
386
    OutputPort (see `examples <XXX>` below)
387

388
COMMENT
389

390

391
.. _Projection_Class_Reference:
392

393
Class Reference
394
---------------
395

396
"""
397
import abc
1✔
398
import inspect
1✔
399
import warnings
1✔
400
from collections import namedtuple, defaultdict
1✔
401

402
import numpy as np
1✔
403
from beartype import beartype
1✔
404

405
from psyneulink._typing import Optional, Union, Type, Literal, Any, Dict, Tuple
1✔
406

407
from psyneulink.core import llvm as pnlvm
1✔
408
from psyneulink.core.components.component import Component, ComponentError
1✔
409
from psyneulink.core.components.functions.function import get_matrix, ValidMatrixSpecType
1✔
410
from psyneulink.core.components.mechanisms.processing.processingmechanism import ProcessingMechanism
1✔
411
from psyneulink.core.components.functions.nonstateful.transformfunctions import MatrixTransform
1✔
412
from psyneulink.core.components.ports.modulatorysignals.modulatorysignal import _is_modulatory_spec
1✔
413
from psyneulink.core.components.ports.port import PortError
1✔
414
from psyneulink.core.components.shellclasses import Mechanism, Process_Base, Projection, Port
1✔
415
from psyneulink.core.globals.context import ContextFlags
1✔
416
from psyneulink.core.globals.graph import EdgeType
1✔
417
from psyneulink.core.globals.mdf import _get_variable_parameter_name
1✔
418
from psyneulink.core.globals.keywords import \
1✔
419
    CONTROL, CONTROL_PROJECTION, CONTROL_SIGNAL, EXPONENT, FEEDBACK, FUNCTION_PARAMS, \
420
    GATE, GATING_PROJECTION, GATING_SIGNAL, \
421
    INPUT_PORT, LEARNING, LEARNING_PROJECTION, LEARNING_SIGNAL, \
422
    MAPPING_PROJECTION, MATRIX, MECHANISM, \
423
    MODEL_SPEC_ID_RECEIVER_MECH, MODEL_SPEC_ID_RECEIVER_PORT, \
424
    MODEL_SPEC_ID_SENDER_MECH, MODEL_SPEC_ID_SENDER_PORT, MODEL_SPEC_ID_METADATA, \
425
    NAME, OUTPUT_PORT, OUTPUT_PORTS, PARAMS, PATHWAY, PROJECTION, PROJECTION_PARAMS, \
426
    PROJECTION_RECEIVER, PROJECTION_SENDER, PROJECTION_TYPE, \
427
    RECEIVER, SENDER, STANDARD_ARGS, PORT, PORTS, WEIGHT, ADD_INPUT_PORT, ADD_OUTPUT_PORT, \
428
    PROJECTION_COMPONENT_CATEGORY
429
from psyneulink.core.globals.parameters import (
1✔
430
    Parameter,
431
    ParameterInvalidSourceError,
432
    ParameterNoValueError,
433
    check_user_specified,
434
    copy_parameter_value,
435
)
436
from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel
1✔
437
from psyneulink.core.globals.registry import register_category, remove_instance_from_registry
1✔
438
from psyneulink.core.globals.socket import ConnectionInfo
1✔
439
from psyneulink.core.globals.utilities import \
1✔
440
    ContentAddressableList, is_matrix, is_matrix_keyword, is_numeric, parse_valid_identifier, convert_to_list
441

442
__all__ = [
1✔
443
    'Projection_Base', 'projection_keywords', 'PROJECTION_SPEC_KEYWORDS',
444
    'ProjectionError', 'DuplicateProjectionError', 'ProjectionRegistry',
445
    'kpProjectionTimeScaleLogEntry'
446
]
447

448
ProjectionRegistry = {}
1✔
449

450
kpProjectionTimeScaleLogEntry = "Projection TimeScale"
1✔
451

452
projection_keywords = set()
1✔
453

454
PROJECTION_ARGS = {PROJECTION_TYPE, SENDER, RECEIVER, WEIGHT, EXPONENT} | STANDARD_ARGS
1✔
455

456
PROJECTION_SPEC_KEYWORDS = {PATHWAY: MAPPING_PROJECTION,
1✔
457
                            LEARNING: LEARNING_PROJECTION,
458
                            LEARNING_SIGNAL: LEARNING_PROJECTION,
459
                            LEARNING_PROJECTION: LEARNING_PROJECTION,
460
                            CONTROL: CONTROL_PROJECTION,
461
                            CONTROL_SIGNAL: CONTROL_PROJECTION,
462
                            CONTROL_PROJECTION: CONTROL_PROJECTION,
463
                            GATE: GATING_PROJECTION,
464
                            GATING_SIGNAL: GATING_PROJECTION,
465
                            GATING_PROJECTION: GATING_PROJECTION
466
                            }
467

468
def projection_param_keyword_mapping():
1✔
469
    """Maps Projection type (key) to Projection parameter keywords (value) used for runtime_params specification
470
    Projection type is one specified in its componentType attribute, and registered in ProjectionRegistry
471
    """
472
    return {k: (k[:k.find('PROJECTION') - 9] + '_' + k[k.find('PROJECTION') - 9:]).upper() + '_PARAMS'
1✔
473
            for k in list(ProjectionRegistry.keys())}
474

475
def projection_param_keywords():
1✔
476
    return set(projection_param_keyword_mapping().values())
1✔
477

478

479
ProjectionTuple = namedtuple("ProjectionTuple", "port, weight, exponent, projection")
1✔
480

481

482
class ProjectionError(ComponentError):
1✔
483
    pass
1✔
484

485
class DuplicateProjectionError(Exception):
1✔
486
    def __init__(self, error_value):
1✔
487
        self.error_value = error_value
1✔
488

489
    def __str__(self):
1✔
490
        return repr(self.error_value)
×
491

492
# Projection factory method:
493
# def projection(name=NotImplemented, params=NotImplemented, context=None):
494
#         """Instantiates default or specified subclass of Projection
495
#
496
#         If called w/o arguments or 1st argument=NotImplemented, instantiates default subclass (ParameterPort)
497
#         If called with a name string:
498
#             - if registered in ProjectionRegistry class dictionary as name of a subclass, instantiates that class
499
#             - otherwise, uses it as the name for an instantiation of the default subclass, and instantiates that
500
#         If a params dictionary is included, it is passed to the subclass
501
#
502
#         :param name:
503
#         :param param_defaults:
504
#         :return:
505
#         """
506
#
507
#         # Call to instantiate a particular subclass, so look up in MechanismRegistry
508
#         if name in ProjectionRegistry:
509
#             return ProjectionRegistry[name].mechanismSubclass(params)
510
#         # Name is not in MechanismRegistry or is not provided, so instantiate default subclass
511
#         else:
512
#             # from Components.Defaults import DefaultProjection
513
#             return DefaultProjection(name, params)
514
#
515

516
class Projection_Base(Projection):
1✔
517
    """
518
    Projection_Base(           \
519
        sender=None,           \
520
        function=MatrixTransform, \
521
        receiver=None,         \
522
        feedback=None          \
523
        )
524

525
    Abstract base class for all Projections.
526

527
    The arguments below can be used in the constructor for any subclass of Mechanism.
528
    See `Component <Component_Class_Reference>` and subclasses for additional arguments and attributes.
529

530
    .. note::
531
       Projection is an abstract class and should *never* be instantiated by a direct call to its constructor.
532
       It should be created by calling the constructor for a subclass` or by using any of the other methods for
533
       `specifying a Projection <Projection_Specification>`.
534

535
    COMMENT:
536
    Gotchas
537
    -------
538
        When referring to the Mechanism that is a Projection's sender or receiver Mechanism, must add ".owner"
539

540
    ProjectionRegistry
541
    ------------------
542
        All Projections are registered in ProjectionRegistry, which maintains a dict for each subclass,
543
        a count for all instances of that type, and a dictionary of those instances
544
    COMMENT
545

546
    Arguments
547
    ---------
548

549
    sender : OutputPort or Mechanism : default None
550
        specifies the source of the Projection's input. If a `Mechanism <Mechanism>` is specified, its
551
        `primary OutputPort <OutputPort_Primary>` is used. If it is not specified, it is assigned in
552
        the context in which the Projection is used, or its initialization will be `deferred
553
        <Projection_Deferred_Initialization>`.
554

555
    function : TransferFunction : default MatrixTransform
556
        specifies function used to convey (and potentially convert) `value <Port_Base.value>` of `sender
557
        <Projection_Base.sender>` `Port` to `variable <Port_Base.variable>` of `receiver <Projection_Base.receiver>`
558
        Port.
559

560
    receiver : InputPort or Mechanism : default None
561
        specifies the destination of the Projection's output.  If a `Mechanism <Mechanism>` is specified, its
562
        `primary InputPort <InputPort_Primary>` will be used. If it is not specified, it will be assigned in
563
        the context in which the Projection is used, or its initialization will be `deferred
564
        <Projection_Deferred_Initialization>`.
565

566
    feedback : bool or FEEDBACK : default None
567
        specifies whether Projection is configured as a feedback edge in the graph of a `Composition` to which
568
        it is assigned (see `Composition_Cycles_and_Feedback`); specifying True or the keyword *FEEDBACK* forces its
569
        assignment as a *feedback* Projection, whereas False precludes it from being assigned as a feedback Projection;
570
        None (the default) allows the Composition to determine whether it is assigned as a feedback Projection.
571

572
    exclude_in_autodiff : bool : default False
573
        specifies whether Projection is included in `AutodiffComposition` gradient calculations.
574

575
    Attributes
576
    ----------
577

578
    variable : value
579
        input to Projection, received from `value <OutputPort.value>` of `sender <Projection_Base.sender>`.
580

581
    sender : Port
582
        Port from which Projection receives its input (see `Projection_Sender` for additional information).
583

584
    receiver : Port
585
        Port to which Projection sends its output  (see `Projection_Receiver` for additional information)
586

587
    function :  TransferFunction
588
        conveys (and potentially converts) `variable <Projection_Base.variable>` to `value <Projection_Base.value>`.
589

590
    value : value
591
        output of Projection, transmitted to variable of function of its `receiver <Projection_Base.receiver>`.
592

593
    parameter_ports : ContentAddressableList[str, ParameterPort]
594
        a read-only list of the Projection's `ParameterPorts <Mechanism_ParameterPorts>`, one for each of its
595
        `modulable parameters <ParameterPort_Modulable_Parameters>`, including those of its `function
596
        <Projection_Base.function>`.  The value of the parameters of the Projection and its `function
597
        <Projection_Base.function>` are also accessible as (and can be modified using) attributes of the Projection,
598
        in the same manner as they can for a `Mechanism <Mechanism_ParameterPorts>`).
599

600
    exclude_in_autodiff : bool : default False
601
        determines whether Projection is included in `AutodiffComposition` gradient calculations.
602

603
    weight : number
604
       multiplies the `value <Projection_Base.value>` of the Projection after applying the `exponent
605
       <Projection_Base.exponent>`, and before combining with any other Projections that project to the same `Port`
606
       to determine that Port's `variable <Port_Base.variable>` (see `Projection_Weight_Exponent` for details).
607

608
    exponent : number
609
        exponentiates the `value <Projection_Base.value>` of the Projection, before applying `weight
610
        <Projection_Base.weight>`, and before combining it with any other Projections that project to the same `Port`
611
        to determine that Port's `variable <Port_Base.variable>` (see `Projection_Weight_Exponent` for details).
612

613
    name : str
614
        the name of the Projection. If the Projection's `initialization has been deferred
615
        <Projection_Deferred_Initialization>`, it is assigned a temporary name (indicating its deferred initialization
616
        status) until initialization is completed, at which time it is assigned its designated name.  If that is the
617
        name of an existing Projection, it is appended with an indexed suffix, incremented for each Projection with the
618
        same base name (see `Registry_Naming`). If the name is not  specified in the **name** argument of its
619
        constructor, a default name is assigned by the subclass (see subclass for details)
620

621
    """
622

623
    color = 0
1✔
624

625
    componentCategory = PROJECTION_COMPONENT_CATEGORY
1✔
626
    className = componentCategory
1✔
627
    suffix = " " + className
1✔
628

629
    class Parameters(Projection.Parameters):
1✔
630
        """
631
            Attributes
632
            ----------
633

634
                exponent
635
                    see `exponent <Projection_Base.exponent>`
636

637
                    :default value: None
638
                    :type:
639

640
                function
641
                    see `function <Projection_Base.function>`
642

643
                    :default value: `MatrixTransform`
644
                    :type: `Function`
645

646
                weight
647
                    see `weight <Projection_Base.weight>`
648

649
                    :default value: None
650
                    :type:
651
        """
652
        weight = Parameter(None, modulable=True)
1✔
653
        exponent = Parameter(None, modulable=True)
1✔
654
        function = Parameter(MatrixTransform, stateful=False, loggable=False)
1✔
655

656
    registry = ProjectionRegistry
1✔
657

658
    classPreferenceLevel = PreferenceLevel.CATEGORY
1✔
659

660
    @check_user_specified
1✔
661
    @abc.abstractmethod
1✔
662
    def __init__(self,
1✔
663
                 receiver,
664
                 sender=None,
665
                 weight=None,
666
                 exponent=None,
667
                 function=None,
668
                 feedback:Optional[Union[bool,Literal[FEEDBACK]]]=None,
669
                 exclude_in_autodiff=False,
670
                 params=None,
671
                 name=None,
672
                 prefs=None,
673
                 context=None,
674
                 **kwargs
675
                 ):
676
        """Assign sender, receiver, and execute method and register Mechanism with ProjectionRegistry
677

678
        This is an abstract class, and can only be called from a subclass;
679
           it must be called by the subclass with a context value
680

681
        # DOCUMENT:  MOVE TO ABOVE, UNDER INSTANTIATION
682
        Initialization arguments:
683
            - sender (Mechanism, Port or dict):
684
                specifies source of input to Projection (default: senderDefault)
685
            - receiver (Mechanism, Port or dict)
686
                 destination of Projection (default: none)
687
            - params (dict) - dictionary of Projection params:
688
                + FUNCTION:<method>
689
        - name (str): if it is not specified, a default based on the class is assigned in register_category,
690
                            of the form: className+n where n is the n'th instantiation of the class
691
            - prefs (PreferenceSet or specification dict):
692
                 if it is omitted, a PreferenceSet will be constructed using the classPreferences for the subclass
693
                 dict entries must have a preference keyPath as key, and a PreferenceEntry or setting as their value
694
                 (see Description under PreferenceSet for details)
695
            - context (str): must be a reference to a subclass, or an exception will be raised
696

697
        NOTES:
698
        * Receiver is required, since can't instantiate a Projection without a receiving Port
699
        * If sender and/or receiver is a Mechanism, the appropriate Port is inferred as follows:
700
            MappingProjection:
701
                sender = <Mechanism>.output_port
702
                receiver = <Mechanism>.input_port
703
            ControlProjection:
704
                sender = <Mechanism>.output_port
705
                receiver = <Mechanism>.<param> IF AND ONLY IF there is a single one
706
                            that is a ParameterPort;  otherwise, an exception is raised
707
        * _instantiate_sender, _instantiate_receiver must be called before _instantiate_function:
708
            - _validate_params must be called before _instantiate_sender, as it validates PROJECTION_SENDER
709
            - instantatiate_sender may alter self.defaults.variable, so it must be called before _validate_function
710
            - instantatiate_receiver must be called before _validate_function,
711
                 as the latter evaluates receiver.value to determine whether to use self.function or FUNCTION
712
        * If variable is incompatible with sender's output, it is set to match that and revalidated (_instantiate_sender)
713
        * if FUNCTION is provided but its output is incompatible with receiver value, self.function is tried
714
        * registers Projection with ProjectionRegistry
715

716
        :param sender: (Port or dict)
717
        :param receiver: (Port or dict)
718
        :param param_defaults: (dict)
719
        :param name: (str)
720
        :param context: (str)
721
        :return: None
722
        """
723
        from psyneulink.core.components.ports.parameterport import ParameterPort
1✔
724
        from psyneulink.core.components.ports.port import Port_Base
1✔
725

726
        if self.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
727
            self._assign_deferred_init_name(name)
1✔
728
            self._store_deferred_init_args(**locals())
1✔
729
            return
1✔
730

731
        self.receiver = receiver
1✔
732
        self.exclude_in_autodiff = exclude_in_autodiff
1✔
733
        self.feedback = feedback  # Assign to _feedback to avoid interference with vertex.feedback used in Composition
1✔
734

735
         # Register with ProjectionRegistry or create one
736
        register_category(entry=self,
1✔
737
                          base_class=Projection_Base,
738
                          name=name,
739
                          registry=ProjectionRegistry)
740

741
        # Create projection's _portRegistry and ParameterPort entry
742
        self._portRegistry = {}
1✔
743

744
        register_category(entry=ParameterPort,
1✔
745
                          base_class=Port_Base,
746
                          registry=self._portRegistry)
747

748
        self._instantiate_sender(sender, context=context)
1✔
749

750
        # FIX: ADD _validate_variable, THAT CHECKS FOR SENDER?
751
        # FIX: NEED TO KNOW HERE IF SENDER IS SPECIFIED AS A MECHANISM OR PORT
752
        try:
1✔
753
            # this should become _default_value when that is fully implemented
754
            variable = copy_parameter_value(self.sender.defaults.value)
1✔
755
        except AttributeError:
×
756
            if receiver.prefs.verbosePref:
×
757
                warnings.warn("Unable to get value of sender ({0}) for {1};  will assign default ({2})".
×
758
                              format(self.sender, self.name, self.class_defaults.variable))
759
            variable = None
×
760

761
        # Assume that if receiver was specified as a Mechanism, it should be assigned to its (primary) InputPort
762
        # MODIFIED 11/1/17 CW: Added " hasattr(self, "prefs") and" in order to avoid errors. Otherwise, this was being
763
        # called and yielding an error: " AttributeError: 'MappingProjection' object has no attribute '_prefs' "
764
        if isinstance(self.receiver, Mechanism):
1✔
765
            if (len(self.receiver.input_ports) > 1 and hasattr(self, 'prefs') and
1!
766
                    (self.prefs.verbosePref or self.receiver.prefs.verbosePref)):
767
                print("{0} has more than one InputPort; {1} has been assigned to the first one".
×
768
                      format(self.receiver.owner.name, self.name))
769
            self.receiver = self.receiver.input_port
1✔
770

771
        if hasattr(self.receiver, "afferents_info"):
1!
772
            if self not in self.receiver.afferents_info:
1✔
773
                self.receiver.afferents_info[self] = ConnectionInfo()
1✔
774

775

776
        self._creates_scheduling_dependency = True
1✔
777

778
       # Validate variable, function and params
779
        # Note: pass name of Projection (to override assignment of componentName in super.__init__)
780
        super(Projection_Base, self).__init__(
1✔
781
            default_variable=variable,
782
            function=function,
783
            param_defaults=params,
784
            weight=weight,
785
            exponent=exponent,
786
            name=self.name,
787
            prefs=prefs,
788
            **kwargs
789
        )
790

791
        self._assign_default_projection_name()
1✔
792

793
    def _validate_params(self, request_set, target_set=None, context=None):
1✔
794
        """Validate PROJECTION_SENDER and/or sender arg (current self.sender), and assign one of them as self.sender
795

796
        Check:
797
        - that PROJECTION_SENDER is a Mechanism or Port
798
        - if it is different from .projection_sender, use it
799
        - if it is the same or is invalid, check if sender arg was provided to __init__ and is valid
800
        - if sender arg is valid use it (if PROJECTION_SENDER can't be used);
801
        - if both were not provided, use .projection_sender
802
        - otherwise, if one was not provided and the other is invalid, generate error
803
        - when done, sender is assigned to self.sender
804

805
        Note: check here only for sender's type, NOT content (e.g., length, etc.); that is done in _instantiate_sender
806

807
        :param request_set:
808
        :param target_set:
809
        :param context:
810
        :return:
811
        """
812

813
        super(Projection, self)._validate_params(request_set, target_set, context)
1✔
814

815
        # FIX: 10/3/17 SHOULD ADD CHECK THAT RECEIVER/SENDER SOCKET SPECIFICATIONS ARE CONSISTENT WITH
816
        # FIX:         PROJECTION_TYPE SPECIFIED BY THE CORRESPONDING PORT TYPES
817

818
        # Validate sender
819
        if (PROJECTION_SENDER in target_set and
1!
820
                not (target_set[PROJECTION_SENDER] in {None, self.projection_sender})):
821
            # If PROJECTION_SENDER is specified it will be the sender
822
            sender = target_set[PROJECTION_SENDER]
×
823
            sender_string = PROJECTION_SENDER
×
824
        else:
825
            # PROJECTION_SENDER is not specified or None, so sender argument of constructor will be the sender
826
            sender = self.sender
1✔
827
            sender_string = "\'{}\' argument".format(SENDER)
1✔
828
        if not ((isinstance(sender, (Mechanism, Port)) or
1✔
829
                 (inspect.isclass(sender) and issubclass(sender, (Mechanism, Port))))):
830
            raise ProjectionError(f"Specification of {sender_string} for {self.name} ({sender}) is invalid; "
831
                                  "it must be a {Mechanism.__name__}, {Port.__name__} or a class of one of these.")
832

833
        # Validate receiver
834
        if (PROJECTION_RECEIVER in target_set and target_set[PROJECTION_RECEIVER] is not None):
1!
835
            # If PROJECTION_RECEIVER is specified it will be the receiver
836
            receiver = target_set[PROJECTION_RECEIVER]
×
837
            receiver_string = PROJECTION_RECEIVER
×
838
        else:
839
            # PROJECTION_RECEIVER is not specified or None, so receiver argument of constructor will be the receiver
840
            receiver = self.receiver
1✔
841
            receiver_string = "\'{}\' argument".format(RECEIVER)
1✔
842
        if not ((isinstance(receiver, (Mechanism, Port)) or
1✔
843
                 (inspect.isclass(receiver) and issubclass(receiver, (Mechanism, Port))))):
844
            raise ProjectionError(f"Specification of {receiver_string} for {self.name} ({sender}) is invalid; "
845
                                  "it must be a {Mechanism.__name__}, {Port.__name__} or a class of one of these.")
846

847
        # MODIFIED JDC 7/11/23 NEW:
848
        # # Validate matrix spec
849
        # if MATRIX in target_set and target_set[MATRIX] is not None:
850
        #     matrix = target_set[MATRIX]
851
        #     # If matrix_spec is keyword and sender and receiver have been instantiated, implement matrix
852
        #     #   so that it can be passed to function (e.g., MatrixTransform) if needed.
853
        #     if not is_matrix(matrix):
854
        #         raise ProjectionError(f"Matrix ('{matrix}') specified for '{self.name}' is not a legal matrix spec.")
855
        #     if self.sender_instantiated and self.receiver_instantiated:
856
        #         if isinstance(matrix, (list, np.ndarray)):
857
        #             # use default value for sender if necessary
858
        #             sender_len = \
859
        #                 len(self.sender.value) if self.sender.value is not None \
860
        #                 else  len(self.sender.defaults.value)
861
        #             # reduce receiver to 1d array if necessary
862
        #             receiver_len = len(np.atleast_1d(np.squeeze(self.receiver.variable)))
863
        #             if matrix.shape != (sender_len, receiver_len):
864
        #                 raise ProjectionError(f"Shape of matrix ('{matrix.shape}') specified for '{self.name}' "
865
        #                                       f"does not shapes of its sender and/or receiver "
866
        #                                       f"({(len(self.sender.value), len(self.receiver.variable))}).")
867
        # MODIFIED JDC 7/11/23 END
868

869
    def _get_matrix_from_keyword(self, keyword):
1✔
870
        return get_matrix(
1✔
871
            keyword, self.sender.socket_width, self.receiver.socket_width
872
        )
873

874
    def _instantiate_attributes_before_function(self, function=None, context=None):
1✔
875

876
        self._instantiate_parameter_ports(function=function, context=context)
1✔
877

878
        # If Projection has a matrix parameter, it is specified as a keyword arg in the constructor,
879
        #    and sender and receiver have been instantiated, then implement it:
880
        if hasattr(self.parameters, MATRIX) and self.parameters.matrix._user_specified:
1✔
881
            try:
1✔
882
                matrix = self.parameters.matrix._get(context)
1✔
883
            except ParameterInvalidSourceError:
1✔
884
                pass
1✔
885
            else:
886
                if (
1!
887
                    is_matrix_keyword(matrix)
888
                    and self.sender_instantiated
889
                    and self.receiver_instantiated
890
                ):
NEW
891
                    matrix = get_matrix(
×
892
                        self.matrix, len(self.sender.value), len(self.receiver.variable)
893
                    )
NEW
894
                    self.parameters.matrix._set(matrix, context)
×
895

896
    def _instantiate_parameter_ports(self, function=None, context=None):
1✔
897

898
        from psyneulink.core.components.ports.parameterport import _instantiate_parameter_ports
1✔
899
        _instantiate_parameter_ports(owner=self, function=function, context=context)
1✔
900

901
    def _instantiate_sender(self, sender, context=None):
1✔
902
        """Assign self.sender to OutputPort of sender
903

904
        Assume self.sender has been assigned in _validate_params, from either sender arg or PROJECTION_SENDER
905
        Validate, and assign projection to sender's efferents attribute
906

907
        If self.sender is a Mechanism, re-assign it to <Mechanism>.output_port
908
        If self.sender is a Port class reference, validate that it is a OutputPort
909
        Assign projection to sender's efferents attribute
910
        """
911
        from psyneulink.core.compositions.composition import Composition
1✔
912
        from psyneulink.core.components.ports.outputport import OutputPort
1✔
913

914
        if not (
1✔
915
            isinstance(sender, (Composition, Mechanism, Port, Process_Base))
916
            or (inspect.isclass(sender) and issubclass(sender, (Mechanism, Port)))
917
        ):
918
            assert False, \
919
                f"PROGRAM ERROR: Invalid specification for {SENDER} ({sender}) of {self.name} " \
920
                f"(including class default: {self.projection_sender})."
921

922
        # If self.sender is specified as a Mechanism (rather than a Port),
923
        #     get relevant OutputPort and assign it to self.sender
924
        # IMPLEMENTATION NOTE: Assume that self.sender should be the primary OutputPort; if that is not the case,
925
        #                      self.sender should either be explicitly assigned, or handled in an override of the
926
        #                      method by the relevant subclass prior to calling super
927
        if isinstance(sender, Composition):
1!
928
            sender = sender.output_CIM
×
929
        if isinstance(sender, Mechanism):
1✔
930
            sender = sender.output_port
1✔
931
        self.sender = sender
1✔
932

933
        # At this point, self.sender should be a OutputPort
934
        if not isinstance(self.sender, OutputPort):
1✔
935
            raise ProjectionError("Sender specified for {} ({}) must be a Mechanism or an OutputPort".
936
                                  format(self.name, self.sender))
937

938
        # Assign projection to self.sender's efferents list attribute
939
        # First make sure that projection is not already in efferents
940
        # IMPLEMENTATON NOTE:  Currently disallows *ANY* Projections with same sender and receiver
941
        #                      (even if they are in different Compositions)
942
        if self not in self.sender.efferents:
1✔
943
            # Then make sure there is not already a projection to its receiver
944
            receiver = self.receiver
1✔
945
            if isinstance(receiver, Composition):
1!
946
                receiver = receiver.input_CIM
×
947
            if isinstance(receiver, Mechanism):
1✔
948
                receiver = receiver.input_port
1✔
949
            assert isinstance(receiver, (Port)), \
1✔
950
                f"Illegal receiver ({receiver}) detected in _instantiate_sender() method for {self.name}"
951
            dup = receiver._check_for_duplicate_projections(self)
1✔
952
            # If duplicate is a deferred_init Projection, delete it and use one currently being instantiated
953
            # IMPLEMENTATION NOTE:  this gives precedence to a Projection to a Component specified by its sender
954
            #                      (e.g., controller of a Composition for a ControlProjection)
955
            #                       over its specification in the constructor for the receiver or its owner
956
            # IMPLEMENTATION NOTE:  This should be removed if/when different Projections are permitted between
957
            #                       the same sender and receiver in different Compositions
958
            if dup:
1✔
959
                if dup.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
960
                    del receiver.mod_afferents[receiver.mod_afferents.index(dup)]
1✔
961
                else:
962
                    raise DuplicateProjectionError(f"Attempt to assign {Projection.__name__} to '{receiver.name}' "
963
                                                   f"of '{receiver.owner.name}' that already has an identical "
964
                                                   f"{Projection.__name__}.")
965
            self.sender.efferents.append(self)
1✔
966
        else:
967
            raise DuplicateProjectionError(f"Attempt to assign {Projection.__name__} from '{sender.name}' of "
968
                                           f"'{sender.owner.name}' that already has an identical "
969
                                           f"{Projection.__name__}.")
970

971
    def _instantiate_attributes_after_function(self, context=None):
1✔
972
        from psyneulink.core.components.ports.parameterport import _instantiate_parameter_port
1✔
973
        self._instantiate_receiver(context=context)
1✔
974
        # instantiate parameter ports from UDF custom parameters if necessary
975
        try:
1✔
976
            cfp = self.function.cust_fct_params
1✔
977
            udf_parameters_lacking_ports = {param_name: cfp[param_name]
×
978
                                            for param_name in cfp if param_name not in self.parameter_ports.names}
979

980
            _instantiate_parameter_port(self, FUNCTION_PARAMS,
×
981
                                        udf_parameters_lacking_ports,
982
                                        context=context,
983
                                        function=self.function)
984
        except AttributeError:
1✔
985
            pass
1✔
986

987
        super()._instantiate_attributes_after_function(context=context)
1✔
988

989
    def _instantiate_receiver(self, context=None):
1✔
990
        """Call receiver's owner to add projection to its afferents list
991

992
        Notes:
993
        * Assume that subclasses implement this method in which they:
994
          - test whether self.receiver is a Mechanism and, if so, replace with Port appropriate for projection
995
          - calls this method (as super) to assign projection to the Mechanism
996
        * Constraint that self.value is compatible with receiver.input_port.value
997
            is evaluated and enforced in _instantiate_function, since that may need to be modified (see below)
998
        * Verification that projection has not already been assigned to receiver is handled by _add_projection_to;
999
            if it has, a warning is issued and the assignment request is ignored
1000

1001
        :param context: (str)
1002
        :return:
1003
        """
1004
        # IMPLEMENTATION NOTE: since projection is added using Mechanism.add_projection(projection, port) method,
1005
        #                      could add port specification as arg here, and pass through to add_projection()
1006
        #                      to request a particular port
1007
        # IMPLEMENTATION NOTE: should check that projection isn't already received by receivers
1008

1009
        if isinstance(self.receiver, Port):
1!
1010
            _add_projection_to(receiver=self.receiver.owner,
1✔
1011
                               port=self.receiver,
1012
                               projection_spec=self,
1013
                               context=context)
1014

1015
        # This should be handled by implementation of _instantiate_receiver by projection's subclass
1016
        elif isinstance(self.receiver, Mechanism):
×
1017
            raise ProjectionError("PROGRAM ERROR: receiver for {0} was specified as a Mechanism ({1});"
1018
                                  "this should have been handled by _instantiate_receiver for {2}".
1019
                                  format(self.name, self.receiver.name, self.__class__.__name__))
1020

1021
        else:
1022
            raise ProjectionError("Unrecognized receiver specification ({0}) for {1}".format(self.receiver, self.name))
1023

1024
    def _update_parameter_ports(self, runtime_params=None, context=None):
1✔
1025
        for port in self._parameter_ports:
1✔
1026
            port_Name = port.name
1✔
1027
            port._update(params=runtime_params, context=context)
1✔
1028

1029
            # Assign version of ParameterPort.value matched to type of template
1030
            #    to runtime param
1031
            # FYI (7/18/17 CW) : in addition to the params and attribute being set, the port's variable is ALSO being
1032
            # set by the statement below. For example, if port_Name is 'matrix', the statement below sets
1033
            # params['matrix'] to port.value, calls setattr(port.owner, 'matrix', port.value), which sets the
1034
            # 'matrix' ParameterPort's variable to ALSO be equal to port.value! If this is unintended, please change.
1035
            value = port.parameters.value._get(context)
1✔
1036
            getattr(self.parameters, port_Name)._set(value, context)
1✔
1037
            # manual setting of previous value to matrix value (happens in above param['matrix'] setting
1038
            if port_Name == MATRIX:
1!
1039
                port.function.parameters.previous_value._set(value, context)
1✔
1040

1041
    def add_to(self, receiver, port, context=None):
1✔
1042
        _add_projection_to(receiver=receiver, port=port, projection_spec=self, context=context)
×
1043

1044
    def _execute(self, variable=None, context=None, runtime_params=None):
1✔
1045
        if variable is None:
1✔
1046
            variable = self.sender.parameters.value._get(context)
1✔
1047

1048
        value = super()._execute(
1✔
1049
            variable=variable,
1050
            context=context,
1051
            runtime_params=runtime_params,
1052

1053
        )
1054
        return value
1✔
1055

1056
    def _activate_for_compositions(self, composition):
1✔
1057
        try:
1✔
1058
            self.receiver.afferents_info[self].add_composition(composition)
1✔
1059
        except KeyError:
×
1060
            self.receiver.afferents_info[self] = ConnectionInfo(compositions=composition)
×
1061

1062
        try:
1✔
1063
            if self not in composition.projections:
1✔
1064
                composition._add_projection(self)
1✔
1065
        except AttributeError:
1✔
1066
            # composition may be ALL or None, in this case we don't need to add
1067
            pass
1✔
1068

1069
    def _activate_for_all_compositions(self):
1✔
1070
        self._activate_for_compositions(ConnectionInfo.ALL)
1✔
1071

1072
    def _deactivate_for_compositions(self, composition):
1✔
1073
        try:
1✔
1074
            self.receiver.afferents_info[self].remove_composition(composition)
1✔
1075
        except KeyError:
×
1076
            warnings.warn(f'{self} was not active for {composition}')
×
1077

1078
    def _deactivate_for_all_compositions(self):
1✔
1079
        self._deactivate_for_all_compositions(ConnectionInfo.ALL)
×
1080

1081
    def is_active_in_composition(self, composition):
1✔
1082
        return self.receiver.afferents_info[self].is_active_in_composition(composition)
1✔
1083

1084
    def _delete_projection(projection, context=None):
1✔
1085
        """Delete Projection, its entries in receiver and sender Ports, and in ProjectionRegistry"""
1086
        projection.sender._remove_projection_from_port(projection)
1✔
1087
        projection.receiver._remove_projection_to_port(projection)
1✔
1088
        remove_instance_from_registry(ProjectionRegistry, projection.__class__.__name__,
1✔
1089
                                      component=projection)
1090

1091
    # FIX: 10/3/17 - replace with @property on Projection for receiver and sender
1092
    @property
1✔
1093
    def socket_assignments(self):
1✔
1094

1095
        if self.initialization_status == ContextFlags.DEFERRED_INIT:
1!
1096
            sender = self._init_args[SENDER]
1✔
1097
            receiver = self._init_args[RECEIVER]
1✔
1098
        else:
1099
            sender = self.sender
×
1100
            receiver = self.receiver
×
1101

1102
        return {SENDER:sender,
1✔
1103
                RECEIVER:receiver}
1104

1105
    def _projection_added(self, projection, context=None):
1✔
1106
        """Stub that can be overidden by subclasses that need to know when a projection is added to the Projection"""
1107
        pass
1✔
1108

1109
    def _assign_default_name(self, **kwargs):
1✔
1110
        self._assign_default_projection_name(**kwargs)
1✔
1111

1112
    def _assign_default_projection_name(self, port=None, sender_name=None, receiver_name=None):
1✔
1113
        raise ProjectionError("PROGRAM ERROR: {} must implement _assign_default_projection_name().".
1114
                              format(self.__class__.__name__))
1115

1116
    @property
1✔
1117
    def sender_instantiated(self):
1✔
1118
        sender_instantiated = isinstance(self.sender, Port)
×
1119
        if sender_instantiated:
×
1120
            sender_instantiated = self.sender.initialization_status == ContextFlags.INITIALIZED
×
1121
        return sender_instantiated
×
1122

1123
    @property
1✔
1124
    def receiver_instantiated(self):
1✔
1125
        receiver_instantiated = isinstance(self.receiver, Port)
×
1126
        if receiver_instantiated:
×
1127
            receiver_instantiated = self.receiver.initialization_status == ContextFlags.INITIALIZED
×
1128
        return receiver_instantiated
×
1129

1130
    @property
1✔
1131
    def parameter_ports(self):
1✔
1132
        """Read-only access to _parameter_ports"""
1133
        return self._parameter_ports
1✔
1134

1135
    # Provide invocation wrapper
1136
    def _gen_llvm_function_body(self, ctx, builder, params, state, arg_in, arg_out, *, tags:frozenset):
1✔
1137

1138
        if "passthrough" in tags:
1✔
1139
            assert arg_in.type == arg_out.type, "Requestd passthrough projection but types are not compatible IN: {} OUT: {}".format(arg_in.type, arg_out.type)
1✔
1140
            builder.store(builder.load(arg_in), arg_out)
1✔
1141
            return builder
1✔
1142

1143
        mf_params, mf_state = ctx.get_param_or_state_ptr(builder,
1✔
1144
                                                         self,
1145
                                                         self.parameters.function,
1146
                                                         param_struct_ptr=params,
1147
                                                         state_struct_ptr=state)
1148
        main_function = ctx.import_llvm_function(self.function)
1✔
1149
        builder.call(main_function, [mf_params, mf_state, arg_in, arg_out])
1✔
1150

1151
        return builder
1✔
1152

1153
    @property
1✔
1154
    def _dependent_components(self):
1✔
1155
        res = super()._dependent_components
1✔
1156
        try:
1✔
1157
            res.extend(self.parameter_ports)
1✔
1158
        except AttributeError:
1✔
1159
            # when in DEFERRED_INIT, _parameter_ports doesn't exist yet
1160
            pass
1✔
1161
        if isinstance(self.sender, Component):
1✔
1162
            res.append(self.sender)
1✔
1163
        return res
1✔
1164

1165
    @property
1✔
1166
    def feedback(self):
1✔
1167
        return self._feedback
1✔
1168

1169
    @feedback.setter
1✔
1170
    def feedback(self, value: Union[bool, EdgeType]):
1✔
1171
        if value is None:
1✔
1172
            self._feedback = None
1✔
1173
        else:
1174
            self._feedback = EdgeType.from_any(value)
1✔
1175

1176
    @property
1✔
1177
    def _model_spec_parameter_blacklist(self):
1✔
1178
        """
1179
            A set of Parameter names that should not be added to the generated
1180
            constructor string
1181
        """
1182
        return super()._model_spec_parameter_blacklist.union(
1✔
1183
            {'variable'}
1184
        )
1185

1186
    def as_mdf_model(self, simple_edge_format=True):
1✔
1187
        import modeci_mdf.mdf as mdf
1✔
1188

1189
        from psyneulink.core.components.mechanisms.processing.compositioninterfacemechanism import CompositionInterfaceMechanism
1✔
1190
        from psyneulink.core.globals.mdf import _get_id_for_mdf_port
1✔
1191

1192
        # these may occur during deferred init
1193
        if hasattr(self, 'sender') and not isinstance(self.sender, type):
1✔
1194
            sender_name = _get_id_for_mdf_port(self.sender)
1✔
1195
            if isinstance(self.sender.owner, CompositionInterfaceMechanism):
1!
1196
                sender_mech = parse_valid_identifier(self.sender.owner.composition.name)
×
1197
            else:
1198
                sender_mech = parse_valid_identifier(self.sender.owner.name)
1✔
1199
        else:
1200
            sender_name = ''
1✔
1201
            sender_mech = ''
1✔
1202

1203
        if hasattr(self, 'receiver') and not isinstance(self.receiver, type):
1✔
1204
            try:
1✔
1205
                num_path_afferents = len(self.receiver.path_afferents)
1✔
1206
            except PortError:
1✔
1207
                # ParameterPort as receiver
1208
                num_path_afferents = 0
1✔
1209

1210
            if num_path_afferents > 1:
1✔
1211
                afferent = self
1✔
1212
            else:
1213
                afferent = None
1✔
1214
            receiver_name = _get_id_for_mdf_port(self.receiver, afferent=afferent)
1✔
1215

1216
            if isinstance(self.receiver.owner, CompositionInterfaceMechanism):
1!
1217
                receiver_mech = parse_valid_identifier(self.receiver.owner.composition.name)
×
1218
            else:
1219
                receiver_mech = parse_valid_identifier(self.receiver.owner.name)
1✔
1220
        else:
1221
            receiver_name = ''
1✔
1222
            receiver_mech = ''
1✔
1223

1224
        socket_dict = {
1✔
1225
            MODEL_SPEC_ID_SENDER_PORT: sender_name,
1226
            MODEL_SPEC_ID_RECEIVER_PORT: receiver_name,
1227
            MODEL_SPEC_ID_SENDER_MECH: sender_mech,
1228
            MODEL_SPEC_ID_RECEIVER_MECH: receiver_mech
1229
        }
1230

1231
        parameters = self._mdf_model_parameters
1✔
1232
        if self.defaults.weight is None:
1!
1233
            parameters[self._model_spec_id_parameters]['weight'] = 1
1✔
1234

1235
        if (
1✔
1236
            simple_edge_format
1237
            and self.parameters.function.get(fallback_value=None) is not None
1238
            and not self.function._is_identity(defaults=True)
1239
        ):
1240
            edge_node = ProcessingMechanism(
1✔
1241
                name=f'{self.name}_dummy_node',
1242
                default_variable=self.defaults.variable,
1243
                function=self.function
1244
            )
1245
            edge_function = edge_node.function
1✔
1246
            edge_node = edge_node.as_mdf_model()
1✔
1247

1248
            func_model = [f for f in edge_node.functions if f.id == parse_valid_identifier(f'{edge_node.id}_{edge_function.name}')][0]
1✔
1249
            var_name = _get_variable_parameter_name(edge_function)
1✔
1250

1251
            # 2d variable on MatrixTransform will be incorrect on import back to psyneulink
1252
            func_model.metadata[var_name] = func_model.metadata[var_name][-1]
1✔
1253

1254
            pre_edge = mdf.Edge(
1✔
1255
                id=parse_valid_identifier(f'{self.name}_dummy_pre_edge'),
1256
                # assume weight applied before function
1257
                **{
1258
                    self._model_spec_id_parameters: {
1259
                        'weight': parameters[self._model_spec_id_parameters]['weight']
1260
                    },
1261
                    MODEL_SPEC_ID_SENDER_PORT: sender_name,
1262
                    MODEL_SPEC_ID_RECEIVER_PORT: edge_node.input_ports[0].id,
1263
                    MODEL_SPEC_ID_SENDER_MECH: sender_mech,
1264
                    MODEL_SPEC_ID_RECEIVER_MECH: edge_node.id
1265
                }
1266
            )
1267

1268
            for name, value in parameters[self._model_spec_id_parameters].items():
1✔
1269
                if name not in {'weight'}:
1!
1270
                    edge_node.parameters.append(mdf.Parameter(id=name, value=value))
×
1271
            edge_node.metadata.update(self._mdf_metadata[MODEL_SPEC_ID_METADATA])
1✔
1272

1273
            post_edge = mdf.Edge(
1✔
1274
                id=parse_valid_identifier(f'{self.name}_dummy_post_edge'),
1275
                **{
1276
                    MODEL_SPEC_ID_SENDER_PORT: edge_node.output_ports[0].id,
1277
                    MODEL_SPEC_ID_RECEIVER_PORT: receiver_name,
1278
                    MODEL_SPEC_ID_SENDER_MECH: edge_node.id,
1279
                    MODEL_SPEC_ID_RECEIVER_MECH: receiver_mech
1280
                }
1281
            )
1282
            return pre_edge, edge_node, post_edge
1✔
1283
        else:
1284
            metadata = self._mdf_metadata
1✔
1285
            try:
1✔
1286
                metadata[MODEL_SPEC_ID_METADATA]['functions'] = mdf.Function.to_dict(self.function.as_mdf_model())
1✔
1287
            except (AttributeError, ParameterNoValueError):
1✔
1288
                # projection is in deferred init, special handling here?
1289
                pass
1✔
1290

1291
            return mdf.Edge(
1✔
1292
                id=parse_valid_identifier(self.name),
1293
                **socket_dict,
1294
                **parameters,
1295
                **metadata
1296
            )
1297

1298

1299
ProjSpecType = Union[
1✔
1300
    Projection, Port,
1301
    Type[Projection], Type[Port],
1302
    Literal['pathway', 'LEARNING', 'LearningSignal', 'LearningProjection', 'control',
1303
            'ControlSignal', 'ControlProjection', 'gate', 'GatingSignal', 'GatingProjection'],
1304
    ValidMatrixSpecType,
1305
    Dict[Literal['PROJECTION_TYPE', 'sender', 'receiver', 'matrix'], Any],
1306
]
1307

1308
ProjSpecTypeWithTuple = Union[ProjSpecType, Tuple[ProjSpecType, Union[Literal['MappingProjection']]]]
1✔
1309

1310

1311
@beartype
1✔
1312
def _is_projection_spec(spec, proj_type: Optional[Type] = None, include_matrix_spec=True):
1✔
1313
    """Evaluate whether spec is a valid Projection specification
1314

1315
    Return `True` if spec is any of the following:
1316
    + Projection object, and of specified type (if proj_type is specified)
1317
    + Projection class (or keyword string constant for one), and of specified type (if proj_type is specified)
1318
    + 2-item tuple of which the second is a projection_spec (checked recursively with this method):
1319
    + specification dict containing:
1320
        + PROJECTION_TYPE:<Projection class> - must be a subclass of Projection
1321
    + valid matrix specification (if include_matrix_spec is set to `True`)
1322
    + port
1323

1324
    Otherwise, return :keyword:`False`
1325
    """
1326

1327
    if isinstance(spec, Projection):
1✔
1328
        if proj_type is None or isinstance(spec, proj_type):
1!
1329
            return True
1✔
1330
        else:
1331
            return False
×
1332
    if isinstance(spec, Port):
1✔
1333
        # FIX: CHECK PORT AGAIN ALLOWABLE PORTS IF type IS SPECIFIED
1334
        return True
1✔
1335
    # # MODIFIED 11/29/17 NEW:
1336
    # if isinstance(spec, Mechanism):
1337
    #     if proj_type is None:
1338
    #     # FIX: CHECK PORT AGAIN ALLOWABLE PORTS IF type IS SPECIFIED
1339
    #         return True
1340
    # MODIFIED 11/29/17 END
1341
    if inspect.isclass(spec):
1✔
1342
        if issubclass(spec, Projection):
1✔
1343
            if proj_type is None or issubclass(spec, proj_type):
1!
1344
                return True
1✔
1345
            else:
1346
                return False
×
1347
        if issubclass(spec, Port):
1!
1348
            # FIX: CHECK PORT AGAIN ALLOWABLE PORTS IF type IS SPECIFIED
1349
            return True
1✔
1350
    # # MODIFIED 11/29/17 NEW:
1351
        # if issubclass(spec, Mechanism):
1352
        #     # FIX: CHECK PORT AGAIN ALLOWABLE PORTS IF type IS SPECIFIED
1353
        #     return True
1354
    # MODIFIED 11/29/17 END
1355
    if isinstance(spec, dict) and any(key in spec for key in {PROJECTION_TYPE, SENDER, RECEIVER, MATRIX}):
1✔
1356
        # FIX: CHECK PORT AGAIN ALLOWABLE PORTS IF type IS SPECIFIED
1357
        return True
1✔
1358
    if isinstance(spec, str) and spec in PROJECTION_SPEC_KEYWORDS:
1✔
1359
        # FIX: CHECK PORT AGAIN ALLOWABLE PORTS IF type IS SPECIFIED
1360
        return True
1✔
1361
    if include_matrix_spec:
1✔
1362
        if is_matrix_keyword(spec):
1✔
1363
            return True
1✔
1364
        if get_matrix(spec) is not None:
1✔
1365
            return True
1✔
1366
    if isinstance(spec, tuple) and len(spec) == 2:
1✔
1367
        # Call recursively on first item, which should be a standard projection spec
1368
        if _is_projection_spec(spec[0], proj_type=proj_type, include_matrix_spec=include_matrix_spec):
1✔
1369
            if spec[1] is not None:
1✔
1370
                # IMPLEMENTATION NOTE: keywords must be used to refer to subclass, to avoid import loop
1371
                if _is_projection_subclass(spec[1], MAPPING_PROJECTION):
1!
1372
                    return True
×
1373
                if _is_modulatory_spec(spec[1]):
1!
1374
                    return True
×
1375
        # if _is_projection_spec(spec[1], proj_type=proj_type, include_matrix_spec=include_matrix_spec):
1376
        #         # IMPLEMENTATION NOTE: keywords must be used to refer to subclass, to avoid import loop
1377
        #     if is_numeric(spec[0]):
1378
        #         # if _is_projection_subclass(spec[1], MAPPING_PROJECTION):
1379
        #         #     return True
1380
        #         if _is_modulatory_spec(spec[1]):
1381
        #             return True
1382

1383
    return False
1✔
1384

1385

1386
def _is_projection_subclass(spec, keyword):
1✔
1387
    """Evaluate whether spec is a valid specification of type
1388

1389
    keyword must specify a class registered in ProjectionRegistry
1390

1391
    Return true if spec ==
1392
    + keyword
1393
    + subclass of Projection associated with keyword (from ProjectionRegistry)
1394
    + instance of the subclass
1395
    + specification dict for instance of the subclass:
1396
        keyword is a keyword for an entry in the spec dict
1397
        keyword[spec] is a legal specification for the subclass
1398

1399
    Otherwise, return :keyword:`False`
1400
    """
1401
    if spec is keyword:
1!
1402
        return True
×
1403
    # Get projection subclass specified by keyword
1404
    try:
1✔
1405
        proj_type = ProjectionRegistry[keyword].subclass
1✔
1406
    except KeyError:
×
1407
        pass
×
1408
    else:
1409
        # Check if spec is either the name of the subclass or an instance of it
1410
        if inspect.isclass(spec) and issubclass(spec, proj_type):
1!
1411
            return True
×
1412
        if isinstance(spec, proj_type):
1!
1413
            return True
×
1414
    # spec is a specification dict for an instance of the projection subclass
1415
    if isinstance(spec, dict) and keyword in spec:
1!
1416
        # Recursive call to determine that the entry of specification dict is a legal spec for the projection subclass
1417
        if _is_projection_subclass(spec[keyword], keyword):
×
1418
            return True
×
1419
    return False
1✔
1420

1421
def _parse_projection_spec(projection_spec,
1✔
1422
                           owner = None,       # Used only for error message
1423
                           port_type = None,  # Used only for default assignment
1424
                           # socket=None,
1425
                           **kwargs):
1426
    """Return either Projection object or Projection specification dict for projection_spec
1427

1428
    All keys in kwargs must be from PROJECTION_ARGS
1429

1430
    If projection_spec is or resolves to a Projection object, returns Projection object.
1431
    Otherwise, return Projection specification dictionary using any arguments provided as defaults
1432
    """
1433

1434
    bad_arg = next((key for key in kwargs if key not in PROJECTION_ARGS), None)
1✔
1435
    if bad_arg:
1✔
1436
        raise ProjectionError("Illegal argument in call to _parse_port_spec: {}".format(bad_arg))
1437

1438
    proj_spec_dict = defaultdict(lambda :None)
1✔
1439
    proj_spec_dict.update(kwargs)
1✔
1440

1441
    # Projection object
1442
    if isinstance(projection_spec, Projection):
1✔
1443
        projection = projection_spec
1✔
1444
        # FIX: NOT SURE WHICH TO GIVE PRECEDENCE: SPEC IN ProjectionTuple OR INSTANTIATED Projection:
1445
        if ((proj_spec_dict[WEIGHT] is not None and projection.weight is not None) or
1✔
1446
            (proj_spec_dict[EXPONENT] is not None and projection.exponent is not None)):
1447
            raise ProjectionError("PROGRAM ERROR: Conflict in weight and/or exponent specs "
1448
                                  "between Projection and ProjectionTuple")
1449
        if projection.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
1450
            projection._init_args[NAME] = proj_spec_dict[NAME] or projection._init_args[NAME]
1✔
1451
        else:
1452
            projection.name = proj_spec_dict[NAME] or projection.name
1✔
1453

1454
        return projection
1✔
1455

1456
    # Projection class
1457
    elif inspect.isclass(projection_spec) and issubclass(projection_spec, Projection):
1✔
1458
        proj_spec_dict[PROJECTION_TYPE] = projection_spec
1✔
1459

1460
    # Matrix
1461
    elif is_matrix(projection_spec):
1✔
1462
        is_matrix(projection_spec)
1✔
1463
        proj_spec_dict[MATRIX] = projection_spec
1✔
1464

1465
    # Projection keyword
1466
    elif isinstance(projection_spec, str):
1✔
1467
        proj_spec_dict[PROJECTION_TYPE] = _parse_projection_keyword(projection_spec)
1✔
1468

1469
    # Port object or class
1470
    elif (isinstance(projection_spec, Port)
1✔
1471
          or (isinstance(projection_spec, type) and issubclass(projection_spec, Port))):
1472
        proj_spec_dict[PROJECTION_TYPE] = projection_spec.projection_type
1✔
1473
        port_type = projection_spec.__class__
1✔
1474

1475
    # Mechanism object or class
1476
    elif (isinstance(projection_spec, Mechanism)
1✔
1477
          or (isinstance(projection_spec, type) and issubclass(projection_spec, Mechanism))):
1478
        proj_spec_dict[PROJECTION_TYPE] = projection_spec.outputPortTypes.projection_type
1✔
1479

1480
    # Dict
1481
    elif isinstance(projection_spec, dict):
1✔
1482

1483
        proj_spec_dict = projection_spec
1✔
1484

1485
        # Get projection params from specification dict
1486
        if PROJECTION_PARAMS in proj_spec_dict:
1!
1487
            proj_spec_dict[PARAMS].update = proj_spec_dict[PROJECTION_PARAMS]
×
1488
            # projection_spec[PARAMS].update(projection_params)
1489
            assert False, "PROJECTION_PARAMS ({}) passed in spec dict in ProjectionTuple for {}.".\
1490
                           format(proj_spec_dict[PROJECTION_PARAMS], projection_spec, proj_spec_dict[NAME])
1491

1492
    # None
1493
    if not proj_spec_dict[PROJECTION_TYPE]:
1✔
1494
        # Assign default type
1495
        proj_spec_dict[PROJECTION_TYPE] = port_type.projection_type
1✔
1496

1497
        # prefs is not always created when this is called, so check
1498
        try:
1✔
1499
            owner.prefs
1✔
1500
            has_prefs = True
1✔
1501
        except AttributeError:
1✔
1502
            has_prefs = False
1✔
1503

1504
        if has_prefs and owner.prefs.verbosePref:
1!
1505
            warnings.warn("Unrecognized specification ({}) for a Projection for {} of {}; "
×
1506
                          "default {} has been assigned".
1507
                          format(projection_spec,
1508
                                 port_type.__class__.__name__,
1509
                                 owner.name,
1510
                                 proj_spec_dict[PROJECTION_TYPE]))
1511
    return proj_spec_dict
1✔
1512

1513
def _parse_projection_keyword(projection_spec:str):
1✔
1514
    """Takes keyword (str) and returns corresponding Projection class
1515
    """
1516
    # get class for keyword in registry
1517
    try:
1✔
1518
        projection_type = ProjectionRegistry[PROJECTION_SPEC_KEYWORDS[projection_spec]].subclass
1✔
1519
    except KeyError:
×
1520
        # projection_spec was not a recognized key
1521
        raise ProjectionError("{} is not a recognized {} keyword".format(projection_spec, Projection.__name__))
1522
    # projection_spec was legitimate keyword
1523
    else:
1524
        return projection_type
1✔
1525

1526

1527
def _parse_connection_specs(connectee_port_type,
1✔
1528
                            owner,
1529
                            connections):
1530
    """Parse specification(s) for Ports to/from which the connectee_port_type should be connected
1531

1532
    TERMINOLOGY NOTE:
1533
        "CONNECTION" is used instead of "PROJECTION" because:
1534
            - the method abstracts over type and direction of Projection, so it is ambiguous whether
1535
                the projection involved is to or from connectee_port_type; however, can always say it "connects with"
1536
            - specification is not always (in fact, usually is not) in the form of a Projection; usually it is a
1537
                Mechanism or Port to/from which the connectee_port_type should send/receive the Projection
1538

1539
    Connection attributes declared for each type (subclass) of Port that are used here:
1540
        connectsWith : Port
1541
           - specifies the type (subclass) of Port with which the connectee_port_type should be connected
1542
        connectsWithAttribute : str
1543
           - specifies the name of the attribute of the Mechanism that holds the ports of the connectsWith's type
1544
        projectionSocket : [SENDER or RECEIVER]
1545
           - specifies for this method whether to use a Projection's sender or receiver for the connection
1546
        modulators : ModulatorySignal
1547
           -  class of ModulatorySignal that can send ModulatoryProjection to the connectee_port_type
1548

1549
    This method deals with connection specifications that are made in one of the following places/ways:
1550
        - *PROJECTIONS* entry of a Port specification dict;
1551
        - last item of a Port specification tuple.
1552

1553
    In both cases, the connection specification can be a single (stand-alone) item or a list of them.
1554

1555
    Projection(s) in connection(s) can be specified in any of the ways a Projection can be specified;
1556
        * Mechanism specifications are resolved to a primary InputPort or OutputPort, as appropriate
1557
        * Port specifications are assumed to be for connect_with Port,
1558
            and checked for compatibilty of assignment (using projection_socket)
1559
        * keyword specifications are resolved to corresponding Projection class
1560
        * Class assignments are checked for compatiblity with connectee_port_type and connect_with Port
1561

1562
    Each connection specification can, itself, be one of the following:
1563
        * Port object or class;
1564
        * Mechanism object or class - primary Port is used, if applicable, otherwise an exception is generated;
1565
        * dict - must have the first and can have any of the additional entries below:
1566
            *PORT*:<port_spec> - required; must resolve to an instantiated port;  can use any of the following:
1567
                                       Port - the Port is used;
1568
                                       Mechanism - primary Port will be used if appropriate,
1569
                                                   otherwise generates an exception;
1570
                                       {Mechanism:port_spec or [port_spec<, port_spec...>]} -
1571
                                                   each port_spec must be for an instantiated Port of the Mechanism,
1572
                                                   referenced by its name or in a CONNECTION specification that uses
1573
                                                   its name (or, for completeness, the Port itself);
1574
                                                   _parse_connections() is called recursively for each port_spec
1575
                                                   (first replacing the name with the actual port);
1576
                                                   and returns a list of ProjectionTuples; any weights, exponents,
1577
                                                   or projections assigned in those tuples are left;  otherwise, any
1578
                                                   values in the entries of the outer dict (below) are assigned;
1579
                                                   note:  the dictionary can have multiple Mechanism entries
1580
                                                          (which permits the same defaults to be assigned to all the
1581
                                                          Ports for all of the Mechanisms)
1582
                                                          or they can be assigned each to their own dictionary
1583
                                                          (which permits different defaults to be assigned to the
1584
                                                          Ports for each Mechanism);
1585
            *WEIGHT*:<int> - optional; specifies weight given to projection by receiving InputPort
1586
            *EXPONENT:<int> - optional; specifies weight given to projection by receiving InputPort
1587
            *PROJECTION*:<projection_spec> - optional; specifies projection (instantiated or matrix) for connection
1588
                                             default is PROJECTION_TYPE specified for PORT
1589
        * tuple or list of tuples: (specification requirements same as for dict above);  each must be:
1590
            (port_spec, projection_spec) or
1591
            (port_spec, weight, exponent, projection_spec)
1592

1593
    Returns list of ProjectionTuples, each of which specifies:
1594
        - the port to be connected with
1595
        - weight and exponent for that connection (assigned to the projection)
1596
        - projection specification
1597

1598
    """
1599

1600
    from psyneulink.core.components.ports.port import _get_port_for_socket
1✔
1601
    from psyneulink.core.components.ports.port import PortRegistry
1✔
1602
    from psyneulink.core.components.ports.inputport import InputPort
1✔
1603
    from psyneulink.core.components.ports.outputport import OutputPort
1✔
1604
    from psyneulink.core.components.ports.parameterport import ParameterPort
1✔
1605
    from psyneulink.core.components.mechanisms.modulatory.modulatorymechanism import ModulatoryMechanism_Base
1✔
1606
    from psyneulink.core.components.mechanisms.modulatory.control.controlmechanism import _is_control_spec
1✔
1607
    from psyneulink.core.components.mechanisms.modulatory.control.gating.gatingmechanism import _is_gating_spec
1✔
1608

1609
    if not inspect.isclass(connectee_port_type):
1✔
1610
        raise ProjectionError("Called for {} with \'connectee_port_type\' arg ({}) that is not a class".
1611
                         format(owner.name, connectee_port_type))
1612

1613
    # Get connection attributes for connectee
1614
    connects_with = [PortRegistry[name].subclass for name in connectee_port_type.connectsWith]
1✔
1615
    connect_with_attr = connectee_port_type.connectsWithAttribute
1✔
1616
    projection_socket = connectee_port_type.projectionSocket
1✔
1617
    modulators = [PortRegistry[name].subclass for name in connectee_port_type.modulators]
1✔
1618

1619
    DEFAULT_WEIGHT = None
1✔
1620
    DEFAULT_EXPONENT = None
1✔
1621
    DEFAULT_PROJECTION = None
1✔
1622

1623
    # Convert to list for subsequent processing
1624
    if isinstance(connections, set):
1✔
1625
        # if owner.verbosePref:
1626
        #     warnings.warn("Connection specification for {} of {} was a set ({});"
1627
        #                   "it was converted to a list, but the order of {} assignments is not "
1628
        #                   "predictable".format(connectee_port_type, owner.name,
1629
        #                                        connections, Projection.__name__))
1630
        # connections = list(connections)
1631
        raise ProjectionError("Connection specification for {} of {} is a set ({}); it should be a list.".
1632
                              format(connectee_port_type.__name__, owner.name, connections, Projection.__name__))
1633

1634
    elif not isinstance(connections, list):
1✔
1635
        connections = [connections]
1✔
1636
    connect_with_ports = []
1✔
1637

1638
    for connection in connections:
1✔
1639

1640
        # If a Mechanism, Port, or Port type is used to specify the connection on its own (i.e., w/o dict or tuple)
1641
        #     put in ProjectionTuple as both Port spec and Projection spec (to get Projection for that Port)
1642
        #     along with defaults for weight and exponent, and call _parse_connection_specs recursively
1643
        #     to validate the port spec and append ProjectionTuple to connect_with_ports
1644
        if isinstance(connection, (Mechanism, Port, type)):
1✔
1645
            # FIX: 10/3/17 - REPLACE THIS (AND ELSEWHERE) WITH ProjectionTuple THAT HAS BOTH SENDER AND RECEIVER
1646
            # FIX: 11/28/17 - HACKS TO HANDLE PROJECTION FROM GatingSignal TO InputPort or OutputPort
1647
            # FIX:            AND PROJECTION FROM ControlSignal to ParameterPort
1648
            # # If it is a ModulatoryMechanism specification, get its ModulatorySignal class
1649
            # # (so it is recognized by _is_projection_spec below (Mechanisms are not for secondary reasons)
1650
            # if isinstance(connection, type) and issubclass(connection, ModulatoryMechanism_Base):
1651
            #     connection = connection.outputPortTypes
1652
            if ((isinstance(connectee_port_type, (InputPort, OutputPort, ParameterPort))
1✔
1653
                 or isinstance(connectee_port_type, type)
1654
                and issubclass(connectee_port_type, (InputPort, OutputPort, ParameterPort)))
1655
                and _is_modulatory_spec(connection)):
1656
                # Convert ModulatoryMechanism spec to corresponding ModulatorySignal spec
1657
                if isinstance(connection, type) and issubclass(connection, ModulatoryMechanism_Base):
1✔
1658
                    # If the connection supports multiple outputPortTypes,
1659
                    #    get the one compatible with the current connectee:
1660
                    output_port_types = connection.outputPortTypes
1✔
1661
                    if not isinstance(output_port_types, list):
1!
1662
                        output_port_types = [output_port_types]
1✔
1663
                    output_port_type = [o for o in output_port_types if o.__name__ in
1✔
1664
                                          connectee_port_type.connectsWith]
1665
                    assert len(output_port_type)==1, \
1✔
1666
                        f"PROGRAM ERROR:  More than one {OutputPort.__name__} type found for {connection}  " \
1667
                            f"({output_port_types}) that can be assigned a modulatory {Projection.__name__} " \
1668
                            f"to {connectee_port_type.__name__} of {owner.name}"
1669
                    connection = output_port_type[0]
1✔
1670
                elif isinstance(connection, ModulatoryMechanism_Base):
1✔
1671
                    connection = connection.output_port
1✔
1672

1673
                projection_spec = connection
1✔
1674

1675
            else:
1676
                projection_spec = connectee_port_type
1✔
1677

1678
            projection_tuple = (connection, DEFAULT_WEIGHT, DEFAULT_EXPONENT, projection_spec)
1✔
1679
            connect_with_ports.extend(_parse_connection_specs(connectee_port_type, owner, projection_tuple))
1✔
1680

1681
        # If a Projection specification is used to specify the connection:
1682
        #  assign the Projection specification to the projection_specification item of the tuple,
1683
        #  but also leave it is as the connection specification (it will get resolved to a Port reference when the
1684
        #    tuple is created in the recursive call to _parse_connection_specs below).
1685
        elif _is_projection_spec(connection, include_matrix_spec=False):
1✔
1686
            projection_spec = connection
1✔
1687
            projection_tuple = (connection, DEFAULT_WEIGHT, DEFAULT_EXPONENT, projection_spec)
1✔
1688
            connect_with_ports.extend(_parse_connection_specs(connectee_port_type, owner, projection_tuple))
1✔
1689

1690
        # Dict of one or more Mechanism specifications, used to specify individual Ports of (each) Mechanism;
1691
        #   convert all entries to tuples and call _parse_connection_specs recursively to generate ProjectionTuples;
1692
        #   main purpose of this is to resolve any str references to name of port (using context of owner Mechanism)
1693
        elif isinstance(connection, dict):
1!
1694

1695
            # Check that dict has at least one entry with a Mechanism as the key
1696
            if (not any(isinstance(spec, Mechanism) for spec in connection) and
×
1697
                    not any(spec == PORTS for spec in connection)):
1698
                raise ProjectionError("There are no {}s or {}s in the list ({}) specifying {}s for an {} of {}".
1699
                                 format(Mechanism.__name__, Port.__name__, connection, Projection.__name__,
1700
                                        connectee_port_type.__name__, owner.name))
1701

1702
            # Add default WEIGHT, EXPONENT, and/or PROJECTION specification for any that are not aleady in the dict
1703
            #    (used as the default values for all the Ports of all Mechanisms specified for this dict;
1704
            #    can use different dicts to implement different sets of defaults for the Ports of diff Mechanisms)
1705
            if WEIGHT not in connection:
×
1706
                connection[WEIGHT] = DEFAULT_WEIGHT
×
1707
            if EXPONENT not in connection:
×
1708
                connection[EXPONENT] = DEFAULT_EXPONENT
×
1709
            if PROJECTION not in connection:
×
1710
                connection[PROJECTION] = DEFAULT_PROJECTION
×
1711

1712
            # Now process each entry that has *PORTS* or a Mechanism as its key
1713
            for key, port_connect_specs in connection.items():
×
1714

1715
                # Convert port_connect_specs to a list for subsequent processing
1716
                if not isinstance(port_connect_specs, list):
×
1717
                    port_connect_specs = [port_connect_specs]
×
1718

1719
                for port_connect_spec in port_connect_specs:
×
1720

1721
                    # Port, str (name) or Projection specification
1722
                    if isinstance(port_connect_spec, (Port, str, _is_projection_spec)):
×
1723

1724
                        # If port_connection_spec is a string (name), it has to be in a Mechanism entry
1725
                        if isinstance(port_connect_spec, str) and isinstance(key, Mechanism):
×
1726
                            mech = key
×
1727
                        else:
1728
                            raise ProjectionError("{} specified by name ({}) is not in a {} entry".
1729
                                                  format(Port.__name__, port_connect_spec, Mechanism.__name__))
1730

1731
                        # Call _get_port_for_socket to parse if it is a str,
1732
                        #    and in either case to make sure it belongs to mech
1733
                        port = _get_port_for_socket(owner=owner,
×
1734
                                                      port_spec=port_connect_spec,
1735
                                                      port_types=connect_with_attr,
1736
                                                      mech=mech,
1737
                                                      projection_socket=projection_socket)
1738
                        if isinstance(port, list):
×
1739
                            assert False, 'Got list of allowable ports for {} as specification for {} of {}'.\
1740
                                          format(port_connect_spec, projection_socket, mech.name)
1741

1742
                        # Assign port along with dict's default values to tuple
1743
                        port_connect_spec = (port,
×
1744
                                              connection[WEIGHT],
1745
                                              connection[EXPONENT],
1746
                                              connection[PROJECTION])
1747

1748
                    # Dict specification for port itself
1749
                    elif isinstance(port_connect_spec, dict):
×
1750
                        # Get PORT entry
1751
                        port_spec = port_connect_spec[PORT]
×
1752
                        # Parse it to get reference to actual Port make sure it belongs to mech:
1753
                        port = _get_port_for_socket(owner=owner,
×
1754
                                                    port_spec=port_spec,
1755
                                                    port_types=connect_with_attr,
1756
                                                    mech=mech,
1757
                                                    projection_socket=projection_socket)
1758
                        if isinstance(port, list):
×
1759
                            assert False, 'Got list of allowable ports for {} as specification for {} of {}'.\
1760
                                           format(port_connect_spec, projection_socket, mech.name)
1761
                        # Re-assign to PORT entry of dict (to preserve any other connection specifications in dict)
1762
                        port_connect_spec[PORT] = port
×
1763

1764
                    # Tuple specification for Port itself
1765
                    elif isinstance(port_connect_spec, tuple):
×
1766
                        # Get PORT entry
1767
                        port_spec = port_connect_spec[0]
×
1768
                        # Parse it to get reference to actual Port make sure it belongs to mech:
1769
                        port = _get_port_for_socket(owner=owner,
×
1770
                                                    port_spec=port_spec,
1771
                                                    port_types=connect_with_attr,
1772
                                                    mech=mech,
1773
                                                    projection_socket=projection_socket)
1774
                        if isinstance(port, list):
×
1775
                            assert False, 'Got list of allowable ports for {} as specification for {} of {}'.\
1776
                                           format(port_connect_spec, projection_socket, mech.name)
1777
                        # Replace parsed value in original tuple, but...
1778
                        #    tuples are immutable, so have to create new one, with port_spec as (new) first item
1779
                        # Get items from original tuple
1780
                        port_connect_spec_tuple_items = [item for item in port_connect_spec]
×
1781
                        # Replace port_spec
1782
                        port_connect_spec_tuple_items[0] = port
×
1783
                        # Reassign to new tuple
1784
                        port_connect_spec = tuple(port_connect_spec_tuple_items)
×
1785

1786
                    # Recusively call _parse_connection_specs to get ProjectionTuple and append to connect_with_ports
1787
                    connect_with_ports.extend(_parse_connection_specs(connectee_port_type, owner, port_connect_spec))
×
1788

1789
        # Process tuple, including final validation of Port specification
1790
        # Tuple could be:
1791
        #     (port_spec, projection_spec) or
1792
        #     (port_spec, weight, exponent, projection_spec)
1793
        # Note:  this is NOT the same as the Port specification tuple (which can have a similar format);
1794
        #        the weights and exponents here specify *individual* Projections to a particular port,
1795
        #            (vs. weights and exponents for an entire port, such as for InputPort);
1796
        #        Port specification tuple is handled in the _parse_port_specific_specs() method of Port subclasses
1797

1798
        elif isinstance(connection, tuple):
1!
1799

1800
            # 2-item tuple: can be (<value>, <projection_spec>) or (<port name or list of port names>, <Mechanism>)
1801
            mech=None
1✔
1802

1803
            if len(connection) == 2:
1✔
1804
                first_item, last_item = connection
1✔
1805
                weight = DEFAULT_WEIGHT
1✔
1806
                exponent = DEFAULT_EXPONENT
1✔
1807
            elif len(connection) == 4:
1✔
1808
                first_item, weight, exponent, last_item = connection
1✔
1809
            else:
1810
                raise ProjectionError("{} specification tuple for {} ({}) must have either two or four items".
1811
                                      format(connectee_port_type.__name__, owner.name, connection))
1812

1813
            # Default assignments, possibly overridden below
1814
            port_spec = first_item
1✔
1815
            projection_spec = last_item
1✔
1816

1817
            # (<value>, <projection_spec>)
1818
            if is_numeric(first_item):
1!
1819
                projection_spec = first_item
×
1820

1821
            # elif is_matrix(first_item):
1822
            #     projection_spec = last_item
1823
            #     port_spec = None
1824

1825
            elif _is_projection_spec(last_item):
1✔
1826

1827
                # If specification is a list of Ports and/or Mechanisms, get Projection spec for each
1828
                if isinstance(first_item, list):
1✔
1829
                    # Call _parse_connection_spec for each Port or Mechanism, to generate a connection spec for each
1830
                    for connect_with_spec in first_item:
1✔
1831
                        if not isinstance(connect_with_spec, (Port, Mechanism)):
1✔
1832
                            raise PortError(f"Item in the list used to specify a {last_item.__name__} "
1833
                                            f"for {owner.name} ({connect_with_spec.__name__}) "
1834
                                            f"is not a {Port.__name__} or {Mechanism.__name__}")
1835
                        c = _parse_connection_specs(connectee_port_type=connectee_port_type,
1✔
1836
                                                    owner=owner,
1837
                                                    connections=ProjectionTuple(connect_with_spec,
1838
                                                                                weight, exponent,
1839
                                                                                last_item))
1840
                        connect_with_ports.extend(c)
1✔
1841
                    # Move on to other connections
1842
                    continue
1✔
1843
                # Otherwise, go on to process this Projection specification
1844
                port_spec = first_item
1✔
1845
                projection_spec = last_item
1✔
1846

1847

1848
            # (<port name or list of port names>, <Mechanism>)
1849
            elif isinstance(first_item, (str, list)):
1✔
1850
                port_item = first_item
1✔
1851
                mech_item = last_item
1✔
1852

1853
                if not isinstance(mech_item, Mechanism):
1✔
1854
                    raise ProjectionError("Expected 2nd item of the {} specification tuple for {} ({}) to be a "
1855
                                          "Mechanism".format(connectee_port_type.__name__, owner.name, mech_item))
1856
                # First item of tuple is a list of Port names, so recursively process it
1857
                if isinstance(port_item, list):
1✔
1858
                    # Call _parse_connection_spec for each Port name, to generate a connection spec for each
1859
                    for port_Name in port_item:
1✔
1860
                        if not isinstance(port_Name, str):
1✔
1861
                            raise ProjectionError("Expected 1st item of the {} specification tuple for {} ({}) to be "
1862
                                                  "the name of a {} of its 2nd item ({})".
1863
                                                  format(connectee_port_type.__name__, owner.name, port_Name,
1864
                                                         connects_with, mech_item.name))
1865
                        c = _parse_connection_specs(connectee_port_type=connectee_port_type,
1✔
1866
                                                    owner=owner,
1867
                                                    connections=ProjectionTuple(port_Name,
1868
                                                                                weight, exponent,
1869
                                                                                mech_item))
1870
                        connect_with_ports.extend(c)
1✔
1871
                    # Move on to other connections
1872
                    continue
1✔
1873
                # Otherwise, go on to process (<Port name>, Mechanism) spec
1874
                port_spec = port_item
1✔
1875
                projection_spec = None
1✔
1876
                mech=mech_item
1✔
1877

1878
            # Validate port specification, and get actual port referenced if it has been instantiated
1879
            try:
1✔
1880
                # FIX: 11/28/17 HACK TO DEAL WITH GatingSignal Projection to OutputPort
1881
                # FIX: 5/11/19: CORRECTED TO HANDLE ControlMechanism SPECIFIED FOR GATING
1882
                if ((_is_gating_spec(first_item) or _is_control_spec(first_item))
1✔
1883
                    and (isinstance(last_item, OutputPort) or last_item == OutputPort)
1884
                ):
1885
                    projection_socket = SENDER
1✔
1886
                    port_types = [OutputPort]
1✔
1887
                    mech_port_attribute = [OUTPUT_PORTS]
1✔
1888
                else:
1889
                    port_types = connects_with
1✔
1890
                    mech_port_attribute=connect_with_attr
1✔
1891

1892
                port = _get_port_for_socket(owner=owner,
1✔
1893
                                              connectee_port_type=connectee_port_type,
1894
                                              port_spec=port_spec,
1895
                                              port_types=port_types,
1896
                                              mech=mech,
1897
                                              mech_port_attribute=mech_port_attribute,
1898
                                              projection_socket=projection_socket)
1899
                if not isinstance(port, Port) and not any([p in port_types for p in convert_to_list(port)]):
1!
1900
                    if isinstance(port, list):
×
1901
                        type_list = ' ,'.join([p.__name__ for p in port])
×
1902
                    from_or_to_1 = f"from" if projection_socket == RECEIVER else "to"
×
1903
                    from_or_to_2 = f"to" if projection_socket == RECEIVER else "from"
×
1904
                    raise ProjectionError(f"Specification of the Projection {from_or_to_1} "
1905
                                          f"'{connectee_port_type.__name__}' of '{owner.name}' {from_or_to_2} "
1906
                                          f"'{projection_spec.name}' resolves to a type of Port ({type_list}) that is "
1907
                                          f"not of a required type ({' ,'.join([pt.__name__ for pt in port_types])}).")
1908
            except PortError as e:
×
1909
                raise ProjectionError(f"Problem with specification for {Port.__name__} in {Projection.__name__} "
1910
                                      f"specification{(' for ' + owner.name) if owner else ' '}: " + e.args[0])
1911

1912
            # Check compatibility with any Port(s) returned by _get_port_for_socket
1913

1914
            if isinstance(port, list):
1!
1915
                ports = port
×
1916
            else:
1917
                ports = [port]
1✔
1918

1919
            for item in ports:
1✔
1920
                if inspect.isclass(item):
1✔
1921
                    port_type = item
1✔
1922
                else:
1923
                    port_type = item.__class__
1✔
1924

1925
                # # Test that port_type is in the list for port's connects_with
1926
                from psyneulink.core.components.ports.modulatorysignals.controlsignal import ControlSignal
1✔
1927

1928
                # KAM 7/26/18 modified to allow ControlMechanisms to be terminal nodes of compositions
1929
                # We could only include ControlSignal in the allowed types if the receiver is a CIM?
1930
                allowed = connects_with + modulators + [ControlSignal]
1✔
1931

1932
                if not any(issubclass(connects_with_port, port_type)
1!
1933
                           for connects_with_port in allowed):
1934
                    spec = projection_spec or port_type.__name__
×
1935
                    raise ProjectionError(f"Projection specification (\'{spec}\') for an incompatible connection: "
1936
                                          f"{port_type.__name__} with {connectee_port_type.__name__} of {owner.name};"
1937
                                          f" spec should be one of the following: "
1938
                                          f"{' or '.join([r for r in port_type.canReceive])}, "
1939
                                          f" or connectee should be one of the following: "
1940
                                          f"{' or '.join([c.__name__ for c in connects_with])},")
1941

1942
            # Parse projection specification into Projection specification dictionary
1943
            # Validate projection specification
1944
            if _is_projection_spec(projection_spec) or _is_modulatory_spec(projection_spec) or projection_spec is None:
1✔
1945

1946
                # FIX: 11/21/17 THIS IS A HACK TO DEAL WITH GatingSignal Projection TO InputPort or OutputPort
1947
                from psyneulink.core.components.ports.inputport import InputPort
1✔
1948
                from psyneulink.core.components.ports.outputport import OutputPort
1✔
1949
                from psyneulink.core.components.ports.modulatorysignals.gatingsignal import GatingSignal
1✔
1950
                from psyneulink.core.components.projections.modulatory.gatingprojection import GatingProjection
1✔
1951
                from psyneulink.core.components.projections.modulatory.controlprojection import ControlProjection
1✔
1952
                if (not isinstance(projection_spec, GatingProjection)
1✔
1953
                    and isinstance(port, GatingSignal)
1954
                    and connectee_port_type in {InputPort, OutputPort}):
1955
                    projection_spec = port
1✔
1956
                if (
1✔
1957
                        (not isinstance(projection_spec, GatingProjection)
1958
                         and port.__class__ == GatingSignal
1959
                         and connectee_port_type in {InputPort, OutputPort})
1960
                # # MODIFIED 9/27/19 NEW: [JDC]
1961
                #     or
1962
                #         (not isinstance(projection_spec, ControlProjection)
1963
                #          and port.__class__ == ControlSignal
1964
                #          and connectee_port_type in {InputPort, OutputPort})
1965
                ):
1966
                    projection_spec = port
1✔
1967

1968
                elif (_is_gating_spec(first_item) or _is_control_spec((first_item))
1✔
1969
                      and not isinstance(last_item, (GatingProjection, ControlProjection))):
1970
                    projection_spec = first_item
1✔
1971
                projection_spec = _parse_projection_spec(projection_spec,
1✔
1972
                                                         owner=owner,
1973
                                                         port_type=connectee_port_type)
1974

1975
                _validate_connection_request(owner,
1✔
1976
                                             connects_with + modulators,
1977
                                             projection_spec,
1978
                                             projection_socket,
1979
                                             connectee_port_type)
1980
            else:
1981
                raise ProjectionError(f"Invalid {Projection.__name__} specification ({projection_spec}) "
1982
                                      f"for connection between {port_type.__name__} '{port.name}' "
1983
                                      f"and {connectee_port_type.__name__} of '{owner.name}'.")
1984

1985
            connect_with_ports.extend([ProjectionTuple(port, weight, exponent, projection_spec)])
1✔
1986

1987
        else:
1988
            # raise ProjectionError(f"Unrecognized, invalid or insufficient specification "
1989
            #                       f"of connection for {owner.name}: '{connection}'.")
1990
            pass
×
1991

1992
    if not all(isinstance(projection_tuple, ProjectionTuple) for projection_tuple in connect_with_ports):
1✔
1993
        raise ProjectionError("PROGRAM ERROR: Not all items are ProjectionTuples for {}".format(owner.name))
1994

1995
    return connect_with_ports
1✔
1996

1997

1998
@beartype
1✔
1999
def _validate_connection_request(
1✔
2000
        owner,  # Owner of Port seeking connection
2001
        connect_with_ports: list,  # Port to which connection is being sought
2002
        projection_spec, #: _is_projection_spec,  # projection specification
2003
        projection_socket: str,  # socket of Projection to be connected to target port
2004
        connectee_port: Optional[Type] = None):  # Port for which connection is being sought
2005
    """Validate that a Projection specification is compatible with the Port to which a connection is specified
2006

2007
    Carries out undirected validation (i.e., without knowing whether the connectee is the sender or receiver).
2008
    Use _validate_receiver or ([TBI] validate_sender) for directed validation.
2009
    Note: connectee_port is used only for name in errors
2010

2011
    If projection_spec is a Projection:
2012
        - if it is instantiated, compare the projection_socket specified (sender or receiver) with connect_with_port
2013
        - if it in deferred_init, check to see if the specified projection_socket has been specified in _init_args;
2014
            otherwise, use Projection's type
2015
    If projection_spec is a class specification, use Projection's type
2016
    If projection_spec is a dict:
2017
        - check if there is an entry for the socket and if so, use that
2018
        - otherwise, check to see if there is an entry for the Projection's type
2019

2020
    Returns:
2021
        `True` if validation has been achieved to same level (though possibly with warnings);
2022
        `False` if validation could not be done;
2023
        raises an exception if an incompatibility is detected.
2024
    """
2025

2026

2027
    if connectee_port:
1!
2028
        connectee_str = " {} of".format(connectee_port.__name__)
1✔
2029
    else:
2030
        connectee_str = ""
×
2031

2032
    # Convert connect_with_ports (a set of classes) into a tuple for use as second arg in isinstance()
2033
    connect_with_ports = tuple(connect_with_ports)
1✔
2034
    # Make sure none of its entries are None (which will fail in isinstance()):
2035
    if None in connect_with_ports:
1✔
2036
        raise ProjectionError("PROGRAM ERROR: connect_with_ports ({}) should not have any entries that are \'None\'; "
2037
                              "Check assignments to \'connectsWith' and \'modulators\' for each Port class".
2038
                              format(connect_with_ports))
2039

2040
    connect_with_port_Names = ", ".join([c.__name__ for c in connect_with_ports if c is not None])
1✔
2041

2042
    # Used below
2043
    def _validate_projection_type(projection_class):
1✔
2044
        # Validate that Projection's type can connect with a class in connect_with_ports
2045
        if any(port.__name__ in getattr(projection_class.sockets, projection_socket) for port in connect_with_ports):
1✔
2046
            return True
1✔
2047
        else:
2048
            return False
1✔
2049

2050
    # If it is an actual Projection
2051
    if isinstance(projection_spec, Projection):
1✔
2052

2053
        # It is in deferred_init status
2054
        if projection_spec.initialization_status == ContextFlags.DEFERRED_INIT:
1✔
2055

2056
            # Try to get the Port to which the Projection will be connected when fully initialized
2057
            #     as confirmation that it is the correct type for port_type
2058
            try:
1✔
2059
                projection_socket_port = projection_spec.socket_assignments[projection_socket]
1✔
2060
            # Port for projection's socket couldn't be determined
2061
            except KeyError:
×
2062
                # Use Projection's type for validation
2063
                # At least validate that Projection's type can connect with a class in connect_with_ports
2064
                return _validate_projection_type(projection_spec.__class__)
×
2065
                    # Projection's socket has been assigned to a Port
2066
            else:
2067
                # if both SENDER and RECEIVER are specified:
2068
                if projection_spec._init_args[SENDER] and projection_spec._init_args[RECEIVER]:
1✔
2069
                    # Validate that the Port is a class in connect_with_ports
2070
                    if (isinstance(projection_socket_port, connect_with_ports) or
1✔
2071
                            (inspect.isclass(projection_socket_port)
2072
                             and issubclass(projection_socket_port, connect_with_ports))):
2073
                        return True
1✔
2074
                # Otherwise, revert again to validating Projection's type
2075
                else:
2076
                    return _validate_projection_type(projection_spec.__class__)
1✔
2077

2078
        # Projection has been instantiated
2079
        else:
2080
            # Determine whether the Port to which the Projection's socket has been assigned is in connect_with_ports
2081
            # FIX: 11/4/17 - THIS IS A HACK TO DEAL WITH THE CASE IN WHICH THE connectee_port IS AN OutputPort
2082
            # FIX:               THE projection_socket FOR WHICH IS USUALLY A RECEIVER;
2083
            # FIX:           HOWEVER, IF THE projection_spec IS A GatingSignal
2084
            # FIX:               THEN THE projection_socket MUST BE SENDER
2085
            from psyneulink.core.components.ports.outputport import OutputPort
1✔
2086
            from psyneulink.core.components.projections.modulatory.gatingprojection import GatingProjection
1✔
2087
            from psyneulink.core.components.projections.modulatory.controlprojection import ControlProjection
1✔
2088
            if connectee_port is OutputPort and isinstance(projection_spec, (GatingProjection, ControlProjection)):
1✔
2089
                projection_socket = SENDER
1✔
2090
            projection_socket_port = getattr(projection_spec, projection_socket)
1✔
2091
            if issubclass(projection_socket_port.__class__, connect_with_ports):
1✔
2092
                return True
1✔
2093

2094
        # None of the above worked, so must be incompatible
2095
        raise ProjectionError("{} specified to be connected with{} {} "
2096
                              "is not consistent with the {} of the specified {} ({})".
2097
                              format(Port.__name__, connectee_str, owner.name,
2098
                                     projection_socket, Projection.__name__, projection_spec))
2099

2100
    # Projection class
2101
    elif inspect.isclass(projection_spec) and issubclass(projection_spec, Port):
1!
2102
        if issubclass(projection_spec, connect_with_ports):
×
2103
            return True
×
2104
        raise ProjectionError("{} type specified to be connected with{} {} ({}) "
2105
                              "is not compatible with the {} of the specified {} ({})".
2106
                              format(Port.__name__, connectee_str, owner.name, projection_spec.__name__,
2107
                                     projection_socket, Projection.__name__, connect_with_port_Names))
2108

2109
    # Port
2110
    elif isinstance(projection_spec, Port):
1!
2111
        if isinstance(projection_spec, connect_with_ports):
×
2112
            return True
×
2113
        raise ProjectionError("{} specified to be connected with{} {} ({}) "
2114
                              "is not compatible with the {} of the specified {} ({})".
2115
                              format(Port.__name__, connectee_str, owner.name, projection_spec,
2116
                                     projection_socket, Projection.__name__, connect_with_port_Names))
2117

2118
    # Port class
2119
    elif inspect.isclass(projection_spec) and issubclass(projection_spec, Projection):
1!
2120
        _validate_projection_type(projection_spec)
×
2121
        return True
×
2122

2123
    # Projection specification dictionary
2124
    elif isinstance(projection_spec, dict):
1!
2125
        # Try to validate using entry for projection_socket
2126
        if projection_socket in projection_spec and projection_spec[projection_socket] is not None:
1!
2127
            # Specification for the [projection_socket] entry (i.e., SENDER or RECEIVER)
2128
            #    should be either of the correct class or a Mechanism
2129
            #    (which assumes it will get properly resolved in context when the Projection is instantiated)
2130
            if (projection_spec[projection_socket] in connect_with_ports or
×
2131
                    isinstance(projection_spec[projection_socket], Mechanism)):
2132
                return True
×
2133
            else:
2134
                raise ProjectionError("{} ({}) specified to be connected with{} {} is not compatible "
2135
                                      "with the {} ({}) in the specification dict for the {}.".
2136
                                      format(Port.__name__,
2137
                                             connect_with_port_Names,
2138
                                             connectee_str,
2139
                                             owner.name,
2140
                                             projection_socket,
2141
                                             projection_spec[projection_socket],
2142
                                             Projection.__name__))
2143
        # Try to validate using entry for Projection' type
2144
        elif PROJECTION_TYPE in projection_spec and projection_spec[PROJECTION_TYPE] is not None:
1!
2145
            _validate_projection_type(projection_spec[PROJECTION_TYPE])
1✔
2146
            return True
1✔
2147

2148
    # Projection spec is too abstract to validate here
2149
    #    (e.g., value or a name that will be used in context to instantiate it)
2150
    return False
×
2151

2152
def _get_projection_value_shape(sender, matrix):
1✔
2153
    """Return shape of a Projection's value given its sender and matrix"""
2154
    from psyneulink.core.components.functions.nonstateful.transferfunctions import get_matrix
1✔
2155
    matrix = get_matrix(matrix)
1✔
2156
    return np.zeros(matrix.shape[np.atleast_1d(sender.value).ndim :])
1✔
2157

2158
# IMPLEMENTATION NOTE: MOVE THIS TO ModulatorySignals WHEN THAT IS IMPLEMENTED
2159
@check_user_specified
1✔
2160
@beartype
1✔
2161
def _validate_receiver(sender_mech:Mechanism,
1✔
2162
                       projection:Projection,
2163
                       expected_owner_type:type,
2164
                       spec_type=None,
2165
                       context=None):
2166
    """Check that Projection is to expected_receiver_type.
2167

2168
    expected_owner_type must be a Mechanism or a Projection
2169
    spec_type should be LEARNING_SIGNAL, CONTROL_SIGNAL or GATING_SIGNAL
2170

2171
    Note:  this is a "directed" validation;
2172
           for undirected validation of a Projection, use _validate_projection_specification
2173

2174
    """
2175
    spec_type = " in the {} arg ".format(spec_type) or ""
×
2176

2177
    if projection.initialization_status == ContextFlags.DEFERRED_INIT:
×
2178
        # receiver = projection._init_args['receiver'].owner
2179
        port = projection._init_args['receiver']
×
2180
        receiver = port.owner
×
2181
    else:
2182
        # receiver = projection.receiver.owner
2183
        port = projection.receiver
×
2184
        receiver = port.owner
×
2185

2186
    if isinstance(receiver, Mechanism):
×
2187
        receiver_mech = receiver
×
2188
    elif isinstance(receiver, Projection):
×
2189
        receiver_mech = receiver.receiver.owner
×
2190
    else:
2191
        raise ProjectionError("receiver of projection ({}) must be a {} or {}".
2192
                              format(projection.name, MECHANISM, PROJECTION))
2193

2194
    if not isinstance(receiver, expected_owner_type):
×
2195
        raise ProjectionError("A {} specified {}for {} ({}) projects to a component other than the {} of a {}".
2196
                                    format(projection.__class__.__name__,
2197
                                           spec_type,
2198
                                           sender_mech.name,
2199
                                           receiver,
2200
                                           port.__class__.__name__,
2201
                                           expected_owner_type.__name__))
2202

2203
# IMPLEMENTATION NOTE:  THIS SHOULD BE MOVED TO COMPOSITION ONCE THAT IS IMPLEMENTED
2204
def _add_projection_to(receiver, port, projection_spec, context=None):
1✔
2205
    """Assign an "incoming" Projection to a receiver InputPort or ParameterPort of a Component object
2206

2207
    Verify that projection has not already been assigned to receiver;
2208
        if it has, issue a warning and ignore the assignment request.
2209

2210
    Requirements:
2211
       * receiver must be an appropriate Component object (currently, a Mechanism or a Projection);
2212
       * port must be a specification of an InputPort or ParameterPort;
2213
       * specification of InputPort can be any of the following:
2214
                - INPUT_PORT - assigns projection_spec to (primary) InputPort;
2215
                - InputPort object;
2216
                - index for Mechanism.input_ports;
2217
                - name of an existing InputPort (i.e., key for Mechanism.input_ports);
2218
                - the keyword ADD_INPUT_PORT or the name for an InputPort to be added;
2219
       * specification of ParameterPort must be a ParameterPort object
2220
       * projection_spec can be any valid specification of a projection_spec
2221
           (see `Port._instantiate_projections_to_port`).
2222

2223
    Args:
2224
        receiver (Mechanism or Projection)
2225
        port (Port subclass)
2226
        projection_spec: (Projection, dict, or str)
2227
        context
2228

2229
    """
2230
    # IMPLEMENTATION NOTE:  ADD FULL SET OF ParameterPort SPECIFICATIONS
2231
    #                       CURRENTLY, ASSUMES projection_spec IS AN ALREADY INSTANTIATED PROJECTION
2232

2233
    from psyneulink.core.components.ports.port import _instantiate_port
1✔
2234
    from psyneulink.core.components.ports.port import Port_Base
1✔
2235
    from psyneulink.core.components.ports.inputport import InputPort
1✔
2236

2237
    if not isinstance(port, (int, str, Port)):
1✔
2238
        raise ProjectionError("Port specification(s) for {} (as receiver(s) of {}) contain(s) one or more items"
2239
                             " that is not a name, reference to a {} or an index for one".
2240
                              format(receiver.name, projection_spec.name, Port.__name__))
2241

2242
    # port is Port object, so use thatParameterPort
2243
    if isinstance(port, Port_Base):
1!
2244
        return port._instantiate_projections_to_port(projections=projection_spec, context=context)
1✔
2245

2246
    # Generic INPUT_PORT is specified, so use (primary) InputPort
2247
    elif port == INPUT_PORT:
×
2248
        return receiver.input_port._instantiate_projections_to_port(projections=projection_spec, context=context)
×
2249

2250
    # input_port is index into input_ports OrderedDict, so get corresponding key and assign to input_port
2251
    elif isinstance(port, int):
×
2252
        try:
×
2253
            key = receiver.input_ports[port]
×
2254
        except IndexError:
×
2255
            raise ProjectionError(
2256
                f"Attempt to assign projection_spec ('{projection_spec.name}') to InputPort {'port'} "
2257
                f"of {'receiver.name'} but it has only {len(receiver.input_ports)} input_ports.")
2258
        else:
2259
            input_port = key
×
2260

2261
    # input_port is string (possibly key retrieved above)
2262
    #    so try as key in input_ports OrderedDict (i.e., as name of an InputPort)
2263
    if isinstance(port, str):
×
2264
        try:
×
2265
            return receiver.input_port[port]._instantiate_projections_to_port(projections=projection_spec, context=context)
×
2266
        except KeyError:
×
2267
            pass
×
2268
        else:
2269
            if receiver.prefs.verbosePref:
×
2270
                warnings.warn("Projection_spec {0} added to {1} of {2}".
×
2271
                              format(projection_spec.name, port, receiver.name))
2272
            # return
2273

2274
    # input_port is either the name for a new InputPort or ADD_INPUT_PORT
2275
    if not port == ADD_INPUT_PORT:
×
2276
        if receiver.prefs.verbosePref:
×
2277
            reassign = input("\nAdd new InputPort named {0} to {1} (as receiver for {2})? (y/n):".
×
2278
                             format(input_port, receiver.name, projection_spec.name))
2279
            while reassign != 'y' and reassign != 'n':
×
2280
                reassign = input("\nAdd {0} to {1}? (y/n):".format(input_port, receiver.name))
×
2281
            if reassign == 'n':
×
2282
                raise ProjectionError("Unable to assign projection {0} to receiver {1}".
2283
                                      format(projection_spec.name, receiver.name))
2284

2285
    # validate that projection has not already been assigned to receiver
2286
    if receiver.verbosePref or projection_spec.sender.owner.verbosePref:
×
2287
        if projection_spec in receiver.all_afferents:
×
2288
            warnings.warn("Request to assign {} as projection to {} was ignored; it was already assigned".
×
2289
                          format(projection_spec.name, receiver.owner.name))
2290

2291
    input_port = _instantiate_port(owner=receiver,
×
2292
                                     port_type=InputPort,
2293
                                     name=input_port,
2294
                                     reference_value=projection_spec.value,
2295
                                     reference_value_name='Projection_spec value for new InputPort',
2296
                                     context=context)
2297

2298
    #  Update InputPort and input_ports
2299
    if receiver.input_ports:
×
2300
        receiver.parameters.input_ports._get(context)[input_port.name] = input_port
×
2301

2302
    # No InputPort(s) yet, so create them
2303
    else:
2304
        receiver.parameters.input_ports._set(
×
2305
            ContentAddressableList(
2306
                component_type=Port_Base,
2307
                list=[input_port],
2308
                name=receiver.name + '.input_ports'
2309
            ),
2310
            context
2311
        )
2312

2313
    return input_port._instantiate_projections_to_port(projections=projection_spec, context=context)
×
2314

2315

2316
# IMPLEMENTATION NOTE:  THIS SHOULD BE MOVED TO COMPOSITION ONCE THAT IS IMPLEMENTED
2317
def _add_projection_from(sender, port, projection_spec, receiver, context=None):
1✔
2318
    """Assign an "outgoing" Projection from an OutputPort of a sender Mechanism
2319

2320
    projection_spec can be any valid specification of a projection_spec (see Port._instantiate_projections_to_port)
2321
    port must be a specification of an OutputPort
2322
    Specification of OutputPort can be any of the following:
2323
            - OUTPUT_PORT - assigns projection_spec to (primary) OutputPort
2324
            - OutputPort object
2325
            - index for Mechanism OutputPorts OrderedDict
2326
            - name of OutputPort (i.e., key for Mechanism.OutputPorts OrderedDict))
2327
            - the keyword ADD_OUTPUT_PORT or the name for an OutputPort to be added
2328

2329
    Args:
2330
        sender (Mechanism):
2331
        projection_spec: (Projection, dict, or str)
2332
        port (OutputPort, str, or value):
2333
        context:
2334
    """
2335

2336

2337
    from psyneulink.core.components.ports.port import _instantiate_port
×
2338
    from psyneulink.core.components.ports.port import Port_Base
×
2339
    from psyneulink.core.components.ports.outputport import OutputPort
×
2340

2341
    # Validate that projection is not already assigned to sender; if so, warn and ignore
2342

2343
    if isinstance(projection_spec, Projection):
×
2344
        projection = projection_spec
×
2345
        if ((isinstance(sender, OutputPort) and projection.sender is sender) or
×
2346
                (isinstance(sender, Mechanism) and projection.sender is sender.output_port)):
2347
            if sender.verbosePref:
×
2348
                warnings.warn("Request to assign {} as sender of {}, but it has already been assigned".
×
2349
                              format(sender.name, projection.name))
2350
                return
×
2351

2352
    if not isinstance(port, (int, str, OutputPort)):
×
2353
        raise ProjectionError("Port specification for {0} (as sender of {1}) must be the name, reference to "
2354
                              "or index of an OutputPort of {0} )".format(sender.name, projection_spec))
2355

2356
    # port is Port object, so use that
2357
    if isinstance(port, Port_Base):
×
2358
        port._instantiate_projection_from_port(projection_spec=projection_spec, receiver=receiver, context=context)
×
2359
        return
×
2360

2361
    # Generic OUTPUT_PORT is specified, so use (primary) OutputPort
2362
    elif port == OUTPUT_PORT:
×
2363
        sender.output_port._instantiate_projections_to_port(projections=projection_spec, context=context)
×
2364
        return
×
2365

2366
    # input_port is index into OutputPorts OrderedDict, so get corresponding key and assign to output_port
2367
    elif isinstance(port, int):
×
2368
        try:
×
2369
            key = list(sender.output_ports.keys)[port]
×
2370
        except IndexError:
×
2371
            raise ProjectionError(f"Attempt to assign projection_spec ('{projection_spec.name}') to OutputPort {port} "
2372
                                  f"of {'sender.name'}, but it has only {len(sender.output_ports)} OutputPorts.")
2373
        else:
2374
            output_port = key
×
2375

2376
    # output_port is string (possibly key retrieved above)
2377
    #    so try as key in output_ports ContentAddressableList (i.e., as name of an OutputPort)
2378
    if isinstance(port, str):
×
2379
        try:
×
2380
            sender.output_port[port]._instantiate_projections_to_port(projections=projection_spec, context=context)
×
2381
        except KeyError:
×
2382
            pass
×
2383
        else:
2384
            if sender.prefs.verbosePref:
×
2385
                warnings.warn("Projection_spec {0} added to {1} of {2}".
×
2386
                              format(projection_spec.name, port, sender.name))
2387
            # return
2388

2389
    # output_port is either the name for a new OutputPort or ADD_OUTPUT_PORT
2390
    if not port == ADD_OUTPUT_PORT:
×
2391
        if sender.prefs.verbosePref:
×
2392
            reassign = input("\nAdd new OutputPort named {0} to {1} (as sender for {2})? (y/n):".
×
2393
                             format(output_port, sender.name, projection_spec.name))
2394
            while reassign != 'y' and reassign != 'n':
×
2395
                reassign = input("\nAdd {0} to {1}? (y/n):".format(output_port, sender.name))
×
2396
            if reassign == 'n':
×
2397
                raise ProjectionError("Unable to assign projection {0} to sender {1}".
2398
                                      format(projection_spec.name, sender.name))
2399

2400
    output_port = _instantiate_port(owner=sender,
×
2401
                                      port_type=OutputPort,
2402
                                      name=output_port,
2403
                                      reference_value=projection_spec.value,
2404
                                      reference_value_name='Projection_spec value for new InputPort',
2405
                                      context=context)
2406
    #  Update output_port and output_ports
2407
    try:
×
2408
        sender.output_ports[output_port.name] = output_port
×
2409
    # No OutputPort(s) yet, so create them
2410
    except AttributeError:
×
2411
        from psyneulink.core.components.ports.port import Port_Base
×
2412
        sender.parameters.output_ports._set(
×
2413
            ContentAddressableList(
2414
                component_type=Port_Base,
2415
                list=[output_port],
2416
                name=sender.name + '.output_ports'
2417
            )
2418
        )
2419

2420
    output_port._instantiate_projections_to_port(projections=projection_spec, context=context)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc