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

PrincetonUniversity / PsyNeuLink / 5675844369

pending completion
5675844369

push

github-actions

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

Devel

12416 of 15482 branches covered (80.2%)

Branch coverage included in aggregate %.

2630 of 2630 new or added lines in 94 files covered. (100.0%)

30569 of 35352 relevant lines covered (86.47%)

0.86 hits per line

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

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

12
"""
1✔
13
|
14
Function
15
  * `Function_Base`
16

17
Example function:
18
  * `ArgumentTherapy`
19

20

21
.. _Function_Overview:
22

23
Overview
24
--------
25

26
A Function is a `Component <Component>` that "packages" a function for use by other Components.
27
Every Component in PsyNeuLink is assigned a Function; when that Component is executed, its
28
Function's `function <Function_Base.function>` is executed.  The `function <Function_Base.function>` can be any callable
29
operation, although most commonly it is a mathematical operation (and, for those, almost always uses a call to one or
30
more numpy functions).  There are two reasons PsyNeuLink packages functions in a Function Component:
31

32
* **Manage parameters** -- parameters are attributes of a Function that either remain stable over multiple calls to the
33
  function (e.g., the `gain <Logistic.gain>` or `bias <Logistic.bias>` of a `Logistic` function, or the learning rate
34
  of a learning function); or, if they change, they do so less frequently or under the control of different factors
35
  than the function's variable (i.e., its input).  As a consequence, it is useful to manage these separately from the
36
  function's variable, and not have to provide them every time the function is called.  To address this, every
37
  PsyNeuLink Function has a set of attributes corresponding to the parameters of the function, that can be specified at
38
  the time the Function is created (in arguments to its constructor), and can be modified independently
39
  of a call to its :keyword:`function`. Modifications can be directly (e.g., in a script), or by the operation of other
40
  PsyNeuLink Components (e.g., `ModulatoryMechanisms`) by way of `ControlProjections <ControlProjection>`.
41
..
42
* **Modularity** -- by providing a standard interface, any Function assigned to a Components in PsyNeuLink can be
43
  replaced with other PsyNeuLink Functions, or with user-written custom functions so long as they adhere to certain
44
  standards (the PsyNeuLink `Function API <LINK>`).
45

46
.. _Function_Creation:
47

48
Creating a Function
49
-------------------
50

51
A Function can be created directly by calling its constructor.  Functions are also created automatically whenever
52
any other type of PsyNeuLink Component is created (and its :keyword:`function` is not otherwise specified). The
53
constructor for a Function has an argument for its `variable <Function_Base.variable>` and each of the parameters of
54
its `function <Function_Base.function>`.  The `variable <Function_Base.variable>` argument is used both to format the
55
input to the `function <Function_Base.function>`, and assign its default value.  The arguments for each parameter can
56
be used to specify the default value for that parameter; the values can later be modified in various ways as described
57
below.
58

59
.. _Function_Structure:
60

61
Structure
62
---------
63

64
.. _Function_Core_Attributes:
65

66
*Core Attributes*
67
~~~~~~~~~~~~~~~~~
68

69
Every Function has the following core attributes:
70

71
* `variable <Function_Base.variable>` -- provides the input to the Function's `function <Function_Base.function>`.
72
..
73
* `function <Function_Base.function>` -- determines the computation carried out by the Function; it must be a
74
  callable object (that is, a python function or method of some kind). Unlike other PsyNeuLink `Components
75
  <Component>`, it *cannot* be (another) Function object (it can't be "turtles" all the way down!).
76

77
A Function also has an attribute for each of the parameters of its `function <Function_Base.function>`.
78

79
*Owner*
80
~~~~~~~
81

82
If a Function has been assigned to another `Component`, then it also has an `owner <Function_Base.owner>` attribute
83
that refers to that Component.  The Function itself is assigned as the Component's
84
`function <Component.function>` attribute.  Each of the Function's attributes is also assigned
85
as an attribute of the `owner <Function_Base.owner>`, and those are each associated with with a
86
`parameterPort <ParameterPort>` of the `owner <Function_Base.owner>`.  Projections to those parameterPorts can be
87
used by `ControlProjections <ControlProjection>` to modify the Function's parameters.
88

89

90
COMMENT:
91
.. _Function_Output_Type_Conversion:
92

93
If the `function <Function_Base.function>` returns a single numeric value, and the Function's class implements
94
FunctionOutputTypeConversion, then the type of value returned by its `function <Function>` can be specified using the
95
`output_type` attribute, by assigning it one of the following `FunctionOutputType` values:
96
    * FunctionOutputType.RAW_NUMBER: return "exposed" number;
97
    * FunctionOutputType.NP_1D_ARRAY: return 1d np.array
98
    * FunctionOutputType.NP_2D_ARRAY: return 2d np.array.
99

100
To implement FunctionOutputTypeConversion, the Function's FUNCTION_OUTPUT_TYPE_CONVERSION parameter must set to True,
101
and function type conversion must be implemented by its `function <Function_Base.function>` method
102
(see `Linear` for an example).
103
COMMENT
104

105
.. _Function_Modulatory_Params:
106

107
*Modulatory Parameters*
108
~~~~~~~~~~~~~~~~~~~~~~~
109

110
Some classes of Functions also implement a pair of modulatory parameters: `multiplicative_param` and `additive_param`.
111
Each of these is assigned the name of one of the function's parameters. These are used by `ModulatorySignals
112
<ModulatorySignal>` to modulate the `function <Port_Base.function>` of a `Port <Port>` and thereby its `value
113
<Port_Base.value>` (see `ModulatorySignal_Modulation` and `figure <ModulatorySignal_Detail_Figure>` for additional
114
details). For example, a `ControlSignal` typically uses the `multiplicative_param` to modulate the value of a parameter
115
of a Mechanism's `function <Mechanism_Base.function>`, whereas a `LearningSignal` uses the `additive_param` to increment
116
the `value <ParamterPort.value>` of the `matrix <MappingProjection.matrix>` parameter of a `MappingProjection`.
117

118
COMMENT:
119
FOR DEVELOPERS:  'multiplicative_param` and `additive_param` are implemented as aliases to the relevant
120
parameters of a given Function, declared in its Parameters subclass declaration of the Function's declaration.
121
COMMENT
122

123

124
.. _Function_Execution:
125

126
Execution
127
---------
128

129
Functions are executable objects that can be called directly.  More commonly, however, they are called when
130
their `owner <Function_Base.owner>` is executed.  The parameters
131
of the `function <Function_Base.function>` can be modified when it is executed, by assigning a
132
`parameter specification dictionary <ParameterPort_Specification>` to the **params** argument in the
133
call to the `function <Function_Base.function>`.
134

135
For `Mechanisms <Mechanism>`, this can also be done by specifying `runtime_params <Composition_Runtime_Params>` in the
136
`Run` method of their `Composition`.
137

138
Class Reference
139
---------------
140

141
"""
142

143
import abc
1✔
144
import inspect
1✔
145
import numbers
1✔
146
import types
1✔
147
import warnings
1✔
148
from enum import Enum, IntEnum
1✔
149

150
import numpy as np
1✔
151
from beartype import beartype
1✔
152

153
from psyneulink._typing import Optional, Union, Callable
1✔
154

155
from psyneulink.core.components.component import Component, ComponentError, DefaultsFlexibility
1✔
156
from psyneulink.core.components.shellclasses import Function, Mechanism
1✔
157
from psyneulink.core.globals.context import ContextFlags, handle_external_context
1✔
158
from psyneulink.core.globals.keywords import (
1✔
159
    ARGUMENT_THERAPY_FUNCTION, AUTO_ASSIGN_MATRIX, EXAMPLE_FUNCTION_TYPE, FULL_CONNECTIVITY_MATRIX,
160
    FUNCTION_COMPONENT_CATEGORY, FUNCTION_OUTPUT_TYPE, FUNCTION_OUTPUT_TYPE_CONVERSION, HOLLOW_MATRIX,
161
    IDENTITY_MATRIX, INVERSE_HOLLOW_MATRIX, NAME, PREFERENCE_SET_NAME, RANDOM_CONNECTIVITY_MATRIX, VALUE, VARIABLE,
162
    MODEL_SPEC_ID_MDF_VARIABLE, MatrixKeywordLiteral, ZEROS_MATRIX
163
)
164
from psyneulink.core.globals.mdf import _get_variable_parameter_name
1✔
165
from psyneulink.core.globals.parameters import Parameter, check_user_specified
1✔
166
from psyneulink.core.globals.preferences.basepreferenceset import REPORT_OUTPUT_PREF, ValidPrefSet
1✔
167
from psyneulink.core.globals.preferences.preferenceset import PreferenceEntry, PreferenceLevel
1✔
168
from psyneulink.core.globals.registry import register_category
1✔
169
from psyneulink.core.globals.utilities import (
1✔
170
    convert_to_np_array, get_global_seed, is_instance_or_subclass, object_has_single_value, parameter_spec, parse_valid_identifier, safe_len,
171
    SeededRandomState, contains_type, is_numeric, NumericCollections,
172
    random_matrix
173
)
174

175
__all__ = [
1✔
176
    'ArgumentTherapy', 'EPSILON', 'Function_Base', 'function_keywords', 'FunctionError', 'FunctionOutputType',
177
    'FunctionRegistry', 'get_param_value_for_function', 'get_param_value_for_keyword', 'is_Function',
178
    'is_function_type', 'PERTINACITY', 'PROPENSITY', 'RandomMatrix'
179
]
180

181
EPSILON = np.finfo(float).eps
1✔
182
# numeric to allow modulation, invalid to identify unseeded state
183
DEFAULT_SEED = -1
1✔
184

185
FunctionRegistry = {}
1✔
186

187
function_keywords = {FUNCTION_OUTPUT_TYPE, FUNCTION_OUTPUT_TYPE_CONVERSION}
1✔
188

189

190
class FunctionError(ComponentError):
1✔
191
    pass
1✔
192

193

194
class FunctionOutputType(IntEnum):
1✔
195
    RAW_NUMBER = 0
1✔
196
    NP_1D_ARRAY = 1
1✔
197
    NP_2D_ARRAY = 2
1✔
198
    DEFAULT = 3
1✔
199

200

201
# Typechecking *********************************************************************************************************
202

203
# TYPE_CHECK for Function Instance or Class
204
def is_Function(x):
1✔
205
    if not x:
×
206
        return False
×
207
    elif isinstance(x, Function):
×
208
        return True
×
209
    elif issubclass(x, Function):
×
210
        return True
×
211
    else:
212
        return False
×
213

214

215
def is_function_type(x):
1✔
216
    if callable(x):
1✔
217
        return True
1✔
218
    elif not x:
1!
219
        return False
×
220
    elif isinstance(x, (Function, types.FunctionType, types.MethodType, types.BuiltinFunctionType, types.BuiltinMethodType)):
1!
221
        return True
×
222
    elif isinstance(x, type) and issubclass(x, Function):
1!
223
        return True
×
224
    else:
225
        return False
1✔
226

227
# *******************************   get_param_value_for_keyword ********************************************************
228

229
def get_param_value_for_keyword(owner, keyword):
1✔
230
    """Return the value for a keyword used by a subclass of Function
231

232
    Parameters
233
    ----------
234
    owner : Component
235
    keyword : str
236

237
    Returns
238
    -------
239
    value
240

241
    """
242
    try:
1✔
243
        return owner.function.keyword(owner, keyword)
1✔
244
    except FunctionError as e:
1!
245
        # assert(False)
246
        # prefs is not always created when this is called, so check
247
        try:
×
248
            owner.prefs
×
249
            has_prefs = True
×
250
        except AttributeError:
×
251
            has_prefs = False
×
252

253
        if has_prefs and owner.prefs.verbosePref:
×
254
            print("{} of {}".format(e, owner.name))
×
255
        # return None
256
        else:
257
            raise FunctionError(e)
258
    except AttributeError:
1✔
259
        # prefs is not always created when this is called, so check
260
        try:
1✔
261
            owner.prefs
1✔
262
            has_prefs = True
1✔
263
        except AttributeError:
1✔
264
            has_prefs = False
1✔
265

266
        if has_prefs and owner.prefs.verbosePref:
1!
267
            print("Keyword ({}) not recognized for {}".format(keyword, owner.name))
×
268
        return None
1✔
269

270

271
def get_param_value_for_function(owner, function):
1✔
272
    try:
×
273
        return owner.function.param_function(owner, function)
×
274
    except FunctionError as e:
×
275
        if owner.prefs.verbosePref:
×
276
            print("{} of {}".format(e, owner.name))
×
277
        return None
×
278
    except AttributeError:
×
279
        if owner.prefs.verbosePref:
×
280
            print("Function ({}) can't be evaluated for {}".format(function, owner.name))
×
281
        return None
×
282

283
# Parameter Mixins *****************************************************************************************************
284

285
# KDM 6/21/18: Below is left in for consideration; doesn't really gain much to justify relaxing the assumption
286
# that every Parameters class has a single parent
287

288
# class ScaleOffsetParamMixin:
289
#     scale = Parameter(1.0, modulable=True, aliases=[MULTIPLICATIVE_PARAM])
290
#     offset = Parameter(1.0, modulable=True, aliases=[ADDITIVE_PARAM])
291

292

293
# Function Definitions *************************************************************************************************
294

295

296
# KDM 8/9/18: below is added for future use when function methods are completely functional
297
# used as a decorator for Function methods
298
# def enable_output_conversion(func):
299
#     @functools.wraps(func)
300
#     def wrapper(*args, **kwargs):
301
#         result = func(*args, **kwargs)
302
#         return convert_output_type(result)
303
#     return wrapper
304

305
# this should eventually be moved to a unified validation method
306
def _output_type_setter(value, owning_component):
1✔
307
    # Can't convert from arrays of length > 1 to number
308
    if (
×
309
        owning_component.defaults.variable is not None
310
        and safe_len(owning_component.defaults.variable) > 1
311
        and owning_component.output_type is FunctionOutputType.RAW_NUMBER
312
    ):
313
        raise FunctionError(
314
            f"{owning_component.__class__.__name__} can't be set to return a "
315
            "single number since its variable has more than one number."
316
        )
317

318
    # warn if user overrides the 2D setting for mechanism functions
319
    # may be removed when
320
    # https://github.com/PrincetonUniversity/PsyNeuLink/issues/895 is solved
321
    # properly(meaning Mechanism values may be something other than 2D np array)
322
    try:
×
323
        if (
×
324
            isinstance(owning_component.owner, Mechanism)
325
            and (
326
                value == FunctionOutputType.RAW_NUMBER
327
                or value == FunctionOutputType.NP_1D_ARRAY
328
            )
329
        ):
330
            warnings.warn(
×
331
                f'Functions that are owned by a Mechanism but do not return a '
332
                '2D numpy array may cause unexpected behavior if llvm '
333
                'compilation is enabled.'
334
            )
335
    except (AttributeError, ImportError):
×
336
        pass
×
337

338
    return value
×
339

340

341
def _seed_setter(value, owning_component, context):
1✔
342
    if value in {None, DEFAULT_SEED}:
1✔
343
        value = get_global_seed()
1✔
344

345
    # Remove any old PRNG state
346
    owning_component.parameters.random_state.set(None, context=context)
1✔
347
    return int(value)
1✔
348

349

350
def _random_state_getter(self, owning_component, context):
1✔
351

352
    seed_param = owning_component.parameters.seed
1✔
353
    try:
1✔
354
        is_modulated = seed_param.port.is_modulated(context)
1✔
355
    except AttributeError:
1✔
356
        is_modulated = False
1✔
357

358
    if is_modulated:
1✔
359
        seed_value = [int(owning_component._get_current_parameter_value(seed_param, context))]
1✔
360
    else:
361
        seed_value = [int(seed_param._get(context=context))]
1✔
362

363
    if seed_value == [DEFAULT_SEED]:
1✔
364
        raise FunctionError(
365
            "Invalid seed for {} in context: {} ({})".format(
366
                owning_component, context.execution_id, seed_param
367
            )
368
        )
369

370
    current_state = self.values.get(context.execution_id, None)
1✔
371
    if current_state is None:
1✔
372
        return SeededRandomState(seed_value)
1✔
373
    if current_state.used_seed != seed_value:
1✔
374
        return type(current_state)(seed_value)
1✔
375

376
    return current_state
1✔
377

378

379
def _noise_setter(value, owning_component, context):
1✔
380
    def has_function(x):
1✔
381
        return (
1✔
382
            is_instance_or_subclass(x, (Function_Base, types.FunctionType))
383
            or contains_type(x, (Function_Base, types.FunctionType))
384
        )
385

386
    noise_param = owning_component.parameters.noise
1✔
387
    value_has_function = has_function(value)
1✔
388
    # initial set
389
    if owning_component.is_initializing:
1✔
390
        if value_has_function:
1✔
391
            # is changing a parameter attribute like this ok?
392
            noise_param.stateful = False
1✔
393
    else:
394
        default_value_has_function = has_function(noise_param.default_value)
1✔
395

396
        if default_value_has_function and not value_has_function:
1✔
397
            warnings.warn(
1✔
398
                'Setting noise to a numeric value after instantiation'
399
                ' with a value containing functions will not remove the'
400
                ' noise ParameterPort or make noise stateful.'
401
            )
402
        elif not default_value_has_function and value_has_function:
1✔
403
            warnings.warn(
1✔
404
                'Setting noise to a value containing functions after'
405
                ' instantiation with a numeric value will not create a'
406
                ' noise ParameterPort or make noise stateless.'
407
            )
408

409
    return value
1✔
410

411

412
class Function_Base(Function):
1✔
413
    """
414
    Function_Base(           \
415
         default_variable,   \
416
         params=None,        \
417
         owner=None,         \
418
         name=None,          \
419
         prefs=None          \
420
    )
421

422
    Implement abstract class for Function category of Component class
423

424
    COMMENT:
425
        Description:
426
            Functions are used to "wrap" functions used used by other components;
427
            They are defined here (on top of standard libraries) to provide a uniform interface for managing parameters
428
             (including defaults)
429
            NOTE:   the Function category definition serves primarily as a shell, and as an interface to the Function
430
                       class, to maintain consistency of structure with the other function categories;
431
                    it also insures implementation of .function for all Function Components
432
                    (as distinct from other Function subclasses, which can use a FUNCTION param
433
                        to implement .function instead of doing so directly)
434
                    Function Components are the end of the recursive line; as such:
435
                        they don't implement functionParams
436
                        in general, don't bother implementing function, rather...
437
                        they rely on Function_Base.function which passes on the return value of .function
438

439
        Variable and Parameters:
440
        IMPLEMENTATION NOTE:  ** DESCRIBE VARIABLE HERE AND HOW/WHY IT DIFFERS FROM PARAMETER
441
            - Parameters can be assigned and/or changed individually or in sets, by:
442
              - including them in the initialization call
443
              - calling the _instantiate_defaults method (which changes their default values)
444
              - including them in a call the function method (which changes their values for just for that call)
445
            - Parameters must be specified in a params dictionary:
446
              - the key for each entry should be the name of the parameter (used also to name associated Projections)
447
              - the value for each entry is the value of the parameter
448

449
        Return values:
450
            The output_type can be used to specify type conversion for single-item return values:
451
            - it can only be used for numbers or a single-number list; other values will generate an exception
452
            - if self.output_type is set to:
453
                FunctionOutputType.RAW_NUMBER, return value is "exposed" as a number
454
                FunctionOutputType.NP_1D_ARRAY, return value is 1d np.array
455
                FunctionOutputType.NP_2D_ARRAY, return value is 2d np.array
456
            - it must be enabled for a subclass by setting params[FUNCTION_OUTPUT_TYPE_CONVERSION] = True
457
            - it must be implemented in the execute method of the subclass
458
            - see Linear for an example
459

460
        MechanismRegistry:
461
            All Function functions are registered in FunctionRegistry, which maintains a dict for each subclass,
462
              a count for all instances of that type, and a dictionary of those instances
463

464
        Naming:
465
            Function functions are named by their componentName attribute (usually = componentType)
466

467
        Class attributes:
468
            + componentCategory: FUNCTION_COMPONENT_CATEGORY
469
            + className (str): kwMechanismFunctionCategory
470
            + suffix (str): " <className>"
471
            + registry (dict): FunctionRegistry
472
            + classPreference (PreferenceSet): BasePreferenceSet, instantiated in __init__()
473
            + classPreferenceLevel (PreferenceLevel): PreferenceLevel.CATEGORY
474

475
        Class methods:
476
            none
477

478
        Instance attributes:
479
            + componentType (str):  assigned by subclasses
480
            + componentName (str):   assigned by subclasses
481
            + variable (value) - used as input to function's execute method
482
            + value (value) - output of execute method
483
            + name (str) - if not specified as an arg, a default based on the class is assigned in register_category
484
            + prefs (PreferenceSet) - if not specified as an arg, default is created by copying BasePreferenceSet
485

486
        Instance methods:
487
            The following method MUST be overridden by an implementation in the subclass:
488
            - execute(variable, params)
489
            The following can be implemented, to customize validation of the function variable and/or params:
490
            - [_validate_variable(variable)]
491
            - [_validate_params(request_set, target_set, context)]
492
    COMMENT
493

494
    Arguments
495
    ---------
496

497
    variable : value : default class_defaults.variable
498
        specifies the format and a default value for the input to `function <Function>`.
499

500
    params : Dict[param keyword: param value] : default None
501
        a `parameter dictionary <ParameterPort_Specification>` that specifies the parameters for the
502
        function.  Values specified for parameters in the dictionary override any assigned to those parameters in
503
        arguments of the constructor.
504

505
    owner : Component
506
        `component <Component>` to which to assign the Function.
507

508
    name : str : default see `name <Function.name>`
509
        specifies the name of the Function.
510

511
    prefs : PreferenceSet or specification dict : default Function.classPreferences
512
        specifies the `PreferenceSet` for the Function (see `prefs <Function_Base.prefs>` for details).
513

514

515
    Attributes
516
    ----------
517

518
    variable: value
519
        format and default value can be specified by the :keyword:`variable` argument of the constructor;  otherwise,
520
        they are specified by the Function's :keyword:`class_defaults.variable`.
521

522
    function : function
523
        called by the Function's `owner <Function_Base.owner>` when it is executed.
524

525
    COMMENT:
526
    enable_output_type_conversion : Bool : False
527
        specifies whether `function output type conversion <Function_Output_Type_Conversion>` is enabled.
528

529
    output_type : FunctionOutputType : None
530
        used to determine the return type for the `function <Function_Base.function>`;  `functionOuputTypeConversion`
531
        must be enabled and implemented for the class (see `FunctionOutputType <Function_Output_Type_Conversion>`
532
        for details).
533

534
    changes_shape : bool : False
535
        specifies whether the return value of the function is different than the shape of its `variable <Function_Base.variable>.  Used to determine whether the shape of the inputs to the `Component` to which the function is assigned should be based on the `variable <Function_Base.variable>` of the function or its `value <Function.value>`.
536
    COMMENT
537

538
    owner : Component
539
        `component <Component>` to which the Function has been assigned.
540

541
    name : str
542
        the name of the Function; if it is not specified in the **name** argument of the constructor, a default is
543
        assigned by FunctionRegistry (see `Registry_Naming` for conventions used for default and duplicate names).
544

545
    prefs : PreferenceSet or specification dict : Function.classPreferences
546
        the `PreferenceSet` for function; if it is not specified in the **prefs** argument of the Function's
547
        constructor, a default is assigned using `classPreferences` defined in __init__.py (see `Preferences`
548
        for details).
549

550
    """
551

552
    componentCategory = FUNCTION_COMPONENT_CATEGORY
1✔
553
    className = componentCategory
1✔
554
    suffix = " " + className
1✔
555

556
    registry = FunctionRegistry
1✔
557

558
    classPreferenceLevel = PreferenceLevel.CATEGORY
1✔
559

560
    _model_spec_id_parameters = 'args'
1✔
561
    _mdf_stateful_parameter_indices = {}
1✔
562

563
    _specified_variable_shape_flexibility = DefaultsFlexibility.INCREASE_DIMENSION
1✔
564

565
    class Parameters(Function.Parameters):
1✔
566
        """
567
            Attributes
568
            ----------
569

570
                variable
571
                    see `variable <Function_Base.variable>`
572

573
                    :default value: numpy.array([0])
574
                    :type: ``numpy.ndarray``
575
                    :read only: True
576

577
                enable_output_type_conversion
578
                    see `enable_output_type_conversion <Function_Base.enable_output_type_conversion>`
579

580
                    :default value: False
581
                    :type: ``bool``
582

583
                changes_shape
584
                    see `changes_shape <Function_Base.changes_shape>`
585

586
                    :default value: False
587
                    :type: bool
588

589
                output_type
590
                    see `output_type <Function_Base.output_type>`
591

592
                    :default value: FunctionOutputType.DEFAULT
593
                    :type: `FunctionOutputType`
594

595
        """
596
        variable = Parameter(np.array([0]), read_only=True, pnl_internal=True, constructor_argument='default_variable')
1✔
597

598
        output_type = Parameter(
1✔
599
            FunctionOutputType.DEFAULT,
600
            stateful=False,
601
            loggable=False,
602
            pnl_internal=True,
603
            valid_types=FunctionOutputType
604
        )
605
        enable_output_type_conversion = Parameter(False, stateful=False, loggable=False, pnl_internal=True)
1✔
606

607
        changes_shape = Parameter(False, stateful=False, loggable=False, pnl_internal=True)
1✔
608
        def _validate_changes_shape(self, param):
1✔
609
            if not isinstance(param, bool):
1!
610
                return f'must be a bool.'
×
611

612
    # Note: the following enforce encoding as 1D np.ndarrays (one array per variable)
613
    variableEncodingDim = 1
1✔
614

615
    @check_user_specified
1✔
616
    @abc.abstractmethod
1✔
617
    def __init__(
1✔
618
        self,
619
        default_variable,
620
        params,
621
        owner=None,
622
        name=None,
623
        prefs=None,
624
        context=None,
625
        **kwargs
626
    ):
627
        """Assign category-level preferences, register category, and call super.__init__
628

629
        Initialization arguments:
630
        - default_variable (anything): establishes type for the variable, used for validation
631
        Note: if parameter_validation is off, validation is suppressed (for efficiency) (Function class default = on)
632

633
        :param default_variable: (anything but a dict) - value to assign as self.defaults.variable
634
        :param params: (dict) - params to be assigned as instance defaults
635
        :param log: (ComponentLog enum) - log entry types set in self.componentLog
636
        :param name: (string) - optional, overrides assignment of default (componentName of subclass)
637
        :return:
638
        """
639

640
        if self.initialization_status == ContextFlags.DEFERRED_INIT:
1!
641
            self._assign_deferred_init_name(name)
×
642
            self._init_args[NAME] = name
×
643
            return
×
644

645
        register_category(entry=self,
1✔
646
                          base_class=Function_Base,
647
                          registry=FunctionRegistry,
648
                          name=name,
649
                          )
650
        self.owner = owner
1✔
651

652
        super().__init__(
1✔
653
            default_variable=default_variable,
654
            param_defaults=params,
655
            name=name,
656
            prefs=prefs,
657
            **kwargs
658
        )
659

660
    def __call__(self, *args, **kwargs):
1✔
661
        return self.function(*args, **kwargs)
1✔
662

663
    def __deepcopy__(self, memo):
1✔
664
        new = super().__deepcopy__(memo)
1✔
665
        # ensure copy does not have identical name
666
        register_category(new, Function_Base, new.name, FunctionRegistry)
1✔
667
        if "random_state" in new.parameters:
1✔
668
            # HACK: Make sure any copies are re-seeded to avoid dependent RNG.
669
            # functions with "random_state" param must have "seed" parameter
670
            for ctx in new.parameters.seed.values:
1✔
671
                new.parameters.seed.set(
1✔
672
                    DEFAULT_SEED, ctx, skip_log=True, skip_history=True
673
                )
674

675
        return new
1✔
676

677
    @handle_external_context()
1✔
678
    def function(self,
1✔
679
                 variable=None,
680
                 context=None,
681
                 params=None,
682
                 target_set=None,
683
                 **kwargs):
684

685
        # IMPLEMENTATION NOTE:
686
        # The following is a convenience feature that supports specification of params directly in call to function
687
        # by moving the to a params dict, which treats them as runtime_params
688
        if kwargs:
1✔
689
            for key in kwargs.copy():
1✔
690
                if key in self.parameters.names():
1✔
691
                    if not params:
1✔
692
                        params = {key: kwargs.pop(key)}
1✔
693
                    else:
694
                        params.update({key: kwargs.pop(key)})
1✔
695

696
        # Validate variable and assign to variable, and validate params
697
        variable = self._check_args(variable=variable,
1✔
698
                                    context=context,
699
                                    params=params,
700
                                    target_set=target_set,
701
                                    )
702
        # Execute function
703
        try:
1✔
704
            value = self._function(variable=variable,
1✔
705
                                   context=context,
706
                                   params=params,
707
                                   **kwargs)
708
        except ValueError as err:
1✔
709
            err_msg = f"Problem with '{self}' in '{self.owner.name if self.owner else self.__class__.__name__}': {err}"
×
710
            raise FunctionError(err_msg) from err
711
        self.most_recent_context = context
1✔
712
        self.parameters.value._set(value, context=context)
1✔
713
        self._reset_runtime_parameters(context)
1✔
714
        return value
1✔
715

716
    @abc.abstractmethod
1✔
717
    def _function(
1✔
718
        self,
719
        variable=None,
720
        context=None,
721
        params=None,
722

723
    ):
724
        pass
×
725

726
    def _parse_arg_generic(self, arg_val):
1✔
727
        if isinstance(arg_val, list):
×
728
            return np.asarray(arg_val)
×
729
        else:
730
            return arg_val
×
731

732
    def _validate_parameter_spec(self, param, param_name, numeric_only=True):
1✔
733
        """Validates function param
734
        Replace direct call to parameter_spec in tc, which seems to not get called by Function __init__()'s
735
        """
736
        if not parameter_spec(param, numeric_only):
1!
737
            owner_name = 'of ' + self.owner_name if self.owner else ""
×
738
            raise FunctionError(f"{param} is not a valid specification for "
739
                                f"the {param_name} argument of {self.__class__.__name__}{owner_name}.")
740

741
    def _get_current_parameter_value(self, param_name, context=None):
1✔
742
        try:
1✔
743
            param = getattr(self.parameters, param_name)
1✔
744
        except TypeError:
1!
745
            param = param_name
1✔
746
        except AttributeError:
×
747
            # don't accept strings that don't correspond to Parameters
748
            # on this function
749
            raise
×
750

751
        return super()._get_current_parameter_value(param, context)
1✔
752

753
    def get_previous_value(self, context=None):
1✔
754
        # temporary method until previous values are integrated for all parameters
755
        value = self.parameters.previous_value._get(context)
1✔
756

757
        return value
1✔
758

759
    def convert_output_type(self, value, output_type=None):
1✔
760
        if output_type is None:
1✔
761
            if not self.enable_output_type_conversion or self.output_type is None:
1✔
762
                return value
1✔
763
            else:
764
                output_type = self.output_type
1✔
765

766
        value = convert_to_np_array(value)
1✔
767

768
        # Type conversion (specified by output_type):
769

770
        # MODIFIED 6/21/19 NEW: [JDC]
771
        # Convert to same format as variable
772
        if isinstance(output_type, (list, np.ndarray)):
1✔
773
            shape = np.array(output_type).shape
1✔
774
            return np.array(value).reshape(shape)
1✔
775
        # MODIFIED 6/21/19 END
776

777
        # Convert to 2D array, irrespective of value type:
778
        if output_type is FunctionOutputType.NP_2D_ARRAY:
1✔
779
            # KDM 8/10/18: mimicking the conversion that Mechanism does to its values, because
780
            # this is what we actually wanted this method for. Can be changed to pure 2D np array in
781
            # future if necessary
782

783
            converted_to_2d = np.atleast_2d(value)
1✔
784
            # If return_value is a list of heterogenous elements, return as is
785
            #     (satisfies requirement that return_value be an array of possibly multidimensional values)
786
            if converted_to_2d.dtype == object:
1✔
787
                pass
1✔
788
            # Otherwise, return value converted to 2d np.array
789
            else:
790
                value = converted_to_2d
1✔
791

792
        # Convert to 1D array, irrespective of value type:
793
        # Note: if 2D array (or higher) has more than two items in the outer dimension, generate exception
794
        elif output_type is FunctionOutputType.NP_1D_ARRAY:
1✔
795
            # If variable is 2D
796
            if value.ndim >= 2:
1✔
797
                # If there is only one item:
798
                if len(value) == 1:
1✔
799
                    value = value[0]
1✔
800
                else:
801
                    raise FunctionError(f"Can't convert value ({value}: 2D np.ndarray object "
802
                                        f"with more than one array) to 1D array.")
803
            elif value.ndim == 1:
1!
804
                pass
1✔
805
            elif value.ndim == 0:
×
806
                value = np.atleast_1d(value)
×
807
            else:
808
                raise FunctionError(f"Can't convert value ({value} to 1D array.")
809

810
        # Convert to raw number, irrespective of value type:
811
        # Note: if 2D or 1D array has more than two items, generate exception
812
        elif output_type is FunctionOutputType.RAW_NUMBER:
1!
813
            if object_has_single_value(value):
1✔
814
                value = float(value)
1✔
815
            else:
816
                raise FunctionError(f"Can't convert value ({value}) with more than a single number to a raw number.")
817

818
        return value
1✔
819

820
    @property
1✔
821
    def owner_name(self):
1✔
822
        try:
1✔
823
            return self.owner.name
1✔
824
        except AttributeError:
×
825
            return '<no owner>'
×
826

827
    def _is_identity(self, context=None, defaults=False):
1✔
828
        # should return True in subclasses if the parameters for context are such that
829
        # the Function's output will be the same as its input
830
        # Used to bypass execute when unnecessary
831
        return False
1✔
832

833
    @property
1✔
834
    def _model_spec_parameter_blacklist(self):
1✔
835
        return super()._model_spec_parameter_blacklist.union({
1✔
836
            'multiplicative_param', 'additive_param',
837
        })
838

839
    def _assign_to_mdf_model(self, model, input_id) -> str:
1✔
840
        """Adds an MDF representation of this function to MDF object
841
        **model**, including all necessary auxiliary functions.
842
        **input_id** is the input to the singular MDF function or first
843
        function representing this psyneulink Function, if applicable.
844

845
        Returns:
846
            str: the identifier of the final MDF function representing
847
            this psyneulink Function
848
        """
849
        import modeci_mdf.mdf as mdf
1✔
850

851
        extra_noise_functions = []
1✔
852

853
        self_model = self.as_mdf_model()
1✔
854

855
        def handle_noise(noise):
1✔
856
            if is_instance_or_subclass(noise, Component):
1✔
857
                if inspect.isclass(noise) and issubclass(noise, Component):
1!
858
                    noise = noise()
×
859
                noise_func_model = noise.as_mdf_model()
1✔
860
                extra_noise_functions.append(noise_func_model)
1✔
861
                return noise_func_model.id
1✔
862
            elif isinstance(noise, (list, np.ndarray)):
1!
863
                return type(noise)(handle_noise(item) for item in noise)
×
864
            else:
865
                return None
1✔
866

867
        try:
1✔
868
            noise_val = handle_noise(self.defaults.noise)
1✔
869
        except AttributeError:
1✔
870
            noise_val = None
1✔
871

872
        if noise_val is not None:
1✔
873
            noise_func = mdf.Function(
1✔
874
                id=f'{model.id}_{parse_valid_identifier(self.name)}_noise',
875
                value=MODEL_SPEC_ID_MDF_VARIABLE,
876
                args={MODEL_SPEC_ID_MDF_VARIABLE: noise_val},
877
            )
878
            self._set_mdf_arg(self_model, 'noise', noise_func.id)
1✔
879

880
            model.functions.extend(extra_noise_functions)
1✔
881
            model.functions.append(noise_func)
1✔
882

883
        self_model.id = f'{model.id}_{self_model.id}'
1✔
884
        self._set_mdf_arg(self_model, _get_variable_parameter_name(self), input_id)
1✔
885
        model.functions.append(self_model)
1✔
886

887
        # assign stateful parameters
888
        for name, index in self._mdf_stateful_parameter_indices.items():
1✔
889
            # in this case, parameter gets updated to its function's final value
890
            param = getattr(self.parameters, name)
1✔
891

892
            try:
1✔
893
                initializer_value = self_model.args[param.initializer]
1✔
894
            except KeyError:
1✔
895
                initializer_value = self_model.metadata[param.initializer]
1✔
896

897
            index_str = f'[{index}]' if index is not None else ''
1✔
898

899
            model.parameters.append(
1✔
900
                mdf.Parameter(
901
                    id=param.mdf_name if param.mdf_name is not None else param.name,
902
                    default_initial_value=initializer_value,
903
                    value=f'{self_model.id}{index_str}'
904
                )
905
            )
906

907
        return self_model.id
1✔
908

909
    def as_mdf_model(self):
1✔
910
        import modeci_mdf.mdf as mdf
1✔
911
        import modeci_mdf.functions.standard as mdf_functions
1✔
912

913
        parameters = self._mdf_model_parameters
1✔
914
        metadata = self._mdf_metadata
1✔
915
        stateful_params = set()
1✔
916

917
        # add stateful parameters into metadata for mechanism to get
918
        for name in parameters[self._model_spec_id_parameters]:
1✔
919
            try:
1✔
920
                param = getattr(self.parameters, name)
1✔
921
            except AttributeError:
1✔
922
                continue
1✔
923

924
            if param.initializer is not None:
1✔
925
                stateful_params.add(name)
1✔
926

927
        # stateful parameters cannot show up as args or they will not be
928
        # treated statefully in mdf
929
        for sp in stateful_params:
1✔
930
            del parameters[self._model_spec_id_parameters][sp]
1✔
931

932
        model = mdf.Function(
1✔
933
            id=parse_valid_identifier(self.name),
934
            **parameters,
935
            **metadata,
936
        )
937

938
        try:
1✔
939
            model.value = self.as_expression()
1✔
940
        except AttributeError:
1✔
941
            if self._model_spec_generic_type_name is not NotImplemented:
1✔
942
                typ = self._model_spec_generic_type_name
1✔
943
            else:
944
                try:
1✔
945
                    typ = self.custom_function.__name__
1✔
946
                except AttributeError:
1✔
947
                    typ = type(self).__name__.lower()
1✔
948

949
            if typ not in mdf_functions.mdf_functions:
1✔
950
                warnings.warn(f'{typ} is not an MDF standard function, this is likely to produce an incompatible model.')
1✔
951

952
            model.function = typ
1✔
953

954
        return model
1✔
955

956

957
# *****************************************   EXAMPLE FUNCTION   *******************************************************
958
PROPENSITY = "PROPENSITY"
1✔
959
PERTINACITY = "PERTINACITY"
1✔
960

961

962
class ArgumentTherapy(Function_Base):
1✔
963
    """
964
    ArgumentTherapy(                   \
965
         variable,                     \
966
         propensity=Manner.CONTRARIAN, \
967
         pertinacity=10.0              \
968
         params=None,                  \
969
         owner=None,                   \
970
         name=None,                    \
971
         prefs=None                    \
972
         )
973

974
    .. _ArgumentTherapist:
975

976
    Return `True` or :keyword:`False` according to the manner of the therapist.
977

978
    Arguments
979
    ---------
980

981
    variable : boolean or statement that resolves to one : default class_defaults.variable
982
        assertion for which a therapeutic response will be offered.
983

984
    propensity : Manner value : default Manner.CONTRARIAN
985
        specifies preferred therapeutic manner
986

987
    pertinacity : float : default 10.0
988
        specifies therapeutic consistency
989

990
    params : Dict[param keyword: param value] : default None
991
        a `parameter dictionary <ParameterPort_Specification>` that specifies the parameters for the
992
        function.  Values specified for parameters in the dictionary override any assigned to those parameters in
993
        arguments of the constructor.
994

995
    owner : Component
996
        `component <Component>` to which to assign the Function.
997

998
    name : str : default see `name <Function.name>`
999
        specifies the name of the Function.
1000

1001
    prefs : PreferenceSet or specification dict : default Function.classPreferences
1002
        specifies the `PreferenceSet` for the Function (see `prefs <Function_Base.prefs>` for details).
1003

1004

1005
    Attributes
1006
    ----------
1007

1008
    variable : boolean
1009
        assertion to which a therapeutic response is made.
1010

1011
    propensity : Manner value : default Manner.CONTRARIAN
1012
        determines therapeutic manner:  tendency to agree or disagree.
1013

1014
    pertinacity : float : default 10.0
1015
        determines consistency with which the manner complies with the propensity.
1016

1017
    owner : Component
1018
        `component <Component>` to which the Function has been assigned.
1019

1020
    name : str
1021
        the name of the Function; if it is not specified in the **name** argument of the constructor, a default is
1022
        assigned by FunctionRegistry (see `Registry_Naming` for conventions used for default and duplicate names).
1023

1024
    prefs : PreferenceSet or specification dict : Function.classPreferences
1025
        the `PreferenceSet` for function; if it is not specified in the **prefs** argument of the Function's
1026
        constructor, a default is assigned using `classPreferences` defined in __init__.py (see `Preferences`
1027
        for details).
1028

1029

1030
    """
1031

1032
    # Function componentName and type (defined at top of module)
1033
    componentName = ARGUMENT_THERAPY_FUNCTION
1✔
1034
    componentType = EXAMPLE_FUNCTION_TYPE
1✔
1035

1036
    classPreferences = {
1✔
1037
        PREFERENCE_SET_NAME: 'ExampleClassPreferences',
1038
        REPORT_OUTPUT_PREF: PreferenceEntry(False, PreferenceLevel.INSTANCE),
1039
    }
1040

1041
    # Mode indicators
1042
    class Manner(Enum):
1✔
1043
        OBSEQUIOUS = 0
1✔
1044
        CONTRARIAN = 1
1✔
1045

1046
    # Parameter class defaults
1047
    # These are used both to type-cast the params, and as defaults if none are assigned
1048
    #  in the initialization call or later (using either _instantiate_defaults or during a function call)
1049

1050
    @check_user_specified
1✔
1051
    def __init__(self,
1✔
1052
                 default_variable=None,
1053
                 propensity=10.0,
1054
                 pertincacity=Manner.CONTRARIAN,
1055
                 params=None,
1056
                 owner=None,
1057
                 prefs:  Optional[ValidPrefSet] = None):
1058

1059
        super().__init__(
×
1060
            default_variable=default_variable,
1061
            propensity=propensity,
1062
            pertinacity=pertincacity,
1063
            params=params,
1064
            owner=owner,
1065
            prefs=prefs,
1066
        )
1067

1068
    def _validate_variable(self, variable, context=None):
1✔
1069
        """Validates variable and returns validated value
1070

1071
        This overrides the class method, to perform more detailed type checking
1072
        See explanation in class method.
1073
        Note: this method (or the class version) is called only if the parameter_validation attribute is `True`
1074

1075
        :param variable: (anything but a dict) - variable to be validated:
1076
        :param context: (str)
1077
        :return variable: - validated
1078
        """
1079

1080
        if type(variable) == type(self.class_defaults.variable) or \
×
1081
                (isinstance(variable, numbers.Number) and isinstance(self.class_defaults.variable, numbers.Number)):
1082
            return variable
×
1083
        else:
1084
            raise FunctionError(f"Variable must be {type(self.class_defaults.variable)}.")
1085

1086
    def _validate_params(self, request_set, target_set=None, context=None):
1✔
1087
        """Validates variable and /or params and assigns to targets
1088

1089
        This overrides the class method, to perform more detailed type checking
1090
        See explanation in class method.
1091
        Note: this method (or the class version) is called only if the parameter_validation attribute is `True`
1092

1093
        :param request_set: (dict) - params to be validated
1094
        :param target_set: (dict) - destination of validated params
1095
        :return none:
1096
        """
1097

1098
        message = ""
×
1099

1100
        # Check params
1101
        for param_name, param_value in request_set.items():
×
1102

1103
            if param_name == PROPENSITY:
×
1104
                if isinstance(param_value, ArgumentTherapy.Manner):
×
1105
                    # target_set[self.PROPENSITY] = param_value
1106
                    pass  # This leaves param in request_set, clear to be assigned to target_set in call to super below
×
1107
                else:
1108
                    message = "Propensity must be of type Example.Mode"
×
1109
                continue
×
1110

1111
            # Validate param
1112
            if param_name == PERTINACITY:
×
1113
                if isinstance(param_value, numbers.Number) and 0 <= param_value <= 10:
×
1114
                    # target_set[PERTINACITY] = param_value
1115
                    pass  # This leaves param in request_set, clear to be assigned to target_set in call to super below
×
1116
                else:
1117
                    message += "Pertinacity must be a number between 0 and 10"
×
1118
                continue
×
1119

1120
        if message:
×
1121
            raise FunctionError(message)
1122

1123
        super()._validate_params(request_set, target_set, context)
×
1124

1125
    def _function(self,
1✔
1126
                 variable=None,
1127
                 context=None,
1128
                 params=None,
1129
                 ):
1130
        """
1131
        Returns a boolean that is (or tends to be) the same as or opposite the one passed in.
1132

1133
        Arguments
1134
        ---------
1135

1136
        variable : boolean : default class_defaults.variable
1137
           an assertion to which a therapeutic response is made.
1138

1139
        params : Dict[param keyword: param value] : default None
1140
            a `parameter dictionary <ParameterPort_Specification>` that specifies the parameters for the
1141
            function.  Values specified for parameters in the dictionary override any assigned to those parameters in
1142
            arguments of the constructor.
1143

1144

1145
        Returns
1146
        -------
1147

1148
        therapeutic response : boolean
1149

1150
        """
1151
        # Compute the function
1152
        statement = variable
×
1153
        propensity = self._get_current_parameter_value(PROPENSITY, context)
×
1154
        pertinacity = self._get_current_parameter_value(PERTINACITY, context)
×
1155
        whim = np.random.randint(-10, 10)
×
1156

1157
        if propensity == self.Manner.OBSEQUIOUS:
×
1158
            value = whim < pertinacity
×
1159

1160
        elif propensity == self.Manner.CONTRARIAN:
×
1161
            value = whim > pertinacity
×
1162

1163
        else:
1164
            raise FunctionError("This should not happen if parameter_validation == True;  check its value")
1165

1166
        return self.convert_output_type(value)
×
1167

1168

1169

1170
kwEVCAuxFunction = "EVC AUXILIARY FUNCTION"
1✔
1171
kwEVCAuxFunctionType = "EVC AUXILIARY FUNCTION TYPE"
1✔
1172
kwValueFunction = "EVC VALUE FUNCTION"
1✔
1173
CONTROL_SIGNAL_GRID_SEARCH_FUNCTION = "EVC CONTROL SIGNAL GRID SEARCH FUNCTION"
1✔
1174
CONTROLLER = 'controller'
1✔
1175

1176
class EVCAuxiliaryFunction(Function_Base):
1✔
1177
    """Base class for EVC auxiliary functions
1178
    """
1179
    componentType = kwEVCAuxFunctionType
1✔
1180

1181
    class Parameters(Function_Base.Parameters):
1✔
1182
        """
1183
            Attributes
1184
            ----------
1185

1186
                variable
1187
                    see `variable <Function_Base.variable>`
1188

1189
                    :default value: numpy.array([0])
1190
                    :type: numpy.ndarray
1191
                    :read only: True
1192

1193
        """
1194
        variable = Parameter(None, pnl_internal=True, constructor_argument='default_variable')
1✔
1195

1196
    classPreferences = {
1✔
1197
        PREFERENCE_SET_NAME: 'ValueFunctionCustomClassPreferences',
1198
        REPORT_OUTPUT_PREF: PreferenceEntry(False, PreferenceLevel.INSTANCE),
1199
       }
1200

1201
    @check_user_specified
1✔
1202
    @beartype
1✔
1203
    def __init__(self,
1✔
1204
                 function,
1205
                 variable=None,
1206
                 params=None,
1207
                 owner=None,
1208
                 prefs:   Optional[ValidPrefSet] = None,
1209
                 context=None):
1210
        self.aux_function = function
×
1211

1212
        super().__init__(default_variable=variable,
×
1213
                         params=params,
1214
                         owner=owner,
1215
                         prefs=prefs,
1216
                         context=context,
1217
                         function=function,
1218
                         )
1219

1220

1221
class RandomMatrix():
1✔
1222
    """Function that returns matrix with random elements distributed uniformly around **center** across **range**.
1223

1224
    The **center** and **range** arguments are passed at construction, and used for all subsequent calls.
1225
    Once constructed, the function must be called with two floats, **sender_size** and **receiver_size**,
1226
    that specify the number of rows and columns of the matrix, respectively.
1227

1228
    Can be used to specify the `matrix <MappingProjection.matrix>` parameter of a `MappingProjection
1229
    <MappingProjection_Matrix_Specification>`, and to specify a default matrix for Projections in the
1230
    construction of a `Pathway` (see `Pathway_Specification_Projections`) or in a call to a Composition's
1231
    `add_linear_processing_pathway<Composition.add_linear_processing_pathway>` method.
1232

1233
    .. technical_note::
1234
       A call to the class calls `random_matrix <Utilities.random_matrix>`, passing **sender_size** and
1235
       **receiver_size** to `random_matrix <Utilities.random_matrix>` as its **num_rows** and **num_cols**
1236
       arguments, respectively, and passing the `center <RandomMatrix.offset>`-0.5 and `range <RandomMatrix.scale>`
1237
       attributes specified at construction to `random_matrix <Utilities.random_matrix>` as its **offset**
1238
       and **scale** arguments, respectively.
1239

1240
    Arguments
1241
    ----------
1242
    center : float
1243
        specifies the value around which the matrix elements are distributed in all calls to the function.
1244
    range : float
1245
        specifies range over which all matrix elements are distributed in all calls to the function.
1246

1247
    Attributes
1248
    ----------
1249
    center : float
1250
        determines the center of the distribution of the matrix elements;
1251
    range : float
1252
        determines the range of the distribution of the matrix elements;
1253
    """
1254

1255
    def __init__(self, center:float=0.0, range:float=1.0):
1✔
1256
        self.center=center
×
1257
        self.range=range
×
1258

1259
    def __call__(self, sender_size:int, receiver_size:int):
1✔
1260
        return random_matrix(sender_size, receiver_size, offset=self.center - 0.5, scale=self.range)
×
1261

1262

1263
def get_matrix(specification, rows=1, cols=1, context=None):
1✔
1264
    """Returns matrix conforming to specification with dimensions = rows x cols or None
1265

1266
     Specification can be a matrix keyword, filler value or np.ndarray
1267

1268
     Specification (validated in _validate_params):
1269
        + single number (used to fill self.matrix)
1270
        + matrix keyword:
1271
            + AUTO_ASSIGN_MATRIX: IDENTITY_MATRIX if it is square, othwerwise FULL_CONNECTIVITY_MATRIX
1272
            + IDENTITY_MATRIX: 1's on diagonal, 0's elsewhere (must be square matrix), otherwise generates error
1273
            + HOLLOW_MATRIX: 0's on diagonal, 1's elsewhere (must be square matrix), otherwise generates error
1274
            + INVERSE_HOLLOW_MATRIX: 0's on diagonal, -1's elsewhere (must be square matrix), otherwise generates error
1275
            + FULL_CONNECTIVITY_MATRIX: all 1's
1276
            + ZERO_MATRIX: all 0's
1277
            + RANDOM_CONNECTIVITY_MATRIX (random floats uniformly distributed between 0 and 1)
1278
            + RandomMatrix (random floats uniformly distributed around a specified center value with a specified range)
1279
        + 2D list or np.ndarray of numbers
1280

1281
     Returns 2D array with length=rows in dim 0 and length=cols in dim 1, or none if specification is not recognized
1282
    """
1283

1284
    # Matrix provided (and validated in _validate_params); convert to array
1285
    if isinstance(specification, (list, np.matrix)):
1✔
1286
        if is_numeric(specification):
1✔
1287
            return convert_to_np_array(specification)
1✔
1288
        else:
1289
            return
1✔
1290
        # MODIFIED 4/9/22 END
1291

1292
    if isinstance(specification, np.ndarray):
1✔
1293
        if specification.ndim == 2:
1✔
1294
            return specification
1✔
1295
        # FIX: MAKE THIS AN np.array WITH THE SAME DIMENSIONS??
1296
        elif specification.ndim < 2:
1✔
1297
            return np.atleast_2d(specification)
×
1298
        else:
1299
            raise FunctionError("Specification of np.array for matrix ({}) is more than 2d".
1300
                                format(specification))
1301

1302
    if specification == AUTO_ASSIGN_MATRIX:
1✔
1303
        if rows == cols:
1✔
1304
            specification = IDENTITY_MATRIX
1✔
1305
        else:
1306
            specification = FULL_CONNECTIVITY_MATRIX
1✔
1307

1308
    if specification == FULL_CONNECTIVITY_MATRIX:
1✔
1309
        return np.full((rows, cols), 1.0)
1✔
1310

1311
    if specification == ZEROS_MATRIX:
1✔
1312
        return np.zeros((rows, cols))
1✔
1313

1314
    if specification == IDENTITY_MATRIX:
1✔
1315
        if rows != cols:
1✔
1316
            raise FunctionError("Sender length ({}) must equal receiver length ({}) to use {}".
1317
                                format(rows, cols, specification))
1318
        return np.identity(rows)
1✔
1319

1320
    if specification == HOLLOW_MATRIX:
1✔
1321
        if rows != cols:
1✔
1322
            raise FunctionError("Sender length ({}) must equal receiver length ({}) to use {}".
1323
                                format(rows, cols, specification))
1324
        return 1 - np.identity(rows)
1✔
1325

1326
    if specification == INVERSE_HOLLOW_MATRIX:
1✔
1327
        if rows != cols:
1✔
1328
            raise FunctionError("Sender length ({}) must equal receiver length ({}) to use {}".
1329
                                format(rows, cols, specification))
1330
        return (1 - np.identity(rows)) * -1
1✔
1331

1332
    if specification == RANDOM_CONNECTIVITY_MATRIX:
1✔
1333
        return np.random.rand(rows, cols)
1✔
1334

1335
    # Function is specified, so assume it uses random.rand() and call with sender_len and receiver_len
1336
    if isinstance(specification, (types.FunctionType, RandomMatrix)):
1!
1337
        return specification(rows, cols)
×
1338

1339
    # (7/12/17 CW) this is a PATCH (like the one in MappingProjection) to allow users to
1340
    # specify 'matrix' as a string (e.g. r = RecurrentTransferMechanism(matrix='1 2; 3 4'))
1341
    if type(specification) == str:
1✔
1342
        try:
1✔
1343
            return np.array(np.matrix(specification))
1✔
1344
        except (ValueError, NameError, TypeError):
1✔
1345
            # np.matrix(specification) will give ValueError if specification is a bad value (e.g. 'abc', '1; 1 2')
1346
            #                          [JDC] actually gives NameError if specification is a string (e.g., 'abc')
1347
            pass
1✔
1348

1349
    # Specification not recognized
1350
    return None
1✔
1351

1352

1353
# Valid types for a matrix specification, note this is does not ensure that ND arrays are 1D or 2D like the
1354
# above code does.
1355
ValidMatrixSpecType = Union[MatrixKeywordLiteral, Callable, str, NumericCollections, np.matrix]
1✔
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

© 2026 Coveralls, Inc