Changelog#
All notable changes to this project are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[2.0.0] - 2026-09-08#
A streamlining release. The package is the same algorithm with the dead
weight removed: import TACTICS drops from 7.1 s to 0.04 s, six runtime
dependencies are gone (about 200 MB off a fresh install), the wheel shrinks
from 13.5 MB to under 5 MB, and roughly 3,700 lines of unreachable or
duplicated code leave src/. Search behaviour of every preset is unchanged;
the one default that moves is documented under Changed.
Removed#
thompson_sampling.legacy— the pre-1.0 Thompson Sampling and RWS implementations (17 modules). Nothing in the package or tests imported them. To reproduce Zhao et al. (2025) with that code, installchem-tactics==1.2.0.legacy_rwspreset. Same reason. The Boltzmann-weighted posterior update it used is unchanged and remains the update rule ofrecommendedandrecommended_rws.StandardWarmup/StandardWarmupConfig— random-partner warmup, the weakest of the three and kept only as a comparison arm.baseline.py(run_random_baseline,run_exhaustive_baseline,RandomBaselineConfig) — unreachable: it read config fields that did not exist and passed a keywordcreate_reagentsnever accepted.Inert config fields that were stored but never read:
RouletteWheelConfig.{exploration_phase_end, transition_phase_end, min_observations, cats_exploration_fraction},BayesUCBConfig.{exploration_phase_end, transition_phase_end},TopTwoConfig.min_observations, andThompsonSamplingConfig.max_resamples(its early-stop branch compared against a counter that was never incremented).library_analysis.LibraryAnalysis,LibraryVisualization,compile_product_scores,compile_product_smilesand eightdiagnostic_plotsfunctions with no callers.TS_Benchmarksand the eight plot functions used by the tutorials are unchanged.library_enumeration.conformer_gen(never imported; unguarded OpenEye import),LibraryEnumerator,initializer, and a handful of unreferenced helpers.Dependencies:
pandas,dill,useful_rdkit_utils,seaborn.matplotlibandaltairmove to the new optional[viz]extra.ThompsonSamplingConfig.results_filenameand theThompsonSampler(cats_manager=...)parameter — both were stored and never read.search()returns a DataFrame; write it withresults.write_parquet(). Presetoutput_dirnow only places the run log.
Changed#
Direct
ThompsonSampler(...)construction with nowarmup_strategynow defaults toEnhancedWarmup()instead ofStandardWarmup(). This matches whatfrom_config()and every preset already did, so preset users see no change.Polars only.
LookupEvaluatorreads its table with Polars; the SMARTS validator reads CSV reagent files with Polars. No public API accepted or returned pandas objects, so signatures are unchanged.Bundled thrombin scores ship as Parquet (
product_scores.parquet, 4.7 MB) instead of a 12 MB CSV.Strategy and warmup config models reject unknown fields (
extra="forbid"), so scripts still passing a removed knob fail with aValidationErrorinstead of silently ignoring it.get_diagnostics()on a strategy that records no component state returns an empty frame with columnscurrent_cycle,component_idx,criticality— the three columns every strategy-specific schema shares. The old 3-columncycle-named fallback schema was reachable by no strategy.RouletteWheelSelection,TopTwoSelectionandBayesUCBSelectionshare their thermal-cycling and GMIC code through mixins instrategies/_thermal.py. Verified RNG-identical on seeded runs; the only visible change is thatTopTwoSelection._component_gmicis now_cached_gmics, the name RWS already used.RouletteWheelSelection.select_batchandBayesUCBSelection.select_batchare removed; both fall through to theSelectionStrategydefault, which the sampler never called anyway.RouletteWheelSelection(...)andBayesUCBSelection(...)raiseTypeErroron unknown keyword arguments instead of silently ignoring them (Bayes-UCB still warns for its five deprecated names). PreviouslyRouletteWheelSelection(criticality_metric="shannon")was accepted with no effect.The
TACTICSandTACTICS.thompson_samplingnamespaces now also exportEvaluator,CustomEvaluator, and every strategy, warmup and evaluator config model, so one import style covers a whole script.
Performance#
Lazy package re-exports.
TACTICS,TACTICS.thompson_sampling,TACTICS.thompson_sampling.coreandTACTICS.library_analysisresolve their names on first access (PEP 562). Every existing import path keeps working; a config-only import no longer loads RDKit, scipy or sqlitedict.scipy.stats(BayesUCB) andmatplotlib.pyplot(diagnostic plots) are imported inside the function that needs them.useful_rdkit_utils— 3.85 s of the old import and ~149 MB via umap/pynndescent/numba/llvmlite — is replaced by direct RDKit calls that are bit-identical.tests/test_import_time.pypins these guarantees in fresh subprocesses.
Fixed#
diagnostic_plotsusedplt.cm.get_cmap, removed in matplotlib 3.9.tutorials/thompson_sampling_tutorial.pyimported aBoltzmannConfigthat never existed and could not be opened.Three tests skipped as “warmup edge case with small test data” pass with the Enhanced default and are un-skipped.
ParallelEvaluatornow starts its worker pool with thespawnmethod. Under Linux’s defaultfork, a worker rebuilding aLookupEvaluatordeadlocked in Polars (the child inherits a Rayon thread pool with no threads), which hungprocesses > 1runs and the CI test job.
[1.2.0] - 2026-07-18#
Parallel evaluation with slow evaluators (Fred docking, ROCS, ML models) was
non-functional in 1.1.0. Three defects, reported by Donald van Pinxteren on
2026-06-23, combined to make processes > 1 unusable; all three are fixed.
Fixed#
Evaluation parallelism could not be enabled at all.
ThompsonSampler.from_config()hardcodedprocesses=1, so settingprocessesanywhere in a user script had no effect and there was no API-level way to turn on parallel evaluation. A 1000-iteration Fred docking screen ran single-threaded on a 128-core allocation with no error or warning.ThompsonSamplingConfignow exposesprocesses(andmin_cpds_per_core), andfrom_config()passes them through.processes > 1always crashed with OpenEye evaluators.ParallelEvaluatorcalledpool.map(sampler.evaluate, ...), which pickles the bound method and therefore the sampler, the evaluator, and any SWIG-wrapped C++ object it holds.OEDock(Fred) and the ROCS shape engine raiseTypeError: cannot pickle 'SwigPyObject' object, so the run died on the first batch before any docking happened.Workers now build their own evaluator. The pool is created with an
initializerthat constructs the evaluator inside each worker from its picklable Pydantic config, once per worker rather than once per molecule. The OpenEye object is never pickled and never crosses the pipe. This works under bothforkandspawn, so it is correct on Linux, macOS and Windows rather than only whereforkis the default.Verified end to end with a real
OEDockunderspawn: the evaluator is confirmed unpicklable, and all products still dock correctly across workers.Segfault on
import TACTICSwhen OpenEye and prompt_toolkit are both installed.evaluators.pyimported the OpenEye toolkits at module level, which initialises the global C-levellibexpatparser.tqdmpulls inprompt_toolkit, whose progress-bar formatter callsxml.dom.minidom.parseString()at module level and re-enterslibexpatthroughpyexpat, conflicting with OpenEye’s initialisation and killing the interpreter (exit 139) before user code ran. OpenEye is now imported lazily, on first construction of an OpenEye-backed evaluator.MLClassifierEvaluatorfailed when OpenEye was absent.joblibwas imported inside the OpenEyetry/except, leaving it undefined in environments without OpenEye despite being unrelated to it.exp()overflow in the CATS Boltzmann softmax. On heavy-tailed, zero-inflated score landscapes a single outlier combined with a small CATS temperature overflowedexp()toinf, producing NaN probabilities and aborting the search with “probabilities contain NaN”. The softmax now subtractsmax(z)before exponentiating, which is shift-invariant and leaves the resulting probabilities unchanged.
Added#
ThompsonSamplingConfig.processesand.min_cpds_per_core— evaluation parallelism, defaulting to1(unchanged behaviour).ThompsonSampler.set_evaluator(evaluator, evaluator_config=...)— supplies the picklable recipe workers use to rebuild the evaluator.from_config()provides it automatically; a warning is emitted ifprocesses > 1without it.LookupEvaluatorConfig.default_score— score for product codes absent from the lookup table. Defaults toNone(existing NaN behaviour); set to0.0for sparse libraries such as DEL read counts, where an unmeasured combination is a true non-binder rather than missing data.Layer-1 and layer-2 search diagnostics in
library_analysis.diagnostic_plots(plot_gmic_directed_exploration,plot_adaptive_intensity,plot_reagent_usage_action_panel,plot_gmic_vs_oracle).
Changed#
TT-TS GMIC min-observation gate removed.
TopTwoSelection._calculate_gmicreturned0.0whenever the least-observed active reagent fell belowmin_observations;RouletteWheelSelectionnever had this gate. On large components a single under-observed reagent pinned the whole component’s GMIC to zero, distorting the rotation. Removing it raises adenine TT-TS top-100 recovery from 87.1 to 93.2 and roughly halves replicate variance (sd 18.4 → 10.6).min_observationsis still accepted for backward compatibility but is now inert inTopTwoSelection.
Notes for users#
Set processes to the number of cores you have allocated when using a slow
evaluator; leave it at 1 for LookupEvaluator/DBEvaluator, where process
overhead exceeds lookup cost. On macOS and Windows the default start method is
spawn, so entry-point scripts must be guarded with
if __name__ == "__main__":. Under spawn, and on clusters generally, the
design unit (.oedu) must be readable from every node — automatic on shared
filesystems (NFS, GPFS).
[1.1.0] - earlier#
See git history for releases prior to this changelog.