Flexibility¶
Linker ring rotation and isotropic scaling. Used by CageBuilder.rotors(), scaled() and distance_scaling(): cage_isomer_builder.utils.flexibility.
Framework flexibility for descriptors: linker ring rotation and isotropic pore expansion/contraction.
Ring rotation
A rotor is an aromatic ring of a linker that can turn about the axis through its two backbone atoms: the ring atoms bonded (outside the ring) to the rest of the framework, e.g. C1 and C4 of a 1,4-phenylene. Everything attached to the ring except through those two backbone bonds (its H atoms, R sites, substituents) turns with it; the backbone itself does not move, so every bond length and every bond to the node is preserved exactly.
A backbone atom is a ring atom whose exocyclic heavy neighbour leads to a metal, to another ring, or out of the linker; an exocyclic neighbour that only leads to a terminal substituent (NH2, CH3, ...) is not backbone. Rings with other than two backbone atoms (e.g. a tritopic core) are not rotors.
:func:rotation_samples gives every slot's position at each angle, ready for
:func:cage_isomer_builder.utils.ensemble.ensemble_descriptor: slots on one
ring move together; different rings move independently. Angle weights may
come from an energy profile (e.g. a UFF4MOF torsion scan, via
:func:cage_isomer_builder.utils.ensemble.boltzmann_weights); the default is
uniform.
Pore scaling
:func:scale_structure moves every building block rigidly so its centroid's
distance from the pore centre is multiplied by a factor (a periodic cell is
scaled instead, keeping each block's internal geometry). Internal geometry
is untouched, so the result shows how each FG-FG distance responds to an
isotropic expansion or contraction; :func:distance_scaling gives the slope
d(distance)/d(factor) of every pair.
Rotor
dataclass
¶
Attributes:
| Name | Type | Description |
|---|---|---|
ring |
tuple of int
|
|
axis_atoms |
(int, int)
|
The two backbone ring atoms; the rotation axis passes through them. |
moving |
tuple of int
|
Every atom that turns (ring atoms other than the axis atoms, and everything hanging off the ring). |
linker |
int
|
Index of the linker (in the list given to :func: |
Examples:
>>> from cage_isomer_builder import example_data
>>> from cage_isomer_builder.cage import Tri4Di6CageBuilder
>>> cage = Tri4Di6CageBuilder(node=example_data.path("uio66_tri_node"),
... linker=example_data.path("bdc"), scale_multiplier=0.225)
>>> _ = cage.build()
>>> _ = cage.optimise(include_charges=False, logfile=None)
>>> _ = cage.functionalise()
>>> atoms, adjacency = cage.to_ase(), cage.adjacency()
>>> linkers, nodes = cage.building_blocks()
>>> from cage_isomer_builder.utils.flexibility import find_rotors
>>> rotor = find_rotors(atoms, linkers, adjacency)[0]
>>> len(rotor.ring), len(rotor.axis_atoms), len(rotor.moving) # ring C, axis C, moving atoms
(6, 2, 8)
find_rotors ¶
Rotatable aromatic rings (see the module docstring).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
atoms
|
Atoms
|
|
required |
linker_atom_indices
|
list of list of int
|
Atoms of each linker; only rings inside a linker are returned, and a neighbour outside it counts as backbone. Default: every ring. |
None
|
Examples:
>>> from cage_isomer_builder import example_data
>>> from cage_isomer_builder.cage import Tri4Di6CageBuilder
>>> cage = Tri4Di6CageBuilder(node=example_data.path("uio66_tri_node"),
... linker=example_data.path("bdc"), scale_multiplier=0.225)
>>> _ = cage.build()
>>> _ = cage.optimise(include_charges=False, logfile=None)
>>> _ = cage.functionalise()
>>> atoms, adjacency = cage.to_ase(), cage.adjacency()
>>> linkers, nodes = cage.building_blocks()
>>> from cage_isomer_builder.utils.flexibility import find_rotors
>>> len(find_rotors(atoms, linkers, adjacency)) # one phenylene ring per linker
6
Source code in cage_isomer_builder/utils/flexibility.py
rotor_axis ¶
Origin (first axis atom) and unit axis of a rotor.
Examples:
>>> from cage_isomer_builder import example_data
>>> from cage_isomer_builder.cage import Tri4Di6CageBuilder
>>> cage = Tri4Di6CageBuilder(node=example_data.path("uio66_tri_node"),
... linker=example_data.path("bdc"), scale_multiplier=0.225)
>>> _ = cage.build()
>>> _ = cage.optimise(include_charges=False, logfile=None)
>>> _ = cage.functionalise()
>>> atoms, adjacency = cage.to_ase(), cage.adjacency()
>>> linkers, nodes = cage.building_blocks()
>>> import numpy as np
>>> from cage_isomer_builder.utils.flexibility import find_rotors, rotor_axis
>>> origin, axis = rotor_axis(atoms, find_rotors(atoms, linkers, adjacency)[0])
>>> round(float(np.linalg.norm(axis)), 6)
1.0
Source code in cage_isomer_builder/utils/flexibility.py
rotate_rotor ¶
Copy of atoms with one ring turned by angle_deg.
Examples:
>>> from cage_isomer_builder import example_data
>>> from cage_isomer_builder.cage import Tri4Di6CageBuilder
>>> cage = Tri4Di6CageBuilder(node=example_data.path("uio66_tri_node"),
... linker=example_data.path("bdc"), scale_multiplier=0.225)
>>> _ = cage.build()
>>> _ = cage.optimise(include_charges=False, logfile=None)
>>> _ = cage.functionalise()
>>> atoms, adjacency = cage.to_ase(), cage.adjacency()
>>> linkers, nodes = cage.building_blocks()
>>> from cage_isomer_builder.utils.flexibility import find_rotors, rotate_rotor
>>> rotor = find_rotors(atoms, linkers, adjacency)[0]
>>> turned = rotate_rotor(atoms, rotor, 30.0)
>>> bonds = [(i, j) for i in adjacency for j in adjacency[i] if i < j]
>>> bool(max(abs(atoms.get_distance(i, j) - turned.get_distance(i, j)) for i, j in bonds) < 1e-9)
True
Source code in cage_isomer_builder/utils/flexibility.py
rotation_samples ¶
Slot geometry at every rotation angle.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rotors
|
list of Rotor
|
|
required |
slot_indices
|
sequence of int
|
Site atoms in slot order. |
required |
angles
|
sequence of float
|
Rotation angles in degrees (applied to every ring independently). |
required |
centre
|
array - like
|
Pore centre, to re-classify endo/exo at each angle. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
positions |
(ndarray, shape(n_slots, n_angles, 3))
|
|
vectors |
(ndarray, shape(n_slots, n_angles, 3))
|
Unit ring-C -> site direction. |
orientations |
np.ndarray of str, shape (n_slots, n_angles)
|
|
rotor_of_slot |
np.ndarray of int
|
Rotor of each slot (-1: the slot is on no rotor and stays put). |
Examples:
>>> from cage_isomer_builder import example_data
>>> from cage_isomer_builder.cage import Tri4Di6CageBuilder
>>> cage = Tri4Di6CageBuilder(node=example_data.path("uio66_tri_node"),
... linker=example_data.path("bdc"), scale_multiplier=0.225)
>>> _ = cage.build()
>>> _ = cage.optimise(include_charges=False, logfile=None)
>>> _ = cage.functionalise()
>>> atoms, adjacency = cage.to_ase(), cage.adjacency()
>>> linkers, nodes = cage.building_blocks()
>>> from cage_isomer_builder.utils.flexibility import find_rotors, rotation_samples
>>> rotors = find_rotors(atoms, linkers, adjacency)
>>> pos, vec, orientation, rotor_of_slot = rotation_samples(
... atoms, rotors, cage._fg_anchor_indices, [0, 90, 180, 270],
... centre=cage.pore_centre(), adjacency=adjacency)
>>> pos.shape, sorted(set(rotor_of_slot.tolist())) # 24 slots x 4 angles
((24, 4, 3), [0, 1, 2, 3, 4, 5])
Source code in cage_isomer_builder/utils/flexibility.py
scale_structure ¶
Copy of atoms with every building block moved rigidly so its
centroid-to-centre distance is multiplied by factor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
groups
|
list of list of int
|
Atoms of each building block (nodes and linkers). Atoms in no group stay where they are, so every atom should be in some group. |
required |
centre
|
array - like
|
Default: centroid of all atoms (finite structures). For periodic
structures the cell is scaled by |
None
|
Examples:
>>> from cage_isomer_builder import example_data
>>> from cage_isomer_builder.cage import Tri4Di6CageBuilder
>>> cage = Tri4Di6CageBuilder(node=example_data.path("uio66_tri_node"),
... linker=example_data.path("bdc"), scale_multiplier=0.225)
>>> _ = cage.build()
>>> _ = cage.optimise(include_charges=False, logfile=None)
>>> _ = cage.functionalise()
>>> atoms, adjacency = cage.to_ase(), cage.adjacency()
>>> linkers, nodes = cage.building_blocks()
>>> import numpy as np
>>> from cage_isomer_builder.utils.flexibility import scale_structure
>>> centre = cage.pore_centre()
>>> bigger = scale_structure(atoms, linkers + nodes, 1.1, centre)
>>> node = nodes[0]
>>> r0 = np.linalg.norm(atoms.positions[node].mean(axis=0) - centre)
>>> r1 = np.linalg.norm(bigger.positions[node].mean(axis=0) - centre)
>>> round(float(r1 / r0), 6) # each block moves out by 10 %
1.1
Source code in cage_isomer_builder/utils/flexibility.py
distance_scaling ¶
How every slot-slot distance changes with isotropic scaling.
Returns:
| Name | Type | Description |
|---|---|---|
pairs |
(ndarray, shape(n_pairs, 2))
|
Slot pairs (i < j). |
distances |
(ndarray, shape(n_factors, n_pairs))
|
|
slopes |
(ndarray, shape(n_pairs))
|
Least-squares d(distance)/d(factor). A pair whose two slots sit on building blocks that move apart rigidly has slope close to the distance between those blocks' centroids; two slots on one block have slope 0. |
Examples:
>>> from cage_isomer_builder import example_data
>>> from cage_isomer_builder.cage import Tri4Di6CageBuilder
>>> cage = Tri4Di6CageBuilder(node=example_data.path("uio66_tri_node"),
... linker=example_data.path("bdc"), scale_multiplier=0.225)
>>> _ = cage.build()
>>> _ = cage.optimise(include_charges=False, logfile=None)
>>> _ = cage.functionalise()
>>> atoms, adjacency = cage.to_ase(), cage.adjacency()
>>> linkers, nodes = cage.building_blocks()
>>> from cage_isomer_builder.utils.flexibility import distance_scaling
>>> pairs, distances, slopes = distance_scaling(
... atoms, linkers + nodes, cage._fg_anchor_indices, (0.95, 1.0, 1.05), cage.pore_centre())
>>> distances.shape, bool((slopes >= -1e-9).all()) # 3 factors x 276 slot pairs
((3, 276), True)