Forward Retrieval Hints
renkin-forward hints answers a third question, distinct from both
predict and enumerate:
"Given only this one molecule, what should I search a patent/reaction
database for?" This is an information-assisted retrieval tool, not a
product predictor. It never invents a partner molecule and never guesses a
concrete product — it reports, for every compatible template, exactly which
part of the known molecule reacts, the exact SMARTS pattern the still-missing
partner would need to satisfy, and the bond-level change the template
represents, so a chemist can go search a real database (an internal ELN, a
patent corpus, Reaxys, SciFinder, ...) with concrete, mechanically-derived
search terms instead of guessing.
predict / enumerate / hints at a glance
predict |
enumerate |
hints |
|
|---|---|---|---|
| Input | every reactant a template needs | one known reactant | one or more known reactants |
| Partner molecules | caller supplies all of them | filled from an explicit --partners library |
never invented or searched — no partner input exists |
| Output | concrete product SMILES | concrete product SMILES | a query pattern for the product (product_query_smarts), never a concrete SMILES |
| Use case | "what does this exact reaction give?" | "what can this molecule become, given a candidate partner library?" | "what should I search for to find a real partner/reaction?" |
| Report schema | ForwardPredictionReport |
ForwardEnumerationReport |
ForwardRetrievalHintReport — separately versioned, the other two are untouched |
Putting a partner-free retrieval mode into enumerate itself was
deliberately rejected: enumerate without --partners already has a
well-defined, narrower meaning (skip binary templates, apply unary ones
directly), and mixing "a concrete product" and "an abstract query pattern"
into the same report schema would blur a distinction this guide's whole
purpose depends on keeping sharp.
Installation and build
Same binary as predict/enumerate, no separate build step:
Basic usage
renkin-forward hints --reactants "Brc1ccccc1" [--templates <path>] \
[--max-hints N] [--max-matches-per-slot N] [--max-assignments-per-template N]
--reactants <SMILES>...— one or more known reactants (required).--templates <path>— an additional SMIRKS template file, on top of the embedded default rules (hard error if missing/unreadable/empty, same aspredict/enumerate).--max-hints N(default 50) — cap on merged hints returned, applied after cross-template merging (see Merge semantics).--max-matches-per-slot N(default 20) — cap on reported match sites per (template, slot); a capped slot setsmatch_sites_truncated: trueon that assignment rather than silently reporting a partial list as complete.--max-assignments-per-template N(default 200) — cap on the injective known-reactant/slot permutations enumerated per template; hitting this cap incrementsstats.templates_with_assignments_truncatedand is never silently treated as "no assignment found".
One known reactant
Real captured output (embedded default rules, renkin-forward release
binary):
{
"schema_version": 1,
"known_reactants": [
{ "input_index": 0, "canonical_smiles": "c1cc(Br)ccc1" }
],
"hints": [
{
"hint_id": "sha256:04780ac95230798f61c42fd8910c1bad74e82dde7c1d0c48f40429b53e2febe0",
"rank": 0,
"reaction_family": { "label": "C-C bond formation", "basis": "derived_bond_delta" },
"known_assignments": [
{
"input_index": 0,
"slot_index": 0,
"match_sites": [
{ "target_atom_indices": [1], "mapped_atoms": [{ "template_map": 1, "target_atom_index": 1 }] }
],
"match_sites_truncated": false
}
],
"missing_partners": [
{
"slot_index": 1,
"query_smarts": "[C:2](=O)O",
"required_features": {
"required_elements": ["C", "O"],
"excluded_elements": [],
"aromatic": false,
"hydrogen_constraints": [],
"summary_complete": true
}
}
],
"transformation": {
"bonds_formed": [{ "left_map": 1, "right_map": 2, "order": "any" }],
"bonds_broken": [],
"bonds_order_changed": []
},
"product_query_smarts": ["[c:1][C:2](=O)[OH]"],
"search_terms": ["C partner", "C-C bond formation", "O partner", "aryl carboxylation retro"],
"proposal_score": 1.0,
"sources": [
{ "template_id": "rule:aryl_carboxylation_retro", "rule_name": "aryl_carboxylation_retro", "template_weight": 1.0 }
]
}
],
"stats": { "hints_returned": 1, "hints_capped": true }
}
(One match_site/mapped_atoms entry shown above for brevity; a bromobenzene
molecule with its symmetric ring positions typically produces several.)
With --max-hints 5 on the same reactant, the same run also surfaces
aryl carboxylation, Friedel-Crafts acylation, Sonogashira, and Heck
retro-templates (each with its own missing_partners[].query_smarts and
search_terms) plus aryl chloride-to-bromide (a unary rule -- no
missing_partners slot, search_terms only) — a realistic spread of
"what to search for" starting from one aryl bromide, none of it invented.
(Verified against the current 22-rule default_rules() set, 2026-08-24;
the exact top-5 shifts if the rule set changes, since ranking depends on
which templates are loaded.)
Multiple known reactants
Supply more than one --reactants value when you know several starting
materials and want to see which templates connect them directly (both slots
satisfied, zero missing partners) versus which still need an external
partner:
For a template where both known reactants match distinct slots,
known_assignments has two entries (input_index 0 and 1, one per slot)
and missing_partners is empty. hints finds every injective assignment
of known reactants to distinct slots — an assignment never places the same
reactant on two slots, and two rows of the same SMILES supplied as two
separate --reactants arguments are still two distinct inputs, each
eligible for its own slot.
Exact vs. heuristic fields
query_smarts (on each missing partner) and product_query_smarts are
always the literal SMARTS text hints parsed from the reversed
template — authoritative, never summarized or approximated.
required_features, reaction_family, and search_terms are best-effort,
auxiliary summaries derived from that authoritative SMARTS — read them as
"probably useful search hints," not as a guaranteed-complete restatement of
the constraint. Two safeguards keep this honest:
summary_complete:falsewhenever the SMARTS contains a recursive$(...)sub-pattern, aNOTover anything more complex than a single element, or anORwhose branches disagree on some property (e.g.[c,C]— aromatic vs. aliphatic carbon:aromaticis leftnullrather than arbitrarily picking one branch's value). AnORacross values of the same property ([N,O],[N;H1,H2]) is still fully summarized —required_elements: ["N", "O"]/hydrogen_constraints: ["H1 or H2"]already mean "any of these." It is alsofalsewhenever a missing partner's query spans more than one atom or has any bond at all: every field above is a flat union/merge across the whole slot, so it cannot represent which atom carries which constraint or how the atoms connect —[N:1][O:2](two bonded atoms) and[N,O:1](one N-or-O atom) would otherwise look identical whenever their per-field values happen to agree.query_smartsis unaffected either way and remains authoritative.reaction_family.basis:"derived_bond_delta"(computed purely from which mapped bonds appear/disappear between the reactant and product sides),"rule_name"(a readable fallback derived from the template's own name), or"ambiguous_across_sources"(label="mixed") — never an inferred named reaction like "Buchwald-Hartwig" unless the template's own metadata said so (RENKIN'sRetroRuledoesn't currently carry that kind of curated metadata at all)."ambiguous_across_sources"shows up when two differently-named templates merge into the same hint (same retrieval signature) but disagree on a"rule_name"-basis label; rather than silently keeping whichever template happened to be processed first, both are represented as unresolved.search_termsare the union of every merged source's own search terms (not just the first source's), derived fromreaction_family/missing_partnersplus each source'srule_name, and are explicitly auxiliary — don't search a patent database onsearch_termsalone without also checkingquery_smarts.transformation.bonds_*[].order:"directional_unspecified"for anup/down(//\, E/Z-style) bond whose orientation could not be trusted — internally, bonds are keyed by atom-map number sorted low-to-high, which can swap which atom was originally "first" for a directional bond; rather than guess, or worse, report a direction that may be backwards, this is surfaced explicitly rather than silently emitting a possibly-wrong"up"/"down".
Match sites and attachment points
Each known_assignments[].match_sites entry is one structural embedding of
the matched template slot into the known reactant: target_atom_indices is
every atom index chematic assigned in that embedding, and mapped_atoms
gives the subset carrying a template atom-map number (:N) — the actual
reaction-center/attachment atoms, cross-referenced against
transformation.bonds_formed/bonds_broken by that same map number. A
molecule with several chemically-equivalent reactive sites (e.g. 1,4-
dibromobenzene against an aryl-bromide template) reports one match_sites
entry per site, capped by --max-matches-per-slot with
match_sites_truncated set when the cap was hit.
Bond delta
transformation.bonds_formed/bonds_broken/bonds_order_changed are
computed by comparing the reactant-side and product-side mapped bonds by
atom-map number alone — never by calling chematic::rxn::run_reactants.
Each entry's order is one of single/double/triple/aromatic/
any/ring/up/down, or complex for a compound bond query this
describer doesn't attempt to flatten (never silently narrowed to a single
misleading type).
product_query_smarts is not a concrete product
product_query_smarts is a list of query patterns (usually one, plural
because a template's product side can itself have multiple disconnected
components) — a pattern any real product of this transformation would need
to match, not a specific molecule hints claims will form. Never treat it
as predict's or enumerate's concrete products field.
Merge semantics
Two templates converge into one hint when their retrieval signature
matches: the same known-reactant slot roles, the same missing-partner
query_smarts set, the same bond-delta signature, and the same
product_query_smarts set — independent of the templates' own names or
IDs. A merged hint's sources lists every contributing template; nothing
is dropped. --max-hints truncates the post-merge hint list, so two
templates merging into one hint under --max-hints 1 is never reported as
capped.
Caps, truncation, and "unknown" vs. "no match"
Every cap (--max-matches-per-slot, --max-assignments-per-template,
--max-hints) is reported explicitly when hit — match_sites_truncated
per assignment, stats.templates_with_assignments_truncated per report,
stats.hints_capped for the final list. A capped result still returns
whatever was found before the cap; it is never silently reported as "no
match," which would misrepresent a truncated search as a confirmed
negative.
Malformed templates and graph-based rules
A retro SMIRKS that fails to reverse or fails per-component SMARTS parsing
is counted in stats.template_parse_failed and the template is simply
skipped — never a hard error for the whole run. A rule with an empty
smirks field (a graph-based/hard-coded transformation such as Boc
deprotection, which has no retro-SMIRKS to reverse and analyze statically)
is counted separately in stats.graph_rules_skipped, the same convention
predict/enumerate already use — never miscounted as a parse failure.
hints validates every template component with
chematic::smarts::parse_smarts (the correct SMARTS grammar), not
predict/enumerate's parse_reaction-based check (which parses with
parse_smiles and rejects legitimate multi-condition SMARTS like
[N;H1,H2:2]) — so hints accepts a strict superset of what
predict/enumerate can run concretely. Verified against the real
extracted-template corpus: predict/enumerate accept 283/500 templates,
hints accepts all 500.
No implicit partner corpus, no product invention
hints never reads RENKIN's embedded 402-compound retro stock and never
searches, generates, or guesses a partner molecule — there is no
--partners flag at all for this subcommand, by design. It also never
predicts reaction conditions, catalysts, yields, or a calibrated
reaction-feasibility probability; proposal_score is a ranking signal only
(the maximum contributing template's frequency-derived weight), matching
predict/enumerate's own proposal_score semantics.
Performance
See cargo run --release -p renkin-forward --example hints_benchmark for a
reproducible timing + stats report across the embedded 28 default rules,
the first 100 extracted templates, and the full 500-template extracted
corpus, at small/medium/large reactant sizes. Measured on this development
machine: from under 1ms (28 rules, a single small aromatic ring) up to
roughly 570ms (the full 500-template corpus, a moderately complex
multi-ring molecule) — well within interactive/offline-batch use for this
tool's retrieval use case. No partner-side prefilter or caching was added
in this round; the existing per-call caps already bound worst-case cost,
and no measurement crossed into blocking-latency territory that would
justify the added complexity.
Non-goals
- No conditions, catalyst, solvent, temperature, or yield prediction.
- No claim that a hint corresponds to a real, literature-verified reaction
—
hintsreports what a template would require, not that a specific partner satisfying it is known to exist or to react successfully. - No partner generation, search, or ranking — that remains entirely the user's own database/search step.
- No arity ≥3 concrete application (same as
enumerate) — but unlikeenumerate,hintsdoes report the structural shape of an arity-3 template (which known-reactant slots match, which remain missing), since that's still useful retrieval information even though no concrete product can ever be generated for it.
Rust API
use renkin_forward::hints::{generate_retrieval_hints, HintGenerationConfig};
use renkin::chem_env::default_rules;
let report = generate_retrieval_hints(
&["Brc1ccccc1"],
&default_rules(),
&HintGenerationConfig::default(),
)?;
for hint in &report.hints {
println!("{}: {:?}", hint.reaction_family.label, hint.search_terms);
}