avGFP Fluorescence Optimisation with ALSEBO
This tutorial walks through a complete ALSEBO run end-to-end: from a FASTA file of VAE-generated sequences to a recommended set of high-fluorescence avGFP variants, using only ~65 simulated experiments.
Biological context
avGFP (Aequorea victoria Green Fluorescent Protein) is one of the most widely used reporter proteins in cell biology. Engineering brighter variants is a classic protein engineering benchmark.
We use the deep mutational scanning dataset from:
Sarkisyan et al. (2016). Local fitness landscape of the green fluorescent protein. Nature, 533, 397–401. https://doi.org/10.1038/nature17995
A pre-trained Support Vector Regression (SVR) model (best_svr_model.pkl)
trained on this dataset acts as our fitness oracle — standing in for wet-lab
fluorescence measurements.
Files in this tutorial
File |
Description |
|---|---|
|
VAE-generated avGFP variant sequences (candidate pool) |
|
Multiple sequence alignment used for DCA featurisation |
|
Pre-trained SVR fitness oracle |
Pipeline
generated_seqs.fasta + avgfp_jhmmer.fasta
│
▼
Step 1: Featurise sequence space (DCA → seq_space.csv)
│
▼
Step 2: Explore fitness landscape (t-SNE + SVR oracle)
│
▼
Step 3: Sample initial training set (k-means diversity, n=15)
│
▼
Step 4: Simulate experiments (SVR oracle)
│
▼
Step 5: Bayesian Optimisation loop (GPR + UCB, 10 rounds × 5 sequences)
│
▼
Step 6: Analyse results (convergence + trajectory)
Setup
import warnings
warnings.filterwarnings("ignore")
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import joblib
from sklearn.manifold import TSNE
from alsebo.seq_space import generate_seq_space
from alsebo.optimizer import (
read_seq_files,
gpr,
seq_space_prediction,
get_next_seq_bo,
save_next_batch_results,
)
from alsebo.training_space import (
sample_initial_training_sequnces,
generate_sequence_training_file,
)
plt.rcParams.update({
"font.size": 11,
"axes.linewidth": 1.2,
"axes.labelweight": "bold",
"figure.dpi": 120,
})
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[1], line 11
7 import matplotlib.pyplot as plt
8 import joblib
9 from sklearn.manifold import TSNE
10
---> 11 from alsebo.seq_space import generate_seq_space
12 from alsebo.optimizer import (
13 read_seq_files,
14 gpr,
ModuleNotFoundError: No module named 'alsebo'
# ── Configuration ──────────────────────────────────────────────────────────
EXP_DIR = "./" # directory containing all data files
MSA_FILE = "avgfp_jhmmer.fasta" # MSA for DCA featurisation
GEN_SEQS = "generated_seqs.fasta" # VAE-generated candidate sequences
ORACLE_PATH = "best_svr_model.pkl"
INIT_BATCH = 15 # sequences in the initial training set
BO_ROUNDS = 10 # number of Bayesian optimisation rounds
BATCH_SIZE = 5 # sequences recommended per round
BETA = 2.0 # UCB exploration-exploitation trade-off
obj_config = {
"names": ["fitness"],
"directions": ["max"],
}
# ───────────────────────────────────────────────────────────────────────────
# Remove leftovers from a previous run so we start clean
for fname in ["seq_exp_data.csv", "training_seqs.csv", "seq_space.csv"]:
fpath = os.path.join(EXP_DIR, fname)
if os.path.exists(fpath):
os.remove(fpath)
oracle = joblib.load(ORACLE_PATH)
print("Oracle loaded. Ready to start.")
Step 1 — Featurise the Sequence Space
We first convert each candidate sequence in generated_seqs.fasta into a
numerical feature vector using Direct Coupling Analysis (DCA).
DCA fits a maximum-entropy statistical model to the MSA and computes per-position features that capture both single-site preferences and residue–residue co-evolutionary couplings.
The result is written to seq_space.csv — one row per sequence.
Note: This step fits a DCA model on the MSA and is the slowest part of the pipeline (~1–3 min depending on MSA depth). It only needs to run once.
generate_seq_space(
exp_dir=EXP_DIR,
msa_fname=MSA_FILE,
gen_seq_fasta_fname=GEN_SEQS,
featuarization_method="DCA",
)
seq_df = pd.read_csv(f"{EXP_DIR}seq_space.csv")
print(f"Sequence space: {len(seq_df):,} sequences × {seq_df.shape[1]-1} DCA features")
Step 2 — Explore the Fitness Landscape
We use the SVR oracle to predict the fluorescence fitness of every sequence in the candidate pool, then project the high-dimensional DCA feature space down to 2-D with t-SNE to visualise the landscape.
features = seq_df.drop("seq_id", axis=1)
all_seqs = seq_df["seq_id"].tolist()
fitness_all = oracle.predict(features.values)
print("Running t-SNE projection...")
tsne_2d = TSNE(n_components=2, random_state=42).fit_transform(features)
fig, ax = plt.subplots(figsize=(7, 5))
sc = ax.scatter(tsne_2d[:, 0], tsne_2d[:, 1],
c=fitness_all, cmap="magma", alpha=0.75, s=8)
plt.colorbar(sc, ax=ax, label="Predicted fluorescence fitness")
ax.set_xlabel("t-SNE 1")
ax.set_ylabel("t-SNE 2")
ax.set_title("avGFP Sequence Space — Predicted Fitness Landscape")
plt.tight_layout()
plt.show()
print(f"Fitness range : {fitness_all.min():.2f} → {fitness_all.max():.2f}")
Step 3 — Sample a Diverse Initial Training Set
Before running BO we need a small starting set of measured sequences. A random selection risks clustering in one region of the landscape.
ALSEBO avoids this with a t-SNE + k-means strategy:
Project feature space to 2-D
Partition into k =
INIT_BATCHclustersPick the sequence closest to each centroid
This ensures the 15 initial sequences span the full landscape.
sample_initial_training_sequnces(EXP_DIR, training_seq_size=INIT_BATCH, manipold="TSNE")
training_df = pd.read_csv(f"{EXP_DIR}training_seqs.csv")
train_idx = [all_seqs.index(s) for s in training_df["seq_id"] if s in all_seqs]
fig, ax = plt.subplots(figsize=(7, 5))
sc = ax.scatter(tsne_2d[:, 0], tsne_2d[:, 1],
c=fitness_all, cmap="magma", alpha=0.45, s=8, label="All sequences")
ax.scatter(tsne_2d[train_idx, 0], tsne_2d[train_idx, 1],
c="cyan", edgecolors="black", s=70, zorder=5,
label=f"Initial batch (n={INIT_BATCH})")
plt.colorbar(sc, ax=ax, label="Predicted fluorescence fitness")
ax.set_xlabel("t-SNE 1")
ax.set_ylabel("t-SNE 2")
ax.set_title("Diverse Initial Training Set")
ax.legend(frameon=False)
plt.tight_layout()
plt.show()
Step 4 — Simulate Initial Experiments
In a real campaign you would now synthesise and measure the 15 selected
sequences in the lab. Here the SVR oracle simulates those measurements
instantly. The results are written to seq_exp_data.csv — the training
log that ALSEBO appends to after every BO round.
train_features = training_df.drop("seq_id", axis=1)
init_fitness = oracle.predict(train_features.values)
# generate_sequence_training_file expects list[list[float]]
obj_values = [[float(v)] for v in init_fitness]
generate_sequence_training_file(EXP_DIR, obj_config, obj_values)
print("Initial training data written to seq_exp_data.csv")
print(f" Sequences measured : {INIT_BATCH}")
print(f" Mean fitness : {np.mean(init_fitness):.3f}")
print(f" Best fitness : {np.max(init_fitness):.3f}")
Step 5 — Bayesian Optimisation Loop
Each round ALSEBO:
Fits a Gaussian Process surrogate on all sequences measured so far
Predicts posterior mean and uncertainty across every untested candidate
Scores each candidate with UCB:
score = mean + β × stdSelects the top-5 and evaluates them with the oracle
Appends results and repeats
After 10 rounds we will have evaluated 15 + 10 × 5 = 65 sequences — just ~3% of the candidate pool.
history = [] # stores (round, mean_fitness, max_fitness)
for round_idx in range(BO_ROUNDS):
# Load current training data + remaining candidates
x_train, y_train, x_space, seq_ids_remaining = read_seq_files(EXP_DIR, obj_config)
# Fit GPR surrogate — one model per objective
models = gpr(x_train, y_train)
# Predict mean + uncertainty over the full candidate space
preds = seq_space_prediction(models, x_space)
# UCB acquisition → select top-k candidates
next_seqs, scores, idx = get_next_seq_bo(
seq_ids_remaining, obj_config, preds,
top_k=BATCH_SIZE, strategy="UCB", beta=BETA,
)
# Oracle evaluation (simulates wet-lab measurement)
new_fitness = oracle.predict(x_space.iloc[idx].values)
# Append results to seq_exp_data.csv
save_next_batch_results(
EXP_DIR, next_seqs, idx, x_space, obj_config,
obj_values=new_fitness.tolist(),
)
history.append({"round": round_idx + 1,
"mean": float(np.mean(new_fitness)),
"max": float(np.max(new_fitness))})
best_so_far = max(h["max"] for h in history)
print(f"Round {round_idx+1:2d} | batch max = {np.max(new_fitness):.3f} "
f"| best so far = {best_so_far:.3f}")
print("\nOptimisation complete.")
Step 6 — Results
Convergence
Mean and best fitness per round, compared against the oracle maximum.
exp_df = pd.read_csv(f"{EXP_DIR}seq_exp_data.csv")
rounds_labels = ["Init"] + [f"R{h['round']}" for h in history]
mean_by_round = [exp_df["fitness"][:INIT_BATCH].mean()] + [h["mean"] for h in history]
max_by_round = [exp_df["fitness"][:INIT_BATCH].max()] + [h["max"] for h in history]
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(rounds_labels, mean_by_round, "-o", label="Mean fitness", lw=2)
ax.plot(rounds_labels, max_by_round, "-o", label="Best fitness", lw=2)
ax.axhline(fitness_all.max(), color="grey", linestyle="--",
label=f"Oracle max ({fitness_all.max():.2f})")
ax.set_xlabel("Optimisation round")
ax.set_ylabel("Predicted fluorescence fitness")
ax.set_title("ALSEBO Convergence on avGFP")
ax.legend(frameon=False)
plt.tight_layout()
plt.show()
Optimisation Trajectory
Best sequence per round overlaid on the fitness landscape.
exp_seqs = exp_df["seq_id"].tolist()
opt_tsne = []
for i, h in enumerate(history):
start = INIT_BATCH + i * BATCH_SIZE
batch_fit = exp_df["fitness"].iloc[start : start + BATCH_SIZE].values
best_seq = exp_seqs[start + int(np.argmax(batch_fit))]
if best_seq in all_seqs:
opt_tsne.append(tsne_2d[all_seqs.index(best_seq)])
opt_tsne = np.array(opt_tsne)
fig, ax = plt.subplots(figsize=(7, 5))
sc = ax.scatter(tsne_2d[:, 0], tsne_2d[:, 1],
c=fitness_all, cmap="magma", alpha=0.45, s=8)
ax.scatter(tsne_2d[train_idx, 0], tsne_2d[train_idx, 1],
c="cyan", edgecolors="black", s=50, zorder=4, label="Initial training")
if len(opt_tsne) > 1:
ax.plot(opt_tsne[:, 0], opt_tsne[:, 1], "w-", lw=2, zorder=5)
if len(opt_tsne) > 0:
ax.scatter(opt_tsne[:, 0], opt_tsne[:, 1],
c="white", edgecolors="black", s=50, zorder=6, label="Best per round")
ax.scatter(opt_tsne[-1, 0], opt_tsne[-1, 1],
c="red", edgecolors="black", s=90, zorder=7, label="Final best")
plt.colorbar(sc, ax=ax, label="Predicted fluorescence fitness")
ax.set_xlabel("t-SNE 1")
ax.set_ylabel("t-SNE 2")
ax.set_title("Optimisation Trajectory on the Fitness Landscape")
ax.legend(frameon=False, fontsize=9)
plt.tight_layout()
plt.show()
Summary
Initial training set |
15 sequences (k-means diversity sampling) |
BO rounds |
10 |
Batch size per round |
5 |
Total sequences evaluated |
65 out of the full candidate pool (~3%) |
ALSEBO navigated the avGFP fitness landscape using only a small fraction of the candidate pool, guided by the GPR surrogate and UCB acquisition function.
Things to try
manipold="PCA"— swap t-SNE for PCA in the initial sampling step (faster, linear)beta=0.5— more exploitative; converges faster but may miss the global optimumbeta=5.0— more exploratory; better for rugged or multi-modal landscapesESM features — set
featuarization_method="ESM"ingenerate_seq_space()for protein language model embeddingsMulti-objective — add a second objective (e.g. thermostability) to
obj_configand provide two values per sequence inobj_values