The config
system¶
Nengo objects have many parameters that can be modified. Some of these parameters are critical characteristics of that object, and others are hints or suggestions that a backend can use or ignore.
Nengo’s config
system is designed to make setting large numbers of parameters easy, and to allow backends to introduce additional parameters without changing core Nengo objects.
[1]:
import nengo
import nengo.params
from nengo.utils.ipython import hide_input
hide_input()
[1]:
Setting default parameters¶
The config
system aids in setting many parameters with a hierarchy of defaults. When you create a Nengo object, any parameters not specified will be given the value nengo.Default
. This value tells Nengo to use the default value with the highest precedence in the hierarchy. Every Network
has an associated config
object, on which defaults can be set. The network hierarchy is traversed from most to least specific and the first network with a default set for that particular
parameter is used. For example:
with nengo.Network() as net:
with nengo.Network() as subnet:
with nengo.Network() as subsubnet:
ens = nengo.Ensemble(10, 1)
When filling in defaults for ens
, the hierarchy looks like
└── net <- least specific
└── subnet
└── subsubnet <- most specific
so defaults set in subsubnet
will take precedence over those in subnet
, which take precedence over those in net
.
If no default has been set in the network hierarchy, then the parameter default is used. These defaults are specified when the Nengo objects are created. We can investigate these defaults by printing the class attributes associated with them.
[2]:
# Get all info about the radius
print(nengo.Ensemble.radius)
# Just get the default
print(nengo.Ensemble.radius.default)
NumberParam('radius', default=1.0, optional=False, readonly=False)
1.0
We can inspect which defaults have been overridden in a particular config
object by printing it.
[3]:
model = nengo.Network()
print(model.config)
No parameters configured for Connection.
No parameters configured for Ensemble.
No parameters configured for Node.
No parameters configured for Probe.
To configure a parameter (i.e., change its network-local default), set it as shown below.
[4]:
model.config[nengo.Ensemble].radius = 1.5
print(model.config[nengo.Ensemble])
Parameters configured for Ensemble:
radius: 1.5
Within this network, the default radius will be 1.5.
[5]:
with nengo.Network():
ens = nengo.Ensemble(10, 1)
print(f"Normal network: ens.radius = {ens.radius}")
with model:
ens = nengo.Ensemble(10, 1)
print(f"Configured network: ens.radius = {ens.radius}")
Normal network: ens.radius = 1.0
Configured network: ens.radius = 1.5
Note that if a radius is explicitly passed in, it will always be used.
[6]:
with nengo.Network():
ens = nengo.Ensemble(10, 1, radius=2.0)
print(f"Normal network: ens.radius = {ens.radius}")
with model:
ens = nengo.Ensemble(10, 1, radius=2.0)
print(f"Configured network: ens.radius = {ens.radius}")
Normal network: ens.radius = 2.0
Configured network: ens.radius = 2.0
When networks are nested within one another, the most specific network configuration is used. For example, if you create an Ensemble without specifying a radius, it will first check the network that the Ensemble is a part of; if that network has not configured a default, then it will check the network that that network is part of, and so on.
[7]:
with model:
with nengo.Network() as subnet:
subnet.config[nengo.Ensemble].neuron_type = nengo.LIFRate()
with nengo.Network() as subsubnet:
subsubnet.config[nengo.Ensemble].radius = 2.0
print("Creating e1 in subsubnet")
e1 = nengo.Ensemble(10, 1)
# Uses subsubnet.config value for radius
print(" radius =", e1.radius)
# Uses subnet.config value for neuron_type
print(" neuron_type =", e1.neuron_type)
print("Creating e2 in subnet")
e2 = nengo.Ensemble(10, 1)
# Uses model.config value for radius
print(" radius =", e2.radius)
# Uses subnet.config value for neuron_type
print(" neuron_type =", e2.neuron_type)
print("Creating e3 in model")
e3 = nengo.Ensemble(10, 1)
# Uses model.config value for radius
print(" radius =", e3.radius)
# Uses nengo.Ensemble default for neuron_type
print(" neuron_type =", e3.neuron_type)
Creating e1 in subsubnet
radius = 2.0
neuron_type = LIFRate()
Creating e2 in subnet
radius = 1.5
neuron_type = LIFRate()
Creating e3 in model
radius = 1.5
neuron_type = LIF()
Note that each config
object only knows about the defaults set on itself.
[8]:
with model:
with nengo.Network() as subnet:
subnet.config[nengo.Ensemble].neuron_type = nengo.LIFRate()
with nengo.Network() as subsubnet:
subsubnet.config[nengo.Ensemble].radius = 2.0
print("subsubnet:")
print(subsubnet.config[nengo.Ensemble])
print("\nsubnet:")
print(subnet.config[nengo.Ensemble])
print("\nmodel:")
print(model.config[nengo.Ensemble])
subsubnet:
Parameters configured for Ensemble:
radius: 2.0
subnet:
Parameters configured for Ensemble:
neuron_type: LIFRate()
model:
Parameters configured for Ensemble:
radius: 1.5
If you want a more global picture of the defaults in the current context, you can query the Config
class itself (all config
objects are instances of Config
).
To query all parameters, print Config.all_defaults()
. You may pass a Nengo object class to this function to filter the results. For example, to get all defaults set for Ensemble
, use Config.all_defaults(nengo.Ensemble)
.
[9]:
with model:
print("In 'model' context:")
print(nengo.Config.all_defaults())
with nengo.Network() as subnet:
subnet.config[nengo.Ensemble].neuron_type = nengo.LIFRate()
subnet.config[nengo.Ensemble].radius = 3.0
print("In 'subnet' context:")
print(nengo.Config.all_defaults(nengo.Ensemble))
with nengo.Network() as subsubnet:
subsubnet.config[nengo.Ensemble].neuron_type = nengo.Direct()
subsubnet.config[nengo.Ensemble].radius = 2.0
print("In 'subsubnet' context:")
print(nengo.Config.all_defaults(nengo.Ensemble))
In 'model' context:
Current defaults for Connection:
eval_points: None
function_info: None
label: None
learning_rule_type: None
scale_eval_points: True
seed: None
solver: LstsqL2()
synapse: Lowpass(tau=0.005)
transform: None
Current defaults for Ensemble:
bias: None
encoders: ScatteredHypersphere(surface=True)
eval_points: ScatteredHypersphere()
gain: None
intercepts: Uniform(low=-1.0, high=0.9)
label: None
max_rates: Uniform(low=200, high=400)
n_eval_points: None
neuron_type: LIF()
noise: None
normalize_encoders: True
radius: 1.5
seed: None
Current defaults for Node:
label: None
output: None
seed: None
size_in: None
size_out: None
Current defaults for Probe:
attr: None
label: None
sample_every: None
seed: None
solver: ConnectionDefault
synapse: None
In 'subnet' context:
Current defaults for Ensemble:
bias: None
encoders: ScatteredHypersphere(surface=True)
eval_points: ScatteredHypersphere()
gain: None
intercepts: Uniform(low=-1.0, high=0.9)
label: None
max_rates: Uniform(low=200, high=400)
n_eval_points: None
neuron_type: LIFRate()
noise: None
normalize_encoders: True
radius: 3.0
seed: None
In 'subsubnet' context:
Current defaults for Ensemble:
bias: None
encoders: ScatteredHypersphere(surface=True)
eval_points: ScatteredHypersphere()
gain: None
intercepts: Uniform(low=-1.0, high=0.9)
label: None
max_rates: Uniform(low=200, high=400)
n_eval_points: None
neuron_type: Direct()
noise: None
normalize_encoders: True
radius: 2.0
seed: None
The default value for a particular parameter can be queried from the global context with the nengo.Config.default
function. Type help(nengo.Config.default)
for more information.
[10]:
def print_defaults():
def_radius = nengo.Config.default(nengo.Ensemble, "radius")
def_type = nengo.Config.default(nengo.Ensemble, "neuron_type")
print(f" default radius: {def_radius}")
print(f" default neuron_type: {def_type}")
with model:
with nengo.Network() as subnet:
subnet.config[nengo.Ensemble].neuron_type = nengo.LIFRate()
with nengo.Network() as subsubnet:
subsubnet.config[nengo.Ensemble].radius = 2.0
print("subsubnet:")
print_defaults()
print("\nsubnet:")
print_defaults()
print("\nmodel:")
print_defaults()
subsubnet:
default radius: 2.0
default neuron_type: LIFRate()
subnet:
default radius: 1.5
default neuron_type: LIFRate()
model:
default radius: 1.5
default neuron_type: LIF()
Defaults are filled in immediately¶
One important feature about the defaults hierarchy is that defaults are filled in immediately. When you create a Nengo object, the attributes are filled in with the current defaults that are set. Changing the defaults after object creation will not update objects already created.
[11]:
with model:
e1 = nengo.Ensemble(10, 1)
print("e1.radius =", e1.radius)
print("Changing default radius to 2.0")
model.config[nengo.Ensemble].radius = 2.0
e2 = nengo.Ensemble(10, 1)
print("e1.radius =", e1.radius)
print("e2.radius =", e2.radius)
e1.radius = 1.5
Changing default radius to 2.0
e1.radius = 1.5
e2.radius = 2.0
Resetting to default¶
If you ever wish to reset a value back to the default, you can remove it from the config
object you modified.
[12]:
with model:
e1 = nengo.Ensemble(10, 1)
print("e1.radius =", e1.radius)
print("Resetting radius back to default")
del model.config[nengo.Ensemble].radius
print("\n" + str(model.config[nengo.Ensemble]) + "\n")
e2 = nengo.Ensemble(10, 1)
print("e2.radius =", e2.radius)
e1.radius = 2.0
Resetting radius back to default
No parameters configured for Ensemble.
e2.radius = 1.0
Making new config
s¶
Typically, several Nengo objects will share a set of parameters, but won’t make sense to encapsulate in a network. One method of having those objects share parameters is to use dictionary unpacking.
[13]:
with nengo.Network():
hippocampus_args = {"radius": 1.5, "neuron_type": nengo.LIFRate()}
e1 = nengo.Ensemble(100, 2, **hippocampus_args)
e2 = nengo.Ensemble(150, 3, **hippocampus_args)
e3 = nengo.Ensemble(200, 4, **hippocampus_args)
print(e1.radius, e2.radius, e3.radius)
1.5 1.5 1.5
An alternative method that can be very useful for large networks and for more readable models is to create a new config
object to encapsulate those parameter settings.
[14]:
in_hippocampus = nengo.Config(nengo.Ensemble)
in_hippocampus[nengo.Ensemble].radius = 1.5
in_hippocampus[nengo.Ensemble].neuron_type = nengo.LIFRate()
with nengo.Network():
with in_hippocampus:
e1 = nengo.Ensemble(100, 2)
e2 = nengo.Ensemble(150, 3)
e3 = nengo.Ensemble(200, 4)
print(e1.radius, e2.radius, e3.radius)
1.5 1.5 1.5
Advanced: adding new parameters¶
This section is targeted to those implementing new backends or large libraries of networks (like, for example, nengo.SPA
).
Often, you want to associate some kind of metadata with a Nengo object, or a type of Nengo objects. For example, in backends that communicate with specific hardware, it can be helpful to mark certain nodes as being time-dependent, or to assign certain ensembles to a particular portion of the hardware memory.
Python allows us to make new attributes on Nengo objects. However, we highly discourage this activity, because a Nengo object should be a backend-agnostic part of a model. The parameters pre-defined on Nengo objects make up the parameters that all backends should deal with in some way.
For this reason, we raise a warning when creating a new attribute.
[15]:
with nengo.Network():
ens = nengo.Ensemble(10, 1)
ens.memory_location = 0x1000
/home/runner/work/nengo/nengo/nengo/base.py:105: SyntaxWarning: Creating new attribute 'memory_location' on '<Ensemble (unlabeled) at 0x7f6d44020160>'. Did you mean to change an existing attribute?
warnings.warn(
So how should backends associate arbitrary information with Nengo objects? The config
system!
We saw above that we can create new config
objects by specifying which Nengo objects they can configure. We can also create new parameters on those config
objects.
[16]:
my_config = nengo.Config(nengo.Ensemble)
# memory_location must be a positive integer
my_config[nengo.Ensemble].set_param(
"memory_location",
nengo.params.IntParam("memory_location", default=None, optional=True, low=0),
)
Now, we can set that parameter for the nengo.Ensemble
class as a whole, or with individual instances.
[17]:
# Make the network (this code is backend-agnostic)
with nengo.Network():
e1 = nengo.Ensemble(10, 1)
e2 = nengo.Ensemble(10, 1)
# Set backend-specific parameters
my_config[nengo.Ensemble].memory_location = 0x1000 # Set Ensemble default
my_config[e2].memory_location = 0x2000 # Set value for e2
print(f"e1 will be stored at 0x{my_config[e1].memory_location:x}")
print(f"e2 will be stored at 0x{my_config[e2].memory_location:x}")
e1 will be stored at 0x1000
e2 will be stored at 0x2000
Parameter
types for the most common Python objects are available in nengo.params
, as well as other types that Nengo uses frequently, but it is possible to implement your own in order to do additional processing like validation. See the nengo.params
source for examples.
[18]:
print([cls for cls in dir(nengo.params) if cls.endswith("Param")])
['BoolParam', 'DictParam', 'EnumParam', 'FunctionParam', 'IntParam', 'NdarrayParam', 'NumberParam', 'ObsoleteParam', 'ShapeParam', 'StringParam', 'TupleParam']