Calling structures from a map¶
A contact map shows chromatin structure at three scales: A/B compartments (the megabase checkerboard),
domains (TADs, squares on the diagonal) and loops (dots). uchrom.strc calls all three on a map
linked to a ChromData — a bulk Hi-C map, or a pseudo-bulk map of single cells —
with U-Chrom’s own implementations of the standard methods (no cooltools / Mustache dependency). Every
caller reads the map as a sparse band near the diagonal (uchrom.fea.read_band), never as a dense matrix,
so a 5 kb map of human chr1 (49,851 bins) runs on a laptop; the results go to cd.results /
cd.intervals with their provenance, like every other call.
import uchrom as uc
import uchrom.datasets as ds
from uchrom.fea import rebin_map
from uchrom.io import read_juicer_loops
hic = ds.load("rao2014_imr90_chr21") # IMR90 chr21, 5 kb raw counts, linked as "bulk"
rebin_map(hic, "bulk", resolution=10_000) # balanced (ICE) 10 kb copy, linked as "bulk_10kb"
rebin_map(hic, "bulk", resolution=100_000) # ... and "bulk_100kb"
uc.tl.call_compartments(hic, method="eig", contacts="bulk_100kb", phasing=ds.fetch("hg19_chr21")) # GC: E1 > 0 is A
uc.tl.compartment_strength(hic, contacts="bulk_100kb") # saddle (AA + BB) / (AB + BA)
uc.tl.call_tads(hic, method="insulation", contacts="bulk_10kb") # insulation-score boundaries
uc.tl.call_loops(hic, method="hiccups", contacts="bulk_10kb") # or ["bulk_5kb", "bulk_10kb"]: merged
uc.tl.call_loops(hic, method="mustache", contacts="bulk_10kb")
known = read_juicer_loops(published_list, chrom="21") # e.g. the Rao 2014 HiCCUPS list
uc.tl.pileup(hic, known, contacts="bulk_10kb", shifts=(-500_000, 500_000)) # APA, and of the loops shifted
The callers need a balanced map (a cooler weight column): rebin_map sums a linked map into coarser
bins and balances it (cooler balance defaults).
Structure |
Method |
API |
Stored as |
Resolution |
|---|---|---|---|---|
A/B compartments |
cis eigenvector of observed / expected, oriented by a phasing track (GC content, gene density, an active mark); Lieberman-Aiden et al. 2009, cooltools |
|
|
100 kb – 1 Mb |
compartment strength |
saddle plot, |
|
|
as the compartments |
domains |
diamond insulation score, minima above Li’s threshold; Crane et al. 2015, cooltools |
|
|
10 – 25 kb |
domains |
directionality index; Dixon et al. 2012 |
|
|
25 – 50 kb |
loops |
HiCCUPS: local enrichment against four backgrounds, FDR per λ-chunk; Rao et al. 2014, cooltools |
|
|
5 – 25 kb, several merged |
loops |
Mustache: blobs in a Gaussian scale space; Roayaei Ardakany et al. 2020 |
|
|
5 – 10 kb |
known loops / boundaries |
pileup (aggregate peak analysis, APA), with the same features shifted as a control ( |
|
|
5 – 25 kb |
against a reference |
loops with both anchors within a tolerance; boundaries within a number of loci ( |
|
— |
— |
a shallower map |
binomial thinning of the raw counts, written, balanced and linked |
|
— |
any |
Which method, and when¶
Calling needs depth. Loops are the first to go: on the IMR90 chr21 map thinned (
thin_map) to a fifth of its 10.3 M contacts HiCCUPS finds a third of the published loops, at 5 % one, at 2 % none. The pileup of the same loops stays at APA 3.8–4.7 down to 2 % (the loops shifted by ±0.5–1 Mb: at or below 1). On a shallow map — a cell-type pseudo-bulk, a few deep single cells — score known loops by pileup instead of calling them. Domains and compartments need fewer contacts (88 % of the insulation boundaries are still found at 2 %).Compare strengths at matched depth. The saddle strength of a shallow map is inflated (+16 % at 2 % of this map), so groups with different numbers of contacts are compared on random subsets of equal depth (
uc.tl.pseudobulk(cd, groupby=<Series>); the pseudo-bulk tutorial).HiCCUPS or Mustache. HiCCUPS is the conservative caller (most of its calls are in the published list), Mustache the sensitive one (more calls, nearly all published loops at 5 kb, half of its calls unconfirmed). HiCCUPS on several resolutions merges them as Juicer does.
Phasing. An eigenvector’s sign is arbitrary: give
phasing=a FASTA (GC content), acd.bin_trackscolumn or a table; to compare several maps, orient them all by one reference (e.g. the E1 of all cells) withEigCompartmentParams(sort_by_phasing=True).Speed. Everything runs on a laptop: a 5 kb map of chr1 (50,000 bins) takes seconds per caller. With the native kernels installed (
pip install ./packages/uchrom-maps, the extrau-chrom[maps]; Rust, multi-core) HiCCUPS and Mustache run 3–6× faster on such a map, with identical results; without them the numpy code runs.
Validation, in brief¶
On the same balanced maps the implementations reproduce the reference tools (benchmarks/hic_structures/):
compartments = cooltools eigs_cis / saddle to 1e-14; insulation boundaries identical to cooltools
insulation in 20 of 20 map × window cases; HiCCUPS’ enriched pixels identical to cooltools dots on 6 maps;
Mustache calls within 2 bins of mustache-hic for 89–96 %. Against the published Rao 2014 calls on IMR90
chr21 (92 HiCCUPS loops, 86 Arrowhead domains):
Caller |
Map |
Recall |
Precision |
|---|---|---|---|
HiCCUPS |
10 kb (5 + 10 kb merged) |
0.76 (0.79) |
0.74 (0.74) |
Mustache |
5 kb / 10 kb |
0.95 / 0.72 |
0.41 / 0.42 |
insulation (boundaries vs Arrowhead edges, ±2 bins) |
10 kb / 25 kb |
0.66 / 0.67 |
0.84 / 0.94 (chance 0.13 / 0.28) |
The compartments of the same map agree with chromatin tracing of IMR90 chr21 (Su et al. 2020) on 94 % of
the imaged loci. The full tables, the depth series and run times (IMR90 chr1 at 5 kb: insulation 1.5 s,
compartments 8 s, HiCCUPS 6–16 s, Mustache 16–19 s) are in benchmarks/hic_structures/README.md.
Tutorial¶
API: compartments, TADs, loops, maps, pileups, pseudo-bulk.