Source code for pyhdc.components.bundling.addition

from typing import Any, Dict, Optional, Tuple, Union

import numpy as np

# Optional PyTorch support
try:
    import torch

    TORCH_AVAILABLE = True
except ImportError:
    TORCH_AVAILABLE = False
    torch = None

from pyhdc.components.input_formatting import (
    _normalize_bundling,
    _reduce_count,
    _zone_count,
)

# Type aliases
from pyhdc.types import ArrayLike

# ============================================================================
# Element Addition Operations
# ============================================================================


[docs] def ElementAddition( *hypervectors: ArrayLike, random_choice_range: Optional[float] = None, axis: Union[None, int, Tuple[int, ...]] = None, ) -> Tuple[ArrayLike, Dict[str, Any]]: r""" Element-wise addition bundling. Bundles hypervectors by summing corresponding elements. The simplest bundling operation that preserves information from all inputs. When random_choice_range is set, coordinates whose \|sum\| falls within rho * sqrt(N) are replaced by independent fair draws from {-1, +1} (band randomization for MAP_I bipolar integer encoding). Defaulting random_choice_range to 0.0 limits this to exact zero-sums only. Args: *hypervectors: Variable number of hypervectors to bundle, or single 2D batch random_choice_range: Optional float (rho). Coordinates with \|sum\| <= rho * sqrt(N) are randomly assigned. Defaults to 0.0 (zero-ties only). axis: Batch axis (or axes) to fold. Defaults to the last batch axis, so a ``(D, N)`` batch collapses to ``(D,)`` and a ``(D, N, M)`` batch to ``(D, N)``. Returns: Tuple of (bundled hypervector, metadata dict). Metadata contains "random_zone_count". Example: >>> v1 = np.array([1, -1, 1, -1]) >>> v2 = np.array([1, 1, -1, -1]) >>> result, _ = ElementAddition(v1, v2) >>> # result: [2, 0, 0, -2] """ batch, is_torch, _, reduce_axes = _normalize_bundling(*hypervectors, axis=axis) num_vectors = _reduce_count(batch, reduce_axes) if random_choice_range is None: random_choice_range = 0.0 threshold = random_choice_range * np.sqrt(num_vectors) if is_torch: assert torch is not None total = batch.sum(dim=reduce_axes) in_band = torch.abs(total) <= threshold random_zone_count = _zone_count(in_band, is_torch) random_vals = torch.where( torch.rand(total.shape, device=total.device) < 0.5, torch.full_like(total, -1.0), torch.full_like(total, 1.0), ) result = torch.where(in_band, random_vals, total) return result, {"random_zone_count": random_zone_count} else: total = batch.sum(axis=reduce_axes) in_band = np.abs(total) <= threshold random_zone_count = _zone_count(in_band, is_torch) random_vals = np.where(np.random.rand(*total.shape) < 0.5, -1, 1).astype( total.dtype ) result = np.where(in_band, random_vals, total) return result, {"random_zone_count": random_zone_count}
[docs] def ElementAdditionBits( *hypervectors: ArrayLike, min_val: int = -1, max_val: int = 1, axis: Union[None, int, Tuple[int, ...]] = None, ) -> ArrayLike: """ Element-wise addition with a single saturating clip at the end. Bundles by summing all vectors, then clipping the total once to ``[min_val, max_val]``. The sum is accumulated in a wide (int64) integer so it cannot overflow the element dtype before the clip, and the clip saturates at the bounds (no wraparound). Summation is order-independent, so a tuple of axes is supported. Args: *hypervectors: Variable number of hypervectors to bundle, or single batch min_val: Lower saturation bound applied once to the final sum max_val: Upper saturation bound applied once to the final sum axis: Batch axis (or axes) to fold (defaults to the last batch axis). Returns: Bundled hypervector, summed then clipped to ``[min_val, max_val]`` """ batch, is_torch, _, reduce_axes = _normalize_bundling(*hypervectors, axis=axis) if is_torch: total = batch.sum(dim=reduce_axes, dtype=torch.int64) return torch.clamp(total, min_val, max_val).to(batch.dtype) total = batch.sum(axis=reduce_axes, dtype=np.int64) return np.clip(total, min_val, max_val).astype(batch.dtype)
[docs] def ElementAdditionCut( *hypervectors: ArrayLike, min_val: float = -1.0, max_val: float = 1.0, random_choice_range: Optional[float] = None, axis: Union[None, int, Tuple[int, ...]] = None, ) -> Tuple[ArrayLike, Dict[str, Any]]: r""" Element-wise addition with clipping. Bundles hypervectors by summing elements and clipping the result to [min_val, max_val] range. Prevents unbounded growth in element values. When random_choice_range is set, coordinates whose \|sum\| falls within rho * sqrt(N) * element_std (where element_std = 1/sqrt(3) for Uniform[-1,1]) are replaced by independent fair draws from Uniform[min_val, max_val] (band randomization for MAP_C continuous encoding). Defaulting random_choice_range to 0.0 limits this to exact zero-sums only. Args: *hypervectors: Variable number of hypervectors to bundle, or single 2D batch min_val: Minimum element value after bundling max_val: Maximum element value after bundling random_choice_range: Optional float (rho). Coordinates with \|sum\| <= rho * sqrt(N/3) are randomly assigned. Defaults to 0.0 (zero-ties only). axis: Batch axis (or axes) to fold (defaults to the last batch axis). Returns: Tuple of (bundled and clipped hypervector, metadata dict). Metadata contains "random_zone_count". """ batch, is_torch, _, reduce_axes = _normalize_bundling(*hypervectors, axis=axis) num_vectors = _reduce_count(batch, reduce_axes) if random_choice_range is None: random_choice_range = 0.0 # sigma_N for Uniform[-1,1] elements: Var[X] = 1/3, so sigma_N = sqrt(N/3) threshold = random_choice_range * np.sqrt(num_vectors / 3.0) if is_torch: assert torch is not None total = batch.sum(dim=reduce_axes) in_band = torch.abs(total) <= threshold random_zone_count = _zone_count(in_band, is_torch) random_vals = ( torch.rand(total.shape, device=total.device) * (max_val - min_val) + min_val ) result = torch.where(in_band, random_vals, torch.clamp(total, min_val, max_val)) return result, {"random_zone_count": random_zone_count} else: total = batch.sum(axis=reduce_axes) in_band = np.abs(total) <= threshold random_zone_count = _zone_count(in_band, is_torch) random_vals = np.random.uniform(min_val, max_val, total.shape).astype( total.dtype ) result = np.where(in_band, random_vals, np.clip(total, min_val, max_val)) return result, {"random_zone_count": random_zone_count}