Skip to content

BinaryOperations

XOR and majority voting operations for binary hypervectors.

vsax.ops.BinaryOperations

Bases: AbstractOpSet

Binary VSA operations for bipolar {-1, +1} vectors.

Binary VSA uses: - Binding: XOR (element-wise multiplication in bipolar representation) - Bundling: Majority vote - Inverse: Self-inverse (XOR is its own inverse)

This algebra is particularly efficient for hardware implementation and provides exact unbinding (unlike MAP).

Note: Operations assume bipolar {-1, +1} encoding. For {0, 1} encoding, convert to bipolar first.

Example

import jax import jax.numpy as jnp

ops = BinaryOperations() key = jax.random.PRNGKey(0) a = jax.random.choice(key, jnp.array([-1, 1]), shape=(1024,)) b = jax.random.choice(key, jnp.array([-1, 1]), shape=(1024,))

bound = ops.bind(a, b) assert jnp.all(jnp.isin(bound, jnp.array([-1, 1])))

Source code in vsax/ops/binary.py
class BinaryOperations(AbstractOpSet):
    """Binary VSA operations for bipolar {-1, +1} vectors.

    Binary VSA uses:
    - Binding: XOR (element-wise multiplication in bipolar representation)
    - Bundling: Majority vote
    - Inverse: Self-inverse (XOR is its own inverse)

    This algebra is particularly efficient for hardware implementation and
    provides exact unbinding (unlike MAP).

    Note: Operations assume bipolar {-1, +1} encoding. For {0, 1} encoding,
    convert to bipolar first.

    Example:
        >>> import jax
        >>> import jax.numpy as jnp
        >>>
        >>> ops = BinaryOperations()
        >>> key = jax.random.PRNGKey(0)
        >>> a = jax.random.choice(key, jnp.array([-1, 1]), shape=(1024,))
        >>> b = jax.random.choice(key, jnp.array([-1, 1]), shape=(1024,))
        >>>
        >>> bound = ops.bind(a, b)
        >>> assert jnp.all(jnp.isin(bound, jnp.array([-1, 1])))
    """

    def bind(self, a: jnp.ndarray, b: jnp.ndarray) -> jnp.ndarray:
        """Bind two hypervectors using XOR (element-wise multiplication).

        In bipolar {-1, +1} representation, XOR is implemented as
        element-wise multiplication:
        - (+1) XOR (+1) = +1 (same)
        - (+1) XOR (-1) = -1 (different)
        - (-1) XOR (+1) = -1 (different)
        - (-1) XOR (-1) = +1 (same)

        This operation is:
        - Commutative: bind(a, b) = bind(b, a)
        - Associative: bind(a, bind(b, c)) = bind(bind(a, b), c)
        - Self-inverse: bind(bind(a, b), b) = a (exact unbinding)

        Args:
            a: First hypervector as JAX array (bipolar values).
            b: Second hypervector as JAX array (bipolar values).

        Returns:
            Bound hypervector as JAX array (bipolar values).

        Example:
            >>> import jax.numpy as jnp
            >>> ops = BinaryOperations()
            >>> a = jnp.array([1, -1, 1, -1])
            >>> b = jnp.array([1, 1, -1, -1])
            >>> result = ops.bind(a, b)
            >>> expected = jnp.array([1, -1, -1, 1])
            >>> assert jnp.array_equal(result, expected)
        """
        # XOR in bipolar is multiplication
        return a * b

    def bundle(self, *vecs: jnp.ndarray) -> jnp.ndarray:
        """Bundle multiple hypervectors using majority vote.

        Each element in the bundled vector is determined by the majority
        value at that position across all input vectors.

        For even counts, ties are broken by the sign of the sum.

        Args:
            *vecs: Variable number of hypervectors as JAX arrays (bipolar values).

        Returns:
            Bundled hypervector as JAX array (bipolar values).

        Raises:
            ValueError: If no vectors are provided.

        Example:
            >>> import jax.numpy as jnp
            >>> ops = BinaryOperations()
            >>> a = jnp.array([1, -1, 1, -1])
            >>> b = jnp.array([1, 1, -1, -1])
            >>> c = jnp.array([1, 1, 1, 1])
            >>> result = ops.bundle(a, b, c)
            >>> expected = jnp.array([1, 1, 1, -1])  # Majority at each position
            >>> assert jnp.array_equal(result, expected)
        """
        if len(vecs) == 0:
            raise ValueError("bundle() requires at least one vector")

        # Stack all vectors
        stacked = jnp.stack(vecs)

        # Sum across vectors (majority has positive/negative sum)
        summed = jnp.sum(stacked, axis=0)

        # Convert to bipolar: positive sum -> +1, negative sum -> -1
        # Use sign function (0 maps to 0, but we'll handle that)
        result = jnp.sign(summed)

        # Handle zeros (ties) by defaulting to +1
        result = jnp.where(result == 0, 1, result)

        return result.astype(jnp.int32)

    def inverse(self, a: jnp.ndarray) -> jnp.ndarray:
        """Compute the inverse for unbinding.

        For binary XOR, the inverse is the vector itself (self-inverse property).
        This means: bind(bind(a, b), b) = a (exact unbinding).

        Args:
            a: Hypervector as JAX array (bipolar values).

        Returns:
            Inverse hypervector (same as input for XOR).

        Example:
            >>> import jax.numpy as jnp
            >>> ops = BinaryOperations()
            >>> a = jnp.array([1, -1, 1, -1])
            >>> inv_a = ops.inverse(a)
            >>> assert jnp.array_equal(inv_a, a)
        """
        # XOR is self-inverse
        return a

    def unbind(self, a: jnp.ndarray, b: jnp.ndarray) -> jnp.ndarray:
        """Unbind b from a using XOR (self-inverse property).

        Since XOR is self-inverse in binary VSA, unbinding is identical to binding:
            unbind(a, b) = bind(a, b) = a XOR b

        This provides exact unbinding: if c = bind(x, y), then unbind(c, y) = x.

        Args:
            a: Bound hypervector as JAX array (bipolar values).
            b: Hypervector to unbind as JAX array (bipolar values).

        Returns:
            Recovered hypervector as JAX array (exact recovery for Binary VSA).

        Example:
            >>> import jax.numpy as jnp
            >>> ops = BinaryOperations()
            >>> x = jnp.array([1, -1, 1, -1])
            >>> y = jnp.array([1, 1, -1, -1])
            >>>
            >>> # Bind and unbind
            >>> bound = ops.bind(x, y)
            >>> recovered = ops.unbind(bound, y)
            >>>
            >>> # Exact recovery
            >>> assert jnp.array_equal(recovered, x)
        """
        # XOR is self-inverse, so unbind = bind
        return self.bind(a, b)

    def permute(self, a: jnp.ndarray, shift: int) -> jnp.ndarray:
        """Permute a hypervector by circular rotation.

        Args:
            a: Hypervector as JAX array (bipolar values).
            shift: Number of positions to rotate (positive = right, negative = left).

        Returns:
            Permuted hypervector as JAX array.

        Example:
            >>> import jax.numpy as jnp
            >>> ops = BinaryOperations()
            >>> a = jnp.array([1, -1, 1, -1])
            >>> rotated = ops.permute(a, 1)
            >>> expected = jnp.array([-1, 1, -1, 1])
            >>> assert jnp.array_equal(rotated, expected)
        """
        return jnp.roll(a, shift)

Functions

bind(a, b)

Bind two hypervectors using XOR (element-wise multiplication).

In bipolar {-1, +1} representation, XOR is implemented as element-wise multiplication: - (+1) XOR (+1) = +1 (same) - (+1) XOR (-1) = -1 (different) - (-1) XOR (+1) = -1 (different) - (-1) XOR (-1) = +1 (same)

This operation is: - Commutative: bind(a, b) = bind(b, a) - Associative: bind(a, bind(b, c)) = bind(bind(a, b), c) - Self-inverse: bind(bind(a, b), b) = a (exact unbinding)

Parameters:

Name Type Description Default
a ndarray

First hypervector as JAX array (bipolar values).

required
b ndarray

Second hypervector as JAX array (bipolar values).

required

Returns:

Type Description
ndarray

Bound hypervector as JAX array (bipolar values).

Example

import jax.numpy as jnp ops = BinaryOperations() a = jnp.array([1, -1, 1, -1]) b = jnp.array([1, 1, -1, -1]) result = ops.bind(a, b) expected = jnp.array([1, -1, -1, 1]) assert jnp.array_equal(result, expected)

Source code in vsax/ops/binary.py
def bind(self, a: jnp.ndarray, b: jnp.ndarray) -> jnp.ndarray:
    """Bind two hypervectors using XOR (element-wise multiplication).

    In bipolar {-1, +1} representation, XOR is implemented as
    element-wise multiplication:
    - (+1) XOR (+1) = +1 (same)
    - (+1) XOR (-1) = -1 (different)
    - (-1) XOR (+1) = -1 (different)
    - (-1) XOR (-1) = +1 (same)

    This operation is:
    - Commutative: bind(a, b) = bind(b, a)
    - Associative: bind(a, bind(b, c)) = bind(bind(a, b), c)
    - Self-inverse: bind(bind(a, b), b) = a (exact unbinding)

    Args:
        a: First hypervector as JAX array (bipolar values).
        b: Second hypervector as JAX array (bipolar values).

    Returns:
        Bound hypervector as JAX array (bipolar values).

    Example:
        >>> import jax.numpy as jnp
        >>> ops = BinaryOperations()
        >>> a = jnp.array([1, -1, 1, -1])
        >>> b = jnp.array([1, 1, -1, -1])
        >>> result = ops.bind(a, b)
        >>> expected = jnp.array([1, -1, -1, 1])
        >>> assert jnp.array_equal(result, expected)
    """
    # XOR in bipolar is multiplication
    return a * b

bundle(*vecs)

Bundle multiple hypervectors using majority vote.

Each element in the bundled vector is determined by the majority value at that position across all input vectors.

For even counts, ties are broken by the sign of the sum.

Parameters:

Name Type Description Default
*vecs ndarray

Variable number of hypervectors as JAX arrays (bipolar values).

()

Returns:

Type Description
ndarray

Bundled hypervector as JAX array (bipolar values).

Raises:

Type Description
ValueError

If no vectors are provided.

Example

import jax.numpy as jnp ops = BinaryOperations() a = jnp.array([1, -1, 1, -1]) b = jnp.array([1, 1, -1, -1]) c = jnp.array([1, 1, 1, 1]) result = ops.bundle(a, b, c) expected = jnp.array([1, 1, 1, -1]) # Majority at each position assert jnp.array_equal(result, expected)

Source code in vsax/ops/binary.py
def bundle(self, *vecs: jnp.ndarray) -> jnp.ndarray:
    """Bundle multiple hypervectors using majority vote.

    Each element in the bundled vector is determined by the majority
    value at that position across all input vectors.

    For even counts, ties are broken by the sign of the sum.

    Args:
        *vecs: Variable number of hypervectors as JAX arrays (bipolar values).

    Returns:
        Bundled hypervector as JAX array (bipolar values).

    Raises:
        ValueError: If no vectors are provided.

    Example:
        >>> import jax.numpy as jnp
        >>> ops = BinaryOperations()
        >>> a = jnp.array([1, -1, 1, -1])
        >>> b = jnp.array([1, 1, -1, -1])
        >>> c = jnp.array([1, 1, 1, 1])
        >>> result = ops.bundle(a, b, c)
        >>> expected = jnp.array([1, 1, 1, -1])  # Majority at each position
        >>> assert jnp.array_equal(result, expected)
    """
    if len(vecs) == 0:
        raise ValueError("bundle() requires at least one vector")

    # Stack all vectors
    stacked = jnp.stack(vecs)

    # Sum across vectors (majority has positive/negative sum)
    summed = jnp.sum(stacked, axis=0)

    # Convert to bipolar: positive sum -> +1, negative sum -> -1
    # Use sign function (0 maps to 0, but we'll handle that)
    result = jnp.sign(summed)

    # Handle zeros (ties) by defaulting to +1
    result = jnp.where(result == 0, 1, result)

    return result.astype(jnp.int32)

inverse(a)

Compute the inverse for unbinding.

For binary XOR, the inverse is the vector itself (self-inverse property). This means: bind(bind(a, b), b) = a (exact unbinding).

Parameters:

Name Type Description Default
a ndarray

Hypervector as JAX array (bipolar values).

required

Returns:

Type Description
ndarray

Inverse hypervector (same as input for XOR).

Example

import jax.numpy as jnp ops = BinaryOperations() a = jnp.array([1, -1, 1, -1]) inv_a = ops.inverse(a) assert jnp.array_equal(inv_a, a)

Source code in vsax/ops/binary.py
def inverse(self, a: jnp.ndarray) -> jnp.ndarray:
    """Compute the inverse for unbinding.

    For binary XOR, the inverse is the vector itself (self-inverse property).
    This means: bind(bind(a, b), b) = a (exact unbinding).

    Args:
        a: Hypervector as JAX array (bipolar values).

    Returns:
        Inverse hypervector (same as input for XOR).

    Example:
        >>> import jax.numpy as jnp
        >>> ops = BinaryOperations()
        >>> a = jnp.array([1, -1, 1, -1])
        >>> inv_a = ops.inverse(a)
        >>> assert jnp.array_equal(inv_a, a)
    """
    # XOR is self-inverse
    return a

unbind(a, b)

Unbind b from a using XOR (self-inverse property).

Since XOR is self-inverse in binary VSA, unbinding is identical to binding: unbind(a, b) = bind(a, b) = a XOR b

This provides exact unbinding: if c = bind(x, y), then unbind(c, y) = x.

Parameters:

Name Type Description Default
a ndarray

Bound hypervector as JAX array (bipolar values).

required
b ndarray

Hypervector to unbind as JAX array (bipolar values).

required

Returns:

Type Description
ndarray

Recovered hypervector as JAX array (exact recovery for Binary VSA).

Example

import jax.numpy as jnp ops = BinaryOperations() x = jnp.array([1, -1, 1, -1]) y = jnp.array([1, 1, -1, -1])

Bind and unbind

bound = ops.bind(x, y) recovered = ops.unbind(bound, y)

Exact recovery

assert jnp.array_equal(recovered, x)

Source code in vsax/ops/binary.py
def unbind(self, a: jnp.ndarray, b: jnp.ndarray) -> jnp.ndarray:
    """Unbind b from a using XOR (self-inverse property).

    Since XOR is self-inverse in binary VSA, unbinding is identical to binding:
        unbind(a, b) = bind(a, b) = a XOR b

    This provides exact unbinding: if c = bind(x, y), then unbind(c, y) = x.

    Args:
        a: Bound hypervector as JAX array (bipolar values).
        b: Hypervector to unbind as JAX array (bipolar values).

    Returns:
        Recovered hypervector as JAX array (exact recovery for Binary VSA).

    Example:
        >>> import jax.numpy as jnp
        >>> ops = BinaryOperations()
        >>> x = jnp.array([1, -1, 1, -1])
        >>> y = jnp.array([1, 1, -1, -1])
        >>>
        >>> # Bind and unbind
        >>> bound = ops.bind(x, y)
        >>> recovered = ops.unbind(bound, y)
        >>>
        >>> # Exact recovery
        >>> assert jnp.array_equal(recovered, x)
    """
    # XOR is self-inverse, so unbind = bind
    return self.bind(a, b)

permute(a, shift)

Permute a hypervector by circular rotation.

Parameters:

Name Type Description Default
a ndarray

Hypervector as JAX array (bipolar values).

required
shift int

Number of positions to rotate (positive = right, negative = left).

required

Returns:

Type Description
ndarray

Permuted hypervector as JAX array.

Example

import jax.numpy as jnp ops = BinaryOperations() a = jnp.array([1, -1, 1, -1]) rotated = ops.permute(a, 1) expected = jnp.array([-1, 1, -1, 1]) assert jnp.array_equal(rotated, expected)

Source code in vsax/ops/binary.py
def permute(self, a: jnp.ndarray, shift: int) -> jnp.ndarray:
    """Permute a hypervector by circular rotation.

    Args:
        a: Hypervector as JAX array (bipolar values).
        shift: Number of positions to rotate (positive = right, negative = left).

    Returns:
        Permuted hypervector as JAX array.

    Example:
        >>> import jax.numpy as jnp
        >>> ops = BinaryOperations()
        >>> a = jnp.array([1, -1, 1, -1])
        >>> rotated = ops.permute(a, 1)
        >>> expected = jnp.array([-1, 1, -1, 1])
        >>> assert jnp.array_equal(rotated, expected)
    """
    return jnp.roll(a, shift)