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_versionmust be in the supported range.- No duplicate
template_idkeys, no duplicate or danglingreference_ids. - Yield percentages must be in
[0, 100]; anymin/maxrange must havemin <= max. - DOI/patent
identifiermust not be empty (aurl/dataset_recordidentifier may be, since not every one is meaningfully citable). - A
template_idpresent 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:
examplesrequiresschema_version: 2. Aschema_version: 1sidecar with anexampleskey is a hard error, not a silent no-op — v1 sidecars withoutexamplesload and behave exactly as before.schema_version: 2requires reported yields underexamples[].reported_yield, not the template-levelreported_yieldslist. A non-empty template-levelreported_yieldsunderschema_version: 2is 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-levelcondition_candidates,warnings, andreferencesremain allowed underschema_version: 2— onlyreported_yieldsmoves to the example level.)schema_version: 1keeps allowing template-levelreported_yields, unchanged, for backward compatibility.- Every nested
conditions/reported_yield/warningsentry 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_smilesmust parse andprecursor_smilesmust be non-empty;idmust be non-empty and unique within its template;reference_ids(the example's own, and each nestedconditions/reported_yield/warningsentry's own) must each point at a reference declared in that template'sreferenceslist.
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_yieldsis 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'sreported_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 explainalways labels it "not a prediction". step_confidence/success_probabilityare 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.warningsreflects 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 emptywarningslist — 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
- Run
renkin template ids data/templates_extracted_5000.smi --format jsonto get every template's stabletemplate_id. - For the templates you actually have literature/patent evidence for, add an
entry keyed by that
template_id. - Pass the file via
--template-metadata(CLI) ortemplate_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 externaltarget_smiles/precursor_smilesrecords against RENKIN's stabletemplate_ids, reusing the exact same canonicalization and single-step retro application that route search and--format explainalready 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. Seerenkin evidence match --help-equivalent usage below.scripts/ord_evidence_audit.py(Python, offline) — reads ORDDatasetfiles you've already downloaded from--ord-data <dir>, shells out torenkin evidence matchfor template matching, and emits a sidecar, an audit report, and a reproducibility manifest. Requiresord-schema— install it fromscripts/requirements-ord-evidence.txt, a separate pin from RENKIN's own runtime dependencies. Seescripts/README_ord_evidence.mdfor 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_yieldimported 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 aReactionWarningautomatically. - 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_idagainst 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 onetemplate_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 anysmirks-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
ProductMeasurementYIELDtype does not itself distinguish an isolated-weight yield from a calibrated-assay yield (theuses_internal_standard/uses_authentic_standardflags describe quantification method, not that distinction) — RENKIN maps it tobasis: "unknown"rather than guessing, and keeps the underlying measurement's provenance as a note instead. Only ORD's own explicitoutcome.conversionfield maps tobasis: "conversion". - ORD has no distinct "base" role. Its
ReactionRoleTypeenum has noBASEvalue —REAGENT-role components surface under RENKIN'sreagentsfield, andbasesis 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
- Python retrosynthesis guide / Rust retrosynthesis guide
- Python API / Rust API — full
template_metadata_path/sidecar-loading signatures