Nothing
# Ditrack.R:
# Mode tree / SiZer type tracking of feature significance as the kernel size/
# interval width varies.
#
# c 2025-2026 Greg Kreider, Primordial Machine Vision Systems, Inc.
## To Do:
# -
## Manually Tested With: LP Diw
# faithful$eruptions p,f p,f
# faithful$waiting p,f p,f
# iris$Petal.Length p,f p LP: warns small kernel
# iris$Petal.Width p p LP: warns small kernel
# quakes[,3] p,f p,f Diw graphs slant right
# chickwts$weight p p LP: warns small kernel
# tstds1 p,f p,f
# tstds2 p,f p,f
# tstds3 p,f p,f
# tstds4 p,f p,f
# meuse$cadmium (sp) p,f p,f LP: warns small kernel
# meuse$copper (sp) p,f p,f LP: warns small kernel
# paulsen$y (boot) p,f p,f
# statfaculty (diptest) p p LP: warns small kernel
# Mammals$speed (quantreg) p,f p LP: warns small kernel
# LP flat at only one window
# stamps (multimode) p,f p,f
# galaxy (multimode) p,f p,f Diw flat plot crashes scale
# acicdity (multimode) p,f p,f LP: warns small kernel
# enzyme (multimode) p,f p,f LP: warns small kernel
#
##### Public Interface
# Track the peaks and flats in data x in smooth spacing while the filter or
# interval size changes from 0.01 t/m minwindow. All analysis is done with
# the opt options list; the [lp|diw].window value is overwritten and should
# not appear in the param option. Returns a list of class 'Ditrack' with
# elements
# $peaks subset of Dipeak data frame w/ pos, ismax, ppeak, plus winpct
# $flats subset of Diflat data w/ stID, endID, pflat, plus winpct
# $nx length of the data x
# $smooth filter applied, 'lp' or 'diw'
# The winpct column is window/interval size as a percent, ie. 1 t/m 40.
# There is one row in peaks and flats for each feature, thus multiple rows
# per winpct.
Ditrack <- function(x, smooth=c("lp", "diw"), opt=Diopt()) {
smooth <- match.arg(smooth)
opt$analysis <- smooth
pkcol <- c("pos", "ismax", "ppeak", "naccept")
ftcol <- c("stID", "endID", "pflat", "naccept")
# Other ideas from development for data and plotting:
# Add a minimum-minimum width column (rminID-lminID+1) to pk.
# Copy the htsd column to pk.
# Generate graph of matching features, ie. trace features as window changes.
# Measure depth along graph.
maxwin <- opt$track.maxwindow
if (maxwin < 1) {
maxwin <- round(maxwin * 100)
}
feat <- lapply(1:maxwin,
function(i) {
if ("lp" == smooth) {
opt$lp.window <- 0.01 * i
} else if ("diw" == smooth) {
opt$diw.window <- 0.01 * i
} else {
stop("unsupported smoothing method")
}
# We could try to screen specific warnings by using
# warning(warningCondition(msg, class='warn.track'))
# and suppressing by class, but there are several possible,
# including model OOB and kernel too small, so just ignore
# everything.
m <- suppressWarnings(Dimodal(x, opt))
if ("lp" == smooth) {
peaks <- m$lp.peaks
flats <- m$lp.flats
} else if ("diw" == smooth) {
peaks <- m$diw.peaks
flats <- m$diw.flats
} else {
stop("unsupported smoothing method")
}
npk <- nrow(peaks)
if (2 < npk) {
pk <- peaks[-c(1,npk), pkcol, drop=FALSE]
} else {
pk <- matrix(0, nrow=0, ncol=length(pkcol),
dimnames=list(NULL, pkcol))
}
pk <- cbind(winpct=rep(i, nrow(pk)), pk)
pk <- as.matrix(pk)
# Named by row in [lp|diw].peaks, copied into pk. Ignore.
rownames(pk) <- NULL
if (0 < nrow(flats)) {
ft <- flats[,ftcol, drop=FALSE]
} else {
ft <- matrix(0, nrow=0, ncol=length(ftcol),
dimnames=list(NULL, ftcol))
}
ft <- cbind(winpct=rep(i, nrow(flats)), ft)
ft <- as.matrix(ft)
rownames(ft) <- NULL
list(pk=pk, ft=ft)
})
pk <- NULL
ft <- NULL
for (i in seq_along(feat)) {
pk <- rbind(pk, feat[[i]]$pk)
ft <- rbind(ft, feat[[i]]$ft)
}
trk <- list(peaks=pk, flats=ft, nx=length(x), smooth=smooth)
class(trk) <- c("Ditrack", class(trk))
trk
}
# Print a table of the number of significant features as the window size
# varies in Ditrack result x. Extra arguments are ignored. Returns
# x invisibly.
print.Ditrack <- function(x, ...) {
if ((0 == nrow(x$peaks)) && (0 == nrow(x$flats))) {
cat("\nno peaks or flats at any window size\n\n")
return(invisible(x))
}
if ((5 != ncol(x$peaks)) || (5 != ncol(x$flats))) {
cat("\ninternal error - bad Ditrack contents\n\n")
return(invisible(x))
}
if (0 < nrow(x$peaks)) {
pkwrng <- range(x$peaks[,"winpct"])
pkcnt <- sapply(pkwrng[1]:pkwrng[2],
function(i) {
sel <- x$peaks[,"winpct"] == i
c(sum(x$peaks[sel,"ppeak"] <= 0.01, na.rm=TRUE),
sum(x$peaks[sel,"ppeak"] <= 0.05, na.rm=TRUE),
sum(x$peaks[sel,"naccept"]))
}, simplify="array")
} else {
pkwrng <- NULL
pkcnt <- NULL
}
if (0 < nrow(x$flats)) {
ftwrng <- range(x$flats[,"winpct"])
ftcnt <- sapply(ftwrng[1]:ftwrng[2],
function(i) {
sel <- x$flats[,"winpct"] == i
c(sum(x$flats[sel,"pflat"] <= 0.01, na.rm=TRUE),
sum(x$flats[sel,"pflat"] <= 0.05, na.rm=TRUE),
sum(x$flats[sel,"naccept"]))
})
} else {
ftwrng <- NULL
ftcnt <- NULL
}
if ("lp" == x$smooth) {
wtxt <- "LP kernel size"
} else if ("diw" == x$smooth) {
wtxt <- "interval size"
} else {
stop("unsupported method generated features")
}
wtxt2 <- "at level"
wwin <- max(nchar(wtxt), nchar(wtxt2))
wcnts <- 16
ptxt <- ifelse(is.null(pkcnt), "", format("peaks", width=wcnts, justify="c"))
ftxt <- ifelse(is.null(ftcnt), "", format("flats", width=wcnts, justify="c"))
cat("\n")
cat("Significant Feature Counts\n")
cat(sprintf("%*s %*s %*s\n", wwin, "", wcnts, ptxt, wcnts, ftxt))
if (is.null(pkwrng)) {
pk01 <- ""
pk05 <- ""
} else {
pk01 <- "0.01"
pk05 <- "0.05"
}
if (is.null(ftwrng)) {
ft01 <- ""
ft05 <- ""
} else {
ft01 <- "0.01"
ft05 <- "0.05"
}
cat(sprintf("%*s %4s %4s %4s %4s %4s %4s\n",
wwin, wtxt2, pk01, pk05, "pass", ft01, ft05, "pass"))
cat(sprintf("%*s\n", wwin, wtxt))
for (i in min(pkwrng[1],ftwrng[1]):max(pkwrng[2],ftwrng[2])) {
if (is.null(pkwrng) || (i < pkwrng[1]) || (pkwrng[2] < i)) {
pk01 <- ""
pk05 <- ""
pkpass <- ""
} else {
pk01 <- sprintf("%4d", pkcnt[1,i-pkwrng[1]+1])
pk05 <- sprintf("%4d", pkcnt[2,i-pkwrng[1]+1])
pkpass <- sprintf("%4d", pkcnt[3,i-pkwrng[1]+1])
}
if (is.null(ftwrng) || (i < ftwrng[1]) || (ftwrng[2] < i)) {
ft01 <- ""
ft05 <- ""
ftpass <- ""
} else {
ft01 <- sprintf("%4d", ftcnt[1,i-ftwrng[1]+1])
ft05 <- sprintf("%4d", ftcnt[2,i-ftwrng[1]+1])
ftpass <- sprintf("%4d", ftcnt[3,i-ftwrng[1]+1])
}
cat(sprintf("%*d %4s %4s %4s %4s %4s %4s\n",
wwin,i, pk01,pk05,pkpass, ft01,ft05,ftpass))
}
cat("\n")
invisible(x)
}
# Summarize the tracking results object by printing the ranges of window sizes
# with accepted features, for both peaks and flats. Extra arguments ignored.
summary.Ditrack <- function(object, ...) {
cat("\n")
if ("lp" == object$smooth) {
cat("Window Sizes (lp.window) With Accepted Features\n")
} else if ("diw" == object$smooth) {
cat("Interval Sizes (diw.window) With Accepted Features\n")
} else {
stop("unsupported method generated filters")
}
cat(" peaks ", paste0(pass.range(object$peaks), sep=" "), "\n")
cat(" flats ", paste0(pass.range(object$flats), sep=" "), "\n")
cat("\n")
invisible(object)
}
# Return a vector of strings with the continguous ranges of window sizes
# that have more than one passing (naccept > 0) feature for Ditrack
# peaks/flats tracker x.
pass.range <- function(x) {
if (0 == nrow(x)) {
return(c("none"))
}
npass <- c(0, sign(sapply(split(x[,'naccept'], x[,'winpct']), sum)), 0)
st <- names(npass)[which(npass[-1L]>npass[-length(npass)])+1]
end <- names(npass)[which(npass[-1L]<npass[-length(npass)])]
if (length(st) < length(end)) {
st <- c(st, rep(NA, length(end)-length(st)))
} else if (length(end) < length(st)) {
end <- c(st, rep(NA, length(st)-length(end)))
}
st <- as.integer(st)
end <- as.integer(end)
sapply(1:length(st),
function(i) { sprintf("%4.2f-%4.2f", 0.01*st[i],0.01*end[i]) })
}
# Graph the peaks and/or flats as the low-pass filter/interval size changes,
# as stored in Ditrack object x (feature may be a vector), using parameters
# in options opt. Extra arguments are ignored. Returns x invisibly. The
# function uses layout to place the plots, even if there is only one feature.
plot.Ditrack <- function(x, feature=c("peaks", "flats"), opt=Diopt(), ...) {
feature <- match.arg(feature, several.ok=TRUE)
layout(matrix(1:length(feature), nrow=1))
# layout for a 3 grid, as in the Dimodal plot, forces cex to 0.66.
# Duplicate that here.
oldpar <- par(cex=0.66)
on.exit(par(oldpar))
maxwin <- opt$track.maxwindow
if (maxwin < 1) {
maxwin <- round(maxwin * 100)
}
if ("peaks" %in% feature) {
plot_pktrack(x$peaks, x$nx, x$smooth, maxwin)
}
if ("flats" %in% feature) {
plot_fttrack(x$flats, x$nx, x$smooth, maxwin)
}
invisible(x)
}
##### Implementation
# Plot the location of peaks and minima as the smooth filter width varies, as
# stored in the combined matrix pk over windows from 1 t/m maxwin. nx is the
# number of points in the original data.
plot_pktrack <- function(pk, nx, smooth, maxwin) {
if (is.null(pk) || (0 == nrow(pk)) || (5 != ncol(pk))) {
dummyplot("no peaks found")
return()
}
oldpar <- par(mar=c(5,4,4,4)+0.1)
on.exit(par(oldpar))
# Set up coloring of ppeak. Color nbreak+1 will be for minima.
nbreak <- 5
breaks <- c(0, 0.005, 0.01, 0.05, 0.10, 1.0)
brkID <- cut(pk[,"ppeak"], breaks, labels=FALSE, include.lowest=TRUE)
brkID[is.na(brkID)] <- nbreak + 1
ptcex <- seq(1.25, 0.75, len=nbreak)
ptcex <- c(ptcex, 1.10)
clrs <- track.pcol(nbreak)
# ASCII 45 is '-'. 19/20 are larger/smaller dots.
pch <- ifelse(is.na(pk[,"ppeak"]), 45, 20)
pch <- sapply(1:nrow(pk),
function(i) {
if (is.na(pk[i,"ppeak"])) {
45
} else if (0 == pk[i,"naccept"]) {
1
} else {
20
}
})
if ("lp" == smooth) {
ylbl <- "low-pass kernel size"
} else if ("diw" == smooth) {
ylbl <- "interval width"
} else {
stop("unsupported smooth generated peaks")
}
plot(0,0, col=NA, xlim=c(0,nx), ylim=c(0,maxwin+1),
xlab="index", ylab=ylbl, main="Peak Track\n", yaxt="n")
abline(h=seq(5, maxwin, by=5), col="grey50", lty=3)
ytick <- seq(0, maxwin, by=10)
axis(2, at=ytick, labels=round(0.01 * ytick * nx))
axis(4, at=ytick, labels=0.01*ytick)
for (i in 1:nrow(pk)) {
points(pk[i,"pos"], pk[i,"winpct"], col=clrs[brkID[i]], pch=pch[i],
cex=ptcex[brkID[i]]*ifelse(1==pch[i],0.75,1.0))
}
xl <- c(0.1, 0.3, 0.5, 0.7, 0.9)
dxl <- 0.06 / 2
yl <- 1.04 * (maxwin + 1)
rect(nx*(xl-dxl), yl-1, nx*(xl+dxl), yl, col=clrs, border=NA)
mtext(c("0.005", "0.01", "0.05", "0.10", "1.0"), at=nx*xl, cex=0.75)
mtext("ppeak <=", line=1, at=nx*xl[1], cex=0.75)
}
# Plot the spans of flats as the smooth filter window varies, as stored in
# the combined matrix ft over windows from 1 t/m maxwin, coloring by the best
# probability for the feature. nx is the number of points in the original
# data.
plot_fttrack <- function(ft, nx, smooth, maxwin) {
if (is.null(ft) || (0 == nrow(ft)) || (5 != ncol(ft))) {
dummyplot("no flats found")
return()
}
oldpar <- par(mar=c(5,4,4,4)+0.1)
on.exit(par(oldpar))
nbreak <- 5
breaks <- c(0, 0.005, 0.01, 0.05, 0.10, 1.0)
brkID <- cut(ft[,"pflat"], breaks, labels=FALSE, include.lowest=TRUE)
lwd <- seq(2.5, 1.25, len=nbreak)
clrs <- track.pcol(nbreak)
if ("lp" == smooth) {
ylbl <- "low-pass kernel size"
} else if ("diw" == smooth) {
ylbl <- "interval width"
} else {
stop("unsupported smooth generated flats")
}
plot(0,0, col=NA, xlim=c(0,nx), ylim=c(0,maxwin+1),
xlab="index", ylab=ylbl, main="Flat Track\n", yaxt="n")
abline(h=seq(5, maxwin, by=5), col="grey50", lty=3)
ytick <- seq(0, maxwin, by=10)
axis(2, at=ytick, labels=round(0.01 * ytick * nx))
axis(4, at=ytick, labels=0.01*ytick)
if (1 == nrow(ft)) {
# cbind below would turn ftplt into a vector.
ftplt <- matrix(c(ft, brkID[1]), nrow=1,
dimnames=list(NULL, c(colnames(ft), "clrID")))
} else {
# In case of overlapping flats, draw more significant on top = later.
# Not going to worry about marking accepted flats for this reason.
o <- order(ft[,"pflat"], decreasing=TRUE)
# If you pass vectors as x,y, ex. ft[o,c('stID','winpct')] and endID,winpct,
# then lines will connect between flats, ie. must plot one row at a time.
ftplt <- cbind(ft[o,], clrID=brkID[o])
}
apply(ftplt, 1,
function(r) {
y <- r["winpct"]
lines(r[c("stID","endID")], c(y,y), col=clrs[r["clrID"]],
lwd=lwd[r["clrID"]]) })
xl <- c(0.1, 0.3, 0.5, 0.7, 0.9)
dxl <- 0.06 / 2
yl <- 1.04 * (maxwin + 1)
rect(nx*(xl-dxl), yl-1, nx*(xl+dxl), yl, col=clrs, border=NA)
mtext(c("0.005", "0.01", "0.05", "0.10", "1.0"), at=nx*xl, cex=0.75)
mtext("pflat <=", line=1, at=nx*xl[1], cex=0.75)
}
# Generate a list of nclr+1 colors for plotting the p values from the tracks.
# The palette choice is fixed (Plasma, with the last color from YlOrRed).
track.pcol <- function(nclr) {
clrs <- hcl.colors(nclr, "Plasma")
clrs[nclr] <- hcl.colors(nclr+1, "YlOrRd")[nclr]
# Last color duplicated so minima at least significant value.
c(clrs, clrs[length(clrs)])
}
# Create an empty plot with the text message msg in the center.
dummyplot <- function(msg) {
plot(0,0, col=NA, xlim=c(0,1), ylim=c(0,1), ann=FALSE, axes=FALSE)
box()
text(0.5,0.5, msg, adj=c(0.5,0.5), cex=1)
}
# Plot all HCL palettes pals with n colors, to pick a good gradient
# for the p values. Can pass colorspace::[deu|tri|pro]tan to see how
# they look for the color blind.
plot_hclpals <- function(n, cblindfn=identity, pals=hcl.pals("sequential")) {
# Interesting choice: Plasma, Inferno, Viridis, Ballow, ag_Sunset
pals <- sort(pals)
ncol <- ceiling(sqrt(length(pals)))
nrow <- ceiling(length(pals) / ncol)
clrs <- sapply(pals, function(p) { cblindfn(hcl.colors(p, n=n)) })
plot(0,0, col=NA, xlim=c(0,ncol), ylim=c(0,nrow), ann=FALSE, axes=FALSE)
rID <- 0
cID <- 0
for (i in seq_along(pals)) {
xl <- cID + (0:(n-1) / n)
xr <- cID + ((1:n) / n)
rect(xl, rID+0.1, xr, rID+0.7, col=clrs[,i], border=clrs[,i])
rect(xl[1], rID+0.1, xr[n], rID+0.7, col=NA, border="white")
text(cID+0.5, rID+0.85, pals[i], adj=c(0.5,0.5), cex=1.5)
cID <- cID + 1
if (cID == ncol) {
cID <- 0
rID <- rID + 1
}
}
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.