Como Construir uma Bancada de Engenharia de Plasmídeos com Mapeamento Circular, Análise de Restrição, Géis Virtuais e Desenho de Primers
Neste tutorial, construímos uma bancada de plasmídeos nativa do Google Colab que recria as ideias centrais do SpliceCraft dentro de um ambiente de notebook interativo. Em vez de depender de uma TUI baseada em terminal, usamos Biopython, NumPy e Matplotlib para carregar registros de plasmídeos, normalizar características genômicas anotadas, renderizar mapas de plasmídeos circulares e lineares, calcular estatísticas de sequência, analisar enzimas de restrição […] O post Como Construir uma Bancada de Engenharia de Plasmídeos com Mapeamento Circular, Análise de Restrição...
In this tutorial, we build a Google Colab-native plasmid workbench that recreates the core ideas of SpliceCraft inside an interactive notebook environment. Instead of relying on a terminal-based TUI, we use Biopython, NumPy, and Matplotlib to load plasmid records, normalize annotated genomic features, render circular and linear plasmid maps, compute sequence statistics, analyze restriction enzyme cut sites, simulate virtual digests, scan open reading frames, translate CDS features, design primers, and apply sequence edits programmatically. We begin with a synthetic offline plasmid so the workflow runs reliably without external dependencies, while still supporting optional NCBI GenBank fetching and local GenBank uploads.
Copy CodeCopiedUse a different Browser
except ImportError:
import subprocess, sys
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "biopython"], check=True)
import math, io, textwrap, random
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
from Bio import Entrez, SeqIO
from Bio.Seq import Seq
from Bio.SeqRecord import SeqRecord
from Bio.SeqFeature import SeqFeature, FeatureLocation
from Bio.Restriction import RestrictionBatch, Analysis, CommOnly
from Bio.SeqUtils import MeltingTemp as _mt
except Exception:
EMAIL = "you@example.com"
ACCESSION = "L09137"
USE_NCBI = False
Entrez.email = EMAIL
TYPE_COLORS = {
"CDS": "#d1495b", "gene": "#c1666b", "rep_origin": "#2e86ab",
"promoter": "#e3a008", "terminator": "#8d99ae", "misc_feature": "#6a994e",
"primer_bind": "#9d4edd", "protein_bind": "#457b9d", "source": "#adb5bd",
def _color(ftype): return TYPE_COLORS.get(ftype, "#6a994e")
MCS = "GAATTCGAGCTCGGTACCCGGGGATCCTCTAGAGTCGACCTGCAGGCATGCAAGCTT"
def _synthetic_plasmid():
random.seed(19)
body = "".join(random.choice("ACGT") for _ in range(2686))
seq = body[:400] + MCS + body[400:]
rec = SeqRecord(Seq(seq), id="pDEMO", name="pDEMO",
description="Synthetic offline demo plasmid (SpliceCraft-Colab)")
rec.annotations["topology"] = "circular"
rec.annotations["molecule_type"] = "ds-DNA"
def feat(s, e, strand, ftype, label):
f = SeqFeature(FeatureLocation(s, e, strand=strand), type=ftype)
f.qualifiers["label"] = [label]; return f
rec.features += [
feat(400, 400 + len(MCS), 1, "misc_feature", "MCS"),
feat(700, 1561, 1, "CDS", "AmpR"),
feat(1700, 2260, -1, "rep_origin", "ori"),
feat(2350, 2686, 1, "CDS", "lacZ-alpha"),
rec.features[1].qualifiers["product"] = ["beta-lactamase (demo)"]
def load_record():
if USE_NCBI and "@" in EMAIL and not EMAIL.startswith("you@"):
h = Entrez.efetch(db="nucleotide", id=ACCESSION,
rettype="gbwithparts", retmode="text")
rec = SeqIO.read(h, "genbank"); h.close()
print(f"✓ Fetched {ACCESSION} from NCBI: {rec.description}")
except Exception as e:
print(f" NCBI fetch failed ({e}); using synthetic demo plasmid.")
print(" USE_NCBI is off (or EMAIL not set) — using synthetic demo plasmid.")
return _synthetic_plasmid()
def upload_gb():
from google.colab import files
up = files.upload()
name = next(iter(up))
return SeqIO.read(io.StringIO(up[name].decode()), "genbank")
def label_of(f):
for k in ("label", "gene", "product", "note"):
if k in f.qualifiers: return f.qualifiers[k][0]
return f.type
def norm_features(rec, skip_source=True, min_len=0):
L = len(rec.seq); out = []
for f in rec.features:
if skip_source and f.type in ("source",): continue
s = int(f.location.start); e = int(f.location.end)
if (e - s) < min_len: continue
out.append(dict(start=s % L, end=e % L, strand=f.location.strand or 1,
type=f.type, label=label_of(f), color=_color(f.type)))
We set up the notebook environment, install Biopython when needed, and import the scientific, plotting, and sequence-analysis libraries required for the workflow. We define the plasmid configuration, feature color palette, multiple cloning site, and fallback synthetic plasmid so the tutorial works even without an internet connection. We also create helper functions to load records from NCBI or GenBank files and normalize annotated sequence features into drawable metadata.
Copy CodeCopiedUse a different Browser
def _ang(bp, L): return math.pi/2 - 2*math.pi*(bp/L)
def _pt(bp, r, L):
a = _ang(bp, L); return r*math.cos(a), r*math.sin(a)
def _arc(s, e, r, L, n=240):
if e < s: e += L
bps = np.linspace(s, e, max(2, int(n*(e-s)/L)+2))
return ([r*math.cos(_ang(b, L)) for b in bps],
[r*math.sin(_ang(b, L)) for b in bps])
def gc_percent(seq):
s = str(seq).upper(); n = len(s) or 1
return 100.0 * (s.count("G") + s.count("C")) / n
def circular_map(rec, title=None, show_gc=True):
L = len(rec.seq); feats = norm_features(rec)
fig, ax = plt.subplots(figsize=(8, 8)); R = 1.0
if show_gc:
w = max(30, L // 120); step = max(1, w // 2); mean = gc_percent(rec.seq)
s = str(rec.seq).upper()
for i in range(0, L, step):
win = s[i:i+w] or s[i:] + s[:(i+w) % L]
dev = (gc_percent(win) - mean) / 100.0
rr = 0.72 + dev * 0.9
x0, y0 = _pt(i, 0.72, L); x1, y1 = _pt(i, rr, L)
ax.plot([x0, x1], [y0, y1],
color="#2e86ab" if dev >= 0 else "#d1495b", lw=1, alpha=0.5, zorder=1)
xs, ys = _arc(0, L, R, L); ax.plot(xs, ys, color="#333", lw=2.5, zorder=2)
step = max(1, round(L/12/100)*100) or max(1, L//12)
for t in range(0, L, step):
x0, y0 = _pt(t, R*1.015, L); x1, y1 = _pt(t, R*1.045, L)
ax.plot([x0, x1], [y0, y1], color="#999", lw=1, zorder=2)
lx, ly = _pt(t, R*1.10, L)
ax.text(lx, ly, f"{t:,}", ha="center", va="center", fontsize=7, color="#777")
for f in feats:
outer = f["strand"] >= 0
rr = R + 0.09 if outer else R - 0.09
xs, ys = _arc(f["start"], f["end"], rr, L)
ax.plot(xs, ys, color=f["color"], lw=10, solid_capstyle="butt", alpha=0.9, zorder=3)
tip = f["end"] if outer else f["start"]
a = _ang(tip, L); tx, ty = rr*math.cos(a), rr*math.sin(a)
d = -1 if outer else 1
tanx, tany = -math.sin(a)*d, math.cos(a)*d
px, py = math.cos(a), math.sin(a)
ln, wd = 0.055, 0.052
ax.add_patch(Polygon([(tx+tanx*ln, ty+tany*ln),
(tx+px*wd, ty+py*wd),
(tx-px*wd, ty-py*wd)],
color=f["color"], zorder=4))
span = (f["end"] - f["start"]) % L
mid = (f["start"] + span/2) % L
lx, ly = _pt(mid, (rr + 0.16) if outer else (rr - 0.16), L)
ax.text(lx, ly, f["label"], ha="center", va="center",
fontsize=8.5, color=f["color"], weight="bold", zorder=5)
circ = "circular" if rec.annotations.get("topology") == "circular" else "linear"
ax.text(0, 0.05, title or rec.name, ha="center", va="center", fontsize=15, weight="bold")
ax.text(0, -0.07, f"{L:,} bp · {gc_percent(rec.seq):.1f}% GC · {circ}",
ha="center", va="center", fontsize=9.5, color="#555")
ax.set_xlim(-1.45, 1.45); ax.set_ylim(-1.45, 1.45)
ax.set_aspect("equal"); ax.axis("off"); plt.tight_layout(); plt.show()
We implement the circular plasmid visualization by mapping base-pair positions to angular coordinates on a clockwise ring. We calculate GC content, draw the plasmid backbone, add tick marks, render feature arcs, and attach directional arrowheads to clearly indicate strand orientation. We also add labels and central sequence metadata so that the map is both biologically informative and visually readable in Colab.
Copy CodeCopiedUse a different Browser
def linear_map(rec):
L = len(rec.seq); feats = norm_features(rec)
fig, ax = plt.subplots(figsize=(11, 2.6))
ax.hlines(0, 0, L, color="#333", lw=2)
for f in feats:
y = 0.35 if f["strand"] >= 0 else -0.35
s, e = f["start"], f["end"]
segs = [(s, e)] if e >= s else [(s, L), (0, e)]
for a, b in segs:
ax.annotate("", xy=(b if f["strand"] >= 0 else a, y),
xytext=(a if f["strand"] >= 0 else b, y),
arrowprops=dict(arrowstyle="-|>", color=f["color"], lw=7, alpha=0.85))
ax.text((s + ((e - s) % L)/2) % L, y + (0.28 if y > 0 else -0.28), f["label"],
ha="center", va="center", fontsize=8.5, color=f["color"], weight="bold")
ax.set_xlim(-L*0.02, L*1.02); ax.set_ylim(-1, 1)
ax.set_yticks([]); ax.set_xlabel("position (bp)")
ax.set_title(f"{rec.name} — linear view", fontsize=11)
for sp in ("top", "left", "right"): ax.spines[sp].set_visible(False)
plt.tight_layout(); plt.show()
def gc_skew_plot(rec, window=None):
s = str(rec.seq).upper(); L = len(s)
window = window or max(50, L // 100); skew = []; run = 0
for i in range(0, L, window):
w = s[i:i+window]; g, c = w.count("G"), w.count("C")
run += (g - c) / max(1, g + c); skew.append(run)
fig, ax = plt.subplots(figsize=(11, 2.6))
ax.plot(np.arange(len(skew))*window, skew, color="#2e86ab")
ax.axhline(0, color="#aaa", lw=0.8)
ax.set_title(f"Cumulative GC-skew — {rec.name} (min≈replication origin, max≈terminus)",
fontsize=10)
ax.set_xlabel("position (bp)"); ax.set_ylabel("cumulative skew")
plt.tight_layout(); plt.show()
def stats(rec):
seq = str(rec.seq).upper(); L = len(seq)
print(f"── {rec.name} ─────────────────────────────")
print(f"length : {L:,} bp")
print(f"GC content : {gc_percent(seq):.2f} %")
print(f"A/T/G/C : {seq.count('A')}/{seq.count('T')}/{seq.count('G')}/{seq.count('C')}")
print(f"topology : {rec.annotations.get('topology', 'unknown')}")
print(f"features : {len(norm_features(rec))} annotated")
def _is_circular(rec): return rec.annotations.get("topology") == "circular"
def find_sites(rec, enzymes=None):
batch = RestrictionBatch(enzymes) if enzymes else CommOnly
ana = Analysis(batch, rec.seq, linear=not _is_circular(rec))
return {str(k): v for k, v in ana.full().items()}
def unique_cutters(rec, enzymes=None):
hits = find_sites(rec, enzymes)
return sorted([e for e, pos in hits.items() if len(pos) == 1])
def digest_fragments(rec, enzymes):
hits = find_sites(rec, enzymes)
cuts = sorted({p for pos in hits.values() for p in pos})
L = len(rec.seq)
if not cuts: return [L]
if _is_circular(rec):
frags = [cuts[i+1] - cuts[i] for i in range(len(cuts)-1)]
frags.append(L - cuts[-1] + cuts[0])
frags = [cuts[0]] + [cuts[i+1]-cuts[i] for i in range(len(cuts)-1)] + [L - cuts[-1]]
return sorted((f for f in frags if f > 0), reverse=True)
def restriction_report(rec, enzymes=None):
hits = find_sites(rec, enzymes)
cutting = {e: p for e, p in hits.items() if p}
print(f"── Restriction map of {rec.name} "
f"({'circular' if _is_circular(rec) else 'linear'}) ──")
print(f"unique (single) cutters: {', '.join(unique_cutters(rec, enzymes)) or 'none'}")
print("cutting enzymes (site count):")
for e in sorted(cutting, key=lambda x: len(cutting[x])):
print(f" {e:<10} {len(cutting[e])}x at {cutting[e]}")
We build a linear plasmid map that represents the same annotated features along a base-pair axis for easier positional inspection. We then compute the cumulative GC-skew to observe directional nucleotide bias that can indicate replication-related structure. We also add sequence statistics and restriction-analysis utilities to summarize plasmid composition, enzyme cut sites, unique cutters, and expected digest fragments.
Copy CodeCopiedUse a different Browser
def virtual_gel(named_digests):
lanes = [("Ladder", LADDER)] + list(named_digests.items())
fig, ax = plt.subplots(figsize=(1.4*len(lanes)+1, 6))
ax.set_facecolor("#0b1021")
def y(sz): return math.log10(max(sz, 50))
for i, (name, frags) in enumerate(lanes):
for f in frags:
ax.hlines(y(f), i+0.62, i+1.38, color="#e6f1ff",
lw=2 + min(4, f/1500), alpha=0.9)
if name == "Ladder":
ax.text(i+0.5, y(f), f"{f}", color="#9db4d0", ha="right",
va="center", fontsize=7)
ax.text(i+1, y(max(LADDER))+0.15, name, color="#e6f1ff",
ha="center", fontsize=9, rotation=0)
ax.set_xlim(0.2, len(lanes)+0.5); ax.set_ylim(y(80), y(12000))
ax.invert_yaxis(); ax.axis("off")
ax.set_title("Virtual agarose gel", color="#333", fontsize=11)
plt.tight_layout(); plt.show()
def find_orfs(rec, min_aa=50):
seq = rec.seq; L = len(seq); orfs = []
for strand, s in ((+1, seq), (-1, seq.reverse_complement())):
for frame in range(3):
while i < L - 2:
if s[i:i+3] == "ATG":
while j < L - 2:
if s[j:j+3] in ("TAA", "TAG", "TGA"):
if (j - i)//3 >= min_aa:
orfs.append((strand, frame, i, j+3, (j-i)//3))
i = j; break
return sorted(orfs, key=lambda o: -o[4])
def translate_feature(rec, label):
for f in rec.features:
if label.lower() in label_of(f).lower() and f.type in ("CDS", "gene"):
sub = f.location.extract(rec.seq)
prot = sub.translate(table=11, to_stop=True)
return str(prot)
return None
def _tm(primer):
if _mt is not None:
try: return float(_mt.Tm_NN(Seq(primer)))
except Exception: pass
p = primer.upper()
return 2*(p.count("A")+p.count("T")) + 4*(p.count("G")+p.count("C"))
def design_primers(rec, start, end, target_tm=60.0, lo=18, hi=30):
seq = str(rec.seq)
def tune(sub, rev=False):
best = None
for n in range(lo, hi+1):
p = sub[-n:] if rev else sub[:n]
if rev: p = str(Seq(p).reverse_complement())
score = abs(_tm(p) - target_tm)
if best is None or score < best[0]: best = (score, p)
return best[1]
fwd = tune(seq[start:start+hi])
rev = tune(seq[end-hi:end], rev=True)
print(f"── Primers to amplify {start}–{end} ({end-start} bp) ──")
print(f" FWD 5'-{fwd}-3' len {len(fwd)} Tm {_tm(fwd):.1f}°C GC {gc_percent(fwd):.0f}%")
print(f" REV 5'-{rev}-3' len {len(rev)} Tm {_tm(rev):.1f}°C GC {gc_percent(rev):.0f}%")
print(f" amplicon: {end-start} bp")
return fwd, rev
We simulate a virtual agarose gel by plotting digest fragment sizes beside a DNA ladder on a log-scaled migration axis. We scan the plasmid sequence in all six reading frames to identify long open reading frames and rank them by amino-acid length. We also translate annotated CDS features and design primers around a selected region by tuning primer length toward a target melting temperature.
Copy CodeCopiedUse a different Browser
def edit_sequence(rec, pos, delete=0, insert=""):
s = str(rec.seq)
new = s[:pos] + insert + s[pos+delete:]
out = SeqRecord(Seq(new), id=rec.id, name=rec.name + "_edit",
description=rec.description + f" [edit @{pos}:-{delete}+{len(insert)}]")
out.annotations = dict(rec.annotations)
shift = len(insert) - delete
for f in rec.features:
s0, e0 = int(f.location.start), int(f.location.end)
if s0 >= pos: s0 += shift
if e0 > pos: e0 += shift
if 0 <= s0 < e0 <= len(new):
g = SeqFeature(FeatureLocation(s0, e0, strand=f.location.strand), type=f.type)
g.qualifiers = dict(f.qualifiers); out.features.append(g)
LIBRARY = {}
def add_to_library(rec): LIBRARY[rec.name] = rec; print(f"+ added {rec.name} (now {len(LIBRARY)} in library)")
def show_library():
for name, r in LIBRARY.items():
print(f" {name:<16} {len(r.seq):>7,} bp {gc_percent(r.seq):.1f}% GC")
We implement a pure sequence-editing function that supports insertion, deletion, and replacement while returning a new edited SeqRecord. We preserve plasmid annotations and shift downstream feature coordinates so the edited construct remains internally consistent after sequence changes. We also create a lightweight in-notebook plasmid library that lets us store and list original and edited records during the workflow.
Copy CodeCopiedUse a different Browser
if __name__ == "__main__":
rec = load_record()
add_to_library(rec)
print("\n[1] Sequence statistics")
print("\n[2] Circular map (the signature SpliceCraft view)")
circular_map(rec)
print("\n[3] Linear map + GC-skew")
linear_map(rec)
gc_skew_plot(rec)
print("\n[4] Restriction analysis")
restriction_report(rec, enzymes=["EcoRI", "BamHI", "HindIII", "PstI",
"SalI", "XbaI", "KpnI", "SacI", "SphI"])
print("\n[5] Virtual digests on a gel")
virtual_gel({
"EcoRI": digest_fragments(rec, ["EcoRI"]),
"EcoRI+HindIII": digest_fragments(rec, ["EcoRI", "HindIII"]),
"BamHI+SalI": digest_fragments(rec, ["BamHI", "SalI"]),
print("\n[6] Six-frame ORF scan (top 5 by length)")
for strand, frame, s, e, aa in find_orfs(rec, min_aa=40)[:5]:
print(f" strand {strand:+d} frame {frame} {s}-{e} {aa} aa")
print("\n[7] Translate a CDS feature")
prot = translate_feature(rec, "AmpR")
if prot: print(f" AmpR protein ({len(prot)} aa): {prot[:60]}...")
print("\n[8] Design primers to amplify a region")
design_primers(rec, 400, 900)
print("\n[9] Edit the sequence (insert a 6-bp tag) and re-map")
edited = edit_sequence(rec, pos=456, insert="CATCATCAT")
add_to_library(edited)
print("\n[10] Library"); show_library()
print("\nDone. Call any function directly, e.g. circular_map(edited).")
We run the full guided demo by loading a plasmid record, adding it to the library, and printing sequence statistics. We generate the circular map, linear map, GC-skew plot, restriction report, virtual gel, ORF scan, CDS translation, and primer design output in sequence. We finish by editing the plasmid, storing the edited construct, and showing how the notebook can act as a reusable plasmid workbench.
In conclusion, we completed the tutorial by turning a single notebook into a compact yet functional plasmid-engineering workspace. We moved from sequence loading and feature extraction to graphical plasmid visualization, GC-skew analysis, restriction mapping, gel simulation, ORF discovery, primer design, and editable sequence manipulation. By keeping each operation as a reusable Python function, we made the workflow modular enough to inspect real GenBank records, modify plasmid sequences, compare edited constructs, and extend the notebook with additional cloning or annotation utilities. Overall, we demonstrated how we can translate the behavior of a terminal plasmid tool into a reproducible, visual, and notebook-friendly bioinformatics workflow.
Check out the FULL CODES here. Also, feel free to follow us on Twitter and don’t forget to join our 150k+ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.
Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.? Connect with us
The post How to Build Plasmid Engineering Workbench with Circular Mapping, Restriction Analysis, Virtual Gels, and Primer Design appeared first on MarkTechPost.
Leia também