Source code for holovec.models.fhrr

"""FHRR (Fourier Holographic Reduced Representations) VSA model.

FHRR uses complex-valued vectors (unit phasors) with element-wise complex
multiplication for binding. It achieves the best capacity among VSA models
and has exact inverse through complex conjugation.

Properties:
- Exact inverse: unbind using complex conjugate
- Commutative: bind(a, b) = bind(b, a)
- Best capacity: ~330 dimensions for 15 vectors (Schlegel et al.)
- Natural fractional power encoding for continuous data
- Element-wise operations (very efficient)

References:
- Plate (2003): "Holographic Reduced Representations"
- Schlegel et al. (2021): Comparison showing FHRR's superior capacity
- Frady et al. (2021): VFA framework and fractional power encoding
"""

from __future__ import annotations

from typing import Optional, Sequence

from ..backends import Backend
from ..backends.base import Array
from ..spaces import ComplexSpace, VectorSpace
from .base import VSAModel


[docs] class FHRRModel(VSAModel): """FHRR (Fourier HRR) model using complex phasors. Binding: element-wise complex multiplication (phase addition) Unbinding: element-wise multiplication with conjugate (phase subtraction) Bundling: element-wise addition + normalization to unit magnitude Permutation: circular shift (can also use phase rotation) Uses ComplexSpace with unit-magnitude phasors. """
[docs] def __init__( self, dimension: int = 512, space: Optional[VectorSpace] = None, backend: Optional[Backend] = None, seed: Optional[int] = None ): """Initialize FHRR model. Args: dimension: Dimensionality of hypervectors (can be smaller than MAP due to better capacity) space: Vector space (defaults to ComplexSpace) 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 = ComplexSpace(dimension, backend=backend, seed=seed) super().__init__(space, backend)
@property def model_name(self) -> str: return "FHRR" @property def is_self_inverse(self) -> bool: return False # Requires conjugate, not same operation @property def is_commutative(self) -> bool: return True # Complex multiplication is commutative @property def is_exact_inverse(self) -> bool: return True # Conjugate provides exact inverse
[docs] def bind(self, a: Array, b: Array) -> Array: """Bind using element-wise complex multiplication. For unit phasors: (a * b)[i] = a[i] * b[i] This adds phase angles: ∠(a*b) = ∠a + ∠b Args: a: First vector (unit phasors) b: Second vector (unit phasors) Returns: Bound vector c = a ⊙ b (element-wise product) """ result = self.backend.multiply(a, b) # Normalize to unit magnitude return self.normalize(result)
[docs] def unbind(self, a: Array, b: Array) -> Array: """Unbind using element-wise multiplication with conjugate. To recover original from c = a ⊙ b: unbind(c, b) = c ⊙ b* = (a ⊙ b) ⊙ b* = a ⊙ (b ⊙ b*) = a ⊙ 1 = a Args: a: Bound vector (or first operand) b: Second operand Returns: Unbound vector (exact recovery) """ b_conj = self.backend.conjugate(b) result = self.backend.multiply(a, b_conj) return self.normalize(result)
[docs] def bundle(self, vectors: Sequence[Array]) -> Array: """Bundle using element-wise addition. Sum phasors and normalize back to unit magnitude. The result points in the "average" direction of inputs. Args: vectors: Sequence of vectors to bundle Returns: Bundled vector (normalized to unit magnitude) Raises: ValueError: If vectors is empty """ if not vectors: raise ValueError("Cannot bundle empty sequence") vectors = list(vectors) # Sum all vectors (phasors add vectorially) result = self.backend.sum(self.backend.stack(vectors, axis=0), axis=0) # Normalize to unit magnitude return self.normalize(result)
[docs] def permute(self, vec: Array, k: int = 1) -> Array: """Permute using circular shift. For FHRR, permutation can be done as: 1. Circular shift (coordinate permutation) 2. Phase rotation (multiply by exp(i*2πk/D)) We use circular shift for consistency with other models. Args: vec: Vector to permute k: Number of positions to shift Returns: Permuted vector """ return self.backend.roll(vec, shift=k)
[docs] def fractional_power(self, vec: Array, exponent: float) -> Array: """Raise phasor to a fractional power. For unit phasor z = exp(iθ): z^α = exp(iαθ) This is useful for encoding continuous values. Args: vec: Vector of unit phasors exponent: Power to raise to Returns: Vector with phases scaled by exponent Example: >>> base = model.random() >>> # Encode value 2.5 using fractional power >>> encoded = model.fractional_power(base, 2.5) """ # For unit phasors z = exp(iθ), we want: z^α = exp(iαθ) # This is exact and avoids branch cuts from complex logarithms. # # Implementation: # 1. Extract phase θ = arg(z) # 2. Scale by exponent: αθ # 3. Create new phasor: exp(iαθ) # Get phase angles using backend operation angles = self.backend.angle(vec) # Scale angles by exponent scaled_angles = self.backend.multiply_scalar(angles, exponent) # Create new phasors: exp(i * scaled_angles) # exp(iθ) = cos(θ) + i*sin(θ) result = self.backend.exp(1j * scaled_angles) # Renormalize to unit magnitude (handle numerical errors) return self.normalize(result)
def __repr__(self) -> str: return (f"FHRRModel(dimension={self.dimension}, " f"space={self.space.space_name}, " f"backend={self.backend.name})")