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 *-linking components (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_atoms and ccd_type. The ligand_* 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 own lig_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.parquet internally, so mirroring that layout under ccd_dbs lets 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_scores routine the ingest pipeline uses, pointed at the CCD fingerprints layout, 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 from mmpdblib.index_algorithm.cansmirks, so transformations are written exactly as in ccd_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_fraction in ccd_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: TypedDict

One 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: NamedTuple

Lookups 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_smiles must come through canonical_smiles(), as the index keys do. match_kind is one of

exact

The 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 LAT or as the linked sugars GAL-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.

excluded

A single code that is a released CCD component the universe filters out (ions, artifacts, placeholders): known, not novel.

novel

No component is this molecule. Relating it to the universe is the fingerprint’s job (query_ccd_tanimoto(), query_ccd_mmp_pairs()).

component_ccd_ids lists 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_code and ligand_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.parquet for 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: object

Mono-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 directional parity_coverage_1 and parity_coverage_2 (percent) and the largest-fragment atom names fragment_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 -> 2 and 2 -> 1 read 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, but coverage() and directed() raise until it is rebuilt with build_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_similarity and the parity_coverage of 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 what composite_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 directional parity_coverage_1 and parity_coverage_2 (percent of each molecule matched, see RascalParityMatch.coverage()) and the largest-fragment atom names fragment_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 as build_ccd_parity_scores() on each hit.

Returns:
pd.DataFrame

query_ccd_tanimoto() columns plus parity_similarity and the directional parity_coverage_1 (query) and parity_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_dbs directory.

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.SequenceMatcher over 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: NamedTuple

Rooted residue-level graph of a composite ligand.

labels[i] is residue i’s CCD code, adjacency[i] the residues it is covalently bonded to, and roots the residues that only accept linkages. This is the connectivity ccd_code cannot 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. roots fixes the traversal chemically instead - see residue_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 and links each 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

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 by build_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 C1 into the next ring’s oxygen, a peptide bond runs carbonyl C into 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 prefer residue_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_code is 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 (LAT for the same molecule as GAL-BGC, SUC for GLC-FRU, GSH for 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() with ligand_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; pass ccd_node_similarity() to let chemically similar residues match partially, so A-TYR-B and A-PHE-B score 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 through rascal_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, so A-TYR-B and A-PHE-B score as near-misses rather than as unrelated composites.

Parameters:
first, secondstr

CCD component codes.

Returns:
float

1.0 for identical codes, otherwise the ECFP4/1024 Tanimoto of the two components. 0.0 when 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_code cannot 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 and r:-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.