#!/usr/bin/env python
"""
Multiply-Add-Permute Encodings for HDC
HDC-compatible wrapper for MAP encodings.
"""
from functools import partial
from typing import Any, Callable, Optional
import numpy as np
from pyhdc.components.binding import ElementMultiplication
from pyhdc.components.bundling import (
ElementAddition,
ElementAdditionBipolarThreshold,
ElementAdditionBits,
ElementAdditionCut,
)
from pyhdc.components.elements import BernoulliBipolar, UniformBipolar
from pyhdc.components.similarity import CosineSimilarity
from pyhdc.components.thinning import NoThin
from pyhdc.components.unary import IdentityInverse, Negate, SignNormalize
from pyhdc.encodings.base import Encoding
from pyhdc.generation.base import HDCGenerator
from pyhdc.hypervector import EncodingSpec
from pyhdc.types import Backend, Device
# ============================================================================
# Multiply-Add-Permute Encodings
# ============================================================================
[docs]
class MAP_C(Encoding):
r"""
Multiply-Add-Permute encoding with continuous values.
Uses bipolar values with element-wise multiplication for binding
and cosine similarity for comparison.
Args:
random_choice_range: Optional float (rho). When set, coordinates whose
\|pre-aggregate sum\| <= rho * sqrt(N/3) are replaced by independent
Uniform[-1,1] draws during bundling (band randomization).
"""
def __init__(
self,
dimension: int = 10_000,
backend: Optional[Backend] = None,
device: Optional[Device] = None,
dtype: Optional[Any] = None,
mask: Optional[int] = None,
generator: Optional[HDCGenerator] = None,
similarity_remap: Optional[Callable] = None,
random_choice_range: Optional[float] = None,
) -> None:
self._random_choice_range = random_choice_range
super().__init__(
dimension, backend, device, dtype, mask, generator, similarity_remap
)
def _get_encoding_spec(self) -> EncodingSpec:
if self._random_choice_range is not None:
bundling_fn = partial(
ElementAdditionCut, random_choice_range=self._random_choice_range
)
else:
bundling_fn = ElementAdditionCut
return EncodingSpec(
dtype=np.float32,
element_generator=UniformBipolar,
similarity_fn=CosineSimilarity,
bundling_fn=bundling_fn,
thinning_fn=NoThin,
binding_fn=ElementMultiplication,
unbinding_fn=ElementMultiplication,
generator_output_type="floats",
normalize_fn=SignNormalize,
negative_fn=Negate,
)
[docs]
class MAP_I(Encoding):
r"""
Multiply-Add-Permute encoding with integer values.
Uses bipolar integer values with element-wise multiplication for binding
and cosine similarity for comparison.
Args:
random_choice_range: Optional float (rho). When set, coordinates whose
\|pre-aggregate sum\| <= rho * sqrt(N) are replaced by independent
{-1, +1} draws during bundling (band randomization).
"""
def __init__(
self,
dimension: int = 10_000,
backend: Optional[Backend] = None,
device: Optional[Device] = None,
dtype: Optional[Any] = None,
mask: Optional[int] = None,
generator: Optional[HDCGenerator] = None,
similarity_remap: Optional[Callable] = None,
random_choice_range: Optional[float] = None,
) -> None:
self._random_choice_range = random_choice_range
super().__init__(
dimension, backend, device, dtype, mask, generator, similarity_remap
)
def _get_encoding_spec(self) -> EncodingSpec:
if self._random_choice_range is not None:
bundling_fn = partial(
ElementAddition, random_choice_range=self._random_choice_range
)
else:
bundling_fn = ElementAddition
return EncodingSpec(
dtype=np.int32,
element_generator=BernoulliBipolar,
similarity_fn=CosineSimilarity,
bundling_fn=bundling_fn,
thinning_fn=NoThin,
binding_fn=ElementMultiplication,
unbinding_fn=ElementMultiplication,
generator_output_type="bits",
inverse_fn=IdentityInverse,
normalize_fn=SignNormalize,
negative_fn=Negate,
)
[docs]
class MAP_I_Bits(Encoding):
"""
Multiply-Add-Permute encoding with bit-limited integer values.
Similar to MAP_I but with configurable bit limits via mask parameter.
"""
def __init__(
self,
dimension: int = 10_000,
backend: Optional[Backend] = None,
device: Optional[Device] = None,
dtype: Optional[Any] = None,
mask: int = (2**32) - 1,
generator: Optional[HDCGenerator] = None,
similarity_remap: Optional[Callable] = None,
) -> None:
self._mask = mask
super().__init__(
dimension, backend, device, dtype, mask, generator, similarity_remap
)
def _get_encoding_spec(self) -> EncodingSpec:
bundling_fn = partial(
ElementAdditionBits,
min_val=np.iinfo(np.int32).min,
max_val=np.iinfo(np.int32).max,
)
return EncodingSpec(
dtype=np.int32,
element_generator=BernoulliBipolar,
similarity_fn=CosineSimilarity,
bundling_fn=bundling_fn,
thinning_fn=NoThin,
binding_fn=ElementMultiplication,
unbinding_fn=ElementMultiplication,
mask=self._mask,
generator_output_type="words",
inverse_fn=IdentityInverse,
normalize_fn=SignNormalize,
negative_fn=Negate,
)
[docs]
class MAP_B(Encoding):
r"""
Multiply-Add-Permute with bipolar thresholding.
Uses bipolar values with thresholding during bundling.
Args:
random_choice_range: Optional float (rho). When set, coordinates whose
\|bipolar sum\| <= rho * sqrt(N) are replaced by independent {-1, +1}
draws during bundling (band randomization).
"""
def __init__(
self,
dimension: int = 10_000,
backend: Optional[Backend] = None,
device: Optional[Device] = None,
dtype: Optional[Any] = None,
mask: Optional[int] = None,
generator: Optional[HDCGenerator] = None,
similarity_remap: Optional[Callable] = None,
random_choice_range: Optional[float] = None,
) -> None:
self._random_choice_range = random_choice_range
super().__init__(
dimension, backend, device, dtype, mask, generator, similarity_remap
)
def _get_encoding_spec(self) -> EncodingSpec:
if self._random_choice_range is not None:
bundling_fn = partial(
ElementAdditionBipolarThreshold,
random_choice_range=self._random_choice_range,
)
else:
bundling_fn = ElementAdditionBipolarThreshold
return EncodingSpec(
dtype=np.int8,
element_generator=BernoulliBipolar,
similarity_fn=CosineSimilarity,
bundling_fn=bundling_fn,
thinning_fn=NoThin,
binding_fn=ElementMultiplication,
unbinding_fn=ElementMultiplication,
generator_output_type="bits",
inverse_fn=IdentityInverse,
normalize_fn=SignNormalize,
negative_fn=Negate,
)