General characterization of CpG methylation in Ostrea
chilensis: how much of the genome is methylated, how methylation is
distributed across genomic features and along chromosomes, what
gene-body methylation looks like, and whether gene methylation tracks
gene function. Population differences are in 13.4; this
notebook describes the shared methylome, pooling the 9 independent
oysters in data/13-wgbs-samples.tsv (duplicated libraries
excluded; see README Known issues).
Upstream:
code/13-wgbs-submit.sh (13.1 → 13.2 → 13.3) finished:
reads the strand-merged CpG files
output/13-wgbs/methylation/*.CpG_report.merged_CpG_evidence.cov.gz.code/13.5-wgbs-gene-annotation.sh finished: reads
output/13-wgbs/13.5-gene-annotation/GN_gene_annotation.tsv
(Swiss-Prot best hit + GO per GN gene). Only the last section needs
it.Reference: merged_out.fasta +
GN.gene.gff3, as in 09 and 13.4.
The GFF has gene / mRNA / exon / CDS only, with every exon identical to
a CDS: there are no UTRs, so “exon” here means coding
exon and 5’/3’ UTR methylation can’t be separated from the flanks.
Pooling. At ~6–9x raw coverage per oyster, single-sample per-CpG estimates are noisy. For the landscape, counts are summed across all 9 oysters per CpG (a species-level methylome at roughly 9x the depth of one oyster). Per-oyster values are kept for global, per-feature, and per-gene summaries, which pool many CpGs and so are stable within a single animal; these show whether the patterns are consistent across individuals and populations.
Where to run: a compute node inside the lab R container (~100 GB RAM), e.g.
salloc -A srlab -p cpu-g2-mem2x -c 8 --mem=150G -t 8:00:00
apptainer exec /gscratch/srlab/containers/srlab-R4.4-bioinformatics-container-c3d3116.sif R
Paths are relative to code/, like the other
notebooks.
# tidyverse first, so the Bioconductor set operations (reduce, setdiff, union, ...)
# mask the dplyr/purrr versions rather than the other way round.
library(tidyverse)
library(data.table)
library(GenomicRanges)
library(rtracklayer)
library(Biostrings)
library(Rsamtools)
out_dir <- "../output/13-wgbs/13.6-landscape"
meth_dir <- "../output/13-wgbs/methylation"
genome_dir <- "../data/13-wgbs-genome"
annot_file <- "../output/13-wgbs/13.5-gene-annotation/GN_gene_annotation.tsv"
repeat_gff <- NULL # RepeatMasker/EDTA GFF for this assembly, if one becomes available
snp_bed <- "../output/13-wgbs/13.7-snps/wgbs_snps.q20.CpG.bed" # from 13.7; skipped if absent
dir.create(out_dir, showWarnings = FALSE, recursive = TRUE)
# CpG level (pooled across oysters)
min_cov_pooled <- 10 # summed reads per CpG
max_cov_pctl <- 0.999 # drop pile-ups (collapsed repeats)
min_samples <- 5L # CpG must have data in >= 5 of 9 oysters
meth_breaks <- c(-Inf, 10, 50, Inf) # % methylation
meth_labels <- c("unmethylated (<10%)", "sparse (10-50%)", "methylated (>=50%)")
# features
flank_bp <- 2000 # promoter (upstream) and downstream flank; same promoter as 13.4
window_size <- 1e5 # chromosome-scale windows
# genes
min_gene_cpgs <- 5 # covered CpGs needed for a gene-body estimate
min_gene_calls <- 30 # per-oyster reads summed over the gene, for per-oyster values
n_body_bins <- 20 # metagene: scaled gene body
n_flank_bins <- 10 # metagene: each 2 kb flank
# function
min_go_genes <- 10
max_go_genes <- 500
samples <- read_tsv("../data/13-wgbs-samples.tsv", show_col_types = FALSE) %>%
mutate(population = factor(population, levels = c("Pum", "Qui", "Rio")),
file = file.path(meth_dir,
paste0(sample, ".CpG_report.merged_CpG_evidence.cov.gz")))
stopifnot(all(file.exists(samples$file)))
Five non-overlapping compartments, so every base and CpG belongs to exactly one:
| feature | definition |
|---|---|
| exon | coding exon (no UTRs annotated) |
| intron | gene body minus exons |
| promoter | 2 kb upstream of a TSS, outside any gene |
| downstream | 2 kb downstream of a TES, outside any gene or promoter |
| intergenic | everything else |
seqlens <- read_tsv(file.path(genome_dir, "genome.chrom.sizes"),
col_names = c("chr", "len"), show_col_types = FALSE)
si <- Seqinfo(seqlens$chr, seqlens$len)
fa <- FaFile(file.path(genome_dir, "merged_out.fasta"))
gff <- import(file.path(genome_dir, "GN.gene.gff3"))
seqlevels(gff, pruning.mode = "coarse") <- seqlevels(si)
seqinfo(gff) <- si
genes <- gff[gff$type == "gene"]
names(genes) <- genes$ID
exon_gr <- gff[gff$type == "exon"]
stopifnot(all(lengths(exon_gr$Parent) == 1))
exon_gr$tx <- as.character(unlist(exon_gr$Parent))
exon_gr$gene_id <- sub("\\.[0-9]+$", "", exon_gr$tx)
gene_red <- reduce(genes, ignore.strand = TRUE)
exons <- reduce(exon_gr, ignore.strand = TRUE)
introns <- setdiff(gene_red, exons)
promoters <- setdiff(reduce(trim(promoters(genes, upstream = flank_bp, downstream = 0)),
ignore.strand = TRUE), gene_red)
downstream <- setdiff(reduce(trim(flank(genes, flank_bp, start = FALSE)), ignore.strand = TRUE),
union(gene_red, promoters))
intergenic <- gaps(reduce(c(gene_red, promoters, downstream)))
intergenic <- intergenic[strand(intergenic) == "*"]
feature_levels <- c("exon", "intron", "promoter", "downstream", "intergenic")
features <- c(exons, introns, promoters, downstream, intergenic)
features$feature <- factor(rep(feature_levels,
lengths(list(exons, introns, promoters, downstream, intergenic))),
levels = feature_levels)
stopifnot(!any(countOverlaps(features, features) > 1)) # disjoint
stopifnot(sum(as.numeric(width(features))) == sum(as.numeric(seqlengths(si))))
annotate_feature <- function(gr) {
features$feature[findOverlaps(gr, features, ignore.strand = TRUE, select = "first")]
}
# Every CpG dinucleotide in the assembly (1-based position of the C, as in Bismark).
genome_seq <- scanFa(fa)
names(genome_seq) <- sub(" .*", "", names(genome_seq))
genome_cpg <- do.call(c, lapply(seqlevels(si), function(chr) {
GRanges(chr, IRanges(start(matchPattern("CG", genome_seq[[chr]])), width = 1),
seqinfo = si)
}))
genome_cpg$feature <- annotate_feature(genome_cpg)
rm(genome_seq); invisible(gc())
message(length(genome_cpg), " CpGs in the assembly")
# Observed/expected CpG per feature: CG * length / (C * G). Germline methylation
# deaminates mC -> T over evolutionary time, so long-term methylated sequence is CpG-depleted.
composition <- map_dfr(feature_levels, function(f) {
gr <- features[features$feature == f]
seq <- getSeq(fa, gr)
bf <- colSums(letterFrequency(seq, c("C", "G")))
tibble(feature = f,
bp = sum(as.numeric(width(gr))),
n_cpg = sum(as.numeric(vcountPattern("CG", seq))),
gc_pct = 100 * sum(bf) / bp,
cpg_oe = n_cpg * bp / (bf[["C"]] * bf[["G"]]))
}) %>%
mutate(pct_genome = 100 * bp / sum(bp),
cpg_per_kb = 1000 * n_cpg / bp)
composition
## # A tibble: 5 x 7
## feature bp n_cpg gc_pct cpg_oe pct_genome cpg_per_kb
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 exon 25932309 724981 43.9 0.581 2.20 28.0
## 2 intron 109651400 1756427 34.4 0.542 9.29 16.0
## 3 promoter 39443476 892857 35.5 0.718 3.34 22.6
## 4 downstream 37404907 745204 35.1 0.647 3.17 19.9
## 5 intergenic 968488830 21980792 35.9 0.706 82.0 22.7
Each oyster is read once. Per-oyster summaries (global, per feature, per gene) are taken on the way through; per-CpG counts are summed into one pooled table.
SNP mask. CpGs carrying a SNP on their C or G (from
13.7, biscuit genotyping of the bisulfite BAMs) are dropped
as each oyster is read, since a C→T SNP reads as an unmethylated CpG.
Every summary below is therefore SNP-masked; assembly CpG counts
(genome_cpg) are not. If the 13.7 file is missing, nothing
is masked.
read_cov <- function(f) {
fread(cmd = paste("gzip -dc", shQuote(f)), header = FALSE,
select = c(1, 2, 5, 6), col.names = c("chr", "pos", "meth", "unmeth"))
}
snp_gr <- NULL
if (file.exists(snp_bed)) {
snp_gr <- import(snp_bed, format = "bed") # one base: the C or G hit
message(length(snp_gr), " CpG-SNP sites from ", snp_bed)
} else {
message("No SNP mask (", snp_bed, " not found); run code/13.7-wgbs-snps.sh to add one")
}
pooled <- NULL
ps_global <- ps_feature <- ps_gene <- vector("list", nrow(samples))
n_masked <- set_names(integer(nrow(samples)), samples$sample)
for (i in seq_len(nrow(samples))) {
s <- samples$sample[i]
d <- read_cov(samples$file[i])[chr %in% seqlevels(si)]
if (!is.null(snp_gr)) { # each CpG spans its C (pos) and G (pos + 1)
hit <- overlapsAny(GRanges(d$chr, IRanges(d$pos, width = 2)), snp_gr, ignore.strand = TRUE)
n_masked[s] <- sum(hit)
d <- d[!hit]
}
d[, cov := meth + unmeth]
gr <- GRanges(d$chr, IRanges(d$pos, width = 1), seqinfo = si)
d[, feature := annotate_feature(gr)]
ps_global[[i]] <- d[, .(sample = s, cpgs_1x = .N, cpgs_5x = sum(cov >= 5),
median_cov = median(cov),
global_meth_pct = 100 * sum(meth) / sum(cov),
methylated_cpg_pct = 100 * mean(meth[cov >= 5] / cov[cov >= 5] >= 0.5))]
ps_feature[[i]] <- d[, .(sample = s, meth = sum(meth), cov = sum(cov), n_cpg = .N),
by = feature]
ov <- findOverlaps(gr, genes, ignore.strand = TRUE)
ps_gene[[i]] <- data.table(gene_id = names(genes)[subjectHits(ov)],
meth = d$meth[queryHits(ov)], cov = d$cov[queryHits(ov)]
)[, .(sample = s, meth = sum(meth), cov = sum(cov), n_cpg = .N),
by = gene_id]
pooled <- rbind(pooled, d[, .(chr, pos, meth, unmeth, n = 1L)]
)[, .(meth = sum(meth), unmeth = sum(unmeth), n = sum(n)), by = .(chr, pos)]
message(s, ": ", nrow(d), " CpGs; pooled table now ", nrow(pooled))
}
ps_global <- rbindlist(ps_global) %>% left_join(samples, by = "sample")
ps_feature <- rbindlist(ps_feature) %>% left_join(samples, by = "sample")
ps_gene <- rbindlist(ps_gene)
if (!is.null(snp_gr))
write_tsv(tibble(sample = names(n_masked), cpgs_masked = n_masked),
file.path(out_dir, "snp_mask_summary.tsv"))
rm(d, gr, ov); invisible(gc())
pooled[, cov := meth + unmeth]
cov_max <- quantile(pooled$cov, max_cov_pctl)
pooled_f <- pooled[cov >= min_cov_pooled & cov <= cov_max & n >= min_samples]
cpg <- GRanges(pooled_f$chr, IRanges(pooled_f$pos, width = 1), seqinfo = si,
meth = pooled_f$meth, cov = pooled_f$cov,
pct = 100 * pooled_f$meth / pooled_f$cov)
cpg$feature <- annotate_feature(cpg)
cpg$class <- cut(cpg$pct, meth_breaks, meth_labels, right = FALSE)
tibble(step = c("CpGs in assembly", "covered in >= 1 oyster", "pass pooled filters"),
n = c(length(genome_cpg), nrow(pooled), length(cpg))) %>%
mutate(pct_of_assembly = round(100 * n / length(genome_cpg), 1))
## # A tibble: 3 x 3
## step n pct_of_assembly
## <chr> <int> <dbl>
## 1 CpGs in assembly 26104181 100
## 2 covered in >= 1 oyster 18689605 71.6
## 3 pass pooled filters 14433110 55.3
# Pooled per-CpG methylation track for IGV (alongside the 13.2 BAMs).
bw <- granges(cpg); bw$score <- cpg$pct
export.bw(sort(bw), file.path(out_dir, "pooled_CpG_methylation.bw"))
fwrite(pooled_f[, .(chr, start = pos, end = pos + 1L, pct = round(100 * meth / cov, 2),
meth, unmeth, n_samples = n)],
file.path(out_dir, "pooled_CpG_methylation.tsv.gz"), sep = "\t")
rm(pooled, pooled_f); invisible(gc())
CpG methylation in invertebrates is moderate overall (bivalves typically ~10–20% of CpGs methylated), unlike the ~70–80% of vertebrates.
aln <- read_tsv("../output/13-wgbs/alignment_summary.tsv", show_col_types = FALSE)
global <- ps_global %>%
left_join(select(aln, sample, CHG_meth_pct, CHH_meth_pct), by = "sample") %>%
select(sample, population, cpgs_1x, cpgs_5x, median_cov,
global_meth_pct, methylated_cpg_pct, CHG_meth_pct, CHH_meth_pct)
write_tsv(global, file.path(out_dir, "global_methylation_per_oyster.tsv"))
global
## sample population cpgs_1x cpgs_5x median_cov global_meth_pct
## <char> <fctr> <int> <int> <num> <num>
## 1: Pum1 Pum 14851783 7899658 5 19.41441
## 2: Qui1 Qui 15017682 8313533 5 18.90595
## 3: Qui2 Qui 14534059 7144141 4 21.60761
## 4: Qui3 Qui 14942618 8409699 5 20.51988
## 5: Qui4 Qui 14684326 7244626 4 19.29170
## 6: Rio1 Rio 15105950 8949697 5 20.31559
## 7: Rio2 Rio 14776205 7595578 5 20.09963
## 8: Rio3 Rio 14337226 6334602 4 21.13009
## 9: Rio4 Rio 14960910 8607331 5 20.54781
## methylated_cpg_pct CHG_meth_pct CHH_meth_pct
## <num> <num> <num>
## 1: 21.49834 0.6 0.6
## 2: 20.76845 0.8 0.8
## 3: 23.70096 0.7 0.8
## 4: 22.13247 0.7 0.8
## 5: 21.76495 0.7 0.7
## 6: 21.79080 0.6 0.7
## 7: 22.32245 0.6 0.7
## 8: 24.02625 0.8 0.8
## 9: 22.02631 0.7 0.8
global %>%
pivot_longer(c(global_meth_pct, CHG_meth_pct, CHH_meth_pct),
names_to = "context", values_to = "pct") %>%
mutate(context = sub("_meth_pct", "", sub("global", "CpG", context))) %>%
ggplot(aes(population, pct, colour = population)) +
geom_jitter(width = 0.1, size = 2.5) +
facet_wrap(~context, scales = "free_y") +
labs(y = "% methylation (read-level)", title = "Methylation by context") +
theme_bw() + theme(legend.position = "none")
kruskal.test(global_meth_pct ~ population, data = global)
##
## Kruskal-Wallis rank sum test
##
## data: global_meth_pct by population
## Kruskal-Wallis chi-squared = 1.2, df = 2, p-value = 0.5488
A mosaic methylome shows up as a bimodal distribution: most CpGs near 0%, a smaller fully methylated mode near 100%, and few intermediate sites.
ggplot(tibble(pct = cpg$pct), aes(pct)) +
geom_histogram(binwidth = 2, boundary = 0) +
labs(x = "% methylation per CpG (pooled)", y = "CpGs") + theme_bw()
class_summary <- as_tibble(table(class = cpg$class)) %>%
mutate(pct = 100 * n / sum(n))
write_tsv(class_summary, file.path(out_dir, "cpg_class_summary.tsv"))
class_summary
## # A tibble: 3 x 3
## class n pct
## <chr> <int> <dbl>
## 1 unmethylated (<10%) 11196509 77.6
## 2 sparse (10-50%) 610968 4.23
## 3 methylated (>=50%) 2625633 18.2
cpg_tab <- as_tibble(as.data.frame(mcols(cpg)[, c("feature", "pct", "class")]))
feature_summary <- composition %>%
left_join(as_tibble(table(feature = genome_cpg$feature)) %>% dplyr::rename(cpg_in_assembly = n),
by = "feature") %>%
left_join(cpg_tab %>%
group_by(feature) %>%
summarise(cpg_covered = n(),
mean_meth = mean(pct),
median_meth = median(pct),
pct_methylated = 100 * mean(class == meth_labels[3]),
pct_sparse = 100 * mean(class == meth_labels[2]),
pct_unmeth = 100 * mean(class == meth_labels[1])),
by = "feature") %>%
mutate(feature = factor(feature, feature_levels),
pct_cpg_covered = 100 * cpg_covered / cpg_in_assembly)
write_tsv(feature_summary, file.path(out_dir, "feature_summary.tsv"))
feature_summary
## # A tibble: 5 x 15
## feature bp n_cpg gc_pct cpg_oe pct_genome cpg_per_kb cpg_in_assembly
## <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
## 1 exon 2.59e7 7.25e5 43.9 0.581 2.20 28.0 726550
## 2 intron 1.10e8 1.76e6 34.4 0.542 9.29 16.0 1758002
## 3 promoter 3.94e7 8.93e5 35.5 0.718 3.34 22.6 893067
## 4 downstream 3.74e7 7.45e5 35.1 0.647 3.17 19.9 745388
## 5 intergenic 9.68e8 2.20e7 35.9 0.706 82.0 22.7 21981174
## # i 7 more variables: cpg_covered <int>, mean_meth <dbl>, median_meth <dbl>,
## # pct_methylated <dbl>, pct_sparse <dbl>, pct_unmeth <dbl>,
## # pct_cpg_covered <dbl>
cpg_tab %>%
ggplot(aes(feature, fill = class)) +
geom_bar(position = "fill") +
scale_fill_manual(values = c("grey80", "#9ecae1", "#08519c")) +
labs(y = "Proportion of covered CpGs", fill = NULL) + theme_bw()
cpg_tab %>%
ggplot(aes(feature, pct)) +
geom_violin(fill = "grey85", scale = "width") +
stat_summary(fun = mean, geom = "point", colour = "red") +
labs(y = "% methylation per CpG (pooled)") + theme_bw()
Share of the genome, of all assembly CpGs, of covered CpGs, and of methylated CpGs falling in each feature. Enrichment of methylated CpGs is tested per feature against all other features (Fisher), using covered CpGs as the background.
meth_cpg <- cpg_tab$class == meth_labels[3]
feature_share <- bind_rows(
composition %>% transmute(feature, set = "genome (bp)", n = bp),
as_tibble(table(feature = genome_cpg$feature)) %>% mutate(set = "assembly CpGs"),
as_tibble(table(feature = cpg_tab$feature)) %>% mutate(set = "covered CpGs"),
as_tibble(table(feature = cpg_tab$feature[meth_cpg])) %>% mutate(set = "methylated CpGs")
) %>%
group_by(set) %>% mutate(share = n / sum(n)) %>% ungroup() %>%
mutate(set = fct_inorder(set), feature = factor(feature, feature_levels))
ggplot(feature_share, aes(set, share, fill = feature)) +
geom_col() + scale_fill_brewer(palette = "Set2") +
labs(x = NULL, y = "Share") + theme_bw()
feature_enrichment <- map_dfr(feature_levels, function(f) {
in_f <- cpg_tab$feature == f
ft <- fisher.test(table(in_f, meth_cpg))
tibble(feature = f, odds_ratio = unname(ft$estimate),
ci_low = ft$conf.int[1], ci_high = ft$conf.int[2], p = ft$p.value)
}) %>% mutate(p_adj = p.adjust(p, "BH"))
write_tsv(feature_enrichment, file.path(out_dir, "feature_methylated_cpg_enrichment.tsv"))
feature_enrichment
## # A tibble: 5 x 6
## feature odds_ratio ci_low ci_high p p_adj
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 exon 2.12 2.11 2.14 0 0
## 2 intron 5.56 5.54 5.59 0 0
## 3 promoter 0.742 0.736 0.748 0 0
## 4 downstream 1.16 1.15 1.17 0 0
## 5 intergenic 0.323 0.322 0.324 0 0
Per-oyster coverage-weighted methylation in each feature. If the feature ranking holds in every animal, the pooled pattern isn’t driven by a few deep samples.
ps_feature %>%
mutate(pct = 100 * meth / cov) %>%
ggplot(aes(feature, pct, colour = population)) +
geom_point(position = position_jitterdodge(jitter.width = 0.1, dodge.width = 0.6)) +
labs(y = "% methylation (read-level, per oyster)") + theme_bw()
# Not run until a repeat annotation for merged_out.fasta exists. Repeats overlap the
# five features, so they are reported as a second, orthogonal layer.
reps <- import(repeat_gff)
seqlevels(reps, pruning.mode = "coarse") <- seqlevels(si)
rep_class <- sub("/.*", "", coalesce(reps$Target, reps$type)) # adjust to the GFF's class field
cpg$repeat_class <- "non-repeat"
h <- findOverlaps(cpg, reps, ignore.strand = TRUE, select = "first")
cpg$repeat_class[!is.na(h)] <- rep_class[h[!is.na(h)]]
as_tibble(as.data.frame(mcols(cpg)[, c("feature", "repeat_class", "pct")])) %>%
group_by(feature, repeat_class) %>%
summarise(n = n(), mean_meth = mean(pct), .groups = "drop") %>%
arrange(feature, dplyr::desc(n))
Methylation in 100 kb windows along each chromosome, against gene density. In mosaic methylomes methylation tracks gene density.
win <- tileGenome(seqlengths(si), tilewidth = window_size, cut.last.tile.in.chrom = TRUE)
win <- binnedAverage(win, coverage(gene_red), "gene_frac")
win$n_cpg_assembly <- countOverlaps(win, genome_cpg)
h <- findOverlaps(cpg, win)
win_meth <- data.table(w = subjectHits(h), meth = cpg$meth[queryHits(h)],
cov = cpg$cov[queryHits(h)], pct = cpg$pct[queryHits(h)]
)[, .(meth_pct = 100 * sum(meth) / sum(cov),
frac_methylated = mean(pct >= 50), n_cpg = .N), by = w]
windows <- as_tibble(as.data.frame(win)) %>%
mutate(w = row_number()) %>%
left_join(win_meth, by = "w") %>%
filter(n_cpg >= 20) %>%
mutate(cpg_per_kb = 1000 * n_cpg_assembly / width)
write_tsv(windows, file.path(out_dir, "window_methylation.tsv"))
windows %>%
transmute(seqnames, mb = (start + end) / 2e6,
`CpG methylation (%)` = meth_pct, `gene coverage (%)` = 100 * gene_frac) %>%
pivot_longer(-c(seqnames, mb)) %>%
ggplot(aes(mb, value)) +
geom_line(linewidth = 0.2) +
geom_smooth(method = "loess", span = 0.2, se = FALSE, colour = "red", linewidth = 0.5) +
facet_grid(name ~ seqnames, scales = "free", space = "free_x") +
labs(x = "Position (Mb)", y = NULL) + theme_bw() +
theme(strip.text.x = element_text(size = 7), axis.text.x = element_text(size = 6))
cor.test(windows$meth_pct, windows$gene_frac, method = "spearman")
##
## Spearman's rank correlation rho
##
## data: windows$meth_pct and windows$gene_frac
## S = 1.3192e+11, p-value < 2.2e-16
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
## rho
## 0.3651871
cor.test(windows$meth_pct, windows$cpg_per_kb, method = "spearman")
##
## Spearman's rank correlation rho
##
## data: windows$meth_pct and windows$cpg_per_kb
## S = 3.4353e+11, p-value < 2.2e-16
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
## rho
## -0.6531603
ggplot(windows, aes(100 * gene_frac, meth_pct)) +
geom_bin2d(bins = 60) + geom_smooth(method = "loess", colour = "red") +
labs(x = "Gene coverage in window (%)", y = "Window CpG methylation (%)") + theme_bw()
Pooled coverage-weighted methylation over all covered CpGs in each gene, plus the same for its exons, introns, and 2 kb promoter. Genes need ≥ 5 covered CpGs. CpG O/E and GC are computed from sequence for every gene.
region_meth <- function(gr, ids, label) {
h <- findOverlaps(cpg, gr, ignore.strand = TRUE)
data.table(gene_id = ids[subjectHits(h)], meth = cpg$meth[queryHits(h)],
cov = cpg$cov[queryHits(h)], pct = cpg$pct[queryHits(h)]
)[, .(pct = 100 * sum(meth) / sum(cov), n = .N,
frac_meth_cpg = mean(pct >= 50)), by = gene_id] %>%
as_tibble() %>%
rename_with(~ paste0(label, "_", .x), -gene_id)
}
gene_promoters <- trim(promoters(genes, upstream = flank_bp, downstream = 0))
gene_introns <- psetdiff(granges(genes),
split(granges(exon_gr), factor(exon_gr$gene_id, names(genes))))
gene_introns <- unlist(gene_introns)
gene_seq <- getSeq(fa, genes)
gene_comp <- letterFrequency(gene_seq, c("C", "G")) * 1 # double: C * G overflows integer
n_exons <- table(factor(exon_gr$gene_id, names(genes)))
gene_meth <- tibble(gene_id = names(genes),
chr = as.character(seqnames(genes)),
length = width(genes),
n_exons = as.integer(n_exons[names(genes)]),
n_cpg = as.numeric(vcountPattern("CG", gene_seq)),
gc_pct = 100 * rowSums(gene_comp) / width(genes),
cpg_oe = n_cpg * width(genes) / (gene_comp[, "C"] * gene_comp[, "G"])) %>%
left_join(region_meth(genes, names(genes), "body"), by = "gene_id") %>%
left_join(region_meth(exon_gr, exon_gr$gene_id, "exon"), by = "gene_id") %>%
left_join(region_meth(gene_introns, names(gene_introns), "intron"), by = "gene_id") %>%
left_join(region_meth(gene_promoters, names(gene_promoters), "promoter"), by = "gene_id") %>%
mutate(body_n = replace_na(body_n, 0L),
body_class = cut(if_else(body_n >= min_gene_cpgs, body_pct, NA_real_),
meth_breaks, meth_labels, right = FALSE))
rm(gene_seq); invisible(gc())
gbm <- filter(gene_meth, !is.na(body_class))
message(nrow(gbm), " of ", nrow(gene_meth), " genes with >= ", min_gene_cpgs, " covered CpGs")
table(gbm$body_class)
##
## unmethylated (<10%) sparse (10-50%) methylated (>=50%)
## 8103 702 4837
Bivalve gene-body methylation is usually bimodal: one group of genes heavily methylated, the other nearly unmethylated. The local minimum of the density between the modes is printed as a check on the fixed 10/50% class breaks.
dens <- density(gbm$body_pct, from = 0, to = 100)
mid <- dens$x > 10 & dens$x < 90
message("Density minimum between modes: ", round(dens$x[mid][which.min(dens$y[mid])], 1), "%")
ggplot(gbm, aes(body_pct)) +
geom_histogram(binwidth = 2, boundary = 0) +
geom_vline(xintercept = c(10, 50), linetype = 2, colour = "grey50") +
labs(x = "Gene-body CpG methylation (%)", y = "Genes") + theme_bw()
ggplot(gbm, aes(exon_pct, intron_pct)) +
geom_bin2d(bins = 60) + geom_abline(linetype = 2, colour = "red") +
labs(x = "Exon methylation (%)", y = "Intron methylation (%)") + theme_bw()
ggplot(gbm, aes(promoter_pct, body_pct)) +
geom_bin2d(bins = 60) +
labs(x = "Promoter (2 kb upstream) methylation (%)", y = "Gene-body methylation (%)") +
theme_bw()
Genes methylated in the germline lose CpGs over evolutionary time, so methylated genes are expected to have lower CpG O/E. A bimodal CpG O/E distribution across all genes (not only those with WGBS coverage) is the sequence-level signature of this.
ggplot(gene_meth, aes(cpg_oe)) +
geom_histogram(bins = 80) + xlim(0, 1.5) +
labs(x = "Gene CpG O/E (all genes)", y = "Genes") + theme_bw()
ggplot(gbm, aes(cpg_oe, fill = body_class)) +
geom_density(alpha = 0.5) + xlim(0, 1.5) +
scale_fill_manual(values = c("grey70", "#9ecae1", "#08519c")) +
labs(x = "Gene CpG O/E", fill = NULL) + theme_bw()
cor.test(gbm$body_pct, gbm$cpg_oe, method = "spearman")
##
## Spearman's rank correlation rho
##
## data: gbm$body_pct and gbm$cpg_oe
## S = 6.7227e+11, p-value < 2.2e-16
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
## rho
## -0.5887704
gbm %>%
transmute(body_class, `gene length (kb)` = length / 1000, `exons` = n_exons,
`GC (%)` = gc_pct) %>%
pivot_longer(-body_class) %>%
ggplot(aes(body_class, value, fill = body_class)) +
geom_boxplot(outlier.size = 0.3) + scale_y_log10() +
scale_fill_manual(values = c("grey70", "#9ecae1", "#08519c")) +
facet_wrap(~name, scales = "free_y") + labs(x = NULL) + theme_bw() +
theme(legend.position = "none", axis.text.x = element_text(angle = 20, hjust = 1))
write_tsv(gene_meth, file.path(out_dir, "gene_methylation.tsv"))
Methylation along a scaled gene body (TSS → TES, 20 bins) with 2 kb flanks (10 bins each), oriented by strand. Flanks can run into neighbouring genes; that is left in, as it is what a CpG near a TSS actually sees.
ext <- trim(resize(genes, width(genes) + 2 * flank_bp, fix = "center"))
h <- findOverlaps(cpg, ext, ignore.strand = TRUE)
flank_step <- flank_bp / n_flank_bins
mg <- data.table(gene_id = names(genes)[subjectHits(h)],
pos = start(cpg)[queryHits(h)],
gs = start(genes)[subjectHits(h)], ge = end(genes)[subjectHits(h)],
minus = as.character(strand(genes))[subjectHits(h)] == "-",
meth = cpg$meth[queryHits(h)], cov = cpg$cov[queryHits(h)],
pct = cpg$pct[queryHits(h)])
mg[, `:=`(rel = fifelse(minus, ge - pos, pos - gs), len = ge - gs + 1)] # 0 = TSS
mg[, bin := fcase(rel < 0, floor(rel / flank_step), # -10 .. -1
rel < len, floor(rel / len * n_body_bins), # 0 .. 19
default = n_body_bins + floor((rel - len) / flank_step))] # 20 .. 29
mg <- mg[bin >= -n_flank_bins & bin < n_body_bins + n_flank_bins]
mg <- merge(mg, as.data.table(gene_meth[, c("gene_id", "body_class")]), by = "gene_id")
metagene <- rbind(
mg[, .(body_class = "all genes", meth_pct = 100 * sum(meth) / sum(cov),
frac_methylated = mean(pct >= 50), n_cpg = .N), by = bin],
mg[!is.na(body_class), .(meth_pct = 100 * sum(meth) / sum(cov),
frac_methylated = mean(pct >= 50), n_cpg = .N),
by = .(bin, body_class = as.character(body_class))],
use.names = TRUE)
metagene[, body_class := factor(body_class, c("all genes", meth_labels))]
write_tsv(metagene, file.path(out_dir, "metagene_profile.tsv"))
ggplot(metagene, aes(bin + 0.5, meth_pct, colour = body_class)) +
geom_line() +
geom_vline(xintercept = c(0, n_body_bins), linetype = 2, colour = "grey50") +
scale_x_continuous(breaks = c(-n_flank_bins, 0, n_body_bins, n_body_bins + n_flank_bins),
labels = c(paste0("-", flank_bp / 1000, " kb"), "TSS", "TES",
paste0("+", flank_bp / 1000, " kb"))) +
scale_colour_manual(values = c("black", "grey60", "#9ecae1", "#08519c")) +
labs(x = NULL, y = "CpG methylation (%)", colour = NULL) + theme_bw()
rm(mg); invisible(gc())
Methylation by position of the exon/intron along the transcript (5’ → 3’), for multi-exon genes. In many invertebrates the first exon and 5’ end are less methylated than the rest of the body.
ex <- data.table(seqnames = as.character(seqnames(exon_gr)), start = start(exon_gr),
end = end(exon_gr), strand = as.character(strand(exon_gr)),
tx = exon_gr$tx, gene_id = exon_gr$gene_id)
setorder(ex, tx, start)
ex[, `:=`(n_ex = .N, idx = seq_len(.N)), by = tx]
ex[, rank := fifelse(strand == "-", n_ex - idx + 1L, idx)]
intr <- ex[n_ex > 1, .(seqnames = seqnames[1], start = end[-.N] + 1L, end = start[-1] - 1L,
strand = strand[1], n_ex = n_ex[1], idx = seq_len(.N - 1)), by = tx]
intr <- intr[end >= start]
intr[, rank := fifelse(strand == "-", n_ex - idx, idx)]
# Ranks 1-5, then "6+"; the final element of each transcript is pulled out as "last"
# (except a lone intron, which stays rank 1).
rank_meth <- function(dt, what) {
n_el <- if (what == "exon") dt$n_ex else dt$n_ex - 1L
label <- fifelse(dt$rank == n_el & dt$rank > 1, "last", as.character(pmin(dt$rank, 6)))
gr <- makeGRangesFromDataFrame(dt, seqinfo = si)
h <- findOverlaps(cpg, gr, ignore.strand = TRUE)
data.table(rank_label = label[subjectHits(h)],
meth = cpg$meth[queryHits(h)], cov = cpg$cov[queryHits(h)]
)[, .(meth_pct = 100 * sum(meth) / sum(cov), n_cpg = .N), by = rank_label
][, element := what][]
}
rank_tab <- rbind(rank_meth(ex[n_ex > 1], "exon"), rank_meth(intr, "intron")) %>%
mutate(rank_label = factor(recode(rank_label, `6` = "6+"),
c(as.character(1:5), "6+", "last")))
write_tsv(rank_tab, file.path(out_dir, "exon_intron_rank_methylation.tsv"))
ggplot(rank_tab, aes(rank_label, meth_pct, colour = element, group = element)) +
geom_point(size = 2) + geom_line() +
labs(x = "Rank from 5' end", y = "CpG methylation (%)", colour = NULL,
subtitle = "Multi-exon genes; 'last' = final exon / intron") + theme_bw()
Per-oyster gene-body methylation (reads summed over each gene) for genes with ≥ 30 calls in every oyster. In bivalves, heavily methylated genes tend to be the most stable among individuals.
gene_ind <- ps_gene[cov >= min_gene_calls & n_cpg >= min_gene_cpgs]
gene_ind <- gene_ind[, if (.N == nrow(samples)) .SD, by = gene_id]
gene_ind[, pct := 100 * meth / cov]
gene_var <- gene_ind[, .(mean_pct = mean(pct), sd_pct = sd(pct)), by = gene_id] %>%
as_tibble() %>%
left_join(select(gene_meth, gene_id, body_class), by = "gene_id")
write_tsv(gene_var, file.path(out_dir, "gene_methylation_variability.tsv"))
message(nrow(gene_var), " genes with per-oyster estimates in all ", nrow(samples), " oysters")
ggplot(gene_var, aes(mean_pct, sd_pct)) +
geom_bin2d(bins = 60) + geom_smooth(method = "loess", colour = "red") +
labs(x = "Mean gene-body methylation across oysters (%)",
y = "SD among oysters (% points)") + theme_bw()
# Between-oyster agreement in gene-body methylation (pairwise Spearman).
pm_gene <- dcast(gene_ind, gene_id ~ sample, value.var = "pct")
cc <- cor(as.matrix(pm_gene[, -1]), method = "spearman")
summary(cc[upper.tri(cc)])
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.8497 0.8581 0.8620 0.8611 0.8651 0.8687
Functional annotation from 13.5 (DIAMOND blastp of GN
proteins vs Swiss-Prot, E ≤ 1e-8, GO from the best hit). GO terms are
the direct UniProt annotations, not propagated to parent terms, so broad
terms are under-counted; treat the term tests as descriptive.
annot <- read_tsv(annot_file, show_col_types = FALSE,
col_types = cols(.default = col_character()))
gene_fun <- gene_meth %>%
left_join(select(annot, gene_id, accession, protein_name, go_bp), by = "gene_id") %>%
mutate(annotation = case_when(is.na(accession) ~ "no Swiss-Prot hit",
is.na(go_bp) ~ "hit, no GO BP",
TRUE ~ "hit with GO BP"),
annotation = factor(annotation, c("hit with GO BP", "hit, no GO BP",
"no Swiss-Prot hit")))
write_tsv(gene_fun, file.path(out_dir, "gene_methylation_annotated.tsv"))
Genes with a Swiss-Prot hit are mostly conserved; unannotated genes are enriched for lineage-specific and mis-predicted genes. Conserved genes are expected to carry more gene-body methylation.
gf <- filter(gene_fun, !is.na(body_class))
gf %>% count(annotation, body_class) %>%
group_by(annotation) %>% mutate(pct = 100 * n / sum(n)) %>% ungroup()
## # A tibble: 9 x 4
## annotation body_class n pct
## <fct> <fct> <int> <dbl>
## 1 hit with GO BP unmethylated (<10%) 2430 40.5
## 2 hit with GO BP sparse (10-50%) 306 5.1
## 3 hit with GO BP methylated (>=50%) 3264 54.4
## 4 hit, no GO BP unmethylated (<10%) 590 53.2
## 5 hit, no GO BP sparse (10-50%) 61 5.50
## 6 hit, no GO BP methylated (>=50%) 459 41.4
## 7 no Swiss-Prot hit unmethylated (<10%) 5083 77.8
## 8 no Swiss-Prot hit sparse (10-50%) 335 5.13
## 9 no Swiss-Prot hit methylated (>=50%) 1114 17.1
chisq.test(table(gf$annotation, gf$body_class))
##
## Pearson's Chi-squared test
##
## data: table(gf$annotation, gf$body_class)
## X-squared = 1983.4, df = 4, p-value < 2.2e-16
ggplot(gf, aes(annotation, body_pct, fill = annotation)) +
geom_violin(scale = "width") + geom_boxplot(width = 0.1, outlier.shape = NA, fill = "white") +
labs(x = NULL, y = "Gene-body methylation (%)") + theme_bw() +
theme(legend.position = "none")
Background: genes with a gene-body estimate and ≥ 1 GO BP term. Two tests per term (10–500 genes):
delta_median > 0 = term genes more methylated).go_long <- gf %>%
filter(!is.na(go_bp)) %>%
select(gene_id, body_pct, body_class, go_bp) %>%
separate_rows(go_bp, sep = "; ") %>%
mutate(go_id = str_extract(go_bp, "GO:\\d{7}"),
term = str_trim(str_remove(go_bp, "\\s*\\[GO:\\d{7}\\]$"))) %>%
filter(!is.na(go_id)) %>%
distinct(gene_id, go_id, .keep_all = TRUE)
bg <- distinct(go_long, gene_id, body_pct, body_class)
is_high <- set_names(bg$body_class == meth_labels[3], bg$gene_id)
is_low <- set_names(bg$body_class == meth_labels[1], bg$gene_id)
terms <- go_long %>% count(go_id, term) %>%
filter(n >= min_go_genes, n <= max_go_genes)
go_tests <- map_dfr(seq_len(nrow(terms)), function(k) {
ids <- go_long$gene_id[go_long$go_id == terms$go_id[k]]
in_t <- bg$gene_id %in% ids
wt <- wilcox.test(bg$body_pct[in_t], bg$body_pct[!in_t])
fisher_p <- function(flag) fisher.test(table(factor(in_t, c(TRUE, FALSE)),
factor(flag, c(TRUE, FALSE))),
alternative = "greater")$p.value
tibble(go_id = terms$go_id[k], term = terms$term[k], n_genes = sum(in_t),
median_meth = median(bg$body_pct[in_t]),
delta_median = median(bg$body_pct[in_t]) - median(bg$body_pct[!in_t]),
pct_methylated_genes = 100 * mean(is_high[in_t]),
wilcox_p = wt$p.value,
high_fisher_p = fisher_p(is_high),
low_fisher_p = fisher_p(is_low))
}) %>%
mutate(across(ends_with("_p"), ~ p.adjust(.x, "BH"), .names = "{.col}_adj")) %>%
arrange(wilcox_p)
write_tsv(go_tests, file.path(out_dir, "GO_BP_methylation_tests.tsv"))
message(nrow(bg), " background genes; ", nrow(terms), " GO BP terms tested; overall ",
round(100 * mean(is_high), 1), "% of background genes methylated")
go_tests %>% filter(wilcox_p_adj < 0.05) %>% count(direction = sign(delta_median))
## # A tibble: 2 x 2
## direction n
## <dbl> <int>
## 1 -1 195
## 2 1 126
top_go <- go_tests %>%
filter(wilcox_p_adj < 0.05) %>%
group_by(direction = if_else(delta_median > 0, "higher methylation", "lower methylation")) %>%
slice_min(wilcox_p, n = 15, with_ties = FALSE) %>% ungroup()
if (nrow(top_go) > 0) {
top_go %>%
mutate(term = fct_reorder(str_trunc(term, 60), median_meth)) %>%
ggplot(aes(median_meth, term, size = n_genes, colour = -log10(wilcox_p_adj))) +
geom_point() +
geom_vline(xintercept = median(bg$body_pct), linetype = 2) +
facet_grid(direction ~ ., scales = "free_y", space = "free_y") +
scale_colour_viridis_c() +
labs(x = "Median gene-body methylation of term genes (%)", y = NULL,
subtitle = "Dashed line: background median") + theme_bw()
}
A coarse view, grouping genes by keywords in their GO BP term names. The groups follow the housekeeping-vs-inducible contrast often reported for bivalve gene-body methylation (constitutively expressed, housekeeping functions methylated; environmentally responsive functions less so). Genes can fall into more than one group; edit the patterns to suit.
func_groups <- c(
"Translation / ribosome" = "translation|ribosom|rRNA processing",
"RNA processing / splicing" = "mRNA processing|RNA splicing|spliceosom",
"DNA replication / repair" = "DNA replication|DNA repair",
"Cell cycle" = "cell cycle|cell division|mitotic",
"Mitochondria / energy" = "mitochondri|electron transport|tricarboxylic|ATP synthesis",
"Protein degradation" = "ubiquitin|proteasom|protein catabolic",
"Signal transduction" = "signal transduction|signaling pathway|G protein-coupled",
"Immune / defense" = "immune|defense response|inflammatory|innate",
"Stress response" = "response to stress|oxidative stress|heat|hypoxia|unfolded protein",
"Development / differentiation" = "development|morphogenesis|differentiation")
group_meth <- imap_dfr(func_groups, function(pat, grp) {
go_long %>% filter(str_detect(term, regex(pat, ignore_case = TRUE))) %>%
distinct(gene_id, body_pct) %>% mutate(group = grp)
})
group_stats <- group_meth %>%
group_by(group) %>%
summarise(n_genes = n(), median_meth = median(body_pct),
pct_methylated_genes = 100 * mean(body_pct >= 50),
wilcox_p = wilcox.test(body_pct, bg$body_pct[!bg$gene_id %in% gene_id])$p.value) %>%
mutate(wilcox_p_adj = p.adjust(wilcox_p, "BH")) %>%
arrange(dplyr::desc(median_meth))
write_tsv(group_stats, file.path(out_dir, "functional_group_methylation.tsv"))
group_stats
## # A tibble: 10 x 6
## group n_genes median_meth pct_methylated_genes wilcox_p wilcox_p_adj
## <chr> <int> <dbl> <dbl> <dbl> <dbl>
## 1 RNA processin~ 172 85.9 83.1 3.04e-19 1.01e-18
## 2 Translation /~ 325 84.0 83.1 5.59e-33 5.59e-32
## 3 Cell cycle 438 83.3 69.4 1.80e-20 9.02e-20
## 4 Mitochondria ~ 256 82.0 80.5 4.26e-16 7.10e-16
## 5 DNA replicati~ 213 81.8 73.2 1.88e-11 2.35e-11
## 6 Protein degra~ 566 80.4 69.4 8.22e-18 2.06e-17
## 7 Stress respon~ 297 66.6 51.2 2.85e- 1 2.85e- 1
## 8 Development /~ 1468 27.2 46.6 3.14e- 9 3.48e- 9
## 9 Signal transd~ 1390 14.4 45.7 3.63e-16 7.10e-16
## 10 Immune / defe~ 467 1.22 37.7 2.21e-14 3.15e-14
group_meth %>%
mutate(group = factor(group, group_stats$group)) %>%
ggplot(aes(body_pct, group)) +
geom_boxplot(outlier.size = 0.3) +
geom_vline(xintercept = median(bg$body_pct), linetype = 2) +
labs(x = "Gene-body methylation (%)", y = NULL,
subtitle = "Dashed line: median of all genes with GO BP") + theme_bw()
summary_tab <- tribble(
~metric, ~value,
"CpGs in assembly", length(genome_cpg),
"CpGs passing pooled filters", length(cpg),
"mean global CpG methylation, per oyster (%)", mean(global$global_meth_pct),
"methylated CpGs (>=50%, pooled) (%)", 100 * mean(cpg$pct >= 50),
"genes with gene-body estimate", nrow(gbm),
"methylated genes (>=50%) (%)", 100 * mean(gbm$body_class == meth_labels[3]),
"unmethylated genes (<10%) (%)", 100 * mean(gbm$body_class == meth_labels[1]),
"rho gene methylation vs CpG O/E", cor(gbm$body_pct, gbm$cpg_oe, method = "spearman",
use = "complete.obs"),
"rho window methylation vs gene coverage", cor(windows$meth_pct, windows$gene_frac,
method = "spearman"),
"GO BP terms tested", nrow(go_tests),
"GO BP terms, Wilcoxon FDR < 0.05", sum(go_tests$wilcox_p_adj < 0.05)
) %>% mutate(value = signif(value, 4))
write_tsv(summary_tab, file.path(out_dir, "landscape_summary.tsv"))
summary_tab
## # A tibble: 11 x 2
## metric value
## <chr> <dbl>
## 1 CpGs in assembly 26100000
## 2 CpGs passing pooled filters 14430000
## 3 mean global CpG methylation, per oyster (%) 20.2
## 4 methylated CpGs (>=50%, pooled) (%) 18.2
## 5 genes with gene-body estimate 13640
## 6 methylated genes (>=50%) (%) 35.5
## 7 unmethylated genes (<10%) (%) 59.4
## 8 rho gene methylation vs CpG O/E -0.589
## 9 rho window methylation vs gene coverage 0.365
## 10 GO BP terms tested 839
## 11 GO BP terms, Wilcoxon FDR < 0.05 321
merged_out.fasta, set
repeat_gff, and the repeat chunk will split each feature
into repeat vs non-repeat; intronic TEs are a common source of gene-body
methylation.gene_methylation.tsv to per-gene expression from
09 (mean and CV of normalized counts across the 12 RNA-seq
oysters) on GN gene ID. These are different animals from the WGBS
oysters, so this is a gene-level, not individual-level, comparison.GO.db (or
topGO), propagate annotations to ancestors before the term
tests; that will mainly add power to the broad terms.13.4.sessionInfo()
## R version 4.4.3 (2025-02-28)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.4 LTS
##
## Matrix products: default
## BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
##
## locale:
## [1] C
##
## time zone: Etc/UTC
## tzcode source: system (glibc)
##
## attached base packages:
## [1] stats4 stats graphics grDevices utils datasets methods
## [8] base
##
## other attached packages:
## [1] Rsamtools_2.22.0 Biostrings_2.74.1 XVector_0.46.0
## [4] rtracklayer_1.66.0 GenomicRanges_1.58.0 GenomeInfoDb_1.42.3
## [7] IRanges_2.40.1 S4Vectors_0.44.0 BiocGenerics_0.52.0
## [10] data.table_1.17.0 lubridate_1.9.4 forcats_1.0.0
## [13] stringr_1.5.1 dplyr_1.1.4 purrr_1.0.4
## [16] readr_2.1.5 tidyr_1.3.1 tibble_3.2.1
## [19] ggplot2_3.5.2 tidyverse_2.0.0
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 viridisLite_0.4.2
## [3] farver_2.1.2 bitops_1.0-9
## [5] fastmap_1.2.0 RCurl_1.98-1.19
## [7] GenomicAlignments_1.42.0 XML_3.99-0.18
## [9] digest_0.6.37 timechange_0.3.0
## [11] lifecycle_1.0.4 magrittr_2.0.3
## [13] compiler_4.4.3 rlang_1.1.5
## [15] sass_0.4.9 tools_4.4.3
## [17] utf8_1.2.4 yaml_2.3.10
## [19] knitr_1.50 S4Arrays_1.6.0
## [21] labeling_0.4.3 bit_4.6.0
## [23] curl_6.2.2 DelayedArray_0.32.0
## [25] RColorBrewer_1.1-3 abind_1.4-8
## [27] BiocParallel_1.40.2 withr_3.0.2
## [29] grid_4.4.3 colorspace_2.1-1
## [31] scales_1.3.0 SummarizedExperiment_1.36.0
## [33] cli_3.6.4 rmarkdown_2.29
## [35] crayon_1.5.3 generics_0.1.3
## [37] httr_1.4.7 tzdb_0.5.0
## [39] rjson_0.2.23 cachem_1.1.0
## [41] zlibbioc_1.52.0 splines_4.4.3
## [43] parallel_4.4.3 restfulr_0.0.17
## [45] matrixStats_1.5.0 vctrs_0.6.5
## [47] Matrix_1.7-3 jsonlite_2.0.0
## [49] hms_1.1.3 bit64_4.6.0-1
## [51] jquerylib_0.1.4 glue_1.8.0
## [53] codetools_0.2-20 stringi_1.8.7
## [55] gtable_0.3.6 BiocIO_1.16.0
## [57] UCSC.utils_1.2.0 munsell_0.5.1
## [59] pillar_1.10.2 htmltools_0.5.8.1
## [61] GenomeInfoDbData_1.2.13 R6_2.6.1
## [63] vroom_1.6.5 evaluate_1.0.3
## [65] Biobase_2.66.0 lattice_0.22-7
## [67] bslib_0.9.0 SparseArray_1.6.2
## [69] nlme_3.1-168 mgcv_1.9-3
## [71] xfun_0.52 MatrixGenerics_1.18.1
## [73] pkgconfig_2.0.3