1. Library#
What this block is. A combinatorial library is a reaction plus one reagent file per reactant position. TACTICS never enumerates the whole library up front; it makes each product on demand from a tuple of reagents. This block is how you tell it what “make a product” means.
Core#
One reaction SMARTS and two .smi files describe the bundled thrombin
library (130 acids × 3,844 amines ≈ 500 k amides):
from TACTICS.library_enumeration import SynthesisPipeline, ReactionConfig, ReactionDef
data = files("TACTICS.data.thrombin")
config = ReactionConfig(
reactions=[ReactionDef(
# reactant 1: carboxylic acid, reactant 2: primary/secondary amine
reaction_smarts="[#6:1](=[O:2])[OH].[#7X3;H1,H2;!$(N[!#6]);!$(N[#6]=[O]):3]"
">>[#6:1](=[O:2])[#7:3]",
step_index=0,
description="Amide coupling",
)],
reagent_file_list=[str(data / "acids.smi"), str(data / "coupled_aa_sub.smi")],
)
pipeline = SynthesisPipeline(config)
print(pipeline.num_components, "components,", pipeline.num_steps, "step")
Reagent files are whitespace-separated SMILES name lines, one reagent
per line. The name becomes part of every product’s name
(<acid>_<amine>), which is how scores are keyed later — keep names unique
and free of underscores.
Reaction SMARTS map atoms across the arrow; the order of reactants in
the SMARTS is the order of files in reagent_file_list.
Make one product to check the chemistry does what you think:
from rdkit import Chem
from TACTICS.library_enumeration import read_reagent_file
acids = read_reagent_file(str(data / "acids.smi")) # [(smiles, name), ...]
amines = read_reagent_file(str(data / "coupled_aa_sub.smi"))
result = pipeline.enumerate_single(
[Chem.MolFromSmiles(acids[0][0]), Chem.MolFromSmiles(amines[0][0])],
reagent_keys=[acids[0][1], amines[0][1]],
)
print(result.product_name, "->", result.product_smiles)
And check every reagent against its template before you screen — an acid file with 10 % non-acids silently wastes 10 % of your budget:
check = config.reactions[0].validate_reaction(
reagent_files=config.reagent_file_list,
)
for position, pct in check.coverage_stats.items():
print(f"reactant {position}: {pct:.0f}% of reagents match the template")
validate_reaction()
returns a ValidationResult
with the incompatible reagents by position, unparseable SMILES, duplicates,
and reagents carrying protecting groups or salt fragments. Pass
deprotect=True / desalt=True to re-check after cleaning them up.
What it produces: a SynthesisPipeline.
Everything downstream takes it as synthesis_pipeline=.
Build on it#
Multi-step synthesis#
A step can consume the product of an earlier step. step_inputs says, for
each step, where each reactant comes from — a reagent file or a previous
step’s product:
from TACTICS.library_enumeration import (
ReactionConfig, ReactionDef, StepInput, InputSource, DeprotectionSpec,
)
config = ReactionConfig(
reactions=[
# step 0: Boc-protected amine + acid -> amide (the Boc survives)
ReactionDef(
reaction_smarts="[#6:1](=[O:2])[OH].[#7X3;H1,H2:3]>>[#6:1](=[O:2])[#7:3]",
step_index=0,
# remove Boc from the *product* so step 1 sees a free amine
deprotections=[DeprotectionSpec(target="product", group="Boc")],
),
# step 1: that amine + a second acid
ReactionDef(
reaction_smarts="[#6:1](=[O:2])[OH].[#7X3;H1,H2:3]>>[#6:1](=[O:2])[#7:3]",
step_index=1,
),
],
reagent_file_list=["acids_A.smi", "boc_amines.smi", "acids_B.smi"],
step_inputs={
0: [StepInput(source=InputSource.REAGENT_FILE, file_index=0),
StepInput(source=InputSource.REAGENT_FILE, file_index=1)],
1: [StepInput(source=InputSource.REAGENT_FILE, file_index=2),
StepInput(source=InputSource.PREVIOUS_STEP, step_index=0)],
},
)
print(config.num_steps, "steps; multi-step:", config.is_multi_step)
The sampler treats every reagent file as a component, so this config has three components regardless of how many steps use them.
Alternative SMARTS at one step#
Give a step several ReactionDef
entries (same step_index) and set step_modes={0: "alternative"}. The
pipeline tries each pattern in order until one applies — useful when one
reagent file mixes, say, primary and secondary amines that need different
templates. auto_detect_compatibility()
(run for you by ThompsonSampler.from_config) works out which pattern
each reagent matches so the search does not try the wrong one.
Protecting groups and salts#
Reagent files from vendors carry Boc/Fmoc/Cbz groups and counter-ions. Two tools:
Detection —
validate_reaction(...)listsprotected_reagentsandmulti_fragment_reagents. Ten common groups are built in (DEFAULT_PROTECTING_GROUPS); add your own withProtectingGroupInfoandReactionConfig(protecting_groups=[...]).Removal during synthesis — a
DeprotectionSpecon aReactionDefstrips a group from a reactant (target=1) or from the step’s product (target="product") before the next step, as in the multi-step example above.
Enumerate everything (no Thompson Sampling)#
For a library small enough to score exhaustively, or to produce a file for another tool, the pipeline enumerates on its own:
from TACTICS.library_enumeration import (
results_to_dataframe, summarize_failures, write_enumerated_library,
)
results = pipeline.enumerate_library(show_progress=False) # list of EnumerationResult
products = results_to_dataframe(results) # Polars: product_name, product_smiles, ...
print(len(products), "products;", summarize_failures(results)["failures"], "failures")
write_enumerated_library(results, "library.smi", format="smi")
enumerate_library(n_jobs=8) parallelises across reagent combinations.
write_enumerated_library()
writes csv, smi or sdf;
write_products_chunked()
splits large outputs into numbered files.
Gotchas
Reactant order = file order. The first reactant in the SMARTS is
reagent_file_list[0]. Swapping them gives 0 % coverage, not an error.Product names come from reagent names.
LookupEvaluatorandDBEvaluatorkey on<name1>_<name2>; a reagent name containing an underscore breaks the join.SMARTS that parse are not SMARTS that match.
ReactionDefonly checks that the SMARTS parses; runvalidate_reactionagainst the real files to see coverage.Salts and protecting groups are not removed unless you ask. Use
deprotect=True/desalt=Truein validation andDeprotectionSpecin synthesis.
Reference#
Library — SynthesisPipeline,
ReactionDef,
ReactionConfig,
StepInput,
DeprotectionSpec,
ValidationResult,
EnumerationResult.
Interactive: marimo edit tutorials/reaction_config_builder.py builds and
validates a config step by step; tutorials/library_enumeration_tutorial.py
walks single-step, multi-step and alternative-SMARTS cases.
Next: 2. Scoring — how a product gets a number.