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

PrincetonUniversity / PsyNeuLink / 11992518143

12 Nov 2024 03:50AM UTC coverage: 83.719% (-1.2%) from 84.935%
11992518143

push

github

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

Devel

9406 of 12466 branches covered (75.45%)

Branch coverage included in aggregate %.

3240 of 3767 new or added lines in 77 files covered. (86.01%)

120 existing lines in 26 files now uncovered.

32555 of 37655 relevant lines covered (86.46%)

0.86 hits per line

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

77.27
/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
"""
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.NP_0D_ARRAY: return 0d np.array
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
try:
1✔
152
    import torch
1✔
153
except ImportError:
×
154
    torch = None
×
155
from beartype import beartype
1✔
156

157
from psyneulink._typing import Optional, Union, Callable
1✔
158

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

179
__all__ = [
1✔
180
    'ArgumentTherapy', 'EPSILON', 'Function_Base', 'function_keywords', 'FunctionError', 'FunctionOutputType',
181
    'FunctionRegistry', 'get_param_value_for_function', 'get_param_value_for_keyword', 'is_Function',
182
    'is_function_type', 'PERTINACITY', 'PROPENSITY', 'RandomMatrix'
183
]
184

185
EPSILON = np.finfo(float).eps
1✔
186

187

188
# numeric to allow modulation, invalid to identify unseeded state
189
def DEFAULT_SEED():
1✔
190
    return np.array(-1)
1✔
191

192

193
FunctionRegistry = {}
1✔
194

195
function_keywords = {FUNCTION_OUTPUT_TYPE, FUNCTION_OUTPUT_TYPE_CONVERSION}
1✔
196

197

198
class FunctionError(ComponentError):
1✔
199
    pass
1✔
200

201

202
class FunctionOutputType(IntEnum):
1✔
203
    NP_0D_ARRAY = 0
1✔
204
    NP_1D_ARRAY = 1
1✔
205
    NP_2D_ARRAY = 2
1✔
206
    DEFAULT = 3
1✔
207

208

209
# Typechecking *********************************************************************************************************
210

211
# TYPE_CHECK for Function Instance or Class
212
def is_Function(x):
1✔
213
    if not x:
×
214
        return False
×
215
    elif isinstance(x, Function):
×
216
        return True
×
217
    elif issubclass(x, Function):
×
218
        return True
×
219
    else:
220
        return False
×
221

222

223
def is_function_type(x):
1✔
224
    if callable(x):
1✔
225
        return True
1✔
226
    elif not x:
1!
227
        return False
×
228
    elif isinstance(x, (Function, types.FunctionType, types.MethodType, types.BuiltinFunctionType, types.BuiltinMethodType)):
1!
229
        return True
×
230
    elif isinstance(x, type) and issubclass(x, Function):
1!
231
        return True
×
232
    else:
233
        return False
1✔
234

235
# *******************************   get_param_value_for_keyword ********************************************************
236

237
def get_param_value_for_keyword(owner, keyword):
1✔
238
    """Return the value for a keyword used by a subclass of Function
239

240
    Parameters
241
    ----------
242
    owner : Component
243
    keyword : str
244

245
    Returns
246
    -------
247
    value
248

249
    """
250
    try:
1✔
251
        return owner.function.keyword(owner, keyword)
1✔
252
    except FunctionError as e:
1✔
253
        # assert(False)
254
        # prefs is not always created when this is called, so check
255
        try:
×
256
            owner.prefs
×
257
            has_prefs = True
×
258
        except AttributeError:
×
259
            has_prefs = False
×
260

261
        if has_prefs and owner.prefs.verbosePref:
×
262
            print("{} of {}".format(e, owner.name))
×
263
        # return None
264
        else:
265
            raise FunctionError(e)
266
    except AttributeError:
1✔
267
        # prefs is not always created when this is called, so check
268
        try:
1✔
269
            owner.prefs
1✔
270
            has_prefs = True
1✔
271
        except AttributeError:
1✔
272
            has_prefs = False
1✔
273

274
        if has_prefs and owner.prefs.verbosePref:
1!
275
            print("Keyword ({}) not recognized for {}".format(keyword, owner.name))
×
276
        return None
1✔
277

278

279
def get_param_value_for_function(owner, function):
1✔
280
    try:
×
281
        return owner.function.param_function(owner, function)
×
282
    except FunctionError as e:
×
283
        if owner.prefs.verbosePref:
×
284
            print("{} of {}".format(e, owner.name))
×
285
        return None
×
286
    except AttributeError:
×
287
        if owner.prefs.verbosePref:
×
288
            print("Function ({}) can't be evaluated for {}".format(function, owner.name))
×
289
        return None
×
290

291
# Parameter Mixins *****************************************************************************************************
292

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

296
# class ScaleOffsetParamMixin:
297
#     scale = Parameter(1.0, modulable=True, aliases=[MULTIPLICATIVE_PARAM])
298
#     offset = Parameter(1.0, modulable=True, aliases=[ADDITIVE_PARAM])
299

300

301
# Function Definitions *************************************************************************************************
302

303

304
# KDM 8/9/18: below is added for future use when function methods are completely functional
305
# used as a decorator for Function methods
306
# def enable_output_conversion(func):
307
#     @functools.wraps(func)
308
#     def wrapper(*args, **kwargs):
309
#         result = func(*args, **kwargs)
310
#         return convert_output_type(result)
311
#     return wrapper
312

313
# this should eventually be moved to a unified validation method
314
def _output_type_setter(value, owning_component):
1✔
315
    # Can't convert from arrays of length > 1 to number
316
    if (
×
317
        owning_component.defaults.variable is not None
318
        and safe_len(owning_component.defaults.variable) > 1
319
        and owning_component.output_type is FunctionOutputType.NP_0D_ARRAY
320
    ):
321
        raise FunctionError(
322
            f"{owning_component.__class__.__name__} can't be set to return a "
323
            "single number since its variable has more than one number."
324
        )
325

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

346
    return value
×
347

348

349
def _seed_setter(value, owning_component, context, *, compilation_sync):
1✔
350
    if compilation_sync:
1✔
351
        # compilation sync should provide shared memory 0d array with a floating point value.
352
        assert value is not None
1✔
353
        assert value != DEFAULT_SEED()
1✔
354
        assert value.shape == ()
1✔
355

356
        return value
1✔
357

358
    value = try_extract_0d_array_item(value)
1✔
359
    if value is None or value == DEFAULT_SEED():
1✔
360
        value = get_global_seed()
1✔
361

362
    # Remove any old PRNG state
363
    owning_component.parameters.random_state.set(None, context=context)
1✔
364
    return np.asarray(value)
1✔
365

366

367
def _random_state_getter(self, owning_component, context, modulated=False):
1✔
368

369
    seed_param = owning_component.parameters.seed
1✔
370
    try:
1✔
371
        has_modulation = seed_param.port.has_modulation(context.composition)
1✔
372
    except AttributeError:
1✔
373
        has_modulation = False
1✔
374

375
    # 'has_modulation' indicates that seed has an active modulatory projection
376
    # 'modulated' indicates that the modulated value is requested
377
    if has_modulation and modulated:
1✔
378
        seed_value = [int(owning_component._get_current_parameter_value(seed_param, context).item())]
1✔
379
    else:
380
        seed_value = [int(seed_param._get(context=context))]
1✔
381

382
    if seed_value == [DEFAULT_SEED()]:
1✔
383
        raise FunctionError(
384
            "Invalid seed for {} in context: {} ({})".format(
385
                owning_component, context.execution_id, seed_param
386
            )
387
        )
388

389
    current_state = self.values.get(context.execution_id, None)
1✔
390
    if current_state is None:
1✔
391
        return SeededRandomState(seed_value)
1✔
392
    if current_state.used_seed != seed_value:
1✔
393
        return type(current_state)(seed_value)
1✔
394

395
    return current_state
1✔
396

397

398
def _noise_setter(value, owning_component, context):
1✔
399
    def has_function(x):
1✔
400
        return (
1✔
401
            is_instance_or_subclass(x, (Function_Base, types.FunctionType))
402
            or contains_type(x, (Function_Base, types.FunctionType))
403
        )
404

405
    noise_param = owning_component.parameters.noise
1✔
406
    value_has_function = has_function(value)
1✔
407
    # initial set
408
    if owning_component.is_initializing:
1✔
409
        if value_has_function:
1✔
410
            # is changing a parameter attribute like this ok?
411
            noise_param.stateful = False
1✔
412
    else:
413
        default_value_has_function = has_function(noise_param.default_value)
1✔
414

415
        if default_value_has_function and not value_has_function:
1✔
416
            warnings.warn(
1✔
417
                'Setting noise to a numeric value after instantiation'
418
                ' with a value containing functions will not remove the'
419
                ' noise ParameterPort or make noise stateful.'
420
            )
421
        elif not default_value_has_function and value_has_function:
1✔
422
            warnings.warn(
1✔
423
                'Setting noise to a value containing functions after'
424
                ' instantiation with a numeric value will not create a'
425
                ' noise ParameterPort or make noise stateless.'
426
            )
427

428
    return value
1✔
429

430

431
class Function_Base(Function):
1✔
432
    """
433
    Function_Base(           \
434
         default_variable,   \
435
         params=None,        \
436
         owner=None,         \
437
         name=None,          \
438
         prefs=None          \
439
    )
440

441
    Implement abstract class for Function category of Component class
442

443
    COMMENT:
444
        Description:
445
            Functions are used to "wrap" functions used used by other components;
446
            They are defined here (on top of standard libraries) to provide a uniform interface for managing parameters
447
             (including defaults)
448
            NOTE:   the Function category definition serves primarily as a shell, and as an interface to the Function
449
                       class, to maintain consistency of structure with the other function categories;
450
                    it also insures implementation of .function for all Function Components
451
                    (as distinct from other Function subclasses, which can use a FUNCTION param
452
                        to implement .function instead of doing so directly)
453
                    Function Components are the end of the recursive line; as such:
454
                        they don't implement functionParams
455
                        in general, don't bother implementing function, rather...
456
                        they rely on Function_Base.function which passes on the return value of .function
457

458
        Variable and Parameters:
459
        IMPLEMENTATION NOTE:  ** DESCRIBE VARIABLE HERE AND HOW/WHY IT DIFFERS FROM PARAMETER
460
            - Parameters can be assigned and/or changed individually or in sets, by:
461
              - including them in the initialization call
462
              - calling the _instantiate_defaults method (which changes their default values)
463
              - including them in a call the function method (which changes their values for just for that call)
464
            - Parameters must be specified in a params dictionary:
465
              - the key for each entry should be the name of the parameter (used also to name associated Projections)
466
              - the value for each entry is the value of the parameter
467

468
        Return values:
469
            The output_type can be used to specify type conversion for single-item return values:
470
            - it can only be used for numbers or a single-number list; other values will generate an exception
471
            - if self.output_type is set to:
472
                FunctionOutputType.NP_0D_ARRAY, return value is "exposed" as a number
473
                FunctionOutputType.NP_1D_ARRAY, return value is 1d np.array
474
                FunctionOutputType.NP_2D_ARRAY, return value is 2d np.array
475
            - it must be enabled for a subclass by setting params[FUNCTION_OUTPUT_TYPE_CONVERSION] = True
476
            - it must be implemented in the execute method of the subclass
477
            - see Linear for an example
478

479
        MechanismRegistry:
480
            All Function functions are registered in FunctionRegistry, which maintains a dict for each subclass,
481
              a count for all instances of that type, and a dictionary of those instances
482

483
        Naming:
484
            Function functions are named by their componentName attribute (usually = componentType)
485

486
        Class attributes:
487
            + componentCategory: FUNCTION_COMPONENT_CATEGORY
488
            + className (str): kwMechanismFunctionCategory
489
            + suffix (str): " <className>"
490
            + registry (dict): FunctionRegistry
491
            + classPreference (PreferenceSet): BasePreferenceSet, instantiated in __init__()
492
            + classPreferenceLevel (PreferenceLevel): PreferenceLevel.CATEGORY
493

494
        Class methods:
495
            none
496

497
        Instance attributes:
498
            + componentType (str):  assigned by subclasses
499
            + componentName (str):   assigned by subclasses
500
            + variable (value) - used as input to function's execute method
501
            + value (value) - output of execute method
502
            + name (str) - if not specified as an arg, a default based on the class is assigned in register_category
503
            + prefs (PreferenceSet) - if not specified as an arg, default is created by copying BasePreferenceSet
504

505
        Instance methods:
506
            The following method MUST be overridden by an implementation in the subclass:
507
            - execute(variable, params)
508
            The following can be implemented, to customize validation of the function variable and/or params:
509
            - [_validate_variable(variable)]
510
            - [_validate_params(request_set, target_set, context)]
511
    COMMENT
512

513
    Arguments
514
    ---------
515

516
    variable : value : default class_defaults.variable
517
        specifies the format and a default value for the input to `function <Function>`.
518

519
    params : Dict[param keyword: param value] : default None
520
        a `parameter dictionary <ParameterPort_Specification>` that specifies the parameters for the
521
        function.  Values specified for parameters in the dictionary override any assigned to those parameters in
522
        arguments of the constructor.
523

524
    owner : Component
525
        `component <Component>` to which to assign the Function.
526

527
    name : str : default see `name <Function.name>`
528
        specifies the name of the Function.
529

530
    prefs : PreferenceSet or specification dict : default Function.classPreferences
531
        specifies the `PreferenceSet` for the Function (see `prefs <Function_Base.prefs>` for details).
532

533

534
    Attributes
535
    ----------
536

537
    variable: value
538
        format and default value can be specified by the :keyword:`variable` argument of the constructor;  otherwise,
539
        they are specified by the Function's :keyword:`class_defaults.variable`.
540

541
    function : function
542
        called by the Function's `owner <Function_Base.owner>` when it is executed.
543

544
    COMMENT:
545
    enable_output_type_conversion : Bool : False
546
        specifies whether `function output type conversion <Function_Output_Type_Conversion>` is enabled.
547

548
    output_type : FunctionOutputType : None
549
        used to determine the return type for the `function <Function_Base.function>`;  `functionOuputTypeConversion`
550
        must be enabled and implemented for the class (see `FunctionOutputType <Function_Output_Type_Conversion>`
551
        for details).
552

553
    changes_shape : bool : False
554
        specifies whether the return value of the function is different than the shape of either is outermost dimension
555
        (axis 0) of its  its `variable <Function_Base.variable>`, or any of the items in the next dimension (axis 1).
556
        Used to determine whether the shape of the inputs to the `Component` to which the function is assigned
557
        should be based on the `variable <Function_Base.variable>` of the function or its `value <Function.value>`.
558
    COMMENT
559

560
    owner : Component
561
        `component <Component>` to which the Function has been assigned.
562

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

567
    prefs : PreferenceSet or specification dict : Function.classPreferences
568
        the `PreferenceSet` for function; if it is not specified in the **prefs** argument of the Function's
569
        constructor, a default is assigned using `classPreferences` defined in __init__.py (see `Preferences`
570
        for details).
571

572
    """
573

574
    componentCategory = FUNCTION_COMPONENT_CATEGORY
1✔
575
    className = componentCategory
1✔
576
    suffix = " " + className
1✔
577

578
    registry = FunctionRegistry
1✔
579

580
    classPreferenceLevel = PreferenceLevel.CATEGORY
1✔
581

582
    _model_spec_id_parameters = 'args'
1✔
583
    _mdf_stateful_parameter_indices = {}
1✔
584

585
    _specified_variable_shape_flexibility = DefaultsFlexibility.INCREASE_DIMENSION
1✔
586

587
    class Parameters(Function.Parameters):
1✔
588
        """
589
            Attributes
590
            ----------
591

592
                variable
593
                    see `variable <Function_Base.variable>`
594

595
                    :default value: numpy.array([0])
596
                    :type: ``numpy.ndarray``
597
                    :read only: True
598

599
                enable_output_type_conversion
600
                    see `enable_output_type_conversion <Function_Base.enable_output_type_conversion>`
601

602
                    :default value: False
603
                    :type: ``bool``
604

605
                changes_shape
606
                    see `changes_shape <Function_Base.changes_shape>`
607

608
                    :default value: False
609
                    :type: bool
610

611
                output_type
612
                    see `output_type <Function_Base.output_type>`
613

614
                    :default value: FunctionOutputType.DEFAULT
615
                    :type: `FunctionOutputType`
616

617
        """
618
        variable = Parameter(np.array([0]), read_only=True, pnl_internal=True, constructor_argument='default_variable')
1✔
619

620
        output_type = Parameter(
1✔
621
            FunctionOutputType.DEFAULT,
622
            stateful=False,
623
            loggable=False,
624
            pnl_internal=True,
625
            valid_types=FunctionOutputType
626
        )
627
        enable_output_type_conversion = Parameter(False, stateful=False, loggable=False, pnl_internal=True)
1✔
628

629
        changes_shape = Parameter(False, stateful=False, loggable=False, pnl_internal=True)
1✔
630
        def _validate_changes_shape(self, param):
1✔
631
            if not isinstance(param, bool):
1!
632
                return f'must be a bool.'
×
633

634
    # Note: the following enforce encoding as 1D np.ndarrays (one array per variable)
635
    variableEncodingDim = 1
1✔
636

637
    @check_user_specified
1✔
638
    @abc.abstractmethod
1✔
639
    def __init__(
1✔
640
        self,
641
        default_variable,
642
        params,
643
        owner=None,
644
        name=None,
645
        prefs=None,
646
        context=None,
647
        **kwargs
648
    ):
649
        """Assign category-level preferences, register category, and call super.__init__
650

651
        Initialization arguments:
652
        - default_variable (anything): establishes type for the variable, used for validation
653
        Note: if parameter_validation is off, validation is suppressed (for efficiency) (Function class default = on)
654

655
        :param default_variable: (anything but a dict) - value to assign as self.defaults.variable
656
        :param params: (dict) - params to be assigned as instance defaults
657
        :param log: (ComponentLog enum) - log entry types set in self.componentLog
658
        :param name: (string) - optional, overrides assignment of default (componentName of subclass)
659
        :return:
660
        """
661

662
        if self.initialization_status == ContextFlags.DEFERRED_INIT:
1!
663
            self._assign_deferred_init_name(name)
×
664
            self._init_args[NAME] = name
×
665
            return
×
666

667
        register_category(entry=self,
1✔
668
                          base_class=Function_Base,
669
                          registry=FunctionRegistry,
670
                          name=name,
671
                          )
672
        self.owner = owner
1✔
673

674
        super().__init__(
1✔
675
            default_variable=default_variable,
676
            param_defaults=params,
677
            name=name,
678
            prefs=prefs,
679
            **kwargs
680
        )
681

682
    def __call__(self, *args, **kwargs):
1✔
683
        return self.function(*args, **kwargs)
1✔
684

685
    def __deepcopy__(self, memo):
1✔
686
        new = super().__deepcopy__(memo)
1✔
687

688
        if self is not new:
1✔
689
            # ensure copy does not have identical name
690
            register_category(new, Function_Base, new.name, FunctionRegistry)
1✔
691
            if "random_state" in new.parameters:
1✔
692
                # HACK: Make sure any copies are re-seeded to avoid dependent RNG.
693
                # functions with "random_state" param must have "seed" parameter
694
                for ctx in new.parameters.seed.values:
1✔
695
                    new.parameters.seed.set(
1✔
696
                        DEFAULT_SEED(), ctx, skip_log=True, skip_history=True
697
                    )
698

699
        return new
1✔
700

701
    @handle_external_context()
1✔
702
    def function(self,
1✔
703
                 variable=None,
704
                 context=None,
705
                 params=None,
706
                 target_set=None,
707
                 **kwargs):
708

709
        if ContextFlags.COMMAND_LINE in context.source:
1✔
710
            variable = copy_parameter_value(variable)
1✔
711

712
        # IMPLEMENTATION NOTE:
713
        # The following is a convenience feature that supports specification of params directly in call to function
714
        # by moving the to a params dict, which treats them as runtime_params
715
        if kwargs:
1✔
716
            for key in kwargs.copy():
1✔
717
                if key in self.parameters.names():
1✔
718
                    if not params:
1✔
719
                        params = {key: kwargs.pop(key)}
1✔
720
                    else:
721
                        params.update({key: kwargs.pop(key)})
1✔
722

723
        # Validate variable and assign to variable, and validate params
724
        variable = self._check_args(variable=variable,
1✔
725
                                    context=context,
726
                                    params=params,
727
                                    target_set=target_set,
728
                                    )
729
        # Execute function
730
        value = self._function(
1✔
731
            variable=variable, context=context, params=params, **kwargs
732
        )
733
        self.most_recent_context = context
1✔
734
        self.parameters.value._set(value, context=context)
1✔
735
        self._reset_runtime_parameters(context)
1✔
736
        return value
1✔
737

738
    @abc.abstractmethod
1✔
739
    def _function(
1✔
740
        self,
741
        variable=None,
742
        context=None,
743
        params=None,
744

745
    ):
746
        pass
×
747

748
    def _parse_arg_generic(self, arg_val):
1✔
749
        if isinstance(arg_val, list):
×
750
            return np.asarray(arg_val)
×
751
        else:
752
            return arg_val
×
753

754
    def _validate_parameter_spec(self, param, param_name, numeric_only=True):
1✔
755
        """Validates function param
756
        Replace direct call to parameter_spec in tc, which seems to not get called by Function __init__()'s
757
        """
758
        if not parameter_spec(param, numeric_only):
1!
759
            owner_name = 'of ' + self.owner_name if self.owner else ""
×
760
            raise FunctionError(f"{param} is not a valid specification for "
761
                                f"the {param_name} argument of {self.__class__.__name__}{owner_name}.")
762

763
    def _get_current_parameter_value(self, param_name, context=None):
1✔
764
        try:
1✔
765
            param = getattr(self.parameters, param_name)
1✔
766
        except TypeError:
1✔
767
            param = param_name
1✔
768
        except AttributeError:
×
769
            # don't accept strings that don't correspond to Parameters
770
            # on this function
771
            raise
×
772

773
        return super()._get_current_parameter_value(param, context)
1✔
774

775
    def get_previous_value(self, context=None):
1✔
776
        # temporary method until previous values are integrated for all parameters
777
        value = self.parameters.previous_value._get(context)
1✔
778

779
        return value
1✔
780

781
    def convert_output_type(self, value, output_type=None):
1✔
782
        value = convert_all_elements_to_np_array(value)
1✔
783
        if output_type is None:
1✔
784
            if not self.enable_output_type_conversion or self.output_type is None:
1✔
785
                return value
1✔
786
            else:
787
                output_type = self.output_type
1✔
788

789
        # Type conversion (specified by output_type):
790

791
        # MODIFIED 6/21/19 NEW: [JDC]
792
        # Convert to same format as variable
793
        if isinstance(output_type, (list, np.ndarray)):
1✔
794
            shape = np.array(output_type).shape
1✔
795
            return np.array(value).reshape(shape)
1✔
796
        # MODIFIED 6/21/19 END
797

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

804
            converted_to_2d = np.atleast_2d(value)
1✔
805
            # If return_value is a list of heterogenous elements, return as is
806
            #     (satisfies requirement that return_value be an array of possibly multidimensional values)
807
            if converted_to_2d.dtype == object:
1✔
808
                pass
1✔
809
            # Otherwise, return value converted to 2d np.array
810
            else:
811
                value = converted_to_2d
1✔
812

813
        # Convert to 1D array, irrespective of value type:
814
        # Note: if 2D array (or higher) has more than two items in the outer dimension, generate exception
815
        elif output_type is FunctionOutputType.NP_1D_ARRAY:
1✔
816
            # If variable is 2D
817
            if value.ndim >= 2:
1✔
818
                # If there is only one item:
819
                if len(value) == 1:
1✔
820
                    value = value[0]
1✔
821
                else:
822
                    raise FunctionError(f"Can't convert value ({value}: 2D np.ndarray object "
823
                                        f"with more than one array) to 1D array.")
824
            elif value.ndim == 1:
1!
825
                pass
1✔
826
            elif value.ndim == 0:
×
827
                value = np.atleast_1d(value)
×
828
            else:
829
                raise FunctionError(f"Can't convert value ({value} to 1D array.")
830

831
        # Convert to raw number, irrespective of value type:
832
        # Note: if 2D or 1D array has more than two items, generate exception
833
        elif output_type is FunctionOutputType.NP_0D_ARRAY:
1!
834
            if object_has_single_value(value):
1✔
835
                value = np.asfarray(value)
1✔
836
            else:
837
                raise FunctionError(f"Can't convert value ({value}) with more than a single number to a raw number.")
838

839
        return value
1✔
840

841
    @property
1✔
842
    def owner_name(self):
1✔
843
        try:
1✔
844
            return self.owner.name
1✔
845
        except AttributeError:
×
846
            return '<no owner>'
×
847

848
    def _is_identity(self, context=None, defaults=False):
1✔
849
        # should return True in subclasses if the parameters for context are such that
850
        # the Function's output will be the same as its input
851
        # Used to bypass execute when unnecessary
852
        return False
1✔
853

854
    @property
1✔
855
    def _model_spec_parameter_blacklist(self):
1✔
856
        return super()._model_spec_parameter_blacklist.union({
1✔
857
            'multiplicative_param', 'additive_param',
858
        })
859

860
    def _assign_to_mdf_model(self, model, input_id) -> str:
1✔
861
        """Adds an MDF representation of this function to MDF object
862
        **model**, including all necessary auxiliary functions.
863
        **input_id** is the input to the singular MDF function or first
864
        function representing this psyneulink Function, if applicable.
865

866
        Returns:
867
            str: the identifier of the final MDF function representing
868
            this psyneulink Function
869
        """
870
        import modeci_mdf.mdf as mdf
1✔
871

872
        extra_noise_functions = []
1✔
873

874
        self_model = self.as_mdf_model()
1✔
875

876
        def handle_noise(noise):
1✔
877
            if is_instance_or_subclass(noise, Component):
1✔
878
                if inspect.isclass(noise) and issubclass(noise, Component):
1!
879
                    noise = noise()
×
880
                noise_func_model = noise.as_mdf_model()
1✔
881
                extra_noise_functions.append(noise_func_model)
1✔
882
                return noise_func_model.id
1✔
883
            elif isinstance(noise, (list, np.ndarray)):
1!
884
                if noise.ndim == 0:
1!
885
                    return None
1✔
UNCOV
886
                return type(noise)(handle_noise(item) for item in noise)
×
887
            else:
UNCOV
888
                return None
×
889

890
        try:
1✔
891
            noise_val = handle_noise(self.defaults.noise)
1✔
892
        except AttributeError:
1✔
893
            noise_val = None
1✔
894

895
        if noise_val is not None:
1✔
896
            noise_func = mdf.Function(
1✔
897
                id=f'{model.id}_{parse_valid_identifier(self.name)}_noise',
898
                value=MODEL_SPEC_ID_MDF_VARIABLE,
899
                args={MODEL_SPEC_ID_MDF_VARIABLE: noise_val},
900
            )
901
            self._set_mdf_arg(self_model, 'noise', noise_func.id)
1✔
902

903
            model.functions.extend(extra_noise_functions)
1✔
904
            model.functions.append(noise_func)
1✔
905

906
        self_model.id = f'{model.id}_{self_model.id}'
1✔
907
        self._set_mdf_arg(self_model, _get_variable_parameter_name(self), input_id)
1✔
908
        model.functions.append(self_model)
1✔
909

910
        # assign stateful parameters
911
        for name, index in self._mdf_stateful_parameter_indices.items():
1✔
912
            # in this case, parameter gets updated to its function's final value
913
            param = getattr(self.parameters, name)
1✔
914

915
            try:
1✔
916
                initializer_value = self_model.args[param.initializer]
1✔
917
            except KeyError:
1✔
918
                initializer_value = self_model.metadata[param.initializer]
1✔
919

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

922
            model.parameters.append(
1✔
923
                mdf.Parameter(
924
                    id=param.mdf_name if param.mdf_name is not None else param.name,
925
                    default_initial_value=initializer_value,
926
                    value=f'{self_model.id}{index_str}'
927
                )
928
            )
929

930
        return self_model.id
1✔
931

932
    def as_mdf_model(self):
1✔
933
        import modeci_mdf.mdf as mdf
1✔
934
        import modeci_mdf.functions.standard as mdf_functions
1✔
935

936
        parameters = self._mdf_model_parameters
1✔
937
        metadata = self._mdf_metadata
1✔
938
        stateful_params = set()
1✔
939

940
        # add stateful parameters into metadata for mechanism to get
941
        for name in parameters[self._model_spec_id_parameters]:
1✔
942
            try:
1✔
943
                param = getattr(self.parameters, name)
1✔
944
            except AttributeError:
1✔
945
                continue
1✔
946

947
            if param.initializer is not None:
1✔
948
                stateful_params.add(name)
1✔
949

950
        # stateful parameters cannot show up as args or they will not be
951
        # treated statefully in mdf
952
        for sp in stateful_params:
1✔
953
            del parameters[self._model_spec_id_parameters][sp]
1✔
954

955
        model = mdf.Function(
1✔
956
            id=parse_valid_identifier(self.name),
957
            **parameters,
958
            **metadata,
959
        )
960

961
        try:
1✔
962
            model.value = self.as_expression()
1✔
963
        except AttributeError:
1✔
964
            if self._model_spec_generic_type_name is not NotImplemented:
1✔
965
                typ = self._model_spec_generic_type_name
1✔
966
            else:
967
                try:
1✔
968
                    typ = self.custom_function.__name__
1✔
969
                except AttributeError:
1✔
970
                    typ = type(self).__name__.lower()
1✔
971

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

975
            model.function = typ
1✔
976

977
        return model
1✔
978

979
    def _get_pytorch_fct_param_value(self, param_name, device, context):
1✔
980
        """Return the current value of param_name for the function
981
         Use default value if not yet assigned
982
         Convert using torch.tensor if val is an array
983
        """
984
        val = self._get_current_parameter_value(param_name, context=context)
1✔
985
        if val is None:
1✔
986
            val = getattr(self.defaults, param_name)
1✔
987
        if isinstance(val, (str, type(None))):
1✔
988
            return val
1✔
989
        elif np.isscalar(np.array(val)):
1!
990
            return float(val)
×
991
        try:
1✔
992
            # return torch.tensor(val, device=device).double()
993
            return torch.tensor(val, device=device)
1✔
NEW
994
        except Exception as error:
×
995
            raise FunctionError(f"PROGRAM ERROR: unsupported value of parameter '{param_name}' ({val}) "
996
                                f"encountered in pytorch_function_creator(): {error.args[0]}")
997

998

999
# *****************************************   EXAMPLE FUNCTION   *******************************************************
1000
PROPENSITY = "PROPENSITY"
1✔
1001
PERTINACITY = "PERTINACITY"
1✔
1002

1003

1004
class ArgumentTherapy(Function_Base):
1✔
1005
    """
1006
    ArgumentTherapy(                   \
1007
         variable,                     \
1008
         propensity=Manner.CONTRARIAN, \
1009
         pertinacity=10.0              \
1010
         params=None,                  \
1011
         owner=None,                   \
1012
         name=None,                    \
1013
         prefs=None                    \
1014
         )
1015

1016
    .. _ArgumentTherapist:
1017

1018
    Return `True` or :keyword:`False` according to the manner of the therapist.
1019

1020
    Arguments
1021
    ---------
1022

1023
    variable : boolean or statement that resolves to one : default class_defaults.variable
1024
        assertion for which a therapeutic response will be offered.
1025

1026
    propensity : Manner value : default Manner.CONTRARIAN
1027
        specifies preferred therapeutic manner
1028

1029
    pertinacity : float : default 10.0
1030
        specifies therapeutic consistency
1031

1032
    params : Dict[param keyword: param value] : default None
1033
        a `parameter dictionary <ParameterPort_Specification>` that specifies the parameters for the
1034
        function.  Values specified for parameters in the dictionary override any assigned to those parameters in
1035
        arguments of the constructor.
1036

1037
    owner : Component
1038
        `component <Component>` to which to assign the Function.
1039

1040
    name : str : default see `name <Function.name>`
1041
        specifies the name of the Function.
1042

1043
    prefs : PreferenceSet or specification dict : default Function.classPreferences
1044
        specifies the `PreferenceSet` for the Function (see `prefs <Function_Base.prefs>` for details).
1045

1046

1047
    Attributes
1048
    ----------
1049

1050
    variable : boolean
1051
        assertion to which a therapeutic response is made.
1052

1053
    propensity : Manner value : default Manner.CONTRARIAN
1054
        determines therapeutic manner:  tendency to agree or disagree.
1055

1056
    pertinacity : float : default 10.0
1057
        determines consistency with which the manner complies with the propensity.
1058

1059
    owner : Component
1060
        `component <Component>` to which the Function has been assigned.
1061

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

1066
    prefs : PreferenceSet or specification dict : Function.classPreferences
1067
        the `PreferenceSet` for function; if it is not specified in the **prefs** argument of the Function's
1068
        constructor, a default is assigned using `classPreferences` defined in __init__.py (see `Preferences`
1069
        for details).
1070

1071

1072
    """
1073

1074
    # Function componentName and type (defined at top of module)
1075
    componentName = ARGUMENT_THERAPY_FUNCTION
1✔
1076
    componentType = EXAMPLE_FUNCTION_TYPE
1✔
1077

1078
    classPreferences = {
1✔
1079
        PREFERENCE_SET_NAME: 'ExampleClassPreferences',
1080
        REPORT_OUTPUT_PREF: PreferenceEntry(False, PreferenceLevel.INSTANCE),
1081
    }
1082

1083
    # Mode indicators
1084
    class Manner(Enum):
1✔
1085
        OBSEQUIOUS = 0
1✔
1086
        CONTRARIAN = 1
1✔
1087

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

1092
    @check_user_specified
1✔
1093
    def __init__(self,
1✔
1094
                 default_variable=None,
1095
                 propensity=10.0,
1096
                 pertincacity=Manner.CONTRARIAN,
1097
                 params=None,
1098
                 owner=None,
1099
                 prefs:  Optional[ValidPrefSet] = None):
1100

1101
        super().__init__(
×
1102
            default_variable=default_variable,
1103
            propensity=propensity,
1104
            pertinacity=pertincacity,
1105
            params=params,
1106
            owner=owner,
1107
            prefs=prefs,
1108
        )
1109

1110
    def _validate_variable(self, variable, context=None):
1✔
1111
        """Validates variable and returns validated value
1112

1113
        This overrides the class method, to perform more detailed type checking
1114
        See explanation in class method.
1115
        Note: this method (or the class version) is called only if the parameter_validation attribute is `True`
1116

1117
        :param variable: (anything but a dict) - variable to be validated:
1118
        :param context: (str)
1119
        :return variable: - validated
1120
        """
1121

1122
        if type(variable) == type(self.class_defaults.variable) or \
×
1123
                (isinstance(variable, numbers.Number) and isinstance(self.class_defaults.variable, numbers.Number)):
1124
            return variable
×
1125
        else:
1126
            raise FunctionError(f"Variable must be {type(self.class_defaults.variable)}.")
1127

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

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

1135
        :param request_set: (dict) - params to be validated
1136
        :param target_set: (dict) - destination of validated params
1137
        :return none:
1138
        """
1139

1140
        message = ""
×
1141

1142
        # Check params
1143
        for param_name, param_value in request_set.items():
×
1144

1145
            if param_name == PROPENSITY:
×
1146
                if isinstance(param_value, ArgumentTherapy.Manner):
×
1147
                    # target_set[self.PROPENSITY] = param_value
1148
                    pass  # This leaves param in request_set, clear to be assigned to target_set in call to super below
×
1149
                else:
1150
                    message = "Propensity must be of type Example.Mode"
×
1151
                continue
×
1152

1153
            # Validate param
1154
            if param_name == PERTINACITY:
×
1155
                if isinstance(param_value, numbers.Number) and 0 <= param_value <= 10:
×
1156
                    # target_set[PERTINACITY] = param_value
1157
                    pass  # This leaves param in request_set, clear to be assigned to target_set in call to super below
×
1158
                else:
1159
                    message += "Pertinacity must be a number between 0 and 10"
×
1160
                continue
×
1161

1162
        if message:
×
1163
            raise FunctionError(message)
1164

1165
        super()._validate_params(request_set, target_set, context)
×
1166

1167
    def _function(self,
1✔
1168
                 variable=None,
1169
                 context=None,
1170
                 params=None,
1171
                 ):
1172
        """
1173
        Returns a boolean that is (or tends to be) the same as or opposite the one passed in.
1174

1175
        Arguments
1176
        ---------
1177

1178
        variable : boolean : default class_defaults.variable
1179
           an assertion to which a therapeutic response is made.
1180

1181
        params : Dict[param keyword: param value] : default None
1182
            a `parameter dictionary <ParameterPort_Specification>` that specifies the parameters for the
1183
            function.  Values specified for parameters in the dictionary override any assigned to those parameters in
1184
            arguments of the constructor.
1185

1186

1187
        Returns
1188
        -------
1189

1190
        therapeutic response : boolean
1191

1192
        """
1193
        # Compute the function
1194
        statement = variable
×
1195
        propensity = self._get_current_parameter_value(PROPENSITY, context)
×
1196
        pertinacity = self._get_current_parameter_value(PERTINACITY, context)
×
1197
        whim = np.random.randint(-10, 10)
×
1198

1199
        if propensity == self.Manner.OBSEQUIOUS:
×
1200
            value = whim < pertinacity
×
1201

1202
        elif propensity == self.Manner.CONTRARIAN:
×
1203
            value = whim > pertinacity
×
1204

1205
        else:
1206
            raise FunctionError("This should not happen if parameter_validation == True;  check its value")
1207

1208
        return self.convert_output_type(value)
×
1209

1210

1211

1212
kwEVCAuxFunction = "EVC AUXILIARY FUNCTION"
1✔
1213
kwEVCAuxFunctionType = "EVC AUXILIARY FUNCTION TYPE"
1✔
1214
kwValueFunction = "EVC VALUE FUNCTION"
1✔
1215
CONTROL_SIGNAL_GRID_SEARCH_FUNCTION = "EVC CONTROL SIGNAL GRID SEARCH FUNCTION"
1✔
1216
CONTROLLER = 'controller'
1✔
1217

1218
class EVCAuxiliaryFunction(Function_Base):
1✔
1219
    """Base class for EVC auxiliary functions
1220
    """
1221
    componentType = kwEVCAuxFunctionType
1✔
1222

1223
    class Parameters(Function_Base.Parameters):
1✔
1224
        """
1225
            Attributes
1226
            ----------
1227

1228
                variable
1229
                    see `variable <Function_Base.variable>`
1230

1231
                    :default value: numpy.array([0])
1232
                    :type: numpy.ndarray
1233
                    :read only: True
1234

1235
        """
1236
        variable = Parameter(None, pnl_internal=True, constructor_argument='default_variable')
1✔
1237

1238
    classPreferences = {
1✔
1239
        PREFERENCE_SET_NAME: 'ValueFunctionCustomClassPreferences',
1240
        REPORT_OUTPUT_PREF: PreferenceEntry(False, PreferenceLevel.INSTANCE),
1241
       }
1242

1243
    @check_user_specified
1✔
1244
    @beartype
1✔
1245
    def __init__(self,
1✔
1246
                 function,
1247
                 variable=None,
1248
                 params=None,
1249
                 owner=None,
1250
                 prefs:   Optional[ValidPrefSet] = None,
1251
                 context=None):
1252
        self.aux_function = function
×
1253

1254
        super().__init__(default_variable=variable,
×
1255
                         params=params,
1256
                         owner=owner,
1257
                         prefs=prefs,
1258
                         context=context,
1259
                         function=function,
1260
                         )
1261

1262

1263
class RandomMatrix():
1✔
1264
    """Function that returns matrix with random elements distributed uniformly around **center** across **range**.
1265

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

1270
    Can be used to specify the `matrix <MappingProjection.matrix>` parameter of a `MappingProjection
1271
    <MappingProjection_Matrix_Specification>`, and to specify a default matrix for Projections in the
1272
    construction of a `Pathway` (see `Pathway_Specification_Projections`) or in a call to a Composition's
1273
    `add_linear_processing_pathway<Composition.add_linear_processing_pathway>` method.
1274

1275
    .. technical_note::
1276
       A call to the class calls `random_matrix <Utilities.random_matrix>`, passing **sender_size** and
1277
       **receiver_size** to `random_matrix <Utilities.random_matrix>` as its **num_rows** and **num_cols**
1278
       arguments, respectively, and passing the `center <RandomMatrix.offset>`-0.5 and `range <RandomMatrix.scale>`
1279
       attributes specified at construction to `random_matrix <Utilities.random_matrix>` as its **offset**
1280
       and **scale** arguments, respectively.
1281

1282
    Arguments
1283
    ----------
1284
    center : float
1285
        specifies the value around which the matrix elements are distributed in all calls to the function.
1286
    range : float
1287
        specifies range over which all matrix elements are distributed in all calls to the function.
1288

1289
    Attributes
1290
    ----------
1291
    center : float
1292
        determines the center of the distribution of the matrix elements;
1293
    range : float
1294
        determines the range of the distribution of the matrix elements;
1295
    """
1296

1297
    def __init__(self, center:float=0.0, range:float=1.0):
1✔
1298
        self.center=center
×
1299
        self.range=range
×
1300

1301
    def __call__(self, sender_size:int, receiver_size:int):
1✔
1302
        return random_matrix(sender_size, receiver_size, offset=self.center - 0.5, scale=self.range)
×
1303

1304

1305
def get_matrix(specification, rows=1, cols=1, context=None):
1✔
1306
    """Returns matrix conforming to specification with dimensions = rows x cols or None
1307

1308
     Specification can be a matrix keyword, filler value or np.ndarray
1309

1310
     Specification (validated in _validate_params):
1311
        + single number (used to fill self.matrix)
1312
        + matrix keyword:
1313
            + AUTO_ASSIGN_MATRIX: IDENTITY_MATRIX if it is square, othwerwise FULL_CONNECTIVITY_MATRIX
1314
            + IDENTITY_MATRIX: 1's on diagonal, 0's elsewhere (must be square matrix), otherwise generates error
1315
            + HOLLOW_MATRIX: 0's on diagonal, 1's elsewhere (must be square matrix), otherwise generates error
1316
            + INVERSE_HOLLOW_MATRIX: 0's on diagonal, -1's elsewhere (must be square matrix), otherwise generates error
1317
            + FULL_CONNECTIVITY_MATRIX: all 1's
1318
            + ZERO_MATRIX: all 0's
1319
            + RANDOM_CONNECTIVITY_MATRIX (random floats uniformly distributed between 0 and 1)
1320
            + RandomMatrix (random floats uniformly distributed around a specified center value with a specified range)
1321
        + 2D list or np.ndarray of numbers
1322

1323
     Returns 2D array with length=rows in dim 0 and length=cols in dim 1, or none if specification is not recognized
1324
    """
1325

1326
    # Matrix provided (and validated in _validate_params); convert to array
1327
    if isinstance(specification, (list, np.matrix)):
1✔
1328
        if is_numeric(specification):
1✔
1329
            return convert_to_np_array(specification)
1✔
1330
        else:
1331
            return
1✔
1332
        # MODIFIED 4/9/22 END
1333

1334
    if isinstance(specification, np.ndarray):
1✔
1335
        if specification.ndim == 2:
1✔
1336
            return specification
1✔
1337
        # FIX: MAKE THIS AN np.array WITH THE SAME DIMENSIONS??
1338
        elif specification.ndim < 2:
1✔
1339
            return np.atleast_2d(specification)
×
1340
        else:
1341
            raise FunctionError("Specification of np.array for matrix ({}) is more than 2d".
1342
                                format(specification))
1343

1344
    if specification == AUTO_ASSIGN_MATRIX:
1✔
1345
        if rows == cols:
1✔
1346
            specification = IDENTITY_MATRIX
1✔
1347
        else:
1348
            specification = FULL_CONNECTIVITY_MATRIX
1✔
1349

1350
    if specification == FULL_CONNECTIVITY_MATRIX:
1✔
1351
        return np.full((rows, cols), 1.0)
1✔
1352

1353
    if specification == ZEROS_MATRIX:
1✔
1354
        return np.zeros((rows, cols))
1✔
1355

1356
    if specification == IDENTITY_MATRIX:
1✔
1357
        if rows != cols:
1✔
1358
            raise FunctionError("Sender length ({}) must equal receiver length ({}) to use {}".
1359
                                format(rows, cols, specification))
1360
        return np.identity(rows)
1✔
1361

1362
    if specification == HOLLOW_MATRIX:
1✔
1363
        if rows != cols:
1✔
1364
            raise FunctionError("Sender length ({}) must equal receiver length ({}) to use {}".
1365
                                format(rows, cols, specification))
1366
        return 1 - np.identity(rows)
1✔
1367

1368
    if specification == INVERSE_HOLLOW_MATRIX:
1✔
1369
        if rows != cols:
1✔
1370
            raise FunctionError("Sender length ({}) must equal receiver length ({}) to use {}".
1371
                                format(rows, cols, specification))
1372
        return (1 - np.identity(rows)) * -1
1✔
1373

1374
    if specification == RANDOM_CONNECTIVITY_MATRIX:
1✔
1375
        return np.random.rand(rows, cols)
1✔
1376

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

1381
    # (7/12/17 CW) this is a PATCH (like the one in MappingProjection) to allow users to
1382
    # specify 'matrix' as a string (e.g. r = RecurrentTransferMechanism(matrix='1 2; 3 4'))
1383
    if type(specification) == str:
1✔
1384
        try:
1✔
1385
            return array_from_matrix_string(specification)
1✔
1386
        except (ValueError, NameError, TypeError):
1✔
1387
            # np.matrix(specification) will give ValueError if specification is a bad value (e.g. 'abc', '1; 1 2')
1388
            #                          [JDC] actually gives NameError if specification is a string (e.g., 'abc')
1389
            pass
1✔
1390

1391
    # Specification not recognized
1392
    return None
1✔
1393

1394

1395
# Valid types for a matrix specification, note this is does not ensure that ND arrays are 1D or 2D like the
1396
# above code does.
1397
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