Predict Which Mutations Help and Which Hurt

Generate all single-residue variants of your lead peptide, predict stability and solubility across all 324, and map the mutational landscape.




What you'll learn:

  • Generating a complete single-point mutant library programmatically
  • Running multi-model predictions with parallel stages
  • Visualizing a ΔTm mutational landscape heatmap
  • Multi-model consensus ranking

export BIOLMAI_TOKEN=your-token-here

Setup

import os
from biolmai.pipeline import (
    DataPipeline, DuckDBDataStore,
    ThresholdFilter, RankingFilter,
    ValidAminoAcidFilter, EmbeddingSpec,
    DiversitySamplingFilter,
)

TOKEN = os.environ.get("BIOLMAI_TOKEN", "")
if not TOKEN:
    raise EnvironmentError(
        "Set BIOLMAI_TOKEN before running.\n"
        "Get one at https://biolm.ai/ui/accounts/user-api-tokens/"
    )
import numpy as np
import matplotlib.pyplot as plt

Generate the single-point mutant library

WILD_TYPE = "MKTAYIAKQRQISFVKSHFSRQLEER"
CDR3_START, CDR3_END = 5, 22   # 17-residue target region
AMINO_ACIDS = "ACDEFGHIKLMNPQRSTVWY"

variants = [WILD_TYPE]  # include wild type as reference
for pos in range(CDR3_START, CDR3_END):
    for aa in AMINO_ACIDS:
        if aa != WILD_TYPE[pos]:
            mutant = WILD_TYPE[:pos] + aa + WILD_TYPE[pos+1:]
            variants.append(mutant)

print(f"{len(variants)} sequences ({len(variants)-1} single-point variants + wild type)")
324 sequences (323 single-point variants + wild type)

Score with multiple models

Both prediction stages run in parallel. 324 sequences × 2 models, all cached in DuckDB.

from biolmai.pipeline import DataPipeline, ValidAminoAcidFilter, ThresholdFilter, RankingFilter

pipeline = DataPipeline(sequences=variants, verbose=True)

pipeline.add_filter(ValidAminoAcidFilter(), stage_name="validate")

pipeline.add_prediction(
    "temberture-regression", extractions="prediction",
    columns="melting_temperature", stage_name="tm",
    depends_on=["validate"],
)
pipeline.add_prediction(
    "biolmsol", extractions="solubility_score",
    columns="solubility", stage_name="sol",
    depends_on=["validate"],
)

# Keep variants that beat wild-type Tm floor, then top 20 by solubility
pipeline.add_filter(ThresholdFilter("melting_temperature", min_value=48.0))
pipeline.add_filter(RankingFilter("solubility", n=20, ascending=False))

pipeline.run()
pipeline.summary()
Added stage: FilterStage('validate')
Added stage: PredictionStage('tm', depends_on=['validate'])
Added stage: PredictionStage('sol', depends_on=['validate'])
Added stage: FilterStage('filter_3', depends_on=['sol'])
Added stage: FilterStage('filter_4', depends_on=['filter_3'])

############################################################
# Pipeline: DataPipeline
# Run ID: 20260617_141635_3011496a
# Initial sequences: 324
# Streaming: ENABLED
############################################################

Execution plan: 4 level(s)
  Level 1: validate
  Level 2: tm, sol (parallel)
  Level 3: filter_3
  Level 4: filter_4

============================================================
Stage: validate
Input: 324 sequences
  Applying filter: ValidAminoAcidFilter(alphabet='ACDEFGHIKLMNPQRSTVWY')
  Filtered out: 0/324
  Remaining: 324

StageResult(validate: in=324, out=324, cached=0, computed=0, filtered=0, time=0.0s)
============================================================

Executing 2 stages in parallel...

============================================================
Stage: tm
Input: 324 sequences
Depends on: validate
  Cached: 0/324
  To compute: 324
  Calling temberture-regression.predict...
============================================================
Stage: sol
Input: 324 sequences
Depends on: validate
  Cached: 0/324
  To compute: 324
  Calling biolmsol.predict...
Completed: 324 sequences in 11 batches (max 5 concurrent)

StageResult(tm: in=324, out=324, cached=0, computed=324, filtered=0, time=93.0s)
============================================================
Completed: 324 sequences in 11 batches (max 5 concurrent)

StageResult(sol: in=324, out=324, cached=0, computed=324, filtered=0, time=96.4s)
============================================================

============================================================
Stage: filter_3
Input: 324 sequences
Depends on: sol
  Applying filter: ThresholdFilter(column='melting_temperature', min=48.0)
  Filtered out: 78/324
  Remaining: 246

StageResult(filter_3: in=324, out=246, cached=0, computed=0, filtered=78, time=0.0s)
============================================================

============================================================
Stage: filter_4
Input: 246 sequences
Depends on: filter_3
  Applying filter: RankingFilter(column='solubility', n=20, method='top')
  Filtered out: 226/246
  Remaining: 20

StageResult(filter_4: in=246, out=20, cached=0, computed=0, filtered=226, time=0.0s)
============================================================

############################################################
# Pipeline completed in 96.6s
# Final sequences: 20
############################################################
Stage Input Output Filtered Cached Computed Time (s)
0 validate 324 324 0 0 0 0.0
1 tm 324 324 0 0 324 93.0
2 sol 324 324 0 0 324 96.4
3 filter_3 324 246 78 0 0 0.0
4 filter_4 246 20 226 0 0 0.0

Compute ΔTm relative to wild type

df = pipeline.query("""
    SELECT s.sequence,
           MAX(CASE WHEN p.prediction_type = 'melting_temperature' THEN p.value END) AS tm,
           MAX(CASE WHEN p.prediction_type = 'solubility' THEN p.value END) AS solubility
    FROM sequences s
    JOIN predictions p ON s.sequence_id = p.sequence_id
    GROUP BY s.sequence
""")

wt_rows = df[df["sequence"] == WILD_TYPE]["tm"]
if len(wt_rows) == 0:
    raise RuntimeError(
        "Wild-type sequence has no Tm prediction — check API connectivity and BIOLMAI_TOKEN."
    )
wt_tm = wt_rows.iloc[0]
df["delta_tm"] = df["tm"] - wt_tm
print(f"Wild-type Tm: {wt_tm:.1f}°C")
df.sort_values("delta_tm", ascending=False).head(10)
Wild-type Tm: 51.1°C
sequence tm solubility delta_tm
195 MKTAYIAKQRQISFVKSHFSRWLEER 61.396854 0.657912 10.273907
69 MKTAYIAKQRQESFVKSHFSRQLEER 57.286594 0.943208 6.163647
6 MKTAYIAKQRQISFVKSEFSRQLEER 57.137478 0.862783 6.014530
43 MKTAYIAKQRQISFVKSAFSRQLEER 56.999977 0.860618 5.877029
312 MKTAYIAKQRQISFVKSHRSRQLEER 54.931290 1.243444 3.808342
143 MKTAYRAKQRQISFVKSHFSRQLEER 54.901398 1.193492 3.778450
205 MKTAYKAKQRQISFVKSHFSRQLEER 54.590424 1.213113 3.467476
253 MKTAYIAKQRQISFVKSHLSRQLEER 54.496346 0.878563 3.373398
63 MKTAYEAKQRQISFVKSHFSRQLEER 54.472130 0.957137 3.349182
92 MKTAYIAKQRQISFVKSHFARQLEER 54.308273 0.873038 3.185326

Mutational landscape heatmap

Each cell shows the predicted ΔTm for substituting the column's wild-type residue with the row's amino acid. Red = stabilizing, blue = destabilizing.

positions = list(range(CDR3_START, CDR3_END))
aa_list = sorted(AMINO_ACIDS)
heatmap = np.full((len(aa_list), len(positions)), np.nan)

for i, aa in enumerate(aa_list):
    for j, pos in enumerate(positions):
        if aa == WILD_TYPE[pos]:
            continue
        mutant = WILD_TYPE[:pos] + aa + WILD_TYPE[pos+1:]
        row = df[df["sequence"] == mutant]
        if len(row) > 0:
            heatmap[i, j] = row["delta_tm"].iloc[0]

fig, ax = plt.subplots(figsize=(14, 6))
im = ax.imshow(heatmap, cmap="RdBu_r", aspect="auto", vmin=-10, vmax=10)
ax.set_xticks(range(len(positions)))
ax.set_xticklabels([WILD_TYPE[p] + str(p+1) for p in positions], rotation=90)
ax.set_yticks(range(len(aa_list)))
ax.set_yticklabels(aa_list)
plt.colorbar(im, ax=ax, label="ΔTm (°C) vs. wild type")
ax.set_title("Saturation mutagenesis: predicted ΔTm landscape")
ax.set_xlabel("Position (wild-type residue + number)")
ax.set_ylabel("Substitution amino acid")
plt.tight_layout()
plt.show()
No description has been provided for this image

Multi-model consensus

Normalize scores across models and rank by average — mutations that score well on independent predictors are more likely to validate.

import pandas as pd

df_filt = pipeline.get_final_data()
df_filt["tm_norm"] = (df_filt["melting_temperature"] - df_filt["melting_temperature"].mean()) / df_filt["melting_temperature"].std()
df_filt["sol_norm"] = (df_filt["solubility"] - df_filt["solubility"].mean()) / df_filt["solubility"].std()
df_filt["consensus"] = (df_filt["tm_norm"] + df_filt["sol_norm"]) / 2

print("Top 10 consensus candidates:")
df_filt.nlargest(10, "consensus")[["sequence", "melting_temperature", "solubility", "consensus"]]
Top 10 consensus candidates:
sequence melting_temperature solubility consensus
7 MKTAYIAKQRQISFVKSHRSRQLEER 54.931290 1.243444 1.486047
8 MKTAYKAKQRQISFVKSHFSRQLEER 54.590424 1.213113 1.177959
18 MKTAYRAKQRQISFVKSHFSRQLEER 54.901398 1.193492 1.110784
5 MKTAYIAKQRQISFVKSHKSRQLEER 51.282913 1.262610 0.720199
0 MKTAYIAKQRQISKVKSHFSRQLEER 50.021278 1.278279 0.521941
17 MKTAYIAKQRQISFVKSRFSRQLEER 53.886021 1.136280 0.437070
2 MKTAYIAKQRQISRVKSHFSRQLEER 49.715118 1.259116 0.304719
3 MKTAYIAKQRQISFVKSHFKRQLEER 51.815460 1.173266 0.194686
13 MKTAYIAKQRQKSFVKSHFSRQLEER 49.672829 1.229878 0.078906
4 MKTAYIAKQRQISFVKSHFRRQLEER 51.908463 1.154100 0.076674

Next Steps

Check out additional tutorials at jupyter.biolm.ai, or head over to our BioLM Documentation to explore additional models and functionality.

See more use-cases and APIs on your BioLM Console Catalog.


BioLM hosts deep learning models and runs inference at scale. You do the science.

Contact us to learn more.

Accelerate yourLead generation

BioLM offers tailored AI solutions to meet your experimental needs. We deliver top-tier results with our model-agnostic approach, powered by our highly scalable and real-time GPU-backed APIs and years of experience in biological data modeling, all at a competitive price.

CTA

We speak the language of bio-AI

© 2022 - 2026 BioLM. All Rights Reserved.

Scan All Single-Point Variants for Thermal Stability and Solubility | BioLM