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.
"""