HashMol3D is a Python library that implements deterministic 3D molecular geometry identifiers and hashes for computational chemistry, machine learning, and HPC workflows. The library provides the implementation used to develop and define a proposed identifier standard. The accompanying paper describes the software and methods and proposes that standard; the specification is a draft, not an adopted standard.
It produces a readable identifier of the form
<Hill formula><state tag>-<geometry hash>
e.g. H2Oq0m1-8ec3152acf35859ba58ced3e101c87e1 for neutral singlet water. The trailing
geometry hash is rotation-, translation-, permutation-, and
parity-invariant (matching the invariances of the eigenvalues of the
non-relativistic molecular Hamiltonian), and depends on:
- atomic numbers
- coordinates quantized at a user-specified precision in a canonical frame
- a descriptor version tag
The default method projects the original coordinates into a deterministic
canonical frame, sorts the element-labelled coordinate rows, and hashes that
complete representation. Ordinary geometries use the principal axes of the
Z-weighted gyration tensor. Point-like, linear, symmetric-top, and spherical-
top geometries use intrinsic coordinates or canonical atom anchors, so exact
symmetry does not require a random perturbation. The canonical labelled
distance-matrix method remains available as method="canonical".
Charge and spin multiplicity live in the readable prefix, not in the hash, so two states of the same geometry share the same hex tail and can be grouped by suffix matching:
H2Oq0m1-8ec3152acf35859ba58ced3e101c87e1 # neutral singlet water
H2Oq1m2-8ec3152acf35859ba58ced3e101c87e1 # water cation, same geometry → same hex tail
The geometry hash defaults to a fixed length of 32 hex chars (128 bits).
Collision resistance is governed by how many distinct geometries share a
namespace (birthday bound ~ n² / 2^{b+1} for b = 4·length bits), not
by molecule size; the 128-bit default keeps the expected collision count below
one for corpora up to ~2.6·10¹⁹ geometries. Pass length= to pin any value in
[1, 64], or call hash_length_for(n_items, target_prob) to size the hash to
your corpus:
from hashmol3d import hash_length_for, hash_molecule
# z: atomic numbers, coords: (N, 3) Cartesian coordinates in Å
hash_length_for(10**9) # -> 23 hex chars for 1e9 items at p=1e-9
hash_molecule(z, coords, length=hash_length_for(10**9))| distinct geometries | p=1e-6 |
p=1e-9 |
p=1e-12 |
|---|---|---|---|
| 10⁶ | 15 | 18 | 20 |
| 10⁹ | 20 | 23 | 25 |
| 10¹² | 25 | 28 | 30 |
| 10¹⁵ | 30 | 33 | 35 |
(recommended hex length; the default of 32 covers up to ~8·10¹⁴ items at p=1e-9.)
The default format deliberately does not distinguish enantiomers, and
isotopes share the same atomic number and therefore the same identifier at
fixed coordinates and electronic state. The optional RDKit S extension adds
canonical isomeric SMILES in a separate descriptor namespace. The default
library depends only on NumPy; installing RDKit does not change any defaults.
The default method="frame" normally takes O(N log N) time and O(N) memory
(a 100,000-atom asymmetric system hashes in ~0.3 s where a full distance
matrix would need 80 GB):
hash_molecule(z, coords) # method="frame" is the defaultWhen principal moments are degenerate, the method preserves any isolated
axis and canonically anchors only the ambiguous subspace. Linear and point-
like systems are represented in their intrinsic dimension. Exact lines, up to
float64 roundoff, do not need transverse anchors even on coarse grids.
Well-separated principal moments also need no atom anchors: the frame is
accepted even when the centered radius is below ten grid units. Only
unresolved eigenspaces need an atom anchor, and any transverse displacement
of at least one grid unit qualifies (version 9; versions 6-8 required ten),
so nearly linear molecules with a resolved bend anchor their degenerate plane
rather than falling back. A bend below half a grid unit is an intrinsic line.
Canonically tied anchors are all evaluated up to a 10,000-candidate budget.
Clouds below ten grid units of radius with degenerate moments, and over-budget
cases, emit UserWarning and use the complete distance method.
The path is visible in result.descriptor (F: versus C:). Canonical
search must finish within node_budget (default 10,000 visited states),
including after frame fallback. If it exhausts that budget, Python raises
SearchBudgetExceeded; the CLI prints an error to stderr and exits with code
- No descriptor, hash, or identifier is created. Increase the budget and retry:
hash_molecule(z, coords, node_budget=100_000)hashmol3d --node-budget 100000 molecule.xyzA larger canonical node budget permits more work but does not change a successfully completed descriptor. No weaker summary is substituted.
For an explicitly distance-based descriptor, use:
hash_molecule(z, coords, method="canonical")Hashes from different descriptor paths are not comparable; use the same method and descriptor version throughout a corpus.
With the stated portability conditions, the identifiers support:
- workflow deduplication
- caching
- large QC datasets
- MD conformer tracking
- ML potential datasets
- LLM scientific agents
Version 0.14.0 uses descriptor 10-FRAME-SHA256, which makes re-hashing
idempotent (see Re-hashing a stored descriptor).
Version 9 lowered the minimum transverse-anchor extent to one grid unit so
that nearly linear molecules anchor their frame instead of using the distance
fallback. Because the version field is hashed, all digests change from earlier
versions; recompute identifiers consistently when migrating a corpus. The
paper's corpus statistics describe version 8 and are labelled accordingly.
Hashing the same input again always gives the same identifier. Version 10 also makes the stored descriptor self-sufficient, so a corpus can keep descriptors instead of source geometries and still re-derive every identifier:
from hashmol3d import descriptor_geometry, hash_descriptor, hash_molecule
res = hash_molecule(z, coords)
# Exact, for F and C descriptors: no geometry is recomputed.
assert hash_descriptor(res.descriptor) == res.geometry_hash
# F descriptors also carry a canonical-frame geometry that hashes back to
# the same identifier, in any orientation or atom order.
z2, coords2 = descriptor_geometry(res.descriptor)
assert hash_molecule(z2, coords2).hash_str == res.hash_strF rows are stored on a grid 10⁴ times finer than precision, and only their
values rounded back to precision are hashed, so the extra digits never make
the identifier more sensitive to noise. Before returning coordinates,
descriptor_geometry re-hashes them and, in the rare case that recomputing the
frame moves a value across a rounding boundary, settles them by a small
fraction of a grid cell. On all of QM9 at 10⁻², 10⁻³ and 10⁻⁴ Å every round
trip reproduced the identifier (version-9 rows read back as coordinates did so
for 54–56 %). Exactly symmetric inputs with tied anchor frames can occasionally
not be settled; descriptor_geometry then warns, and hash_descriptor remains
exact. C descriptors hold distances, not coordinates; keep their source
geometry if coordinates are needed.
The geometry hash is computed from a UTF-8 descriptor with fields in this order:
V:<version>|P:<precision>|Z:<atomic numbers>|F:<coordinate rows>
V:<version>|P:<precision>|Z:<atomic numbers>|C:<distance matrix upper triangle>
Videntifies the descriptor format and canonicalization rules (currently10-FRAME-SHA256).Precords the quantization grid spacing in angstroms, such as1.0e-04.Zlists atomic numbers in the order used by the selected representation.Fstores the sorted element-labelled canonical-frame rows on a grid 10⁴ times finer thanP.Cstores the quantized distance matrix upper triangle in canonical atom order.
All these fields enter the hash, with F values rounded back to the P grid. The readable q and m tags instead record
charge and spin multiplicity outside the geometry hash.
The opt-in S extension appends |S:<canonical isomeric SMILES> and uses
V:10-FRAME-SHA256-S1-RDKIT-<RDKit version>. It retains the same P, Z,
and F/C fields. Its digest also depends on molecular connectivity,
stereochemistry, isotopes, and formal charges encoded in SMILES, so it has
different invariances from the default geometry-only hash. See
RDKit support for the exact rules and limitations.
Version 0.12.0 added the optional RDKit API and S extension without changing
the default descriptor; the S version is built on the current default
(10-FRAME-SHA256 in 0.14.0).
pip install -e '.[rdkit]' # from a source checkout; see Install
# Read an existing 3D structure; still use the default geometry descriptor.
hashmol3d molecule.sdf --input-format sdf
# Explicitly add canonical isomeric SMILES to the hashed descriptor.
hashmol3d molecule.sdf --input-format sdf --include-smiles -v
hashmol3d molecule.xyz --include-smiles -v
# A SMILES file has no geometry: explicitly generate a new conformer.
hashmol3d molecule.smi --input-format smi --generate-coordinates --include-smilesfrom hashmol3d import canonical_smiles, hash_rdkit, hash_xyz
# mol can come from any RDKit reader; hashing requires a 3D conformer.
result = hash_rdkit(mol) # no S tag
stereo_result = hash_rdkit(mol, include_smiles=True)
smiles = canonical_smiles(mol) # graph stereo; does not require coordinates
stereo_xyz = hash_xyz("molecule.xyz", include_smiles=True)S tagging uses stereochemistry from the selected 3D coordinates. XYZ tagging
also infers connectivity and bond orders; it requires complete atom lists,
including hydrogens, and the correct charge. Chemistry failures produce errors.
MOL, SDF, MOL2, PDB, SMILES, and InChI file adapters are available. PDB file
input supports geometry-only hashing; S tagging requires a format with verified
bond orders or a chemically prepared RDKit Mol. PDB geometry-only input hashes
exactly the atoms in the file, without trusting guessed implicit-H counts;
coordinate generation is refused because guessed bond orders can add incorrect
hydrogens. Other RDKit inputs with hydrogens without coordinates are rejected
unless explicitly allowed with allow_implicit_hydrogens=True
(CLI: --allow-implicit-hydrogens); that override hashes only the atoms present.
Arbitrary RDKit readers can feed hash_rdkit directly. See the
RDKit guide before mixing sources or building an S-tag corpus.
HashMol3D needs Python ≥ 3.8 and NumPy; RDKit (2023.9.6 or newer) is an
optional extra. Install 0.14.0 from source: the release on PyPI is older
(0.9.x), uses an earlier descriptor version and therefore different
identifiers, and lacks hash_descriptor, descriptor_geometry and the RDKit
adapters.
# Clone the repository
git clone https://github.com/Autonomous-Scientific-Agents/HashMol3D.git
cd HashMol3D
# Create and activate a virtual environment (recommended)
# Using uv:
uv venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Or using standard Python:
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install the package in editable mode
uv pip install -e . # Or: pip install -e .
# With the optional RDKit adapters:
uv pip install -e '.[rdkit]' # Or: pip install -e '.[rdkit]'$ hashmol3d water.xyz
H2Oq0m1-8ec3152acf35859ba58ced3e101c87e1
# Cation with explicit multiplicity — only the prefix changes.
$ hashmol3d -c 1 -m 2 water.xyz
H2Oq1m2-8ec3152acf35859ba58ced3e101c87e1
# A coarser precision and a shorter, 20-hex-character hash.
$ hashmol3d -p 1e-3 -l 20 benzene.xyz
# Verbose: a labelled block with the identifier, formula, geometry hash,
# descriptor, and metadata instead of the bare identifier.
$ hashmol3d -v water.xyz
# Request the labelled distance-matrix method explicitly.
$ hashmol3d --method canonical water.xyz
# Show the package version.
$ hashmol3d --versionOptions include -p/--precision, -c/--charge, -m/--multiplicity,
-l/--length, --method, --node-budget, and -v/--verbose. Errors on
missing or malformed input go to stderr with exit code 1 (no Python
traceback); invalid command-line usage exits with code 2.
import numpy as np
from hashmol3d import hash_molecule
atomic_nums = np.array([8, 1, 1])
coords = np.array(
[
[0.0000, 0.0000, 0.0],
[0.7572, 0.5860, 0.0],
[-0.7572, 0.5860, 0.0],
]
)
res = hash_molecule(atomic_nums, coords)
print(res.hash_str) # H2Oq0m1-8ec3152acf35859ba58ced3e101c87e1
print(res.formula) # H2O
print(res.geometry_hash) # 8ec3152acf35859ba58ced3e101c87e1
print(res.charge, res.multiplicity) # 0 1All optional arguments are keyword-only: precision, charge,
multiplicity, length, method, and node_budget.
precision must be a power of ten no greater than 1 Å (1.0, 1e-1,
1e-2, ...). Restricting the grid to powers of ten keeps its descriptor
representation unambiguous and portable. It must also be resolvable in
float64: grids finer than about 3×10⁻¹¹ times the molecule's extent are
rejected with a ValueError naming the finest usable precision (e.g. 1e-10 Å
for water, 1e-8 Å for a 100 Å protein), because identifiers on such grids
change under rotation.
Or read straight from a file:
from hashmol3d import hash_xyz
print(hash_xyz("water.xyz").hash_str) # H2Oq0m1-8ec3152acf35859ba58ced3e101c87e1
print(hash_xyz("water.xyz", charge=1, multiplicity=2).hash_str)
# H2Oq1m2-8ec3152acf35859ba58ced3e101c87e1See docs/ for the full
specification,
API reference, and
CLI guide.