Skip to content

Guests & matching

A guest is described like a pore: by the distances between its feature sites (H-bond donors and acceptors, aromatic rings), averaged over its conformers. Comparing the guest's distances with the pore's distances between complementary sites ranks pores, isomers or guests before any host-guest energy is computed.

Conformers

Conformers can come from anywhere: a GA/PES scan, MD snapshots, or a conformer generator. Here RDKit (installed with STK) makes ten conformers of ascorbic acid with MMFF energies:

from ase import Atoms
from rdkit import Chem
from rdkit.Chem import AllChem

mol = Chem.AddHs(Chem.MolFromSmiles("OC[C@H](O)[C@H]1OC(=O)C(O)=C1O"))   # ascorbic acid
ids = AllChem.EmbedMultipleConfs(mol, numConfs=10, randomSeed=42)
results = AllChem.MMFFOptimizeMoleculeConfs(mol)                      # [(converged, energy)]
symbols = [atom.GetSymbol() for atom in mol.GetAtoms()]
conformers = [Atoms(symbols, positions=mol.GetConformer(i).GetPositions()) for i in ids]
energies = [energy for _, energy in results]                          # kcal/mol

Guest descriptor

from cage_isomer_builder.utils.matching import conformer_descriptor

guest = conformer_descriptor(conformers, energies=energies, unit="kcal/mol", temperature=298.15)
print(guest.keys())    # [('acceptor', 'acceptor'), ('acceptor', 'donor'), ('donor', 'donor')]
print(round(sum(guest.info["weights"]), 6))   # 1.0: Boltzmann weights of the conformers

Without energies every conformer counts equally (e.g. MD snapshots, already Boltzmann-distributed by the simulation); weights= sets them directly.

Matching a pore

A guest donor-acceptor pair at distance d fits a pore acceptor-donor pair at about d. For every guest pair type, the pore pairs of the complementary types are pooled and the two distance distributions compared after normalising each (overlap from 0 to 1); the score is the average over the guest's pair types, weighted by the guest's pair counts.

guest site matched by pore site
donor acceptor
acceptor donor, open metal site
pi pi
from cage_isomer_builder import example_data
from cage_isomer_builder.cage import Tri4Di6CageBuilder
from cage_isomer_builder.utils.matching import match_descriptors

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()

pore = cage.get_descriptor(kinds=("pi", "donor", "acceptor", "open_metal"))
result = match_descriptors(pore, guest)
print(result.unmatched)    # guest pair types with no complementary pore pairs

This pore has acceptors (the node's mu3-O) but no donors, so the guest's acceptor pairs find no partner. Functionalising the pore with NH2 adds donors:

from cage_isomer_builder.utils.features import detect_features
from cage_isomer_builder.utils.distributions import pair_descriptor
from cage_isomer_builder.utils.functionalise import functionalise_isomer_sites
from cage_isomer_builder.utils.sites import read_isomer

isomers = cage.enumerate_isomers(output_path="isomers", limit=2)
hosts = {}
for isomer in isomers:
    name = "-".join(map(str, isomer))
    decorated = functionalise_isomer_sites(read_isomer(f"isomers/{name}.xyz"),
                                           example_data.read("NH2"))
    hosts[name] = pair_descriptor(detect_features(decorated))

for name, desc in hosts.items():
    print(name, round(match_descriptors(desc, guest).score, 3))

rank_hosts(hosts, guest) returns the same scores sorted best first. The score is a screening measure, not an energy: dock the best candidates and compute their energies (see Host-guest docking). A guest with fewer than two features has no pairs at all; its result then has defined=False.

Pass complementarity= to change which kinds match, bandwidth= (default 0.25 A) for how much mismatch a pocket tolerates, and metric="bhattacharyya" for the Bhattacharyya coefficient instead of the overlap.