Project 6 - Antibiotic Resistance Simulation
Drug Design with PyRosetta — Modelling Resistance Mutations in β-Lactamase
What You Will Learn
- How antibiotic resistance arises at the molecular level
- How to model clinically documented resistance mutations computationally
- How to distinguish mutations that confer resistance by weakening drug binding vs. stabilizing the enzyme
- How to compute a resistance profile and interpret it in a clinical context
- How computational drug design responds to resistance: the next-generation drug problem
- How a Python dictionary solves the “two numbering systems” problem that sits at the heart of this project
| Time: ~60–90 minutes | Prerequisite: Projects 1–5 completed | Difficulty: Advanced |
Project Workflow at a Glance
Before diving into code, here’s the full pipeline. Notice that Cell 4 is the hinge of the entire project — everything before it prepares the structure, and everything after it depends on the translation table Cell 4 builds.
flowchart TD
A["Cell 1<br/>Initialize PyRosetta"] --> B["Cell 2<br/>Download & Survey 1ZG4"]
B --> C["Cell 3<br/>Clean structure + compute<br/>Wild-Type binding energy"]
C --> D["Cell 4<br/>🔑 Build pdb_to_rosetta dictionary<br/>(PDB # ↔ Rosetta #)"]
D --> E["Cell 5<br/>Mutate each clinical position<br/>+ measure ΔΔG"]
E --> F["Cell 6<br/>Display ranked<br/>resistance table"]
F --> G["Cell 7<br/>Plot resistance profile<br/>with clinical zones"]
G --> H["Cell 8<br/>Next-generation drug<br/>design strategy"]
D -.->|"Without this dictionary,<br/>Cell 5 mutates the WRONG residues"| E
style D fill:#4c2c82,color:#ffffff !important,stroke:#2e1b52,stroke-width:2px
style E fill:#7456b3,color:#ffffff !important,stroke:#4c2c82,stroke-width:1px
| Stage | Cell | What happens | Depends on |
|---|---|---|---|
| Setup | 1 | Start PyRosetta with ligand support | — |
| Acquire | 2 | Download 1ZG4, identify the inhibitor | Cell 1 |
| Prepare | 3 | Strip to chain A + ligand, compute WT baseline | Cell 2 |
| Translate | 4 | Build the PDB ↔ Rosetta number dictionary | Cell 3 |
| Perturb | 5 | Mutate the correct residue, measure ΔΔG | Cell 4's dictionary |
| Summarize | 6 | Rank mutations, save CSV | Cell 5 |
| Visualize | 7 | Plot ΔΔG with clinical resistance zones | Cell 6 |
| Strategize | 8 | Propose next-gen drug design options | Cells 5–7 |
The dashed arrow above is the most important line in this diagram: every mutation result in Cell 5 is only correct because of the lookup performed in Cell 4. If that dictionary were skipped or built incorrectly, you could easily mutate residue 130 in Rosetta’s numbering while thinking you mutated S130G from the clinical literature — and get a completely meaningless answer that still looks plausible.
Background: The Antibiotic Resistance Crisis
Every year, antimicrobial-resistant infections kill over 1.2 million people directly and contribute to millions more deaths globally. Resistance is not a new phenomenon — bacteria have been evolving ways to neutralize antibiotics since before we invented them. What has changed is the pace and scale, driven by overuse of antibiotics in medicine and agriculture.
At the molecular level, resistance often follows a predictable pattern: a single point mutation in a bacterial enzyme changes one amino acid, and that one change is enough to make an antibiotic fail.
Analogy: Imagine your front door lock is an antibiotic, and the bacteria is the house. The bacteria does not need to build a completely new house to defeat your lock — it just needs to change the lock cylinder by one tiny notch. The locksmith (the drug) no longer fits. Everything else about the house is identical.
This project will teach you to model that “one tiny notch” computationally — quantifying exactly how much each resistance mutation changes the drug’s ability to bind.
The Protein We Will Study: TEM-1 β-Lactamase
β-Lactamases are bacterial enzymes that destroy β-lactam antibiotics (penicillins, cephalosporins, carbapenems — the most widely used class of antibiotics in the world). They do this by breaking the β-lactam ring that makes these antibiotics effective.
TEM-1 β-Lactamase (PDB: 1ZG4) is the most clinically prevalent β-lactamase. It confers resistance to many penicillins but can be inhibited by β-lactamase inhibitors like Clavulanic acid (the “clavulanate” in Augmentin).
We will model the binding of an inhibitor to TEM-1, then introduce known clinical resistance mutations and measure how each one weakens binding.
Clinically Documented Resistance Mutations
The following mutations have been identified in patients with antibiotic-resistant infections. Each has been validated experimentally to confer some degree of resistance.
| Mutation | Position | Notes |
|---|---|---|
| M69L | 69 | Methionine → Leucine. Early step in inhibitor resistance |
| K73R | 73 | Lysine → Arginine. Disrupts the active site catalytic mechanism |
| S130G | 130 | Serine → Glycine. Destroys a key hydrogen bond to inhibitors |
| R244S | 244 | Arginine → Serine. Removes a critical electrostatic interaction |
| N276D | 276 | Asparagine → Aspartate. Changes polarity near the binding site |
These are not hypothetical — they are observed in clinical isolates from hospital patients.
Every “Position” number in this table is a PDB number. Keep that in the back of your mind — it’s the whole reason Cell 4 exists.
Why This Is Different from Projects 1 and 5
| Project 1 | Project 5 | Project 6 |
|---|---|---|
| Mutate to Alanine | Mutate to Alanine | Mutate to the real clinical amino acid |
| Any residue | Binding site residues | Specific known resistance positions |
| Measure protein stability | Measure binding contribution | Measure resistance effect on drug |
| Teaching mutations | Computational design | Real-world clinical relevance |
In Projects 1 and 5, we always mutated to Alanine because it is a neutral probe. Here we mutate to the actual amino acid that bacteria have evolved — these are not hypothetical perturbations, they are real evolutionary solutions that bacteria have discovered to defeat our drugs.
Cell 1 — Initialize PyRosetta
Set up with ligand support
import pyrosetta
from pyrosetta import *
from pyrosetta.toolbox import *
pyrosetta.init(extra_options="-ignore_unrecognized_res false -load_PDB_components true -mute all")
print("PyRosetta ready!")
print("Target protein: TEM-1 β-Lactamase (PDB: 1ZG4)")
print("Drug: Clavulanic acid inhibitor complex")
Cell 2 — Load TEM-1 β-Lactamase
Download the enzyme and identify the inhibitor
import urllib.request
from pyrosetta import pose_from_pdb
import pandas as pd
# Download TEM-1 with bound inhibitor
url = "https://files.rcsb.org/download/1ZG4.pdb"
urllib.request.urlretrieve(url, "1ZG4.pdb")
pose_raw = pose_from_pdb("1ZG4.pdb")
print(f"Raw structure: {pose_raw.total_residue()} residues")
# Survey the non-protein molecules present
print("\nNon-protein residues:")
for i in range(1, pose_raw.total_residue() + 1):
res = pose_raw.residue(i)
chain = pose_raw.pdb_info().chain(i)
if not res.is_protein():
pdb_num = pose_raw.pdb_info().number(i)
print(f" Position {i:4d} | Chain {chain} | PDB# {pdb_num:4d} | {res.name()}")
Note: Inspect the output carefully. You will see a ligand residue (likely labelled with a three-letter code) and possibly water molecules. Identify the ligand code before proceeding to the next cell.
Key Concepts:
- Surveying the structure: Before automating anything, always manually inspect what a PDB structure contains. Crystal structures can contain unexpected molecules from crystallography buffers, multiple copies of the protein, or degradation products.
- A first hint of the numbering problem: notice the printout already shows two numbers side by side —
Position i(Rosetta) andPDB# pdb_num(PDB). They already disagree, even at this early stage.
Cell 3 — Clean the Structure and Compute Wild-Type Binding Energy
Isolate chain A + inhibitor and establish the baseline
scorefxn = pyrosetta.get_fa_scorefxn()
# Identify the ligand (adjust the name string if your output above shows something different)
# Common inhibitor codes for 1ZG4: look for a non-standard residue code in the output above
LIGAND_CODES = {"CLV", "CVN", "FAC", "AXC", "FRO"} # common β-lactamase inhibitor codes
ligand_pos_raw = None
keep_positions = []
for i in range(1, pose_raw.total_residue() + 1):
res = pose_raw.residue(i)
chain = pose_raw.pdb_info().chain(i)
rname = res.name().replace("pdb_", "").strip()[:3] # strip pdb_ prefix
if rname in LIGAND_CODES or (not res.is_protein() and chain == "A" and "HOH" not in res.name()):
if ligand_pos_raw is None:
ligand_pos_raw = i
print(f"Using ligand: {res.name()} at raw position {i}")
keep_positions.append(i)
elif chain == "A" and res.is_protein():
keep_positions.append(i)
# Build the clean pose
wt_pose = pose_raw.clone()
delete_positions = [i for i in range(1, pose_raw.total_residue() + 1)
if i not in keep_positions]
for i in reversed(delete_positions):
wt_pose.delete_residue_slow(i)
print(f"Clean structure: {wt_pose.total_residue()} residues")
# Find ligand in clean pose
ligand_clean = None
for i in range(1, wt_pose.total_residue() + 1):
if not wt_pose.residue(i).is_protein():
ligand_clean = i
print(f"Ligand in clean pose: position {ligand_clean}")
break
# Wild-type binding energy (reusing the function from Project 5)
def compute_binding_energy(pose, ligand_res, scorefxn):
score_complex = scorefxn(pose)
prot_pose = pose.clone()
prot_pose.delete_residue_slow(ligand_res)
score_protein = scorefxn(prot_pose)
lig_pose = pose.clone()
prot_positions = [i for i in range(1, pose.total_residue() + 1)
if pose.residue(i).is_protein()]
for i in reversed(prot_positions):
lig_pose.delete_residue_slow(i)
score_ligand = scorefxn(lig_pose)
return score_complex - (score_protein + score_ligand)
wt_binding = compute_binding_energy(wt_pose, ligand_clean, scorefxn)
print(f"\nWild-type binding energy: {wt_binding:.2f} REU")
print("(This is the inhibitor binding strength in the drug-sensitive strain)")
Key Concepts:
- Drug-sensitive strain: A bacterial strain that is still killed by the antibiotic — the “before resistance” state. Our wild-type structure represents this.
- Drug-resistant strain: A bacterial strain that survives antibiotic treatment. Each resistance mutation we model represents one possible evolutionary path to this state.
- Defensive coding: The
LIGAND_CODESset and the fallback condition handle the possibility that different PyRosetta versions or PDB downloads name the ligand differently. Advanced code should anticipate multiple possible inputs. - ⚠️ This is where the numbering problem gets worse: every
delete_residue_slow()call shifts the Rosetta position of every residue that comes after it. The PDB numbers printed in the file never change — but the Rosetta numbers absolutely do. After this cell, the two numbering systems are now meaningfully out of sync, which is exactly why Cell 4 has to happen next.
Cell 4 — Map PDB Residue Numbers to Rosetta Positions
Build the lookup table before mutating
# Build a dictionary: PDB residue number → Rosetta position
pdb_to_rosetta = {}
for i in range(1, wt_pose.total_residue() + 1):
if wt_pose.residue(i).is_protein():
pdb_num = wt_pose.pdb_info().number(i)
pdb_to_rosetta[pdb_num] = i
print("PDB ↔ Rosetta position mapping (first 10 protein residues):")
count = 0
for pdb_num, ros_pos in sorted(pdb_to_rosetta.items()):
print(f" PDB {pdb_num:4d} → Rosetta {ros_pos:4d} | "
f"{wt_pose.residue(ros_pos).name3()}")
count += 1
if count >= 10:
print(" ...")
break
print(f"\nTotal protein residues mapped: {len(pdb_to_rosetta)}")
# Verify the resistance mutation positions exist
resistance_mutations = {
"M69L": (69, "L"),
"K73R": (73, "R"),
"S130G": (130, "G"),
"R244S": (244, "S"),
"N276D": (276, "D"),
}
print("\nVerifying resistance mutation positions:")
for name, (pdb_pos, new_aa) in resistance_mutations.items():
if pdb_pos in pdb_to_rosetta:
ros_pos = pdb_to_rosetta[pdb_pos]
current_aa = wt_pose.residue(ros_pos).name1()
print(f" {name}: PDB {pdb_pos} → Rosetta {ros_pos} | "
f"Current AA: {current_aa} | {'✓ Matches' if current_aa == name[0] else '⚠ Mismatch'}")
else:
print(f" {name}: PDB position {pdb_pos} NOT FOUND in structure")
This cell builds a dictionary that translates PDB residue numbers (how biochemists refer to positions) into Rosetta’s internal numbering (which may differ after cleaning). Never assume PDB numbers equal Rosetta numbers — cleaning a structure removes residues and shifts Rosetta’s internal count.
Analogy: Imagine a city’s street addresses are renumbered after several houses are demolished. A house that was “123 Main Street” (PDB numbering) might now be “87 Main Street” (Rosetta numbering) because earlier houses were removed. You need the translation table before you can find any house by its old address.
🔍 Deep Dive: How the Dictionary Actually Works
a) Its structure. A dictionary is a set of key → value pairs. You hand it a key, it instantly hands back the matching value — no scanning required. After this cell runs, pdb_to_rosetta looks conceptually like this:
graph LR
subgraph PDB["PDB Numbering (frozen, from the original crystal structure)"]
P1["69"]
P2["73"]
P3["130"]
P4["244"]
P5["276"]
end
subgraph ROS["Rosetta Numbering (sequential, recount after cleaning)"]
R1["45"]
R2["49"]
R3["102"]
R4["198"]
R5["225"]
end
P1 -. "dict lookup" .-> R1
P2 -. "dict lookup" .-> R2
P3 -. "dict lookup" .-> R3
P4 -. "dict lookup" .-> R4
P5 -. "dict lookup" .-> R5
(Exact Rosetta numbers will vary depending on exactly which residues got deleted in Cell 3 — the point is that they are different from the PDB numbers, and shift unpredictably.)
b) How it’s built — line by line:
| Line | What it does |
|---|---|
pdb_to_rosetta = {} | Start an empty dictionary |
for i in range(1, wt_pose.total_residue() + 1) | Walk through every Rosetta position, 1 → N |
if wt_pose.residue(i).is_protein() | Skip the ligand — only protein residues need translating |
pdb_num = wt_pose.pdb_info().number(i) | Ask PyRosetta: “what was this residue called in the original PDB file?” |
pdb_to_rosetta[pdb_num] = i | Store the pair: PDB number is the key, Rosetta position is the value |
By the end of the loop, every protein residue has an entry, so the dictionary is a complete two-way reference table (you only need it in one direction here, but it covers the whole protein).
c) How values get “cited” (looked up) later:
if pdb_pos in pdb_to_rosetta: # does this PDB number exist in the table?
ros_pos = pdb_to_rosetta[pdb_pos] # jump straight to the matching value
This is the same mechanism used again in Cell 5 for the actual mutations. No looping, no searching — pdb_to_rosetta[130] jumps directly to the answer, the same way flipping to a page number in an index beats reading the whole book to find a topic.
d) Why a dictionary, specifically, and not something else:
- Correctness: Clinical literature reports mutations using PDB numbers (e.g., “S130G”). PyRosetta only understands its own sequential numbering. Without a precise translation, you risk mutating the wrong physical residue while believing you modeled a real clinical mutation.
- Speed: A dictionary lookup is O(1) — instant — regardless of how many residues the protein has. Searching a list for a matching number every time would be slower and more error-prone.
- Reusability: Built once in Cell 4, the same dictionary is reused for all five mutations in Cell 5, instead of rebuilding the mapping from scratch each time.
- Safety net: The
if pdb_pos in pdb_to_rosettacheck lets the code fail gracefully (skip and report) if a clinical position happens to be missing from the resolved structure, rather than crashing or silently mutating the wrong atom.
Key Concepts:
- PDB numbering: The residue numbers as published in the PDB entry, reflecting the original experimental numbering. May contain gaps (missing density) or non-sequential numbers.
- Rosetta numbering: Sequential integers from 1 to N assigned by PyRosetta after loading. Always contiguous.
- Lookup dictionary: A data structure that maps one set of identifiers to another. Essential when working with multiple numbering systems.
Cell 5 — Introduce Each Resistance Mutation and Measure ΔΔG
The resistance profile computation
from pyrosetta.toolbox import mutate_residue
print("Modelling resistance mutations...")
print("(Each mutation requires ~1–3 minutes)\n")
resistance_results = []
for mutation_name, (pdb_pos, new_aa) in resistance_mutations.items():
if pdb_pos not in pdb_to_rosetta:
print(f" {mutation_name}: position not found — skipping")
continue
rosetta_pos = pdb_to_rosetta[pdb_pos]
current_aa = wt_pose.residue(rosetta_pos).name1()
print(f" Processing {mutation_name} (Rosetta position {rosetta_pos})...", end=" ")
try:
# Create mutant pose
mutant_pose = wt_pose.clone()
mutate_residue(mutant_pose, rosetta_pos, new_aa)
# Score the mutant
mut_binding = compute_binding_energy(mutant_pose, ligand_clean, scorefxn)
ddg = mut_binding - wt_binding
# Resistance classification
if ddg >= 3.0:
resistance_level = "High resistance"
elif ddg >= 1.5:
resistance_level = "Moderate resistance"
elif ddg >= 0.5:
resistance_level = "Low resistance"
elif ddg <= -0.5:
resistance_level = "Hypersensitivity"
else:
resistance_level = "No effect"
print(f"ΔΔG = {ddg:+.2f} REU — {resistance_level}")
resistance_results.append({
"Mutation": mutation_name,
"PDB Position": pdb_pos,
"Wild-Type AA": current_aa,
"Mutant AA": new_aa,
"WT Binding (REU)": round(wt_binding, 2),
"Mut Binding (REU)": round(mut_binding, 2),
"ΔΔG (REU)": round(ddg, 2),
"Resistance Level": resistance_level
})
except Exception as e:
print(f"ERROR — {e}")
df_resistance = pd.DataFrame(resistance_results).sort_values("ΔΔG (REU)", ascending=False)
print("\nDone! Resistance profile computed.")
The ΔΔG thresholds here are higher than in Project 5 because we are using real resistance mutations, not Alanine probes. Clinical resistance often corresponds to ΔΔG values of 1.5–5.0 REU — large enough to meaningfully impair drug binding, small enough that the enzyme still functions.
Notice the very first line of the loop body:
rosetta_pos = pdb_to_rosetta[pdb_pos]. That single dictionary lookup is the only thing standing between “I modeled the S130G resistance mutation” and “I modeled some unrelated residue that happens to share a number with S130G in the wrong numbering system.”
Key Concepts:
- Hypersensitivity: Occasionally, a mutation actually improves inhibitor binding (ΔΔG < 0). This is rare for resistance mutations but has been observed — sometimes a mutation that confers resistance to one drug simultaneously increases sensitivity to another.
- Functional constraint: Resistance mutations cannot be arbitrary. The enzyme must still function (break down antibiotic) after the mutation — it cannot be so destabilized that it falls apart. This is why most resistance mutations are conservative substitutions (similar amino acids).
Cell 6 — Display the Resistance Profile Table
Rank mutations by clinical resistance potential
print("\n" + "=" * 75)
print("RESISTANCE PROFILE — TEM-1 β-Lactamase vs. Inhibitor")
print("=" * 75)
print(f"\nWild-type binding energy (drug-sensitive): {wt_binding:.2f} REU")
print(f"Positive ΔΔG = inhibitor binds more weakly in the mutant = resistance\n")
print(f"{'Mutation':<10} {'WT AA→Mut':<12} {'Mut Binding':>12} {'ΔΔG (REU)':>12} {'Resistance Level'}")
print("-" * 75)
for _, row in df_resistance.iterrows():
print(f"{row['Mutation']:<10} "
f"{row['Wild-Type AA']}→{row['Mutant AA']:<10} "
f"{row['Mut Binding (REU)']:>12.2f} "
f"{row['ΔΔG (REU)']:>+12.2f} "
f"{row['Resistance Level']}")
print("=" * 75)
highest = df_resistance.iloc[0]
print(f"\nHighest resistance mutation: {highest['Mutation']} (ΔΔG = {highest['ΔΔG (REU)']:+.2f} REU)")
print(f"This mutation is expected to require a {abs(highest['ΔΔG (REU)']):.1f}x-fold increase")
print(f"in inhibitor concentration to maintain the same binding occupancy.")
df_resistance.to_csv("resistance_profile.csv", index=False)
print("\nResistance profile saved to resistance_profile.csv")
Key Concepts:
- Fold-change in IC₅₀: In pharmacology, a 1.4 REU increase in ΔΔG corresponds roughly to a 10-fold increase in the drug concentration needed to achieve the same effect. This is a rough rule of thumb — exact conversion requires experimental validation.
- Clinical threshold: Resistance is clinically relevant when the required drug dose exceeds the safe dosage for patients. A mutation might cause measurable resistance computationally but not be clinically significant if the drug can still be used at a higher (but still safe) dose.
Cell 7 — Visualize the Resistance Profile
Plot ΔΔG for each mutation with clinical significance zones
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
LEVEL_COLORS = {
"High resistance": "#922b21",
"Moderate resistance": "#e67e22",
"Low resistance": "#f4d03f",
"No effect": "#95a5a6",
"Hypersensitivity": "#27ae60"
}
df_plot = df_resistance.sort_values("ΔΔG (REU)", ascending=False)
colors = [LEVEL_COLORS.get(l, "#95a5a6") for l in df_plot["Resistance Level"]]
fig, ax = plt.subplots(figsize=(11, 6))
bars = ax.bar(df_plot["Mutation"], df_plot["ΔΔG (REU)"],
color=colors, edgecolor="white", linewidth=0.5, width=0.5)
# Clinical zone shading
ax.axhspan(3.0, ax.get_ylim()[1] if ax.get_ylim()[1] > 3.0 else 6.0,
alpha=0.08, color="#922b21", label="High resistance zone")
ax.axhspan(1.5, 3.0, alpha=0.08, color="#e67e22", label="Moderate resistance zone")
ax.axhspan(0.5, 1.5, alpha=0.08, color="#f4d03f", label="Low resistance zone")
# Reference lines
ax.axhline(y=3.0, color="#922b21", linestyle="--", linewidth=0.8)
ax.axhline(y=1.5, color="#e67e22", linestyle="--", linewidth=0.8)
ax.axhline(y=0.5, color="#f4d03f", linestyle="--", linewidth=0.8)
ax.axhline(y=0.0, color="black", linestyle="-", linewidth=0.5)
# Annotate bars
for bar, val in zip(bars, df_plot["ΔΔG (REU)"]):
ax.text(bar.get_x() + bar.get_width() / 2, val + 0.05,
f"{val:+.2f}", ha="center", va="bottom",
fontsize=10, fontweight="bold")
ax.set_xlabel("Resistance Mutation", fontsize=12)
ax.set_ylabel("ΔΔG (REU)\n(Positive = drug binds more weakly = resistance)", fontsize=11)
ax.set_title("Antibiotic Resistance Profile — TEM-1 β-Lactamase\n"
"Effect of Clinical Mutations on Inhibitor Binding",
fontsize=13, fontweight="bold")
# Legend
patches = [mpatches.Patch(color=v, label=k) for k, v in LEVEL_COLORS.items()
if k in df_plot["Resistance Level"].values]
ax.legend(handles=patches, loc="upper right", fontsize=9)
plt.tight_layout()
plt.savefig("resistance_profile.png", dpi=150, bbox_inches="tight")
plt.show()
print("Chart saved as resistance_profile.png")
Key Concepts:
- Zone shading (
axhspan): Fills a horizontal band of the plot with a semi-transparent colour. Here each band corresponds to a clinical resistance classification — the chart communicates not just the values but also their clinical meaning. -
axhspanvsaxhline:axhlinedraws a line;axhspanfills a region. Both use axis coordinates (y-values), not pixel coordinates.
Cell 8 — The Next-Generation Drug Problem
Model what a drug needs to overcome resistance
# Find the most resistant mutation
most_resistant = df_resistance.sort_values("ΔΔG (REU)", ascending=False).iloc[0]
mut_name = most_resistant["Mutation"]
ddg_resistance = most_resistant["ΔΔG (REU)"]
print("THE NEXT-GENERATION DRUG CHALLENGE")
print("=" * 60)
print(f"\nThe mutation {mut_name} causes ΔΔG = +{ddg_resistance:.2f} REU")
print(f"This means the inhibitor binds {ddg_resistance:.2f} REU more weakly")
print(f"in the resistant mutant than in the drug-sensitive wild type.")
print()
print("To restore binding in the resistant strain, a next-generation")
print("drug would need to:")
print()
print(f" Option A: Recover the {ddg_resistance:.2f} REU loss")
print(f" → Add new chemical interactions to regain this energy")
print(f" → Target other binding site residues NOT affected by {mut_name}")
print()
print(f" Option B: Design around the mutation")
print(f" → Synthesize a drug that does not rely on position {most_resistant['PDB Position']}")
print(f" → Use the alanine scan (Project 5) to identify alternative hot spots")
print()
print(f" Option C: Combination therapy")
print(f" → Pair the inhibitor with a second drug that targets a different")
print(f" enzyme, reducing the chance of resistance emerging to both simultaneously")
print()
# Summarize the full project pipeline
print("YOUR FULL COMPUTATIONAL DRUG DESIGN PIPELINE")
print("=" * 60)
print(" Project 1 → Characterize protein stability")
print(" Project 2 → Dock drug candidates and rank by binding energy")
print(" Project 3 → Select the highest-quality structure for analysis")
print(" Project 4 → Map the drug binding site chemically")
print(" Project 5 → Identify hot spot residues via alanine scanning")
print(" Project 6 → Model known resistance mutations and their impact")
print()
print("In a real drug discovery program, you would now:")
print(" → Take the hot spots from Project 5")
print(" → Confirm they are NOT affected by resistance mutations from Project 6")
print(" → Design a new drug that engages the resistant mutant at safe hot spots")
print(" → Re-dock (Project 2 workflow) to validate computational predictions")
print(" → Submit top candidates for laboratory synthesis and testing")
This cell synthesizes everything from all six projects into a coherent drug discovery narrative. The most powerful thing about computational methods is not that they replace lab experiments — it is that they dramatically focus which experiments are worth running.
Key Concepts:
- Combination therapy: Using two or more drugs with different targets simultaneously. Resistance to both at once requires two mutations occurring together, which is far less likely than one mutation alone. This is why HIV, tuberculosis, and cancer are treated with drug cocktails.
- Structure-guided drug design: Using detailed structural and energetic information — exactly what we have computed — to rationally design the next generation of drugs. The alternative (random screening) is orders of magnitude more expensive.
- Resistance landscape: The full set of mutations that confer resistance and their severity. Understanding the resistance landscape tells drug designers which positions to avoid anchoring their drug to.
Discussion Questions
1. Which mutation caused the greatest increase in ΔΔG? Based on what you know about that amino acid substitution, can you explain mechanistically why it disrupts inhibitor binding?
2. Were any mutations classified as “No effect” or “Hypersensitivity”? What does it tell you about the binding site if some mutations have no effect on inhibitor binding?
3. A bacterium would need to accumulate multiple resistance mutations to fully escape combination therapy. Why is acquiring two simultaneous mutations much harder than acquiring one? (Think about probability.)
4. Our model assumes the protein structure does not change after mutation. In reality, mutations can cause local or global structural rearrangements. How might you test this assumption computationally?
5. You now have a complete computational pipeline from structure loading to resistance profiling. If you were a pharmaceutical company, which single computational result from Projects 1–6 would you trust most when deciding which drug candidate to invest in developing, and why?
6. (New) Cell 4 builds pdb_to_rosetta using PDB number as the key and Rosetta position as the value. What would go wrong if you built it the other way around (Rosetta position as key, PDB number as value), given how it’s actually used in Cell 5?
Key Vocabulary
| Term | Definition |
|---|---|
| β-Lactamase | A bacterial enzyme that inactivates β-lactam antibiotics by breaking their ring structure |
| Resistance mutation | A naturally occurring amino acid change that reduces drug binding without eliminating enzyme function |
| ΔΔG | The energetic cost of a mutation, expressed as the change in binding energy relative to wild type |
| Drug-sensitive strain | A bacterium killed by the antibiotic at normal clinical doses |
| Drug-resistant strain | A bacterium that survives the antibiotic due to one or more mutations |
| PDB numbering | Residue numbers as published in the PDB entry — may not match Rosetta’s internal numbering |
| Rosetta numbering | Sequential internal numbering assigned by PyRosetta; always starts at 1 |
| Dictionary (Python) | A key → value data structure giving instant (O(1)) lookups; used here as a numbering-system translator |
| Combination therapy | Using two or more drugs to reduce the likelihood of resistance evolving |
| Resistance landscape | The full map of which mutations confer resistance and by how much |
| Structure-guided drug design | Rational drug optimization informed by atomic-level structural and energetic data |
| IC₅₀ | The drug concentration needed to inhibit 50% of the enzyme activity — a standard potency measure |
| Conservative substitution | Replacing an amino acid with one of similar size and chemistry, minimizing structural disruption |