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.
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 similarityTo 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.
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.
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.
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 |
|---|---|
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. |
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 |
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.
# 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.
- 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
CorOencodes 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, sonormalize=Falsereaches ~1e7 by depth 3 and overflowsfloat64somewhere past depth 10. The NumPy backend returnsinf/NaN; the Rust backend raisesValueError. Usenormalize=Truefor depth beyond ~4. - Keep
bandwidthon the order of the feature's value range β a continuous feature encodes asspectrum ** (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 raisesValueErroron the Rust backend and yieldsNaNon NumPy.
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.
Requires Python 3.9+ and a Rust toolchain (1.83+).
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')"# Build the wheel first
./build.sh
# Install the wheel
pip install target/wheels/hyper_fingerprints-*.whlnumpy >= 1.24rdkit >= 2024.0.0- Rust toolchain >= 1.83 (build-time only)
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 availableInstall in dev mode with the Rust extension:
pip install maturin
RUSTFLAGS="-C target-cpu=native" maturin develop --release
pip install -e ".[dev]"Run tests:
pytestBuild a wheel and test it in a clean environment:
nox -s build_testRun tests across Python 3.9-3.13 with nox:
nox -s testsFingerprint outputs are regression-tested against recorded fixtures to ensure numerical stability across releases.
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
This project is licensed under the MIT License.
