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

PrincetonUniversity / PsyNeuLink / 18325271242

07 Oct 2025 08:25PM UTC coverage: 82.142% (-2.9%) from 85.084%
18325271242

push

github

davidt0x
Revert contraints on onnxruntime

9749 of 13096 branches covered (74.44%)

Branch coverage included in aggregate %.

33998 of 40162 relevant lines covered (84.65%)

0.85 hits per line

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

77.69
/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 _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 (
1✔
27
    _get_cached_function_signature,
28
    _is_module_class,
29
    iscompatible,
30
)
31

32
from psyneulink.core import llvm as pnlvm
1✔
33

34
__all__ = ['UserDefinedFunction']
1✔
35

36

37
class _ExpressionVisitor(ast.NodeVisitor):
1✔
38
    def __init__(self, *args, **kwargs):
1✔
39
        super().__init__(*args, **kwargs)
1✔
40
        self.vars = set()
1✔
41
        self.functions = set()
1✔
42

43
    def visit_Name(self, node):
1✔
44
        if node.id not in dir(builtins):
1✔
45
            self.vars.add(node.id)
1✔
46

47
    def visit_Call(self, node):
1✔
48
        try:
1✔
49
            # gives top level module name if module function used
50
            func_id = node.func.value.id
1✔
51
        except AttributeError:
1✔
52
            func_id = node.func.id
1✔
53

54
        if func_id not in dir(builtins):
1✔
55
            self.functions.add(func_id)
1✔
56

57
        for c in ast.iter_child_nodes(node):
1✔
58
            self.visit(c)
1✔
59

60

61
class UserDefinedFunction(Function_Base):
1✔
62
    """UserDefinedFunction(  \
63
    custom_function=None,    \
64
    default_variable=None,   \
65
    params=None,             \
66
    owner=None,              \
67
    name=None,               \
68
    prefs=None)
69

70
    .. _UDF_Description:
71

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

81
    .. _UDF_Variable:
82

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

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

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

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

125
      COMMENT:
126
      # IMPLEMENT INTERFACE FOR OTHER MODULATION TYPES (i.e., for ability to add new custom ones)
127
      COMMENT
128

129
    .. tip::
130
       The format of the `variable <UserDefinedFunction.variable>` passed to the `custom_function
131
       <UserDefinedFunction.custom_function>` function can be verified by adding a ``print(variable)`` or
132
       ``print(type(variable))`` statement to the function.
133

134
    Examples
135
    --------
136

137
    **Assigning a custom function to a Mechanism**
138

139
    .. _UDF_Lambda_Function_Examples:
140

141
    The following example assigns a simple lambda function that returns the sum of the elements of a 1d array) to a
142
    `TransferMechanism`::
143

144
        >>> import psyneulink as pnl
145
        >>> my_mech = pnl.ProcessingMechanism(default_variable=[[0,0,0]],
146
        ...                                   function=lambda x:sum(x[0]))
147
        >>> my_mech.execute(input = [1, 2, 3])
148
        array([[6]])
149

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

156
        >>> my_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
157
        ...                                   function=lambda x: x[0] + x[1])
158
        >>> my_mech.execute(input = [[1],[2]])
159
        array([[3]])
160

161
    .. _UDF_Defined_Function_Examples:
162

163
    The **function** argument can also be assigned a function defined in Python::
164

165
        >>> def my_fct(variable):
166
        ...     return variable[0] + variable[1]
167
        >>> my_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
168
        ...                                   function=my_fct)
169

170
    This will produce the same result as the last example.  This can be useful for assigning the function to more than
171
    one Component.
172

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

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

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

190
    Notice also that ``my_sinusoidal_fct`` takes two values in its ``input`` argument, that it assigns to the
191
    ``frequency`` and ``t`` variables of the function.  While  it could have been specified more compactly as a 1d array
192
    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
193
    it matches the format of the **default_variable** for the ProcessingMechanism to which it will be assigned,
194
    which requires it be formatted this way (since the `variable <Component.variable>` of all Components are converted
195
    to a 2d array).
196

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

202
    .. _UDF_String_Expression_Function_Examples:
203

204
    The **function** argument may also be an expression written as a string::
205

206
        >>> my_mech = pnl.ProcessingMechanism(function='sum(variable, 2)')
207
        >>> my_mech.execute(input=[1])
208
        array([[3]])
209

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

213
    .. _UDF_Explicit_Creation_Examples:
214

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

218
        >>> my_sinusoidal_UDF = pnl.UserDefinedFunction(custom_function=my_sinusoidal_fct)
219
        >>> my_wave_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
220
        ...                                        function=my_sinusoidal_UDF)
221

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

226
        >>> my_sinusoidal_UDF = pnl.UserDefinedFunction(custom_function=my_sinusoidal_fct,
227
        ...                                  phase=10,
228
        ...                                  amplitude=3)
229
        >>> my_wave_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
230
        ...                                        function=my_sinusoidal_UDF)
231

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

236
    .. _UDF_Control_Signal_Example:
237

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

241
        >>> my_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
242
        ...                                   function=UserDefinedFunction(custom_function=my_sinusoidal_fct,
243
        ...                                                                amplitude=(1.0, pnl.CONTROL)))
244

245
    This specifies that the default value of the ``amplitude`` parameter of ``my_sinusoidal_fct`` be ``1.0``, but
246
    its value should be modulated by a `ControlSignal`.
247

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

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

259
    This is required so that the format of the variable can be checked for compatibilty with other Components
260
    with which it interacts.
261

262
    .. note::
263
       Built-in Python functions and methods (including numpy functions) cannot be assigned to a UDF
264

265
    COMMENT
266

267
    Custom functions can be as elaborate as desired, and can even include other PsyNeuLink `Functions <Function>`
268
    indirectly, such as::
269

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

278

279
    .. _UDF_Assign_to_Port_Examples:
280

281
    **Assigning of a custom function to a Port**
282

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

287
        >>> my_wave_mech = pnl.ProcessingMechanism(input_shapes=1,
288
        ...                                        function=pnl.Linear,
289
        ...                                        output_ports=[{pnl.NAME: 'SINUSOIDAL OUTPUT',
290
        ...                                                       pnl.VARIABLE: [(pnl.OWNER_VALUE, 0),pnl.EXECUTION_COUNT],
291
        ...                                                       pnl.FUNCTION: my_sinusoidal_fct}])
292

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

297
.. figure:: _static/sinusoid_005.png
298
   :alt: Sinusoid function
299
   :scale: 50 %
300

301
.. _UDF_Modulatory_Params_Examples:
302

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

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

317
    or in the explicit creation of a UDF::
318

319
        >>> my_sinusoidal_UDF = pnl.UserDefinedFunction(custom_function=my_sinusoidal_fct,
320
        ...                                             phase=0,
321
        ...                                             amplitude=1,
322
        ...                                             params={pnl.ADDITIVE_PARAM:'phase',
323
        ...                                                     pnl.MULTIPLICATIVE_PARAM:'amplitude'})
324

325

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

330
.. _UDF_Compilation:
331

332
    **Compiling a User Defined Function**
333

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

337
.. _UDF_Compilation_Restrictions:
338

339
    * *Lambda Functions* -- User defined functions currently do not support Python Lambda functions
340

341
    * *Loops* -- User defined functions currently do not support Loops
342

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

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

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

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

351
.. _UDF_Compilation_Numpy:
352

353
    **NumPy Support for Compiled User Defined Functions**
354

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

357
    * *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
358

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

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

363
    **Class Definition:**
364

365

366
    Arguments
367
    ---------
368

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

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

390
    owner : Component
391
        `component <Component>` to which to assign the Function.
392

393
    name : str : default see `name <Function.name>`
394
        specifies the name of the Function.
395

396
    prefs : PreferenceSet or specification dict : default Function.classPreferences
397
        specifies the `PreferenceSet` for the Function (see `prefs <Function_Base.prefs>` for details).
398

399
    Attributes
400
    ----------
401

402
    variable: value
403
        format and default value of the function "wrapped" by the UDF.
404

405
    custom_function : function
406
        the user-specified function: called by the Function's `owner <Function_Base.owner>` when it is executed.
407

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

412
    multiplicative_param : str
413
        this contains the name of the multiplicative_param, if one has been specified for the UDF
414
        (see `above <UDF_Modulatory_Params>` for details).
415

416
    COMMENT:
417
    enable_output_type_conversion : Bool : False
418
        specifies whether `function output type conversion <Function_Output_Type_Conversion>` is enabled.
419

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

426
    owner : Component
427
        `component <Component>` to which the Function has been assigned.
428

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

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

439
    componentName = USER_DEFINED_FUNCTION
1✔
440
    componentType = USER_DEFINED_FUNCTION_TYPE
1✔
441

442
    class Parameters(Function_Base.Parameters):
1✔
443
        """
444
            Attributes
445
            ----------
446

447
                custom_function
448
                    see `custom_function <UserDefinedFunction.custom_function>`
449

450
                    :default value: None
451
                    :type:
452
        """
453
        custom_function = Parameter(
1✔
454
            None,
455
            stateful=False,
456
            loggable=False,
457
            pnl_internal=True,
458
        )
459

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

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

494
                if 'variable' in parameters:
1!
495
                    parameters.remove('variable')
×
496
                    variable = kwargs['variable']
×
497
                else:
498
                    variable = None
1✔
499

500
                args = {}
1✔
501
                for p in parameters:
1✔
502
                    if '.' not in p:  # assume . indicates external module function call
1!
503
                        try:
1✔
504
                            args[p] = kwargs[p]
1✔
505
                        except KeyError:
×
506
                            args[p] = None
×
507

508
                return variable, args, args
1✔
509

510
            args = {}
1✔
511
            defaults = {}
1✔
512
            for arg_name, arg in custom_function_signature.parameters.items():
1✔
513

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

528
                # Use definition from the function as default;
529
                #    this allows UDF to assign a value for this instance (including a MODULATORY spec)
530
                #    while assigning an actual value to current/defaults
531
                if arg.default is _empty:
1✔
532
                    defaults[arg_name] = None
1✔
533

534
                else:
535
                    defaults[arg_name] = arg.default
1✔
536

537
                # If arg is specified in the constructor for the UDF, assign that as its value
538
                if arg_name in kwargs:
1✔
539
                    args[arg_name] = kwargs[arg_name]
1✔
540
                # Otherwise, use the default value from the definition of the function
541
                else:
542
                    args[arg_name] = defaults[arg_name]
1✔
543

544
            # Assign default value of first arg as variable and remove from dict
545
            # .keys is ordered
546
            first_arg_name = list(custom_function_signature.parameters.keys())[0]
1✔
547
            variable = args[first_arg_name]
1✔
548
            if variable is _empty:
1!
549
                variable = None
×
550
            del args[first_arg_name]
1✔
551

552
            return variable, args, defaults
1✔
553

554
        self.self_arg = False
1✔
555
        self.owner_arg = False
1✔
556
        self.context_arg = False
1✔
557

558
        # Get variable and names of other any other args for custom_function and assign to cust_fct_params
559
        if params is not None and CUSTOM_FUNCTION in params:
1!
560
            custom_function = params[CUSTOM_FUNCTION]
×
561

562
        if custom_function is None:
1✔
563
            raise FunctionError('custom_function cannot be None')
564

565
        cust_fct_variable, self.cust_fct_params, defaults = get_cust_fct_args(custom_function)
1✔
566

567
        # If params is specified as arg in custom function's definition, move it to params in UDF's constructor
568
        if PARAMS in self.cust_fct_params:
1✔
569
            if self.cust_fct_params[PARAMS]:
1!
570
                if params:
×
571
                    params.update(self.cust_fct_params)
×
572
                else:
573
                    params = self.cust_fct_params[PARAMS]
×
574
            del self.cust_fct_params[PARAMS]
1✔
575

576
        # If context is specified as arg in custom function's definition, delete it
577
        if CONTEXT in self.cust_fct_params:
1!
578
            if self.cust_fct_params[CONTEXT]:
×
579
                context = self.cust_fct_params[CONTEXT]
×
580
            del self.cust_fct_params[CONTEXT]
×
581

582
        if stateful_parameter is not None:
1✔
583
            if stateful_parameter not in self.cust_fct_params:
1✔
584
                raise FunctionError(
585
                    f'{stateful_parameter} specified as integration parameter is not a parameter of {custom_function}'
586
                )
587
        self.stateful_parameter = stateful_parameter
1✔
588

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

603
        super().__init__(
1✔
604
            default_variable=default_variable,
605
            custom_function=custom_function,
606
            params=params,
607
            owner=owner,
608
            prefs=prefs,
609
            **self.cust_fct_params
610
        )
611

612
    def _get_allowed_arguments(self):
1✔
613
        return super()._get_allowed_arguments().union(self.cust_fct_params)
1✔
614

615
    def _validate_params(self, request_set, target_set=None, context=None):
1✔
616
        pass
1✔
617

618
    def _initialize_parameters(self, context=None, **param_defaults):
1✔
619
        # pass custom parameter values here so they can be created as
620
        # Parameters in Component._initialize_parameters and
621
        # automatically handled as if they were normal Parameters
622
        for param_name in self.cust_fct_params:
1✔
623
            param_defaults[param_name] = Parameter(self.cust_fct_params[param_name], modulable=True)
1✔
624

625
        super()._initialize_parameters(context, **param_defaults)
1✔
626

627
    def _function(self, variable, context=None, **kwargs):
1✔
628
        call_params = self.cust_fct_params.copy()
1✔
629

630
        # Update value of parms in cust_fct_params
631
        for param in call_params:
1✔
632

633
            # First check for value passed in params as runtime param:
634
            if PARAMS in kwargs and kwargs[PARAMS] is not None and param in kwargs[PARAMS]:
1✔
635
                call_params[param] = kwargs[PARAMS][param]
1✔
636
            elif param in kwargs:
1!
637
                call_params[param] = kwargs[param]
×
638
            else:
639
                # Otherwise, get current value from ParameterPort (in case it is being modulated by ControlSignal(s)
640
                call_params[param] = self._get_current_parameter_value(param, context)
1✔
641

642
        # # MODIFIED 3/6/19 NEW: [JDC]
643
        # Add any of these that were included in the definition of the custom function:
644
        if self.self_arg:
1!
645
            call_params[SELF] = self
×
646
        if self.owner_arg:
1!
647
            call_params[OWNER] = self.owner
×
648
        if self.context_arg:
1✔
649
            call_params[CONTEXT] = context
1✔
650
        # MODIFIED 3/6/19 END
651

652
        kwargs.update(call_params)
1✔
653

654
        try:
1✔
655
            # Try calling with full list of args (including context and params)
656
            value = self.custom_function(variable, **kwargs)
1✔
657
        except TypeError as e:
1✔
658
            if "'str' object is not callable" != str(e):
1✔
659
                # Try calling with just variable and cust_fct_params
660
                value = self.custom_function(variable, **call_params)
1✔
661
            else:
662
                value = eval(self.custom_function, kwargs)
1✔
663

664
        if self.stateful_parameter is not None and not self.is_initializing:
1✔
665
            # use external set here because we don't control custom_function
666
            getattr(self.parameters, self.stateful_parameter).set(value, context)
1✔
667

668
        return self.convert_output_type(value)
1✔
669

670
    def _gen_llvm_function_body(self, ctx, builder, params, state,
1✔
671
                                arg_in, arg_out, *, tags:frozenset):
672

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

677
        srcfile = getsourcefile(self.custom_function)
1✔
678
        first_line = getsourcelines(self.custom_function)[1]
1✔
679

680
        with open(srcfile) as f:
1✔
681
            for node in ast.walk(ast.parse(f.read(), srcfile)):
1!
682
                if getattr(node, 'lineno', -1) == first_line and isinstance(node, (ast.FunctionDef, ast.Lambda)):
1✔
683
                    func_ast = node
1✔
684
                    break
1✔
685
                func_ast = None
1✔
686

687
        assert func_ast is not None, "UDF function source code not found"
1✔
688

689
        func_globals = closure_vars.globals
1✔
690
        assert len(func_globals) == 0 or (
1✔
691
               len(func_globals) == 1 and np in func_globals.values()), \
692
               "Compiling functions with global variables is not supported! ({})".format(closure_vars.globals)
693
        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✔
694

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

697
        # The generic '_gen_llvm' will append another ret void to this block
698
        post_block = builder.append_basic_block(name="post_udf")
1✔
699
        builder.position_at_start(post_block)
1✔
700
        return builder
1✔
701

702
    def as_mdf_model(self):
1✔
703
        import math
×
704
        import modeci_mdf.functions.standard
×
705

706
        model = super().as_mdf_model()
×
707
        ext_function_str = None
×
708

709
        if self.custom_function in [
×
710
            func_dict['function']
711
            for name, func_dict
712
            in modeci_mdf.functions.standard.mdf_functions.items()
713
        ]:
714
            ext_function_str = self.custom_function.__name__
×
715

716
        if _is_module_class(self.custom_function, math):
×
717
            ext_function_str = f'{self.custom_function.__module__}.{self.custom_function.__name__}'
×
718

719
        if ext_function_str is not None:
×
720
            model.metadata['custom_function'] = ext_function_str
×
721
            del model.metadata['type']
×
722

723
        return model
×
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