SMSD Pro C++ guide¶
A header-only C++17 library in namespace smsd for substructure search,
maximum common substructure (MCS) search and related chemistry. Atom indices
are zero-based. Each example is a complete program; build it as in the C++
quick start. See also the installation
guide, the Java and Python guides
and the changelog.
SMARTS export in 8.1.0¶
MCS::matchWithSmarts(target) returns
MCS::SmartsResult, whose result retains the ordinary search status and whose
smarts uses the prepared chemistry. Match::toSmarts(query, chemistry) exports
an existing selected match. See SMARTS export for portable
query semantics and a library-search example. Unsupported stereo, tautomer-aware,
complete-ring and strict ring-fusion rules raise instead of weakening the query.
The output does not encode induced MCIS or establish an optimality proof.
Headers¶
Include smsd/smsd.hpp, which includes every header below except
depictor.hpp, bond_energies.hpp, color_palette.hpp and
rdkit_adapter.hpp, or only the headers you need: each compiles on its own.
Headers under smsd/detail/ and namespaces ending in detail are
implementation code.
| Area | Headers under smsd/ |
|---|---|
| Search | mcs.hpp (MCS, mapping utilities); substructure.hpp (Substructure, RGroupResult); search_types.hpp (SearchOptions, MCSOptions, SubstructureOptions, Match, SearchResult, SearchStatus, CancellationToken); chem_options.hpp (ChemOptions) |
| Molecules | mol_graph.hpp (MolGraph, MolGraph::Builder); smiles_parser.hpp (parseSMILES, toSMILES, toSMARTS); smarts_parser.hpp (parseSMARTS, SmartsPredicateRegistry); parser_utils.hpp (input limits); mol_reader.hpp (MOL and SDF) |
| Fingerprints and screening | batch.hpp (fingerprints, similarity, TargetCorpus) and fingerprint_options.hpp (FingerprintOptions), in smsd::batch; similarity_screen.hpp (similarityUpperBound, screenTargets); gpu.hpp (optional CUDA or Metal screening with a CPU fallback, in smsd::gpu) |
| Rings, stereo and drawing | ring_finder.hpp (computeSSSR, computeRelevantCycles, computeURFs, computeRings, ringsTruncated when the ring search stops before finding a complete set of rings, relevantCyclesTruncated for incomplete relevant-cycle enumeration, and RingBasisUnavailable, thrown where an exact ring basis is required but unavailable); relevant_cycle_families.hpp (computeRelevantCycleFamilies, relevantCycleFamilyOf, relevantCycleFamilyMembers: relevant cycles grouped into families with exact member counts); cip.hpp (smsd::cip); layout.hpp (2D and 3D coordinates, transform); depict.hpp (SVG); depictor.hpp (2D coordinates constrained to fixed atoms or a reference) |
| Data and utilities | hungarian.hpp (optimalAssign); periodic_table.hpp (getElement, atomicWeight, paulingElectronegativity and more); scaffold_library.hpp (reference ring systems, scaffolds, pharmacophore and functional-group SMARTS, drug-likeness limits); bond_energies.hpp (bondDissociationEnergy, mappingBondChangeCost, in kJ/mol); color_palette.hpp (mapNumberToPaletteColor, a fixed 20-colour cycle for atom-map numbers); rdkit_adapter.hpp (optional) |
Prepared queries¶
MCS::find and Substructure::find take a MolGraph or SMILES;
Substructure::smarts takes SMARTS text or a SmartsQuery. A prepared query
owns copies of its query and options and may be shared between threads.
withOptions(options) and withTimeout(milliseconds) return new queries;
MCS::withOptions throws std::invalid_argument for invalid fragment limits
or atom weights. match and matchAll take a MolGraph or SMILES target,
which must not change during a search.
match(target): one best common subgraph (MCS) or the first embedding (Substructure).matchAll(target, limit = 1000): up tolimitoptimal mappings or embeddings, symmetric ones included; zero throwsstd::invalid_argument.matchMany(targets, numThreads = 0): onematchresult per target, in order, each with its own budget;0means the hardware concurrency,1the calling thread, and a negative count throwsstd::invalid_argument. The exception of the lowest failing index is rethrown.resultsTimeMillis(target): when, in milliseconds from the search start, matches found within the timeout are assembled (the effective timeout plus one eighth).cancellation(): the token of the options, or null.
#include <cassert>
#include "smsd/smsd.hpp"
int main() {
const auto query = smsd::parseSMILES("CC(=O)NC"); // N-methylacetamide
const auto target = smsd::parseSMILES("CCC(=O)N"); // propanamide
const auto result = smsd::MCS::find(query).withTimeout(1000).match(target);
assert(result.status == smsd::SearchStatus::Completed && result.optimal && result.complete);
assert(result.matches.size() == 1 && result.matches.front().atomMapping.size() == 4);
assert(smsd::MCS::validateMapping(query, target, result.matches.front()).empty());
const auto phenol = smsd::parseSMILES("c1ccc(O)cc1");
assert(!smsd::Substructure::find("c1ccccc1").match(phenol).matches.empty());
assert(!smsd::Substructure::smarts("[OH]").match(phenol).matches.empty());
const auto all = smsd::Substructure::find("CO").matchAll("OCO", 10);
assert(all.complete && all.matches.size() == 2);
}
SMARTS¶
SMARTS follows Daylight semantics, as in Java: [H], [2H] and [H+] are
hydrogen atoms, [CH] and [H1] count hydrogens, r<n> is the smallest ring
size, (C.C) requires one target component and (C).(C) different ones. /
and \ are single, non-aromatic bonds; a double bond between them must have
the written configuration unless one is /? or \?, and an unpaired one
matches either way.
parseSMARTS(text, maxRecursionDepth = 20) throws std::invalid_argument for
malformed SMARTS, including a pattern that ends with a bond such as CC-, and
for input over the parser_utils.hpp limits
MAX_SMARTS_LENGTH, MAX_ATOMS (also applied by parseSMILES),
MAX_SMARTS_EXPRESSION_NODES and MAX_SMARTS_NODES_PER_EXPRESSION.
A SmartsPredicateRegistry expands $name tokens into recursive SMARTS with
expandPredicates(text); the static builtinRegistry() supplies the ten
predicates listed in the Java guide, with the same
definitions, and registerPredicate(name, smarts) adds more.
Results¶
SearchResult holds matches, status, optimal, complete, score,
nodesVisited and elapsedMillis. status is Completed when the search ran
to its end, TimedOut or Cancelled when the time limit or the cancellation
token stopped it, and LimitReached when the node limit stopped it, an
enumeration reached its match limit, or an MCES pair beyond the
size limits was declined before any search; a declined pair
returns no match and proves nothing. A stopped search may return an
unproved mapping, and a capped enumeration may be optimal yet incomplete, so
check both flags, and check matches before front(). A stopped MCIS search
can return a result or atom mapping that differs from 8.0.3 or Java (see the
upgrade notes). score counts mapped
atoms for the maximum common induced subgraph (MCIS), selected bonds for the
maximum common edge subgraph (MCES), the summed weight for weighted MCIS, and
query atoms for a Substructure match (zero without one).
completeRingsOnly requires complete relevant-cycle perception;
RingFusionMode::STRICT requires complete perception for its per-atom ring
counts. If that perception is incomplete in either input, the selected mappings
remain, but the result is LimitReached, with optimal and complete both
false; TimedOut and Cancelled take precedence. Positive numeric SMARTS
R<n> (n > 0) uses one deterministic minimum cycle basis, the same in C++
and Java. If ring perception cannot complete it, a search that cannot be
decided without it returns LimitReached without matches, including for
negated, combined and recursive predicates; the empty matches then proves
nothing. Literal R and R0 remain independent of that basis. Other
functions that need the basis throw smsd::RingBasisUnavailable, a
std::runtime_error, naming the cause; computeSSSR and computeRings may
return incomplete rings with truncation flags. relevantCyclesTruncated alone
does not make the basis incomplete. Ring
perception works within one fixed, deterministic work limit; see
Results and budgets.
A Match holds atomMapping (a std::map<int, int> from query to target
atom), bondMapping (a std::array<int, 4> per selected bond: two query
atoms, then two target atoms; keep it with MCES results, as atom pairs do not
show which bonds were left out) and fragments (the components of the selected
bonds, as query atoms). Explicit hydrogen atoms are vertices; bracket hydrogen
counts are not. Match::toSmiles(query) writes the selection with valences
capped at cut bonds; Match::toGraph(query) returns it, compactly renumbered,
with its original chemical context.
Corpus threshold decisions¶
Substructure::matchCorpus(corpus, gpuPrefilter, numThreads) and
MCS::matchCorpus(corpus, minimumAtoms, gpuPrefilter, numThreads) return one
CorpusSearchResult per original target slot. targetIndex is that slot;
result contains the ordinary CPU SearchResult when searched. It is absent
only when a conservative filter proves rejection. thresholdReached needs a
real witness; provenBelowThreshold needs a bound or completed CPU proof.
Both false means unknown, including a stopped search below the threshold.
#include <smsd/mcs.hpp>
#include <smsd/substructure.hpp>
#include <cassert>
int main() {
smsd::batch::TargetCorpus corpus;
corpus.addTargets({smsd::parseSMILES("CCO"), smsd::parseSMILES("N")});
auto embeddings = smsd::Substructure::find("CO").matchCorpus(corpus, false, 1);
assert(embeddings[0].thresholdReached && embeddings[0].result);
assert(embeddings[1].provenBelowThreshold && !embeddings[1].result);
auto decisions = smsd::MCS::find("CC").matchCorpus(corpus, 2, false, 1);
assert(decisions[0].thresholdReached && decisions[1].atomUpperBound == 0);
}
gpuPrefilter=false, the default, runs the same conservative filter on the
CPU. true attempts the optional provider and falls back to the CPU filter.
There is no automatic GPU selection for these new methods. For unweighted
MCIS, atomUpperBound is an upper bound on the MCS atom count, and a target
whose bound equals minimumAtoms is searched. When no bound is available,
atomUpperBound is -1. With unsupported query chemistry, SMARTS, caps,
immediate timeouts, cancellation tokens, MCS weights, MCES or exclusions,
every target is searched on the CPU. A weighted or MCES result does not prove
an atom-threshold negative. Existing matchMany is unchanged.
Searches retain original target order and atom numbering, and each call
searches the targets the corpus held when it started, even if targets are
added meanwhile. numThreads has the ordinary batch meaning. Each target's
search budget excludes the pre-screen, and these methods do not promise a
whole-corpus wall-time limit.
Size limits¶
An MCES search declines a pair before searching it, with LimitReached, no
match, and optimal and complete both false, when:
- query atoms × target atoms exceed 2³⁰ (1,073,741,824), as for two molecules of more than 32,768 atoms each;
- the two molecules are too large in bonds, as for two molecules of more than 16,384 bonds each;
- rarely, a molecule has an atom with 512 or more bonds and the two molecules share many kinds of bond.
MCIS searches are never declined; the time and node limits bound them. Java and Python use the same limits.
Objectives and fragments¶
MCIS, the default, maximises mapped atoms, then common bonds, and preserves edges and non-edges between mapped atoms. MCES maximises selected common bonds, then their atoms; isolated atoms do not count. Connectivity and fragment limits apply to the selected bonds.
MCS::Options fields, with defaults:
objective(Objective::MCIS):MCISorMCES.connectivity(Connectivity::Connected):Fragmentsallows a disconnected result.minFragmentAtoms(1) andmaxFragments(INT_MAX): positive limits on fragment size and count.excludedTargetAtoms(empty): target atoms the search may not use.atomWeights(empty): MCIS only, one finite, signed weight per query atom.
With atomWeights, MCIS maximises the summed weight, then atoms, then bonds,
comparing whole thousandths truncated towards zero. Weights with MCES, or whose
positive or negative sum in thousandths exceeds the 32-bit range, throw
std::invalid_argument.
#include <cassert>
#include "smsd/smsd.hpp"
int main() {
const auto query = smsd::parseSMILES("C1CC1"); // cyclopropane
const auto target = smsd::parseSMILES("CC(C)C"); // isobutane
const auto induced = smsd::MCS::find(query).match(target);
assert(induced.matches.front().atomMapping.size() == 2);
smsd::MCS::Options options;
options.objective = smsd::MCS::Objective::MCES;
const auto edges = smsd::MCS::find(query).withOptions(options).match(target);
const auto& match = edges.matches.front();
assert(match.atomMapping.size() == 3 && match.bondMapping.size() == 2);
const auto output = smsd::parseSMILES(match.toSmiles(query));
std::size_t degreeSum = 0;
for (const auto& neighbors : output.neighbors) degreeSum += neighbors.size();
assert(output.n == 3 && degreeSum == 4);
assert(smsd::MCS::validateMapping(query, target, match, options).empty());
options.objective = smsd::MCS::Objective::MCIS;
options.connectivity = smsd::MCS::Connectivity::Fragments;
const auto fragments = smsd::MCS::find("CCOCC").withOptions(options).match("CCNCC");
assert(fragments.matches.front().fragments.size() == 2);
}
Search options¶
MCS::Options and Substructure::Options share the SearchOptions fields:
chemistry(ChemOptions{}): atom and bond compatibility rules.timeoutMs(-1): milliseconds; negative selects the default,min(30000, 500 + 2 × query atoms × target atoms)forMCS(1,300 for two 20-atom molecules, 5,500 for two 50-atom molecules, 30,000 from 122 atoms each) and 10,000 forSubstructure, and zero stops at once.nodeLimit(0): most search nodes; zero is unlimited.cancellation(null): astd::shared_ptr<CancellationToken>;cancel()stops the search.induced(false,Substructure::Optionsonly): also preserve non-edges.
The budget starts once the query is built and string inputs are parsed, and one step can overrun it.
#include <cassert>
#include <memory>
#include <vector>
#include "smsd/smsd.hpp"
int main() {
const auto query = smsd::Substructure::smarts("[#6]-[#8]");
const std::vector<smsd::MolGraph> targets = {
smsd::parseSMILES("CO"), smsd::parseSMILES("CC")
};
const auto results = query.withTimeout(1000).matchMany(targets, 2);
assert(results.size() == 2);
assert(results[0].complete && results[0].matches.size() == 1);
assert(results[1].complete && results[1].matches.empty());
auto token = std::make_shared<smsd::CancellationToken>();
smsd::Substructure::Options options;
options.cancellation = token;
token->cancel();
const auto stopped = query.withOptions(options).match(targets.front());
assert(stopped.status == smsd::SearchStatus::Cancelled);
assert(stopped.matches.empty() && !stopped.optimal && !stopped.complete);
const auto immediate = smsd::MCS::find("CC").withTimeout(0).match("CCC");
assert(immediate.status == smsd::SearchStatus::TimedOut);
assert(immediate.matches.empty() && !immediate.optimal && !immediate.complete);
}
Chemistry options¶
ChemOptions fields, with defaults:
matchAtomType(true),matchFormalCharge(false) andmatchIsotope(false): mapped atoms share their element, their formal charge, or, when both specify one, their mass number.matchBondOrder(BondOrderMode::STRICT):LOOSEandANYignore bond-order differences.aromaticityMode(AromaticityMode::FLEXIBLE): aromatic bonds may match single or double bonds;STRICTrequires equal aromatic flags.ringMatchesRingOnly(false): ring atoms and bonds map only to ring atoms and bonds.completeRingsOnly(false): retained ring atoms and bonds lie in fully selected relevant rings of their own input; a fusion atom needs only one.ringFusionMode(RingFusionMode::IGNORE):STRICTrequires mapped ring atoms to have the same per-atom ring count;PERMISSIVEadds no constraint, likeIGNORE.useChiralityanduseBondStereo(false): mapped tetrahedral centres and double bonds must agree in configuration, compared by the parity of their mapped neighbours, not by CIP labels (R/S, E/Z).tautomerAware(false): relax bond orders where tautomer rules apply.pH(7.4) andsolvent(Solvent::AQUEOUS): the tautomer confidence context.
The fluent setters are withBondOrderMode, withAromaticityMode,
withCompleteRingsOnly, withMatchIsotope, withRingFusionMode,
withTautomerAware, withPH (clamped to 0–14; throws std::invalid_argument
if not finite) and withSolvent. ChemOptions::profile(name), ignoring case,
returns default, strict (strict aromaticity), pharma (charge, ring-only
and complete-ring matching), compat-fmcs, compat-substruct or reaction
(loose bond orders), and throws std::invalid_argument otherwise;
ChemOptions::tautomerProfile() adds tautomer-aware matching with loose bond
orders. Deprecated: ChemOptions::fmcsProfile() (use profile("compat-fmcs"))
and the unread aromaticityModel field (MolGraph::Builder::build() and
perceiveAromaticity() take the model).
Stereo: tetrahedral centres have four neighbours, or three plus an implicit
hydrogen or, for N, P, S, As, Se, Sb and Te, a lone pair; the SMILES parser
drops other tags, and MolGraph::Builder tags on other atoms are compared as
given. A Builder tag (1 for @, 2 for @@) winds the stored neighbours, the
implicit hydrogen or lone pair coming second if the first neighbour's index is
below the atom's and first otherwise, as in Java. Substructure requires
tagged query stereo to recur in the target, and untagged query stereo matches
anything; MCS compares the stereo the mapping fixes, treating unspecified
target stereo as a wildcard. The comparison uses the parity of the mapped
neighbours, not CIP labels, whose priorities depend on atoms outside the
mapping. In MCS it is a hard constraint: an MCIS is the largest common
subgraph whose mapped stereo agrees, so it can be smaller than the MCIS found
without these options, and an MCES leaves out the fewest common bonds that
resolve each disagreement.
Mapping utilities¶
MCS::validateMapping(query, target, match, options = {}): a message per rule aMatchor atom map violates; empty when valid.MCS::mappingMatch(query, target, atoms, options = {}): aMatchfrom an atom map, with the compatible bonds between mapped atoms.MCS::isMappingMaximal(query, target, match, options = {}): whether the mapping is valid with no valid strict extension, which is weaker than optimal; throwsstd::runtime_errorif it cannot decide, including when required ring perception was truncated.MCS::canonicalizeMapping(query, target, mapping): one representative under the symmetry of both molecules; throws if the symmetry cannot be resolved.smsd::validateTautomerConsistency(query, target, atomMapping): whether mobile N–H, O–H and S–H protons are conserved in each mapped tautomeric group.MCS::computeTautomerConfidence(query, target, match, chemistry): a heuristic confidence, not a probability, in the selected tautomeric relaxations at the solvent and pH ofchemistry; identical chemistry scores 1.
Per atom, MolGraph exposes tautomerClass (from 0, independent of atom order
except among symmetric classes; −1 outside every class), tautomerWeight (for
water at pH 7.4 unless MolGraph::Builder::solvent and pH set another
context), tautomerSite (TAUTOMER_SITE_ACID, TAUTOMER_SITE_BASE or
TAUTOMER_SITE_NONE) and tautomerPKa; the MolGraph::TW_* and TPKA_*
constants hold the motif weights and pKa values. Java MolGraph.tautomerWeight
is unscaled, so the arrays can differ, but the confidences agree to within
rounding.
Several molecules¶
MCS::matchConstrained(queries, target, options = {})runs the queries in order against one target, one result each; atoms used by an earlier query are unavailable to later ones, and indices refer to the original target.MCS::consensus(molecules, options = {}, threshold = 1.0)returns aConsensusResult: a common graph (graph,fragment,sourceIndex,toSmiles()) in at least the fractionthresholdof the inputs, withsupportCountand verifiedmappingskeyed by input index;foundis false when none qualifies.thresholdmust lie in (0, 1]; atom weights and excluded target atoms are rejected. Consensus is heuristic, shares one budget and always reportsoptimal == false, including empty and single-input calls.completereports whether the progressive pass finished. Neither it norCompletedproves a global optimum or absence of a qualifying common graph. With complete rings or strict ring fusion, truncated ring perception in any input makes itLimitReachedand incomplete, unless it is alreadyTimedOutorCancelled; its fragment and supporting mappings are retained.Substructure::decomposeRGroups(core, molecules, options = {})returns anRGroupResultper molecule: the matchedcore, substituents inrgroups(R1,R2, …) and thesearchresult.
Graphs, SMILES, MOL and SDF¶
parseSMILES(text) throws std::invalid_argument for malformed SMILES,
including bracket atoms beyond the OpenSMILES limits;
parseSMILES(text, options) with ParseOptions::lenient = true recovers where
it can, such as from unclosed rings or unknown elements. toSMILES(graph)
writes canonical SMILES, with / and \ on single bonds wherever a configured
double bond has one at an end; an end with only aromatic ring bonds, as at an
exocyclic C=N on an aromatic ring, carries them on an aromatic bond, as RDKit
writes it, which strict OpenSMILES readers take as a single bond.
toSMARTS(graph, options) writes canonical SMARTS;
SmartsWriteOptions writes aromaticity and charge by default, and ring
membership, hydrogen count and isotope on request. Both writers reject a graph
that needs more than 99 ring closures open at once. For a graph of more than
5,000 atoms, or when canonical labelling reaches its search limit, canonical
output can depend on the input atom order;
automorphismGeneratorsTruncated() is then true.
MolGraph::Builder sets atoms (atomCount, atomicNumbers, formalCharges,
massNumbers, hydrogenCounts, atomClasses, ringFlags, aromaticFlags),
bonds (setNeighbors, setBondOrders, bondRingFlags, bondAromaticFlags),
stereo (tetrahedralChirality, doubleBondStereo, doubleBondStereoSparse)
and metadata (atomIds, name, programLine, comment, properties,
solvent, pH). build() perceives aromaticity, giving supplied aromatic
systems a Kekulé structure and perceiving them again, and build(false) keeps
the supplied flags. When perception cannot read every candidate cycle of a
molecule, as with a large bowl-shaped polyarene, the aromatic systems supplied
on its fused ring systems keep their flags.
MolGraph::ringCountsTruncated() reports whether the per-atom ring counts may
be incomplete because ring perception reached its work limit before the
minimum cycle basis was complete; it computes the counts first if needed.
MolGraph::computeRelevantCyclesFromBasis(basis, truncated) returns the
relevant cycles found from a given set of the graph's cycles, normally a
minimum cycle basis, and sets truncated to whether the result may lack
relevant cycles: the basis has fewer cycles than the cycle rank, or the
enumeration reached one of its limits. With its optional third argument set to
false, a basis with fewer cycles than the cycle rank no longer sets
truncated and the work limit no longer stops the enumeration, which can then
take much longer.
computeRelevantCycleFamilies(graph) groups the relevant cycles into families
and caches the result on the graph. Each RelevantCycleFamily gives the ring
size, the root (the highest-numbered atom of every member), first,
second and opposite, which locate the bond or atom opposite the root, one
member as prototype and the exact memberCount as a decimal string, because
counts can exceed 64 bits. RelevantCycleFamilies::relevantCycleCount is the
exact number of members of the families listed, so the relevant cycles can be
counted without listing them; truncated reports an incomplete enumeration,
whose families are relevant but may not be all of them.
relevantCycleFamilyOf(graph, families, ring) returns the index of the family
that has ring as a member, or -1, and
relevantCycleFamilyMembers(graph, family, limit) returns up to limit
members, starting with the prototype. When the enumeration is complete, the
relevant cycles and their number do not depend on the input atom order; the
grouping into families can.
extractSubgraph(graph, atoms): the induced subgraph, in the given order, keeping a stereocentre whose neighbours all stay and the E/Z configuration of a double bond whose ends keep the neighbours that define it.murckoScaffold(graph): the Bemis–Murcko scaffold (ring atoms, linkers and atoms double- or triple-bonded to them), capped with hydrogen, keeping surviving stereo; a molecule without rings is unchanged.translateToAtomIds(mapping, query, target): a mapping in external atom IDs.readMolBlock(text)andreadMolFile(path)read V2000 or V3000 and throwstd::invalid_argumentfor a block they cannot read, such as one with a bond to a missing atom, a V3000 integer field outside its range, a V3000 coordinate that is not a decimal number in the range of a double or more thanMAX_ATOMSatoms; V2000 numeric fields are read leniently. Coordinates are read by one rule whatever the locale or platform, and one below 1e-307 in magnitude reads as zero.readSDF(path)reads every record, a malformed one giving an empty graph.writeMolBlock(graph)writes V2000, or V3000 above 999 atoms or bonds;writeMolBlockV3000(graph)writes V3000;writeSDFRecord(graph)writes one record ending in$$$$, andwriteSDF(graphs, path)a file.
MOL round trips do not imply support for MDL query features.
#include <cassert>
#include <string>
#include "smsd/smsd.hpp"
int main() {
const auto query = smsd::MolGraph::Builder().atomCount(4)
.atomicNumbers({7, 6, 6, 8})
.setNeighbors({{1}, {0, 2}, {1, 3}, {2}})
.setBondOrders({{1}, {1, 1}, {1, 1}, {1}}).build();
assert(smsd::MCS::find(query).match("CC(O)CN").matches.front().atomMapping.size() == 4);
const auto text = smsd::writeMolBlockV3000(query);
const auto restored = smsd::readMolBlock(text);
assert(!smsd::Substructure::find(query).match(restored).matches.empty());
assert(smsd::writeSDFRecord(query).find("$$$$") != std::string::npos);
}
Optional native adapter¶
Configure with SMSD_WITH_RDKIT=ON and include smsd/rdkit_adapter.hpp; the
static library is exported as smsd::smsd_rdkit and needs an RDKit development
installation and C++20. smsd::fromRDKit(molecule, removeHs = true) removes
explicit hydrogens by default, which can change atom indices, and copies
element, charge, isotope, ring, aromaticity and tetrahedral data, but not
hydrogen counts or double-bond stereo. smsd::fromSmiles(text) parses with
RDKit and removes hydrogens likewise; smsd::parseSMILES(text) is the native
parser.
Fingerprints, screening and assignment¶
#include <cassert>
#include "smsd/smsd.hpp"
int main() {
const auto fingerprint = smsd::batch::fingerprintFromSmiles("c1ccc(O)cc1", 2, 2048);
assert(fingerprint.size() == 32);
assert(smsd::batch::fingerprintTanimoto(fingerprint, fingerprint) == 1.0);
const auto assignment = smsd::optimalAssign({{1.0, 3.0}, {4.0, 2.0}});
assert(assignment.totalCost == 3.0);
}
Packed fingerprints are vectors of 64-bit words, count fingerprints vectors of
integer bins; compare only fingerprints of the same family, radius and size.
In smsd::batch:
fingerprintFromSmiles(smiles, radius = 2, fpSize = 2048): ECFP; radius 2 is ECFP4.batchFingerprint(molecules, pathLength = 7, fpSize = 1024, numThreads = 0): path fingerprints over all atoms, explicit hydrogens included,fpSizerounded up to a multiple of 64; Pythonpath_fingerprintuses heavy atoms only and differs.fingerprintTanimoto,fingerprintOverlap,fingerprintDice,fingerprintCosineandfingerprintSoergelcompare packed fingerprints; for two empty ones, Tanimoto and overlap give 1 and the others 0, as in Java and Python.countTanimoto,countDiceandcountCosinecompare equal-length count vectors, giving 0 for empty or unequal ones.fingerprintSubset(query, target)tests whether every query bit is set in the target;batchFingerprintScreen(query, targets, numThreads = 0)returns the passing target indices.fingerprintQuality(fingerprint, fpSize), for packed words or set positions, returns aFingerprintQualitywithsetBits,fpSize,density,expectedCollisionRateandsaturated(density overFINGERPRINT_SATURATION_DENSITY, 0.75), as Pythonanalyze_fp_qualityand JavaanalyzeFpQualityreport them.toHex,fromHex,toBinaryStringandcountsToArrayconvert formats.TargetCorpusholds reusable targets:addTarget,addTargets,prewarm(numThreads = 0),size,isPrewarmed,targets,screen(query, threshold, numThreads = 0), which returns the indices of the targets with ECFP4 Tanimoto similarity of at leastthreshold, and none beforeprewarm, andscreenTargets(query, threshold, chemistry, numThreads = 0), the bound screen below for the held targets, which needs noprewarm.TargetCorpus::screenMany(queries, threshold, numThreads = 0)andscreenTargetsMany(queries, threshold, chemistry, numThreads = 0)return one hit-index vector per query, in query order. Each vector keeps target order.screenManyneeds prewarmed ECFP4 fingerprints;screenTargetsManyuses the atom-count MCS similarity bound and needs noprewarm.- Errors in
prewarmand in CPU screening reach the caller; when several targets fail, the error of the first in target order is raised, whatever the thread count.
similarityUpperBound(query, target, chemistry) bounds the atom-count Tanimoto
similarity of the MCS from above;
screenTargets(query, targets, threshold, chemistry) returns the indices of
the targets whose bound reaches threshold, on the calling thread, or with a
fifth argument on up to numThreads threads (0 = the OpenMP thread limit),
giving the same indices. gpu::batchSimilarityScreenAuto
is a separate native CUDA or Metal source-build interface with a CPU fallback,
returning a ScreenHit{index, score} per passing target. The optional Python
smsd-cuda package uses the corpus screening interface described in the
Python guide.
gpu::isAvailable() and gpu::deviceInfo() report the backend; a non-empty
SMSD_DISABLE_GPU other than 0 makes isAvailable() false. Batch
fingerprints, screens and TargetCorpus run in parallel when compiled with
OpenMP.
optimalAssign(cost, penalty = 1.0) solves rectangular minimum-cost assignment
over every row or column of the smaller side, returning the (row, column) pairs
and totalCost, which excludes unmatched penalties; ragged or non-finite input
throws std::invalid_argument.
Optional Metal fingerprint screening¶
On macOS, the native source build compiles the smsd_metal library by default
when Metal is available; -DSMSD_BUILD_METAL=OFF leaves it out and
-DSMSD_BUILD_METAL=ON makes it required. A program uses Metal only when it
links that library from the same source build, for example with
target_link_libraries(my_target PRIVATE smsd smsd_metal); the installed
header-only package does not include smsd_metal. In a program linked with
smsd_metal, gpu::batchSimilarityScreenAuto runs on a Metal GPU when one is
available, unless SMSD_DISABLE_GPU disables it, and on the CPU otherwise. For
fingerprint screening, include smsd/metal_gpu.hpp and
smsd/screen_accelerator.hpp.
metal::fingerprintAccelerator() supplies a resident packed-fingerprint
provider. Pass it explicitly as the final argument of
screen_accelerator::fingerprintScreens(queries, targets, threshold, cache,
score, provider). Automatic selection is disabled: its minimum_targets
returns UINT64_MAX, so attaching the provider alone does not enable it.
Use equal, non-zero word lengths and the same fingerprint settings for all
queries and targets. Keep target fingerprints unchanged while reusing a
screen_accelerator::ResidentCache. The score(commonBits, queryBits,
targetBits) callback must use the CPU score expression and be non-decreasing
in commonBits. The result keeps query and target order and matches the CPU
screen exactly.
A declined screen or reported provider failure returns std::nullopt; the
caller must then run its CPU screen. Provider errors are recorded by
screen_accelerator::failureCount() and latestFailure(). A non-empty
SMSD_DISABLE_GPU other than 0 disables GPU use.
This provider supports packed fingerprints only and has no Python export.
The separate float-valued metal::batchSimilarityScreen and graph-kernel
APIs are outside this integer fingerprint contract. MCS and substructure
search remain on the CPU.
The Metal screening example
screens a few molecules this way, falls back to the CPU screen when the provider
declines or fails, and reports which path ran; configure with
-DSMSD_BUILD_EXAMPLES=ON to build it. Without smsd_metal it builds and runs
on the CPU alone.
Configurable fingerprints¶
FingerprintOptions gives the same bits in C++, Java and Python for
computeFingerprint(graph, options) (packed bits),
computeFingerprintCounts(graph, options) (count bins),
fingerprintFromSmiles and
batchFingerprint(molecules, options, numThreads = 0), which reads any
sequence of graphs and throws the exception of the first molecule that fails.
An smsd::RingBasisUnavailable error, or the error of a key left undecided by
its time or embedding limit, keeps its type and adds the zero-based input index
to its message.
FingerprintOptions::ecfp(radius = 2, size = 2048),
fcfp(radius = 2, size = 2048), path(pathLength = 7, size = 2048) and
torsion(size = 2048) reproduce computeCircularFingerprintECFP,
computeCircularFingerprintFCFP, Python path_fingerprint and
computeTopologicalTorsion; a default-constructed FingerprintOptions is
ECFP4. Turning on another switch adds terms, which can change the classic bits,
so compare only fingerprints built with the same options.
#include <cassert>
#include "smsd/smsd.hpp"
int main() {
using smsd::batch::FingerprintOptions;
auto options = FingerprintOptions::ecfp(2, 2048);
options.cipAtoms = true; // adds CIP R/S terms
options.ringSize = true; // adds smallest-ring-size terms
const auto l = smsd::batch::fingerprintFromSmiles("C[C@H](N)C(=O)O", options);
const auto d = smsd::batch::fingerprintFromSmiles("C[C@@H](N)C(=O)O", options);
assert(l != d);
assert(smsd::batch::fingerprintFromSmiles("C[C@H](N)C(=O)O", FingerprintOptions::ecfp())
== smsd::batch::fingerprintFromSmiles("C[C@@H](N)C(=O)O", FingerprintOptions::ecfp()));
auto framework = FingerprintOptions::ecfp();
framework.scaffold = smsd::batch::ScaffoldMode::MURCKO;
assert(smsd::batch::fingerprintFromSmiles("Cc1ccccc1", framework)
== smsd::batch::fingerprintFromSmiles("c1ccccc1", FingerprintOptions::ecfp()));
const auto keys = smsd::batch::fingerprintFromSmiles("CC(=O)Oc1ccccc1C(=O)O", FingerprintOptions::maccs());
const auto set = smsd::bitPositions(keys, 166);
assert(std::find(set.begin(), set.end(), 162) != set.end()); // MACCS key 163, a six-membered ring
}
FingerprintOptions members:
family(ECFP,FCFP,PATH,TORSIONorKEYS),radius(2, or −1 for the whole component),pathLengthin bonds (7) andsizein bits or bins (2048).- Atom terms:
element,degree(explicit neighbours),valence(bond order sum; pi electrons forTORSION),formalCharge,isotope,hydrogenCount,tautomerClass,aromaticity,ringMembership, andringSize, the smallest SSSR ring, with rings ofringSizeCap(8) atoms or more sharing a class. Bond terms:bondOrder,aromaticBondandringBond. cipAtomsandcipBonds: CIP R/S (and r/s) and E/Z fromsmsd/cip.hpp. Circular families separate stereoisomers from radius 1; double bonds in rings under eight atoms have no E/Z term.fragments:WHOLE(default),LARGEST(most heavy atoms) orPER_FRAGMENT(each atom term carries its component).scaffold:NONE(default) orMURCKO, themurckoScaffoldscaffold, applied first.keys: theFingerprintKey(smarts, minimum = 1)entries of aKEYSfamily; a key is set when at leastminimumdistinct atom sets match.FingerprintOptions::maccs()gives the 166 MACCS keys, key k at bit k − 1, listed bymaccsKeys();FingerprintOptions::withKeys(keys)builds a custom family.
Defaults: ECFP sets every atom term except ringSize, and bondOrder; FCFP
sets degree, ringMembership and bondOrder and always adds the
pharmacophore class; PATH sets element; TORSION sets element, degree,
valence and ringMembership. Other switches are off. Inconsistent options
and unparsable SMARTS keys throw std::invalid_argument; a key count that
cannot be settled within its budget throws std::runtime_error. These options
do not configure the MCS screening fingerprint.
A key whose minimum number of matches is found before the embedding limit is
set; a key search that runs out of time throws, as in 8.0.3. Ring-size terms,
double-bond stereo terms, aromatic-ring keys and SMARTS keys whose search needs
the minimum cycle basis throw smsd::RingBasisUnavailable naming the cause if
ring perception cannot complete that basis; a separate relevant-cycle truncation
does not prevent their use. The ring refusals replace values that 8.0.3
could compute from incomplete rings.
Stereochemistry and depiction¶
cip::assignAll(graph)returnsCIPDescriptors, with per-atom R/S labels inrsLabelsand E/Z labels inezBonds;cip::assignRSandcip::assignEZlabel one centre or bond,cip::assignFromSMILEStakes SMILES andcip::to_stringformats a label. IUPAC 2013 Rule 1a applies literally to duplicate atoms, so phosphinates and phosphonates may get the opposite descriptor from software that ranks the oxo oxygen lower.generateCoords2D(graph, targetBondLength = 1.5)andgenerateCoords3D(graph, targetBondLength = 1.5)generate coordinates;layoutQualityscores a 2D layout;transformtranslates, rotates, scales, mirrors and aligns coordinates.depict(graph, options)returns SVG;depictWithMapping(graph, atomMapping, options)highlights mapped atoms;depictPair(query, target, mapping, options)draws a mapped pair;depictSMILESanddepictMCStake SMILES;DepictOptionssets the style.depictor.hppprovidescompute2DCoords(graph, options), an overload keeping the atoms of aCoordMap2Dfixed, andgenerateDepictionMatching2DStructure, which aligns a molecule to a reference by matched atom pairs.
Deprecated¶
smsd::computeTautomerConfidence(query, target, mapping): useMCS::computeTautomerConfidence(query, target, match, chemistry); the old form selects every mapped bond in both molecules and assumes water at pH 7.4.smsd::implicitHydrogens(z, bondOrderSum, charge): useMolGraph::implicitHydrogenCount(atom).smsd::ChemOps: useMCS::validateMapping.- Static
MolGraph::refinePartition,comparePermandcomputeCanonicalLabeling: useMolGraph::getCanonicalLabeling(). MolGraph::TW_NITRO_ACI_NITRO: useMolGraph::TW_NITRO_ACI.hungarianSolveandHungarianResult: useoptimalAssignandAssignmentResult.runHortonPipeline: usecomputeRingsorcomputeRelevantCycles.
Deprecated with no replacement: MolGraph::ensurePatternFP(),
getNeighborsByDegDesc(), getNLF1(), getNLF2(), getNLF3(), nlfCheckOk,
TW_SULFOXIDE and TW_NITRILE_ISONITRILE; popcountAnd, popcountN,
anyBitAnd and popcountAndNot; and transform::translate3D, rotate3DX,
rotate3DY, rotate3DZ, scale3D, center3D and rmsd2D.