Hasenauer FC, Barreto HC, Lotton C, Matic I (2025). Genome-wide mapping of spontaneous DNA replication error-hotspots using mismatch repair proteins in rapidly proliferating Escherichia coli. Nucleic Acids Research 53(2): gkae1196. doi:10.1093/nar/gkae1196.
Spontaneous DNA replication errors are not randomly distributed along the Escherichia coli chromosome. Hasenauer et al. (2025) mapped these hotspots genome-wide by tagging the mismatch-repair protein MutL in rapidly proliferating mutH-deficient cells, where mismatches can be detected but not corrected. MutL ChIP-seq peaks (MutL-associated regions, MutL-AR) are enriched for sequences of lower thermal stability, mononucleotide repeats (microsatellites), cruciform-forming DNA, and single-stranded DNA, and are depleted for GATC methylation sites. These associations motivate a direct comparison between replication-error hotspots and local DNA melting temperature (Tm).
This vignette uses TmCalculator to compute Tm across
the E. coli K-12 MG1655 genome (NCBI assembly GCF_000005845.2 /
ASM584v2) for every non-overlapping 200 bp window – the bin size used by
Hasenauer et al. themselves – then overlays their annotation
tracks (shipped as ecoli_rep_hotspots) to ask whether
MutL-AR windows differ in Tm and GC content from the rest of the
chromosome. Because any window-based Tm profile depends on the chosen
bandwidth, we complement the primary 200 bp analysis with a window-size
sensitivity analysis (50, 100, 200 and 500 bp windows). Briefly,
absolute Tm scales with window length as expected from duplex
thermodynamics, but the spatial landscape is invariant (cross-scale
correlations >= 0.975) and the MutL-AR association – about 1 degree C
lower median Tm and ~2.7 percentage points lower GC inside peaks – is
reproduced at every window size (all p < 1e-12), so the conclusions
do not hinge on the 200 bp choice.
The workflow covers five steps:
make_genomiccoord().tm_calculate().plot_genome_track() — circular plots, linear plots, zoom
views, and multi-omics overlays.compare_groups().TmCalculator retrieves sequences from BSgenome data
packages. A prebuilt E. coli package is available on
Bioconductor (BSgenome.Ecoli.NCBI.20080805), but it is an
older multi-strain snapshot and does not provide the RefSeq assembly
used here (GCF_000005845.2, ASM584v2, sequence U00096.3). Because the
MutL-AR peaks, microsatellite, cruciform, ssDNA, and GATC tracks are all
defined against this assembly, we forge a matching BSgenome package
locally from the NCBI RefSeq sequence using
BSgenomeForge, ensuring that genome sequence and
feature coordinates share a single reference. This step only needs to be
run once.
library(BSgenomeForge)
forgeBSgenomeDataPkgFromNCBI(
assembly_accession = "GCF_000005845.2",
pkg_maintainer = "Junhui Li <ljh.biostat@gmail.com>",
destdir = "."
)
install.packages(
"./BSgenome.Ecoli.NCBI.ASM584v2",
repos = NULL,
type = "source"
)During TmCalculator package development and vignette
building (e.g. R CMD check), we cannot call
forgeBSgenomeDataPkgFromNCBI() in the vignette environment,
so the chunks below install the pre-forged
BSgenome.Ecoli.NCBI.ASM584v2 package from GitHub
instead.
ecoli_pkg <- "BSgenome.Ecoli.NCBI.ASM584v2"
genome_obj <- "Ecoli" # BSgenomeObjname in DESCRIPTION; not the package name
.ecoli_genome_ready <- function() {
if (!requireNamespace(ecoli_pkg, quietly = TRUE)) return(FALSE)
exists(genome_obj, envir = asNamespace(ecoli_pkg), inherits = FALSE)
}
if (!requireNamespace(ecoli_pkg, quietly = TRUE)) {
if (!requireNamespace("remotes", quietly = TRUE)) {
utils::install.packages("remotes", repos = "https://cloud.r-project.org")
}
remotes::install_github(
"JunhuiLi1017/BSgenome.Ecoli.NCBI.ASM584v2",
upgrade = "never",
quiet = TRUE
)
}
if (!.ecoli_genome_ready()) {
if (!requireNamespace("BiocManager", quietly = TRUE)) {
utils::install.packages("BiocManager", repos = "https://cloud.r-project.org")
}
if (!requireNamespace("BSgenomeForge", quietly = TRUE)) {
BiocManager::install("BSgenomeForge", ask = FALSE, update = FALSE)
}
pkgdir <- BSgenomeForge::forgeBSgenomeDataPkgFromNCBI(
assembly_accession = "GCF_000005845.2",
pkg_maintainer = "Junhui Li <ljh.biostat@gmail.com>",
destdir = tempdir()
)
utils::install.packages(pkgdir, repos = NULL, type = "source", quiet = TRUE)
if (ecoli_pkg %in% loadedNamespaces()) {
unloadNamespace(ecoli_pkg)
}
}
if (!.ecoli_genome_ready()) {
stop(
"Could not load genome object '", genome_obj, "' from package '", ecoli_pkg, "'.\n",
"Run the manual 'forge' chunk above, or forgeBSgenomeDataPkgFromNCBI() locally.",
call. = FALSE
)
}Load the genome (object Ecoli; package
BSgenome.Ecoli.NCBI.ASM584v2 for coordinates):
ecoli_pkg <- "BSgenome.Ecoli.NCBI.ASM584v2"
genome_obj <- "Ecoli"
suppressPackageStartupMessages(library(ecoli_pkg, character.only = TRUE))
genome <- base::get(genome_obj, envir = asNamespace(ecoli_pkg))
genome_name <- ecoli_pkg
chr_name <- "U00096.3"
chr_length <- length(genome[[chr_name]])
cat("Chromosome:", chr_name, "\n")## Chromosome: U00096.3
## Length: 4,641,652 bp
make_genomiccoord() tiles the chromosome into
non-overlapping 200 bp bins.
runtime0 <- system.time({
bins_gc <- make_genomiccoord(
bsgenome = genome_name,
chromosomes = chr_name,
window = 200L,
slide = 200L,
start = 1,
end = chr_length,
strand = "+"
)
})
cat("Total windows:", length(bins_gc), "\n")## Total windows: 23208
## Window generation: 0.46 s (elapsed)
Resolve coordinates against the BSgenome package:
input_new <- list(pkg_name = genome_name, seq = bins_gc)
runtime1 <- system.time({
gr_batch <- to_genomic_ranges_fast(input_new)
})
cat(sprintf(
"Coordinate resolution: %.2f s (elapsed)\n",
runtime1["elapsed"]
))## Coordinate resolution: 0.51 s (elapsed)
We use nearest-neighbour thermodynamics with the Breslauer et al. (1986) parameter set at 50 mM Na+, matching the Methods of Hasenauer et al. (2025), who computed Tm with this package and the same parameters.
runtime2 <- system.time({
tm_ASM584v2 <- tm_calculate(
gr_batch,
method = "tm_nn",
nn_table = "DNA_NN_Breslauer_1986",
Na = 50 # mM; standard PCR-like conditions
)
})
cat(sprintf(
"Tm calculation: %.2f s (elapsed) for %s windows\n",
runtime2["elapsed"],
format(length(bins_gc), big.mark = ",")
))## Tm calculation: 0.59 s (elapsed) for 23,208 windows
## Tm GC
## Min. : 81.05 Min. :20.50
## 1st Qu.: 97.07 1st Qu.:47.50
## Median : 99.61 Median :52.00
## Mean : 98.91 Mean :50.79
## 3rd Qu.:101.49 3rd Qu.:55.00
## Max. :111.36 Max. :75.00
plot_genome_track()plot_genome_track() is the unified plotting function in
TmCalculator. It supports both linear
(karyoploteR-based) and circular (base R graphics)
layouts from the same track list, with features including ideogram
tracks, per-track highlights, proportional track heights, multi-region
zoom, and customisable legends.
We overlay the Tm/GC profile with the Hasenauer et al.
(2025) multi-omics layers from ecoli_rep_hotspots:
| Track | Description |
|---|---|
| MutL-AR | MutL ChIP-seq peaks marking replication-error / mismatch-repair hotspots |
| Microsatellites | Tandem-repeat (mononucleotide-repeat) density per 1 kb bin |
| Cruciform | Cruciform-forming sequence density per 1 kb bin |
| ssDNA | Single-stranded DNA regions enriched near error hotspots |
| GATC sites | Dam methylation-site (5’-GATC-3’) density per 1 kb bin |
# Reference labels: replication origin (ori) and terminus (dif)
label <- data.frame(
seqnames = genome_name,
start = c(3925804, 1590777),
end = c(3925804, 1590777),
label = c("ori", "dif")
)
data(ecoli_rep_hotspots)
tracks <- list(
# Ideogram: MutL-AR peaks shown inside the chromosome bar
list(type = "rect", data = ecoli_rep_hotspots$all_peaks_IP_mutH,
col = "#2C3E50", bg.col = "grey", name = "MutL-AR",
legend_font_col = "#2C3E50", ideogram = TRUE, height = 0.5),
# Sequence thermodynamics
list(type = "line", data = Tm, value_col = "GC",
name = "GC content", col = "#4A90E2",
legend_font_col = "#4A90E2"),
list(type = "line", data = Tm, value_col = "Tm",
name = "Melting temp", col = "#E06666",
legend_font_col = "#E06666", height = 2),
# Repeat / structural features
list(type = "line", data = ecoli_rep_hotspots$bins_rep,
value_col = "count", name = "Microsatellites", col = "#2ECC71",
legend_font_col = "#2ECC71"),
list(type = "line", data = ecoli_rep_hotspots$bins_cru,
value_col = "count", name = "Cruciform", col = "#3B3E6B",
legend_font_col = "#3B3E6B"),
# ssDNA regions
list(data = ecoli_rep_hotspots$ssdna, name = "ssDNA",
col = "#8E44AD", legend_font_col = "#8E44AD"),
# GATC methylation sites
list(type = "line", data = ecoli_rep_hotspots$bins_gatc,
value_col = "count", name = "GATC sites", col = "#D35400",
legend_font_col = "#D35400"),
# Global highlight: translucent bands at MutL-AR peaks across all tracks
list(type = "highlight", data = ecoli_rep_hotspots$all_peaks_IP_mutH,
col = "#F1C40F", alpha = 0.18)
)plot_genome_track(
genome_name = genome_name,
genome_size = chr_length,
track_list = tracks,
circular = TRUE,
label = label
)Circular genome map of E. coli K-12 MG1655. Concentric rings from outside in: MutL-AR peaks (grey ideogram), GC content, melting temperature, microsatellite density, cruciform sequences, ssDNA regions, and GATC site density. Yellow highlight bands mark MutL-AR peak regions.
The same tracks list works for a linear karyoploteR
layout — simply omit circular = TRUE. The ideogram track is
drawn inside the chromosome bar; all other tracks are stacked as
horizontal panels.
Linear genome view of E. coli K-12 MG1655. MutL-AR peaks are drawn inside the chromosome ideogram bar.
The zoom parameter accepts a character string
(e.g. "chr:start-end") or a GRanges object. In linear mode,
the karyoploteR view is restricted to that region; in circular mode,
only data overlapping the region is drawn.
plot_genome_track(
genome_name = genome_name,
genome_size = chr_length,
track_list = tracks,
zoom = "U00096.3:100000-500000",
## Seven tracks in a linear panel leave each one little vertical room. The
## gap is relative to the panel, so it has to grow with the track count.
track.gap = 0.03,
axis.cex = 0.55
)
Zoomed linear view of the 0.1-0.5 Mb region. Seven tracks share one
panel, so track.gap separates them and
axis.cex shrinks the tick labels; without both the y-axis
labels of adjacent tracks overlap.
Pass a character vector to zoom to view several disjoint
regions at once. In linear mode each region is drawn as a separate
stacked panel; in circular mode the regions are concatenated around the
circle with small gaps between them.
plot_genome_track(
genome_name = genome_name,
genome_size = chr_length,
track_list = tracks,
circular = TRUE,
zoom = c("U00096.3:100000-500000",
"U00096.3:3600000-4500000")
)Two zoomed regions, 0.1-0.5 Mb and 3.6-4.5 Mb, concatenated around the circle with a gap between them.
Use canvas.xlim and canvas.ylim to pan and
magnify a portion of the circular plot. The circle.margin
parameter controls whitespace around the plot.
plot_genome_track(
genome_name = genome_name,
genome_size = chr_length,
track_list = tracks,
circular = TRUE,
canvas.xlim = c(0.5, 1),
canvas.ylim = c(0, 1),
circle.margin = c(0.05, 0.05)
)Panned circular view showing the upper-right quadrant of the E. coli chromosome.
Individual tracks can carry a highlight field to draw
coloured bands within that track only. This is useful for marking
specific regions of interest on a per-track basis:
tracks_hl <- tracks
tracks_hl[[4]]$highlight <- list(
data = ecoli_rep_hotspots$bins_rep[1100:1200, ],
col = "black",
alpha = 0.12
)
plot_genome_track(
genome_name = genome_name,
genome_size = chr_length,
track_list = tracks_hl,
circular = TRUE
)Circular plot with per-track highlight bands on the Microsatellites track.
The tracks above show GC content and Tm as two separate rings, which raises the obvious question of whether the second adds anything to the first. It does, and the genome-wide profile is enough to measure how much.
Group the windows by GC content. Within a group every window has the same length, the same salt, and the same GC percentage, so an empirical formula whose only sequence input is GC percentage assigns all of them a single temperature. A nearest-neighbour model reads the dinucleotide stacks and does not.
## The GC-content prediction for the same windows, for comparison.
tm_gc_ecoli <- tm_calculate(gr_batch, method = "tm_gc",
variant = "Schildkraut1965", Na = 50)
GCmod <- as.data.frame(tm_gc_ecoli$gr[, c("Tm", "GC")])
## Keep only windows whose GC percentage is an exact integer. A 200 bp
## window can only take GC values in steps of 0.5%, so binning 49.5% with
## 50.0% would put a real GC difference inside a group and some of the
## spread below could be attributed to GC rather than to composition.
## The same test also removes windows that contained an ambiguous base,
## whose GC denominator is smaller than the window.
prep <- function(d) {
d <- d[!is.na(d$Tm) & !is.na(d$GC) & d$width == 200L, ]
d <- d[d$GC == round(d$GC), ]
d$GCi <- as.integer(d$GC)
d
}
Cnn <- prep(as.data.frame(tm_ASM584v2$gr[, c("Tm", "GC")]))
Cgc <- prep(GCmod)
cnt <- table(Cnn$GCi)
lv <- sort(as.integer(names(cnt[cnt >= 30]))) # enough windows to be stable
Cnn <- Cnn[Cnn$GCi %in% lv, ]
Cgc <- Cgc[Cgc$GCi %in% lv, ]
modal <- lv[which.max(cnt[as.character(lv)])]
at_mode <- Cnn$Tm[Cnn$GCi == modal]
cat(sprintf("At %d%% GC: %s windows, Tm from %.2f to %.2f (spread %.2f C)\n",
modal, format(length(at_mode), big.mark = ","),
min(at_mode), max(at_mode), diff(range(at_mode))))## At 53% GC: 927 windows, Tm from 97.92 to 102.52 (spread 4.60 C)
cat(sprintf("GC-content formula predicts a single value: %.2f C\n",
median(Cgc$Tm[Cgc$GCi == modal])))## GC-content formula predicts a single value: 78.26 C
cat(sprintf("Largest spread at any GC value: %.2f C\n",
max(tapply(Cnn$Tm, Cnn$GCi, function(v) diff(range(v))))))## Largest spread at any GC value: 6.07 C
gc_line <- tapply(Cgc$Tm, Cgc$GCi, median)[as.character(lv)]
op <- par(mar = c(4.6, 4.6, 1.2, 1.2), las = 1, mgp = c(2.9, 0.7, 0))
boxplot(Tm ~ factor(GCi, levels = lv), data = Cnn, at = seq_along(lv),
outline = FALSE, border = "#34495E", col = "#D6E4F0", lwd = 0.7,
xaxt = "n", bty = "n", xlab = "GC content (%)",
ylab = expression(paste("Melting temperature (", degree, "C)")))
sel <- seq(1, length(lv), by = 5) # one label per box is unreadable
axis(1, at = seq_along(lv)[sel], labels = lv[sel])
lines(seq_along(lv), gc_line, col = "#C0392B", lwd = 2.2)
legend("topleft", bty = "n", cex = 0.85,
legend = c("Nearest-neighbour (Breslauer 1986)",
"GC-content formula (Schildkraut 1965)"),
col = c("#34495E", "#C0392B"), lwd = c(0.7, 2.2), seg.len = 1.4)Melting temperature against GC content for 200 bp windows of the E. coli chromosome. Each box holds windows of identical length, salt and GC content. The red line is the GC-content formula, which by construction has no width within a group; the boxes do, and that width is the composition effect the formula cannot represent.
Two things to read off this, and one not to.
The boxes have real width. Windows that agree on GC content to the base still differ in Tm, because AA/TT, AT/TA and TA/AT stacks are not interchangeable and neither are the ten GC-containing ones. That difference is the whole content of a nearest-neighbour model, and it is invisible to any formula parameterised on GC percentage alone.
The width is not uniform. It is largest in the middle of the GC range, where the number of distinct sequences compatible with a given GC content is largest, and narrows at both extremes, where composition is increasingly constrained. A GC-content formula is therefore least reliable exactly where most of the genome sits.
What should not be read off it is the vertical offset between the line and the boxes. The two are different parameterisations calibrated against different reference conditions, and comparing their absolute values is a separate question from the one this panel asks. The claim here concerns the spread at fixed GC, which is a property of the sequences and does not depend on where either scale is anchored.
The distribution above says the effect exists across the genome. To see it in one place, scan for the locus in which two windows of exactly equal GC content are furthest apart in Tm, then zoom to it.
W <- as.data.frame(tm_ASM584v2$gr[, c("Tm", "GC")])
W <- W[!is.na(W$Tm) & !is.na(W$GC) & W$width == 200L, ]
W <- W[order(W$start), ]
## Which windows lie inside a MutL-AR peak. Matched on coordinates alone:
## the peak table and the window table both describe one contig, and
## matching on sequence names would fail silently if one calls it by the
## accession and the other by the genome package name.
peaks <- as.data.frame(ecoli_rep_hotspots$all_peaks_IP_mutH)
W$in_peak <- IRanges::overlapsAny(
IRanges::IRanges(W$start, W$end),
IRanges::IRanges(as.numeric(peaks$start), as.numeric(peaks$end)))
## The k GC values inside a locus whose windows are furthest apart in Tm.
## Each group contributes its two extreme windows; shading every window at
## that GC would fill the panel with stripes and hide the tracks.
top_groups <- function(d, k = 3L) {
g <- split(seq_len(nrow(d)), d$GC)
g <- g[lengths(g) >= 2L]
if (!length(g)) return(list())
sp <- vapply(g, function(ix) diff(range(d$Tm[ix])), numeric(1))
o <- order(sp, decreasing = TRUE)[seq_len(min(k, length(g)))]
lapply(o, function(j) {
ix <- g[[j]]
list(gc = as.numeric(names(g)[j]), spread = unname(sp[j]),
rows = ix[c(which.min(d$Tm[ix]), which.max(d$Tm[ix]))])
})
}
n_loc <- 100L # 100 x 200 bp = 20 kb
starts <- seq_len(nrow(W) - n_loc + 1L)
## Two restrictions on the candidate loci.
##
## Contiguity: a locus is n_loc consecutive ROWS of W, and W has had short
## and ambiguous windows dropped, so consecutive rows need not be adjacent
## on the chromosome. A locus spanning such a gap would be drawn with an
## axis covering far more than 20 kb and would quietly stop being a zoom.
##
## Peaks: the panel is meant to say something about the regions this study
## is about, not only about the model, so only loci centred on a MutL-AR
## peak are considered.
contig <- (W$start[seq(n_loc, nrow(W))] - W$start[starts]) == (n_loc - 1L) * 200L
starts <- starts[contig]
starts <- starts[W$in_peak[pmin(starts + n_loc %/% 2L, nrow(W))]]
## Scored on the SUM of the top three spreads rather than the single best
## one: a locus with one spectacular pair and nothing around it reads as a
## peculiar sequence, whereas three groups at three GC values read as a
## property of the model.
sc <- vapply(starts, function(i)
sum(vapply(top_groups(W[i:(i + n_loc - 1L), ]), function(g) g$spread,
numeric(1))), numeric(1))
i0 <- starts[which.max(sc)]
d <- W[i0:(i0 + n_loc - 1L), ]
gs <- top_groups(d)
do.call(rbind, lapply(gs, function(g)
data.frame(GC = g$gc, Tm_low = min(d$Tm[g$rows]), Tm_high = max(d$Tm[g$rows]),
difference = g$spread)))## GC Tm_low Tm_high difference
## 1 50.5 96.12472 100.53795 4.413234
## 2 48.0 94.43605 98.73723 4.301182
## 3 54.0 98.61195 102.73552 4.123567
grp_cols <- c("#B7791F", "#2E8B57", "#7D3C98")
tracks_zoom <- c(
list(
## The MutL-AR peaks are drawn as the ideogram, in the same black used
## in the whole-genome map, so the three panels agree on what a peak
## looks like. As the ideogram it also names the chromosome bar, which
## is why `genome_name` below is the track name rather than the contig.
list(type = "rect", data = ecoli_rep_hotspots$all_peaks_IP_mutH,
col = "black", bg.col = "grey", name = "MutL-AR",
legend_font_col = "black", ideogram = TRUE, height = 0.5),
list(type = "line", data = Tm, value_col = "GC", name = "GC",
col = "#4A90E2", legend_font_col = "#4A90E2", height = 1),
list(type = "line", data = Tm, value_col = "Tm", name = "Tm",
col = "#E06666", legend_font_col = "#E06666", height = 1.4),
list(type = "highlight", data = ecoli_rep_hotspots$all_peaks_IP_mutH,
col = "#F1C40F", alpha = 0.18)),
lapply(seq_along(gs), function(i)
list(type = "highlight", data = d[gs[[i]]$rows, c("seqnames", "start", "end")],
col = grp_cols[i], alpha = 0.40)))
plot_genome_track(
genome_name = "MutL-AR",
genome_size = chr_length,
track_list = tracks_zoom,
zoom = sprintf("%s:%d-%d", chr_name, min(d$start), max(d$end)),
track.gap = 0.06,
axis.cex = 0.8,
legend.show = FALSE,
## Without this the panel carries no base positions at all: the default
## tick spacing is 500 kb for any view under 10 Mb, which places no tick
## inside a 20 kb window.
base.tick.dist = 5000,
base.tick.units = TRUE
)
## plot_genome_track() drops highlight entries before building its legend,
## so the shaded groups would otherwise be unlabelled.
legend("topright", bty = "n", cex = 0.8, border = NA,
legend = c("GC", "Tm", "MutL-AR peak",
vapply(gs, function(g)
sprintf("GC %.1f%%: Tm %.1f-%.1f", g$gc,
min(d$Tm[g$rows]), max(d$Tm[g$rows])),
character(1))),
fill = c("#4A90E2", "#E06666",
adjustcolor("#F1C40F", alpha.f = 0.18),
adjustcolor(grp_cols, alpha.f = 0.40)),
text.col = c("#4A90E2", "#E06666", "#B7950B", grp_cols))High-magnification view of a 20 kb locus centred on a MutL-AR peak. Three pairs of windows are shaded, one pair per colour; the two windows of a pair have identical length and identical GC content and differ only in the arrangement of their bases. Yellow marks the MutL-AR peaks. Only the GC and Tm tracks are drawn: seven tracks in one linear panel leave each of them too little height to read at this scale.
The GC track is flat across each shaded pair and the Tm track is not.
That is the whole argument for a nearest-neighbour model in one picture,
and it is the panel used as Figure 2C in the manuscript; the combined
three-panel figure is produced by
inst/scripts/make_figure2.R.
Two caveats worth stating plainly. This locus was selected
because it is extreme, so it shows a large effect rather than a
typical one; the box plot above is the honest summary and the two belong
together. And the pairs are extreme within the locus but need not both
lie inside a peak: the search places the locus on a peak, not every
shaded window. Requiring both windows of a pair to be inside a peak is
one argument away in inst/scripts/make_figure2.R
(--pairs-in-peaks) and lowers the effect.
We test whether Tm values inside MutL-AR peaks differ significantly from the rest of the genome.
mutH_peaks$peak_id <- paste0("mutH_", seq_along(mutH_peaks))
tm_annot <- integrate_granges(
gr_tm = tm_ASM584v2$gr,
gr_features = mutH_peaks,
strategy = "overlap",
feature_cols = "peak_id",
keep_unmatched = TRUE
)
tm_annot$in_mutH <- ifelse(is.na(tm_annot$peak_id), "non_peak", "peak")
table(tm_annot$in_mutH)##
## non_peak peak
## 22402 806
res <- compare_groups(
gr = tm_annot,
target = c("Tm", "GC"),
method = "wilcoxon",
group = "in_mutH",
alternative = "greater",
posthoc = FALSE
)
res$results## target group method test statistic df p.value n_groups
## 1 Tm in_mutH wilcoxon Wilcoxon rank-sum 11650602 NA 4.824689e-45 2
## 2 GC in_mutH wilcoxon Wilcoxon rank-sum 10806568 NA 8.549859e-22 2
## n_total group_levels group_n
## 1 23208 non_peak | peak non_peak=22402, peak=806
## 2 23208 non_peak | peak non_peak=22402, peak=806
## group n mean sd median target
## 1 non_peak 22402 98.97778 3.791850 99.65325 Tm
## 2 peak 806 96.96551 4.206166 97.88956 Tm
## 3 non_peak 22402 50.88340 6.346952 52.00000 GC
## 4 peak 806 48.22333 7.394710 50.00000 GC
MutL-AR-associated windows have lower Tm and lower GC content than the rest of the genome (p < 0.001 for both; median Tm about 1 degree C lower and mean GC 0.48 vs 0.51 inside peaks), consistent with the enrichment of low-thermal-stability, AT-rich sequence reported by Hasenauer et al. MutL-AR peaks cluster near the replication terminus, where replication forks converge and mismatch density is highest. This spatial coincidence with locally elevated microsatellite density and cruciform-forming sequences is consistent with the replication stress model of MMR recruitment.
GATC methylation sites are distributed genome-wide but show local density fluctuations that partially anti-correlate with GC content, reflecting the sequence context requirements for Dam methyltransferase (5’-GATC-3’).
On a standard desktop computer (single core, R 4.4, compiled Rcpp nearest-neighbor core):
| Step | Function | Time (elapsed) |
|---|---|---|
| Window generation | make_genomiccoord() |
~0.46 s |
| Sequence extraction | to_genomic_ranges_fast() |
~0.51 s |
| Tm calculation (23,208 windows) | tm_calculate() |
~0.59 s |
| Total | ~1.57 s |
## R version 4.4.1 (2024-06-14)
## Platform: x86_64-apple-darwin20
## Running under: macOS Sonoma 14.6
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
##
## locale:
## [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: America/New_York
## tzcode source: internal
##
## attached base packages:
## [1] stats4 stats graphics grDevices utils datasets methods
## [8] base
##
## other attached packages:
## [1] BSgenome.Ecoli.NCBI.ASM584v2_1.0.0 BSgenome_1.72.0
## [3] rtracklayer_1.64.0 BiocIO_1.14.0
## [5] Biostrings_2.72.1 XVector_0.44.0
## [7] GenomicRanges_1.56.2 GenomeInfoDb_1.40.1
## [9] IRanges_2.38.1 S4Vectors_0.42.1
## [11] BiocGenerics_0.50.0 TmCalculator_1.1.0
##
## loaded via a namespace (and not attached):
## [1] DBI_1.2.3 bitops_1.0-9
## [3] gridExtra_2.3 rlang_1.1.7
## [5] magrittr_2.0.4 biovizBase_1.52.0
## [7] otel_0.2.0 matrixStats_1.5.0
## [9] compiler_4.4.1 RSQLite_2.4.0
## [11] GenomicFeatures_1.56.0 png_0.1-8
## [13] vctrs_0.7.1 ProtGenerics_1.36.0
## [15] stringr_1.6.0 pkgconfig_2.0.3
## [17] crayon_1.5.3 fastmap_1.2.0
## [19] backports_1.5.0 Rsamtools_2.20.0
## [21] rmarkdown_2.30 UCSC.utils_1.0.0
## [23] bit_4.6.0 xfun_0.58
## [25] zlibbioc_1.50.0 cachem_1.1.0
## [27] jsonlite_2.0.0 blob_1.2.4
## [29] DelayedArray_0.30.1 BiocParallel_1.38.0
## [31] parallel_4.4.1 cluster_2.1.6
## [33] R6_2.6.1 VariantAnnotation_1.50.0
## [35] stringi_1.8.7 bslib_0.10.0
## [37] RColorBrewer_1.1-3 bezier_1.1.2
## [39] rpart_4.1.23 jquerylib_0.1.4
## [41] Rcpp_1.1.2 SummarizedExperiment_1.34.0
## [43] knitr_1.51 base64enc_0.1-6
## [45] Matrix_1.7-0 nnet_7.3-19
## [47] tidyselect_1.2.1 rstudioapi_0.18.0
## [49] dichromat_2.0-0.1 abind_1.4-8
## [51] yaml_2.3.12 codetools_0.2-20
## [53] curl_6.2.3 lattice_0.22-6
## [55] tibble_3.2.1 regioneR_1.36.0
## [57] Biobase_2.64.0 KEGGREST_1.44.1
## [59] evaluate_1.0.5 foreign_0.8-87
## [61] karyoploteR_1.30.0 pillar_1.10.2
## [63] MatrixGenerics_1.16.0 checkmate_2.3.2
## [65] generics_0.1.4 RCurl_1.98-1.17
## [67] ensembldb_2.28.1 ggplot2_3.5.2
## [69] scales_1.4.0 glue_1.8.0
## [71] lazyeval_0.2.2 Hmisc_5.2-3
## [73] tools_4.4.1 data.table_1.17.4
## [75] GenomicAlignments_1.40.0 XML_3.99-0.18
## [77] grid_4.4.1 colorspace_2.1-1
## [79] AnnotationDbi_1.66.0 GenomeInfoDbData_1.2.12
## [81] htmlTable_2.4.3 restfulr_0.0.15
## [83] Formula_1.2-5 cli_3.6.5
## [85] S4Arrays_1.4.1 dplyr_1.1.4
## [87] AnnotationFilter_1.28.0 gtable_0.3.6
## [89] sass_0.4.10 digest_0.6.39
## [91] SparseArray_1.4.8 rjson_0.2.23
## [93] htmlwidgets_1.6.4 farver_2.1.2
## [95] memoise_2.0.1 htmltools_0.5.9
## [97] lifecycle_1.0.5 httr_1.4.7
## [99] bit64_4.6.0-1 bamsignals_1.36.0