Find the best compounds in a combinatorial library without enumerating all of them.
TACTICS (Thompson Sampling-Assisted Chemical Targeting and Iterative Compound Selection) uses Thompson Sampling to search ultra-large combinatorial libraries, scoring only a small fraction of the products while still recovering most of the top hits. A typical run evaluates 1–2% of the library and recovers ~85–95% of the true top-100.
📖 Documentation · 🧪 Tutorials · 📋 Changelog
TACTICS fits if you have:
- A combinatorial library defined by a reaction plus lists of reagents (2 or 3 components), and
- A scoring function that is too slow to run on every product — docking, shape similarity, or an ML model — or a big table of precomputed scores.
If your library is small enough to score exhaustively, you don't need TACTICS.
pip install chem-tacticsRequires Python 3.11+.
Optional extras and development install
pip install chem-tactics[tutorials] # interactive marimo notebooks
pip install chem-tactics[test] # test dependencies
# development install
git clone https://github.com/aakankschit/TACTICS.git
cd TACTICS
pip install -e ".[test]"Screen a two-component amide library against a table of precomputed scores:
from TACTICS.library_enumeration import SynthesisPipeline, ReactionConfig, ReactionDef
from TACTICS.thompson_sampling import ThompsonSampler, get_preset
from TACTICS.thompson_sampling.core.evaluator_config import LookupEvaluatorConfig
# 1. Describe the library: one reaction + one reagent file per component
pipeline = SynthesisPipeline(ReactionConfig(
reactions=[ReactionDef(
reaction_smarts="[C:1](=O)[OH].[NH2:2]>>[C:1](=O)[NH:2]",
step_index=0,
)],
reagent_file_list=["acids.smi", "amines.smi"],
))
# 2. Describe how to score a product
evaluator = LookupEvaluatorConfig(ref_filename="scores.csv")
# 3. Take a tuned preset and run
config = get_preset(synthesis_pipeline=pipeline, evaluator_config=evaluator)
sampler = ThompsonSampler.from_config(config)
sampler.warm_up(num_warmup_trials=config.num_warmup_trials)
results = sampler.search(num_cycles=config.num_ts_iterations)
sampler.close()
print(results.sort("score", descending=True).head(10))results is a Polars DataFrame of every product that was evaluated, with
columns score, SMILES and Name.
With
LookupEvaluatorandDBEvaluator, TACTICS skips molecule generation entirely and scores by product code, soSMILESreadsFAILand products are identified byName(the underscore-joined reagent names). Evaluators that need a molecule — docking, ROCS, fingerprints — populateSMILES.
Docking and ROCS take seconds per molecule, so evaluation dominates runtime.
Set processes to the number of cores you have — each worker builds its own
docking session, so OpenEye evaluators parallelize correctly:
from TACTICS.thompson_sampling.core.evaluator_config import FredEvaluatorConfig
config = get_preset(
synthesis_pipeline=pipeline,
evaluator_config=FredEvaluatorConfig(design_unit_file="receptor.oedu"),
mode="minimize", # docking scores: lower is better
batch_size=100,
)
config.processes = 32 # or os.cpu_count()
sampler = ThompsonSampler.from_config(config)Leave
processes=1forLookupEvaluatorandDBEvaluator. Their lookups are faster than the cost of shipping work to a worker, so parallelism makes those runs slower.
On macOS/Windows, guard your entry point with
if __name__ == "__main__":— the defaultspawnstart method re-imports your script in every worker. On a cluster, make sure the.oedureceptor is readable from every node (automatic on NFS/GPFS).
Scoring functions (evaluator_config):
| Evaluator | Use it for | Speed |
|---|---|---|
LookupEvaluatorConfig |
A CSV/Parquet table of precomputed scores | instant |
DBEvaluatorConfig |
Scores in a SQLite database | instant |
FPEvaluatorConfig |
Fingerprint similarity to a reference ligand | fast |
ROCSEvaluatorConfig |
3D shape/colour overlay (OpenEye) | slow |
FredEvaluatorConfig |
Docking into a receptor (OpenEye) | slow |
MLClassifierEvaluatorConfig |
A trained scikit-learn model | varies |
Presets (get_preset(name, ...)):
| Preset | What it does |
|---|---|
recommended (default) |
Top-Two Thompson Sampling — best overall |
recommended_rws |
Roulette wheel with criticality-aware thermal cycling |
baseline |
Balanced warmup + greedy, for measuring what the search adds |
legacy_rws |
Reproduces Zhao et al. 2025 |
Start with recommended. If you are benchmarking, run recommended and
recommended_rws and take the better result — they favour different library
structures.
Building a config by hand instead of using a preset
Presets are just ThompsonSamplingConfig objects, so you can construct one
directly to control the strategy, warmup and search budget:
from TACTICS.thompson_sampling import ThompsonSamplingConfig, ThompsonSampler
from TACTICS.thompson_sampling.strategies.config import TopTwoConfig
from TACTICS.thompson_sampling.warmup.config import EnhancedWarmupConfig
config = ThompsonSamplingConfig(
synthesis_pipeline=pipeline,
evaluator_config=evaluator,
strategy_config=TopTwoConfig(mode="maximize", beta=0.5),
warmup_config=EnhancedWarmupConfig(),
num_warmup_trials=3,
num_ts_iterations=1000,
batch_size=100,
processes=1,
seed=42,
)
sampler = ThompsonSampler.from_config(config)Configs are Pydantic models, so invalid values raise ValidationError at
construction rather than failing mid-run. Other selection strategies —
GreedyConfig, RouletteWheelConfig, UCBConfig, EpsilonGreedyConfig,
BayesUCBConfig — are available for baselines and comparisons.
See the configuration docs for the full reference.
Interactive marimo notebooks in tutorials/ (they default
to a bundled thrombin dataset, so they run with no setup):
marimo run tutorials/thompson_sampling_tutorial.py # compare strategies
marimo edit tutorials/library_enumeration_tutorial.py # build a library| Tutorial | What it covers |
|---|---|
thompson_sampling_tutorial.py |
Comparing selection strategies |
library_enumeration_tutorial.py |
SynthesisPipeline and enumeration |
reaction_config_builder.py |
Building and validating reaction SMARTS |
custom_evaluator_tester.py |
Writing your own evaluator |
- API reference and guides: TACTICS Documentation
- Runnable scripts: see
examples/ - Build docs locally:
cd docs && make html
Fork, branch, add tests for new behaviour, make sure pytest tests/ passes,
then open a pull request.
@software{tactics,
title={TACTICS: Thompson Sampling-Assisted Chemical Targeting and
Iterative Compound Selection for Drug Discovery},
author={Aakankschit Nandkeolyar},
year={2025},
url={https://github.com/aakankschit/TACTICS}
}Open an issue on GitHub, or contact anandkeo@uci.edu.
Licensed under the MIT License — see LICENSE.
This work is based on previous work by Patrick Walters. This project is a collaboration between the University of California Irvine, Leiden University and Groningen University.
