Single-cell ATAC pseudobulk
TL;DR: pseudobulk outside JBrowse, pooling each cluster's cells into one coverage BigWig, then load the whole set as a single MultiWiggle track, which draws one row per file.
Prerequisites
- cells already clustered and labeled: either a fragments file (or a barcoded
BAM) plus a barcode-to-label table, or the project object your analysis tool
already holds, an
AnnDatain SnapATAC2 (Python), anArchRProjectin ArchR, or a Seurat/Signac object in R - the pseudobulk tool that follows from whichever of those you have:
pip install snapatac2,pip install deeptools sinto, orbedGraphToBigWigfor the fragments-file route (ArchR and Signac install from R) - a JBrowse instance to load the finished BigWigs into (see the
web quickstart, or the
desktop quickstart, which opens local
.bwfiles with nothing hosted)
Where the data comes from
SnapATAC2's annotated release of the 10x 5k PBMC scATAC dataset, already clustered and cell-type-labeled by that tool's own pipeline.
- the annotated
AnnDatathatsnap.datasets.pbmc5k(type="annotated_h5ad")downloads and caches: https://scverse.org/SnapATAC2/api/_autosummary/snapatac2.datasets.pbmc5k.html - CATlas' published hg38 per-cell-type accessibility BigWigs from: https://decoder-genetics.wustl.edu/catlasv1/humanenhancer/data/bw/
Pooling cells into rows
One ATAC cell contributes only a few thousand fragments, so a single cell's coverage track is almost entirely zero. Pseudobulking pools every fragment belonging to a label into one profile, a dense track resembling a bulk ATAC experiment on that cell type. JBrowse stacks the files as rows of one track.
PBMC markers are the check: at a T-cell marker the T-cell rows carry the signal, and at a B-cell marker the B-cell rows light up.
The BigWigs can also be viewed inline from the clustering environment through the Python anywidget interface or JBrowseR.
Generating per-group BigWigs
Clustering and cell-type labeling stay upstream, in Cell Ranger ATAC, ArchR, Signac, or SnapATAC2. Two settings decide whether the rows can be compared, whichever tool writes them:
- Normalization. Groups differ in cell count and total fragments, so each track needs normalizing (CPM / RPKM, or per-cell-count) for a peak's height to mean accessibility
- Bin size, which trades resolution against file size. The bin has to stay
well inside one peak;
export_coveragebelow uses 25 bp
SnapATAC2's export_coverage splits cells by a metadata column and writes one
normalized BigWig per group in a single call:
import snapatac2 as snap
# adata: an AnnData with fragments imported and a cell-type/cluster label in obs
snap.ex.export_coverage(
adata,
groupby="cell_type", # column in adata.obs to split on
bin_size=25, # bp per bin
normalization="RPKM", # comparable across groups
out_dir="bw",
suffix=".bw",
n_jobs=2, # each worker holds a genome-wide coverage vector
# blacklist= takes an ENCODE blacklist BED and drops those intervals from
# every group. The build script does not pass it, so the figures below are
# unmasked coverage.
)
# writes bw/<cell_type>.bw, one per group, keyed by group in the returned dict
n_jobs is a memory knob: each worker holds a genome-wide coverage vector, and
the writer dies partway through the groups when memory runs out. groupby picks
the rows: the cluster column ("leiden") for one per cluster, or the annotated
column ("cell_type") for one per cell type.
Other starting points
Every route ends at one .bw per cell type. The tools are linked under
References:
- An
ArchRProject:getGroupBW(groupBy = "CellType", tileSize = 25)writes one BigWig per group.normMethod = "ReadsInTSS"normalizes by signal-in-TSS;"nCells"and"nFrags"are the alternatives - A barcoded BAM (Cell Ranger ATAC, or a Signac workflow's input): split it
by label with
sinto filterbarcodes, then run deepToolsbamCoverageon each with--binSize 25 --normalizeUsing CPM --extendReads.RPGCalso needs--effectiveGenomeSize - A
fragments.tsv.gzalone: filter it to each cluster's barcodes, thenbedtools genomecov -bgandbedGraphToBigWigper group. This route is unnormalized, so scale each group yourself (1e6 / total fragments for CPM) before the conversion
Loading the BigWigs as a MultiWiggle track
All the per-cell-type BigWigs go into one MultiQuantitativeTrack whose
MultiWiggleAdapter holds one BigWigAdapter per file, each with a name, an
optional color, and an optional group. assemblyNames names an assembly
already configured (see the
assemblies configuration guide). Three cell
types:
{
"type": "MultiQuantitativeTrack",
"trackId": "scatac_pseudobulk",
"name": "scATAC by cell type",
"category": ["Single cell", "Chromatin accessibility"],
"assemblyNames": ["hg38"],
"adapter": {
"type": "MultiWiggleAdapter",
"subadapters": [
{
"type": "BigWigAdapter",
"name": "CD8 Naive",
"group": "T cell",
"color": "#4363d8",
"uri": "https://example.com/bw/CD8_Naive.bw"
},
{
"type": "BigWigAdapter",
"name": "CD8 Memory",
"group": "T cell",
"color": "#3cb44b",
"uri": "https://example.com/bw/CD8_Memory.bw"
},
{
"type": "BigWigAdapter",
"name": "Naive B",
"group": "B cell",
"color": "#f58231",
"uri": "https://example.com/bw/Naive_B.bw"
}
]
}
}
jbrowse add-track-json '{
"type": "MultiQuantitativeTrack",
"trackId": "scatac_pseudobulk",
"name": "scATAC by cell type",
"category": ["Single cell", "Chromatin accessibility"],
"assemblyNames": ["hg38"],
"adapter": {
"type": "MultiWiggleAdapter",
"subadapters": [
{
"type": "BigWigAdapter",
"name": "CD8 Naive",
"group": "T cell",
"color": "#4363d8",
"uri": "https://example.com/bw/CD8_Naive.bw"
},
{
"type": "BigWigAdapter",
"name": "CD8 Memory",
"group": "T cell",
"color": "#3cb44b",
"uri": "https://example.com/bw/CD8_Memory.bw"
},
{
"type": "BigWigAdapter",
"name": "Naive B",
"group": "B cell",
"color": "#f58231",
"uri": "https://example.com/bw/Naive_B.bw"
}
]
}
}'
In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "MultiQuantitativeTrack",
"trackId": "scatac_pseudobulk",
"name": "scATAC by cell type",
"category": ["Single cell", "Chromatin accessibility"],
"assemblyNames": ["hg38"],
"adapter": {
"type": "MultiWiggleAdapter",
"subadapters": [
{
"type": "BigWigAdapter",
"name": "CD8 Naive",
"group": "T cell",
"color": "#4363d8",
"uri": "https://example.com/bw/CD8_Naive.bw"
},
{
"type": "BigWigAdapter",
"name": "CD8 Memory",
"group": "T cell",
"color": "#3cb44b",
"uri": "https://example.com/bw/CD8_Memory.bw"
},
{
"type": "BigWigAdapter",
"name": "Naive B",
"group": "B cell",
"color": "#f58231",
"uri": "https://example.com/bw/Naive_B.bw"
}
]
}
}
Three things in that list are worth writing by hand:
- Order. Subadapters draw in the order given, so group them by lineage
color. Take each row's from the cluster's color in your analysis, so a cell type matches its UMAP colorgroup. What the sidebar tree branches on, and what Clustering rows reorders
Without per-row names, colors or groups, the bigWigs shorthand takes a plain
array of URLs and labels each row from its filename:
{
"type": "MultiQuantitativeTrack",
"trackId": "scatac_pseudobulk_simple",
"name": "scATAC pseudobulk",
"assemblyNames": ["hg38"],
"adapter": {
"type": "MultiWiggleAdapter",
"bigWigs": [
"https://example.com/bw/CD8_Naive.bw",
"https://example.com/bw/CD8_Memory.bw",
"https://example.com/bw/Naive_B.bw"
]
}
}
jbrowse add-track-json '{
"type": "MultiQuantitativeTrack",
"trackId": "scatac_pseudobulk_simple",
"name": "scATAC pseudobulk",
"assemblyNames": ["hg38"],
"adapter": {
"type": "MultiWiggleAdapter",
"bigWigs": [
"https://example.com/bw/CD8_Naive.bw",
"https://example.com/bw/CD8_Memory.bw",
"https://example.com/bw/Naive_B.bw"
]
}
}'
In JBrowse Desktop, or in any running JBrowse Web session, open a view on this track’s assembly, then File → Open track..., choose Add track from pasted JSON, and paste:
{
"type": "MultiQuantitativeTrack",
"trackId": "scatac_pseudobulk_simple",
"name": "scATAC pseudobulk",
"assemblyNames": ["hg38"],
"adapter": {
"type": "MultiWiggleAdapter",
"bigWigs": [
"https://example.com/bw/CD8_Naive.bw",
"https://example.com/bw/CD8_Memory.bw",
"https://example.com/bw/Naive_B.bw"
]
}
}
A published atlas needs no pipeline at all: CATlas
serves hg38 coverage from
https://decoder-genetics.wustl.edu/catlasv1/humanenhancer/data/bw/, one file
per cell type. Percent-encode the + in a cell-type name
(T_lymphocyte_2_CD4%2B.bw), the one way those URLs go wrong quietly.
defaultRendering
lists every drawing mode, and the track menu switches between them live.
multirowxy (the default, and the figures here) compares peak shape;
multirowdensity maps score to color and fits more rows.
User guide: Multi-quantitative track covers the rest of the menu.
Building the subadapter list from files
Two workflows write the list from a set of files.
"Add multi-wiggle track", in the "Add track" workflow, takes BigWig URLs one per
line, or a JSON array of subadapter objects. Exporting the session gets the JSON
config back out. On JBrowse Desktop it reads local .bw files directly.
jbrowse add-track --multiwig takes the whole set of BigWigs in place of the
single positional file, labeling rows from the filenames:
jbrowse add-track --multiwig "$(find bw -name '*.bw' | sort | paste -sd,)" \
--name "scATAC by cell type" --assemblyNames hg38 \
--load copy --subDir bw --out /var/www/html/jbrowse2
--load copy --subDir bw copies local files in beside config.json; both drop
out for BigWigs already served over HTTP. For per-row names, colors and groups,
pass a .json file of subadapter objects instead of the comma list.
Reproduce it end to end
One script runs the whole path,
build_scatac_pseudobulk.sh:
curl -fO https://raw.githubusercontent.com/GMOD/jbrowse-components/main/scripts/build_scatac_pseudobulk.sh
bash build_scatac_pseudobulk.sh # builds ./scatac_pseudobulk_build
npx --yes serve scatac_pseudobulk_build/jbrowse2
Its input is SnapATAC2's annotated release of the 10x 5k-PBMC dataset, what that
tool's standard pipeline
and
cell-type annotation
tutorials produce: per-barcode fragments alongside an obs["cell_type"] call.
The script's own work:
export_coverage(groupby="cell_type", bin_size=25, normalization="RPKM"), one BigWig per cell type intobw/- a
sources.jsonof subadapters, taking each row's color from the same object and itsgroupand position from a lineage map the script states outright. Running it on your own experiment means replacing that map jbrowse createplusadd-assemblyfor hg38 and a RefSeq gene track, then the oneMultiQuantitativeTrack
Rows that stay open everywhere usually mean the normalization step was skipped, since an unnormalized group's height tracks its cell count.
See also
- Single-cell RNA pseudobulk
- Config guide: Multi-quantitative track
- MultiWiggleAdapter
- MultiLinearWiggleDisplay
- Clustering rows
- ChromHMM chromatin states
References
Pseudobulk / coverage tools:
- SnapATAC2
export_coverage - ArchR: exporting pseudobulk BigWigs (
getGroupBW) - deepTools
bamCoverageand its normalization methods - sinto
filterbarcodes(split BAM by barcode/label)
Reference datasets:
- SnapATAC2's 5k PBMC scATAC dataset, the 10x Genomics experiment this page pseudobulks, in its clustered and cell-type-annotated form
- CATlas: a single-cell atlas of chromatin accessibility in the human genome (Zhang et al., Cell 2021) · resource portal, the published atlas a track reads without building anything
Feedback on this tutorial is welcome: contact us.