UserDefinedFunction¶
- class psyneulink.core.components.functions.userdefinedfunction.UserDefinedFunction(custom_function=None, default_variable=None, params=None, owner=None, prefs=None, stateful_parameter=None, **kwargs)¶
A UserDefinedFunction (UDF) is used to “wrap” a Python function or method, lamdba function, or an expression written in string format 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 thefunction
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: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 as the
variable
attribute of the UDF’sfunction
. 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. If providing a string expression, variable is optional. However, if variable is not included in the expression, the resulting UDF will not use variable at all in its calculation.
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 Mechanism or Projection, these parameters are each automatically assigned a ParameterPort so that they can be modulated by ControlSignals or LearningSignals, respectively. If the UDF is assigned to (or automatically created for) an InputPort or OutputPort, 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, 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>
ADDITIVE_PARAM: <parameter name>
These are used only when the UDF is assigned as the
function
of an InputPort or OutputPort that receives one more more GatingProjections.
Tip
The format of the
variable
passed to thecustom_function
function can be verified by adding aprint(variable)
orprint(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
ofmy_mech
is defined in the input_shapes argument of its constructor as having a single item (a 1d array of length 3; (seeinput_shapes
). 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 ofvariable
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 usevariable
, as thevariable
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 itsinput
argument, that it assigns to thefrequency
andt
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 thevariable
of all Components are converted to a 2d array).my_sinusoidal_fct
also has two other arguments,phase
andamplitude
. When it is assigned tomy_wave_mech
, those parameters are assigned to ParameterPorts ofmy_wave_mech
, which that be used to modify their values by ControlSignals (seeexample below
).The function argument may also be an expression written as a string:
>>> my_mech = pnl.ProcessingMechanism(function='sum(variable, 2)') >>> my_mech.execute(input=[1]) array([[3]])
This option is primarily designed for compatibility with other packages that use string expressions as their main description of computation and may be less flexible or reliable than the previous styles.
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 thefunction
formy_mech
, but with the default values of itsphase
andamplitude
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 ofmy_sinusoidal_fct
be1.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(input_shapes = 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 Port
A custom function can also be assigned as the
function
of an InputPort or OutputPort. For example, the following assignsmy_sinusoidal_fct
to thefunction
of an OutputPort ofmy_mech
, rather the Mechanism’sfunction
:>>> my_wave_mech = pnl.ProcessingMechanism(input_shapes=1, ... function=pnl.Linear, ... output_ports=[{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 OutputPort, see OutputPort Customization. Below is an example plot of the output of the ‘SINUSOIDAL OUTPUT’ OutputPort 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 InputPort or OutputPort 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, ... params={pnl.ADDITIVE_PARAM:'phase', ... 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, ... params={pnl.ADDITIVE_PARAM:'phase', ... pnl.MULTIPLICATIVE_PARAM:'amplitude'})
The
phase
andamplitude
parameters ofmy_sinusoidal_fct
can now be used as the Modulatory Parameters for gating any InputPort or OutputPort to which the function is assigned (see Specifying gating and Examples).Compiling a User Defined Function
User defined functions may also be automatically compiled, by adding them as a mechanism or projection function. There are several restrictions to take into account:
Lambda Functions – User defined functions currently do not support Python Lambda functions
Loops – User defined functions currently do not support Loops
Python Data Types – User defined functions currently do not support dict and class types
Nested Functions – User defined functions currently do not support recursive calls, nested functions, or closures
Slicing and comprehensions – User defined functions currently have limited support for slicing, and do not support comprehensions
Libraries – User defined functions currently do not support libraries, aside from NumPy (with limited support)
NumPy Support for Compiled User Defined Functions
Compiled User Defined Functions also provide access to limited compiled NumPy functionality; The supported state_features are listed as follows:
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
Numerical functions – the
exp
andtanh
methods are currently supported in compiled mode
It is highly recommended that users who require additional functionality request it as an issue here.
Class Definition:
- custom_functionfunction
specifies the function to “wrap.” It can be any function or method, including a lambda function; see above for additional details.
- paramsDict[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/ormultiplicative_param
for the UDF, by including one or both of the following entries:ADDITIVE_PARAM: <param_name>
MULTIPLICATIVE_PARAM: <param_name>
Values specified for parameters in the dictionary override any assigned to those parameters in arguments of the constructor.
- ownerComponent
component to which to assign the Function.
- namestrdefault see
name
specifies the name of the Function.
- prefsPreferenceSet or specification dictdefault Function.classPreferences
specifies the
PreferenceSet
for the Function (seeprefs
for details).
- variable: value
format and default value of the function “wrapped” by the UDF.
- custom_functionfunction
the user-specified function: called by the Function’s
owner
when it is executed.- additive_paramstr
this contains the name of the additive_param, if one has been specified for the UDF (see above for details).
- multiplicative_paramstr
this contains the name of the multiplicative_param, if one has been specified for the UDF (see above for details).
- ownerComponent
component to which the Function has been assigned.
- namestr
the name of the Function; if it is not specified in the name argument of the constructor, a default is assigned by FunctionRegistry (see Naming for conventions used for default and duplicate names).
- prefsPreferenceSet or specification dict
the
PreferenceSet
for the Function; if it is not specified in the prefs argument of the constructor, a default is assigned usingclassPreferences
defined in __init__.py (see Preferences for details).
- _get_allowed_arguments()¶
Returns a set of argument names that can be passed into __init__, directly or through params dictionaries
- Includes:
all Parameter constructor_argument names
all Parameter names except for those that have a constructor_argument
all ParameterAlias names
all other explicitly specified named arguments in __init__
- _validate_params(request_set, target_set=None, context=None)¶
Validate params and assign validated values to targets,
This performs top-level type validation of params
This can be overridden by a subclass to perform more detailed checking (e.g., range, recursive, etc.) It is called only if the parameter_validation attribute is
True
(which it is by default)- IMPLEMENTATION NOTES:
future versions should add recursive and content (e.g., range) checking
should method return validated param set?
- Parameters
validated (dict (target_set) - repository of params that have been) –
validated –
- Return none
- _initialize_parameters(context=None, **param_defaults)¶
- Parameters
**param_defaults – maps Parameter names to their default
any (values. Sets instance-level Parameters dynamically for) –
object. (name that maps to a Parameter) –