import nengo
import numpy as np
from nengo.exceptions import ValidationError
from nengo_spa.algebras.base import AbstractAlgebra, AbstractSign, ElementSidedness
from nengo_spa.algebras.hrr_algebra import HrrAlgebra
from nengo_spa.ast.base import Fixed, TypeCheckedBinaryOp, infer_types
from nengo_spa.ast.expr_tree import (
AttributeAccess,
BinaryOperator,
FunctionCall,
Leaf,
Node,
UnaryOperator,
limit_str_length,
)
from nengo_spa.typechecks import is_array, is_array_like, is_number
from nengo_spa.types import TAnyVocab, TScalar, TVocabulary
[docs]class SemanticPointer(Fixed):
"""A Semantic Pointer, based on Holographic Reduced Representations.
Operators are overloaded so that ``+`` and ``-`` are addition,
``*`` is circular convolution, and ``~`` is the two-sided inversion operator.
The left and right inverese can be obtained with the `linv` and `rinv`
methods.
Parameters
----------
data : array_like
The vector constituting the Semantic Pointer.
vocab : Vocabulary, optional
Vocabulary that the Semantic Pointer is considered to be part of.
Mutually exclusive with the *algebra* argument.
algebra : AbstractAlgebra, optional
Algebra used to perform vector symbolic operations on the Semantic
Pointer. Defaults to `.HrrAlgebra`. Mutually exclusive
with the *vocab* argument.
name : str, optional
A name for the Semantic Pointer.
Attributes
----------
v : array_like
The vector constituting the Semantic Pointer.
algebra : AbstractAlgebra
Algebra that defines the vector symbolic operations on this Semantic
Pointer.
vocab : Vocabulary or None
The vocabulary the this Semantic Pointer is considered to be part of.
name : str or None
Name of the Semantic Pointer.
"""
MAX_NAME = 1024
def __init__(self, data, vocab=None, algebra=None, name=None):
super(SemanticPointer, self).__init__(
TAnyVocab if vocab is None else TVocabulary(vocab)
)
self.algebra = self._get_algebra(vocab, algebra)
self.v = np.array(data, dtype=float)
if len(self.v.shape) != 1:
raise ValidationError("'data' must be a vector", "data", self)
self.v.setflags(write=False)
self.vocab = vocab
if name is not None:
if not isinstance(name, Node):
name = Leaf(name)
name = limit_str_length(name, self.MAX_NAME)
self._expr_tree = name
@property
def name(self):
return None if self._expr_tree is None else str(self._expr_tree)
def _get_algebra(cls, vocab, algebra):
if algebra is None:
if vocab is None:
algebra = HrrAlgebra()
else:
algebra = vocab.algebra
elif vocab is not None and vocab.algebra is not algebra:
raise ValueError("vocab and algebra argument are mutually exclusive")
if not isinstance(algebra, AbstractAlgebra):
raise ValidationError(
"'algebra' must be an instance of AbstractAlgebra", "algebra", algebra
)
return algebra
def _get_unary_name(self, op):
return UnaryOperator(op, self._expr_tree) if self._expr_tree else None
def _get_method_name(self, method):
return (
FunctionCall(tuple(), AttributeAccess(method, self._expr_tree))
if self._expr_tree
else None
)
def _get_binary_name(self, other, op, swap=False):
if isinstance(other, SemanticPointer):
other_expr_tree = other._expr_tree
else:
other_expr_tree = Leaf(str(other))
self_expr_tree = self._expr_tree
if self_expr_tree and other_expr_tree:
if swap:
self_expr_tree, other_expr_tree = other_expr_tree, self._expr_tree
return BinaryOperator(op, self_expr_tree, other_expr_tree)
else:
return None
def evaluate(self):
return self
def connect_to(self, sink, **kwargs):
return nengo.Connection(self.construct(), sink, **kwargs)
def construct(self):
return nengo.Node(self.v, label=str(self).format(len(self)))
[docs] def normalized(self):
"""Normalize the Semantic Pointer and return it as a new object.
If the vector length is zero, the Semantic Pointer will be returned
unchanged.
The original object is not modified.
"""
nrm = np.linalg.norm(self.v)
if nrm <= 0.0:
nrm = 1.0
return SemanticPointer(
self.v / nrm,
vocab=self.vocab,
algebra=self.algebra,
name=self._get_method_name("normalized"),
)
[docs] def unitary(self):
"""Make the Semantic Pointer unitary and return it as a new object.
The original object is not modified.
A unitary Semantic Pointer has the property that it does not change
the length of Semantic Pointers it is bound with.
"""
return SemanticPointer(
self.algebra.make_unitary(self.v),
vocab=self.vocab,
algebra=self.algebra,
name=self._get_method_name("unitary"),
)
[docs] def sign(self):
"""Return the sign of the Semantic Pointer.
See `.AbstractAlgebra.sign` for details on signs of Semantic Pointers.
Returns
-------
SemanticPointerSign
Sign of the Semantic Pointer.
"""
return SemanticPointerSign(
self.algebra.sign(self.v),
dimensions=len(self),
vocab=self.vocab,
algebra=self.algebra,
name=self._get_method_name("sign"),
)
[docs] def abs(self):
"""Return the absolute Semantic Pointer.
See `.AbstractAlgebra.abs` for details on absolute Semantic Pointers.
"""
return SemanticPointer(
self.algebra.abs(self.v),
vocab=self.vocab,
algebra=self.algebra,
name=self._get_method_name("abs"),
)
[docs] def copy(self):
"""Return another semantic pointer with the same data."""
return SemanticPointer(
data=self.v, vocab=self.vocab, algebra=self.algebra, name=self.name
)
[docs] def length(self):
"""Return the L2 norm of the vector."""
return np.linalg.norm(self.v)
def __len__(self):
"""Return the number of dimensions in the vector."""
return len(self.v)
def __str__(self):
if self.name:
return f"SemanticPointer<{self.name}>"
else:
return repr(self)
def __repr__(self):
return (
f"SemanticPointer({self.v!r}, vocab={self.vocab!r}, "
f"algebra={self.algebra!r}, name={self.name!r}"
)
@TypeCheckedBinaryOp(Fixed)
def __add__(self, other):
return self._add(other, swap=False)
@TypeCheckedBinaryOp(Fixed)
def __radd__(self, other):
return self._add(other, swap=True)
def _add(self, other, swap=False):
type_ = infer_types(self, other)
vocab = None if type_ == TAnyVocab else type_.vocab
if vocab is None:
self._ensure_algebra_match(other)
other_pointer = other.evaluate()
a, b = self.v, other_pointer.v
if swap:
a, b = b, a
return SemanticPointer(
data=self.algebra.superpose(a, b),
vocab=vocab,
algebra=self.algebra,
name=self._get_binary_name(other_pointer, "+", swap),
)
def __neg__(self):
return SemanticPointer(
data=-self.v,
vocab=self.vocab,
algebra=self.algebra,
name=self._get_unary_name("-"),
)
def __sub__(self, other):
return self + (-other)
def __rsub__(self, other):
return (-self) + other
def __mul__(self, other):
"""Multiplication of two SemanticPointers is circular convolution.
If multiplied by a scalar, we do normal multiplication.
"""
return self._mul(other, swap=False)
def __rmul__(self, other):
"""Multiplication of two SemanticPointers is circular convolution.
If multiplied by a scalar, we do normal multiplication.
"""
return self._mul(other, swap=True)
def _mul(self, other, swap=False):
if is_number(other):
return SemanticPointer(
data=self.v * other,
vocab=self.vocab,
algebra=self.algebra,
name=self._get_binary_name(other, "*", swap),
)
elif is_array(other):
raise TypeError(
"Multiplication of Semantic Pointers with arrays in not allowed."
)
elif isinstance(other, Fixed):
if other.type == TScalar:
return SemanticPointer(
data=self.v * other.evaluate(),
vocab=self.vocab,
algebra=self.algebra,
name=self._get_binary_name(other, "*", swap),
)
else:
return self._bind(other, swap=swap)
else:
return NotImplemented
[docs] def __pow__(self, other):
"""Binding power of the Semantic Pointer.
Integer exponents are supported for any algebra.
See the `~.AbstractAlgebra.binding_power` documentation of the respective
algebra for details about fractional exponents.
Parameters
----------
other : float
Exponent of the binding power.
Returns
-------
SemanticPointer
"""
return SemanticPointer(
data=self.algebra.binding_power(self.v, other),
vocab=self.vocab,
algebra=self.algebra,
name=self._get_binary_name(other, "**"),
)
def __truediv__(self, other):
if is_number(other):
if other == 0:
raise ZeroDivisionError("Semantic Pointer division by zero")
return SemanticPointer(
data=self.v / other,
vocab=self.vocab,
algebra=self.algebra,
name=self._get_binary_name(other, "/"),
)
elif is_array(other):
raise TypeError("Division of Semantic Pointers with arrays is not allowed.")
else:
return NotImplemented
[docs] def __invert__(self):
"""Return a reorganized `SemanticPointer` that acts as a two-sided
inverse for binding.
.. seealso:: `linv`, `rinv`
"""
return SemanticPointer(
data=self.algebra.invert(self.v, sidedness=ElementSidedness.TWO_SIDED),
vocab=self.vocab,
algebra=self.algebra,
name=self._get_unary_name("~"),
)
[docs] def linv(self):
"""Return a reorganized `SemanticPointer` that acts as a left inverse
for binding.
.. seealso:: `__invert__`, `rinv`
"""
return SemanticPointer(
data=self.algebra.invert(self.v, sidedness=ElementSidedness.LEFT),
vocab=self.vocab,
algebra=self.algebra,
name=self._get_method_name("rinv"),
)
[docs] def rinv(self):
"""Return a reorganized `SemanticPointer` that acts as a right inverse
for binding.
.. seealso:: `__invert__`, `linv`
"""
return SemanticPointer(
data=self.algebra.invert(self.v, sidedness=ElementSidedness.RIGHT),
vocab=self.vocab,
algebra=self.algebra,
name=self._get_method_name("rinv"),
)
[docs] def bind(self, other):
"""Return the binding of two SemanticPointers."""
return self._bind(other, swap=False)
[docs] def rbind(self, other):
"""Return the binding of two SemanticPointers."""
return self._bind(other, swap=True)
def _bind(self, other, swap=False):
type_ = infer_types(self, other)
vocab = None if type_ == TAnyVocab else type_.vocab
if vocab is None:
self._ensure_algebra_match(other)
other_pointer = other.evaluate()
a, b = self.v, other_pointer.v
if swap:
a, b = b, a
return SemanticPointer(
data=self.algebra.bind(a, b),
vocab=vocab,
algebra=self.algebra,
name=self._get_binary_name(other_pointer, "*", swap),
)
[docs] def get_binding_matrix(self, swap_inputs=False):
"""Return the matrix that does a binding with this vector.
This should be such that
``A*B == dot(A.get_binding_matrix(), B.v)``.
"""
return self.algebra.get_binding_matrix(self.v, swap_inputs=swap_inputs)
[docs] def dot(self, other):
"""Return the dot product of the two vectors."""
if isinstance(other, Fixed):
infer_types(self, other)
other = other.evaluate().v
if is_array_like(other):
return np.dot(self.v, other)
else:
return other.dot(self)
def __matmul__(self, other):
return self.dot(other)
[docs] def compare(self, other):
"""Return the similarity between two SemanticPointers.
This is the normalized dot product, or (equivalently), the cosine of
the angle between the two vectors.
"""
if isinstance(other, SemanticPointer):
infer_types(self, other)
other = other.evaluate().v
scale = np.linalg.norm(self.v) * np.linalg.norm(other)
if scale == 0:
return 0
return np.dot(self.v, other) / scale
[docs] def reinterpret(self, vocab):
"""Reinterpret the Semantic Pointer as part of vocabulary *vocab*.
The *vocab* parameter can be set to *None* to clear the associated
vocabulary and allow the *source* to be interpreted as part of the
vocabulary of any Semantic Pointer it is combined with.
"""
return SemanticPointer(self.v, vocab=vocab, name=self.name)
[docs] def translate(self, vocab, populate=None, keys=None, solver=None):
"""Translate the Semantic Pointer to vocabulary *vocab*.
The translation of a Semantic Pointer uses some form of projection to
convert the Semantic Pointer to a Semantic Pointer of another
vocabulary. By default the outer products of terms in the source and
target vocabulary are used, but if *solver* is given, it is used to
find a least squares solution for this projection.
Parameters
----------
vocab : Vocabulary
Target vocabulary.
populate : bool, optional
Whether the target vocabulary should be populated with missing
keys. This is done by default, but with a warning. Set this
explicitly to *True* or *False* to silence the warning or raise an
error.
keys : list, optional
All keys to translate. If *None*, all keys in the source vocabulary
will be translated.
solver : nengo.Solver, optional
If given, the solver will be used to solve the least squares
problem to provide a better projection for the translation.
"""
tr = self.vocab.transform_to(vocab, populate=populate, keys=keys, solver=solver)
return SemanticPointer(
np.dot(tr, self.evaluate().v), vocab=vocab, name=self.name
)
[docs] def distance(self, other):
"""Return a distance measure between the vectors.
This is ``1-cos(angle)``, so that it is 0 when they are identical, and
the distance gets larger as the vectors are farther apart.
"""
return 1 - self.compare(other)
[docs] def mse(self, other):
"""Return the mean-squared-error between two vectors."""
if isinstance(other, SemanticPointer):
infer_types(self, other)
other = other.evaluate().v
return np.sum((self.v - other) ** 2) / len(self.v)
def _ensure_algebra_match(self, other):
"""Check the algebra of the *other*.
If the *other* parameter is a `SemanticPointer` and uses a different
algebra, a `TypeError` will be raised.
"""
if isinstance(other, SemanticPointer):
if self.algebra is not other.algebra:
raise TypeError(
"Operation not supported for SemanticPointer with "
"different algebra."
)
[docs]class SemanticPointerSign(AbstractSign):
"""Sign of a Semantic Pointer.
This class acts as proxy to the actual sign instance of the underlying
algebra. Use the *sign* attribute if you want to perform equality checks
with other signs.
Parameters
----------
sign : AbstractSign
Actual underlying sign.
algebra : AbstractAlgebra, optional
The underlying algebra of the Semantic Pointer for which the sign is
represented.
dimensions : int, optional
Number of dimensions of the Semantic Pointer for which the sign is
represented. If not given, *vocab* must be given.
vocab : Vocabulary, optional
Vocabulary of the Semantic Pointer for which the sign is represented.
If not given, *dimensions* must be given. If *dimensions* and *vocab*
are given, they must agree on the dimensionality.
name : str or Node, optional
Name of the Semantic Pointer including the invoked sign operation.
Attributes
----------
sign : AbstractSign
Actual underlying sign
algebra : AbstractAlgebra or None
The underlying algebra of the Semantic Pointer for which the sign is
represented.
dimensions : int
Number of dimensions of the Semantic Pointer for which the sign is
represented.
vocab : Vocabulary or None
Vocabulary of the Semantic Pointer for which the sign is represented.
"""
def __init__(self, sign, *, algebra=None, dimensions=None, vocab=None, name=None):
self.sign = sign
self.algebra = algebra
self.dimensions = dimensions
self.vocab = vocab
if name is not None and not isinstance(name, Node):
name = Leaf(name)
self._expr_tree = name
if self.dimensions is None:
self.dimensions = self.vocab.dimensions
if self.vocab is not None and self.vocab.dimensions != self.dimensions:
raise ValueError(
"dimensions must match vocab.dimensions if both are given."
)
[docs] def is_positive(self):
return self.sign.is_positive()
[docs] def is_negative(self):
return self.sign.is_negative()
[docs] def is_zero(self):
return self.sign.is_zero()
[docs] def is_indefinite(self):
return self.sign.is_indefinite()
[docs] def to_vector(self, d):
return self.sign.to_vector(d)
def __repr__(self):
return (
f"SemanticPointerSign({self.sign!r}, "
f"algebra={self.algebra!r}, "
f"dimensions={self.dimensions!r}, "
f"vocab={self.vocab!r}, "
f"name={self._expr_tree!r})"
)
def __eq__(self, other):
raise NotImplementedError(
"Do not check SemanticPointerSign instances for equality. "
"Instead use the underlying `sign` attribute."
)
[docs] def to_semantic_pointer(self):
"""Return the Semantic Pointer corresponding to the represented sign.
Returns
-------
SemanticPointer
Semantic Pointer corresponding to the represented sign.
"""
return SemanticPointer(
self.to_vector(self.dimensions),
algebra=self.algebra,
vocab=self.vocab,
name=FunctionCall(
tuple(), AttributeAccess("to_semantic_pointer", self._expr_tree)
)
if self._expr_tree is not None
else None,
)
[docs]class Identity(SemanticPointer):
"""Identity element.
Parameters
----------
n_dimensions : int
Dimensionality of the identity vector.
vocab : Vocabulary, optional
Vocabulary that the Semantic Pointer is considered to be part of.
Mutually exclusive with the *algebra* argument.
algebra : AbstractAlgebra, optional
Algebra used to perform vector symbolic operations on the Semantic
Pointer. Defaults to `.HrrAlgebra`. Mutually exclusive
with the *vocab* argument.
sidedness : ElementSidedness, optional
Side in the binding operation on which the element acts as identity.
"""
def __init__(
self,
n_dimensions,
vocab=None,
algebra=None,
*,
sidedness=ElementSidedness.TWO_SIDED,
):
data = self._get_algebra(vocab, algebra).identity_element(
n_dimensions, sidedness=sidedness
)
super(Identity, self).__init__(
data, vocab=vocab, algebra=algebra, name="Identity"
)
[docs]class NegativeIdentity(SemanticPointer):
"""Negative identity element.
Parameters
----------
n_dimensions : int
Dimensionality of the negative identity vector.
vocab : Vocabulary, optional
Vocabulary that the Semantic Pointer is considered to be part of.
Mutually exclusive with the *algebra* argument.
algebra : AbstractAlgebra, optional
Algebra used to perform vector symbolic operations on the Semantic
Pointer. Defaults to `.HrrAlgebra`. Mutually exclusive
with the *vocab* argument.
sidedness : ElementSidedness, optional
Side in the binding operation on which the element acts as identity.
"""
def __init__(
self,
n_dimensions,
vocab=None,
algebra=None,
*,
sidedness=ElementSidedness.TWO_SIDED,
):
data = self._get_algebra(vocab, algebra).negative_identity_element(
n_dimensions, sidedness=sidedness
)
super(NegativeIdentity, self).__init__(
data, vocab=vocab, algebra=algebra, name="NegativeIdentity"
)
[docs]class AbsorbingElement(SemanticPointer):
r"""Absorbing element.
If :math:`z` denotes the absorbing element, :math:`v \circledast z = c z`,
where :math:`v` is a Semantic Pointer and :math:`c` is a real-valued
scalar. Furthermore :math:`\|z\| = 1`.
Parameters
----------
n_dimensions : int
Dimensionality of the identity vector.
vocab : Vocabulary, optional
Vocabulary that the Semantic Pointer is considered to be part of.
Mutually exclusive with the *algebra* argument.
algebra : AbstractAlgebra, optional
Algebra used to perform vector symbolic operations on the Semantic
Pointer. Defaults to `.HrrAlgebra`. Mutually exclusive with the *vocab*
argument.
sidedness : ElementSidedness, optional
Side in the binding operation on which the element acts as absorbing element.
"""
def __init__(
self,
n_dimensions,
vocab=None,
algebra=None,
*,
sidedness=ElementSidedness.TWO_SIDED,
):
data = self._get_algebra(vocab, algebra).absorbing_element(
n_dimensions, sidedness=sidedness
)
super(AbsorbingElement, self).__init__(
data, vocab=vocab, algebra=algebra, name="AbsorbingElement"
)
[docs]class Zero(SemanticPointer):
"""Zero element.
Parameters
----------
n_dimensions : int
Dimensionality of the identity vector.
vocab : Vocabulary, optional
Vocabulary that the Semantic Pointer is considered to be part of.
Mutually exclusive with the *algebra* argument.
algebra : AbstractAlgebra, optional
Algebra used to perform vector symbolic operations on the Semantic
Pointer. Defaults to `.HrrAlgebra`. Mutually exclusive with the *vocab*
argument.
sidedness : ElementSidedness, optional
Side in the binding operation on which the element acts as zero element.
"""
def __init__(
self,
n_dimensions,
vocab=None,
algebra=None,
sidedness=ElementSidedness.TWO_SIDED,
):
data = self._get_algebra(vocab, algebra).zero_element(
n_dimensions, sidedness=sidedness
)
super(Zero, self).__init__(data, vocab=vocab, algebra=algebra, name="Zero")