A pipeline and self-contained web viewer that link protein 3D structure, protein sequence, and the underlying genome through a shared, per-element color code. For each protein that has a structure model, every secondary-structure element (α-helix / β-strand) is detected, assigned a unique color, and that same color is used to paint:
- the residues in the 3D structure (Mol*),
- the residues in the protein sequence track, and
- the exact genomic bases (CDS codons, split across exons) that encode the element, in a purpose-built genome track.
The result: you can see a helix in 3D, find the same-colored stretch in the sequence, and read off the exon(s) and genomic coordinates that encode it — with introns and strand correctly represented.
| File | What it is |
|---|---|
pipeline/genostruct.py |
The data pipeline (generic; no organism/file names hardcoded). |
pipeline/build_viewer.py |
Assembles the self-contained viewer HTML from pipeline output. |
pipeline/genostruct_export.py |
GenBank export of any transcript locus or genomic region, annotated with colored structural features. |
viewer_src/ |
Front-end source: app.js, app.css, template.html. |
genostruct_viewer.html |
The deliverable — one self-contained file, built by the pipeline from your data. |
tests/validate_designs.py |
End-to-end correctness checks for the design pipeline. |
This repository contains the tool only. Pipeline output — viewer/data/,
viewer/structures/, the built genostruct_viewer.html, and anything under
exports/ — is generated from your own genome, annotation and structures, and
is deliberately not committed. Open datasets will be published separately.
Build it from your data, then open genostruct_viewer.html in any modern
browser. No server, no network — Mol*, the decompressor, all structure
coordinates and per-transcript data are embedded (gzip + base64; ~15 MB for 188
transcripts with tag-design annotation).
Pick a transcript from the dropdown. The four stacked panels are color-linked: hovering or clicking an element in any panel highlights it everywhere.
- 3D structure (Mol*) — rotate (drag), zoom (scroll), pan (right-drag). Native Mol* interaction.
- Insertion residue — selecting a design paints the residue(s) the tag exon splices between in magenta, in the 3D structure and in the sequence track, in every color mode and never dimmed.
- Color mode — the button cycles the cartoon through three schemes: per-element colors (matched to the sequence and genome tracks), the AF2 pLDDT confidence ramp, and relative solvent accessibility (buried → exposed), which is what decides whether a tag-insertion junction is on the protein surface.
- Protein sequence — zoomable: use the − / reset / + buttons, or Ctrl/⌘ + scroll over the sequence.
- Drag-select a residue range — dims the rest of the structure, recenters the 3D camera on the selection, and zooms the genome track to the encoding bases. Double-click the sequence to clear.
- Genome track — scroll to zoom (centered on the cursor), drag to pan, double-click to reset to the full locus. A readout shows current zoom factor and window size in bp.
- Show genome 5'→3' / Show protein N→C — toggle the genome-track orientation. By default the track is drawn in the protein's N→C direction (reversed for minus-strand genes); the toggle switches to native 5'→3'.
- Structural elements (legend or sequence) — click an element (a legend chip, a colored sequence block, or a genome feature) to focus it in 3D (that element stays vivid, the rest dims), recenter the camera on it, and zoom the genome track to its bases. Click again or click empty space to clear. Hovering an element highlights it across all three panels.
- Tag knock-in candidates — a ranked table of insertion sites for this transcript, with a separate uniqueness column per nuclease. Selecting a design shows each guide aligned against its off-target sites: sequences in a fixed-width column with mismatched bases in red and the PAM in amber, and the genomic coordinates, mismatch count and score in a separate left-justified column beside them. Click a row to draw its insertion window, both guide pairs and their cut sites in the genome track, zoom to the intron, and highlight the junction residues in the sequence and 3D. The detail block lists each guide's spacer, PAM, GC, heuristic score, identical-site count, distance to the nearest other genomic site and where those sites are, plus the designed cassette sequence. Hide design lane removes the lane from the genome track.
- Search box — type a gene number to filter the transcript list; the full numerically-ordered dropdown stays available.
- Export GenBank (transcript / viewed region) — downloads an annotated GenBank record; the viewed-region export includes the full sequence of the current genome window (see note below).
A short screen recording of the viewer in use — selecting a transcript, focusing structural elements in 3D, and the linked highlighting across the structure, sequence, and genome panels:
Use_genostruct.mp4
(The video plays inline on GitHub. A copy is also committed at docs/Use_genostruct.mp4 for offline/download use.)
The pipeline is generic — nothing about the organism, file names, or gene IDs is hardcoded. It needs three inputs:
- a genome FASTA (
*.fa), - a GFF3 annotation (genes / mRNA / exon / CDS with phase),
- a folder of PDB structure models (e.g. AlphaFold predictions), for any subset of the proteins.
The protein FASTA is derived automatically from the genome + GFF3 (CDS translation, keyed by transcript ID, using the same pure-Python GFF3 parser/translator the pipeline validates its CDS round-trip against — no external tools required) — you no longer need to supply one. If you already have a peptide FASTA you trust (e.g. a curated annotation release), you can still pass it explicitly to skip derivation.
import sys; sys.path.insert(0, "pipeline")
import genostruct
genostruct.build(
genome_fa = "path/to/genome.fa",
gff3 = "path/to/annotation.gff3",
pdb_dir = "path/to/pdbs",
out_dir = "viewer",
# pep_fa is optional — omit it to derive protein.fa from genome+gff automatically.
# pep_fa = "path/to/pep.fa",
# id_regex is optional. By default, each PDB is matched to a transcript by
# SEQUENCE (searching the model's own residues against every candidate
# protein), not by filename — protein/gene names in a PDB filename
# frequently don't correspond to the transcript/gene IDs in a GFF3. Only
# set id_regex if you already know filenames reliably encode the
# transcript id and want to skip sequence search for speed:
# id_regex = r"(.+)_ranked_0", # example for *_ranked_0.pdb
)Or from the CLI:
python pipeline/genostruct.py \
--genome path/to/genome.fa --gff path/to/annotation.gff3 \
--pdb-dir path/to/pdbs --out viewerThis writes viewer/data/<transcript>.json, viewer/data/index.json, and copies
matched PDBs into viewer/structures/.
Then build the standalone HTML:
python pipeline/build_viewer.py \
--out viewer \
--molstar molstar.js --molstar-css molstar.css --pako pako_inflate.min.js \
--template viewer_src/template.html \
--app-js viewer_src/app.js --app-css viewer_src/app.css \
--html viewer/genostruct_viewer.html(molstar.js, molstar.css, pako_inflate.min.js are bundled here; they are the
only third-party assets and are embedded into the output.)
app.py is a Shiny for Python app that wraps the
pipeline end to end: upload a genome FASTA, a GFF3, and a folder of PDB models,
click Build viewer, and the app runs genostruct.build() (deriving
protein.fa automatically, no external tools) + build_viewer.build_html()
server-side, then renders the resulting self-contained viewer inline (with a
download button for the generated HTML).
Run locally:
pip install -r requirements.txt
shiny run app.pyDeploy to shinyapps.io:
pip install rsconnect-python
rsconnect deploy shiny . --name <account> --title genostruct-viewerEverything the app needs (shiny, numpy, biotite) is pip-installable —
there's no system binary dependency to configure on the deploy target.
genostruct_export.py produces a GenBank record — with genomic sequence — for
either a single transcript's locus or an arbitrary region. Structural elements are
written as misc_features whose location is the exact codon ranges (as
join(...), complement(...) for minus strand), carrying:
/label=transcript:element(e.g.g113516.t1-00001:H2),/note= element kind + protein residue range,/color= the viewer hex color (e.g.#e6194B),/colour= Artemis/Geneious-styleR G B(for tools that read that convention),/structure_type=helixorstrand.
# a single transcript's locus, with 200 bp flanks
python pipeline/genostruct_export.py \
--genome genome.fa --data-dir viewer/data \
--transcript g113516.t1-00001 --flank 200 \
--species "Acyrthosiphon pisum" --assembly JIC1_v1.0 \
--out exports/g113516_locus.gb
# an arbitrary region (all overlapping transcripts included)
python pipeline/genostruct_export.py \
--genome genome.fa --data-dir viewer/data \
--region scaffold_2:89950000-90000000 \
--out exports/region.gbThe Export GenBank button in the viewer produces a lightweight feature-table-only
version (no sequence — kept out of the browser to hold file size down). For a
complete record with sequence, use genostruct_export.py.
Given the same inputs the viewer uses, the pipeline will nominate introns where an artificial exon encoding an epitope tag can be knocked in by NHEJ, design the cassette, and find the overlapping gRNA pairs needed to cut there — one pair for Cas9 and, independently, one for Cas12a. Everything is annotated in the exported GenBank.
The premise: an artificial exon spliced into an internal intron places the tag at the corresponding exon–exon junction of the protein, which is tolerated when that junction sits in a solvent-exposed, unstructured loop on the outside of the fold.
# rank candidate introns, design cassettes, find overlapping gRNA pairs
python pipeline/tagdesign.py \
--data-dir viewer/data --structures viewer/structures \
--tags ALFA,HiBiT --out-tsv exports/tag_designs.tsv \
--out-json-dir viewer/data_tagged
# then export an annotated GenBank for any transcript
python pipeline/genostruct_export.py \
--genome genome.fa --data-dir viewer/data_tagged \
--transcript g119212.t1-00001 --flank 200 --max-designs 2 \
--out exports/g119212_tag_design.gb| Module | What it does |
|---|---|
pipeline/introns.py |
Coding introns in transcript orientation; splice dinucleotides; intron phase (from cumulative coding length, cross-checked against the GFF3 phase column); the NHEJ insertion-safe window. |
pipeline/surface.py |
Per-residue SASA / relative solvent accessibility (Shrake–Rupley in numpy — no new dependency). |
pipeline/tagsites.py |
Scores each intron's exon–exon junction on loop flanks (below), exposure, clearance from secondary structure and distance from the termini. |
pipeline/cassette.py |
Designs the artificial exon and validates it by in-silico splicing. |
pipeline/grna.py |
Cas9 / Cas12a site finding on both strands, and overlapping-pair detection. |
pipeline/tagdesign.py |
Drives all of the above; writes ranked TSV + augmented JSON. |
tests/validate_designs.py |
Independent end-to-end correctness checks. |
The artificial exon is spliced between the last residue the upstream exon encodes and the first residue the downstream exon encodes. For the tag to sit inside a surface loop, both of those must be coil, and stay coil for a few residues.
Averaging coil content over a window around the junction is not sufficient — it passes sites where one flank is a long loop and the other is a helix, so the tag lands at the edge of the loop, hard against secondary structure:
upstream exon | downstream exon verdict
I3 aaaacccc | cccaaaaa 4/4 loop both sides accepted
I8 accccccc | caaaaaaa 7/2 end of a loop rejected
I9 aaaaaaac | cccaaaaa 1/4 start of a loop rejected
I14 cccccccc | aaaaaaaa 19/1 loop then helix rejected
So tagsites.flank_coil_runs() counts the consecutive coil residues running
back into the upstream exon and forward into the downstream exon
separately. Each run is confined twice over:
- to the residues its own exon encodes, so a loop in a neighbouring exon cannot be borrowed across an intron;
- to the residues the structure model actually covers.
residue_ssedefaults to coil wherever the model does not reach (a trimmed signal peptide, say), so counting those would manufacture a long "loop" out of absent evidence — precisely where a tag could land in structure nobody looked at.
Both runs must reach --min-flank-coil (default 3);
--allow-nonloop-flanks relaxes it.
This is a hard gate, and a strict one: on the bundled dataset it rejects
1724 of 2247 otherwise-viable junctions, leaving 490 designs whose tags are
genuinely interior to a loop that was actually modelled. Both runs are reported in the TSV
(upstream_coil_run, downstream_coil_run), in the viewer's design table, and
drive the unstructured subscore.
A cassette is spliced in using its own acceptor and donor, while the native
donor and acceptor keep serving the flanking exons. So the break must avoid the
native signals: by default the first 30 bp of the intron (5′ donor) and the last
60 bp (branch point, polypyrimidine tract, 3′ acceptor) are excluded
(--donor-pad, --acceptor-pad). Guides are only reported when their whole
footprint lies inside that window.
With intron phase p (nucleotides of the junction codon contributed by the
upstream exon), the exon body is pad5 + tag codons + pad3 where
pad5 = (3-p) % 3 and pad3 = p % 3, so the body is always a multiple of 3 and
the downstream exon stays in frame. For a split junction (p ≠ 0) the padding
reconstitutes the junction codon from the flanking exons' own bases, so both
resulting codons are guaranteed to be sense codons and the native residue is
duplicated rather than mutated. Phase is not used to rank sites.
Introns at or past the stop codon are refused — a cassette spliced in there is never translated.
Both species-dependent inputs are learned from the supplied annotation:
- codon usage, counted over every CDS, used to encode the tag;
- splice-site consensus, as 5′/3′ position weight matrices over every annotated intron, used to build the synthetic acceptor and donor.
On the bundled A. pisum data that recovers AAG|GTAAGTA (1.96 bits on the
donor GT), a long T-rich polypyrimidine tract before AG (1.99 bits), and
strongly AT-biased codon usage — all correct for this genome, with no species
assumption anywhere in the code.
Cutting is much more efficient when two guides target overlapping sites, so the unit of output is a pair whose protospacer+PAM footprints overlap.
| Nuclease | PAM | Spacer | Cut |
|---|---|---|---|
| SpCas9 | NGG, 3′ of the spacer |
20 nt | blunt, 3 bp 5′ of the PAM |
| Cas12a (As/Lb) | TTTV, 5′ of the spacer |
23 nt | staggered, 5-nt 5′ overhang |
Pairs are found independently per nuclease, and either nuclease is
sufficient — a site with an overlapping Cas9 pair but no Cas12a pair (or vice
versa) is a perfectly usable design and is kept and scored on its own merits,
not penalised. A design is only rejected when neither nuclease has an
overlapping pair, since then there is no way to make the break.
nucleases_with_pairs records which apply; --require-both-nucleases opts in
to the stricter rule.
Tunable: --min-overlap, --max-cut-separation, --opposite-strands,
--min-efficiency.
On-target efficiency is a labelled heuristic. No published trained scorer (Doench Rule Set 2/3, Azimuth, DeepCpf1) is installable as a pure-Python offline dependency — they ship as R/Bioconductor wrappers over Keras — and this project deploys without system binaries. The default scorer therefore combines only well-established, checkable determinants (GC optimum, poly-T terminator, homopolymers, hairpin propensity, seed composition, PAM-proximal base) and reports every term separately. It is a relative ranking aid, not a calibrated activity prediction, and is labelled
heuristic_v1wherever it appears. Register a real trained model withgrna.register_scorer()to replace it.
On this dataset corr(window pLDDT, junction RSA) = −0.33: AlphaFold draws
low-confidence regions as extended chain, which inflates apparent solvent
exposure. Rewarding "flexible" (low pLDDT) and "exposed" together would count
one artefact twice and flood the ranking with the least trustworthy geometry.
Low pLDDT is also genuinely ambiguous — a good predictor of intrinsic disorder
(a real positive for an internal tag) and a warning that the RSA and
secondary-structure calls there are unreliable. So pLDDT becomes a reported
site_class (ordered_loop / intermediate / disordered) that you filter
on with --site-class, rather than a hidden term in the score.
| Feature | Meaning | Colour |
|---|---|---|
intron |
candidate intron, with phase, length, splice class, site score | grey |
misc_feature :insertion_window |
the NHEJ-safe interval | pale green |
misc_feature :insertion_point |
preferred insertion coordinate; carries the full cassette sequence in /cassette |
dark green |
misc_feature :junction_resN |
the junction codon(s), as join() of their actual bases |
purple |
protein_bind |
each guide's protospacer, on its own strand, with sequence, PAM, GC, efficiency and pair overlap | blue (Cas9) / orange (Cas12a) |
misc_feature _PAM / _cut |
the PAM, and the blunt cut or staggered overhang | light / red |
pipeline/offtarget.py compares the whole protospacer plus its PAM against
every PAM-adjacent site in the assembly. The search is exhaustive up to a
mismatch budget, by enumeration rather than seeding: every sequence within
--max-mismatches of a guide is generated (32,551 variants for a 20-mer at ≤3),
2-bit encoded and sorted once, then the genome is streamed and each site matched
with a vectorised searchsorted. Guides are batched so the variant index fits
in memory; the bundled 526 Mb assembly takes a couple of minutes, cached by
genome size, mtime and format version.
This matters: a seed-indexed search (match the 12-nt PAM-proximal seed exactly, then count mismatches) cannot see an off-target that differs inside the seed. Switching to the exhaustive search on this dataset cut the guides called unique from ~55% to ~25% — the earlier figure was an artefact of an incomplete search, not a property of the guides.
A mismatch next to the PAM largely abolishes cutting; one at the distal end is often tolerated. So each hit carries a weighted score rather than a bare mismatch count:
w(position) = W_MIN + (W_MAX - W_MIN) * proximity_to_PAM ** W_EXP
hit_score = PROD (1 - w_i) over mismatched positions i
A single mismatch beside the PAM gives 0.05 (very unlikely to cut); one at
the far end gives 0.80 (likely still cuts). Correctly inverted for Cas12a,
whose PAM is 5′. Per guide, specificity = 1 / (1 + Σ hit_score) over all other
sites, so 1.0 means nothing else was found.
Heuristic, not a fitted model. This has the same shape as published specificity models (CFD/MIT) but not their fitted values — no published coefficient table is reproduced here from memory. Install a measured table with
offtarget.set_position_weights(nuclease, weights)to replace it.
Each guide reports:
| field | meaning |
|---|---|
additional_identical_sites |
other places the protospacer occurs exactly with a valid PAM. 0 for a unique guide — the guide's own locus is not counted |
found_own_locus |
whether the guide was located at all (false means the spacer is not in this assembly) |
nearest_mismatches |
mismatches to the closest other site, or none if nothing within the budget |
specificity |
1 / (1 + Σ hit_score) over every other site; 1.0 = clean |
worst_hit_score |
the most plausible single off-target |
total_other_sites |
how many other sites exist within the budget |
hits / closest |
each site's sequence and PAM, scaffold:start-end(strand, Nmm), the mismatched positions and that site's hit_score, most dangerous first |
These appear in the TSV, in the viewer (genome-track tooltips and the design
table) and in the GenBank as /additional_identical_sites,
/nearest_other_site and /closest_offtargets.
Scope. Sites differing by more than
--max-mismatches(default 3) are not reported.nearest_mismatches = nonemeans nothing within that budget, not genome-wide uniqueness. Raising to 4 costs ~13× more variants per guide (425k) and is best run on a shortlist.
Not a scoring artefact — the BICYCLE family is paralogous:
- Only 944 of 1762 candidate spacers have no additional identical site. The rest occur perfectly elsewhere — some in repeats, with up to 7205 extra copies. That is fatal regardless of weighting.
- Uniqueness is reported per nuclease, because a site can be unusable with Cas9 and clean with Cas12a. 118 of 490 designs have a fully unique pair for at least one nuclease (Cas12a 96, Cas9 43), against only 31 where every nuclease present is clean — so a single combined flag would have hidden 87 usable designs.
Two different questions are reported separately rather than collapsed into one "unique" flag, because they mean different things:
| shown | meaning |
|---|---|
additional identical sites |
the protospacer occurs again exactly. Fatal. |
worst off-target score |
no exact duplicate, but the best mismatched site is plausible enough to cut |
In the viewer each nuclease column shows duplicates · worst score, green when
neither applies, amber when there is no duplicate but a plausible mismatch site,
red when an exact duplicate exists.
Use --require-unique to keep only those, and sort on specificity rather
than treating uniqueness as binary.
tests/validate_designs.py re-derives every claim independently of the code that
made it. On the bundled dataset, all pass:
- 2396 introns — cumulative-length phase == GFF3 phase column; windows strictly inside their intron
- 2393 flank coil runs — recounted straight off the secondary-structure string, confined to the correct exon and to the modelled region
- 2393 cassettes — frame preserved, padding matches phase, native protein preserved on both sides, no internal stop, both tags in frame
- 147613 gRNA sites — spacer and PAM re-extracted from their genomic coordinates, PAM motif valid, footprint size, overhang length, PAM adjacency
- 1787 pairs — reported overlap exact, guides distinct, cuts clear of the native splice signals
- GenBank round-trip — each annotated guide feature extracts back to its stated spacer
- off-target search — identical-site, per-mismatch and total counts all recompared against brute force on a miniature genome; every located hit re-extracted to confirm it differs at exactly the stated positions; scores bounded; PAM-proximal mismatches verified to score below distal ones
python3 tests/validate_designs.py --data-dir viewer/data \
--genbank exports/g119212_tag_design.gb- Secondary structure is computed from the model's Cα coordinates with
biotite's
annotate_sse(the P-SEA algorithm), then segmented into contiguous helix/strand elements. Each element gets a unique color from a warm palette (helices) or cool palette (strands), cycled if there are many. - Residue → genome mapping walks each CDS block with its GFF phase, in translation order, on the correct strand, so every protein residue maps to its three genomic codon positions — correctly handling introns and codons that straddle exon boundaries.
- Model ↔ protein alignment. AlphaFold models here were predicted from signal-peptide-trimmed sequences and are numbered from 1, so each model is aligned to its full protein to recover an N-terminal offset (exact substring → gapless scan → local alignment fallback). Colors are indexed by full-protein residue, and the Mol* color theme applies the offset so 3D coloring lines up with the sequence and genome.
- Coordinate correctness: for all 187 transcripts, the exported CDS translates
back to the exact deposited protein (187/187), and extracting + translating
every structural-element
misc_featurereproduces its protein residues exactly (1113/1113 elements, 187/187 transcripts fully correct — no CDS mismatches, no out-of-range elements, no empty ranges). - Element residue ranges are clamped to the coding protein length in the pipeline, so models that are longer than their protein (from a gapped local alignment) can no longer map an element past the C-terminus into the stop codon or out-of-range codon positions.
validation_colorlink.pngvisually confirms sequence-color → genome-segment correspondence, including multi-exon elements on the minus strand.
- 3 PDB models in the source set correspond to stale isoform IDs absent from the protein FASTA and are excluded (documented in the pipeline output warnings).
- 10 models differ from their deposited protein by internal indels and are placed by local alignment (identity ≥ 0.6); their per-residue mapping is still validated correct by the CDS/element round-trip above.
- The viewer runs entirely offline in a single self-contained HTML file. If it is opened inside a sandboxed iframe with a restrictive Content-Security-Policy (some hosted environments do this), the file-download of a GenBank export may be blocked by the sandbox; a copy-to-clipboard / open-in-new-tab fallback is provided for that case. Opened directly in a browser (double-clicking the file, or served from a normal web server), downloads work normally.