TACTICS

TACTICS#

Thompson Sampling over combinatorial libraries: define, score, search.

You have reagent files, a reaction, and a way to score a product. TACTICS finds the best products in a library of millions by scoring a few thousand — maintaining a posterior per reagent and choosing what to make next from what it has learned. Six building blocks, composed in order:

1 · Library

A reaction SMARTS and one reagent file per component → SynthesisPipeline. Validate coverage; multi-step; deprotection.

1. Library
2 · Scoring

A product in, a number out. A lookup table, your own Python function, fingerprints, docking, a model.

2. Scoring
3 · Search

get_preset → from_config → warm_up → search. Presets, hand-built configs, choosing a strategy and warmup.

3. Search
4 · Scale

Worker processes for slow scorers; pre-enumerated products; budgeting a run on a cluster.

4. Scale
5 · Inspect

What the search learned: per-cycle diagnostics, the posterior landscape, a SAR summary, analysis functions.

5. Inspect
6 · Visualise

Recovery benchmarks (Altair) and mechanism plots (matplotlib). pip install chem-tactics[viz].

6. Visualise
Extend

Your own strategy, warmup or evaluator class.

Extending TACTICS
Reference

Every class and function, generated from the code.

API Reference
Theory

The mathematics: Thompson Sampling, CATS, TT-TS, Bayes-UCB.

Theory
Tutorials

Interactive marimo notebooks.

Tutorials

Ten minutes#

pip install chem-tactics

Then run the bundled thrombin library (130 acids × 3,844 amines) against its precomputed docking scores — this is also the README quickstart, and the test suite executes it:

from TACTICS import ThompsonSampler, get_preset
from TACTICS.library_enumeration import SynthesisPipeline, ReactionConfig, ReactionDef
from TACTICS.thompson_sampling import LookupEvaluatorConfig

data = files("TACTICS.data.thrombin")  # bundled example: 130 acids x 3844 amines

# 1. Describe the library: one reaction, one reagent file per component
pipeline = SynthesisPipeline(ReactionConfig(
    reactions=[ReactionDef(
        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,
    )],
    reagent_file_list=[str(data / "acids.smi"), str(data / "coupled_aa_sub.smi")],
))

# 2. Describe how a product is scored (here: a precomputed docking table)
evaluator = LookupEvaluatorConfig(ref_filename=str(data / "product_scores.parquet"))

# 3. Take the tuned preset, run, and read the results
config = get_preset(
    synthesis_pipeline=pipeline,
    evaluator_config=evaluator,
    mode="minimize",        # docking scores: lower is better
    num_iterations=20,      # cycles; 1000+ for a real screen
    batch_size=50,          # compounds per cycle
)
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").head(5))

results is a Polars DataFrame of every product scored — score, SMILES, Name. From here, 1. Library is where you swap in your own reagents and reaction, and 2. Scoring is where you swap in your own scorer.

Install extras#

pip install "chem-tactics[viz]"        # matplotlib + altair: Block 6
pip install "chem-tactics[tutorials]"  # marimo + viz
pip install "chem-tactics[openeye]"    # ROCS / FRED evaluators (licence required)
pip install -e ".[test,docs]"          # development

Requires Python 3.11+. Core dependencies are RDKit, NumPy, SciPy, Polars, Pydantic, tqdm, sqlitedict and joblib; everything else is optional and imported lazily.