UserDefinedFunction¶

class psyneulink.core.components.functions.userdefinedfunction.UserDefinedFunction(custom_function=None, default_variable=None, params=None, owner=None, name=None, prefs=None)

A UserDefinedFunction (UDF) is used to “wrap” a Python function or method, including a lamdba function, as a PsyNeuLink Function, so that it can be used as the function of a Component. This is done automatically if a Python function or method is assigned as the function attribute of a Component. A Python function or method can also be wrapped explicitly, using the UserDefinedFunction constructor, and assigning the Python function or method to its custom_function argument. A Python function or method wrapped as a UDF must obey the following conventions to be treated correctly:

• It must have at least one argument (that can be a positional or a keyword argument); this will be treated as the variable attribute of the UDF’s function. When the UDF calls the function or method that it wraps, an initial attempt is made to do so with variable as the name of the first argument; if that fails, it is called positionally. The argument is always passed as a 2d np.array, that may contain one or more items (elements in axis 0), depending upon the Component to which the UDF is assigned. It is the user’s responsibility to insure that the number of items expected in the first argument of the function or method is compatible with the circumstances in which it will be called.
• It may have have any number of additional arguments (positional and/or keyword); these are treated as parameters of the UDF, and can be modulated by ModulatorySignals like the parameters of ordinary PsyNeuLink Functions. If the UDF is assigned to (or automatically created for) a Mechanisms or Projection, these parameters are each automatically assigned a ParameterState so that they can be modulated by ControlSignals or LearningSignals, respectively. If the UDF is assigned to (or automatically created for) an InputState or OutputState, and any of the parameters are specified as Modulatory Parameters (see below), then they can be modulated by GatingSignals. The function or method wrapped by the UDF is called with these parameters by their name and with their current values (i.e., as determined by any ModulatorySignals assigned to them).
• It may include self, owner, execution_id, context and/or params arguments; none of these are required, but can be included to gain access to the standard Function parameters (such as the history of its parameter values), those of the Component to which it has been assigned (i.e., its owner, and/or receive information about the current conditions of execution. When the function or method is called, an initial attempt is made to pass these arguments; if that fails, it is called again without them.
• The parameters of a UDF can be specified as Modulatory Parameters in a parameter specification dictionary assigned to the params argument of the constructor for either the Python function or method, or of an explicitly defined UDF (see examples below). It can include either or both of the following two entries:

MULTIPLICATIVE_PARAM: <parameter name>

These are used only when the UDF is assigned as the function of an InputState or OutputState that receives one more more GatingProjections.

Tip

The format of the variable passed to the custom_function function can be verified by adding a print(variable) or print(type(variable)) statement to the function.

Assigning a custom function to a Mechanism

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

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


Note that the function treats its argument, x, as a 2d array, and accesses its first item for the calculation. This is because the variable of my_mech is defined in the size argument of its constructor as having a single item (a 1d array of length 3; (see size). In the following example, a function is defined for a Mechanism in which the variable has two items, that are summed by the function:

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


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

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


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

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

>>> def my_sinusoidal_fct(input=[[0],[0]],
...                       phase=0,
...                       amplitude=1):
...    frequency = input[0]
...    t = input[1]
...    return amplitude * np.sin(2 * np.pi * frequency * t + phase)
>>> my_wave_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
...                                        function=my_sinusoidal_fct)


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

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

my_sinusoidal_fct also has two other arguments, phase and amplitude. When it is assigned to my_wave_mech, those parameters are assigned to ParameterStates of my_wave_mech, which that be used to modify their values by ControlSignals (see example below).

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

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


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

>>> my_sinusoidal_UDF = pnl.UserDefinedFunction(custom_function=my_sinusoidal_fct,
...                                  phase=10,
...                                  amplitude=3)
>>> my_wave_mech = pnl.ProcessingMechanism(default_variable=[[0],[0]],
...                                        function=my_sinusoidal_UDF)


assigns my_sinusoidal_fct as the function for my_mech, but with the default values of its phase and amplitude parameters assigned new values. This can be useful for assigning the same function to different Mechanisms with different default values.

Explicitly defining the UDF can also be used to specify control for parameters of the function, as in the following example:

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


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

Custom functions can be as elaborate as desired, and can even include other PsyNeuLink Functions indirectly, such as:

>>> import psyneulink as pnl
>>> L = pnl.Logistic(gain = 2)
>>> def my_fct(variable):
...     return L(variable) + 2
>>> my_mech = pnl.ProcessingMechanism(size = 3, function = my_fct)
>>> my_mech.execute(input = [1, 2, 3])
array([[2.88079708, 2.98201379, 2.99752738]])


Assigning of a custom function to a State

A custom function can also be assigned as the function of an InputState or OutputState. For example, the following assigns my_sinusoidal_fct to the function of an OutputState of my_mech, rather the Mechanism’s function:

>>> my_wave_mech = pnl.ProcessingMechanism(size=1,
...                                        function=pnl.Linear,
...                                        output_states=[{pnl.NAME: 'SINUSOIDAL OUTPUT',
...                                                       pnl.VARIABLE: [(pnl.OWNER_VALUE, 0),pnl.EXECUTION_COUNT],
...                                                       pnl.FUNCTION: my_sinusoidal_fct}])


For details on how to specify a function of an OutputState, see OutputState Customization. Below is an example plot of the output of the ‘SINUSOIDAL OUTPUT’ OutputState from my_wave_mech above, as the execution count increments, when the input to the mechanism is 0.005 for 1000 runs:

The parameters of a custom function assigned to an InputState or OutputState can also be used for gating. However, this requires that its Modulatory Parameters be specified (see above). This can be done by including a params argument in the definition of the function itself:

>>> def my_sinusoidal_fct(input=[[0],[0]],
...                      phase=0,
...                      amplitude=1,
...                              pnl.MULTIPLICATIVE_PARAM:'amplitude'}):
...    frequency = input[0]
...    t = input[1]
...    return amplitude * np.sin(2 * np.pi * frequency * t + phase)


or in the explicit creation of a UDF:

>>> my_sinusoidal_UDF = pnl.UserDefinedFunction(custom_function=my_sinusoidal_fct,
...                                             phase=0,
...                                             amplitude=1,
...                                                     pnl.MULTIPLICATIVE_PARAM:'amplitude'})


The phase and amplitude parameters of my_sinusoidal_fct can now be used as the Modulatory Parameters for gating any InputState or OutputState to which the function is assigned (see Specifying gating and Examples).

Class Definition:

custom_function : function
specifies the function to “wrap.” It can be any function or method, including a lambda function; see above for additional details.
params : Dict[param keyword: param value] : default None

a parameter dictionary that specifies the parameters for the function. This can be used to define an additive_param and/or multiplicative_param for the UDF, by including one or both of the following entries:

MULTIPLICATIVE_PARAM: <param_name>

Values specified for parameters in the dictionary override any assigned to those parameters in arguments of the constructor.

owner : Component
component to which to assign the Function.
name : str : default see name
specifies the name of the Function.
prefs : PreferenceSet or specification dict : default Function.classPreferences
specifies the PreferenceSet for the Function (see prefs for details).
variable: value
format and default value of the function “wrapped” by the UDF.
custom_function : function
the user-specified function: called by the Function’s owner when it is executed.
the PreferenceSet for the Function; if it is not specified in the prefs argument of the constructor, a default is assigned using classPreferences defined in __init__.py (see PreferenceSet for details).