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

PrincetonUniversity / PsyNeuLink / 15917726689

27 Jun 2025 03:50AM UTC coverage: 84.497% (+0.02%) from 84.473%
15917726689

Pull #3299

github

web-flow
Merge branch 'devel' into docs/BasicAndPrimer/rename-encoder-to-ffn
Pull Request #3299: docs: rename 'encoder' to 'feedforward network'

9912 of 12968 branches covered (76.43%)

Branch coverage included in aggregate %.

34493 of 39584 relevant lines covered (87.14%)

0.87 hits per line

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

78.69
/psyneulink/library/components/projections/pathway/autoassociativeprojection.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
# *******************************************  AutoAssociativeProjection ***********************************************
10

11
"""
12

13
Contents
14
--------
15

16
  * `AutoAssociative_Overview`
17
  * `AutoAssociative_Creation`
18
  * `AutoAssociative_Structure`
19
      - `AutoAssociative_Configurable_Attributes`
20
  * `AutoAssociative_Execution`
21
  * `AutoAssociative_Class_Reference`
22

23

24
.. _AutoAssociative_Overview:
25

26
Overview
27
--------
28

29
An AutoAssociativeProjection is a subclass of `MappingProjection` that acts as the recurrent projection for a
30
`RecurrentTransferMechanism`. The primary difference between an AutoAssociativeProjection and a basic MappingProjection
31
is that an AutoAssociativeProjection uses the `auto <RecurrentTransferMechanism.auto>` and
32
`hetero <RecurrentTransferMechanism.hetero>` parameters *on the RecurrentTransferMechanism* to help update its matrix:
33
this allows for a `ControlMechanism <ControlMechanism>` to control the `auto <RecurrentTransferMechanism.auto>` and
34
`hetero <RecurrentTransferMechanism.hetero>` parameters and thereby control the matrix.
35

36
AutoAssociativeProjection represents connections between nodes in a single-layer recurrent network. It multiplies
37
the output of the `RecurrentTransferMechanism` by a matrix, then presents the product as input to the
38
`RecurrentTransferMechanism`.
39

40

41

42
.. _AutoAssociative_Creation:
43

44
Creating an AutoAssociativeProjection
45
-------------------------------------
46

47
An AutoAssociativeProjection is created automatically by a RecurrentTransferMechanism (or its subclasses), and is
48
stored as the `recurrent_projection <RecurrentTransferMechanism.recurrent_projection>` parameter of the
49
RecurrentTransferMechanism. It is not recommended to create an AutoAssociativeProjection on its own, because during
50
execution an AutoAssociativeProjection references parameters owned by its RecurrentTransferMechanism (see
51
`Execution <AutoAssociative_Execution>` below).
52

53
.. _AutoAssociative_Structure:
54

55
Auto Associative Structure
56
--------------------------
57

58
In structure, the AutoAssociativeProjection is almost identical to a MappingProjection: the only additional attributes
59
are `auto <AutoAssociativeProjection.auto>` and `hetero <AutoAssociativeProjection.hetero>`.
60

61
.. _AutoAssociative_Configurable_Attributes:
62

63
*Configurable Attributes*
64
~~~~~~~~~~~~~~~~~~~~~~~~~
65

66
Due to its specialized nature, most parameters of the AutoAssociativeProjection are not configurable: the `variable` is
67
determined by the format of the output of the RecurrentTransferMechanism, the `function` is always MatrixTransform, and so
68
on. The only configurable parameter is the matrix, configured through the **matrix**, **auto**, and/or **hetero**
69
arguments for a RecurrentTransferMechanism:
70

71
.. _AutoAssociative_Matrix:
72

73
* **matrix** - multiplied by the input to the AutoAssociativeProjection in order to produce the output. Specification of
74
  the **matrix**, **auto**, and/or **hetero** arguments determines the values of the matrix; **auto** determines the
75
  diagonal entries (representing the strength of the connection from each node to itself) and **hetero** determines
76
  the off-diagonal entries (representing connections between nodes).
77

78
.. _AutoAssociative_Execution:
79

80
Execution
81
---------
82

83
An AutoAssociativeProjection uses its `matrix <AutoAssociativeProjection.matrix>` parameter to transform the value of
84
its `sender <AutoAssociativeProjection.sender>`, and provide the result as input for its
85
`receiver <AutoAssociativeProjection.receiver>`, the primary InputPort of the RecurrentTransferMechanism.
86

87
.. note::
88
     During execution the AutoAssociativeProjection updates its `matrix <AutoAssociativeProjection.matrix> parameter
89
     based on the `auto <RecurrentTransferMechanism.auto>` and `hetero <RecurrentTransferMechanism.hetero>` parameters
90
     *on the `RecurrentTransferMechanism`*. (The `auto <AutoAssociativeProjection.auto>` and
91
     `hetero <AutoAssociativeProjection.hetero>` parameters of the AutoAssociativeProjection simply refer to their
92
     counterparts on the RecurrentTransferMechanism as well.) The reason for putting the `auto
93
     <RecurrentTransferMechanism.auto>` and `hetero <RecurrentTransferMechanism.hetero>` parameters on the
94
     RecurrentTransferMechanism is that this allows them to be modified by a `ControlMechanism <ControlMechanism>`.
95

96
.. _AutoAssociative_Class_Reference:
97

98
Class Reference
99
---------------
100

101
"""
102
import numpy as np
1✔
103
from beartype import beartype
1✔
104

105
from psyneulink._typing import Optional
1✔
106

107
from psyneulink.core.components.component import parameter_keywords
1✔
108
from psyneulink.core.components.functions.nonstateful.transformfunctions import MatrixTransform
1✔
109
from psyneulink.core.components.functions.function import get_matrix
1✔
110
from psyneulink.core.components.projections.pathway.mappingprojection import MappingError, MappingProjection
1✔
111
from psyneulink.library.components.projections.pathway.maskedmappingprojection import MaskedMappingProjection
1✔
112
from psyneulink.core.components.projections.projection import projection_keywords
1✔
113
from psyneulink.core.components.shellclasses import Mechanism
1✔
114
from psyneulink.core.components.ports.outputport import OutputPort
1✔
115
from psyneulink.core.globals.keywords import AUTO_ASSOCIATIVE_PROJECTION, DEFAULT_MATRIX, HOLLOW_MATRIX, FUNCTION, OWNER_MECH
1✔
116
from psyneulink.core.globals.parameters import SharedParameter, Parameter, check_user_specified
1✔
117
from psyneulink.core.globals.preferences.basepreferenceset import ValidPrefSet
1✔
118
from psyneulink.core.globals.preferences.preferenceset import PreferenceLevel
1✔
119
from psyneulink.core.globals.utilities import is_numeric_scalar
1✔
120

121
__all__ = [
1✔
122
    'AutoAssociativeError', 'AutoAssociativeProjection', 'get_auto_matrix', 'get_hetero_matrix',
123
]
124

125
parameter_keywords.update({AUTO_ASSOCIATIVE_PROJECTION})
1✔
126
projection_keywords.update({AUTO_ASSOCIATIVE_PROJECTION})
1✔
127

128

129
class AutoAssociativeError(MappingError):
1✔
130
    pass
1✔
131

132

133
class AutoAssociativeProjection(MaskedMappingProjection):
1✔
134
    """
135
    AutoAssociativeProjection(
136
        )
137

138
    Subclass of `MappingProjection` that is self-recurrent on a `RecurrentTransferMechanism`.
139
    See `MappingProjection <MappingProjection_Class_Reference>` and `Projection <Projection_Class_Reference>`
140
    for additional arguments and attributes.
141

142
    COMMENT:
143
        JDC [IN GENERAL WE HAVE TRIED TO DISCOURAGE SUCH DEPENDENCIES;  BETTER TO HAVE IT ACCEPT ARGUMENTS THAT
144
        RecurrentTransferMechanism (or Composition) PROVIDES THAN ASSUME THEY ARE ON ANOTHER OBJECT THAT CREATED
145
        THIS ONE]
146
    Note: The reason **auto** and **hetero** are not arguments to the constructor of the AutoAssociativeProjection is
147
    because it is only ever created by a RecurrentTransferMechanism: by the time the AutoAssociativeProjection is
148
    created, the **auto** and **hetero** arguments are already incorporated into the **matrix** argument.
149

150
    COMMENT
151

152
    Arguments
153
    ---------
154

155
    sender : OutputPort or Mechanism : default None
156
        specifies the source of the Projection's input; must be (or belong to) the same Mechanism as **receiver**,
157
        and the length of its `value <OutputPort.value>` must match that of the `variable <InputPort.variable>` of
158
        the **receiver**.
159

160
    receiver: InputPort or Mechanism : default None
161
        specifies the destination of the Projection's output; must be (or belong to) the same Mechanism as **sender**,
162
        and the length of its `variable <InputPort.variable>` must match the `value <OutputPort.value>` of **sender**.
163

164
    matrix : list, np.ndarray, function or keyword : default DEFAULT_MATRIX
165
        specifies the matrix used by `function <Projection_Base.function>` (default: `LinearCombination`) to
166
        transform the `value <Projection_Base.value>` of the `sender <MappingProjection.sender>` into a value
167
        provided to the `variable <InputPort.variable>` of the `receiver <MappingProjection.receiver>` `InputPort`;
168
        must be a square matrix (i.e., have the same number of rows and columns).
169

170
    Attributes
171
    ----------
172

173
    sender : OutputPort
174
        the `OutputPort` of the `Mechanism <Mechanism>` that is the source of the Projection's input; in the case of
175
        an AutoAssociativeProjection, it is an OutputPort of the same Mechanism to which the `receiver
176
        <AutoAssociativeProjection.receiver>` belongs.
177

178
    receiver: InputPort
179
        the `InputPort` of the `Mechanism <Mechanism>` that is the destination of the Projection's output; in the case
180
        of an AutoAssociativeProjection, it is an InputPort of the same Mechanism to which the `sender
181
        <AutoAssociativeProjection.sender>` belongs.
182

183
    matrix : 2d np.ndarray
184
        square matrix used by `function <AutoAssociativeProjection.function>` to transform input from the `sender
185
        <MappingProjection.sender>` to the value provided to the `receiver <AutoAssociativeProjection.receiver>`;
186
        in the case of an AutoAssociativeProjection.
187

188
    """
189

190
    componentType = AUTO_ASSOCIATIVE_PROJECTION
1✔
191
    className = componentType
1✔
192
    suffix = " " + className
1✔
193

194
    class Parameters(MappingProjection.Parameters):
1✔
195
        """
196
            Attributes
197
            ----------
198

199
                variable
200
                    see `variable <AutoAssociativeProjection.variable>`
201

202
                    :default value: numpy.array([[0]])
203
                    :type: ``numpy.ndarray``
204
                    :read only: True
205

206
                auto
207
                    see `auto <AutoAssociativeProjection.auto>`
208

209
                    :default value: 1
210
                    :type: ``int``
211

212
                function
213
                    see `function <AutoAssociativeProjection.function>`
214

215
                    :default value: `MatrixTransform`
216
                    :type: `Function`
217

218
                hetero
219
                    see `hetero <AutoAssociativeProjection.hetero>`
220

221
                    :default value: 0
222
                    :type: ``int``
223

224
                matrix
225
                    see `matrix <AutoAssociativeProjection.matrix>`
226

227
                    :default value: `AUTO_ASSIGN_MATRIX`
228
                    :type: ``str``
229
        """
230
        variable = Parameter(np.array([[0]]), read_only=True, pnl_internal=True, constructor_argument='default_variable')
1✔
231
        # function is always MatrixTransform that requires 1D input
232
        function = Parameter(MatrixTransform, stateful=False, loggable=False)
1✔
233

234
        auto = SharedParameter(1, attribute_name=OWNER_MECH)
1✔
235
        hetero = SharedParameter(0, attribute_name=OWNER_MECH)
1✔
236
        matrix = SharedParameter(DEFAULT_MATRIX, attribute_name=OWNER_MECH)
1✔
237

238
    classPreferenceLevel = PreferenceLevel.TYPE
1✔
239

240
    @check_user_specified
1✔
241
    @beartype
1✔
242
    def __init__(self,
1✔
243
                 owner=None,
244
                 sender=None,
245
                 receiver=None,
246
                 matrix=None,
247
                 function=None,
248
                 params=None,
249
                 name=None,
250
                 prefs:  Optional[ValidPrefSet] = None,
251
                 **kwargs
252
                 ):
253

254
        if owner is not None:
1✔
255
            if not isinstance(owner, Mechanism):
1✔
256
                raise AutoAssociativeError('Owner of AutoAssociative Mechanism must either be None or a Mechanism')
257
            if sender is None:
1✔
258
                sender = owner
1✔
259
            if receiver is None:
1✔
260
                receiver = owner
1✔
261

262
        super().__init__(sender=sender,
1✔
263
                         receiver=receiver,
264
                         matrix=matrix,
265
                         function=function,
266
                         params=params,
267
                         name=name,
268
                         prefs=prefs,
269
                         **kwargs)
270

271
    # temporary override to make sure matrix/auto/hetero parameters
272
    # get passed properly. should be replaced with a better organization
273
    # of auto/hetero, in which the base parameters are stored either on
274
    # AutoAssociativeProjection or on MatrixTransform itself
275
    def _instantiate_parameter_classes(self, context):
1✔
276
        if FUNCTION not in self.initial_shared_parameters:
1!
277
            try:
1✔
278
                self.initial_shared_parameters[FUNCTION] = {
1✔
279
                    'matrix': self.initial_shared_parameters[OWNER_MECH]['matrix']
280
                }
281
            except KeyError:
×
282
                pass
×
283

284
        super()._instantiate_parameter_classes(context)
1✔
285

286
    # COMMENTED OUT BY KAM 1/9/2018 -- this method is not currently used; should be moved to Recurrent Transfer Mech
287
    #     if it is used in the future
288

289
    # def _update_auto_and_hetero(self, owner_mech=None, runtime_params=None, time_scale=TimeScale.TRIAL, context=None):
290
    #     if owner_mech is None:
291
    #         if isinstance(self.sender, OutputPort):
292
    #             owner_mech = self.sender.owner
293
    #         elif isinstance(self.sender, Mechanism):
294
    #             owner_mech = self.sender
295
    #         else:
296
    #             raise AutoAssociativeError("The sender of the {} \'{}\' must be a Mechanism or OutputPort: currently"
297
    #                                        " the sender is {}".
298
    #                                        format(self.__class__.__name__, self.name, self.sender))
299
    #     if AUTO in owner_mech._parameter_ports and HETERO in owner_mech._parameter_ports:
300
    #         owner_mech._parameter_ports[AUTO].update(context=context, params=runtime_params, time_scale=time_scale)
301
    #         owner_mech._parameter_ports[HETERO].update(context=context, params=runtime_params, time_scale=time_scale)
302
    #
303

304
    # END OF COMMENTED OUT BY KAM 1/9/2018
305

306
    # NOTE 7/25/17 CW: Originally, this override was written because if the user set the 'auto' parameter on the
307
        # recurrent mechanism, the ParameterPort wouldn't update until after the mechanism executed: since the system
308
        # first runs the projection, then runs the mechanism, the projection initially uses the 'old' value. However,
309
        # this is commented out because this may in fact be the desired behavior.
310
        # Two possible solutions: allow control to be done on projections, or build a more general way to allow
311
        # projections to read parameters from mechanisms.
312
    # def _update_parameter_ports(self, runtime_params=None, context=None):
313
    #     """Update this projection's owner mechanism's `auto` and `hetero` parameter ports as well! The owner mechanism
314
    #     should be a RecurrentTransferMechanism, which DOES NOT update its own `auto` and `hetero` parameter ports during
315
    #     its _update_parameter_ports function (so that the ParameterPort is not redundantly updated).
316
    #     Thus, if you want to have an AutoAssociativeProjection on a mechanism that's not a RecurrentTransferMechanism,
317
    #     your mechanism must similarly exclude `auto` and `hetero` from updating.
318
    #     """
319
    #     super()._update_parameter_ports(runtime_params, context)
320
    #
321
    #     if isinstance(self.sender, OutputPort):
322
    #         owner_mech = self.sender.owner
323
    #     elif isinstance(self.sender, Mechanism):
324
    #         owner_mech = self.sender
325
    #     else:
326
    #         raise AutoAssociativeError("The sender of the {} \'{}\' must be a Mechanism or OutputPort: currently the"
327
    #                                    " sender is {}".
328
    #                                    format(self.__class__.__name__, self.name, self.sender))
329
    #
330
    #     if AUTO in owner_mech._parameter_ports and HETERO in owner_mech._parameter_ports:
331
    #         owner_mech._parameter_ports[AUTO].update(context=context, params=runtime_params)
332
    #         owner_mech._parameter_ports[HETERO].update(context=context, params=runtime_params)
333
    #     else:
334
    #         raise AutoAssociativeError("Auto or Hetero ParameterPort not found in {0} \"{1}\"; here are names of the "
335
    #                                    "current ParameterPorts for {1}: {2}".format(owner_mech.__class__.__name__,
336
    #                                    owner_mech.name, owner_mech._parameter_ports.key_values))
337

338
    @property
1✔
339
    def owner_mech(self):
1✔
340
        if isinstance(self.sender, OutputPort):
1!
341
            return self.sender.owner
1✔
342
        elif isinstance(self.sender, Mechanism):
×
343
            return self.sender
×
344
        else:
345
            raise AutoAssociativeError(
346
                "The sender of the {} \'{}\' must be a Mechanism or OutputPort: currently the sender is {}".format(
347
                    self.__class__.__name__, self.name, self.sender
348
                )
349
            )
350

351
    @property
1✔
352
    def matrix(self):
1✔
353
        owner_mech = self.owner_mech
1✔
354
        if hasattr(owner_mech, "matrix"):
1!
355
            return owner_mech.matrix
1✔
356
        return super(AutoAssociativeProjection, self.__class__).matrix.fget(self)
×
357

358
    @matrix.setter
1✔
359
    def matrix(self, setting):
1✔
360
        owner_mech = self.owner_mech
1✔
361
        if hasattr(owner_mech, "matrix"):
1!
362
            owner_mech.matrix = setting
1✔
363
        else:
364
            super(AutoAssociativeProjection, self.__class__).matrix.fset(self, setting)
×
365

366

367
# a helper function that takes a specification of `hetero` and returns a hollow matrix with the right values
368
def get_hetero_matrix(raw_hetero, size):
1✔
369
    if is_numeric_scalar(raw_hetero):
1✔
370
        return get_matrix(HOLLOW_MATRIX, size, size) * raw_hetero
1✔
371
    elif ((isinstance(raw_hetero, np.ndarray) and raw_hetero.ndim == 1) or
1!
372
              (isinstance(raw_hetero, list) and np.array(raw_hetero).ndim == 1)):
373
        if len(raw_hetero) != 1:
×
374
            return None
×
375
        return get_matrix(HOLLOW_MATRIX, size, size) * raw_hetero[0]
×
376
    elif (isinstance(raw_hetero, np.matrix) or
1!
377
              (isinstance(raw_hetero, np.ndarray) and raw_hetero.ndim == 2) or
378
              (isinstance(raw_hetero, list) and np.array(raw_hetero).ndim == 2)):
379
        np.fill_diagonal(raw_hetero, 0)
1✔
380
        return np.array(raw_hetero)
1✔
381
    else:
382
        return None
×
383

384

385
# similar to get_hetero_matrix() above
386
def get_auto_matrix(raw_auto, size):
1✔
387
    if is_numeric_scalar(raw_auto):
1✔
388
        return np.diag(np.full(size, raw_auto, dtype=float))
1✔
389
    elif ((isinstance(raw_auto, np.ndarray) and raw_auto.ndim == 1) or
1!
390
              (isinstance(raw_auto, list) and np.array(raw_auto).ndim == 1)):
391
        if len(raw_auto) == 1:
1✔
392
            return np.diag(np.full(size, raw_auto[0], dtype=float))
1✔
393
        else:
394
            if len(raw_auto) != size:
1!
395
                return None
×
396
            return np.diag(raw_auto)
1✔
397
    elif (isinstance(raw_auto, np.matrix) or
×
398
              (isinstance(raw_auto, np.ndarray) and raw_auto.ndim == 2) or
399
              (isinstance(raw_auto, list) and np.array(raw_auto).ndim == 2)):
400
        # we COULD add a validation here to ensure raw_auto is diagonal, but it would slow stuff down.
401
        return np.array(raw_auto)
×
402
    else:
403
        return None
×
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