Implementation of the SPA syntax¶
The SPA syntax is implemented with Python operator overloading at its core. There is a hierarchy of certain sets to consider in this.
An operation on two
SemanticPointerinstances will always produce another
SemanticPointerinstance (or scalar in case of a dot product). Thus, the set of
SemanticPointeris closed under most operations.
Next is the set of symbols created with
nengo_spa.sym. These will be instances of
PointerSymboland any operation on two of these instance will give another
PointerSymbol. For example,
PointerSymbol('A') * PointerSymbol('B')will give
PointerSymbol('A*B'). In addition, some operations are defined between
PointerSymbol. When using a number in such an operation it will be converted automatically to a
The final set are operations based on module outputs that will change over the course of a simulation. All of these fall into the set of
DynamicNodeinstances. The implementation of the operators for
DynamicNodeis a little bit more involved as the operands may not just be other
DynamicNodeinstances, but also
SemanticPointerinstances. The result will usually be also a
DynamicNode, but certain specializations exist.
Summedis used for the sum of multiple dynamic outputs,
Transformedis used for a dynamic output to which a constant transformation matrix is applied, and
ModuleOutputis the bare output of a SPA module.
Nengo SPA used to construct a complete abstract syntax tree (AST) before
constructing any network, but this has since been changed to greedily construct
networks as soon as all the required information has been obtained. For this
historical reason, most of the classes mentioned still live in
Furthermore, all of this classes derive from
specifies the minimal interface of all these classes:
A type attribute used to ensure type safety within the SPA syntax and provides the type a node evaluates to (see below).
A connect_to(sink, **kwargs) method that creates all the network components to implement the node and connects the output to the Nengo object sink. The keyword arguments are to be passed to the
A construct() method that creates all the network components required to implement the node and returns a Nengo object providing the output (instead of connecting into a receiving Nengo object).
For fixed values nodes of the type
Fixed are used that need to implement an
additional evaluate() method that evaluates the fixed expression and returns
Types in Nengo SPA are defined by deriving from
types will be considered equal if their name attributes match (as long as
__eq__ is not overwritten). For more complex relationships, the __gt__
method may be overwritten to specify a partial ordering. A call to
a.__gt__(b) should return True, if (and only if) b can be cast to the
type a. For example, the type for an unspecified vocabulary can be cast to
the type for a specific vocabulary.
Currently, the types
TVocabulary are defined, with partial ordering
TScalar < TAnyVocab,
TAnyVocab < TAnyVocabOfDim,
TAnyVocab < TVocabulary,
TAnyVocabOfDim(vocab.dimensions) < TVocabulary(vocab).
nengo_spa.types.coerce_types can be used to determine the smallest type
in the partial ordering enclosing all given types.
Actions and the
>> (right shift) operator for creating connections is also implemented
through operator overloading on the
ModuleInput class. Depending on the
context, it either creates the connection immediately or creates
RoutedConnection instance in the context of an
nengo_spa.actions.ifmax will then trigger the actual creation of the gated
connection with the appropriate gating.
Application of the operator overloading to
So far the operator overloading has only been done on specific classes deriving
nengo_spa.ast.base.Node. However, the operators need to work with SPA
networks. For this,
nengo_spa.Network derives from
Defines the overloaded operators
Converts the operands to the appropriate
Delegates to the class’s implementation of the operator.
Furthermore, the operators need to be overloaded for the input and outputs of
a SPA network. These will be basic Nengo objects and should continue to be
usable as such. Thus, the
Network.declare_output methods will dynamically insert
into the inheritance list of a single instance. They also register the
associated vocabulary for the type checking.