Skip to content

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:

cargo build --release -p renkin-forward

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 as predict/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 sets match_sites_truncated: true on 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 increments stats.templates_with_assignments_truncated and is never silently treated as "no assignment found".

One known reactant

renkin-forward hints --reactants "Brc1ccccc1" --max-hints 1

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:

renkin-forward hints --reactants "Brc1ccccc1" "NCC"

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: false whenever the SMARTS contains a recursive $(...) sub-pattern, a NOT over anything more complex than a single element, or an OR whose branches disagree on some property (e.g. [c,C] — aromatic vs. aliphatic carbon: aromatic is left null rather than arbitrarily picking one branch's value). An OR across 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 also false whenever 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_smarts is 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's RetroRule doesn'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_terms are the union of every merged source's own search terms (not just the first source's), derived from reaction_family/missing_partners plus each source's rule_name, and are explicitly auxiliary — don't search a patent database on search_terms alone without also checking query_smarts.
  • transformation.bonds_*[].order: "directional_unspecified" for an up/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 — hints reports 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 unlike enumerate, hints does 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);
}