Skip to content

Reaction Evidence Metadata

A retrosynthesis route tells you what bond to break. It doesn't tell you what conditions were actually used, what yield was reported, which paper or patent this comes from, or what can go wrong. RENKIN's evidence metadata system exists to attach exactly that — as curated, cited data you supply, never as something RENKIN invents.

The Problem: Templates Don't Have Stable Identity by Default

An extracted SMIRKS template's only name is its position in a file (extracted_0, extracted_1, ...). Re-sort the file, re-run the extraction pipeline, or add a template in the middle, and every name after that point shifts. You can't attach a DOI to "extracted_412" if extracted_412 might be a completely different reaction next week.

RENKIN gives every template a stable template_id instead:

  • Hand-crafted rules: rule:<rule_name> (e.g. rule:suzuki_retro) — the name is a literal source-code identifier, so it's already stable.
  • Extracted templates: smirks-sha256:<hex> — the SHA-256 hex digest of the trimmed SMIRKS string itself. Independent of file position, load order, or count. Purely syntactic: no SMIRKS canonicalization is performed, so an equivalent reaction written with different atom-map numbering gets a different ID.

Run renkin template ids <file.smi> to list every template's template_id, current display name, SMIRKS, and weight — this is how you look up the ID you need to reference in a sidecar file.

Attaching Evidence

--template-metadata sidecar.json (CLI) or template_metadata_path=... (Python find_routes) loads a JSON file keyed by template_id:

{
  "schema_version": 1,
  "templates": {
    "smirks-sha256:ef8778a2888469d619c52cce7e74f6848e101049050dd1b765b78f32e3c94498": {
      "references": [
        { "id": "ref-1", "kind": "doi", "identifier": "10.xxxx/example" }
      ],
      "condition_candidates": [
        {
          "catalysts": ["Pd(PPh3)4"],
          "bases": ["K2CO3"],
          "solvents": ["EtOH", "water"],
          "temperature_c": { "min": 75.0, "max": 85.0 },
          "source": "literature",
          "scope": "template",
          "reference_ids": ["ref-1"]
        }
      ],
      "reported_yields": [
        {
          "percentage": { "min": 72.0, "max": 81.0 },
          "basis": "isolated",
          "source": "literature",
          "scope": "template",
          "reference_ids": ["ref-1"]
        }
      ],
      "warnings": [
        {
          "code": "possible_protodeboronation",
          "severity": "medium",
          "message": "Protodeboronation has been reported under prolonged aqueous heating.",
          "source": "literature",
          "scope": "template",
          "reference_ids": ["ref-1"]
        }
      ]
    }
  }
}

A step whose template matches this template_id gets an evidence field with condition_candidates, reported_yields, references, and warnings. A step whose template has no entry simply has no evidence key at all.

kind accepts doi, patent, url, or dataset_record. basis (for a reported yield) accepts isolated, assay, conversion, or unknown — the same word chemists already use to qualify a literature yield.

Validation

The sidecar is loaded and validated before search starts — a malformed file is a hard error, not a silent partial load:

  • schema_version must be in the supported range.
  • No duplicate template_id keys, no duplicate or dangling reference_ids.
  • Yield percentages must be in [0, 100]; any min/max range must have min <= max.
  • DOI/patent identifier must not be empty (a url/dataset_record identifier may be, since not every one is meaningfully citable).
  • A template_id present in the sidecar but absent from the loaded rule set prints a warning (not a failure) — it's not silently ignored, but it also doesn't block a search over a different template set.

Substrate-Specific Examples (schema_version: 2)

Everything above is template-level evidence: it applies to every step that uses that template, regardless of the actual molecule. schema_version: 2 adds examples — a per-template array where each entry is one curated record of this exact reaction, not the template in general:

{
  "schema_version": 2,
  "templates": {
    "smirks-sha256:ef8778a2888469d619c52cce7e74f6848e101049050dd1b765b78f32e3c94498": {
      "references": [
        { "id": "ref-1", "kind": "doi", "identifier": "10.xxxx/example" }
      ],
      "examples": [
        {
          "id": "ex-1",
          "target_smiles": "c1ccc(-c2ccccc2)cc1",
          "precursor_smiles": ["Brc1ccccc1", "c1ccccc1"],
          "conditions": {
            "catalysts": ["Pd(PPh3)4"],
            "solvents": ["EtOH"],
            "source": "literature",
            "scope": "substrate_specific",
            "reference_ids": ["ref-1"]
          },
          "reported_yield": {
            "percentage": 78.0,
            "basis": "isolated",
            "source": "literature",
            "scope": "substrate_specific",
            "reference_ids": ["ref-1"]
          },
          "reference_ids": ["ref-1"]
        }
      ]
    }
  }
}

Key rules:

  • examples requires schema_version: 2. A schema_version: 1 sidecar with an examples key is a hard error, not a silent no-op — v1 sidecars without examples load and behave exactly as before.
  • schema_version: 2 requires reported yields under examples[].reported_yield, not the template-level reported_yields list. A non-empty template-level reported_yields under schema_version: 2 is a hard error — otherwise a substrate-specific number could be placed at the template level and get applied to every step using that template, defeating the point of substrate-specific evidence. (Template-level condition_candidates, warnings, and references remain allowed under schema_version: 2 — only reported_yields moves to the example level.) schema_version: 1 keeps allowing template-level reported_yields, unchanged, for backward compatibility.
  • Every nested conditions/reported_yield/warnings entry inside an example must be scoped "substrate_specific". This is enforced at load time (any other scope there is rejected), unlike template-level entries, which aren't scope-restricted.
  • target_smiles/precursor_smiles must parse and precursor_smiles must be non-empty; id must be non-empty and unique within its template; reference_ids (the example's own, and each nested conditions/ reported_yield/warnings entry's own) must each point at a reference declared in that template's references list.

Matching an example to a route step. This happens once per step, not just in --format explain — a step's evidence.examples are resolved, not merely copied from the sidecar. Each example is compared against that step's actual target/precursors by canonical SMILES (target must match exactly; the precursor set must match after canonicalizing, sorting, and deduplicating both sides, so precursor_smiles order in the sidecar never matters). Every exact-substrate match is kept; same-template-different-substrate precedents are capped at 3. Each resolved entry in JSON carries a match_kind ("exact_substrate" or "template_only") right alongside the example's own fields, and evidence.template_examples_total reports how many examples the template declared in total — so a JSON/Python consumer can tell "evidence for this exact reaction" from "literature precedent for a different substrate" without re-implementing the canonical-SMILES comparison itself, and can tell how many precedents were truncated. When a template has examples, evidence.references is trimmed to only the ids actually cited by what's kept (template-level entries plus the retained examples), not the template's full reference list — this is what keeps output bounded when a template carries hundreds of dataset-derived examples. A template with no examples (every schema_version: 1 entry, and any schema_version: 2 entry that doesn't happen to use examples) keeps its full references list untouched, including a standalone citation not cited by any condition/yield/warning — trimming only ever kicks in alongside examples.

In --format explain: each step shows every exact-substrate example plus up to 3 template-only ones, exact matches first, each labeled either Exact substrate example: or Template-level literature example (different substrate; not a prediction):. Any examples beyond what's shown are summarized as ... and N more template examples rather than silently dropped. Under each example, conditions/reported_yield/warnings each show their own cited references directly beneath them (not just the example's top-level reference_ids) — a reference cited in more than one place for the same example is shown once, not repeated.

Reported vs. Predicted — Read This Before You Cite a Number

This is the single most important distinction on this page:

  • reported_yields is a citation, not a prediction. It's exactly what the reference you supplied says was achieved, for that specific template/reaction — never a value RENKIN computed or estimated for your target molecule. The same is true of an example's reported_yield, even when the example is an exact substrate match: it's what was reported for that reaction, not a RENKIN forecast for your route.
  • A template-only example (different substrate) is reference context, not a forecast. It shows the template has literature precedent, not that your specific target/precursors will behave the same way — that's exactly why --format explain always labels it "not a prediction".
  • step_confidence / success_probability are unrelated to yield. They are template-frequency-derived search-ranking scores (how common a disconnection was in the training corpus) — not a measured yield, not a predicted yield, and not a probability that your specific synthesis will succeed. Nothing in this system changes what those two fields mean.
  • warnings reflects only what's in the sidecar you supplied. RENKIN does not run automatic side-reaction detection, does not search the literature for you, and does not infer a warning from structure alone. An empty warnings list — whether at the template level or inside a specific example — means no warning was curated for that template or substrate. It does not mean no side reaction is possible.
  • Templates without a sidecar entry get nothing fabricated. No made-up conditions, no invented yield, no synthesized-sounding warning.

Building a Sidecar File

  1. Run renkin template ids data/templates_extracted_5000.smi --format json to get every template's stable template_id.
  2. For the templates you actually have literature/patent evidence for, add an entry keyed by that template_id.
  3. Pass the file via --template-metadata (CLI) or template_metadata_path (Python) alongside your normal search.

There's no requirement to cover every template — the sidecar is additive, and uncovered templates behave exactly as they did before you supplied one.

Importing from ORD (Open Reaction Database)

Hand-authoring a sidecar doesn't scale past a handful of templates. For a locally-downloaded ORD corpus, two tools turn it into a schema_version: 2 sidecar deterministically and auditably, without ever calling out to the network:

  • renkin evidence match (Rust CLI) — exact-set batch matching of external target_smiles/precursor_smiles records against RENKIN's stable template_ids, reusing the exact same canonicalization and single-step retro application that route search and --format explain already use. No fuzzy matching, no partial-structure or stereochemistry-blind similarity — a rule counts as matching only if its retro-application reproduces the input precursor set exactly. See renkin evidence match --help-equivalent usage below.
  • scripts/ord_evidence_audit.py (Python, offline) — reads ORD Dataset files you've already downloaded from --ord-data <dir>, shells out to renkin evidence match for template matching, and emits a sidecar, an audit report, and a reproducibility manifest. Requires ord-schema — install it from scripts/requirements-ord-evidence.txt, a separate pin from RENKIN's own runtime dependencies. See scripts/README_ord_evidence.md for full usage, acceptance criteria, and field-mapping rules.
python scripts/ord_evidence_audit.py \
  --ord-data /path/to/ord-data \
  --renkin-bin target/release/renkin \
  --templates data/templates_extracted_5000.smi \
  --output-sidecar artifacts/ord_candidates.json \
  --output-report artifacts/ord_audit.json \
  --output-manifest artifacts/ord_manifest.json

What this is not:

  • Not automatic literature search. RENKIN never fetches, searches, or scrapes anything — every input file is one you already have locally.
  • Not yield or condition prediction. A reported_yield imported this way is exactly what the ORD record says, for that exact reaction — never a number RENKIN computed or estimated. A template-only precedent is still not a forecast for your target (see "Reported vs. Predicted" above).
  • Not automatic side-reaction detection. Undesired/side products present in an ORD outcome are counted in the audit report (with_non_desired_products) for future review — never turned into a ReactionWarning automatically.
  • Not a guarantee that every unique match is safe evidence. A record is only written to the sidecar if RENKIN's matcher reports a unique template match — ambiguous or no-match records are excluded and counted in the audit report, never guessed at. Even a unique match is not enough by itself: the importer also checks the matched template_id against an explicit, reviewed export allowlist (rule:ester_cleavage, rule:amide_cleavage, rule:reductive_amination_retro). Three generic single-bond-break rules (rule:cn_aliphatic_cleavage, rule:michael_retro, rule:co_aliphatic_cleavage) are audited but never exported to the sidecar in this phase: a broad rule like these can cover chemically distinct reaction contexts under one template_id, so a unique exact-precursor match alone isn't yet trusted as sufficient evidence for them. A unique match on any other template — another hand-crafted rule, or any smirks-sha256:* extracted template — is likewise rejected (out_of_scope_template): the allowlist is enforced explicitly, not inferred from the absence of the three audit-only ids.
  • Yield basis is conservative by construction. ORD's ProductMeasurement YIELD type does not itself distinguish an isolated-weight yield from a calibrated-assay yield (the uses_internal_standard/uses_authentic_standard flags describe quantification method, not that distinction) — RENKIN maps it to basis: "unknown" rather than guessing, and keeps the underlying measurement's provenance as a note instead. Only ORD's own explicit outcome.conversion field maps to basis: "conversion".
  • ORD has no distinct "base" role. Its ReactionRoleType enum has no BASE value — REAGENT-role components surface under RENKIN's reagents field, and bases is always empty for ORD-imported conditions. This isn't guessed around by name-matching.

Licensing. The importer code (renkin evidence match, scripts/ord_evidence_audit.py) is MIT, same as the rest of RENKIN. ORD reaction data itself is CC-BY-SA-4.0. A generated sidecar or audit report is a derivative of whatever ORD data you fed it — it carries ORD-sourced SMILES, yields, conditions, and reference identifiers — so treat it as CC-BY-SA-4.0 with attribution, not as MIT. This repository does not (and will not, in this phase) bundle any real ORD corpus; only a small hand-authored test fixture is committed (see scripts/tests/fixtures/README.md). A curated, real starter evidence pack is expected to follow as a separate PR once an audit report has been reviewed.

Next Steps