## There are two peak detection/fitting algorithms: one uses an NLS gaussian
## fit, the other is an approximation through parabolas.
findPeaks.gauss <- function(object, alpha = 0.99, egh = TRUE) {
prof <- object@env$profile
quant <- quantile(prof, alpha, na.rm = TRUE)
fits <- apply(prof, 1, fit_profile, object@scantime, quant, egh)
fits_flat <- unlist(fits, F)
mz <- rep(profMz(object), sapply(fits, length))
pks <- fits_to_peaks(fits_flat, mz)
if(nrow(pks)==0) cat('No peaks are found, please adjust your parameters\n')
pks
}
descendMin <- function(y, istart = which.max(y)) {
if (!is.double(y)) y <- as.double(y)
unlist(.C("DescendMin",
y,
length(y),
as.integer(istart-1),
ilower = integer(1),
iupper = integer(1),
DUP = FALSE, PACKAGE = "xcms")[4:5]) + 1
}
SSfixed <- selfStart(~ h*exp(-(x-mu)^2/(2*2.3^2)), function(mCall, data, LHS) {
xy <- sortedXyData(mCall[["x"]], LHS, data)
len <- dim(xy)[1]
maxpos <- which.max(xy[,2])
mu <- xy[maxpos,1]
h <- xy[maxpos,2]
value <- c(mu, h)
names(value) <- mCall[c("mu", "h")]
value
}, c("mu", "h"))
## Exponential-Gaussian hybrid
egh <- function(x, mu, h, sigma, t) {
sapply(x, function(x)
if ((2*sigma^2 + t*(x - mu)) > 0)
h*exp(-(x-mu)^2/(2*sigma^2 + t*(x-mu)))
else 0
)
}
## ~ egh(mu, h, sigma, t)
SShybrid <- selfStart(egh, function(mCall, data, LHS) {
xy <- sortedXyData(mCall[["x"]], LHS, data)
integ <- function(a, b)
sum((xy[(a+1):b,2]+xy[a:(b-1),2])*(xy[(a+1):b,1]-xy[a:(b-1),1]))/2
maxpos <- which.max(xy[,2])
maxval <- xy[maxpos,2]
mu <- xy[maxpos,1]
h <- xy[maxpos,2]
sigma <- integ(1, nrow(xy))/(h*sqrt(2*pi))
t <- 0
value <- c(mu, h, sigma, t)
names(value) <- mCall[c("mu", "h", "sigma", "t")]
value
}, c("mu", "h", "sigma", "t"))
fit_profile <-
function(profile, times,
quant = quantile(profile, .99, na.rm = TRUE), egh = TRUE, min_points = 3,
noise_factor = 3, min_fit_radius = min_points*2)
{
## fit a specific region of the profile, fill results into 'env'
fit_region <- function(region, plot = FALSE) {
if (diff(region) <= 1) # ignore empties
return(NULL)
noise.sd <- sd(profile[profile < quant], na.rm=T)
noise <- noise_factor*noise.sd
## try a different way to define noise ratio.
##range <- (diff(region) + 1) * 2
## try a smaller region
range <- max(round((diff(region) + 1)/2), min_fit_radius)
maxpos <- region[1] + which.max(profile[region[1]:region[2]]) - 1
## intervals are trimmed, based on crossing above the quantile cutoff again and
## whether an increase happens in the wrong direction while within regions
left <- max(1, region[1] - range)
left_crossed <- which(profile[left:(region[1]-1)] > quant)
if (length(left_crossed))
left <- left + left_crossed[length(left_crossed)]
left_down <- diff(profile[region[1]:maxpos]) < 0
local_max <- which(diff(c(FALSE, left_down)) > 0) - 1 + region[1]
local_min <- as.numeric(sapply(local_max, function(m)
descendMin(profile[m:maxpos], 1)[2] + m - 1))
## collect convoluted data
## if((profile[local_max] - profile[local_min])/profile[local_max] > noise){
## sink('~/Desktop/local2.txt',append=T)
## cat('local_max:',profile[local_max],'local_min:',profile[local_min],'diff:',
## profile[local_max] - profile[local_min],'noise:',noise,
## 'result:',profile[local_max] - profile[local_min] > noise,'\n')
## sink()
## }
## normalize local diff
## local_min <- local_min[(profile[local_max] - profile[local_min])/profile[local_max] > noise]
local_min <- local_min[profile[local_max] - profile[local_min] > noise]
if (length(local_min)) {
local_min <- local_min[1]
fit_region(c(region[1], local_min))#, profile, times, quant, env, plot)
fit_region(c(local_min, region[2]))#, profile, times, quant, env, plot)
return()
}
right <- min(length(profile), region[2] + range)
right_crossed <- which(profile[(region[2]+1):right] > quant)
if (length(right_crossed))
right <- region[2] + right_crossed[1] - 1
right_up <- diff(profile[maxpos:region[2]]) > 0
local_max <- which(diff(c(right_up, FALSE)) < 0) + maxpos
local_min <- as.numeric(sapply(local_max, function(m)
descendMin(profile[maxpos:m], m - maxpos + 1)[1] + maxpos - 1))
local_min <- local_min[profile[local_max] - profile[local_min] > noise]
if (length(local_min)) {
local_min <- local_min[1]
fit_region(c(region[1], local_min))#, profile, times, quant, env, plot)
fit_region(c(local_min, region[2]))#, profile, times, quant, env, plot)
return()
}
## right <- maxpos + right_up[1] - 1
##xcms <- try(nls(y ~ SSfixed(x, mu, h), data.frame(x = times[left:right],
## y = profile[left:right])), T)
hybrid <- character(0)
if (egh)
hybrid <- try(nls(y ~ SShybrid(x, mu, h, sigma, t),
data.frame(x = times[left:right], y = profile[left:right])), T)
if (is.character(hybrid))
gauss <- try(nls(y ~ SSgauss(x, mu, sigma, h), data.frame(x = times[left:right],
y = profile[left:right])), T)
if (plot) {
plot(left:right, profile[left:right], xlab = "scan", ylab = "residuals")
if (is.character(hybrid) && !is.character(gauss)) {
points(left:right, fitted(gauss), type = "l", col = "red", lwd = 2)
print(coefficients(gauss))
}
##if (!is.character(xcms))
## points(left:right, fitted(xcms), type = "l", col = "blue", lwd = 2)
if (!is.character(hybrid)) {
points(left:right, fitted(hybrid), type = "l", col = "green", lwd = 2)
print(coefficients(hybrid))
}
}
name <- paste(region[1], ":", region[2], sep="")
if (is.character(hybrid))
model <- gauss
else model <- hybrid
assign(name, model, env)
}
filt <- profile > quant & !is.na(profile)
diffs <- diff(c(FALSE, filt, FALSE))
regions <- cbind(which(diffs == 1), which(diffs == -1)-1)
## here we ignore regions with less than min_points points
regions <- regions[regions[,2] - regions[,1] >= min_points - 1,,drop=F]
env <- new.env()
apply(regions, 1, fit_region)
as.list(env)
}
fits_to_peaks <- function(fits, masses = NULL)
{
good_fits <- !sapply(fits, is.character)
fits <- fits[good_fits]
names(fits) <- NULL
coeff_names <- c("mu", "h", "sigma", "t")
integ <- function(xy, a, b)
sum((xy[(a+1):b,2]+xy[a:(b-1),2])*(xy[(a+1):b,1]-xy[a:(b-1),1]))/2
peaks <- t(sapply(fits, function(model) {
e <- model$m$getEnv()
span <- diff(range(e$x))
coeffs <- coefficients(model)[coeff_names]
coeffs[is.na(coeffs)] <- 0 # if couldn't fit egh, assume 't' is zero
coeffs[["sigma"]] <- abs(coeffs[["sigma"]])
names(coeffs)<-coeff_names
c(rt = coeffs[["mu"]],
rtmin = e$x[1],
rtmax = tail(e$x, 1),
into = integ(cbind(e$x, e$y), 1, length(e$x)),
intf = sqrt(2*pi)*coeffs[["h"]]*coeffs[["sigma"]],
maxo = max(e$y),
maxf = coeffs[["h"]],
tau = coeffs[["t"]],
sigma = coeffs[["sigma"]],
error = sqrt(deviance(model)/df.residual(model)),
span = span / 5, # is this necessary?
delta_i = coeffs[["h"]] - max(e$y),
delta_t = coeffs[["mu"]] - e$x[which.max(e$y)],
peak_sd = sd(e$y) ,
"sigma/h" = coeffs[["sigma"]]/coeffs[["h"]]
)
}))
if (!is.null(masses)) {
masses <- masses[good_fits]
peaks <- cbind(mz = masses, mzmin = masses, mzmax = masses, peaks)
}
## try to get rid of the "flat" peaks
## flat <- peaks[,"span"] < peaks[,"sigma.sigma"] | peaks[,"ret.mu"] < object@scantime[peaks[,"retmin"]] |
## peaks[,"ret.mu"] > object@scantime[peaks[,"retmax"]]
## peaks[!flat,]
## get rid of the 'bad' fit peaks and 'flat' peaks
## bad <- peaks[,'rt']<peaks[,'rtmin']|peaks[,'rt']>peaks[,'rtmax']|abs(peaks[,'sigma'])<=sigma.cutoff
## cat(length(bad),'\n')
## cat(nrow(peaks),'\n')
## peaks <- peaks[!bad,]
new("cpPeaks", peaks)
}
#############################################################
## Peak detection using parabola approximation
#############################################################
findPeaks.parabola <- function(object) {
raw <- object
## the white noise is assumed to have a poisson distribution
## if the noise is significant, we can approximate the poisson with the normal
## we then estimate sigma from the MAD (sigma = 1.4826*MAD)
## (could become an estimateNoise protocol)
prof <- raw@env$profile
mad_prof <- xcms::medianFilter(abs(prof), mrad = 0, nrad = 100)
## Filter out maxima that are piggybacking on peaks. With small peaks
## or the flat tails of large peaks, noise can cause the detection of
## spurious maxima, as the values are similar and above the noise
## cutoff. In a sense, the peak is adding an additional baseline. One
## could attempt to fit that baseline. AMDIS locally fits a line,
## which is just an approximation of fitting the actual peak. This
## approach is a bit complicated though, and it's not clear if it is
## needed since we are already subtracting a global baseline. AMDIS
## also checks whether a maximum rises above an adjacent minimum by
## some number of noise units. All of these seem to be variants of
## smoothing. What about just running a loess? For now, trying a
## median filter of radius 2. Spurious maxima are avoided, but maxima
## tend to be misestimated. It's hard to say where the maxima is, but
## when a minimum is chosen, the fit suffers. For now, when the
## filtered signal is flat, we pick the maximum.
filt_prof <- xcms::medianFilter(prof, 0, 2)
##filt_prof <- prof
## now we filter out the noise
## hack: convert profile matrix into a single series, compress to Rle
## the Rle has significant overhead -- how much memory must we save?
## right now, the compression is about 5X, while a sparse-vector might
## give 6X.
rleProf <- function(x) Rle(as.integer(t(cbind(x, NA))))
series <- rleProf(raw_prof@env$profile)
cor_series <- rleProf(prof)
mad_series <- rleProf(mad_prof)
filt_series <- rleProf(filt_prof)
## We take the AMDIS approach of computing a
## global (median) noise factor, ignoring regions with imputed values
## (zeros). Then just multiple the sqrt(raw signal) by that factor.
## tricky: make sure we don't violate the profile row boundaries
imputed <- runValue(series) == min(series, na.rm=TRUE)
imputed[is.na(imputed)] <- FALSE
breaks_prof <- which(is.na(runValue(series)))
## start_prof <- c(1L, head(start(series)[breaks_prof] + 1L, -1))
## start_prof <- rep(start_prof, diff(c(0L, breaks_prof)))
## end_prof <- end(series)[breaks_prof] - 1L
## end_prof <- rep(end_prof, diff(c(0L, breaks_prof)))
## prof_r <- IRanges(start_prof, end_prof)
breaks_r <- PartitioningByEnd(start(series)[breaks_prof])
prof_r <- IRanges(start(breaks_r), width = width(breaks_r) - 1)
prof_r <- prof_r[rep(seq_len(length(prof_r)), diff(c(0L, breaks_prof)))]
imputed_ir <- IRanges(start(series)[imputed] - 50, end(series)[imputed] + 50)
imputed_ir <- pintersect(imputed_ir, prof_r[imputed])
primary_ir <- gaps(imputed_ir, 1L, length(series))
### FIXME: should not use the single point intensity here, instead
### take value from median fit in baseline subtraction.
noise <- mad_series[primary_ir] / sqrt(series[primary_ir])
nf <- median(noise, na.rm = TRUE)
## a reasonable peak cut-off is 3*sd, multiply this by sqrt(intensity)
cutoff_series <- nf*3*1.4826*sqrt(series)
## peak fitting (per ion and then TIC)
## 1) find local maxima that pass a noise filter
##max_series <- which(diff(diff(filt_series) > 0) == -1) + 1
diff_series <- diff(filt_series)
dd_series <- diff(diff_series > 0)
max_series <- which(dd_series == -1) + 1
max_r <- findRange(max_series, filt_series)
max_views <- Views(series, max_r[which(diff_series[end(max_r)] < 0)])
max_series <- viewWhichMaxs(max_views)
## maximum must be above noise cutoff
max_series <- max_series[which(cor_series[max_series] >
cutoff_series[max_series])]
## filter out maxima without flanking scans above threshold could
## reduce the threshold (maybe 2*sd), since it is less likely for
## three points to be above the threshold (from the geometric), but we
## are not just trying to detect peaks. we are trying to characterize
## them, which is tough if there are not enough non-noisy points.
max_series <- max_series[which(cor_series[max_series-1] >
cutoff_series[max_series-1])]
max_series <- max_series[which(cor_series[max_series+1] >
cutoff_series[max_series+1])]
max_runs <- findRun(max_series, series)
## 2) fit each maximum with a gaussian
## could just fit a parabola, which matches the top-half of the
## gaussian fairly well. then estimate sigma from sigma =
## FWHM/(2*sqrt(2*log(2))). We could take the 5 points centered on the
## maximum. For small peaks, the fit will likely be affected by noise,
## but the measurements lack accuracy at low levels anyway.
## do we want to down-weight points (the ends) that fall under the
## threshold? Maybe by 0.5?
fit_max <- start(series)[max_runs]
fit_r <- IRanges(fit_max - 2L, fit_max + 2L)
fit_r <- pintersect(fit_r, prof_r[max_runs])
fit_fw <- width(fit_r) == 5L ## some are on the end of time course
max_runs <- max_runs[fit_fw]
fit_r <- fit_r[fit_fw]
## avoid overlap between fit regions, splitting at minimum
## fit_crossed <- which(head(end(fit_r), -1) >= tail(start(fit_r), -1))
## imin <- viewWhichMins(Views(cor_series, fit_max[fit_crossed],
## fit_max[fit_crossed+1]))
## end(fit_r)[fit_crossed] <- imin
fit_max_off <- integer(length(fit_r))
## fit_max_off[fit_crossed+1] <- start(fit_r)[fit_crossed+1] - imin
## start(fit_r)[fit_crossed+1] <- imin
fit_f <- rep(seq_len(length(fit_r)), width(fit_r))
fit_int <- as.integer(cor_series[fit_r])
fit_y <- split(fit_int, fit_f)
fit_r_scan <- IRanges(start(fit_r) - start(prof_r)[max_runs] + 1,
w = width(fit_r))
fit_t <- raw@scantime[as.integer(fit_r_scan)]
## to avoid numerical overflow
fit_t_off <- raw@scantime[start(fit_r_scan)]
fit_t <- fit_t - rep(fit_t_off, width(fit_r))
fit_x <- cbind(a = 1L, b = fit_t, c = fit_t^2)
fit_d <- split.data.frame(fit_x, fit_f)
fits <- do.call("rbind", lapply(seq_len(length(fit_r)), function(r) {
coefficients(lm.fit(fit_d[[r]], fit_y[[r]]))
}))
## determine mu, sigma, and h
a <- fits[,"a"]
b <- fits[,"b"]
c <- fits[,"c"]
mu <- -b/(2*c)
h <- a + b*mu + c*(mu^2)
mu <- mu + fit_t_off # correct 'mu' after using it with 'a', 'b', or 'c'
sigma <- suppressWarnings((-sqrt(b^2-4*(a-h/2)*c)/c)/2.35703) # simplify?
fit_t_i <- head(cumsum(c(1, width(fit_r))), -1)
fit_t_max <- fit_t_i + fit_max_off
span <- width(fit_r)
fit_t_abs <- fit_t + rep(fit_t_off, span)
## integrate points under each peak
xy <- cbind(fit_t, fit_int)
m <- 1
n <- nrow(xy)
t_int <- (xy[(m+1):n,2]+xy[m:(n-1),2])*(xy[(m+1):n,1]-xy[m:(n-1),1])
if (length(fit_t_i > 1)) # get rid of intervening points
t_int <- t_int[-(tail(fit_t_i, -1) - 1)]
into <- colSums(matrix(t_int, span-1))/2
##into <- viewSums(Views(Rle(t_int), successiveIRanges(span-1)))/2
## calculate residuals and error
resid <- fit_int -
(rep(a, span) + rep(b, span)*fit_x[,2] + rep(c, span) * fit_x[,3])
##error<-sqrt(viewSums(Views(Rle(resid^2), successiveIRanges(span))) /
## (span - 3))
error <- sqrt(colSums(matrix(resid^2, span))/(span - 3))
minmz <- raw@mzrange[1]
mz <- findInterval(start(fit_r), which(is.na(series))) + minmz
peaks <-
cbind(mz = mz, mzmin = mz, mzmax = mz,
rt = mu, rtmin = mu - 3*sigma, rtmax = mu + 3*sigma,
into = into, intf = sqrt(2*pi)*h*sigma,
maxo = fit_int[fit_t_max], maxf = h,
tau = 0, sigma = sigma, error = error, span = span,
rto = fit_t_abs[fit_t_max])
## scanmin <- pmax(1, findInterval(peaks[,"rtmin"], raw@scantime))
## scanmax <- pmin(findInterval(peaks[,"rtmax"], raw@scantime)+1, ncol(prof))
## scan_off <- cumsum(c(0,rep(ncol(prof),nrow(prof)-1)))[peaks[,"mz"]-minmz+1]
## peaks <- cbind(peaks, scanmin = scanmin + scan_off,
## scanmax = scanmax + scan_off)
## peak filtering: there are 3 types of peaks: the good fits, the weak
## fits and the bad fits. The bad fits are those that we cannot trust
## at all, and so should be discarded. The weak fits are often due to
## convolution and may be resolved using the good fits in the
## deconvolution stage.
## filter out obviously bad fits
## "upside down" or 'mu' estimated outside of fitted domain
## peaks <- peaks[c < 0 & mu <= fit_t[fit_t_i + width(fit_r) - 1] + fit_t_off &
## mu >= fit_t[fit_t_i] + fit_t_off,]
new("cpPeaks", peaks)
}
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.