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"
)

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")

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 FHRR 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 ~1e7 by depth 3 and overflows float64 somewhere past depth 10. 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 drives the exponent large and the embedding with it (a bandwidth of 0.05 on a feature reaching 3 gives magnitudes of ~1e63, which is numerically meaningless on either backend). Too small a bandwidth raises ValueError on the Rust backend and yields NaN on NumPy.

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 (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