Skip to content

Python API

find_routes

renkin.find_routes(
    target: str,
    depth: int = 5,
    max_routes: int = 5,
    beam_width: int = 0,
    building_blocks: list[str] | None = None,
    avoid_elements: str = "",
    require_elements: str = "",
    verbose: bool = False,
    bb_prices_path: str | None = None,
    templates_path: str | None = None,
    template_metadata_path: str | None = None,
    reranker_model_path: str | None = None,
    reranker_freq_table_path: str | None = None,
    top_templates: int | None = None,
    search_mode: str = "standard",
    coverage_templates_path: str | None = None,
    coverage_timeout_seconds: int | None = None,
    search_diagnostics: bool = False,
) -> str

Find retrosynthetic routes for a target molecule. Returns a JSON string, not a dict — parse it with json.loads() before accessing fields.

Parameters:

Parameter Type Default Description
target str required Target molecule as SMILES string
depth int 5 Maximum number of retrosynthetic steps
max_routes int 5 Maximum number of routes to return
beam_width int 0 A* beam width (0 = unlimited BFS/A*)
building_blocks list[str] \| None None Custom building block SMILES list. If None, uses data/building_blocks.smi (402 unique compounds) when that path resolves relative to the current working directory, otherwise falls back to a compiled-in 152-compound set — see Building Blocks below
avoid_elements str "" Comma-separated element symbols to ban from building blocks (e.g. "Br,I")
require_elements str "" Comma-separated element symbols that must each appear in at least one leaf building block (e.g. "B" for Suzuki-type routes)
verbose bool False Print search statistics (nodes expanded, elapsed time) to stderr
bb_prices_path str \| None None CSV (SMILES,price_per_gram) for route cost scoring
templates_path str \| None None Path to an extracted SMIRKS templates .smi file (tab-separated). None = hand-crafted rules only
template_metadata_path str \| None None Path to a JSON evidence sidecar keyed by template_id (see Template Evidence Metadata). Matching steps get an evidence field; nothing is fabricated for unmatched templates
reranker_model_path str \| None None Path to a frozen LightGBM model.txt for candidate reranking. Requires reranker_freq_table_path too. Ordering-only — never changes which candidates are considered, only their order
reranker_freq_table_path str \| None None Path to the TRAIN-frozen template frequency_table.json the reranker needs alongside reranker_model_path
top_templates int \| None None Keep only the top-N templates_path templates by frequency weight. Applies only to Stage 1 (templates_path) — coverage mode's Stage 2 (coverage_templates_path) always uses its full template set unfiltered
search_mode str "standard" "standard" (unchanged behavior) or "coverage" — see Coverage Mode below
coverage_templates_path str \| None None Stage 2's template set; required when search_mode="coverage", validated before Stage 1 even runs
coverage_timeout_seconds int \| None None Optional positive-integer wall-clock budget for Stage 2 only (cooperative cancellation, not a hard bound). 0 raises ValueError
search_diagnostics bool False Add a search_diagnostics block (beam eviction, cross-template dedup, branching factor) to the JSON output — identical field names/shape to the renkin CLI's own --search-diagnostics flag

Returns: a JSON string shaped like:

{
    "target": str,
    "routes_found": int,
    # joint_success_probability is present only when routes_found > 0:
    # 1 - Π(1 - route.success_probability) across every returned route --
    # a frequency-derived score, not a calibrated experimental probability.
    "joint_success_probability": float,
    "routes": [
        {
            "depth": int,
            "score": float,
            "confidence": float,
            "success_probability": float,
            "convergency": float,
            "route_cost": float,
            "building_blocks": [str],
            "steps": [
                {
                    "target": str,           # SMILES of molecule being disconnected
                    "rule": str,             # reaction rule name
                    "template_id": str,      # stable template identity, see Template Evidence Metadata
                    "precursors": [str],     # SMILES of precursor molecules
                    "step_confidence": float,
                    # atom_economy_status: "normal" / "above_expected_range" / "not_evaluable" (always present)
                    # conditions / atom_economy / atom_economy_raw_percent / procedure_hint /
                    # reaction_family / metadata_source / metadata_scope / evidence are present
                    # when applicable and omitted from the JSON otherwise. evidence, when
                    # present, may itself include an "examples" array (schema_version
                    # 2 sidecars only), each entry carrying a "match_kind" of
                    # "exact_substrate" or "template_only" -- see Template Evidence Metadata
                }
            ]
        }
    ]
}

When routes_found == 0, routes is [] and joint_success_probability is absent; instead there's a diagnostics object: nodes_expanded (int), max_depth_reached/beam_limit_hit (bool), matched_templates/stock_hits (int), likely_causes ([str]), suggestions ([str]) — identical shape to the renkin CLI's own empty-route JSON output.

When search_diagnostics=True, a search_diagnostics object is added in both cases above (beam-prune/crowd-out counters — see the renkin CLI's --search-diagnostics flag for the full field list).

Example (also run in CI — see examples/quickstart.py):

"""RENKIN Python quickstart. Runs as part of CI so this example can never
silently drift from the real API (see .github/workflows/ci.yml)."""

import json

import renkin

result = json.loads(
    renkin.find_routes(
        target="CC(=O)Oc1ccccc1C(=O)O",  # Aspirin
        depth=5,
        max_routes=3,
    )
)

print(f"Routes found: {result['routes_found']}")
for route in result["routes"]:
    print(f"Route (depth {route['depth']}):")
    for step in route["steps"]:
        print(f"  {step['target']} -> {' + '.join(step['precursors'])}")
        print(f"  via {step['rule']}")

Coverage Mode

Opt-in Stage-1/Stage-2 escalation: only if the default templates_path search finds nothing does Stage 2 run, against a separately loaded, larger coverage_templates_path template set, cooperatively cancellable via coverage_timeout_seconds. See the design doc for the full rationale and the formal-TEST confirmation numbers.

"""RENKIN Python coverage-mode quickstart. Runs as part of CI so this
example can never silently drift from the real API (see
.github/workflows/ci.yml).

Coverage mode: Stage 1 (the default template set) runs first; only if it
finds nothing does Stage 2 run against a separately loaded, larger template
set. This target (N-phenylsuccinimide) is unsolvable by Stage 1's default
rules alone at this depth, but solvable once Stage 2 escalates to
tests/fixtures/coverage_mode_templates.smi's two extracted templates -- see
that file's header comment for how the target/template pairing was chosen.
"""

import json
import os

import renkin

REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
COVERAGE_TEMPLATES = os.path.join(
    REPO_ROOT, "tests", "fixtures", "coverage_mode_templates.smi"
)

result = json.loads(
    renkin.find_routes(
        target="O=C1CCC(=O)N1c1ccccc1",  # N-phenylsuccinimide
        depth=2,
        max_routes=1,
        beam_width=100,
        search_mode="coverage",
        coverage_templates_path=COVERAGE_TEMPLATES,
    )
)

print(f"search_mode: {result['search_mode']}")
print(f"selected_stage: {result['selected_stage']}")
print(f"stage2_invoked: {result['stage2_invoked']}")
print(f"routes_found: {result['routes_found']}")
for route in result["routes"]:
    for step in route["steps"]:
        print(f"  {step['target']} -> {' + '.join(step['precursors'])}")

Standard-mode output (the default) is byte-for-byte unchanged: the extra fields below are omitted entirely, not null, unless search_mode="coverage" is actually passed.

Returns, in addition to everything above, when search_mode="coverage":

{
    "search_mode": "coverage",
    "selected_stage": "stage1" | "stage2",
    "stage2_invoked": bool,
    "stage1_timeout": bool,
    "stage2_timeout": bool,
    "stage1_elapsed_ms": float,
    "stage2_elapsed_ms": float | None,  # None iff Stage 2 never ran
    "total_elapsed_ms": float,
}

Identical field names and shapes to the renkin CLI's own coverage-mode JSON output.

predict_forward

renkin.predict_forward(
    reactants: list[str],
    templates_path: str | None = None,
    max_results: int = 5,
) -> str

Predicts forward reaction products from a list of reactant SMILES, by running retrosynthetic SMIRKS templates in reverse. Graph-based rules (e.g. ester_cleavage, amide_cleavage) are not reversible this way and are silently skipped. Returns a JSON string: [{"template": str, "products": [str], "weight": float}, ...].

validate_forward

renkin.validate_forward(
    route_json: str,
    templates_path: str | None = None,
    max_results: int = 5,
) -> str

Validates each step of a retrosynthetic route by checking whether forward template application reproduces the claimed target from its precursors. route_json must be a single route object with a top-level steps array — i.e. one entry of find_routes()'s routes list, not the full find_routes() output itself (which has no top-level steps key and raises ValueError: route JSON must have a 'steps' array if passed directly):

result = json.loads(renkin.find_routes(target="CC(=O)Oc1ccccc1C(=O)O", depth=1, max_routes=1))
route_json = json.dumps(result["routes"][0])
validation = json.loads(renkin.validate_forward(route_json))

Returns a JSON string: [{"step_index": int, "target": str, "verified": bool, "top_predictions": [...]}, ...].

audit_route

renkin.audit_route(
    content: str,
    format: str = "auto",
    stock_text: str = "",
    policy: str = "standard",
) -> str

Audits an already-completed retrosynthesis route (a RENKIN --format json export or an AiZynthFinder single-route/batch export) for structural integrity, stock coverage, element accounting, and forward-reaction reproducibility -- the Python binding for renkin audit-route, calling the identical pipeline the CLI and the WASM audit_route_v2 export use, so the same input and policy get the same verdict from every surface. See Audit Reproducibility and Compatibility Contract for the full audit_manifest/report shape and what each policy means.

A thin binding on purpose: content is JSON text you already have in hand (read any file yourself, including a gzip-compressed AiZynthFinder batch export -- decompress it before passing it in, this function never touches the filesystem). format is "auto" (default) / "renkin" / "aizynthfinder" / "syntheseus". stock_text is an optional .smi-style listing (one SMILES per line, #-comments allowed); omitted, stock validation reports not_evaluable, never a silent pass. policy is "informational" / "standard" (default) / "strict" -- controls only how each route's status is derived from findings already collected, never which findings are detected or reported.

with open("trees.json", encoding="utf-8") as f:
    report = json.loads(
        renkin.audit_route(f.read(), format="aizynthfinder", policy="strict")
    )
print(report["summary"])

Returns a JSON string: the same AuditRouteReport shape renkin audit-route --output json produces, including audit_manifest.policy recording the policy actually used. Raises ValueError on malformed JSON, an unrecognized route shape, or an invalid format/policy value -- fail-loud, never a partial or guessed result.

audit_route_report

renkin.audit_route_report(
    content: str,
    format: str = "auto",
    stock_text: str = "",
    policy: str = "standard",
) -> AuditRouteReport

Same arguments, same validation, same ValueErrors as audit_route() -- the only difference is the return type. audit_route() itself is completely unchanged by this: it's still there, still returns a plain str, for anyone who wants the raw JSON. audit_route_report() is a pure-Python convenience layer on top (python/renkin/audit_report.py, defined outside the compiled extension) that calls audit_route(), json.loads()s it, and hands back attribute-accessible dataclasses instead of a dict-of-dicts:

report = renkin.audit_route_report(content, format="aizynthfinder", policy="strict")
print(report.audit_manifest.policy)
print(report.routes[0].status)
for finding in report.routes[0].findings:
    print(finding.code, finding.severity)
print(report.routes[0].steps[0].forward_validation.status)

Returns an AuditRouteReport:

Field Type
schema_version int
source_format str
audit_manifest AuditManifest
summary AuditRouteSummary
routes list[AuditReport]

AuditManifest: renkin_version, report_schema_version (int), source_format, input_sha256, policy (all str), plus source_version: str | None and stock_sha256: str | None.

AuditRouteSummary: routes_total, passed, fail, partial (all int) -- note passed, not pass: the wire JSON's key really is "pass", renamed here since pass is a Python reserved word.

AuditReport (one per audited route): source, status (str), route_tree_parseable (bool), reaction_steps_parseable: bool | None, stock_validation: StockValidationResult | None, target_element_accounting_status: str | None, normalized_route_sha256: str | None, steps: list[AuditedStep], findings: list[AuditFinding].

AuditedStep: target: str, precursors: list[str], forward_validation: ForwardValidationResult.

ForwardValidationResult: status: str, method: str, evidence_basis: str | None ("declared_rule_template" | "derived_graph_rule_roundtrip" | "source_tool_reaction" | None -- see Audit Reproducibility and Compatibility Contract for what each means), reason: str | None. StockValidationResult: status: str, reason: str | None. AuditFinding: code: str, severity: str, node: str | None.

Every str | None field here collapses two different wire-level states into one Python value. In the raw JSON, some optional fields are an explicit null and some are entirely absent keys (Rust's skip_serializing_if) -- both mean "not applicable here", and both become None on the typed side. This loses no information that matters to a caller of this convenience API; anyone who genuinely needs to tell "explicit null" apart from "key absent" should use audit_route() (the string API) and inspect the parsed JSON directly instead.

Status/code/severity fields stay plain str, not a Python Enum. A real Enum would raise the moment a future RENKIN version ships a new variant value this stub doesn't know about yet; str degrades gracefully. The current closed set of values for each is documented in Audit Reproducibility and Compatibility Contract.

__version__

>>> import renkin
>>> renkin.__version__
'0.35.0'

The version string is a module attribute, not a function.

Typed Usage

RENKIN ships a type stub (renkin.pyi + py.typed) alongside the compiled extension in every published wheel. Editors and type checkers (mypy, pyright) pick it up automatically once renkin is installed — no extra import or configuration needed:

import renkin

result: str = renkin.find_routes("CC(=O)Oc1ccccc1C(=O)O", depth=3)  # type-checked

The stub only types the function signatures (arguments and the fact that every function returns str); it doesn't type the JSON contents of that string — parse with json.loads() and refer to the return-shape documentation above for the actual fields.

Building Blocks

There are two different building-block sets, and which one you get by default depends on where you run Python from:

  • data/building_blocks.smi — the full curated library, 402 unique compounds (by canonical SMILES). Loaded automatically only when that relative path resolves from your current working directory — in practice, when you're running from a checkout of the renkin repository itself. A wheel installed from PyPI (pip install renkin) does not bundle this file.
  • Compiled-in fallback (DEFAULT_BUILDING_BLOCKS) — 152 unique compounds, built into the extension module itself. Used automatically whenever the 402-compound file above isn't found — which, for a typical pip install renkin used outside a repo checkout, is every time.

Both cover similar ground (simple aliphatics, aryl/heteroaryl halides, boronic acids, common heterocycles and pharmaceutical amines, protecting-group reagents, amino acids), but they are not the same list — don't assume a specific compound is present in one because it's present in the other.

To get a specific, known set reliably, pass it explicitly rather than relying on either default:

result = renkin.find_routes(
    target="...",
    building_blocks=["CC(=O)O", "Oc1ccccc1", ...],  # or read your own data/building_blocks.smi
)

Entries that fail to parse as SMILES are silently skipped (not an error) — they simply can't match as a leaf building block during search.

Error Handling

import renkin

try:
    result = renkin.find_routes("not_a_valid_smiles!!!")
except ValueError as e:
    print(f"Error: {e}")
    # Error: Failed to parse SMILES: not_a_valid_smiles!!!

find_routes/predict_forward/validate_forward raise ValueError (via PyO3) when the target SMILES fails to parse, when template_metadata_path points to malformed or invalid metadata (validated before search starts), or when route_json isn't valid JSON.

reranker_model_path/reranker_freq_table_path are the one exception to "bad input raises": a missing file, a malformed model, or only one of the two paths given never raises — it prints a warning to stderr and falls back to find_routes's legacy candidate ordering for that call. This matches the renkin CLI's --reranker-model/--reranker-freq-table flags exactly, since a broken optional reranker file shouldn't be able to take down an otherwise-working search.