Skip to content

About

No description, website, or topics provided.

Resources

Stars

7 stars

Watchers

1 watching

Forks

Repository files navigation

Hyperdimensional Fingerprints (HDF)

banner

Python 3.9+ License: MIT NumPy + optional Rust

Real-valued, fixed-size molecular fingerprints β€” no training, just NumPy (with optional Rust acceleration).

Hyper Fingerprints encodes molecules into continuous vector representations using Holographic Reduced Representations (HRR) with graph message passing. The result is a deterministic, real-valued fingerprint that works as a drop-in feature vector for similarity search, clustering, or any downstream ML task.

πŸš€ Quick start

from hyper_fingerprints import Encoder, cosine_similarity

enc = Encoder(dimension=512, seed=42)

# Encode molecules (SMILES strings or RDKit Mol objects)
fps = enc.encode(["CCO", "CO", "c1ccccc1"])  # shape: (3, 512), dtype: float64

# Cosine similarity β€” similar molecules get similar vectors
sim = cosine_similarity(fps, fps)

print(f"ethanol vs methanol: {sim[0, 1]:.3f}")   # high similarity
print(f"ethanol vs benzene:  {sim[0, 2]:.3f}")    # low similarity

To use a custom atom vocabulary, pass atom_types at init:

enc = Encoder(dimension=512, atom_types=["C", "N", "O", "H", "Si"])

See examples/00_quickstart.ipynb for a full walkthrough covering similarity search, joint fingerprints, custom atom types, save/load, and scikit-learn integration.

πŸ“– API

Encoder

Encoder(
    dimension=256,      # hypervector size
    depth=3,            # message-passing layers (structural context radius)
    atom_types=None,    # atom vocabulary (default: Br, C, Cl, F, I, N, O, P, S)
    seed=None,          # random seed for reproducible codebook generation
    normalize=False,    # L2-normalize after each message-passing layer
    backend="auto",     # "auto" | "rust" | "numpy"
    unit_modulus=True,  # unit-magnitude (FHRR) codebooks; False = Gaussian codebooks of < 0.5.0
)

Molecules can be passed as SMILES strings, RDKit Mol objects, or lists of either.

Methods

encode(molecules) -> np.ndarray β€” Encode molecules into order-N hypervector fingerprints. Returns shape (batch_size, dimension).

encode_joint(molecules) -> np.ndarray β€” Concatenation of order-0 (atom identity only, no structural context) and order-N (full message-passing) embeddings. Returns shape (batch_size, 2 * dimension). Useful when you want both local atom-level and structural information in one vector.

Persistence

save(path) / Encoder.load(path) β€” Persist and restore an encoder (config + codebook) as a single .npz file. Useful for sharing a fixed fingerprint scheme or deploying without needing to track the seed.

enc.save("encoder.npz")
loaded = Encoder.load("encoder.npz")

Codebooks

Every random base vector (the product-codebook rows, the categorical feature vectors and the base spectra of continuous features) is sampled with Fourier coefficients of magnitude one: a Gaussian draw keeps only its random phases, as in Fourier holographic reduced representations (FHRR). Binding by circular convolution multiplies spectra, and message passing binds at every layer, so the random magnitudes of Gaussian vectors compound until a few Fourier components carry most of a fingerprint. With unit-magnitude vectors binding preserves norms and the fingerprint keeps using all components (at dimension=2048, depth=2: ~390-480 effective components instead of ~8-75), which also makes results far less dependent on the seed. This matches the encoder of the HDF paper and graph_hdc's continuous-mode encoders (the legacy product codebook has no graph_hdc counterpart and is switched as well).

Unit-magnitude codebooks are the default since 0.5.0, so the same seed gives different fingerprints than in 0.4.x. unit_modulus=False reproduces the Gaussian codebooks of earlier versions bit for bit. Saved encoders keep the vectors they were saved with.

Parameter guidance

Parameter Guidance
dimension 32-256 for Bayesian optimization. 1024-2048 as a starting point for property prediction.
depth Controls structural context radius, analogous to Morgan radius. depth=3 captures up to 3-bond neighborhoods. Higher values capture more global structure but increase computation.

Atom features

Each atom is described by 5 discrete features:

Feature Bins Values
Atom type len(atom_types) (varies with vocabulary) Index into the atom vocabulary
Degree 6 0-5
Formal charge 3 neutral, positive, negative
Total Hs 4 0-3
Is aromatic 2 0, 1

🌊 Continuous features (FHRR)

Passing features as a dict switches to the composite encoder: every feature gets its own codebook, and the per-feature hypervectors are bound together with circular convolution. Count-like features can then be encoded continuously via Fourier Holographic Reduced Representations, so that degree 3 is close to degree 4 rather than orthogonal to it:

from hyper_fingerprints import Encoder, FEATURES_CONTINUOUS, GRAPH_FEATURES_GRAPH_HDC

enc = Encoder(dimension=1024, seed=42, features=FEATURES_CONTINUOUS, normalize=True)
fps = enc.encode(["CCCC", "CCCCC", "c1ccccc1"])

FEATURES_CONTINUOUS expands to

{
    "element":   {"kind": "categorical"},
    "degree":    {"kind": "continuous", "bandwidth": 2.0},
    "hydrogens": {"kind": "continuous", "bandwidth": 2.0},
    "charge":    {"kind": "categorical"},
    "aromatic":  {"kind": "categorical"},
}

A continuous value v is encoded as ifft(spectrum ** (v / bandwidth)); bandwidth sets how far apart two values must be before their vectors stop resembling each other. Any built-in feature (element, degree, charge, hydrogens, implicit_hydrogens, aromatic) can be used with either kind; seed_offset (default 10 * position) decorrelates the per-feature codebooks.

Other presets: FEATURES_GRAPH_HDC / GRAPH_FEATURES_GRAPH_HDC mirror the configuration of the graph_hdc project; FEATURES_DEFAULT is the legacy list.

Graph-level features (graph_size, graph_diameter, graph_total_charge) can be added with graph_features= and enter the fingerprint according to graph_mode:

graph_mode Result
"bundle" (default) normalize(normalize(emb) + normalize(graph_hv)) β€” graph_hdc convention
"bind" emb βŠ› graph_hv
"concat" `[emb

Further options: bidirectional=False uses a single directed edge per bond (graph_hdc's default), unknown_atoms="slot" maps atoms outside atom_types to a shared extra codebook row instead of raising.

Recommendation: use normalize=True with continuous features; without per-layer normalization the magnitudes compound across message-passing layers.

Importing a graph_hdc codebook

# in graph_hdc
hyper_net.export_legacy_codebook("codebook.npz")

# in hyper_fingerprints -- identical embeddings, no torch required
enc = Encoder.from_legacy_codebook("codebook.npz")

The composite encoder runs on the Rust backend too: both its node encoding (per-feature lookups and FHRR fractional powers) and its message passing are Rust-accelerated, ~3x end-to-end over the NumPy backend. Node encoding additionally computes each distinct feature row only once β€” atoms of the same kind share a hypervector exactly β€” which is a ~39x saving on the NumPy backend and applies to both. All codebook sampling stays in NumPy, so seeded reproducibility is unchanged. Composite molecules are still parsed by RDKit β€” the Rust SMILES parser covers the legacy feature set only β€” which is now the dominant remaining cost.

⚠️ Limitations

  • No bond type features β€” bonds are treated as unweighted edges. Single, double, and aromatic bonds are not distinguished in the current feature scheme.
  • No stereochemistry β€” chirality and cis/trans isomerism are not encoded.
  • No GPU acceleration β€” encoding is CPU-only (NumPy or optional Rust extension).
  • No Rust SMILES parsing for composite features β€” the Rust parser extracts the legacy feature set only, so composite (dict-features) input is parsed by RDKit. Node encoding and message passing are Rust-accelerated in both modes.
  • Legacy single-atom batching quirk β€” in the default (list-features) mode, a single-atom molecule such as C or O encodes differently alone than in a batch with bonded molecules. Kept for output stability; the composite (dict-features) encoder is batch-invariant.
  • Codebook scales with vocabulary β€” the codebook has product(feature_bins) entries (1296 for the default 9 atom types). Large custom atom type lists will increase memory usage.
  • Unnormalized embeddings overflow at large depth β€” each layer multiplies hypervector magnitudes, so normalize=False reaches ~1e2 by depth 3 and overflows float64 at depth ~10 (with unit_modulus=False: ~1e4 to 1e7 by depth 3, overflow at ~9). The NumPy backend returns inf/NaN; the Rust backend raises ValueError. Use normalize=True for depth beyond ~4.
  • Keep bandwidth on the order of the feature's value range β€” a continuous feature encodes as spectrum ** (value / bandwidth), so a bandwidth far below the values makes neighbouring values nearly orthogonal. With unit_modulus=False the random spectral magnitudes are raised to that power too, which drives the embedding magnitude up (a bandwidth of 0.05 on a feature reaching 3 gives ~1e63, numerically meaningless on either backend; far smaller bandwidths raise ValueError on the Rust backend and yield NaN on NumPy). Unit-magnitude spectra keep magnitude one at any bandwidth.

Backend agreement

The Rust and NumPy backends are not bit-identical β€” they use different FFT implementations, and Rust evaluates z ** e as exp(e Β· ln z) where NumPy switches to repeated multiplication for real integer exponents with |e| < 100. Measured deviation, scaled by each row's own magnitude, over 100 real PubChem molecules across dimensions 64–512, both feature kinds and both normalize settings:

regime agreement
depth 1–3 better than 1e-14
depth 4–6 better than 5e-13
depth 7–8 usually ~1e-13, but excursions to ~1e-9
magnitudes near the float64 ceiling (~1e186) arbitrary β€” meaningless on either backend
bandwidth far below the value range, unit_modulus=False (magnitudes ~1e63) ~5.6e-7

Depth 6 is the supported range for tight agreement. Message passing is ill-conditioned at greater depth: each layer binds the state with an aggregate of its neighbours, so rounding differences amplify β€” perturbing the input by one ULP moves the NumPy result by a comparable amount. This is a property of the algorithm, not of the port. All of these boundaries are pinned by tests in tests/test_rust_backend.py so they cannot drift unnoticed.

πŸ“¦ Installation

Requires Python 3.9+ and a Rust toolchain (1.83+).

From source (recommended for development)

git clone https://github.com/the16thpythonist/hyper-fingerprints.git
cd hyper-fingerprints

# Install maturin (builds the Rust extension)
pip install maturin

# Build and install in development mode (editable, release-optimized)
RUSTFLAGS="-C target-cpu=native" maturin develop --release

# Verify the Rust backend is available
python -c "from hyper_fingerprints._core import encode_batch_rs; print('Rust OK')"

From a pre-built wheel

# Build the wheel first
./build.sh

# Install the wheel
pip install target/wheels/hyper_fingerprints-*.whl

Dependencies

  • numpy >= 1.24
  • rdkit >= 2024.0.0
  • Rust toolchain >= 1.83 (build-time only)

πŸ”§ Rust backend

The Rust extension accelerates both SMILES parsing/feature extraction (~23x) and the message-passing pipeline (~22x), for a combined ~22x end-to-end speedup. When installed, it is used automatically:

enc = Encoder(dimension=512, seed=42, backend="rust")   # require Rust
enc = Encoder(dimension=512, seed=42, backend="numpy")  # force pure Python
enc = Encoder(dimension=512, seed=42, backend="auto")   # default: Rust if available

πŸ§ͺ Development

Install in dev mode with the Rust extension:

pip install maturin
RUSTFLAGS="-C target-cpu=native" maturin develop --release
pip install -e ".[dev]"

Run tests:

pytest

Build a wheel and test it in a clean environment:

nox -s build_test

Run tests across Python 3.9-3.13 with nox:

nox -s tests

Fingerprint outputs are regression-tested against recorded fixtures to ensure numerical stability across releases.

πŸ“š References

This project builds on the theory of Holographic Reduced Representations and Vector Symbolic Architectures:

  • Plate, T. A. (1995). Holographic Reduced Representations. IEEE Transactions on Neural Networks, 6(3), 623-641. doi:10.1109/72.377968
  • Kanerva, P. (2009). Hyperdimensional Computing: An Introduction to Computing in Distributed Representation with High-Dimensional Random Vectors. Cognitive Computation, 1(2), 139-159. doi:10.1007/s12559-009-9009-8

πŸ“„ License

This project is licensed under the MIT License.

About

No description, website, or topics provided.

Resources

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages