Symmetry-aware clustering and the torsion representation¶
This page explains four things the torsional analysis does, in the order a frame meets them: how a symmetric torsion is duplicated and weighted, how the distance is minimised over permutations, how HDBSCAN is used with a 1 % floor, and how a frame is written in one labelling (the Dirichlet-domain rule). The tool reference is Analysis; the results are on the paracetamol and c(RGDfV) pages.
What is released
The distance, the clustering and the weights are in 0.6.5. THDBSCAN.density_peak,
THDBSCAN.align_all and SymmetricTorsionDescriptor.align_to_reference, which implement section 4,
and the resonance option of section 1b are not in a release yet.
The problem¶
A torsion is measured through four named atoms, and a molecular symmetry renames atoms without
moving any. Paracetamol's phenyl ring flip renames C5 as C10, so the same configuration is
aryl = -16 degrees when measured through C5 and aryl = 164 degrees when measured through C10.
Two consequences follow for anything built on the stored columns:
- a distance between frames that ignores this calls one configuration two, and a clustering built on it returns one cluster per naming;
- a histogram of one stored column is the sum of one conformer written twice, with peaks half a turn apart that are not two states.
Sections 1-3 fix the first, section 4 the second. In all four, relabelling is not motion: no atom moves, and no frame is added or dropped.
1. Duplicating torsion bonds, with weights¶
The molecule is matched against itself with RDKit (uniquify=False, useChirality=True). Every
match is a graph automorphism: an element-, bond-order- and stereo-preserving permutation of the atoms.
c(RGDfV) has 147 456 of them and paracetamol has 12 (c(RGDfV) has 36 864 if the drawn structure is
compared instead of the resonance hybrid, see section 1b).
Most do nothing to the torsions that were selected, so permutations that act identically on the
descriptor collapse to one operation:
| molecule | automorphisms | distinct operations on the selected torsions | what they are |
|---|---|---|---|
| paracetamol | 12 | 2 | identity, the ring flip (the 3! orderings of the methyl hydrogens move no selected torsion) |
| c(RGDfV), 17 torsions | 147 456 | 4 | identity, the Phe ring flip, the Val methyl swap, both |
c(RGDfV), 19 torsions (adding Asp chi2, Arg chi5) |
147 456 | 16 | those four exchanges, the carboxylate oxygens and the guanidinium nitrogens, in every combination |
An operation maps a selected quadruplet to another quadruplet with the same central bond and a
different outer atom: aryl (1-3-4-5) becomes 1-3-4-10. If nobody selected 1-3-4-10, it is added.
The descriptor is closed: every image of a selected torsion becomes a column of its own, named
<torsion>@<atoms>, and its value is recomputed from coordinates. It is never approximated by a
180 degree shift: ring distortion moves the outer atoms independently of the central bond, and on
paracetamol the shift is wrong by up to 25 degrees.
Closing makes the operations act as permutations of columns, which is what makes the distance in section 2 exact. It also duplicates a torsion, and a duplicated torsion would count twice. So each orbit of columns (the columns an operation can exchange) carries the summed weight of the torsions selected in it, split equally among its columns:
| column | weight | |
|---|---|---|
| paracetamol | omega |
1 |
aryl, aryl@1-3-4-10, phenol, phenol@9-7-8-17 |
0.5 each | |
| c(RGDfV) | the 15 backbone phi, psi, omega | 1 each |
Val:chi1, Val:chi1@4-3-1-2, Phe:chi2, Phe:chi2@7-8-9-14 |
0.5 each |
The ring orientation therefore counts once, as it should. Two torsions selected in one orbit must have equal weights, or the constructor refuses: nothing says which one the orbit should follow. The group closure and the isometry property of section 2 are checked when the descriptor is built, and it raises if either fails.
Each column contributes sqrt(w) (cos θ, sin θ) to the embedding Φ(θ), so a torsion is compared as
a point on a circle: -π and π are the same place.
1b. Resonance: atoms the drawing keeps apart¶
A graph with localised bond orders and charges draws a carboxylate as C=O and C-O-. RDKit's
self-match compares those, finds the two oxygens different, and returns no permutation that
exchanges them: the torsion measured to one oxygen is never folded onto the torsion measured to the
other, half a turn away. The same holds for the two terminal nitrogens of a guanidinium.
enumerate_symmetry therefore compares the resonance hybrid (resonance=True, the default).
The charge of each atom and the order of each bond are averaged over the resonance structures that
RDKit's ResonanceMolSupplier generates, and two atoms with the same hybrid values, the same
element and the same neighbours are one atom. A molecule with no delocalised group gets back exactly
its own integers, so nothing else changes.
Three rules keep this from over-reaching:
- Aromatic atoms and bonds keep their own values. The supplier returns an aromatic ring Kekulised, and averaging the alternating 1 and 2 would break the ring's own symmetry (phenol would lose its ring flip). A ring is already one delocalised system.
- The structure count is a limit that refuses. A result of 1000 structures cannot be told from a
truncated one, and is not used (
max_structures). - Every returned permutation is re-verified against the hybrid values, as before against the drawn ones.
RDKit does not generate the alternative drawings of a hypervalent group, so one rule is added: the terminal oxygens (or sulfurs) of a sulfur, phosphorus or chlorine centre are one atom, whichever way the group is drawn.
Measured automorphism counts, drawn structure against hybrid (hydrogens explicit):
| group | example | drawn | hybrid |
|---|---|---|---|
| carboxylate | acetate | 6 | 12 |
| carboxylate on a ring | benzoate | 2 | 4 |
| guanidinium | N-methylguanidinium | 24 | 48 |
| amidinium | acetamidinium | 24 | 48 |
| nitro | nitromethane | 6 | 12 |
| nitrate | nitrate | 2 | 6 |
| carbonate | carbonate | 2 | 6 |
| sulfonate (rule) | methanesulfonate | 12 | 36 |
| phosphate monoester (rule) | methyl phosphate | 12 | 36 |
| unchanged | acetic acid, acetamide, urea, phenol, paracetamol, phenolate, an asymmetric azide | = | = |
An example: propionate¶
One torsion to a carboxylate oxygen, C0-C1-C2-O3, in CCC(=O)[O-] (atoms C0 C1 C2 O3 O4).
import types
import numpy as np
from rdkit import Chem
from rdkit.Chem import AllChem, rdMolTransforms
from md_tools.analysis import TorsionDefinitions, SymmetricTorsionDescriptor, enumerate_symmetry
mol = Chem.AddHs(Chem.MolFromSmiles("CCC(=O)[O-]"))
AllChem.EmbedMolecule(mol, randomSeed=3)
torsions = TorsionDefinitions(quads=((0, 1, 2, 3),), names=("chi",),
indexing="zero-based", units="radian")
enumerate_symmetry(mol, resonance=False).order # the drawn structure
enumerate_symmetry(mol).order # the resonance hybrid (the default)
for flag in (False, True):
d = SymmetricTorsionDescriptor(mol, torsions, resonance=flag)
print(flag, d.names, list(d.column_weights), len(d.operations))
# one frame, and the same frame with the two oxygens named the other way round
conf = mol.GetConformer(); rdMolTransforms.SetDihedralRad(conf, 0, 1, 2, 3, 0.4)
xyz = conf.GetPositions()[None]
swap = list(range(mol.GetNumAtoms())); swap[3], swap[4] = 4, 3
atoms = [types.SimpleNamespace(index=a.GetIdx(), element=types.SimpleNamespace(symbol=a.GetSymbol()))
for a in mol.GetAtoms()]
top = types.SimpleNamespace(atoms=atoms, n_atoms=len(atoms), bonds=[
(atoms[b.GetBeginAtomIdx()], atoms[b.GetEndAtomIdx()]) for b in mol.GetBonds()])
kw = dict(atom_mapping=list(range(mol.GetNumAtoms())), periodic=False, topology=top)
d = SymmetricTorsionDescriptor(mol, torsions)
a = d.closed_angles(None, coordinates=xyz, **kw)[0]
b = d.closed_angles(None, coordinates=xyz[:, swap, :], **kw)[0]
d.distance(a, b) # (distance, minimising operation)
d.nearest_labelling(np.vstack([a, b]), reference=a) # a crystal that offers both assignments
automorphisms, drawn structure : 12
automorphisms, resonance hybrid: 24
resonance=False: columns ('chi',), weights [1.0], operations 1
resonance=True : columns ('chi', 'chi@0-1-2-4'), weights [0.5, 0.5], operations 2
frame, O named one way : [ 22.9 -156.2] ('chi', 'chi@0-1-2-4')
same frame, O swapped : [-156.2 22.9]
distance between them : 0.0 (minimising operation 1)
crystal candidates -> chosen assignment 0 angles [ 22.9 -156.2]
The same frame is chi = 22.9 degrees named one way and -156.2 degrees named the other, a difference
of 179 degrees, which is the two-peak split of a carboxylate torsion in a histogram of the stored
column. Without resonance the descriptor has one column and one operation and nothing relates
the two; with it they are at distance zero.
Other places the same thing happens, not measured here but by the same mechanism: thiocarboxylates,
sulfates and phosphonates (the last two through the rule), and the charged side chains of Glu and the
C terminus. Enolates and 1,3-dicarbonyl anions are exchanged through resonance only when the
drawing is not already symmetric. A carboxylic acid is not one: C=O and C-OH are different
atoms, and so are the two oxygens of an ester.
What this does not cover.
- Tautomers are not resonance. Moving a hydrogen (imidazole N-H, keto-enol) changes the graph, and a protomer or tautomer is a different molecule, not a relabelling.
- The hybrid weights the structures equally. RDKit lists the structures, not their contributions. For a group whose two sides contribute unequally, an equal average could make two atoms look alike that are not; none of the groups above is such a case.
- The legacy post-hoc route (
TorsionSymmetry,t_symmetry) still compares the drawn structure. The defaultt_hdbscanroute, the descriptor andenumerate_symmetryuse the hybrid. - A larger group. Resonance multiplies the automorphism count (c(RGDfV): 36 864 to 147 456, and about 14 s to enumerate), and the 200 000 cap applies as before.
On c(RGDfV) the 17 torsions of the tutorial are not moved by the carboxylate and guanidinium
exchanges, so their four operations and their clusters are unchanged. Adding Asp chi2 and Arg
chi5, which those exchanges do move, folds each of their two peaks half a turn apart into one, and
the descriptor has 16 operations; see the
c(RGDfV) page.
2. The distance, minimised through the permutations¶
g y is y with its columns permuted by operation g. The one-sided minimum is exact when every
operation is an isometry of the embedding, which holds exactly when each operation permutes the
columns and the weights are constant on every orbit; section 1 guarantees both and checks them. The
two-sided minimum over g, h is kept as exhaustive_distance, and the test suite checks the two
agree, including on a molecule with a three-fold axis where an operation and its inverse differ.
Cost: |G| matrix products, because every embedded point has the same squared norm W (the total
weight), so ||a - b||² = 2W - 2 a·b. For c(RGDfV) that is 4, not 36 864.
The operation acts on the whole row. In para-disubstituted benzene the flip moves aryl and
phenol together. Folding each torsion into its own period independently would also identify the
configuration where only one of them turned, which is a different conformer. Ties go to the earlier
operation, with the identity first.
What is not done. Choosing a canonical labelling per frame and taking ordinary Euclidean distances between the representatives breaks pairs that sit on either side of the canonicalisation boundary. Relabelling is never appended to the data as extra frames, which would multiply the frame counts and the statistical weights by the group order.
Because d_sym is a minimum over relabellings and not Euclidean in any fixed embedding, it is
passed to HDBSCAN as an exact dense matrix (metric="precomputed"): 8 n² bytes per copy. Above
10 000 frames the fit refuses and names the subset route (resampling=N).
3. HDBSCAN, with a floor of 1 %¶
HDBSCAN is fitted on the d_sym matrix, so frames that differ only by a relabelling are at distance
zero before the density is built, and symmetry-related basins share a cluster during the fit.
Nothing is merged afterwards.
Two numbers set the density estimate, and both are fractions of the fitted frames:
| rule | paracetamol, n = 4000 | c(RGDfV), n = 4002 | |
|---|---|---|---|
min_cluster_size (the floor) |
ceil(0.01 n) |
40 | 41 |
min_samples (the core-distance k) |
round(0.01 n) |
40 | 40 |
- The floor is a mass, not a count. A cluster holding under 1 % of the ensemble cannot be a cluster; its frames become noise. A fraction asks the same question at every dataset size; the earlier absolute default of 500 frames loosened the floor to 0.17 % at 300 000 frames.
min_samplesis pinned separately. sklearn letsmin_samplesfollowmin_cluster_size, andmin_samplessets the core distances, so raising the floor alone changes the density estimate instead of filtering clusters out of a fixed one. Measured on 15 000 hot c(RGDfV) frames, a 5 % floor withmin_samplesfollowing turned all frames into noise; pinned, the same floor returned the two clusters above 5 %.- Tying
min_samplestonhas a cost, measured on alanine dipeptide: it smooths away a marginal state (a 1.44 % basin was absorbed).min_samples=25restores the pinned behaviour.
Then every frame, fitted or not, is classified with the same distance by an absolute vote of its
20 nearest clustered neighbours (18 of 20 assigns). A frame whose min_samples-th neighbour is
farther than that of any clustered frame is density noise (label -2); one whose neighbours disagree
is ambiguous (-1, which is not evidence of a barrier). Clusters are numbered by population, 0 is
the largest, and allow_single_cluster defaults to True: with False a unimodal ensemble cannot come
back as one state. See Analysis for both.
A cluster is a density basin, not a metastable state. Nothing here looks at time.
The choice of torsions moves the answer. On c(RGDfV) the 17 columns (15 backbone plus the two
symmetric side-chain columns) give two clusters, 0.834 and 0.145, whichever way allow_single_cluster
is set. All 24 heavy-atom torsions give one cluster when it is True and two when it is False: the Arg
chi angles and the Asp carboxylate rotation are close to uniform, and they dilute the density that
separates the backbone conformers. The torsion set is an input, and the
c(RGDfV) page shows
the table.
4. The Dirichlet-domain rule: one labelling for the histogram¶
The clustering needs no choice of labelling. A histogram does. Each frame is written in the labelling nearest one reference frame:
This picks one point from each orbit of frames, the one in the Dirichlet domain of the reference: the region of the torus closer to the reference than to any of its images. The reference is the density peak of the largest cluster, an actual frame (the one with the smallest core distance), not a mean, since a circular mean of a multimodal torsion can describe no configuration.
fit = t_hdbscan(torsions, molecule=mol, torsion_definitions=definitions,
coordinates=traj, atom_mapping=mapping).fit()
ref = fit.density_peak(0) # densest frame of the largest cluster
al = fit.align_all(per_cluster=True) # every frame in the labelling nearest `ref`
al["selected"] # (n_frames, n_selected) angles, new labelling
al["angles"] # every closed column, images included
al["operations"], al["margin"] # the operation used, and how safe the choice was
The same rule as a function of the descriptor: descriptor.align_to_reference(closed, ref_row)
returns the aligned angles, the operation per frame, the distance, the distance with no
relabelling, and the margin.
The margin is the second-best operation's distance minus the best. A small margin means the frame sits on the wall between two labellings, and the choice for that frame is arbitrary.
One reference or two levels¶
With one reference, every frame is compared with the one frame. A cluster far from it can sit
astride the wall: both of its labellings are about equally far from the reference, so the choice
flips from frame to frame and the cluster is cut in two. On paracetamol this is exactly the case.
Cluster B differs from A by half a turn in the aryl, or, written through the flip, by half a turn in
the phenol; B is equidistant from A's peak under either labelling, and 58 % of its frames have a
margin below 0.1. Its aryl histogram shows two peaks.
align_all(per_cluster=True) uses two levels. Each cluster's frames are aligned to the cluster's
own density peak, and only that one peak is written in the labelling nearest the global reference.
Each cluster is then compact, and the one arbitrary choice left is a single frame per cluster, which
fixes where the cluster is drawn and nothing about the cluster itself. On paracetamol no frame has a
margin below 0.1 any more.
On c(RGDfV) the two agree to the precision of the figures (margin below 0.1 for 2-4 % of frames in either), because the clusters differ in the backbone and are not equidistant. The two-level alignment is therefore the safe default, and it costs one extra alignment per cluster.
Fold one angle, leave the partner free?¶
A natural shortcut for a symmetric pair, such as the two ring torsions: fold one angle into a
window of width 2π/N around the reference value, and leave the other unfolded. This is a valid
fundamental domain only when each frame has exactly one image in the window. That holds when the
operation acts on the orbit as a rotor, N columns spaced 2π/N apart, and nothing sits near the window
edge. The test is cheap: for every frame count the distinct images of the first column inside the
window.
| orbit (c(RGDfV) frames) | no image in window | exactly one | two | median spacing of the pair |
|---|---|---|---|---|
Phe:chi2 / its ring image |
0.05 % | 99.93 % | 0.02 % | 175 degrees |
Val:chi1 / its methyl image |
37.4 % | 62.6 % | 0 | 122 degrees |
The phenyl ring passes: its two columns are half a turn apart, so one of them always lands in a window of width π. The valine isopropyl fails: its two methyl torsions are 120 degrees apart, and a window of width π catches neither for 37 % of the frames. The fold-one shortcut is not a general rule; the Dirichlet rule has no such condition and covers it.
The two are not the same cut either. They are different fundamental domains of one group action:
the Dirichlet domain is defined by the distance of the whole row to the reference, so the
operation chosen for a frame can depend on its other torsions, while fold-one looks at one column. On
paracetamol fold-one places both clusters at aryl near 0 and separates them by the phenol; the
two-level rule places cluster B at aryl near 180 degrees, where its own peak is.
The second angle of a ring pair is not a free torsion. It is aryl + 180 degrees + δ, where δ is
the small ring distortion. The flip swaps the two columns, so it maps δ to -δ: the sign of δ
is a property of the labelling. On the paracetamol trajectory δ is symmetric about zero (mean 0.1
degrees, standard deviation 7.1), so relabelling does not change its histogram.
A crystal structure, or any other reference¶
A crystal ligand drawn over the histograms follows the same rule. Matching it onto the simulated molecule gives one atom assignment per graph match (the two oxygens of a carboxylate, the two sides of a ring, a swapped methyl), and each assignment writes the same structure as a different row of closed angles. Drawing a line per assignment puts several lines on a panel for one structure.
descriptor.nearest_labelling(candidates, reference) chooses one: the candidate row nearest the
reference under the same distance, taken over the columns measurable in every candidate (a torsion
through a hydrogen is NaN in a structure without hydrogens and takes no part). The reference is
the row the histograms are drawn in: the density peak of cluster A for a panel pooling the clusters,
and a cluster's own density peak, already relabelled, for that cluster's row. Frames drawn as
stored, in any labelling, keep every assignment, because each is a legitimate place for the crystal.
The crystal's line can still sit far from a cluster's histogram; that is a statement about the conformation, and the labelling no longer contributes to it.
What this does and does not establish¶
- Where the cut falls. A rotor and its relatives give a clean picture. A peak that crosses the
wall appears at both ends of the axis, as a peak at
±πdoes on any circle. The cut belongs to the display; it is not in the data. - The reference changes where a cluster is drawn, never the clusters, the frames in them or their populations.
- Fixed points. The operations are permutations of columns, never reflections: matching is stereo-aware, so a mirror image is not an automorphism. A permutation that swaps two columns fixes the frames where the two are equal, and frames near one are nearly equidistant to two labellings; the margin reports them.
- Structural, not thermodynamic. A graph automorphism says two configurations are one conformer
under another naming. It does not make their populations equal: an atom-specific restraint or a
REST2 region covering one side breaks the energetic symmetry and leaves the graph unchanged
(
broken_by=[...]). - Resonance is in the graph now, by the hybrid of section 1b, and the carboxylate and guanidinium torsions fold. Atoms that are equivalent only by tautomerism are not.
- Weights. Populations are weighted sums over frames and the density the fit sees is that of the fitted frames, as in Analysis.
The idea, minimising over the automorphisms, is the same one symmetry-corrected RMSD uses for atom coordinates; here it acts on a descriptor of torsions instead.