plinder.data.annotations.ccd_ligand_dbs#
CCD-anchored ligand relationship databases (MMP + ECFP4/Tanimoto).
Builds the ligand relationship databases over the fixed Chemical Component Dictionary universe rather than one ingest run’s ligands, so the artifacts are stable across releases and dataset ligands join by CCD identity first and fall back to chemistry only when they have no CCD identity.
See make_ligand_dbs.md for the design and the wire-in checklist.
Notes#
The component table deliberately reuses the column names of the existing
unique-SMILES table (ligand_smiles_id / ligand_rdkit_canonical_smiles)
so plinder.data.annotations.mmpdb_utils and the ECFP4 writer in
plinder.data.annotations.get_similarity_scores can be reused verbatim on
this universe.
- plinder.data.annotations.ccd_ligand_dbs.ccd_component_table(*, exclude_non_druglike: bool = True, exclude_polymer_linking: bool = False, limit: int | None = None) DataFrame[source]#
Build the active-CCD node universe with canonical SMILES.
- Parameters:
- exclude_non_druglikebool, default=True
Apply plinder’s own
is_excluded_mol()filter, the same rule that marks a dataset ligand an artifact. Keeps the CCD universe consistent with native ligand treatment rather than inventing a second notion of “real ligand”. Of the dataset’s four exclusion sources (these chemistry rules, the artifact bad-list, the cofactor list and the dummy codes) only this one applies here: listed artifacts and cofactors stay nodes, since a ligand may still be related to them; dummies and ions drop out for lacking a molecule.- exclude_polymer_linkingbool, default=False
Drop
*-linkingcomponents (amino acids, nucleotides, saccharides). plinder treats these as polymer/receptor rather than ligand, so enable this for a ligand-only universe; the default keeps the broad chemical reference set.- limitint or None
Process only the first limit released components (testing aid).
- Returns:
- pd.DataFrame
Columns
ccd_id,ligand_rdkit_canonical_smiles(stereo included, the key the dataset deduplicates ligands on and the one chemistry matching uses),ligand_smiles_id,num_heavy_atomsandccd_type. Theligand_*names intentionally match the unique-SMILES table so the MMP and ECFP4 builders can be reused.
Notes
Only
pdbx_release_status == "REL"components are kept, so obsolete entries never enter the databases. Placeholder codes are dropped via plinder’s ownlig_has_dummies().
- plinder.data.annotations.ccd_ligand_dbs.ccd_universe_signature(components: DataFrame) str[source]#
Digest of the component universe, for the build manifest.
- plinder.data.annotations.ccd_ligand_dbs.ccd_dbs_dir(data_dir: Path) Path[source]#
Directory holding the CCD-anchored database artifacts.
- plinder.data.annotations.ccd_ligand_dbs.ccd_fingerprint_path(data_dir: Path) Path[source]#
Path of the CCD ECFP4 table, in the layout the Tanimoto scorer expects.
plinder.data.annotations.get_similarity_scores.ligand_scores()resolves<data_dir>/fingerprints/ligands_per_smiles.parquetinternally, so mirroring that layout underccd_dbslets the existing scorer run on the CCD universe unchanged - no new parameter, no edit to a shared file.
- plinder.data.annotations.ccd_ligand_dbs.build_ccd_tanimoto_scores(components: DataFrame, data_dir: Path, *, minimum_similarity: float = 30.0, batch_size: int = 5000) Path[source]#
Compute ECFP4 Tanimoto edges over the CCD universe.
- Parameters:
- componentspd.DataFrame
Output of
ccd_component_table(); supplies the node ids.- data_dirPath
Release directory; scores land in
<data_dir>/ccd_dbs/ligand_scores.- minimum_similarityfloat
Percentage cutoff below which edges are dropped.
- batch_sizeint
Node ids scored per output shard, mirroring the ingest scatter so a 50k-node universe never materialises an all-pairs matrix at once.
- Returns:
- Path
Directory holding the per-batch score shards.
Notes
Delegates to the same
ligand_scoresroutine the ingest pipeline uses, pointed at the CCDfingerprintslayout, so the edge semantics and the ECFP4 metadata check are identical to the dataset-level scores.
- plinder.data.annotations.ccd_ligand_dbs.build_ccd_ecfp_db(components: DataFrame, output_path: Path) Path[source]#
Write the ECFP4/1024 fingerprint table over the CCD universe.
Mirrors
fingerprints/ligands_per_smiles.parquet(same columns, same parquet metadata) so the existing similarity/clustering machinery can run against this node universe unchanged.
- plinder.data.annotations.ccd_ligand_dbs.build_ccd_mmp_db(components: DataFrame, output_path: Path, *, scratch_dir: Path, threads: int = 4, fragments_path: Path | None = None, exclusions_path: Path | None = None) Path[source]#
Write matched molecular pairs over the CCD universe.
Reuses the existing mmpdb flow (
smi_split -> fragment -> fragdb_partition -> index) unchanged; only the input universe differs.- Parameters:
- fragments_pathPath or None
When given, persist every fragmentation of the universe there, keyed by constant part;
query_ccd_mmp_pairs()pairs molecules outside the universe against it without a rebuild.
Notes
The universe is CCD components only; composite ligands are deliberately not added. Pairing joins fragmentations on their shared constant part, and residues of one class leave the same constant when their side chain is cut - every amino acid leaves the backbone - so composites of that class all pair with one another: recall without precision. Fragmentation also enumerates cuts over acyclic single bonds, of which a composite has one per linkage plus its side chains, so cost grows combinatorially with residue count while those shared constants make the pair count quadratic in the number of composites. Relate composites by their residue graph instead (
composite_similarity()), and use this database only to describe a residue substitution, never to decide whether two composites are related.
- plinder.data.annotations.ccd_ligand_dbs.query_ccd_mmp_pairs(queries: Mapping[str, str], data_dir: Path, *, max_variable_heavies: int | None = 10, max_heavies_transf: int | None = None) DataFrame[source]#
Matched molecular pairs between molecules outside the universe and the CCD.
Runs mmpdb’s pairing rule against the persisted fragmentation dictionary: a query and a component are a pair when a fragmentation of each leaves the same constant part (cut count and symmetry class included) with different variable parts; a one-cut constant whose hydrogen-capped form is itself a molecule of the other side pairs against the hydrogen fragment, as in
mmpdb index; two enumerated-chirality constants never pair. SMIRKS come frommmpdblib.index_algorithm.cansmirks, so transformations are written exactly as inccd_mmp_pairs.parquet. Defaults are mmpdb’s index defaults (--max-variable-heavies 10).- Parameters:
- queriesmapping
{query_id: smiles}for the molecules to place (novel or external).- data_dirPath
Release directory holding
ccd_dbs.- max_variable_heavies, max_heavies_transfint or None
mmpdb index filters on the variable part and on the change in its size.
- Returns:
- pd.DataFrame
One row per (query, component, shared core, transformation) with
query_id,query_smiles,ligand_smiles_id,ccd_id,ligand_smiles,transformation(query >> component),shared_core_smiles,num_cuts,shared_core_num_heavy_atoms.
Notes
Components with no fragmentation of their own (very small molecules) cannot be hydrogen partners, since the molecule map is read from the dictionary.
TODO (discuss): the pair relation is symmetric; the directed quantity plinder uses is core coverage per side (
shared_core_fractioninccd_mmp_pairs.parquet), which this output lacks. Also whether mmpdb’s index defaults (10-atom variable cap, no coverage floor) and the downstream 5-atom core floor are the cutoffs wanted for the CCD reference.
- class plinder.data.annotations.ccd_ligand_dbs.CcdMatch[source]#
Bases:
TypedDictOne ligand resolved against the CCD universe.
- match_kind: str#
- matched_ccd_ids: list[str]#
- component_ccd_ids: list[str]#
- class plinder.data.annotations.ccd_ligand_dbs.CcdIndex(ccd_ids: frozenset[str], smiles_to_ccd_ids: dict[str, list[str]], released_ids: frozenset[str])[source]#
Bases:
NamedTupleLookups over the universe that
match_ligand_to_ccd()needs.- ccd_ids: frozenset[str]#
Alias for field number 0
- smiles_to_ccd_ids: dict[str, list[str]]#
Alias for field number 1
- released_ids: frozenset[str]#
Alias for field number 2
- plinder.data.annotations.ccd_ligand_dbs.ccd_index(components: DataFrame) CcdIndex[source]#
Build the matcher’s lookups from
ccd_component_table()output.
- plinder.data.annotations.ccd_ligand_dbs.canonical_smiles(smiles: str | None) str | None[source]#
RDKit canonical SMILES with stereo, the form the CCD table stores.
- plinder.data.annotations.ccd_ligand_dbs.match_ligand_to_ccd(ccd_code: str, ligand_smiles: str | None, index: CcdIndex) CcdMatch[source]#
Resolve one ligand against the CCD universe by code, then by chemistry.
ligand_smilesmust come throughcanonical_smiles(), as the index keys do.match_kindis one ofexactThe ligand is a universe component: its single code is one, or its whole-molecule canonical SMILES equals a component’s. The second test is what lets one molecule match however it was deposited, e.g. lactose as
LATor as the linked sugarsGAL-BGC. Stereo is part of the identity, as in the dataset’s own ligand key: the ligand’s SMILES carries the stereo perceived from its coordinates, the component’s the stereo of its ideal coordinates, so cellobiose and maltose never match lactose.excludedA single code that is a released CCD component the universe filters out (ions, artifacts, placeholders): known, not novel.
novelNo component is this molecule. Relating it to the universe is the fingerprint’s job (
query_ccd_tanimoto(),query_ccd_mmp_pairs()).
component_ccd_idslists a composite’s codes that are universe components, as annotation only: a missing component says nothing about what the whole molecule is.
- plinder.data.annotations.ccd_ligand_dbs.load_ccd_components(data_dir: Path) DataFrame[source]#
Load the persisted component universe the artifacts were built from.
Always prefer this over rebuilding with
ccd_component_table()when joining against existing artifacts: node ids are positions in this table, so a table built with different filters would silently mis-map every id.
- plinder.data.annotations.ccd_ligand_dbs.build_ligand_ccd_match(ligands: DataFrame, components: DataFrame, output_path: Path | None = None) DataFrame[source]#
Map dataset ligands onto the CCD universe.
- Parameters:
- ligandspd.DataFrame
ligand_id,ligand_ccd_codeandligand_smiles(the whole-molecule SMILES; composites are one connected molecule), plus- componentspd.DataFrame
Output of
ccd_component_table().- output_pathPath or None
When given, write the join table as parquet.
- Returns:
- pd.DataFrame
ligand_id,ligand_ccd_code,match_kind,matched_ccd_ids,ccd_node_ids(nodes of the matched components),component_ccd_ids.
Notes
Emitted as a sidecar join table rather than annotation columns, so building these databases never contends with the index schema.
- plinder.data.annotations.ccd_ligand_dbs.make_ligand_ccd_match(*, data_dir: Path) Path[source]#
Write
ccd_dbs/ligand_ccd_match.parquetfor the collated annotation table.
- plinder.data.annotations.ccd_ligand_dbs.query_ccd_tanimoto(queries: Mapping[str, str], data_dir: Path, *, minimum_similarity: float = 30.0) DataFrame[source]#
ECFP4/1024 Tanimoto neighbours of molecules outside the universe.
The same fingerprint, table and percentage cutoff as
ligand_scores(), so a novel or external ligand (a composite included, through its whole-molecule SMILES) is related to the CCD exactly as dataset ligands are to each other.- Returns:
- pd.DataFrame
query_id,query_smiles,ligand_smiles_id,tanimoto_similarity_ecfp4_1024(percent),ccd_id; best hit first.
- plinder.data.annotations.ccd_ligand_dbs.ccd_parity_path(data_dir: Path) Path[source]#
Path of the CCD-vs-CCD PARITY-like score table.
- class plinder.data.annotations.ccd_ligand_dbs.CcdParityTable(rows: DataFrame)[source]#
Bases:
objectMono-to-mono PARITY-like matches keyed by CCD code pair, for composite scoring.
Rows carry
ccd_id_1,ccd_id_2,parity_similarity(percent), the directionalparity_coverage_1andparity_coverage_2(percent) and the largest-fragment atom namesfragment_atoms_1->fragment_atoms_2, one list per equally good fragment, the scored one first.Notes
Each unordered pair is stored once, so the symmetric score and the fragment mapping are not duplicated. The direction is resolved on access:
1 -> 2and2 -> 1read the same row, with the mapping inverted and the two coverage columns swapped (mapping(),mappings(),coverage()).directed()expands the table to one row per direction in memory for joins on query and target.The class API works in fractions in [0, 1]; the parquet stores percent. Coverage is the mean of the atom and bond fractions of one molecule matched, see
RascalParityMatch.coverage(). A table built before the coverage columns existed still loads, butcoverage()anddirected()raise until it is rebuilt withbuild_ccd_parity_scores().- classmethod load(data_dir: Path) CcdParityTable#
The precompiled table of
build_ccd_parity_scores().
- classmethod compute(codes: Iterable[str], *, threads: int = 1) CcdParityTable#
Match every pair of codes now, for small residue sets and tests.
- similarity(first: str, second: str) float#
Node kernel: 1 for identical codes, the table score, 0 when absent.
- coverage(first: str, second: str) float#
Fraction of first matched by second: 1 for identical codes, 0 when absent.
Directional, unlike
similarity(): a small molecule inside a larger one is fully covered as first and only partly as second.
- directed() DataFrame#
One row per direction:
query_ccd_id,target_ccd_id,parity_similarityand theparity_coverageof the query, in [0, 1].
- mappings(first: str, second: str) list[dict[str, str]]#
Equally good largest fragments of first onto second, scored one first.
- mapping(first: str, second: str) dict[str, str]#
The scored fragment of first onto second; empty when absent.
- plinder.data.annotations.ccd_ligand_dbs.build_ccd_parity_scores(components: DataFrame, data_dir: Path, *, threads: int = 4, batch_size: int = 5000) Path[source]#
Score every ECFP4 edge of the CCD universe with
rascal_parity_score().The Tanimoto shards written by
build_ccd_tanimoto_scores()are the prefilter: each unordered pair above the ECFP4 cutoff is scored once, so the graph-based score never runs over the all-pairs matrix. Molecules are built from the CCD atoms so the stored fragment carries CCD atom names, which is whatcomposite_parity()joins on. A pair with an unbuildable molecule scores NaN rather than being dropped.- Returns:
- Path
Parquet with
ligand_smiles_id_1 < ligand_smiles_id_2,tanimoto_similarity_ecfp4_1024,parity_similarity(percent), the directionalparity_coverage_1andparity_coverage_2(percent of each molecule matched, seeRascalParityMatch.coverage()) and the largest-fragment atom namesfragment_atoms_1->fragment_atoms_2.
- plinder.data.annotations.ccd_ligand_dbs.query_ccd_parity(queries: Mapping[str, str], data_dir: Path, *, minimum_similarity: float = 30.0, threads: int = 4) DataFrame[source]#
PARITY-like scores of molecules outside the universe against their ECFP4 neighbours.
Extends the precompiled CCD-vs-CCD table to ligands that only exist at dataset build time (novel molecules, composites through their whole-molecule SMILES): the same Tanimoto prefilter as
query_ccd_tanimoto(), then the same score asbuild_ccd_parity_scores()on each hit.- Returns:
- pd.DataFrame
query_ccd_tanimoto()columns plusparity_similarityand the directionalparity_coverage_1(query) andparity_coverage_2(universe molecule), all percent; best PARITY-like hit first.
- plinder.data.annotations.ccd_ligand_dbs.make_ccd_ligand_dbs(*, data_dir: Path, scratch_dir: Path, threads: int = 4, force_update: bool = False, limit: int | None = None, build_tanimoto_scores: bool = True, build_parity_scores: bool = False, minimum_similarity: float = 30.0) Path[source]#
Build CCD-anchored MMP and ECFP4 databases, optionally scoring PARITY.
- Parameters:
- data_dirPath
Release directory; artifacts land in
<data_dir>/ccd_dbs.- scratch_dirPath
Working area for the mmpdb fragment/index run.
- threadsint
Parallelism for the mmpdb fragmentation and indexing steps.
- force_updatebool
Rebuild even when the manifest matches.
- limitint or None
Restrict to the first limit released components (testing aid).
- build_tanimoto_scoresbool
Build the Tanimoto edges.
- build_parity_scoresbool
Also score every retained CCD pair with PARITY. This is an expensive optional step; targeted PARITY queries remain available separately.
- minimum_similarityfloat
ECFP4 Tanimoto percentage cutoff for retained pairs.
- Returns:
- Path
The
ccd_dbsdirectory.
Notes
The manifest records the component universe, mmpdb version, and score settings. Matching builds reuse their files; changed settings rebuild them.
- plinder.data.annotations.ccd_ligand_dbs.ccd_code_sequence(ccd_code: str) list[str][source]#
Ordered component codes of a (possibly composite) ligand code.
No CCD id contains
-(ids are at most five characters), so splitting on the hyphen recovers the components unambiguously.
- plinder.data.annotations.ccd_ligand_dbs.component_sequence_identity(query: Sequence[str], target: Sequence[str]) float[source]#
Fractional identity between two component-code sequences.
- Parameters:
- query, targetsequence of str
Ordered CCD component codes, e.g.
["SER", "TPO", "GLY"]for a peptide,["DA", "DT", "DG"]for DNA,["NAG", "BMA"]for a glycan.
- Returns:
- float
Identity in
[0, 1]: matched positions over the longer sequence.
Notes
Applies to any covalently-linked biopolymer composite - peptide, RNA/DNA or oligosaccharide - because in every case the discriminating signal is the component sequence, not the chemistry:
mmpdb cuts rotatable single bonds. The peptide backbone amide and the nucleic-acid phosphodiester are both such bonds, so every peptide and every oligonucleotide shatters into the same repeating residue fragments.
ECFP4’s radius-2 window is dominated by the repeating backbone motif (
-NH-CO-CH-or sugar-phosphate). Nucleic acids are worse than peptides here: with only four canonical nucleotides the fingerprint space is far more degenerate than with twenty amino acids.Worst of all for sugars: plinder builds ECFP4 with
include_chirality=false, so hexose stereoisomers (glucose / galactose / mannose) have identical connectivity and therefore identical fingerprints. Nothing but the component code separates them.
Alignment uses
difflib.SequenceMatcherover tokens, since residue codes are multi-character and the protein-sequence aligners do not apply.Caveat: this models a linear sequence. Branched glycans are trees flattened into residue order, so identity here is a weak proxy for them - exact-set or topology-aware comparison is more appropriate for branched composites.
- plinder.data.annotations.ccd_ligand_dbs.match_composite_by_sequence(ccd_code: str, reference_codes: Mapping[str, str], *, min_identity: float = 0.8) list[tuple[str, float]][source]#
Rank reference ligands whose component-code sequence matches the query.
Works for any biopolymer composite (peptide, RNA/DNA, oligosaccharide); see
component_sequence_identity()for why sequence beats chemistry here.- Parameters:
- ccd_codestr
Query ligand code (composite, hyphen-joined).
- reference_codesmapping
{reference_id: ccd_code}to compare against.- min_identityfloat
Minimum fractional identity to report.
- Returns:
- list of (reference_id, identity)
Sorted by descending identity; empty when nothing clears the threshold.
Notes
The sequence arm of the matching strategy: name first, chemistry never. Use it instead of fingerprint or MMP similarity for any linked-residue composite.
- class plinder.data.annotations.ccd_ligand_dbs.ResidueGraph(labels: tuple[str, ...], adjacency: tuple[frozenset[int], ...], roots: tuple[int, ...], donors: tuple[frozenset[int], ...] = (), atoms: tuple[frozenset[str], ...] = (), links: tuple[tuple[int, int, str, str], ...] = ())[source]#
Bases:
NamedTupleRooted residue-level graph of a composite ligand.
labels[i]is residue i’s CCD code,adjacency[i]the residues it is covalently bonded to, androotsthe residues that only accept linkages. This is the connectivityccd_codecannot express: the code string is one arbitrary traversal, so any comparison built on it is order-dependent and blind to branching.The rooting matters as much as the edges. Undirected, a linear and a branched trisaccharide are the same three-node path, so a comparison free to pick its own roots scores them identical.
rootsfixes the traversal chemically instead - seeresidue_graph_from_atoms().donors[i]holds the residues donating a linkage into residue i, i.e. the edges directed by chemistry. It orients traversal of a macrocycle, which has no terminus to root at, and may be left empty for a graph built without structural information.atoms[i]is the heavy-atom names present in residue i andlinkseach inter-residue bond as(i, j, atom_in_i, atom_in_j); both empty for a graph built without structural information.- labels: tuple[str, ...]#
Alias for field number 0
- adjacency: tuple[frozenset[int], ...]#
Alias for field number 1
- roots: tuple[int, ...]#
Alias for field number 2
- donors: tuple[frozenset[int], ...]#
Alias for field number 3
- atoms: tuple[frozenset[str], ...]#
Alias for field number 4
- links: tuple[tuple[int, int, str, str], ...]#
Alias for field number 5
- donors_of(residue: int) frozenset[int]#
Residues donating a linkage into residue, empty when undirected.
- property is_connected: bool#
Whether every residue is reachable from the first one.
- property is_tree: bool#
- plinder.data.annotations.ccd_ligand_dbs.residue_graph_from_atoms(atoms: struc.AtomArray) ResidueGraph[source]#
Build the rooted residue graph of a ligand from its bond graph.
- Parameters:
- atomsstruc.AtomArray
Ligand atoms carrying
bonds, as produced bybuild_biounit.
- Returns:
- ResidueGraph
Residues as nodes, inter-residue covalent bonds as edges, rooted at the residues that only accept linkages.
- Raises:
- ValueError
If the atoms carry no bond graph, which is the whole input here.
Notes
Linkages are directed by the chemistry of the bond rather than by residue numbering, which is not comparable across entries. In both families plinder sees, one side donates a carbon and the other accepts through a heteroatom: a glycosidic bond runs anomeric
C1into the next ring’s oxygen, a peptide bond runs carbonylCinto the next residue’s nitrogen. Directing every linkage donor -> acceptor therefore roots the graph at the reducing end of a glycan and the C-terminus of a peptide, both canonical and both derived from the structure alone.Cyclic residue graphs (cyclic peptides, macrocycles) have no such terminus; every residue is then reported as a root, and comparison falls back to the best alignment over root pairs.
- plinder.data.annotations.ccd_ligand_dbs.ccd_code_residue_graph(ccd_code: str) ResidueGraph[source]#
Residue graph implied by a hyphenated
ccd_code, assuming linearity.Fallback for records that carry only the code string. It reproduces the linear assumption of
component_sequence_identity(), so preferresidue_graph_from_atoms()whenever the structure is available.
- plinder.data.annotations.ccd_ligand_dbs.composite_similarity(query: ResidueGraph, target: ResidueGraph, *, node_similarity: Callable[[str, str], float] | None = None) float[source]#
Similarity of two composite ligands following their connectivity.
A rooted tree alignment: residues match residues by a node score and subtrees are aligned recursively, each level resolved by an optimal (1:1) assignment between the two child sets. Unlike sequence identity this follows the actual bond graph, so branch topology is respected and the arbitrary residue order in
ccd_codeis irrelevant.This is not Feature Trees (Rarey & Dixon 1998), though it shares the tree-alignment shape and, with a graded
node_similarity, the fuzzy node comparison. FTrees’ defining step is a split search in which one node may match a contiguous set of nodes; matching here is strictly one residue to one residue.That 1:1 restriction is a real limitation, not a free simplification. The CCD assigns single codes to molecules that are chemically composites (
LATfor the same molecule asGAL-BGC,SUCforGLC-FRU,GSHfor a tripeptide), so the same chemistry does appear at two granularities, and this function scores such pairs 0.0 - it can only see residue labels, which share nothing across a regrouping.This function therefore measures topology agreement given a decomposition, and is only meaningful between ligands decomposed comparably. For a measure that does not assume that, use
ligand_features()withligand_similarity(), where molecular and residue descriptions share one feature space and the decomposition is not privileged.- Parameters:
- query, targetResidueGraph
Rooted residue graphs, e.g. from
residue_graph_from_atoms().- node_similaritycallable, optional
(code_a, code_b) -> [0, 1]. Defaults to exact CCD-code identity; passccd_node_similarity()to let chemically similar residues match partially, soA-TYR-BandA-PHE-Bscore as near-misses rather than as a plain mismatch. Note that a graded kernel also raises the floor for unrelated composites - every amino acid shares backbone chemistry - so thresholds tuned for exact matching are too permissive under it.
- Returns:
- float
Score in
[0, 1], normalised by the larger residue count, so a subgraph match is penalised by the residues it leaves unmatched. Each residue match is additionally weighted by how well the two residues’ linkage counts agree, which is what separates a macrocycle from the open chain of the same residues.
Notes
Only the roots recorded on each graph are tried, which is what keeps a linear and a branched composite distinguishable; a single residue, having no topology, is tried at every residue of the other graph. Residue graphs run to a handful of nodes, so the assignment at each level is negligible.
Comparison runs over the spanning tree from each root (see
_rooted_children()), so a macrocycle is handled like any other graph rather than recursing around its ring.
- plinder.data.annotations.ccd_ligand_dbs.ligand_parity(query: ResidueGraph, target: ResidueGraph, table: CcdParityTable, query_mol: Mol, target_mol: Mol) float[source]#
PARITY-like similarity of two ligands, assembled or whole-molecule as fits.
Two composites are assembled from the mono table by
composite_parity(). When either ligand is a single residue the whole molecules go throughrascal_parity_score(): a mono may span several residues of the other side, which one-to-one residue alignment cannot express, and one molecule of residue size keeps the engine in its fast range.
- plinder.data.annotations.ccd_ligand_dbs.composite_parity(query: ResidueGraph, target: ResidueGraph, table: CcdParityTable) float[source]#
PARITY-like similarity of two composites assembled from mono-to-mono matches.
Residues are aligned one-to-one with the table as node kernel; each aligned pair contributes its largest matched fragment, restricted to the atoms the structure actually holds (a linked residue has lost its leaving atom); a linkage counts, and joins the two fragments, only when both mappings carry its atoms onto the partner linkage. The assembled match is a common subgraph of the two molecules, so the score sits at or just below the whole-molecule
rascal_parity_score(). No stereo term, as in PARITY.Residues match one-to-one, so a single residue that spans several residues of the other side (chitotriose against NAG-NAG-NAG) is credited for one of them;
ligand_parity()routes such pairs to the whole-molecule engine.Where the table holds several equally good fragments for a residue pair, the one carrying the most linkage atoms onto the partner’s linkage is taken, then the biggest, then the first in the table’s fixed order, so the choice is deterministic and no atom is used twice on either side. A fragment that misses a linkage atom is also tried rewired onto it (a beta1-4 against a beta1-6 chain keeps the chain connected at the cost of the two displaced hydroxyl bonds, as the whole-molecule engine does).
- plinder.data.annotations.ccd_ligand_dbs.ccd_node_similarity(first: str, second: str) float[source]#
Graded similarity between two CCD components, for use as a node kernel.
Pass to
composite_similarity()to let chemically similar residues match partially instead of all-or-nothing, soA-TYR-BandA-PHE-Bscore as near-misses rather than as unrelated composites.- Parameters:
- first, secondstr
CCD component codes.
- Returns:
- float
1.0for identical codes, otherwise the ECFP4/1024 Tanimoto of the two components.0.0when either has no derivable chemistry, which keeps an unresolvable residue from inventing similarity.
Notes
Achiral, like the rest of plinder’s fingerprint stack, so stereoisomeric residues score 1.0 against each other - see the TODO above
component_sequence_identity().
- plinder.data.annotations.ccd_ligand_dbs.residue_graph_features(graph: ResidueGraph, *, iterations: int = 2) Counter[str][source]#
Weisfeiler-Lehman features of a residue graph.
Each residue’s label is refined by the labels of the residues donating into it and those it donates to, repeatedly; every label seen at every round is a feature. Features depend only on labels and linkage, never on residue numbering, so relabelling a ring or reordering a
ccd_codecannot change them, while a different cyclic order or branch topology does.- Parameters:
- graphResidueGraph
Residue graph, from
residue_graph_from_atoms().- iterationsint
Refinement rounds. Two reaches each residue’s neighbours-of-neighbours, which is the residue-level analogue of ECFP4’s radius 2.
- Returns:
- Counter of str
Feature strings,
r:-prefixed to share a space with molecular bits, counted by how many residues carry each. Multiplicity is kept because residue composition is exact and unhashed: a decapeptide genuinely has two alanines where a pentapeptide has one, and a presence-only feature set scores those two molecules identical.
- plinder.data.annotations.ccd_ligand_dbs.ligand_features(smiles: str | None, graph: ResidueGraph | None) Counter[str][source]#
The feature set describing one ligand: molecular bits plus residue graph.
Both descriptions live in one space, so similarity is a single Tanimoto over whatever each ligand has. Nothing branches on how a ligand was decomposed: the molecular bits are identical for the same chemistry however it was carved into residues, and the residue features add the topology that fingerprints cannot see.
- Parameters:
- smilesstr or None
Whole-molecule SMILES (plinder’s
ligand_rdkit_canonical_smiles).- graphResidueGraph or None
Residue graph, when the structure is available.
- Returns:
- Counter of str
m:-prefixed molecular bits andr:-prefixed residue features.
Notes
Molecular bits are kept presence-only, matching plinder’s ECFP4 tables and
ligand_scores(), so this measure stays consistent with every other similarity in the codebase. Counting them was measured and rejected: a larger molecule’s multiplicities inflate the denominator, sharpening the penalty on any size difference, and since folded bits outnumber residue features it also dilutes the residue signal that separates a resequenced macrocycle. Residue features are counted instead, where multiplicity is exact rather than hashed.
- plinder.data.annotations.ccd_ligand_dbs.ligand_similarity(query: Counter[str], target: Counter[str]) float[source]#
Similarity between two
ligand_features()sets.sum(min) / sum(max)over the pooled features - the count-aware form of Tanimoto, which it equals exactly when every count is 1. Counts only enter through the residue features, so a molecule and a tandem repeat of it are no longer identical, while any pair whose residue multiplicities already agree scores exactly as it did presence-only.