Isomer ensembles¶
Exact configuration-averaged FG distributions for several groups, ratios, defects and flexibility. Used by CageBuilder.get_ensemble_descriptor(): cage_isomer_builder.utils.ensemble.
Functional-group pair distributions averaged over an isomer ensemble - multivariate MFMs, partial functionalisation, defects, ratios and linker flexibility (Objective 2).
The question answered here: "over all ways of placing these groups, how many NH2-NH2 / NH2-CH3 / CH3-CH3 pairs (endo-endo, endo-exo, ...) are there at each distance, per structure?"
Exact average over configurations
Every raw configuration (every placement of the groups on the slots, before
symmetry) counts once. Because a symmetry-unique isomer stands for exactly
|orbit| raw configurations, this is the same as averaging over the
unique isomers weighted by orbit size, which describes a sample with no
energetic preference. That average has a closed form, so no
enumeration is needed:
-
linkers carry patterns (
"AB","A","", a vacancy, ...); with fixed numbersN_Pof linkers per pattern (mode="fixed", a real cage or cell), two different linkers carry patterns P and Q with probabilitypi(P, Q) = N_P (N_Q - [P = Q]) / (n (n - 1));
with independent linkers (mode="independent", a large crystal with
fraction f_P of each pattern) it is f_P f_Q;
* within a linker carrying P, every arrangement is equally likely, so a slot
carries label a with probability c_P(a) / F, and two slots of the same
linker carry a and b with probability c_P(a) (c_P(b) - [a = b]) / (F (F - 1)).
So two slots on different linkers carry (a, b) with probability
P(a, b) = sum_{P,Q} pi(P, Q) c_P(a) c_Q(b) / F^2,
the same for every such pair of slots. Each slot pair contributes its
distance with weight P(a, b) to pair type (a, b). The total weight is the
expected number of pairs per structure. tests/test_ensemble.py checks the
formula against explicit orbit-weighted enumeration.
Other weightings (each unique isomer once, or a Boltzmann weight from an
energy per isomer) use :func:isomer_ensemble_descriptor.
Defects
A missing linker is a linker pattern whose every slot carries the reserved
label :data:VACANCY. Its slots then contribute no pairs, and the symmetry
counting in :mod:cage_isomer_builder.utils.rgroup treats it exactly like any
other pattern (one arrangement, never mixed with real groups).
Periodic structures
For a MOF the configuration of the unit cell repeats in every cell (as for
the enumerated MOF isomers), and pairs are counted per cell over every image
up to max_distance. A slot and its own image then always carry the same
group, and a slot and the image of another slot of the same linker are
correlated like two slots of one linker; both cases are exact.
mode="independent" (an infinite random crystal) is for finite pore
models only.
Flexibility
Slot positions may carry samples (e.g. ring rotation angles, see
:mod:cage_isomer_builder.utils.flexibility). Slots on the same rotor move
together (paired samples); slots on different rotors move independently, so
every combination of their samples contributes with the product of the
sample weights.
LabelProbabilities ¶
Exact label probabilities of slots, for every linker type.
For a linker of type t carrying pattern P with probability pi_t(P)
(N_P / n_t with fixed counts, or the given fraction), one slot carries
label a with probability single(t)[a] = sum_P pi_t(P) c_P(a) / F_t.
Two slots of different linkers of the same type with fixed counts are
correlated (N_P (N_Q - [P = Q]) / (n_t (n_t - 1))); slots of linkers of
different types, or in "independent" mode, are not.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
RGroupSpec
|
|
required |
layout
|
SlotLayout
|
The real layout (slot counts per type). |
required |
mode
|
(fixed, independent)
|
|
"fixed"
|
fractions
|
array - like
|
|
None
|
Examples:
>>> from cage_isomer_builder.utils.rgroup import make_spec
>>> from cage_isomer_builder.utils.ensemble import LabelProbabilities
>>> spec = make_spec(4, 2, linker_patterns={"A": 2, "B": 2}) # 4 linkers, 2 slots each
>>> probs = LabelProbabilities(spec, spec.layout)
>>> probs.single(0).tolist() # a slot carries A or B with 1/4 each
[0.25, 0.25]
Source code in cage_isomer_builder/utils/ensemble.py
single ¶
Probability that one slot of a type-t linker carries each label.
Examples:
>>> from cage_isomer_builder.utils.rgroup import make_spec
>>> from cage_isomer_builder.utils.ensemble import LabelProbabilities
>>> spec = make_spec(2, 4, groups="AB")
>>> LabelProbabilities(spec, spec.layout).single(0).tolist()
[0.25, 0.25]
Source code in cage_isomer_builder/utils/ensemble.py
across ¶
Two slots on different linkers of types t and u.
Examples:
>>> from cage_isomer_builder.utils.rgroup import make_spec
>>> from cage_isomer_builder.utils.ensemble import LabelProbabilities
>>> spec = make_spec(4, 1, linker_patterns={"A": 2, "B": 2}) # one slot per linker
>>> probs = LabelProbabilities(spec, spec.layout)
>>> probs.across(0, 0).round(4).tolist() # P(A,A) = 2*1/(4*3): fixed counts are correlated
[[0.1667, 0.3333], [0.3333, 0.1667]]
Source code in cage_isomer_builder/utils/ensemble.py
within ¶
Two different slots on one linker of type t.
Examples:
>>> from cage_isomer_builder.utils.rgroup import make_spec
>>> from cage_isomer_builder.utils.ensemble import LabelProbabilities
>>> spec = make_spec(1, 2, groups="AB") # one linker, two slots, A and B
>>> LabelProbabilities(spec, spec.layout).within(0).tolist()
[[0.0, 0.5], [0.5, 0.0]]
Source code in cage_isomer_builder/utils/ensemble.py
vacancy_pattern ¶
pattern_pair_probabilities ¶
pi(P, Q) for two different linkers and pi(P) for one linker (single linker type).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
RGroupSpec
|
|
required |
mode
|
(fixed, independent)
|
"fixed": exactly |
"fixed"
|
Examples:
>>> from cage_isomer_builder.utils.rgroup import make_spec
>>> from cage_isomer_builder.utils.ensemble import pattern_pair_probabilities
>>> spec = make_spec(4, 1, linker_patterns={"A": 2, "B": 2})
>>> pair, single = pattern_pair_probabilities(spec)
>>> single.tolist(), pair.round(4).tolist()
([0.5, 0.5], [[0.1667, 0.3333], [0.3333, 0.1667]])
Source code in cage_isomer_builder/utils/ensemble.py
label_pair_probabilities ¶
Probabilities that two slots carry labels (a, b), labels numbered
1..len(spec.labels) as in spec.labels (0 = H is left out), for a
single linker type (see :class:LabelProbabilities for several).
Returns:
| Name | Type | Description |
|---|---|---|
across |
(ndarray, shape(n_labels, n_labels))
|
Two slots on different linkers. |
within |
(ndarray, shape(n_labels, n_labels))
|
Two different slots on the same linker. |
Examples:
>>> from cage_isomer_builder.utils.rgroup import make_spec
>>> from cage_isomer_builder.utils.ensemble import label_pair_probabilities
>>> spec = make_spec(3, 2, groups="A") # one A on each 2-slot linker
>>> across, within = label_pair_probabilities(spec)
>>> across.tolist(), within.tolist()
([[0.25]], [[0.0]])
Source code in cage_isomer_builder/utils/ensemble.py
single_label_probabilities ¶
Probability that one slot carries each label (single linker type).
Examples:
>>> from cage_isomer_builder.utils.rgroup import make_spec
>>> from cage_isomer_builder.utils.ensemble import single_label_probabilities
>>> single_label_probabilities(make_spec(3, 4, groups="A")).tolist()
[0.25]
Source code in cage_isomer_builder/utils/ensemble.py
ensemble_descriptor ¶
ensemble_descriptor(slot_features, fg_per_linker, groups='A', linker_patterns=None, mode='fixed', fractions=None, by='orientation', include_same_linker=False, positions=None, vectors=None, orientations=None, sample_weights=None, rotor_of_slot=None, max_distance=None)
Exact configuration-averaged FG pair descriptor (see the module docstring).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
slot_features
|
FeatureSet
|
|
required |
fg_per_linker
|
int or SlotLayout
|
Slots per linker, or the layout of linkers of several types
( |
required |
groups
|
As in :func: |
'A'
|
|
linker_patterns
|
As in :func: |
'A'
|
|
mode
|
(fixed, independent)
|
See :func: |
"fixed"
|
fractions
|
dict
|
With |
None
|
by
|
(orientation, label)
|
Type pairs by group and endo/exo class, or by group only. |
"orientation"
|
include_same_linker
|
bool
|
Also count pairs of two groups on one linker. |
False
|
positions
|
(arrays, shape(n_slots, n_samples, ...))
|
Sampled slot geometry (flexibility). Default: the static geometry. |
None
|
vectors
|
(arrays, shape(n_slots, n_samples, ...))
|
Sampled slot geometry (flexibility). Default: the static geometry. |
None
|
orientations
|
(arrays, shape(n_slots, n_samples, ...))
|
Sampled slot geometry (flexibility). Default: the static geometry. |
None
|
sample_weights
|
(array, shape(n_samples) or (n_slots, n_samples))
|
Weight of each sample (normalised per slot). Default: uniform. |
None
|
rotor_of_slot
|
sequence of int
|
Slots with the same rotor id move together (paired samples). Default: one rotor per linker when samples are given. |
None
|
max_distance
|
float
|
Periodic structures (required there): count pairs over every periodic image up to this distance, per unit cell. The configuration of the cell repeats in every cell, as for the enumerated isomers of a MOF, so a slot and its own image carry the same group, and a slot and an image of another slot of the same linker are correlated like two slots of one linker. |
None
|
Returns:
| Type | Description |
|---|---|
Descriptor
|
Weights are expected pair counts per structure (per unit cell for periodic structures). |
Examples:
>>> from cage_isomer_builder.utils.features import FeatureSet
>>> # three linkers with two FG slots each, along a line
>>> slots = FeatureSet.from_rows([dict(position=[x, 0, 0], kind="fg", owner=x // 10)
... for x in (0, 1, 10, 11, 20, 21)])
>>> from cage_isomer_builder.utils.ensemble import ensemble_descriptor, vacancy_pattern
>>> d = ensemble_descriptor(slots, 2, groups="A", by="label")
>>> d.total() # 3 groups -> 3 pairs per structure
3.0
>>> mixed = ensemble_descriptor(slots, 2, linker_patterns={"A": 2, "B": 1}, by="label")
>>> {k: round(v, 3) for k, (_, v) in mixed.summary().items()}
{('fg:A', 'fg:A'): 1.0, ('fg:A', 'fg:B'): 2.0}
>>> defect = ensemble_descriptor(slots, 2, linker_patterns={"A": 2, vacancy_pattern(2): 1},
... by="label")
>>> defect.total() # one linker missing: one A-A pair left
1.0
Source code in cage_isomer_builder/utils/ensemble.py
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 | |
isomer_ensemble_descriptor ¶
isomer_ensemble_descriptor(slot_features, isomers, fg_per_linker, weights=None, by='orientation', include_same_linker=False, max_distance=None)
FG pair descriptor averaged over explicit isomers with given weights.
Each isomer's active slots are passed to
:func:cage_isomer_builder.utils.distributions.pair_descriptor (so
periodic images are handled the same way; max_distance is required
for periodic structures).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
isomers
|
sequence of RGroupIsomer
|
|
required |
weights
|
sequence of float
|
One per isomer, normalised to sum 1. Default: each isomer equally.
Use :func: |
None
|
Examples:
>>> from cage_isomer_builder.utils.features import FeatureSet
>>> # three linkers with two FG slots each, along a line
>>> slots = FeatureSet.from_rows([dict(position=[x, 0, 0], kind="fg", owner=x // 10)
... for x in (0, 1, 10, 11, 20, 21)])
>>> from cage_isomer_builder.utils.rgroup import RGroupIsomer
>>> from cage_isomer_builder.utils.ensemble import isomer_ensemble_descriptor
>>> iso = RGroupIsomer((1, 0, 1, 0, 1, 0), ("A",), ("A", "A", "A")) # first slot of each
>>> isomer_ensemble_descriptor(slots, [iso], 2, by="label").histogram()[0].tolist()
[10.0, 20.0]
Source code in cage_isomer_builder/utils/ensemble.py
orbit_sizes ¶
Number of raw configurations each isomer stands for:
|G| / |stabiliser|, using a prepared rgroup.SlotGroup.
Examples:
>>> from cage_isomer_builder.utils import rgroup
>>> from cage_isomer_builder.utils.ensemble import orbit_sizes
>>> group = rgroup.prepare_group([[1, 0]], 2, 2) # one linker, its two slots swap
>>> isomers = rgroup.enumerate_rgroup_isomers(group, 2, 2, groups="A")
>>> orbit_sizes(group, isomers).tolist() # the one isomer stands for 2 placements
[2]
Source code in cage_isomer_builder/utils/ensemble.py
boltzmann_weights ¶
Normalised Boltzmann weights exp(-(E - E_min) / kT).
Examples:
>>> from cage_isomer_builder.utils.ensemble import boltzmann_weights
>>> boltzmann_weights([0.0, 0.05], temperature=298.15).round(3).tolist()
[0.875, 0.125]
Source code in cage_isomer_builder/utils/ensemble.py
ratio_series ¶
ratio_series(slot_features, fg_per_linker, labels=('A', 'B'), by='orientation', include_same_linker=False, max_distance=None, linker_type=None, other_groups='')
Exact descriptors for every whole-linker ratio of two groups, one group
per linker: {fraction of linkers with labels[0]: Descriptor} for
k = 0..n linkers. Distances are the same at every ratio; only the
weights of the label pairs change. max_distance is required for
periodic structures (see :func:ensemble_descriptor).
With several linker types, the ratio is varied on linker_type (a
type name) and every other type carries other_groups (bare by
default).
Examples:
>>> from cage_isomer_builder.utils.features import FeatureSet
>>> # three linkers with two FG slots each, along a line
>>> slots = FeatureSet.from_rows([dict(position=[x, 0, 0], kind="fg", owner=x // 10)
... for x in (0, 1, 10, 11, 20, 21)])
>>> from cage_isomer_builder.utils.ensemble import ratio_series
>>> series = ratio_series(slots, 2, labels=("A", "B"), by="label")
>>> [round(f, 3) for f in series] # 0, 1, 2 or 3 of the 3 linkers carry A
[0.0, 0.333, 0.667, 1.0]
>>> series[1.0].total([("fg:A", "fg:A")])
3.0
Source code in cage_isomer_builder/utils/ensemble.py
interpolate_descriptor ¶
Linear interpolation between the two descriptors of series (a
{fraction: Descriptor} dict) that bracket x. Because every
descriptor holds the same pairs with different weights, this is exactly
linear interpolation of their KDE curves.
Note: for a fixed number of linkers the exact weights are quadratic in
the fraction (e.g. P(A, A) = k (k - 1) / (n (n - 1))), so the
interpolation is approximate between grid points. For an exact value at
any fraction in a large crystal use :func:ensemble_descriptor with
mode="independent".
Examples:
>>> from cage_isomer_builder.utils.features import FeatureSet
>>> # three linkers with two FG slots each, along a line
>>> slots = FeatureSet.from_rows([dict(position=[x, 0, 0], kind="fg", owner=x // 10)
... for x in (0, 1, 10, 11, 20, 21)])
>>> from cage_isomer_builder.utils.ensemble import interpolate_descriptor, ratio_series
>>> series = ratio_series(slots, 2, labels=("A", "B"), by="label")
>>> half = interpolate_descriptor(series, 0.5) # between 1/3 and 2/3
>>> round(half.total([("fg:A", "fg:A")]), 3)
0.5