Source code for nengo_spa.algebras.base

import warnings
from abc import ABC, ABCMeta
from enum import Enum


[docs]class ElementSidedness(Enum): """The side in a binary operation for which a special element's properties hold.""" LEFT = "left" RIGHT = "right" TWO_SIDED = "two-sided"
class _DuckTypedABCMeta(ABCMeta): def __instancecheck__(cls, instance): if super().__instancecheck__(instance): return True for member in dir(cls): if member.startswith("_"): continue if not hasattr(instance, member) or not hasattr( getattr(instance, member), "__self__" ): return False warnings.warn( DeprecationWarning( f"Please do not rely on pure duck-typing for {cls.__name__}. " f"Explicitly register your class {instance.__class__.__name__} " f"as a virtual subclass of {cls.__name__} or derive from it." ) ) return True
[docs]class AbstractAlgebra(metaclass=_DuckTypedABCMeta): """Abstract base class for algebras. Custom algebras can be defined by implementing the interface of this abstract base class. """
[docs] def is_valid_dimensionality(self, d): """Checks whether *d* is a valid vector dimensionality. Parameters ---------- d : int Dimensionality Returns ------- bool *True*, if *d* is a valid vector dimensionality for the use with the algebra. """ raise NotImplementedError()
[docs] def create_vector(self, d, properties, *, rng=None): """Create a vector fulfilling given properties in the algebra. Valid properties and combinations thereof depend on the concrete algebra. It is suggested that the *properties* is either a *set* of *str* (if order does not matter) or a *list* of *str* (if order does matter). Use the constants defined in `.CommonProperties` where appropriate. Parameters ---------- d : int Vector dimensionality properties Definition of properties for the vector to fulfill. Type and specification format depend on the concrete algbra, but it is suggested to use either a *set* or *list* of *str* (depending on whether order of properties matters) utilizing the constants defined in `.CommonProperties` where applicable. rng : numpy.random.RandomState, optional The random number generator to use to create the vector. Returns ------- ndarray Random vector with desired properties. """ raise NotImplementedError()
[docs] def make_unitary(self, v): """Returns a unitary vector based on the vector *v*. A unitary vector does not change the length of a vector it is bound to. Parameters ---------- v : (d,) ndarray Vector to base unitary vector on. Returns ------- ndarray Unitary vector. """ raise NotImplementedError()
[docs] def superpose(self, a, b): """Returns the superposition of *a* and *b*. This is commonly elementwise addition. Parameters ---------- a : (d,) ndarray Left operand in superposition. b : (d,) ndarray Right operand in superposition. Returns ------- (d,) ndarray Superposed vector. """ raise NotImplementedError()
[docs] def bind(self, a, b): """Returns the binding of *a* and *b*. The resulting vector should in most cases be dissimilar to both inputs. Parameters ---------- a : (d,) ndarray Left operand in binding. b : (d,) ndarray Right operand in binding. Returns ------- (d,) ndarray Bound vector. """ raise NotImplementedError()
[docs] def binding_power(self, v, exponent): """Returns the binding power of *v* using the *exponent*. For a positive *exponent*, the binding power is defined as binding (*exponent*-1) times bindings of *v* to itself. For a negative *exponent*, the binding power is the approximate inverse bound to itself according to the prior definition. Depending on the algebra, fractional exponents might be valid or return a *ValueError*, if not. Usually, a fractional binding power will require that *v* has a positive sign. Note the following special exponents: * an exponent of -1 will return the approximate inverse, * an exponent of 0 will return the identity vector, * and an *exponent* of 1 will return *v* itself. The default implementation supports integer exponents only and will apply the `.bind` method multiple times. It requires the algebra to have a left identity. Parameters ---------- v : (d,) ndarray Vector to bind repeatedly to itself. exponent : int or float Exponent of the binding power. Returns ------- (d,) ndarray Binding power of *v*. See also -------- AbstractAlgebra.sign """ if not int(exponent) == exponent: raise ValueError( "{} only supports integer binding powers.".format( self.__class__.__name__ ) ) exponent = int(exponent) power = self.identity_element(len(v), sidedness=ElementSidedness.LEFT) for _ in range(abs(exponent)): power = self.bind(power, v) if exponent < 0: power = self.invert(power) return power
[docs] def invert(self, v, sidedness=ElementSidedness.TWO_SIDED): """Invert vector *v*. A vector bound to its inverse will result in the identity vector. Some algebras might not have an inverse only on specific sides. In that case a *NotImplementedError* may be raised for non-existing inverses. Parameters ---------- v : (d,) ndarray Vector to invert. sidedness : ElementSidedness, optional Side in the binding operation on which the returned value acts as inverse. Returns ------- (d,) ndarray Inverted vector. """ raise NotImplementedError()
[docs] def get_binding_matrix(self, v, swap_inputs=False): """Returns the transformation matrix for binding with a fixed vector. Parameters ---------- v : (d,) ndarray Fixed vector to derive binding matrix for. swap_inputs : bool, optional By default the matrix will be such that *v* becomes the *right* operand in the binding. By setting *swap_inputs*, the matrix will be such that *v* becomes the *left* operand. For binding operations that are commutative (such as circular convolution), this has no effect. Returns ------- (d, d) ndarray Transformation matrix to perform binding with *v*. """ raise NotImplementedError()
[docs] def get_inversion_matrix(self, d, sidedness=ElementSidedness.TWO_SIDED): """Returns the transformation matrix for inverting a vector. Some algebras might not have an inverse only on specific sides. In that case a *NotImplementedError* may be raised for non-existing inverses. Parameters ---------- d : int Vector dimensionality (determines the matrix size). sidedness : ElementSidedness, optional Side in the binding operation on which a transformed vectors acts as inverse. Returns ------- (d, d) ndarray Transformation matrix to invert a vector. """ raise NotImplementedError()
[docs] def implement_superposition(self, n_neurons_per_d, d, n): """Implement neural network for superposing vectors. Parameters ---------- n_neurons_per_d : int Neurons to use per dimension. d : int Dimensionality of the vectors. n : int Number of vectors to superpose in the network. Returns ------- tuple Tuple *(net, inputs, output)* where *net* is the implemented `nengo.Network`, *inputs* a sequence of length *n* of inputs to the network, and *output* the network output. """ raise NotImplementedError()
[docs] def implement_binding(self, n_neurons_per_d, d, unbind_left, unbind_right): """Implement neural network for binding vectors. Parameters ---------- n_neurons_per_d : int Neurons to use per dimension. d : int Dimensionality of the vectors. unbind_left : bool Whether the left input should be unbound from the right input. unbind_right : bool Whether the right input should be unbound from the left input. Returns ------- tuple Tuple *(net, inputs, output)* where *net* is the implemented `nengo.Network`, *inputs* a sequence of the left and the right input in that order, and *output* the network output. """ raise NotImplementedError()
[docs] def sign(self, v): """Returns the sign of *v* defined by the algebra. The exact definition of the sign depends on the concrete algebra, but should be analogous to the sign of a (complex) number in so far that binding two vectors with the same sign produces a "positive" vector. There might, however, be multiple types of negative signs, where binding vectors with different types of negative signs will produce another "negative" vector. Furthermore, if the algebra supports fractional binding powers, it should do so for all "non-negative" vectors, but not "negative" vectors. If an algebra does not have the notion of a sign, it may raise a :py:class:`NotImplementedError`. Parameters ---------- v : (d,) ndarray Vector to determine sign of. Returns ------- AbstractSign The sign of the input vector. See Also -------- AbstractAlgebra.abs """ raise NotImplementedError()
[docs] def abs(self, v): """Returns the absolute vector of *v* defined by the algebra. The exact definition of "absolute vector" may depend on the concrete algebra. It should be a "positive" vector (see `.sign`) that relates to the input vector. The default implementation requires that the possible signs of the algebra correspond to actual vectors within the algebra. It will bind the inverse of the sign vector (from the left side) to the vector *v*. If an algebra does not have the notion of a sign or absolute vector, it may raise a :py:class:`NotImplementedError`. Parameters ---------- v : (d,) ndarray Vector to obtain the absolute vector of. Returns ------- (d,) ndarray The absolute vector relating to the input vector. """ return self.bind(self.invert(self.sign(v).to_vector(len(v))), v)
[docs] def absorbing_element(self, d, sidedness=ElementSidedness.TWO_SIDED): """Return the standard absorbing element of dimensionality *d*. An absorbing element will produce a scaled version of itself when bound to another vector. The standard absorbing element is the absorbing element with norm 1. Some algebras might not have an absorbing element other than the zero vector. In that case a *NotImplementedError* may be raised. Parameters ---------- d : int Vector dimensionality. sidedness : ElementSidedness, optional Side in the binding operation on which the element absorbs. Returns ------- (d,) ndarray Standard absorbing element. """ raise NotImplementedError()
[docs] def identity_element(self, d, sidedness=ElementSidedness.TWO_SIDED): """Return the identity element of dimensionality *d*. The identity does not change the vector it is bound to. Some algebras might not have an identity element. In that case a *NotImplementedError* may be raised. Parameters ---------- d : int Vector dimensionality. sidedness : ElementSidedness, optional Side in the binding operation on which the element acts as identity. Returns ------- (d,) ndarray Identity element. """ raise NotImplementedError()
[docs] def negative_identity_element(self, d, sidedness=ElementSidedness.TWO_SIDED): """Returns the negative identity element of dimensionality *d*. The negative identity only changes the sign of the vector it is bound to. Some algebras might not have a negative identity element (or even the notion of a sign). In that case a :py:class`NotImplementedError` may be raised. Parameters ---------- d : int Vector dimensionality. sidedness : ElementSidedness, optional Side in the binding operation on which the element acts as negative identity. Returns ------- (d,) ndarray Negative identity element. """ raise NotImplementedError()
[docs] def zero_element(self, d, sidedness=ElementSidedness.TWO_SIDED): """Return the zero element of dimensionality *d*. The zero element produces itself when bound to a different vector. Usually this will be the zero vector. Some algebras might not have a zero element. In that case a *NotImplementedError* may be raised. Parameters ---------- d : int Vector dimensionality. sidedness : ElementSidedness, optional Side in the binding operation on which the element acts as zero. Returns ------- (d,) ndarray Zero element. """ raise NotImplementedError()
[docs]class AbstractSign(ABC): """Abstract base class for implementing signs for an algebra."""
[docs] def is_positive(self): """Return whether the sign is positive.""" raise NotImplementedError()
[docs] def is_negative(self): """Return whether the sign is negative.""" raise NotImplementedError()
[docs] def is_zero(self): """Return whether the sign neither positive nor negative (i.e. zero), but definite.""" return not (self.is_positive() or self.is_negative() or self.is_indefinite())
[docs] def is_indefinite(self): """Return whether the sign is neither positive nor negative nor zero.""" raise NotImplementedError()
[docs] def to_vector(self, d): """Return the vector in the algebra corresponding to the sign. Parameters ---------- d : int Vector dimensionality. Returns ------- (d,) ndarray Vector corresponding to the sign. """ raise NotImplementedError()
[docs]class GenericSign(AbstractSign): """A generic sign implementation. Parameters ---------- sign : -1, 0, 1, None The represented sign. *None* is used for an indefinite sign. """ def __init__(self, sign): if sign not in (-1, 0, 1, None): raise ValueError("sign must be one of -1, 0, 1, None") self.sign = sign
[docs] def is_positive(self): return not self.is_indefinite() and self.sign > 0
[docs] def is_negative(self): return not self.is_indefinite() and self.sign < 0
[docs] def is_zero(self): return not self.is_indefinite() and self.sign == 0
[docs] def is_indefinite(self): return self.sign is None
def __repr__(self): return "{}(sign={})".format(self.__class__.__name__, self.sign) def __eq__(self, other): return isinstance(other, self.__class__) and self.sign == other.sign
[docs]class CommonProperties: """Definition of constants for common properties of vectors in an algebra. Use these for best interoperability between algebras. """ UNITARY = "unitary" """A unitary vector does not change the length of a vector it is bound to.""" POSITIVE = "positive" """A positive vector does not change the sign of a vector it is bound to. A positive vector allows for fractional binding powers. """