"""MAP (Multiply-Add-Permute) VSA model.
MAP uses element-wise multiplication for binding, which is self-inverse.
It's one of the simplest VSA models and works with both bipolar {-1, +1}
and continuous real values.
Properties:
- Self-inverse: bind(a, b) = unbind(a, b)
- Commutative: bind(a, b) = bind(b, a)
- Exact inverse: unbind(bind(a, b), b) = a (for bipolar)
- Simple hardware: Only XOR for bipolar, multiply for continuous
References:
- Kanerva (2009): "Hyperdimensional Computing: An Introduction"
- Schlegel et al. (2021): Comparison of VSA models
"""
from __future__ import annotations
from typing import Optional, Sequence
import numpy as np
from ..backends import Backend
from ..backends.base import Array
from ..spaces import BipolarSpace, VectorSpace
from .base import VSAModel
[docs]
class MAPModel(VSAModel):
"""MAP (Multiply-Add-Permute) model.
Binding: element-wise multiplication
Unbinding: element-wise multiplication (self-inverse)
Bundling: element-wise addition + normalization
Permutation: circular shift
Best used with BipolarSpace or RealSpace.
"""
[docs]
def __init__(
self,
dimension: int = 10000,
space: Optional[VectorSpace] = None,
backend: Optional[Backend] = None,
seed: Optional[int] = None
):
"""Initialize MAP model.
Args:
dimension: Dimensionality of hypervectors
space: Vector space (defaults to BipolarSpace)
backend: Computational backend
seed: Random seed for space
"""
if space is None:
from ..backends import get_backend
backend = backend if backend is not None else get_backend()
space = BipolarSpace(dimension, backend=backend, seed=seed)
super().__init__(space, backend)
# Pre-compute permutation indices for efficiency
self._permutation_indices = list(range(self.dimension))
@property
def model_name(self) -> str:
return "MAP"
@property
def is_self_inverse(self) -> bool:
return True
@property
def is_commutative(self) -> bool:
return True
@property
def is_exact_inverse(self) -> bool:
# Exact for bipolar, approximate for continuous
return self.space.space_name == "bipolar"
[docs]
def bind(self, a: Array, b: Array) -> Array:
"""Bind using element-wise multiplication.
For bipolar: XOR when represented as {0,1}
For real: Hadamard product
Args:
a: First vector
b: Second vector
Returns:
Bound vector c = a ⊙ b
"""
result = self.backend.multiply(a, b)
# Normalize to maintain unit norm for continuous spaces
if self.space.space_name != "bipolar":
result = self.normalize(result)
return result
[docs]
def unbind(self, a: Array, b: Array) -> Array:
"""Unbind using element-wise multiplication (self-inverse).
Since binding is self-inverse: unbind(c, b) = c ⊙ b
Args:
a: Bound vector (or first operand)
b: Second operand
Returns:
Unbound vector (exact for bipolar, approximate for continuous)
"""
# For MAP, binding = unbinding
return self.bind(a, b)
[docs]
def bundle(self, vectors: Sequence[Array]) -> Array:
"""Bundle using element-wise addition.
For bipolar: majority vote after summing
For real: sum and normalize
Args:
vectors: Sequence of vectors to bundle
Returns:
Bundled vector
Raises:
ValueError: If vectors is empty
"""
if not vectors:
raise ValueError("Cannot bundle empty sequence")
vectors = list(vectors)
# Sum all vectors
result = self.backend.sum(self.backend.stack(vectors, axis=0), axis=0)
# Normalize according to space
if self.space.space_name == "bipolar":
# Majority vote: sign of sum
result = self.backend.sign(result)
# Handle zeros (shouldn't happen in practice, but be safe)
# If sum is 0, randomly choose ±1
zeros_mask = (result == 0)
if self.backend.to_numpy(zeros_mask).any():
# For any zeros, use the first vector's value
first_vec = vectors[0]
result = self.backend.where(zeros_mask, first_vec, result)
else:
# For continuous spaces, L2 normalize
result = self.normalize(result)
return result
[docs]
def permute(self, vec: Array, k: int = 1) -> Array:
"""Permute using circular shift.
Shifts vector elements by k positions to the right.
Negative k shifts left.
Args:
vec: Vector to permute
k: Number of positions to shift
Returns:
Permuted vector
"""
return self.backend.roll(vec, shift=k)
def __repr__(self) -> str:
return (f"MAPModel(dimension={self.dimension}, "
f"space={self.space.space_name}, "
f"backend={self.backend.name})")