Pair distributions¶
Pair descriptors, KDEs, additivity and comparison metrics. Used by CageBuilder.get_descriptor(): cage_isomer_builder.utils.distributions.
Pair distributions of feature sites: the descriptor.
A :class:Descriptor stores, for every pair type (e.g. ("acceptor",
"donor") or ("fg:R1:endo", "fg:R2:exo")), the distance of each pair of
feature sites, the angle between their direction vectors, and a weight. The
weight is 1 for a plain structure; ensembles (isomers, conformers, ring
rotations) use fractional weights so that a descriptor is the expected
number of pairs at each distance per structure.
Smoothed curves are weighted Gaussian kernel density estimates,
rho(r) = sum_k w_k N(r; d_k, h),
so rho has units of pairs per Angstrom and integrates to the total pair
weight. Curves of different ensembles are therefore directly comparable and
add up. The 2-D version (distance x angle) uses a product kernel, with the
angle kernel reflected at the ends of its range so no weight leaks past
0 / 90 / 180 degrees.
Additivity
A descriptor of the union of two feature sets A and B is exactly
D(A + B) = D(A) + D(B) + D(A, B)
where D(A, B) holds the cross pairs (one site from each). So partial
distributions (e.g. node H-bond sites, linker pi-rings, functional groups)
build the full pore descriptor only when the cross terms are included:
D(A) + D(B) alone is missing every A-B pair. :func:assemble does this
sum and :func:additivity_residual checks it numerically.
Periodic structures
Pairs are counted per unit cell over every periodic image within
max_distance (like a radial distribution function), not just the
nearest image, so a distribution of a MOF does not depend on the choice of
cell.
PairSet
dataclass
¶
Distances (Angstrom), angles (degrees, NaN if undefined) and weights
of the pairs of one pair type. max_angle is 90 when the angles are
folded because a vector is a sign-less axis (a ring normal), else 180.
Examples:
>>> import numpy as np
>>> from cage_isomer_builder.utils.distributions import PairSet
>>> ps = PairSet(np.array([2.0, 3.0]), np.array([np.nan, 90.0]), np.array([1.0, 0.5]))
>>> len(ps), ps.total
(2, 1.5)
total
property
¶
empty
classmethod
¶
A pair set with no pairs.
Examples:
Source code in cage_isomer_builder/utils/distributions.py
scaled ¶
Copy with every weight multiplied by factor.
Examples:
>>> import numpy as np
>>> from cage_isomer_builder.utils.distributions import PairSet
>>> PairSet(np.array([2.0]), np.array([np.nan]), np.array([1.0])).scaled(0.25).total
0.25
Source code in cage_isomer_builder/utils/distributions.py
Descriptor
dataclass
¶
Pair distributions keyed by pair type.
Attributes:
| Name | Type | Description |
|---|---|---|
pairs |
dict
|
|
info |
dict
|
Free-form metadata (e.g. the ensemble it was averaged over). |
Examples:
>>> from ase.build import molecule
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import pair_descriptor
>>> desc = pair_descriptor(detect_features(molecule("H2O")))
>>> desc.summary() # {pair type: (number of pairs, weight)}
{('acceptor', 'donor'): (2, 2.0), ('donor', 'donor'): (1, 1.0)}
keys ¶
Pair types present, sorted.
Examples:
>>> from ase.build import molecule
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import pair_descriptor
>>> desc = pair_descriptor(detect_features(molecule("H2O")))
>>> desc.keys()
[('acceptor', 'donor'), ('donor', 'donor')]
Source code in cage_isomer_builder/utils/distributions.py
add ¶
Add pairs to pair type key (the order of the two type names does not matter).
Examples:
>>> import numpy as np
>>> from cage_isomer_builder.utils.distributions import Descriptor, PairSet
>>> desc = Descriptor()
>>> desc.add(("pi", "acceptor"), PairSet(np.array([4.0]), np.array([np.nan]), np.array([1.0])))
>>> desc.add(("acceptor", "pi"), PairSet(np.array([5.0]), np.array([np.nan]), np.array([1.0])))
>>> desc.summary()
{('acceptor', 'pi'): (2, 2.0)}
Source code in cage_isomer_builder/utils/distributions.py
scaled ¶
Every weight multiplied by factor.
Examples:
>>> from ase.build import molecule
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import pair_descriptor
>>> desc = pair_descriptor(detect_features(molecule("H2O")))
>>> desc.scaled(0.5).total()
1.5
Source code in cage_isomer_builder/utils/distributions.py
merged ¶
One PairSet of the given keys (default: all). A key may also be a single type name, meaning every pair type containing it.
Examples:
>>> from ase.build import molecule
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import pair_descriptor
>>> desc = pair_descriptor(detect_features(molecule("H2O")))
>>> len(desc.merged()), len(desc.merged("donor")), len(desc.merged([("acceptor", "donor")]))
(3, 3, 2)
Source code in cage_isomer_builder/utils/distributions.py
total ¶
Total pair weight of the given keys (default: all).
Examples:
>>> from ase.build import molecule
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import pair_descriptor
>>> desc = pair_descriptor(detect_features(molecule("H2O")))
>>> desc.total(), desc.total([("donor", "donor")])
(3.0, 1.0)
Source code in cage_isomer_builder/utils/distributions.py
kde ¶
Weighted Gaussian KDE (pairs per Angstrom) on grid.
Examples:
>>> from ase.build import molecule
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import pair_descriptor
>>> desc = pair_descriptor(detect_features(molecule("H2O")))
>>> import numpy as np
>>> r = np.linspace(0, 5, 501)
>>> round(float(np.trapezoid(desc.kde(r), r)), 3) # integrates to the pair count
3.0
Source code in cage_isomer_builder/utils/distributions.py
kde2d ¶
2-D KDE (pairs per Angstrom per degree) over distance x angle, using only pairs whose angle is defined. The angle range is [0, 90] for pair types involving a sign-less axis (ring normals) and [0, 180] otherwise; keys mixing the two raise ValueError.
Examples:
>>> from ase.build import molecule
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import pair_descriptor
>>> desc = pair_descriptor(detect_features(molecule("H2O")))
>>> import numpy as np
>>> d, a = np.linspace(0, 4, 201), np.linspace(0, 180, 181)
>>> z = desc.kde2d(d, a, [("acceptor", "donor")], bandwidth=0.2, angle_bandwidth=10)
>>> z.shape, round(float(np.trapezoid(np.trapezoid(z, a, axis=1), d)), 2)
((201, 181), 2.0)
Source code in cage_isomer_builder/utils/distributions.py
histogram ¶
Exact table: sorted unique distances (rounded) and total weight at each.
Examples:
>>> from ase.build import molecule
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import pair_descriptor
>>> desc = pair_descriptor(detect_features(molecule("H2O")))
>>> distances, weights = desc.histogram([("donor", "donor")])
>>> distances.round(2).tolist(), weights.tolist() # the H...H distance of water
([1.53], [1.0])
Source code in cage_isomer_builder/utils/distributions.py
summary ¶
{key: (number of pairs, total weight)}.
Examples:
>>> from ase.build import molecule
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import pair_descriptor
>>> desc = pair_descriptor(detect_features(molecule("H2O")))
>>> desc.summary()[("donor", "donor")]
(1, 1.0)
Source code in cage_isomer_builder/utils/distributions.py
pair_key ¶
Order-independent key of a pair type.
Examples:
>>> from cage_isomer_builder.utils.distributions import pair_key
>>> pair_key("pi", "acceptor") == pair_key("acceptor", "pi")
True
>>> pair_key("pi", "acceptor")
('acceptor', 'pi')
Source code in cage_isomer_builder/utils/distributions.py
feature_type ¶
Type name of feature i:
"kind":"donor""label":"donor:O-H""orientation":"fg:R1:endo"(label plus endo/exo/tangential when the feature has one)
Examples:
>>> from ase.build import molecule
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import feature_type
>>> feats = detect_features(molecule("H2O")).select(kinds="donor")
>>> feature_type(feats, 0, "kind"), feature_type(feats, 0, "label")
('donor', 'donor:O-H')
Source code in cage_isomer_builder/utils/distributions.py
pair_angles ¶
Angle (degrees) between paired unit vectors; folded into [0, 90] when either is a sign-less axis; NaN when either vector is zero.
Examples:
>>> import numpy as np
>>> from cage_isomer_builder.utils.distributions import pair_angles
>>> z, minus_z = np.array([[0.0, 0.0, 1.0]]), np.array([[0.0, 0.0, -1.0]])
>>> pair_angles(z, minus_z, [True], [True]).tolist() # directed vectors
[180.0]
>>> pair_angles(z, minus_z, [False], [False]).tolist() # ring normals (sign-less)
[0.0]
Source code in cage_isomer_builder/utils/distributions.py
pair_descriptor ¶
pair_descriptor(features, other=None, by='kind', exclude_same_owner=True, feature_weights=None, other_weights=None, max_distance=None)
Descriptor of every pair of features (other=None) or of every cross
pair between two feature sets.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
features
|
FeatureSet
|
|
required |
other
|
FeatureSet
|
|
required |
by
|
str
|
How pairs are typed (see :func: |
'kind'
|
exclude_same_owner
|
bool
|
Skip pairs inside one building block (e.g. two sites on one ring), as in the original FG-FG statistics. Periodic images of one building block are different building blocks and are kept. |
True
|
feature_weights
|
array - like
|
Occupation probability of each feature; a pair gets the product. |
None
|
other_weights
|
array - like
|
Occupation probability of each feature; a pair gets the product. |
None
|
max_distance
|
float
|
Only pairs up to this distance. Required for periodic structures. |
None
|
Examples:
>>> from cage_isomer_builder import example_data
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import pair_descriptor
>>> feats = detect_features(example_data.read("ibuprofen"))
>>> sorted(pair_descriptor(feats).keys())
[('acceptor', 'acceptor'), ('acceptor', 'donor'), ('acceptor', 'pi'), ('donor', 'pi')]
>>> # cross pairs between two feature sets:
>>> cross = pair_descriptor(feats.select(kinds="pi"), feats.select(kinds="acceptor"))
>>> cross.total()
2.0
Source code in cage_isomer_builder/utils/distributions.py
assemble ¶
Full descriptor of several feature sets taken together: every partial descriptor plus every cross term (see the module docstring).
Examples:
>>> from cage_isomer_builder import example_data
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import assemble, pair_descriptor
>>> feats = detect_features(example_data.read("ibuprofen"))
>>> parts = [feats.select(kinds="pi"), feats.select(kinds=["donor", "acceptor"])]
>>> assemble(parts).total() == pair_descriptor(feats).total() # partials + cross terms
True
Source code in cage_isomer_builder/utils/distributions.py
additivity_residual ¶
Largest absolute difference between the KDE of the union of parts
and the KDE of :func:assemble (partials + cross terms), and the
weight that the partials alone miss. The first number should be at
floating-point noise level.
Returns:
| Name | Type | Description |
|---|---|---|
residual |
float
|
|
missing_cross_weight |
float
|
Total weight of the cross pairs, which summing the partial distributions alone would leave out. |
Examples:
>>> import numpy as np
>>> from cage_isomer_builder import example_data
>>> from cage_isomer_builder.utils.features import detect_features
>>> from cage_isomer_builder.utils.distributions import additivity_residual
>>> feats = detect_features(example_data.read("ibuprofen"))
>>> parts = [feats.select(kinds="pi"), feats.select(kinds=["donor", "acceptor"])]
>>> residual, missing = additivity_residual(parts, np.linspace(0, 15, 301))
>>> residual < 1e-12, missing # 2 pi-acceptor + 1 pi-donor cross pairs
(True, 3.0)
Source code in cage_isomer_builder/utils/distributions.py
kde_1d ¶
sum_k w_k N(grid; d_k, bandwidth), in pairs per Angstrom.
Examples:
>>> import numpy as np
>>> from cage_isomer_builder.utils.distributions import PairSet, kde_1d
>>> ps = PairSet(np.array([3.0]), np.array([np.nan]), np.array([2.0]))
>>> r = np.linspace(0, 6, 601)
>>> round(float(np.trapezoid(kde_1d(ps, r, bandwidth=0.2), r)), 3)
2.0
Source code in cage_isomer_builder/utils/distributions.py
kde_2d ¶
kde_2d(pairset, distance_grid, angle_grid, bandwidth=0.1, angle_bandwidth=5.0, angle_range=(0.0, 180.0))
2-D product-kernel KDE, shape (len(distance_grid), len(angle_grid)).
Examples:
>>> import numpy as np
>>> from cage_isomer_builder.utils.distributions import PairSet, kde_2d
>>> ps = PairSet(np.array([3.0]), np.array([0.0]), np.array([1.0]))
>>> d, a = np.linspace(0, 6, 301), np.linspace(0, 90, 181)
>>> z = kde_2d(ps, d, a, bandwidth=0.2, angle_bandwidth=5.0, angle_range=(0.0, 90.0))
>>> round(float(np.trapezoid(np.trapezoid(z, a, axis=1), d)), 3) # reflected at 0 degrees
1.0
Source code in cage_isomer_builder/utils/distributions.py
overlap ¶
Overlap of two curves after normalising each to unit area: integral of min(p, q), from 0 (disjoint) to 1 (identical shape).
Examples:
>>> import numpy as np
>>> from cage_isomer_builder.utils.distributions import overlap
>>> x = np.linspace(0, 10, 1001)
>>> p, q = np.exp(-(x - 4) ** 2), np.exp(-(x - 6) ** 2)
>>> round(overlap(p, p, x), 3), round(overlap(p, q, x), 3)
(1.0, 0.157)
Source code in cage_isomer_builder/utils/distributions.py
bhattacharyya ¶
Bhattacharyya coefficient of the two normalised curves (0 to 1).
Examples:
>>> import numpy as np
>>> from cage_isomer_builder.utils.distributions import bhattacharyya
>>> x = np.linspace(0, 10, 1001)
>>> p, q = np.exp(-(x - 4) ** 2), np.exp(-(x - 6) ** 2)
>>> round(bhattacharyya(p, p, x), 3), round(bhattacharyya(p, q, x), 3)
(1.0, 0.368)