Yet Another Methylation Encoder

clilatest

Bit-packing for DNA methylation. Arrays and whole genomes in one binary format family, where analysis is bitwise — and so stays fast across a thousandfold of scale, from 28K probes to 29M CpGs.

Fetch — nothing ships with the binary

No reference data is bundled: no coordinates, no feature sets, no example methylomes. yame fetch opens a browser over the catalogue — species, then platform or genome build, then its files.

yame fetch
yame fetch   •   ~/.local/share/yame
        TARGET          KIND                                 TAG        IN STORE
  ▪ HUMAN                                                                       
  [*] hg38            genome                               mixed ▰▱▱▱▱    5/47
  ├──    ●  cpg_nocontig.cr                                  v2          28.9 MB
  ├──  [ ] KYCG        sets                                 v2    ▱▱▱▱▱    0/32
  ├──   ✓  data        sets                                 v3    ▰▰▰▰▰     5/5
  ────────────────────────────────────────
  hg38   genome build

Arrows move, / filters by name, source, collection or title, x takes a row (a folder takes everything under it) and f fetches. What the store already holds shows as present and cannot be re-taken.

Files land in a digest-verified store shared by every tool in the suite — $YAME_DATA_HOME, default ~/.local/share/yame — so what you fetch here is what KnowYourCG and SeSAMe find. Everything else on this page runs against it.

The store is laid out the way the tree reads. What you see under a name is what lands under that name on disk, so hg38/data in the browser is $YAME_DATA_HOME/hg38/data, and a genome's knowledgebase simply nests inside it. Which upstream repo published a directory is a column in fetch -l, not a path component — one name, whether you are browsing, fetching or looking at the disk.

What it carries are the row spaces everything else is measured in. Each length is distinct, so the count is the identity — which is what lets -R and -m take a name instead of a path.

row space        rows   row space        rows
─────────────────────────────────────────────
hg38       29,401,795   HM450         486,427
mm10       21,867,837   MM285         287,692
mm39       21,889,506   MSA           284,309
EPICv2        937,690   Mammal40       38,607
EPIC          866,553   HM27           27,722

Piped or redirected, fetch prints the catalogue as TSV instead, so a script never blocks on a keystroke. A folder is confirmed before it transfers — off a terminal that means -y, since a short name can reach a lot — but a name that picks out one file is its own confirmation. A filtered directory is still a directory, though, even when the filter leaves exactly one file, so the two -g lines below carry -y; with those, every fetch line on this page runs in a script as it stands. Naming a target, filtering it, dry runs and the rest: yame fetch -h.

Data conversion — three kinds of data, text in and out

Almost everything in practice is one of three kinds of data, and they land in two of the seven formats. One table covers how each is stored and what pack reads to build it.

the data                 format           on disk     pack  one line per CpG
────────────────────────────────────────────────────────────────────────────────
methylation, M/U counts  3  M/U counts    1-8 B/site  -fm   M ⇥ U
methylation, binarized   6  set+universe  2 bits/row  -fd   call ⇥ covered
a CpG set                6  set+universe  2 bits/row  -fd   in set ⇥ in universe

pack is the only way in. Input is one row per CpG already in the reference's order — nothing is sorted or matched for you. Columns are tab-separated, and under -fd the first column must be exactly 1 to count as in-set while the second is any nonzero int.

yame fetch -c hg38/data/human_hg38_test.cg
yame fetch hg38/cpg_nocontig.cr   # the coordinates, into the store
yame unpack -R hg38 human_hg38_test.cg | awk '$4!=2' | head -5
chr1  659296  659298  1
chr1  779099  779101  0
chr1  795737  795739  1
chr1  933194  933196  0
chr1  958160  958162  1

Format 6 prints one column by default; -f -1 gives set and universe separately, which is what pack -f6 reads back:

yame unpack -f -1 human_hg38_test.cg | awk '$1!="NA"' | head -3
1  1
0  1
1  1
yame unpack -f -1 human_hg38_test.cg > rt.txt
yame pack -f6 rt.txt rt.cg  # and back again

Verified byte-identical on the round trip.

Visualization — look at it without leaving the shell

Rows are samples, columns are CpGs or windows. The reference is inferred from the row count, so -r alone is enough.

yame fetch -c hg38/data/human_hg38_celltypes.cg
yame fetch hg38/cpg_nocontig.cr   # the coordinates, into the store
yame hprint -r chr20:30000000-30001000 human_hg38_celltypes.cg
                      chr20:30000011-30000862 (10 CpGs)
                      |.........
                      30000011  
Oligodendrocyte       .....
Pancreas-Beta         .......
Blood-NK              ....
Blood-Monocytes       .....

-w caps the width — wider regions are window-averaged rather than truncated — and -R alone gives the whole-genome view. With neither, every row is dumped as one glyph, which is how a whole reconstruction is compared against a whole truth: M/U counts and fractions render as H/M/L (-g for deciles), binary calls as 1/0/2.

Multi-sample indexing — many samples, one file

Records concatenate, so a cohort is one file. The .idx beside it maps sample names to offsets, which is what makes them addressable by name.

yame fetch -c hg38/data/human_hg38_celltypes.cg
yame info human_hg38_celltypes.cg | cut -f2-7
Sample        NSample      Nrow  Format  UnitBytes  Keys
Oligodendrocyte     4  29401795       6          2    NA
Pancreas-Beta       4  29401795       6          2    NA
Blood-NK            4  29401795       6          2    NA
Blood-Monocytes     4  29401795       6          2    NA
yame chunk -s 500000 human_hg38_celltypes.cg chunks/  # fixed row blocks
yame subset -o two.cg human_hg38_celltypes.cg Blood-NK Blood-Monocytes
yame info human_hg38_celltypes.cg | cut -f2 | tail -n +2 > names.txt
yame split -s names.txt human_hg38_celltypes.cg out_  # one file each
yame index -s names.txt human_hg38_celltypes.cg  # rebuild the index
Sample               Nrow  Format
Blood-NK         29401795       6
Blood-Monocytes  29401795       6

info -1 reports the first record of each file — the cheap way to see what a directory holds:

File                              Sample                               Nrow  Format
human_hg38_40_celltypes_chr20.cg  GSM5652176_Adipocytes-Z000000T7    773477       3
human_hg38_celltypes.cg           Oligodendrocyte                  29401795       6
human_hg38_immune_mixture.cg      mac70_mono30_2pow22              29401795       3
human_hg38_test.cg                1                                29401795       6
human_hg38_test.truth.cg          1                                29401795       6

Compare & summarize — the analysis command

With no mask, summary describes the file. With one it gives the overlap table — universe, query, mask, overlap, log2 odds ratio, beta, depth. The mask is named, not spelled out: the query's row count says which row space to resolve it in.

yame fetch -c hg38/data/human_hg38_test.cg
yame fetch -y -g CGI hg38/KYCG   # the mask, into the store
yame summary -m CGI human_hg38_test.cg
QFile               Query  MFile            Mask     N_univ  N_query  N_mask  N_overlap  Log2OddsRatio   Beta  Depth
human_hg38_test.cg      1  CGI.20220904.cm  OpenSea   29000    23857   23534      21149           3.17  0.899     NA
human_hg38_test.cg      1  CGI.20220904.cm  Shelf     29000    23857    1224       1076           0.67  0.879     NA
human_hg38_test.cg      1  CGI.20220904.cm  Shore     29000    23857    2074       1263          -1.74  0.609     NA
human_hg38_test.cg      1  CGI.20220904.cm  Island    29000    23857    2168        369          -5.10  0.170     NA

Read the last row first: CpG islands are depleted in this methylome (log2 odds −5.10) and what little overlaps is unmethylated (beta 0.170), against 0.899 in the open sea. That contrast is the whole point of the table, and it is one -m away.

Two spellings, two namespaces. fetch takes the BROWSER path, hg38/KYCG/CGI, because that is where the file goes in the store. -m takes the BARE name, CGI, and resolves it inside the query's own row space. The two are not interchangeable: -m hg38/KYCG/CGI fails, because the slashes make it look for a file of that literal name. The block above uses both, one line apart, and that is the asymmetry most likely to cost you an afternoon.

Many masks cost little more than one. The query is read once and every mask is measured against it 64 rows at a time, so a whole knowledgebase is one pass rather than one per set: the 1359-set TFBS collection against a 29.4M-row methylome takes 6 s where it used to take 193, with the same numbers and the same 45 MB. YAME_SUMMARY_PATH=1 reports on stderr how many masks took that path, since the output cannot tell you.

A format 2 mask yields one row per state, so -m ChromHMM tests every chromatin state at once. -b picks the set in the browser instead.

yame fetch -c hg38/data/human_hg38_immune_mixture.cg
yame fetch -c hg38/data/human_hg38_40_celltypes_chr20.cg
yame binarize -t 0.5 -c 3 -o calls.cg human_hg38_immune_mixture.cg  # M/U -> set + universe
yame subset -o a.cg human_hg38_40_celltypes_chr20.cg GSM5652176_Adipocytes-Z000000T7
yame subset -o b.cg human_hg38_40_celltypes_chr20.cg GSM5652181_Saphenous-Vein-Endothel-Z000000RM
yame pairwise -H 1 -c 5 -d 0.2 a.cg b.cg  # differential, fmt3 -> fmt6
yame pairwise -S a.cg b.cg  # or score the agreement instead: one TSV line
yame rowop -o binasum human_hg38_40_celltypes_chr20.cg pseudobulk.cg  # collapse
Sample    Nrow  Format
     1  773477       3

That last one is how pseudobulks are made: subset the cells of a cluster, pipe into rowop.

pairwise -S answers the other question about the same two samples — not where they differ but how much they agree — as one line of n mae rmse acc pearson over the sites both cover. Either side may be beta (format 4) rather than counts, since comparing a model’s output to its ground truth is inherently cross-format. Give it one sample and a multi-sample store and it scores every record against that sample, inflating the sample once while the store streams:

yame pairwise -S -1 GSM5652176_Adipocytes-Z000000T7 human_hg38_40_celltypes_chr20.cg predictions.cg

To score imputation rather than recall, blank the sites the model was shown before comparing — mask with the query as the mask leaves exactly the sites it had to guess:

yame mask truth.cg query.cg | yame pairwise -S -1 <cell> - predictions.cg

Perturb & mask — for benchmarking

Two different jobs. mask is deterministic and file-driven — it decides which sites are valid. dsample and perturb are stochastic and seed-driven — they decide how much signal is left.

yame fetch -c hg38/data/human_hg38_test.cg
yame dsample -N 5000 -s 42 -o ds.cg human_hg38_test.cg
yame summary ds.cg          # dsample itself prints nothing
QFile  Query  MFile  Mask    N_univ  N_query  N_mask  N_overlap  Log2OddsRatio   Beta  Depth
ds.cg      0     NA  global    5000     4087    5000       4087             NA  0.817     NA
yame perturb -p 0.05 -s 7 -o noisy.cg human_hg38_test.cg
yame summary noisy.cg
QFile     Query  MFile  Mask    N_univ  N_query  N_mask  N_overlap  Log2OddsRatio   Beta  Depth
noisy.cg      1     NA  global   29000    22953   29000      22953             NA  0.791     NA

Set -s. Without a fixed seed neither is reproducible, and the same seed reproduces a run only on the same build and platform — these counts come from Linux, and a macOS build of the same version draws different sites. mask takes its mask as a positional argument, not -m, and both files must have the same row count.

yame fetch -y -c -g blacklist hg38/KYCG
yame mask -o clean.cg human_hg38_test.cg Blacklist.20220304.cm