Source code for pyhdc.components.bundling.addition

from typing import Any, Dict, Optional, Tuple

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

# Type aliases
from pyhdc.types import ArrayLike

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


[docs] def ElementAddition( *hypervectors: ArrayLike, random_choice_range: Optional[float] = None ) -> Tuple[ArrayLike, Dict[str, Any]]: """ 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). 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, _ = _normalize_bundling(*hypervectors) num_vectors = batch.shape[1] 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=1) in_band = torch.abs(total) <= threshold random_zone_count = int(in_band.sum().item()) 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=1) in_band = np.abs(total) <= threshold random_zone_count = int(in_band.sum()) 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 ) -> ArrayLike: """ Element-wise addition with per-step clipping (bit-limited). Bundles hypervectors by iteratively adding and clipping after each addition. Useful for fixed-point or integer arithmetic where overflow must be prevented. Args: *hypervectors: Variable number of hypervectors to bundle, or single 2D batch min_val: Minimum element value (clipped after each addition) max_val: Maximum element value (clipped after each addition) Returns: Bundled hypervector with per-step clipping """ batch, is_torch, _ = _normalize_bundling(*hypervectors) num_vectors = batch.shape[1] if is_torch: result = torch.zeros_like(batch[:, 0]) for j in range(num_vectors): result = result + batch[:, j] result = torch.clamp(result, min_val, max_val) else: result = np.zeros_like(batch[:, 0]) for j in range(num_vectors): result = np.add(result, batch[:, j]) result = np.clip(result, min_val, max_val) return result
[docs] def ElementAdditionCut( *hypervectors: ArrayLike, min_val: float = -1.0, max_val: float = 1.0, random_choice_range: Optional[float] = None, ) -> Tuple[ArrayLike, Dict[str, Any]]: """ 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). Returns: Tuple of (bundled and clipped hypervector, metadata dict). Metadata contains "random_zone_count". """ batch, is_torch, _ = _normalize_bundling(*hypervectors) num_vectors = batch.shape[1] 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=1) in_band = torch.abs(total) <= threshold random_zone_count = int(in_band.sum().item()) 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=1) in_band = np.abs(total) <= threshold random_zone_count = int(in_band.sum()) 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}