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.

  1. An operation on two SemanticPointer instances will always produce another SemanticPointer instance (or scalar in case of a dot product). Thus, the set of SemanticPointer is closed under most operations.
  2. Next is the set of symbols created with nengo_spa.sym. These will be instances of PointerSymbol and 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 FixedScalar and PointerSymbol. When using a number in such an operation it will be converted automatically to a FixedScalar instance.
  3. 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 DynamicNode instances. The implementation of the operators for DynamicNode is a little bit more involved as the operands may not just be other DynamicNode instances, but also PointerSymbol and SemanticPointer instances. The result will usually be also a DynamicNode, but certain specializations exist. Summed is used for the sum of multiple dynamic outputs, Transformed is used for a dynamic output to which a constant transformation matrix is applied, and ModuleOutput is 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 nengo_spa.ast. Furthermore, all of this classes derive from nengo_spa.ast.base.Node which 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 nengo.Connection.
  • 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 the result.

Types

Types in Nengo SPA are defined by deriving from nengo_spa.types.Type. Two 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 TScalar, TAnyVocab, TAnyVocabOfDim, and TVocabulary are defined, with partial ordering TScalar < TAnyVocab, TAnyVocab < TAnyVocabOfDim, TAnyVocab < TVocabulary, TAnyVocabOfDim(vocab.dimensions) < TVocabulary(vocab).

The nengo_spa.types.coerce_types can be used to determine the smallest type in the partial ordering enclosing all given types.

Actions and the >> operator

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 a RoutedConnection instance in the context of an nengo_spa.Actions object. nengo_spa.actions.ifmax will then trigger the actual creation of the gated connection with the appropriate gating.

Application of the operator overloading to nengo_spa.Network

So far the operator overloading has only been done on specific classes deriving from nengo_spa.ast.base.Node. However, the operators need to work with SPA networks. For this, nengo_spa.Network derives from SpaOperatorMixin which:

  1. Defines the overloaded operators
  2. Converts the operands to the appropriate nengo_spa.ast.base.Node (or ModuleInput) class
  3. 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_input and Network.declare_output methods will dynamically insert SpaOperatorMixin into the inheritance list of a single instance. They also register the associated vocabulary for the type checking.