Scripts for mapping and analyzing seqlets from TF-MoDISco-lite.
The scripts map clustered seqlets back to genome coordinates, compare sequence and attribution within patterns and subpatterns, count how many seqlets are retained at each stage, and export motif reports as PDF or self-contained HTML.
| Task | Script |
|---|---|
| Map clustered seqlets to genome coordinates | build_seqlet_annotation.py |
| Count candidate and retained seqlets | count_leftover_seqlets.py |
| Explore pattern and subpattern heterogeneity | seqlet_viz.py |
| Create a standalone motif report | upgrade_modisco_report.py |
| Run tests | tests/ |
For example plots, see Visualizing the clusters. For interpretation, see seqlet retention limits. Replace the example filenames below with the corresponding files from your MoDISco run.
This is a collection of research scripts. Back-annotation was the first command tested.
python -m pip install -r requirements.txtFor local development:
python -m pip install -r requirements-dev.txt
python -m pytesthdf5plugin is required for many production MoDISco HDF5 files because they may use compressed HDF5 filters. The small synthetic tests do not require compressed HDF5.
The seqlet-counting script additionally requires modiscolite, so run it in the same ChromBPNet/MoDISco environment or Docker image used to generate the MoDISco output.
Coordinates are BED-style: 0-based, half-open intervals. With --genome, the script samples seqlets across patterns, subpatterns, and strands and checks stored sequences against the reference FASTA before writing the annotation table.
python build_seqlet_annotation.py \
--modisco GC_modisco_profile_v2.h5 \
--bed GC_mm10.interpreted_regions.bed \
--output GC_profile_seqlet_annotation \
--window 1000 \
--input-len 2114 \
--genome mm10.fapython count_leftover_seqlets.py \
--scores GC_mm10.profile_scores.h5 \
--modisco GC_modisco_profile_v2.h5 \
--window 1000 \
-n 100000This reports candidate seqlets, positive/negative threshold survivors, an
estimated upper bound on clustering input after the -n cap, and seqlets saved
in final patterns. It does not rerun clustering.
# Atlas across all parent patterns. Keep the cap low on laptops.
python seqlet_viz.py atlas \
--modisco GC_modisco_profile_v2.h5 \
--per-pattern-cap 75 \
--output umap_all_patterns.png
# Subpatterns inside one parent pattern.
python seqlet_viz.py subpattern \
--modisco GC_modisco_profile_v2.h5 \
--arm pos_patterns \
--pattern pattern_0 \
--max-seqlets 3000 \
--output umap_pattern0_subpatterns.png
# Sequence-vs-attribution UMAP panels.
python seqlet_viz.py design1 \
--modisco GC_modisco_profile_v2.h5 \
--pattern pattern_0 \
--output design1_pattern0.png
# Interpretable center-similarity scatter.
python seqlet_viz.py design2 \
--modisco GC_modisco_profile_v2.h5 \
--pattern pattern_0 \
--output design2_pattern0.pngThe UMAP visualizations use a pairwise Continuous Jaccard matrix, so memory and
time scale as O(N²). Reduce --max-seqlets or --per-pattern-cap before
running large plots locally.
The original MoDISco HTML report can reference images by relative paths, so images may disappear if the HTML is moved away from its report image directory. Generate a standalone PDF while the HTML is still next to its image files:
python upgrade_modisco_report.py \
--report profile_report/motifs.html \
--h5 GC_modisco_profile_v2.h5 \
--meme JASPAR2024_CORE_non-redundant_pfms_meme.txt \
--out-pdf motifs_upgraded.pdfThe PDF embeds the original report images that can be resolved from the HTML, plus regenerated forward/reverse PFM logos from the MoDISco H5 file.
To also write self-contained HTML with base64-embedded images:
python upgrade_modisco_report.py \
--report profile_report/motifs.html \
--h5 GC_modisco_profile_v2.h5 \
--meme JASPAR2024_CORE_non-redundant_pfms_meme.txt \
--out-html motifs_upgraded.html \
--embed-html-imagesOn the GCP VM, run back-annotation with the exact files used for the MoDISco
run. The key check is that --genome verification passes before outputs are
trusted:
cd /home/jupyter/GC_chrombpnet/GC_contribs
python /path/to/Modisco_Annotation/build_seqlet_annotation.py \
--modisco GC_modisco_profile_v2.h5 \
--bed GC_mm10.interpreted_regions.bed \
--output GC_profile_seqlet_annotation \
--window 1000 \
--input-len 2114 \
--genome /home/jupyter/GC_chrombpnet/data/downloads/mm10.faExpected behavior:
- the script reports
MATCH=Truefor sampled FASTA checks; - it writes
GC_profile_seqlet_annotation.bed; - it writes
GC_profile_seqlet_annotation.csv; - the annotated seqlet count should match the final clustered seqlet total.
Each point is one seqlet. Seqlets are embedded by similarity, so points that sit close together are alike.
UMAP of all Patterns — different motifs form separate islands:
UMAP of the Subpatterns inside one Pattern — reveals the finer structure Modisco found within a single cluster:
A Pattern can vary in two independent ways: its sequence (which bases are present) and its attribution (how much the model weights each base). Each seqlet therefore has two feature vectors:
- a sequence vector — one-hot encoded (1 for the base present, 0 otherwise)
- an attribution vector — the model's per-base contribution scores
Both plots below compare these two views. To compare seqlets we use Continuous Jaccard similarity, the same metric Modisco uses internally — it is sign-aware and robust to overall magnitude, so it captures the shape of a seqlet rather than its raw scale.
Two UMAP embeddings of the same seqlets — one built from the sequence vectors (left), one from the attribution vectors (right). Each point is the same seqlet in both panels, colored identically.
- Left — how seqlets group by sequence.
- Right — how seqlets group by attribution.
When a group is tight on one side but spread on the other, sequence and attribution disagree — for example, same sequence, different grammar.
Note: UMAP preserves which points are neighbors, but distances between separated blobs are not meaningful — read groupings, not gaps.
This asks the same question on a single pair of axes that have direct meaning (unlike UMAP axes, which are abstract). We first compute the cluster center for each view — the average of all seqlet vectors in the Pattern, giving one "typical sequence" and one "typical attribution profile." Then, for every seqlet, we measure its Continuous Jaccard similarity to that center:
- X axis — sequence similarity to the cluster center (how typical this seqlet's sequence is).
- Y axis — attribution similarity to the cluster center (how typical this seqlet's attribution is).
Each seqlet becomes one point, and the corners are interpretable:
- top-right — typical in both: the canonical motif.
- bottom-right — typical sequence, atypical attribution: same sequence, the model treats it differently.
- top-left — atypical sequence, typical attribution: different sequence, same grammar.
- bottom-left — atypical in both.
The dashed lines mark the median of each axis, splitting the plot into these four regions.
MoDISco builds patterns from only a fraction of the seqlets it identifies.
MoDISco runs in stages, and seqlets are lost at each one:
- Seqlet identification scans every position in the motif-discovery window
and flags windows whose importance passes a (deliberately lenient) FDR
threshold (
target_fdr=0.2by default). - Metacluster cap — each metacluster is capped at
max_seqlets_per_metacluster(the-nflag; library default 20,000). Only the strongest-attribution seqlets enter clustering; the rest are discarded before clustering begins. - Clustering drops more (noise filtering, clusters too small to survive).
The cap is not discussed in the MoDISco README or technical note — it exists only as a settable parameter. It is a reasonable engineering choice (clustering millions of seqlets is infeasible) and it keeps the strongest seqlets, not random ones. But a user reading the documentation would not know that most of their signal was set aside.
A ChromBPNet profile-head model on mouse cerebellar granule cells
(158,710 peak regions), run with -w 1000 -n 100000:
| Stage | Seqlets | Note |
|---|---|---|
| Candidate seqlets identified | 478,887 | ~3.0 per 1 kb window |
| Survived pos/neg threshold | 478,887 | threshold dropped 0 here |
| Assigned to final patterns | 62,767 | ~13% of candidates |
In this run the importance threshold removed nothing — every candidate was confidently positive or negative. The entire gap from 478,887 down to 62,767 is the 100,000-per-metacluster cap plus clustering: there were 418,064 positive candidates, but only the top 100,000 by strength entered clustering, and clustering reduced those further.
Note: the candidate count is sensitive to the
-w(window) parameter. This run used-w 1000, so extraction scanned only the central 1 kb of each 2114 bp input. Counting on the full 2114 bp window instead gives ~3.6M candidates — an over-count that does not reflect what the real run saw. Always match-w.
MoDISco discards seqlets at every stage. Here is the full accounting for one
ChromBPNet profile-head run (mouse cerebellar granule cells, 158,710 peaks,
-w 1000 -n 100000):
| Stage | Seqlets | What happens |
|---|---|---|
| Candidates identified | 478,887 | windows passing the lenient FDR (~3 per 1 kb region) |
| Survived pos/neg threshold | 478,887 | split by sign; here the threshold dropped 0 |
| → positive metacluster | 418,064 | |
| → negative metacluster | 60,823 | |
| Entered clustering | ~160,823 | top 100,000 positive (cap) + all 60,823 negative |
| Assigned to final patterns | 62,767 | survived clustering into 53 patterns |
Seqlets are removed at two stages:
1. The metacluster cap (the largest loss).
Each metacluster is capped at -n (here 100,000). The positive metacluster had
418,064 seqlets but only the top 100,000 by attribution strength entered
clustering — the other ~318,000 were discarded before clustering began. The
negative metacluster (60,823) was under the cap, so all of it entered.
2. Clustering-stage quality filters (~98,000 dropped). Of the ~160,823 seqlets that entered clustering, only 62,767 survived. The ~98,000 difference is removed by three quality filters inside MoDISco:
- noise filtering — seqlets whose coarse and fine similarity disagree (look like noise, no consistent neighbors) are discarded;
- small-cluster disbanding — clusters below the minimum size are broken up, and their seqlets dropped unless similar enough to a surviving pattern;
- low-information-content filtering — whole patterns with too little sequence information are removed.
These three are quality filters — the seqlets entered clustering but never formed or joined a clean motif. Their individual counts are not saved by MoDISco, so only the combined ~98,000 is measurable; the per-cause breakdown would require re-running clustering with added logging.
Of 478,887 confidently-important candidate seqlets, only 62,767 (~13%) end up in final patterns. The loss is dominated by the metacluster cap (a tractability limit, not a biological one) and clustering-stage quality filters. MoDISco patterns therefore describe the strongest, cleanest, most common motifs — not the full set of important sequence in the genome.
The ~3 candidate seqlets per region are a mix of real motif instances and weaker-importance windows. Seqlets below the cap have lower attribution scores, but some may still represent regulatory elements, such as rarer motifs or weaker binding sites. These are excluded because they fall outside the top 100,000 by attribution strength.
Seqlets that pass the importance threshold but never enter clustering need further analysis. Possible approaches include:
- cluster the discarded seqlets separately, in batches, rather than capping;
- match weak seqlets against the strong patterns already found, to rescue weak instances of known motifs;
- raise or remove the cap with a more scalable clustering backend.
Until then, any biological claim from a MoDISco run should be read as a statement about the dominant motifs, explicitly not about the full set of important sequence in the genome.
(The script count_leftover_seqlets.py reproduces this funnel for any
modisco-lite run — pass the matching --window.)