DEL technology uses split-and-pool combinatorial synthesis where each compound is covalently tagged with a unique DNA barcode encoding the building blocks used. Libraries routinely contain 108–1012 compounds. After affinity selection against an immobilised target, surviving compounds are identified by Illumina sequencing of the DNA tags. The readout is compound counts, not activities — statistical enrichment over negative control determines hits.
The key ML opportunity: train a model on DEL count data to predict activity for "off-DNA" compounds (the DNA tag is removed before testing). This vastly expands the actionable hit space beyond the subset of DEL compounds that can be practically resynthesised.
DEL Data Processing and Enrichment Scoring
import numpy as np
import pandas as pd
from scipy import stats
def compute_enrichment_score(
selection_counts: np.ndarray,
control_counts: np.ndarray,
pseudocount: float = 0.5,
normalise_to_million: bool = True,
) -> np.ndarray:
"""
Compute enrichment score for each compound in a DEL selection.
Enrichment = log2((CPM_selection + ε) / (CPM_control + ε))
Args:
selection_counts: Raw read counts per compound in target selection
control_counts: Raw read counts per compound in no-target control
pseudocount: Added before division to handle zero counts
normalise_to_million: Whether to normalise to counts-per-million (CPM)
Returns:
Log2 enrichment scores for each compound.
"""
sel = selection_counts.astype(float)
ctrl = control_counts.astype(float)
if normalise_to_million:
sel = sel / sel.sum() * 1e6
ctrl = ctrl / ctrl.sum() * 1e6
enrichment = np.log2((sel + pseudocount) / (ctrl + pseudocount))
return enrichment
def flag_del_hits(
enrichment_scores: np.ndarray,
compound_ids: list[str],
zscore_threshold: float = 3.0,
min_selection_count: int = 5,
selection_counts: np.ndarray | None = None,
) -> pd.DataFrame:
"""
Identify statistically enriched DEL hits using Z-score thresholding.
Args:
enrichment_scores: log2 enrichment scores
compound_ids: Compound identifiers (barcode decoded)
zscore_threshold: Number of SDs above mean for hit calling
min_selection_count: Minimum raw selection reads to be considered
selection_counts: Raw counts for minimum count filter
Returns:
DataFrame of hit compounds with enrichment and Z-score.
"""
mean_e = enrichment_scores.mean()
std_e = enrichment_scores.std()
zscores = (enrichment_scores - mean_e) / (std_e + 1e-9)
df = pd.DataFrame({
"compound_id": compound_ids,
"enrichment_log2": enrichment_scores,
"zscore": zscores,
})
if selection_counts is not None:
df["selection_count"] = selection_counts
df = df[df["selection_count"] >= min_selection_count]
hits = df[df["zscore"] >= zscore_threshold].sort_values("enrichment_log2", ascending=False)
return hits.reset_index(drop=True)
Synthon-Level SAR Analysis
def synthon_sar_table(
del_data: pd.DataFrame,
enrichment_col: str = "enrichment_log2",
synthon_cols: list[str] = None,
min_observations: int = 5,
) -> dict[str, pd.DataFrame]:
"""
Compute per-synthon mean enrichment to identify productive building blocks.
In a tri-synthon library (3 cycles of split-and-pool), each compound is
A–linker–B–linker–C. Averaging enrichment over all compounds containing
synthon A_i reveals its individual contribution.
Args:
del_data: DataFrame with enrichment scores and synthon ID columns
enrichment_col: Column name for enrichment values
synthon_cols: List of column names for each synthon cycle (e.g. ['bb1', 'bb2', 'bb3'])
min_observations: Minimum compound count per synthon to report
Returns:
Dict mapping synthon_col → ranked DataFrame of synthon SAR.
"""
if synthon_cols is None:
synthon_cols = [c for c in del_data.columns if c.startswith("bb") or c.startswith("synthon")]
results = {}
for col in synthon_cols:
synthon_summary = (
del_data.groupby(col)[enrichment_col]
.agg(
mean_enrichment="mean",
median_enrichment="median",
n_compounds="count",
max_enrichment="max",
)
.reset_index()
.rename(columns={col: "synthon_id"})
.query(f"n_compounds >= {min_observations}")
.sort_values("mean_enrichment", ascending=False)
)
results[col] = synthon_summary
return results
Off-DNA Activity Prediction
import numpy as np
from rdkit import Chem
from rdkit.Chem import AllChem
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
def build_del_activity_model(
smiles_list: list[str],
enrichment_scores: list[float],
binary_threshold: float = 3.0,
task: str = "regression",
) -> dict:
"""
Train a model on DEL enrichment data for off-DNA activity prediction.
Critical consideration: DEL data is noisy count data, not IC50.
Binarise at a Z-score threshold for classification, or use log2
enrichment directly for regression.
Args:
smiles_list: SMILES for each DEL compound (synthon-reconstructed)
enrichment_scores: log2 enrichment scores
binary_threshold: Z-score/enrichment cutoff for positives (regression only)
task: 'regression' (predict enrichment) or 'classification' (hit/no-hit)
Returns:
Dict with model and validation metrics.
"""
fps, valid_scores = [], []
for smi, score in zip(smiles_list, enrichment_scores):
mol = Chem.MolFromSmiles(smi)
if mol is not None:
fp = list(AllChem.GetMorganFingerprintAsBitVect(mol, 2, 2048))
fps.append(fp)
valid_scores.append(score)
X = np.array(fps)
y = np.array(valid_scores)
if task == "classification":
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
y_binary = (y >= binary_threshold).astype(int)
model = Pipeline([
("clf", RandomForestClassifier(n_estimators=500, class_weight="balanced", random_state=42, n_jobs=-1))
])
cv_scores = cross_val_score(model, X, y_binary, cv=5, scoring="roc_auc", n_jobs=-1)
model.fit(X, y_binary)
return {
"model": model, "task": "classification",
"cv_roc_auc": round(cv_scores.mean(), 3),
"cv_roc_auc_std": round(cv_scores.std(), 3),
"positive_rate": round(y_binary.mean(), 3),
}
else:
model = Pipeline([
("scaler", StandardScaler()),
("gbr", GradientBoostingRegressor(n_estimators=400, max_depth=4, learning_rate=0.05, subsample=0.8, random_state=42)),
])
cv_scores = cross_val_score(model, X, y, cv=5, scoring="neg_mean_absolute_error", n_jobs=-1)
model.fit(X, y)
return {
"model": model, "task": "regression",
"cv_mae_log2_enrichment": round(-cv_scores.mean(), 3),
"description": "Predict log2 enrichment for off-DNA compounds",
}
DEL Technology Comparison
| Library Type | Size Range | Typical Cycles | Output | Key Limitation |
|---|---|---|---|---|
| 3-cycle split-and-pool DEL | 107–1010 | 3 (A×B×C) | Sequencing count matrix | Requires linker-compatible chemistry |
| Encoded self-assembling chemical (ESAC) | 108–1012 | 2 (pairing) | Fragment pair enrichment | Fragment pairs, not covalent compounds |
| YoctoReactor DEL (Vipergen) | 108–109 | 3 | Solution-phase selection | Complex implementation |
| DEL-ML (ML-augmented) | 1012+ | Virtual + DEL-trained model | Predicted enrichment scores | Model generalisation to novel scaffolds |
Key Takeaways
- DEL count data requires careful normalisation and Z-score-based hit calling — raw counts are dominated by sequencing depth and PCR amplification bias. Always run a no-target control selection in parallel.
- Synthon SAR analysis is the highest-value first step after hit deconvolution. Identifying productive building blocks at each cycle position guides focused resynthesis of "off-DNA" analogues with optimal synthon combinations.
- Off-DNA prediction models (trained on DEL enrichment data) have lower accuracy than conventional QSAR because enrichment is a noisy proxy for potency, not a direct thermodynamic measurement. Use them for prioritisation, not selectivity profiling.
- The major challenge in DEL ML is generalisation: structure-activity models trained on a single DEL chemical space may not extrapolate reliably to the resynthesised off-DNA analogues that differ structurally from library members.
Try it live
Explore a related model hands-on: open Molecule Screening Engine — Similarity Search Live and change its parameters while it runs. Everything stays in your browser.
▶ Open Molecule Screening Engine — Similarity Search Live simulation