Skip to content

Commit

Permalink
Merge pull request #1733 from PrincetonUniversity/devel
Browse files Browse the repository at this point in the history
Devel
  • Loading branch information
dillontsmith authored Aug 3, 2020
2 parents 52996d6 + 60dd0e3 commit 2fba3de
Show file tree
Hide file tree
Showing 21 changed files with 457 additions and 534 deletions.
130 changes: 130 additions & 0 deletions docs/source/BasicsAndPrimer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,8 @@ different contexts (often referred to as `"statefulness" <Parameter_Statefulness
previous values, and the ability to be `modulated <ModulatorySignal_Modulation>` by other Components in PsyNeuLink.
These features are supported by methods on the Parameter class, as described below.

.. _BasicsAndPrimer_Accessing_Parameters:

Accessing Parameter Values
^^^^^^^^^^^^^^^^^^^^^^^^^^
The most recently assigned value of a parameter can be accessed like any other attrribute in Python,
Expand Down Expand Up @@ -568,6 +570,134 @@ If included in the script above, then the following would return the ``output``

Notice that this is the value from Trial 1 in the example above.

.. _BasicsAndPrimer_Setting_Parameters:

Setting Parameter Values
^^^^^^^^^^^^^^^^^^^^^^^^
There are two different ways to set parameter values in PsyNeuLink, that correspond to the two ways of accessing
their values discussed in the preceding section:

- "dot notation" -- that is, ``<Component>.<parameter> = some_new_value``
- the `set <Parameter.set>` method of the Parameter class; that is
``<Component>.parameters.<parameter>.set(<value>, <context>)``).

In the case of `non-stateful parameters <Parameter_Statefulness>`, these two approaches behave equivalently, in which
case it is easier and clearer to use dot notation.

.. warning::
In most cases, non-stateful parameters are intended to be configured automatically for a component upon
instantiation by PsyNeuLink. Oftentimes, there are additional steps of validation and setup beyond simply
assigning a value to some attribute of the component instance. Be cautious if you decide to manually set a
non-stateful parameter.

In the case of `stateful parameters <Parameter_Statefulness>` (the most common type), assigning a value using dot
notation and the `set <Parameter.set>` method can behave differently under some circumstances.

Dot notation is simpler, but assumes that the `context <Context>` for which the specified value should apply is the
most recent context in which the Parameter's owner Component has executed. If the value should apply to a different
context, then the `set <Parameter.set>` method must be used, which allows explicit specification of the context.
See below for examples of appropriate use cases for each, as well as explanations of contexts when executing
`Mechanisms <Mechanism_Execution_Composition>` and `Compositions <Composition_Execution_Context>`, respectively.

.. warning::
If a given component has not yet been executed, or has only been executed standalone without a user-provided context,
then dot notation will set the Parameter's value in the baseline context. Analogously, calling the `set
<Parameter.set>` method without providing a context will also set the Parameter's value in the baseline
context. In either of these cases, any new context in which the Component executes after that will use the
newly-provided value as the Parameter's baseline.

Example use-cases for dot notation
**********************************

- Run a Composition, then change the value of a component's parameter, then run the Composition again with the new
parameter value:

>>> # instantiate the mechanism
>>> m = ProcessingMechanism(name='m')
>>> # create a Composition containing the Mechanism
>>> comp1 = Composition(name='comp1', nodes=[m])
>>> comp1.run(inputs={m:1})
>>> # returns: [array([1.])]
>>> # set slope of m1's function to 2 for the most recent context (which is now comp1)
>>> m.function.slope = 2
>>> comp1.run(inputs={m:1})
>>> # returns: [array([2.])]
>>> # note that changing the slope of m's function produced a different result
>>> comp2 = Composition(name='comp2', nodes=[m])
>>> comp2.run(inputs={m:1})
>>> # returns: [array([1.])]
>>> # because slope is a stateful parameter,
>>> # executing the Mechanism in a different context is unaffected by the change

- Cautionary example: using dot notation before a component has been executed in a non-default context will change
its value in the default context, which will in turn propagate to new contexts in which it is executed:

>>> # instantiate the mechanism
>>> m = ProcessingMechanism(name='m')
>>> # create two Compositions, each containing m
>>> comp1 = Composition(name='comp1', nodes=[m])
>>> comp2 = Composition(name='comp2', nodes=[m])
>>> m.execute([1])
>>> # returns: [array([1.])]
>>> # executing m outside of a Composition uses its default context
>>> m.function.slope = 2
>>> m.execute([1])
>>> # returns: [array([2.])]
>>> comp1.run(inputs={m:1})
>>> # returns: [array([2.])]
>>> comp2.run(inputs={m:1})
>>> # returns: [array([2.])]
>>> # the change of the slope of m's function propagated to the new Contexts in which it executes

Example use-cases for Parameter set method
******************************************

- Change the value of a parameter for a context that was not the last context in which the parameter's owner executed:

>>> # instantiate the mechanism
>>> m = ProcessingMechanism(name='m')
>>> # create two Compositions, each containing m
>>> comp1 = Composition(name='comp1', nodes=[m])
>>> comp2 = Composition(name='comp2', nodes=[m])
>>> comp1.run({m:[1]})
>>> # returns: [array([1.])]
>>> comp2.run({m:[1]})
>>> # returns: [array([1.])]
>>> # the last context in which m was executed was comp2,
>>> # so using dot notation to change a parameter of m would apply only for the comp2 context;
>>> # changing the parameter value for the comp1 context requires use of the set method
>>> m.function_parameters.slope.set(2, comp1)
>>> comp1.run({m:[1]})
>>> # returns: [array([2.])]
>>> comp2.run({m:[1]})
>>> # returns: [array([1.])]

- Cautionary example: calling the set method of a parameter without a context specified changes its value in the
default context which, in turn, will propagate to new contexts in which it is executed:

>>> # instantiate the mechanism
>>> m = ProcessingMechanism(name='m')
>>> # create two Compositions, each containing m
>>> comp1 = Composition(name='comp1', nodes=[m])
>>> comp2 = Composition(name='comp2', nodes=[m])
>>> comp1.run({m:[1]})
>>> # returns: [array([1.])]
>>> comp2.run({m:[1]})
>>> # returns: [array([1.])]
>>> # calling the set method without specifying a context will change the default value for new contexts
>>> m.function_parameters.slope.set(2)
>>> comp1.run({m: [1]})
>>> # returns: [array([1.])]
>>> # no change because the context had already been set up before the call to set
>>> comp2.run({m:[1]})
>>> # returns: [array([1.])]
>>> # no change because the context had already been set up before the call to set
>>> # set up a new context
>>> comp3 = Composition(name='comp3', nodes=[m])
>>> comp3.run({m: [1]})
>>> # returns: [array([2.])]
>>> # 2 is now the default value for the slope of m's function, so it now produces a different result

Function Parameters
^^^^^^^^^^^^^^^^^^^
The `parameters <Component_Parameters>` attribute of a Component contains a list of all of its parameters. It is
Expand Down
10 changes: 0 additions & 10 deletions docs/source/InterfaceFunctions.rst

This file was deleted.

129 changes: 69 additions & 60 deletions psyneulink/core/components/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@
import base64
import copy
import dill
import functools
import inspect
import logging
import numbers
Expand Down Expand Up @@ -1244,9 +1245,12 @@ def _get_compilation_state(self):
whitelist = {"previous_time", "previous_value", "previous_v",
"previous_w", "random_state", "is_finished_flag",
"num_executions_before_finished", "num_executions",
"execution_count", "value"}
# mechanism functions are handled separately
blacklist = {"function"} if hasattr(self, 'ports') else {"value"}
"execution_count", "value", "input_ports", "output_ports"}
blacklist = { # References to other components
"objective_mechanism", "agent_rep", "projections"}
# Only mechanisms use "value" state
if not hasattr(self, 'ports'):
blacklist.add("value")
def _is_compilation_state(p):
val = p.get() # memoize for this function
return val is not None and p.name not in blacklist and \
Expand All @@ -1265,27 +1269,24 @@ def llvm_state_ids(self):
setattr(self, "_state_ids", ids)
return ids

def _get_state_values(self, context=None):
def _state_values(p):
val = p.get(context)
if isinstance(val, Component):
return val._get_state_values(context)
return [val for i in range(p.history_min_length + 1)]

return tuple(map(_state_values, self._get_compilation_state()))

def _get_state_initializer(self, context):
def _convert(x):
def _convert(p):
x = p.get(context)
if isinstance(x, np.random.RandomState):
# Skip first element of random state (id string)
return x.get_state()[1:]
val = pnlvm._tupleize(x.get_state()[1:])
elif isinstance(x, Time):
return (getattr(x, Time._time_scale_attr_map[t]) for t in TimeScale)
try:
return (_convert(i) for i in x)
except TypeError:
return x
return pnlvm._tupleize(_convert(self._get_state_values(context)))
val = tuple(getattr(x, Time._time_scale_attr_map[t]) for t in TimeScale)
elif isinstance(x, Component):
return x._get_state_initializer(context)
elif isinstance(x, ContentAddressableList):
return tuple(p._get_state_initializer(context) for p in x)
else:
val = pnlvm._tupleize(x)

return tuple(val for _ in range(p.history_min_length + 1))

return tuple(map(_convert, self._get_compilation_state()))

def _get_compilation_params(self):
# FIXME: MAGIC LIST, detect used parameters automatically
Expand All @@ -1300,23 +1301,24 @@ def _get_compilation_params(self):
"input_labels_dict", "output_labels_dict",
"modulated_mechanisms", "grid",
"activation_derivative_fct", "input_specification",
# Reference to other components
"objective_mechanism", "agent_rep", "projections",
# Shape mismatch
"costs", "auto", "hetero",
# autodiff specific types
"pytorch_representation", "optimizer"}
# Mechanism's need few extra entires:
# * function -- might overload _get_{param,state}_struct_type
# * matrix -- is never used directly, and is flatened below
# * integration rate -- shape mismatch with param port input
if hasattr(self, 'ports'):
blacklist.update(["function", "matrix", "integration_rate"])
blacklist.update(["matrix", "integration_rate"])
def _is_compilation_param(p):
if p.name not in blacklist and not isinstance(p, ParameterAlias):
#FIXME: this should use defaults
val = p.get()
# Check if the value type is valid for compilation
return not isinstance(val, (str, ComponentsMeta,
ContentAddressableList, type(max),
type(max),
type(_is_compilation_param),
type(self._get_compilation_params)))
return False
Expand All @@ -1334,52 +1336,58 @@ def llvm_param_ids(self):
setattr(self, "_param_ids", ids)
return ids

def _get_param_values(self, context=None):
def _get_values(p):
param = p.get(context)
is_modulated = False
try:
is_modulated = is_modulated or p.name in self.owner.parameter_ports
except AttributeError:
pass
if not is_modulated:
try:
is_modulated = is_modulated or p.name in self.parameter_ports
except AttributeError:
pass
if not is_modulated:
try:
modulated_params = (
getattr(self.parameters, p.sender.modulation).source
for p in self.owner.mod_afferents)
is_modulated = is_modulated or p in modulated_params
except AttributeError:
pass
# Modulated parameters change shape to array
if is_modulated and np.isscalar(param):
param = [param]
elif p.name == 'matrix': # Flatten matrix
param = np.asfarray(param).flatten().tolist()
elif isinstance(param, Component):
param = param._get_param_values(context)
return param
def _is_param_modulated(self, p):
try:
if p.name in self.owner.parameter_ports:
return True
except AttributeError:
pass
try:
if p.name in self.parameter_ports:
return True
except AttributeError:
pass
try:
modulated_params = (
getattr(self.parameters, p.sender.modulation).source
for p in self.owner.mod_afferents)
if p in modulated_params:
return True
except AttributeError:
pass

return tuple(map(_get_values, self._get_compilation_params()))
return False

def _get_param_initializer(self, context):
def _convert(x):
if isinstance(x, Enum):
return x.value
elif isinstance(x, SampleIterator):
if isinstance(x.generator, list):
return (float(v) for v in x.generator)
return tuple(v for v in x.generator)
else:
return (float(x.start), float(x.step), int(x.num))
try:
return (_convert(i) for i in x)
return (x.start, x.step, x.num)
elif isinstance(x, Component):
return x._get_param_initializer(context)

try: # This can't use tupleize and needs to recurse to handle
# 'search_space' list of SampleIterators
return tuple(_convert(i) for i in x)
except TypeError:
return x
return pnlvm._tupleize(_convert(self._get_param_values(context)))
return x if x is not None else tuple()

def _get_values(p):
param = p.get(context)
# Modulated parameters change shape to array
if np.isscalar(param) and self._is_param_modulated(p):
return (param,)
elif p.name == 'num_estimates':
return 0 if param is None else param
elif p.name == 'matrix': # Flatten matrix
return tuple(np.asfarray(param).flatten())
return _convert(param)

return tuple(map(_get_values, self._get_compilation_params()))

def _gen_llvm_function_reset(self, ctx, builder, *_, tags):
assert "reset" in tags
Expand Down Expand Up @@ -1882,7 +1890,8 @@ def _initialize_parameters(self, context=None, **param_defaults):
Process_Base,
Composition_Base,
ComponentsMeta,
types.MethodType
types.MethodType,
functools.partial,
)
alias_names = {p.name for p in self.class_parameters if isinstance(p, ParameterAlias)}

Expand Down
3 changes: 0 additions & 3 deletions psyneulink/core/components/functions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from . import function
from . import userdefinedfunction
from . import combinationfunctions
from . import interfacefunctions
from . import transferfunctions
from . import selectionfunctions
from . import statefulfunctions
Expand All @@ -13,7 +12,6 @@
from .function import *
from .userdefinedfunction import *
from .combinationfunctions import *
from .interfacefunctions import *
from .transferfunctions import *
from .selectionfunctions import *
from .statefulfunctions import *
Expand All @@ -25,7 +23,6 @@
__all__ = list(function.__all__)
__all__.extend(userdefinedfunction.__all__)
__all__.extend(combinationfunctions.__all__)
__all__.extend(interfacefunctions.__all__)
__all__.extend(transferfunctions.__all__)
__all__.extend(selectionfunctions.__all__)
__all__.extend(statefulfunctions.__all__)
Expand Down
Loading

0 comments on commit 2fba3de

Please sign in to comment.