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

PrincetonUniversity / PsyNeuLink / 11884856299

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

push

github

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

Devel

9405 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

83.4
/psyneulink/core/components/functions/userdefinedfunction.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
# *****************************************  USER-DEFINED FUNCTION  ****************************************************
11

12
import builtins
1✔
13
import numpy as np
1✔
14
from beartype import beartype
1✔
15

16
from psyneulink._typing import Optional
1✔
17
from inspect import signature, _empty, getsourcelines, getsourcefile, getclosurevars
1✔
18
import ast
1✔
19

20
from psyneulink.core.components.functions.function import FunctionError, Function_Base
1✔
21
from psyneulink.core.globals.keywords import \
1✔
22
    CONTEXT, CUSTOM_FUNCTION, OWNER, PARAMS, \
23
    SELF, USER_DEFINED_FUNCTION, USER_DEFINED_FUNCTION_TYPE
24
from psyneulink.core.globals.parameters import Parameter, check_user_specified
1✔
25
from psyneulink.core.globals.preferences import ValidPrefSet
1✔
26
from psyneulink.core.globals.utilities import _is_module_class, iscompatible
1✔
27

28
from psyneulink.core import llvm as pnlvm
1✔
29

30
__all__ = ['UserDefinedFunction']
1✔
31

32

33
class _ExpressionVisitor(ast.NodeVisitor):
1✔
34
    def __init__(self, *args, **kwargs):
1✔
35
        super().__init__(*args, **kwargs)
1✔
36
        self.vars = set()
1✔
37
        self.functions = set()
1✔
38

39
    def visit_Name(self, node):
1✔
40
        if node.id not in dir(builtins):
1✔
41
            self.vars.add(node.id)
1✔
42

43
    def visit_Call(self, node):
1✔
44
        try:
1✔
45
            # gives top level module name if module function used
46
            func_id = node.func.value.id
1✔
47
        except AttributeError:
1✔
48
            func_id = node.func.id
1✔
49

50
        if func_id not in dir(builtins):
1✔
51
            self.functions.add(func_id)
1✔
52

53
        for c in ast.iter_child_nodes(node):
1✔
54
            self.visit(c)
1✔
55

56

57
class UserDefinedFunction(Function_Base):
1✔
58
    """UserDefinedFunction(  \
59
    custom_function=None,    \
60
    default_variable=None,   \
61
    params=None,             \
62
    owner=None,              \
63
    name=None,               \
64
    prefs=None)
65

66
    .. _UDF_Description:
67

68
    A UserDefinedFunction (UDF) is used to "wrap" a Python function or method, lamdba function,
69
    or an expression written in string format
70
    as a PsyNeuLink `Function <Function>`, so that it can be used as the `function <Component.function>` of a
71
    `Component <Component>`.  This is done automatically if a Python function or method is assigned as the `function
72
    <Component.function>` attribute of a Component.  A Python function or method can also be wrapped explicitly,
73
    using the UserDefinedFunction constructor, and assigning the Python function or method to its **custom_function**
74
    argument.  A Python function or method wrapped as a UDF must obey the following conventions to be treated
75
    correctly:
76

77
    .. _UDF_Variable:
78

79
    * If providing a Python function, method, or lambda function, it must have **at least one argument** (that can be a positional or a keyword argument);  this will be treated
80
      as the `variable <UserDefinedFunction.variable>` attribute of the UDF's `function <UserDefinedFunction.function>`.
81
      When the UDF calls the function or method that it wraps, an initial attempt is made to do so with **variable**
82
      as the name of the first argument; if that fails, it is called positionally.  The argument is always passed as a
83
      2d np.array, that may contain one or more items (elements in axis 0), depending upon the Component to which the
84
      UDF is assigned.  It is the user's responsibility to insure that the number of items expected in the first
85
      argument of the function or method is compatible with the circumstances in which it will be called.
86
      If providing a string expression, **variable** is optional. However, if **variable** is not included in
87
      the expression, the resulting UDF will not use **variable** at all in its calculation.
88
    ..
89
    .. _UDF_Additional_Arguments:
90

91
    * It may have have **any number of additional arguments** (positional and/or keyword);  these are treated as
92
      parameters of the UDF, and can be modulated by `ModulatorySignals <ModulatorySignal>` like the parameters of
93
      ordinary PsyNeuLink `Functions <Function>`.  If the UDF is assigned to (or automatically created for) a
94
      `Mechanism <Mechanism>` or `Projection <Projection>`, these parameters are each automatically assigned a
95
      `ParameterPort` so that they can be modulated by `ControlSignals <ControlSignal>` or `LearningSignals
96
      <LearningSignal>`, respectively.  If the UDF is assigned to (or automatically created for) an `InputPort` or
97
      `OutputPort`, and any of the parameters are specified as `Function_Modulatory_Params` (see `below
98
      <UDF_Modulatory_Params>`), then they can be modulated by `GatingSignals <GatingSignal>`. The function or method
99
      wrapped by the UDF is called with these parameters by their name and with their current values (i.e.,
100
      as determined by any `ModulatorySignals <ModulatorySignal>` assigned to them).
101
    ..
102
    .. _UDF_Params_Context:
103

104
    * It may include **self**, **owner**, **context**, and/or **params** arguments;  none of these are
105
      required, but can be included to gain access to the standard `Function` parameters (such as the history of its
106
      parameter values), those of the `Component` to which it has been assigned (i.e., its `owner <Function.owner>`,
107
      and/or receive information about the current conditions of execution.   When the function or method is called,
108
      an initial attempt is made to pass these arguments; if that fails, it is called again without them.
109
    ..
110
    .. _UDF_Modulatory_Params:
111

112
    * The parameters of a UDF can be specified as `Function_Modulatory_Params` in a `parameter specification dictionary
113
      <ParameterPort_Specification>` assigned to the **params** argument of the constructor for either the Python
114
      function or method, or of an explicitly defined UDF (see `examples below <UDF_Modulatory_Params_Examples>`).
115
      It can include either or both of the following two entries:
116
         *MULTIPLICATIVE_PARAM*: <parameter name>\n
117
         *ADDITIVE_PARAM*: <parameter name>
118
      These are used only when the UDF is assigned as the `function <Port_Base.function>` of an InputPort or
119
      OutputPort that receives one more more `GatingProjections <GatingProjection>`.
120

121
      COMMENT:
122
      # IMPLEMENT INTERFACE FOR OTHER MODULATION TYPES (i.e., for ability to add new custom ones)
123
      COMMENT
124

125
    .. tip::
126
       The format of the `variable <UserDefinedFunction.variable>` passed to the `custom_function
127
       <UserDefinedFunction.custom_function>` function can be verified by adding a ``print(variable)`` or
128
       ``print(type(variable))`` statement to the function.
129

130
    Examples
131
    --------
132

133
    **Assigning a custom function to a Mechanism**
134

135
    .. _UDF_Lambda_Function_Examples:
136

137
    The following example assigns a simple lambda function that returns the sum of the elements of a 1d array) to a
138
    `TransferMechanism`::
139

140
        >>> import psyneulink as pnl
141
        >>> my_mech = pnl.ProcessingMechanism(default_variable=[[0,0,0]],
142
        ...                                   function=lambda x:sum(x[0]))
143
        >>> my_mech.execute(input = [1, 2, 3])
144
        array([[6]])
145

146
    Note that the function treats its argument, x, as a 2d array, and accesses its first item for the calculation.
147
    This is because  the `variable <Mechanism_Base.variable>` of ``my_mech`` is defined in the **input_shapes** argument of
148
    its constructor as having a single item (a 1d array of length 3;  (see `input_shapes <Component.input_shapes>`).  In the
149
    following example, a function is defined for a Mechanism in which the variable has two items, that are summed by
150
    the function::
151

152
        >>> my_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
153
        ...                                   function=lambda x: x[0] + x[1])
154
        >>> my_mech.execute(input = [[1],[2]])
155
        array([[3]])
156

157
    .. _UDF_Defined_Function_Examples:
158

159
    The **function** argument can also be assigned a function defined in Python::
160

161
        >>> def my_fct(variable):
162
        ...     return variable[0] + variable[1]
163
        >>> my_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
164
        ...                                   function=my_fct)
165

166
    This will produce the same result as the last example.  This can be useful for assigning the function to more than
167
    one Component.
168

169
    More complicated functions, including ones with more than one parameter can also be used;  for example::
170

171
        >>> def my_sinusoidal_fct(input=[[0],[0]],
172
        ...                       phase=0,
173
        ...                       amplitude=1):
174
        ...    frequency = input[0]
175
        ...    t = input[1]
176
        ...    return amplitude * np.sin(2 * np.pi * frequency * t + phase)
177
        >>> my_wave_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
178
        ...                                        function=my_sinusoidal_fct)
179

180
    Note that in this example, ``input`` is used as the name of the first argument, instead of ``variable``
181
    as in the examples above. The name of the first argument of a function to be "wrapped" as a UDF does not matter;
182
    in general it is good practice to use ``variable``, as the `variable <Component.variable>` of the Component
183
    to which the UDF is assigned is what is passed to the function as its first argument.  However, if it is helpful to
184
    name it something else, that is fine.
185

186
    Notice also that ``my_sinusoidal_fct`` takes two values in its ``input`` argument, that it assigns to the
187
    ``frequency`` and ``t`` variables of the function.  While  it could have been specified more compactly as a 1d array
188
    with two elements (i.e. [0,0]), it is specified in the example as a 2d array with two items to make it clear that
189
    it matches the format of the **default_variable** for the ProcessingMechanism to which it will be assigned,
190
    which requires it be formatted this way (since the `variable <Component.variable>` of all Components are converted
191
    to a 2d array).
192

193
    ``my_sinusoidal_fct`` also has two other arguments, ``phase`` and ``amplitude``.   When it is assigned to
194
    ``my_wave_mech``, those parameters are assigned to `ParameterPorts <ParameterPort>` of ``my_wave_mech``, which
195
    that be used to modify their values by `ControlSignals <ControlSignal>` (see `example below <_
196
    UDF_Control_Signal_Example>`).
197

198
    .. _UDF_String_Expression_Function_Examples:
199

200
    The **function** argument may also be an expression written as a string::
201

202
        >>> my_mech = pnl.ProcessingMechanism(function='sum(variable, 2)')
203
        >>> my_mech.execute(input=[1])
204
        array([[3]])
205

206
    This option is primarily designed for compatibility with other packages that use string expressions as
207
    their main description of computation and may be less flexible or reliable than the previous styles.
208

209
    .. _UDF_Explicit_Creation_Examples:
210

211
    In all of the examples above, a UDF was automatically created for the functions assigned to the Mechanism.  A UDF
212
    can also be created explicitly, as follows:
213

214
        >>> my_sinusoidal_UDF = pnl.UserDefinedFunction(custom_function=my_sinusoidal_fct)
215
        >>> my_wave_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
216
        ...                                        function=my_sinusoidal_UDF)
217

218
    When the UDF is created explicitly, parameters of the function can be included as arguments to its constructor,
219
    to assign them default values that differ from the those in the definition of the function, or for parameters
220
    that don't define default values.  For example::
221

222
        >>> my_sinusoidal_UDF = pnl.UserDefinedFunction(custom_function=my_sinusoidal_fct,
223
        ...                                  phase=10,
224
        ...                                  amplitude=3)
225
        >>> my_wave_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
226
        ...                                        function=my_sinusoidal_UDF)
227

228
    assigns ``my_sinusoidal_fct`` as the `function <Mechanism_Base.function>` for ``my_mech``, but with the default
229
    values of its ``phase`` and ``amplitude`` parameters assigned new values.  This can be useful for assigning the
230
    same function to different Mechanisms with different default values.
231

232
    .. _UDF_Control_Signal_Example:
233

234
    Explicitly defining the UDF can also be used to `specify control <ControlSignal_Specification>` for parameters of
235
    the function, as in the following example::
236

237
        >>> my_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
238
        ...                                   function=UserDefinedFunction(custom_function=my_sinusoidal_fct,
239
        ...                                                                amplitude=(1.0, pnl.CONTROL)))
240

241
    This specifies that the default value of the ``amplitude`` parameter of ``my_sinusoidal_fct`` be ``1.0``, but
242
    its value should be modulated by a `ControlSignal`.
243

244
    COMMENT:
245
    Note:  if a function explicitly defined in a UDF does not assign a default value to its first argument (i.e.,
246
    it is a positional argument), then the UDF that must define the variable, as in:
247

248
    Note:  if the function does not assign a default value to its first argument i.e., it is a positional arg),
249
    then if it is explicitly wrapped in a UDF that must define the variable, as in:
250
        xxx my_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
251
        ...                                   function=UserDefinedFunction(default_variable=[[0],[0]],
252
        ...                                                                custom_function=my_sinusoidal_fct,
253
        ...                                                                amplitude=(1.0, pnl.CONTROL)))
254

255
    This is required so that the format of the variable can be checked for compatibilty with other Components
256
    with which it interacts.
257

258
    .. note::
259
       Built-in Python functions and methods (including numpy functions) cannot be assigned to a UDF
260

261
    COMMENT
262

263
    Custom functions can be as elaborate as desired, and can even include other PsyNeuLink `Functions <Function>`
264
    indirectly, such as::
265

266
        >>> import psyneulink as pnl
267
        >>> L = pnl.Logistic(gain = 2)
268
        >>> def my_fct(variable):
269
        ...     return L(variable) + 2
270
        >>> my_mech = pnl.ProcessingMechanism(input_shapes = 3, function = my_fct)
271
        >>> my_mech.execute(input = [1, 2, 3])  #doctest: +SKIP
272
        array([[2.88079708, 2.98201379, 2.99752738]])
273

274

275
    .. _UDF_Assign_to_Port_Examples:
276

277
    **Assigning of a custom function to a Port**
278

279
    A custom function can also be assigned as the `function <Port_Base.function>` of an `InputPort` or `OutputPort`.
280
    For example, the following assigns ``my_sinusoidal_fct`` to the `function <OutputPort.function>` of an OutputPort
281
    of ``my_mech``, rather the Mechanism's `function <Mechanism_Base.function>`::
282

283
        >>> my_wave_mech = pnl.ProcessingMechanism(input_shapes=1,
284
        ...                                        function=pnl.Linear,
285
        ...                                        output_ports=[{pnl.NAME: 'SINUSOIDAL OUTPUT',
286
        ...                                                       pnl.VARIABLE: [(pnl.OWNER_VALUE, 0),pnl.EXECUTION_COUNT],
287
        ...                                                       pnl.FUNCTION: my_sinusoidal_fct}])
288

289
    For details on how to specify a function of an OutputPort, see `OutputPort Customization <OutputPort_Customization>`.
290
    Below is an example plot of the output of the 'SINUSOIDAL OUTPUT' `OutputPort` from my_wave_mech above, as the
291
    execution count increments, when the input to the mechanism is 0.005 for 1000 runs::
292

293
.. figure:: _static/sinusoid_005.png
294
   :alt: Sinusoid function
295
   :scale: 50 %
296

297
.. _UDF_Modulatory_Params_Examples:
298

299
    The parameters of a custom function assigned to an InputPort or OutputPort can also be used for `gating
300
    <GatingMechanism_Specifying_Gating>`.  However, this requires that its `Function_Modulatory_Params` be specified
301
    (see `above <UDF_Modulatory_Params>`). This can be done by including a **params** argument in the definition of
302
    the function itself::
303

304
        >>> def my_sinusoidal_fct(input=[[0],[0]],
305
        ...                      phase=0,
306
        ...                      amplitude=1,
307
        ...                      params={pnl.ADDITIVE_PARAM:'phase',
308
        ...                              pnl.MULTIPLICATIVE_PARAM:'amplitude'}):
309
        ...    frequency = input[0]
310
        ...    t = input[1]
311
        ...    return amplitude * np.sin(2 * np.pi * frequency * t + phase)
312

313
    or in the explicit creation of a UDF::
314

315
        >>> my_sinusoidal_UDF = pnl.UserDefinedFunction(custom_function=my_sinusoidal_fct,
316
        ...                                             phase=0,
317
        ...                                             amplitude=1,
318
        ...                                             params={pnl.ADDITIVE_PARAM:'phase',
319
        ...                                                     pnl.MULTIPLICATIVE_PARAM:'amplitude'})
320

321

322
    The ``phase`` and ``amplitude`` parameters of ``my_sinusoidal_fct`` can now be used as the
323
    `Function_Modulatory_Params` for gating any InputPort or OutputPort to which the function is assigned (see
324
    `GatingMechanism_Specifying_Gating` and `GatingSignal_Examples`).
325

326
.. _UDF_Compilation:
327

328
    **Compiling a User Defined Function**
329

330
    User defined functions may also be `automatically compiled <Composition_Compilation>`, by adding them as a mechanism or projection function.
331
    There are several restrictions to take into account:
332

333
.. _UDF_Compilation_Restrictions:
334

335
    * *Lambda Functions* -- User defined functions currently do not support Python Lambda functions
336

337
    * *Loops* -- User defined functions currently do not support Loops
338

339
    * *Python Data Types* -- User defined functions currently do not support *dict* and *class* types
340

341
    * *Nested Functions* -- User defined functions currently do not support recursive calls, nested functions, or closures
342

343
    * *Slicing and comprehensions* -- User defined functions currently have limited support for slicing, and do not support comprehensions
344

345
    * *Libraries* -- User defined functions currently do not support libraries, aside from **NumPy** (with limited support)
346

347
.. _UDF_Compilation_Numpy:
348

349
    **NumPy Support for Compiled User Defined Functions**
350

351
    Compiled User Defined Functions also provide access to limited compiled NumPy functionality; The supported state_features are listed as follows:
352

353
    * *Data Types* -- Numpy Arrays and Matrices are supported, as long as their dimensionality is less than 3. In addition, the elementwise multiplication and addition of NumPy arrays and matrices is fully supported
354

355
    * *Numerical functions* -- the `exp` and `tanh` methods are currently supported in compiled mode
356

357
    It is highly recommended that users who require additional functionality request it as an issue `here <https://github.com/PrincetonUniversity/PsyNeuLink/issues>`_.
358

359
    **Class Definition:**
360

361

362
    Arguments
363
    ---------
364

365
    COMMENT:
366
        CW 1/26/18: Again, commented here is the old version, because I'm afraid I may have missed some functionality.
367
        custom_function : function
368
        specifies function to "wrap." It can be any function, take any arguments (including standard ones,
369
        such as :keyword:`params` and :keyword:`context`) and return any value(s), so long as these are consistent
370
        with the context in which the UserDefinedFunction will be used.
371
    COMMENT
372
    custom_function : function
373
        specifies the function to "wrap." It can be any function or method, including a lambda function;
374
        see `above <UDF_Description>` for additional details.
375

376
    params : Dict[param keyword: param value] : default None
377
        a `parameter dictionary <ParameterPort_Specification>` that specifies the parameters for the function.
378
        This can be used to define an `additive_param <UserDefinedFunction.additive_param>` and/or
379
        `multiplicative_param <UserDefinedFunction.multiplicative_param>` for the UDF, by including one or both
380
        of the following entries:\n
381
          *ADDITIVE_PARAM*: <param_name>\n
382
          *MULTIPLICATIVE_PARAM*: <param_name>\n
383
        Values specified for parameters in the dictionary override any assigned to those parameters in arguments of
384
        the constructor.
385

386
    owner : Component
387
        `component <Component>` to which to assign the Function.
388

389
    name : str : default see `name <Function.name>`
390
        specifies the name of the Function.
391

392
    prefs : PreferenceSet or specification dict : default Function.classPreferences
393
        specifies the `PreferenceSet` for the Function (see `prefs <Function_Base.prefs>` for details).
394

395
    Attributes
396
    ----------
397

398
    variable: value
399
        format and default value of the function "wrapped" by the UDF.
400

401
    custom_function : function
402
        the user-specified function: called by the Function's `owner <Function_Base.owner>` when it is executed.
403

404
    additive_param : str
405
        this contains the name of the additive_param, if one has been specified for the UDF
406
        (see `above <UDF_Modulatory_Params>` for details).
407

408
    multiplicative_param : str
409
        this contains the name of the multiplicative_param, if one has been specified for the UDF
410
        (see `above <UDF_Modulatory_Params>` for details).
411

412
    COMMENT:
413
    enable_output_type_conversion : Bool : False
414
        specifies whether `function output type conversion <Function_Output_Type_Conversion>` is enabled.
415

416
    output_type : FunctionOutputType : None
417
        used to specify the return type for the `function <Function_Base.function>`;  `functionOuputTypeConversion`
418
        must be enabled and implemented for the class (see `FunctionOutputType <Function_Output_Type_Conversion>`
419
        for details).
420
    COMMENT
421

422
    owner : Component
423
        `component <Component>` to which the Function has been assigned.
424

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

429
    prefs : PreferenceSet or specification dict
430
        the `PreferenceSet` for the Function; if it is not specified in the **prefs** argument of the
431
        constructor, a default is assigned using `classPreferences` defined in __init__.py (see `Preferences`
432
        for details).
433
    """
434

435
    componentName = USER_DEFINED_FUNCTION
1✔
436
    componentType = USER_DEFINED_FUNCTION_TYPE
1✔
437

438
    class Parameters(Function_Base.Parameters):
1✔
439
        """
440
            Attributes
441
            ----------
442

443
                custom_function
444
                    see `custom_function <UserDefinedFunction.custom_function>`
445

446
                    :default value: None
447
                    :type:
448
        """
449
        custom_function = Parameter(
1✔
450
            None,
451
            stateful=False,
452
            loggable=False,
453
            pnl_internal=True,
454
        )
455

456
    @check_user_specified
1✔
457
    @beartype
1✔
458
    def __init__(self,
1✔
459
                 custom_function=None,
460
                 default_variable=None,
461
                 params=None,
462
                 owner=None,
463
                 prefs:  Optional[ValidPrefSet] = None,
464
                 stateful_parameter=None,
465
                 **kwargs):
466

467
        def get_cust_fct_args(custom_function):
1✔
468
            """Get args of custom_function
469
            Return:
470
                - value of first arg (to be used as default_variable for UDF)
471
                - dict with all others (to be assigned as params of UDF)
472
                - dict with default values (from function definition, else set to None)
473
            """
474
            try:
1✔
475
                custom_function_signature = signature(custom_function)
1✔
476
            except ValueError:
1✔
477
                raise FunctionError(
478
                    "Assignment of a function or method ({}) without an "
479
                    "inspect.signature to a {} is not supported".format(
480
                        custom_function, self.__class__.__name__
481
                    )
482
                )
483
            except TypeError:
1✔
484
                v = _ExpressionVisitor()
1✔
485
                v.visit(ast.parse(custom_function))
1✔
486
                parameters = v.vars.union(v.functions)
1✔
487

488
                if 'variable' in parameters:
1!
489
                    parameters.remove('variable')
×
490
                    variable = kwargs['variable']
×
491
                else:
492
                    variable = None
1✔
493

494
                args = {}
1✔
495
                for p in parameters:
1✔
496
                    if '.' not in p:  # assume . indicates external module function call
1!
497
                        try:
1✔
498
                            args[p] = kwargs[p]
1✔
499
                        except KeyError:
×
500
                            args[p] = None
×
501

502
                return variable, args, args
1✔
503

504
            args = {}
1✔
505
            defaults = {}
1✔
506
            for arg_name, arg in custom_function_signature.parameters.items():
1✔
507

508
                # MODIFIED 3/6/19 NEW: [JDC]
509
                # Custom function specified owner as arg
510
                if arg_name in {SELF, OWNER, CONTEXT}:
1✔
511
                    # Flag for inclusion in call to function
512
                    if arg_name == SELF:
1!
513
                        self.self_arg = True
×
514
                    elif arg_name == OWNER:
1!
515
                        self.owner_arg = True
×
516
                    else:
517
                        self.context_arg = True
1✔
518
                    # Skip rest, as these don't need to be params
519
                    continue
1✔
520
                # MODIFIED 3/6/19 END
521

522
                # Use definition from the function as default;
523
                #    this allows UDF to assign a value for this instance (including a MODULATORY spec)
524
                #    while assigning an actual value to current/defaults
525
                if arg.default is _empty:
1✔
526
                    defaults[arg_name] = None
1✔
527

528
                else:
529
                    defaults[arg_name] = arg.default
1✔
530

531
                # If arg is specified in the constructor for the UDF, assign that as its value
532
                if arg_name in kwargs:
1✔
533
                    args[arg_name] = kwargs[arg_name]
1✔
534
                # Otherwise, use the default value from the definition of the function
535
                else:
536
                    args[arg_name] = defaults[arg_name]
1✔
537

538
            # Assign default value of first arg as variable and remove from dict
539
            # .keys is ordered
540
            first_arg_name = list(custom_function_signature.parameters.keys())[0]
1✔
541
            variable = args[first_arg_name]
1✔
542
            if variable is _empty:
1!
543
                variable = None
×
544
            del args[first_arg_name]
1✔
545

546
            return variable, args, defaults
1✔
547

548
        self.self_arg = False
1✔
549
        self.owner_arg = False
1✔
550
        self.context_arg = False
1✔
551

552
        # Get variable and names of other any other args for custom_function and assign to cust_fct_params
553
        if params is not None and CUSTOM_FUNCTION in params:
1!
554
            custom_function = params[CUSTOM_FUNCTION]
×
555

556
        cust_fct_variable, self.cust_fct_params, defaults = get_cust_fct_args(custom_function)
1✔
557

558
        # If params is specified as arg in custom function's definition, move it to params in UDF's constructor
559
        if PARAMS in self.cust_fct_params:
1✔
560
            if self.cust_fct_params[PARAMS]:
1!
561
                if params:
×
562
                    params.update(self.cust_fct_params)
×
563
                else:
564
                    params = self.cust_fct_params[PARAMS]
×
565
            del self.cust_fct_params[PARAMS]
1✔
566

567
        # If context is specified as arg in custom function's definition, delete it
568
        if CONTEXT in self.cust_fct_params:
1!
569
            if self.cust_fct_params[CONTEXT]:
×
570
                context = self.cust_fct_params[CONTEXT]
×
571
            del self.cust_fct_params[CONTEXT]
×
572

573
        if stateful_parameter is not None:
1✔
574
            if stateful_parameter not in self.cust_fct_params:
1✔
575
                raise FunctionError(
576
                    f'{stateful_parameter} specified as integration parameter is not a parameter of {custom_function}'
577
                )
578
        self.stateful_parameter = stateful_parameter
1✔
579

580
        # Assign variable to default_variable if default_variable was not specified
581
        if default_variable is None:
1✔
582
            default_variable = cust_fct_variable
1✔
583
        elif cust_fct_variable and not iscompatible(default_variable, cust_fct_variable):
1!
584
            owner_name = ' ({})'.format(owner.name) if owner else ''
×
585
            cust_fct_name = repr(custom_function.__name__)
×
586
            raise FunctionError("Value passed as \'default_variable\' for {} {} ({}) conflicts with specification of "
587
                                "first argument in constructor for {} itself ({}). "
588
                                "Try modifying specification of \'default_variable\' "
589
                                "for object to which {} is being assigned{}, and/or insuring that "
590
                                "the first argument of {} is at least a 2d array".
591
                                format(self.__class__.__name__, cust_fct_name, default_variable,
592
                                       cust_fct_name, cust_fct_variable, cust_fct_name, owner_name, cust_fct_name))
593

594
        super().__init__(
1✔
595
            default_variable=default_variable,
596
            custom_function=custom_function,
597
            params=params,
598
            owner=owner,
599
            prefs=prefs,
600
            **self.cust_fct_params
601
        )
602

603
    def _get_allowed_arguments(self):
1✔
604
        return super()._get_allowed_arguments().union(self.cust_fct_params)
1✔
605

606
    def _validate_params(self, request_set, target_set=None, context=None):
1✔
607
        pass
1✔
608

609
    def _initialize_parameters(self, context=None, **param_defaults):
1✔
610
        # pass custom parameter values here so they can be created as
611
        # Parameters in Component._initialize_parameters and
612
        # automatically handled as if they were normal Parameters
613
        for param_name in self.cust_fct_params:
1✔
614
            param_defaults[param_name] = Parameter(self.cust_fct_params[param_name], modulable=True)
1✔
615

616
        super()._initialize_parameters(context, **param_defaults)
1✔
617

618
    def _function(self, variable, context=None, **kwargs):
1✔
619
        call_params = self.cust_fct_params.copy()
1✔
620

621
        # Update value of parms in cust_fct_params
622
        for param in call_params:
1✔
623

624
            # First check for value passed in params as runtime param:
625
            if PARAMS in kwargs and kwargs[PARAMS] is not None and param in kwargs[PARAMS]:
1✔
626
                call_params[param] = kwargs[PARAMS][param]
1✔
627
            elif param in kwargs:
1!
NEW
628
                call_params[param] = kwargs[param]
×
629
            else:
630
                # Otherwise, get current value from ParameterPort (in case it is being modulated by ControlSignal(s)
631
                call_params[param] = self._get_current_parameter_value(param, context)
1✔
632

633
        # # MODIFIED 3/6/19 NEW: [JDC]
634
        # Add any of these that were included in the definition of the custom function:
635
        if self.self_arg:
1!
636
            call_params[SELF] = self
×
637
        if self.owner_arg:
1!
638
            call_params[OWNER] = self.owner
×
639
        if self.context_arg:
1✔
640
            call_params[CONTEXT] = context
1✔
641
        # MODIFIED 3/6/19 END
642

643
        kwargs.update(call_params)
1✔
644

645
        try:
1✔
646
            # Try calling with full list of args (including context and params)
647
            value = self.custom_function(variable, **kwargs)
1✔
648
        except TypeError as e:
1✔
649
            if "'str' object is not callable" != str(e):
1✔
650
                # Try calling with just variable and cust_fct_params
651
                value = self.custom_function(variable, **call_params)
1✔
652
            else:
653
                value = eval(self.custom_function, kwargs)
1✔
654

655
        if self.stateful_parameter is not None and not self.is_initializing:
1✔
656
            # use external set here because we don't control custom_function
657
            getattr(self.parameters, self.stateful_parameter).set(value, context)
1✔
658

659
        return self.convert_output_type(value)
1✔
660

661
    def _gen_llvm_function_body(self, ctx, builder, params, state,
1✔
662
                                arg_in, arg_out, *, tags:frozenset):
663

664
        # Check for global and nonlocal vars. we can't compile those.
665
        closure_vars = getclosurevars(self.custom_function)
1✔
666
        assert len(closure_vars.nonlocals) == 0, "Compiling functions with non-local variables is not supported!"
1✔
667

668
        srcfile = getsourcefile(self.custom_function)
1✔
669
        first_line = getsourcelines(self.custom_function)[1]
1✔
670

671
        with open(srcfile) as f:
1✔
672
            for node in ast.walk(ast.parse(f.read(), srcfile)):
1!
673
                if getattr(node, 'lineno', -1) == first_line and isinstance(node, (ast.FunctionDef, ast.Lambda)):
1✔
674
                    func_ast = node
1✔
675
                    break
1✔
676
                func_ast = None
1✔
677

678
        assert func_ast is not None, "UDF function source code not found"
1✔
679

680
        func_globals = closure_vars.globals
1✔
681
        assert len(func_globals) == 0 or (
1✔
682
               len(func_globals) == 1 and np in func_globals.values()), \
683
               "Compiling functions with global variables is not supported! ({})".format(closure_vars.globals)
684
        func_params = {param_id: ctx.get_param_or_state_ptr(builder, self, param_id, param_struct_ptr=params) for param_id in self.llvm_param_ids}
1✔
685

686
        pnlvm.codegen.UserDefinedFunctionVisitor(ctx, builder, func_globals, func_params, arg_in, arg_out).visit(func_ast)
1✔
687

688
        # The generic '_gen_llvm' will append another ret void to this block
689
        post_block = builder.append_basic_block(name="post_udf")
1✔
690
        builder.position_at_start(post_block)
1✔
691
        return builder
1✔
692

693
    def as_mdf_model(self):
1✔
694
        import math
1✔
695
        import modeci_mdf.functions.standard
1✔
696

697
        model = super().as_mdf_model()
1✔
698
        ext_function_str = None
1✔
699

700
        if self.custom_function in [
1!
701
            func_dict['function']
702
            for name, func_dict
703
            in modeci_mdf.functions.standard.mdf_functions.items()
704
        ]:
705
            ext_function_str = self.custom_function.__name__
1✔
706

707
        if _is_module_class(self.custom_function, math):
1!
708
            ext_function_str = f'{self.custom_function.__module__}.{self.custom_function.__name__}'
×
709

710
        if ext_function_str is not None:
1!
711
            model.metadata['custom_function'] = ext_function_str
1✔
712
            del model.metadata['type']
1✔
713

714
        return model
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