KnowYourCG

clilatest Cite ↗

Functional analysis of DNA methylation at CpG resolution. One C binary, one verified knowledgebase store, one implementation of every statistic.

Install — then fetch once

# build from source (YAME is a submodule)
git clone --recurse-submodules \
  https://github.com/zhou-lab/kycg
cd kycg && make

Dependencies are YAME's — vendored htslib, zlib, libm, libcurl — with YAME linked statically from a pinned submodule. kycg --version reports the coupled YAME version.

# guided: location, collection, sets, confirm
kycg fetch

# or name what you want outright
kycg fetch mm10
kycg fetch hg38:CGI,ChromHMM

# open with hg38 checked; f to start
kycg fetch hg38
Downloads happen in fetch and nowhere else, and every question is gated on an interactive terminal. Inside a Nextflow job or a Docker build there is nobody to answer, so an explicit target proceeds without asking and a missing one is an error rather than a hang.

The store — fetch, verified

Knowledgebases lived in three places with three access patterns and no verification. fetch is the tie.

# -f skips the browser: plan, then fetch
$ kycg fetch -f mm10:CGI

==> Knowledgebase sets to download
    mm10  •  KYCGKB_mm10 v2  •  ~/.local/share/yame/mm10/KYCG

    CGI.20220904.cm                                  120 KB
    cpg_nocontig.cr                                 22.7 MB

    2 file(s), 22.8 MB

✓ CGI.20220904.cm                    120 KB
✓ cpg_nocontig.cr                    22.7 MB

✓ 2 fetched (22.8 MB).

…into a plain layout keyed on the target you fetch — the address you browse to, not the repo it came from — shared with the rest of the suite. A .cm in the store is an ordinary file that kycg test -m takes by path. No database, no lockfile, no bookkeeping.

~/.local/share/yame/
├ mm10/                   # whole genome (KYCGKB_mm10)
│ ├ cpg_nocontig.cr         # the row list
│ └ KYCG/
│   ├ ChromHMM.20220414.cm
│   └ SHA256SUMS
└ MSA/                    # array (InfiniumAnnotation)
  ├ MSA.ordering.tsv.gz     # probe ordering
  ├ SHA256SUMS              # platform manifest
  └ KYCG/
    ├ CGI.20220904.cm(+.idx)
    └ SHA256SUMS
Re-runs are cheap: a file already present with the right digest is not re-downloaded. Set the root with $YAME_DATA_HOME.

Pick what you need — r, i, -o

The full hg38 collection is 363 MB. You rarely want all of it.

# a tree — → unfolds a target in place
$ kycg fetch
    target  kind          rows        source           cached_sets
   hg38    whole genome  29,401,795  KYCGKB_hg38 v2   3/32
❯ ▾ mm10    whole genome  21,867,837  KYCGKB_mm10 v2  28/28
    ├ CGI       CGI…cm       120 KB  cached
    ├ ChromHMM  ChromHMM…cm  857 KB  cached
    ├ PMD       PMD…cm      16.3 KB  cached
  row 1 of 9 · → open  space select  r recommended  f fetch  i info  d store

# or name it directly — opens with those checked
$ kycg fetch hg38:CGI,ChromHMM

# no browser, no questions (scripts, Docker, Nextflow)
$ kycg fetch -f hg38:CGI,ChromHMM

r checks the recommended selection for that collection — the dozen sets worth having before you know what you are looking for, instead of all forty. i opens a pane answering what a set actually is. It is on by default and follows the cursor, so arrowing down walks you through the catalogue with each set explained as you reach it — i hides it when you want the full screen for scanning:

  TFBSrm  Transcription factor binding sites, ReMap

  A larger, uniformly reprocessed TF binding compendium — around 1,188
  factors — built from published ChIP-seq rather than one lab's panel.
  The rm suffix is ReMap, not repeat-masked.

  source      ReMap 2022, non-redundant MACS2 peaks
  citation    Hammal et al. 2022, Nucleic Acids Res, doi:10.1093/nar/gkab996
  processing  remap2022_nr_macs2 peaks, one set per factor.
Provenance is compiled into the binary from data/knowledgebases.tsv, so i works offline. A field nobody has been able to establish reads not recorded rather than being quietly omitted — an honest gap beats a plausible invention. -o takes set names, not file names — ChromHMM, not ChromHMM.20220303.cm.

Test — the core

Hypergeometric tail, six effect sizes, stratified FDR. 21.8M rows in 0.12 s.

kycg test -m ~/.local/share/yame/mm10/KYCG/ChromHMM.20220414.cm \
  query.cg > res.tsv

-m is repeatable — this is what makes a store worth having. One pass, one table, one FDR correction:

kycg test $(for f in ~/.local/share/yame/mm10/KYCG/*.cm; do
        printf -- '-m %s ' $f; done) \
  query.cg > res.tsv

Omit -m on a terminal and kycg offers the store — filtered to the sets that share your query's row space, so the list cannot be picked wrong:

$ kycg test onecell.cg

Knowledgebases for 21,867,837 rows -- space to choose, t to test
    target  kind          rows        cached_sets
❯ ▾ mm10    whole genome  21,867,837  28
   ├ [ ] Blacklist              Blacklist.20220304.cm            cached
   ├ [x] CGI                    CGI.20220904.cm                  cached
   ├ [x] ChromHMM               ChromHMM.20220414.cm             cached
   ├ [ ] ChromHMMfullStack      ChromHMMfullStack…cm             cached
  
  → open  space select  r recommended  i hide  f fetch  t test

It is the same browser fetch uses, showing only the collections whose row count matches the query. A set you do not have yet is still listed: check it, f to fetch it, then t to test against it without leaving. Both fall back to plain numbered text under NO_COLOR or TERM=dumb, and fetch writes TSV whenever stdout is redirected.

Testing against 30 knowledgebases in 30 processes decompresses the query 30 times. Pooling them decompresses it once.

Read the result — top hits

One row per (query sample, knowledgebase record), sorted by significance.

$ kycg test -m ChromHMM.cm -m CGI.cm -m PMD.cm \
    onecell.cg | cut -f3,4,10,13

db_file          db         estimate  fdr
PMD…cm           commonHMD  0.275     0
ChromHMM…cm      Quies      0.199     0
ChromHMM…cm      QuiesG     0.253     0
ChromHMM…cm      Tx         0.307     0
CGI…cm           Shore      0.196     9.17e-266
Those zeros are real, and they are why this tool is log-space. p_value and fdr are conveniences, recovered as 10^log10_p; they underflow around -308 while real enrichments run past -1500. Sort and threshold on log10_p and neglog10_fdr.

Check the row space — yame info

Everything is positional: row i must mean the same CpG in the query and in the knowledgebase. Confirm with yame info (kycg's backend) before you test.

$ yame info onecell.cg ChromHMM.20220414.cm
File        Sample Ncol Nrow     Format UnitBytes Keys
onecell.cg  1      NA   21867837 3      1         NA
ChromHMM…cm 1      NA   21867837 2      1         N=19|Quies,…

Mismatches fail loudly, naming both files and both counts:

Row count mismatch: query 'small.cg' record '1'
has 5 rows but knowledgebase 'chromhmm.cm'
record '1' has 21867837 rows.
These files index different reference row
lists and cannot be compared.
kycg checks lengths and stops there. It cannot detect two files that share a row count but come from different row spaces — sequencing indexes a whole-genome .cr, arrays a per-platform ordering. Matching them is yours to get right; the picker above helps by offering only sets whose row count already agrees.

Plot — cinderplot, separately

kycg emits TSV; cinderplot renders it as its own process. No linking, so Cairo never enters kycg's dependency surface.

# volcano
cinderplot 'res.tsv
  + aes(estimate, neglog10_fdr)
  + geom_point() + theme_minimal()' -o v.pdf
neglog10_fdr is precomputed because cinderplot has no -log10() transform in aesthetics, and it is the y-axis of three separate plots. Any arithmetic a plot needs is done in C.

Case studies — recovering known biology

Does kycg recover known biology? Two angles. A tissue's hypomethylated CpGs mark the regulatory DNA it holds open — fed to kycg test against transcription-factor binding sites (ReMap, 638 factors), the top hits are that tissue's lineage regulators, no tuning. And a whole sample's methylation, averaged over metagene coordinates, redraws the classic gene architecture. Click any figure to enlarge; everything regenerates with the scripts below (plotted with cinderplot, no R, from kycg-examples).

git clone https://github.com/zhou-lab/kycg-examples
bash kycg-examples/scripts/tissue_tfbs_enrichment.sh   # the three TF panels
bash kycg-examples/scripts/metagene_profile.sh         # the metagene profile
The signal tracks the cells present, not the organ name: fetal_liver Hypo returns GATA1, TAL1, GATA2 — hematopoietic master regulators, because fetal liver is the site of fetal hematopoiesis. The same command reproduces it.

What the store holds — the comprehensive list

Nine collections behind one command. Sequencing sets index a whole-genome CpG reference and ship with the cpg_nocontig.cr that defines it; array sets index a platform's probe ordering. The two are byte-compatible and semantically incompatible — that is exactly why kycg test checks row counts.

Whole genome — one repo each

TargetSetsSizeSource
hg3832363 MBKYCGKB_hg38 v2
mm1028266 MBKYCGKB_mm10 v2

ChromHMM · CGI · TFBS · PMD · rmsk · HM (histone marks) · ABCompartment · Blacklist · CTCFbind · ImprintingDMR · MetagenePC · Tetranuc · TissueSignature (BLUEPRINT / Brain / Loyfer) · nFlankCG · Centromere · Chromosome · and more.

Arrays — InfiniumAnnotation, per platform

TargetSetsProbes
MSA32284,309
HM45018486,427
EPIC17866,553
MM28517287,692
EPICv216937,690
HM27 / Mammal401527,722 / 38,607

Integrity — two channels, two publishers, two digests

Both channels publish a SHA256SUMS at a pinned tag, and kycg pins its digest at build time — so in neither case is kycg trusting a digest it fetched at run time. Anchoring on the manifest rather than the file list is what lets upstream add a set without a kycg rebuild.

Arrays — a sha256 chain

InfiniumAnnotation ships a SHA256SUMS per directory. The pin is sha256(SHA256SUMS); every file chains from it. Anchoring on the manifest rather than each file means upstream can add a set without a kycg rebuild.

{ "MSA", "cc63264354f71f43…" }

Whole genome — the same chain

The KYCGKB_<genome> repositories publish a SHA256SUMS too, so both channels are one code path and one digest. Zenodo keeps the DOI and remains the citable archive; it is no longer the fetch path.

{ "hg38", "KYCGKB_hg38", "v2",
  "8e0594ade2936a7b…" }

Verify it yourself

Downloads land on a .part sibling and are renamed only after the digest matches, so an interrupted fetch cannot leave a file that later reads as valid. The store is re-checkable with no kycg code at all:

cd ~/.local/share/yame/MSA/KYCG
shasum -a 256 -c SHA256SUMS

Output schema — tidy TSV, one row per test

The first columns are yame summary's, renamed. Everything from estimate onward is what kycg adds — yame gives counts and a rounded effect size; kycg tells you which results survive multiple testing.

Identity & counts

ColumnMeaning
query_file, querythe .cg and the sample in it
db_file, dbthe .cm and the record in it
groupFDR stratum (the knowledgebase)
nU nQ nD overlapthe 2×2 table: universe, query, database, overlap
beta, depthmean over the record — knowYourCG's dbStats, free

Inference & effect size

ColumnMeaning
estimatelog2 odds ratio, full precision
log10_pthe p-value. One-sided hypergeometric tail
neglog10_fdrthe FDR. BH within stratum
p_value, fdrconvenience only — these underflow to 0
cf_jaccard cf_mcc cf_overlap cf_npmi cf_dicefive alternative coefficients
FDR is corrected within one query sample and one knowledgebase — the family knowYourCG corrects over, since testEnrichment() takes a single query and calls p.adjust() on that query's frame. Adding more samples or more knowledgebases to an invocation therefore never changes an FDR already reported. -G widens the stratum to all knowledgebases for a sample.

Correctness — against R, not against intuition

The whole point of collapsing two codebases into one is that the numbers stop drifting. Both layers are checked against R directly.

Unit — vs phyper / p.adjust

$ make test
all checks passed
(worst relative error: 2.6e-16)

Across four regimes: small, array-scale (486K), whole-genome (29M), and the deep tail at log10 p ≈ -584141.

End to end — every column

$ Rscript tests/validate_vs_R.R res.tsv
log10_p     OK   worst 4.09e-12
estimate    OK   worst 4.75e-10
log10 fdr   OK   worst 2.96e-12
PASS: agrees with R on every column.

Residual is output text precision, not computation. Counts are byte-identical to yame summary.

Two things that had to be right

Log space or bust. A linear-space BH assigns FDR 0 to every underflowed row — which is every row anyone cares about.

Loader's saddle-point expansion for the point mass, as R's dhyper() does. A difference of lgamma calls loses ~7 digits to cancellation over a 29M-row universe. htslib's kt_fisher_exact() is unusable for a separate reason: int counts, and it loops "until underflow" by construction.