Skip to content

Commit f238c83

Browse files
committed
Improve default peak detection default parameters
MassSpecWavelet::peakDetectionCWT() has a lot of parameters. Some of those parameters depend on the chosen scales, and/or the expected peak widths or signal lengths. This commit defines defaultPeakDetectionCWTParams() which is a function from the GCIMS package that sets reasonable default values for those parameters, suitable for GCIMS data.
1 parent ced2047 commit f238c83

File tree

8 files changed

+258
-85
lines changed

8 files changed

+258
-85
lines changed

DESCRIPTION

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
Package: GCIMS
22
Type: Package
33
Title: Pre-processing for GC-IMS data
4-
Version: 0.1.1
4+
Version: 0.2.0
55
Encoding: UTF-8
6-
Date: 2024-04-29
6+
Date: 2025-05-03
77
Authors@R: c(
88
person(
99
given = "Sergio",
@@ -118,7 +118,7 @@ Suggests:
118118
viridisLite
119119
biocViews: Software, Preprocessing, Visualization, Classification,
120120
Cheminformatics, Metabolomics, DataImport
121-
RoxygenNote: 7.3.1
121+
RoxygenNote: 7.3.2
122122
VignetteBuilder: knitr
123123
Config/testthat/edition: 3
124124
Roxygen: list(markdown = TRUE)

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export(clusterPeaks)
3030
export(create_annotations_table)
3131
export(cubic_root_trans)
3232
export(decimate)
33+
export(defaultPeakDetectionCWTParams)
3334
export(description)
3435
export(download_three_ketones_dataset)
3536
export(dtime)

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# GCIMS 0.2.0 (2025-05-03)
2+
3+
- Deprecate `peakDetectionCWTParams = list(exclude0scaleAmpThresh = TRUE)`. Please
4+
remove that argument, since the new default is better. For further details on
5+
the new default please check the vignette or `?GCIMS::defaultPeakDetectionCWTParams`.
6+
17
# GCIMS 0.1.1 (2024-04-29)
28

39
- Fix undefined variable on RIP saturation detection (#21)

R/findPeaks.R

Lines changed: 122 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,10 @@ interleave_peaks_with_zeros <- function(peak_idx, zero_idx, signal_idx) {
6767
out
6868
}
6969

70-
detect_peaks_and_zeros_one_signal <- function(x, x_zeros, idx, save_debugging, prep_wav, peakDetectionCWTParams) {
70+
detect_peaks_and_zeros_one_signal <- function(x, x_zeros, idx, save_debugging, peakDetectionCWTParams) {
7171
peakdet <- rlang::exec(
7272
MassSpecWavelet::peakDetectionCWT,
7373
ms = x,
74-
scales = prep_wav,
7574
!!!peakDetectionCWTParams
7675
)
7776
if (is.null(x_zeros)) {
@@ -97,8 +96,11 @@ detect_peaks_and_zeros_one_signal <- function(x, x_zeros, idx, save_debugging, p
9796
)
9897
}
9998

100-
detect_peaks_and_zeros <- function(xmat, rowwise, scales, peakDetectionCWTParams = list(exclude0scaleAmpThresh = TRUE),
101-
signals_to_save_extra = NULL, xmat_zeros = NULL) {
99+
detect_peaks_and_zeros <- function(
100+
xmat, rowwise,
101+
peakDetectionCWTParams,
102+
signals_to_save_extra = NULL, xmat_zeros = NULL
103+
) {
102104
if (rowwise) {
103105
direction <- "rowwise"
104106
signal_length <- ncol(xmat)
@@ -129,7 +131,10 @@ detect_peaks_and_zeros <- function(xmat, rowwise, scales, peakDetectionCWTParams
129131
}
130132

131133
}
132-
prep_wav <- MassSpecWavelet::prepareWavelets(signal_length, scales = scales)
134+
peakDetectionCWTParams$scales <- MassSpecWavelet::prepareWavelets(
135+
signal_length,
136+
scales = peakDetectionCWTParams$scales
137+
)
133138
save_debugging <- rep(FALSE, num_signals)
134139
if (!is.null(signals_to_save_extra)) {
135140
save_debugging[signals_to_save_extra] <- TRUE
@@ -145,22 +150,9 @@ detect_peaks_and_zeros <- function(xmat, rowwise, scales, peakDetectionCWTParams
145150
save_debugging = save_debugging
146151
),
147152
detect_peaks_and_zeros_one_signal,
148-
prep_wav = prep_wav,
149153
peakDetectionCWTParams = peakDetectionCWTParams,
150154
.progress = ifelse(show_progress_bar(), glue("Detecting peaks and zeros ({direction})"), FALSE)
151155
)
152-
# peaks_and_zeros_and_extra <- mapply(
153-
# FUN = detect_peaks_and_zeros_one_signal,
154-
# x = signals,
155-
# x_zeros = signals_zeros,
156-
# idx = seq_len(num_signals),
157-
# save_debugging = save_debugging,
158-
# MoreArgs = list(
159-
# prep_wav = prep_wav,
160-
# peakDetectionCWTParams = peakDetectionCWTParams
161-
# ),
162-
# SIMPLIFY = FALSE
163-
# )
164156

165157
peaks_and_zeros <- purrr::map_dfr(peaks_and_zeros_and_extra, "peaks_and_zeros")
166158
stopifnot(ncol(peaks_and_zeros) == 4L)
@@ -391,12 +383,10 @@ prep_wav_from_peakwidths <- function(axis, peakwidth_min, peakwidth_max) {
391383
#' 5. We merge similar ROIs using a threshold on the intersection over union
392384
#' 6. Get some ROI metrics and return.
393385
#'
394-
#' For the MassSpecWavelet-based peak detection, the `scales` are computed based on
395-
#' the requested peak widths. Besides, the scales, further tuning beyond the
396-
#' MassSpecWavelet defaults is possible through the `*_peakDetectionCWTParams` argument.
397-
#' By default, the only change we introduce is the `exclude0scaleAmpThresh = TRUE`
398-
#' which is a reasonable peak detection setting not enabled in MassSpecWavelet
399-
#' for backwards compatibility reasons.
386+
#' For the MassSpecWavelet-based peak detection, custom parameters are calculated
387+
#' with our [GCIMS::defaultPeakDetectionCWTParams()], feel free to call that
388+
#' function and customize the `SNR.Th` or any other parameter as shown in the
389+
#' vignette.
400390
#'
401391
#' @keywords internal
402392
#' @param drift_time The drift time vector
@@ -418,15 +408,50 @@ findPeaksImpl <- function(
418408
rt_length_s = 8,
419409
dt_peakwidth_range_ms = c(0.15, 0.4),
420410
rt_peakwidth_range_s = c(10, 25),
421-
dt_peakDetectionCWTParams = list(exclude0scaleAmpThresh = TRUE),
422-
rt_peakDetectionCWTParams = list(exclude0scaleAmpThresh = TRUE),
411+
dt_peakDetectionCWTParams = defaultPeakDetectionCWTParams(x = drift_time, peakwidth_range_xunits = dt_peakwidth_range_ms),
412+
rt_peakDetectionCWTParams = defaultPeakDetectionCWTParams(x = retention_time, peakwidth_range_xunits = rt_peakwidth_range_s),
423413
dt_extension_factor = 0,
424414
rt_extension_factor = 0,
425415
exclude_rip = FALSE,
426416
iou_overlap_threshold = 0.2,
427417
debug_idx = list(dt = NULL, rt = NULL)
428418
) {
429419

420+
#### Handle older dt_peakDetectionCWTParams. Remove in 2026-01-01.
421+
old_peakDetectionCWTParams <- list(exclude0scaleAmpThresh = TRUE)
422+
if (identical(dt_peakDetectionCWTParams, old_peakDetectionCWTParams)) {
423+
cli::cli_warn(c(
424+
"Deprecated dt_peakDetectionCWTParams value",
425+
"!" = "Passing the argument {.code dt_peakDetectionCWTParams = list(exclude0scaleAmpThresh = TRUE)} is deprecated since GCIMS v0.2.1.",
426+
"i" = "Please remove the {.arg dt_peakDetectionCWTParams} argument. The new default value is better."
427+
))
428+
dt_peakDetectionCWTParams = defaultPeakDetectionCWTParams(
429+
x=drift_time,
430+
peakwidth_range_xunits = dt_peakwidth_range_ms
431+
)
432+
}
433+
if (identical(rt_peakDetectionCWTParams, old_peakDetectionCWTParams)) {
434+
cli::cli_warn(c(
435+
"Deprecated rt_peakDetectionCWTParams value",
436+
"!" = "Passing the argument {.code rt_peakDetectionCWTParams = list(exclude0scaleAmpThresh = TRUE)} is deprecated since GCIMS v0.2.1.",
437+
"i" = "Please remove the {.arg rt_peakDetectionCWTParams} argument. The new default value is better."
438+
))
439+
rt_peakDetectionCWTParams = defaultPeakDetectionCWTParams(
440+
x=retention_time,
441+
peakwidth_range_xunits = rt_peakwidth_range_s
442+
)
443+
}
444+
#### Code handling older dt_peakDetectionCWTParams ends here.
445+
446+
if (!is.null(dt_peakwidth_range_ms) && !identical(attr(dt_peakDetectionCWTParams, "peakwidth_range_xunits"), dt_peakwidth_range_ms)) {
447+
stop("Please provide a new dt_peakDetectionCWTParams created with the given dt_peakwidth_range_ms")
448+
}
449+
450+
if (!is.null(rt_peakwidth_range_s) && !identical(attr(rt_peakDetectionCWTParams, "peakwidth_range_xunits"), rt_peakwidth_range_s)) {
451+
stop("Please provide a new rt_peakDetectionCWTParams created with the given rt_peakwidth_range_s")
452+
}
453+
454+
430455
debug_info <- list()
431456
if (!is.null(debug_idx$rt) && length(debug_idx$rt) > 0) {
432457
debug_info$rt <- list()
@@ -459,31 +484,9 @@ findPeaksImpl <- function(
459484
rm(deriv2)
460485

461486
# 5. Peaks and Zero-crossings
462-
scales_rt <- prep_wav_from_peakwidths(
463-
retention_time,
464-
peakwidth_min = rt_peakwidth_range_s[1L],
465-
peakwidth_max = rt_peakwidth_range_s[2L]
466-
)
467-
scales_dt <- prep_wav_from_peakwidths(
468-
drift_time,
469-
peakwidth_min = dt_peakwidth_range_ms[1L],
470-
peakwidth_max = dt_peakwidth_range_ms[2L]
471-
)
472-
473-
if (verbose) {
474-
cli_inform(
475-
message = c(
476-
"Using the following scales",
477-
"i" = "Retention time scales: {glue::glue_collapse(scales_rt$scales, sep = ', ')}",
478-
"i" = "Drift time scales: {glue::glue_collapse(scales_dt$scales, sep = ', ')}"
479-
)
480-
)
481-
}
482-
483487
peaks_and_zeros_drt_extra <- detect_peaks_and_zeros(
484488
xmat = int_mat,
485489
rowwise = TRUE,
486-
scales = scales_rt,
487490
peakDetectionCWTParams = rt_peakDetectionCWTParams,
488491
signals_to_save_extra = debug_idx$rt,
489492
xmat_zeros = drt
@@ -497,7 +500,6 @@ findPeaksImpl <- function(
497500
peaks_and_zeros_ddt_extra <- detect_peaks_and_zeros(
498501
xmat = int_mat,
499502
rowwise = FALSE,
500-
scales = scales_dt,
501503
peakDetectionCWTParams = dt_peakDetectionCWTParams,
502504
signals_to_save_extra = debug_idx$dt,
503505
xmat_zeros = ddt
@@ -567,6 +569,77 @@ findPeaksImpl <- function(
567569
}
568570

569571

572+
#' Get the default peakDetectionCWT parameters for GCIMS peak detection
573+
#'
574+
#' @description
575+
#'
576+
#' Our selection of defaults for [peakDetectionCWT()] consist of the following:
577+
#'
578+
#' \itemize{
579+
#' \item{`exclude0scaleAmpThresh=TRUE`: Does not include the zeroth scale in the default amplitude threshold.}
580+
#' \item{`nearbyPeak=TRUE`: This is already a default, but since we customize nearbyWinSize we prefer to be explicit.}
581+
#' \item{`nearbyWinSize`: This is set to the maximum desired peak width in points (instead of the arbitrary 150 from [MassSpecWavelet::identifyMajorPeaks()])}
582+
#' \item{`ridgeLength`: We set this to the second calculated scale, that would correspond to a peak with shorter width in our scale preparation.}
583+
#' \item{`excludeBoundariesSize`: Instead of an arbitrary number, we exclude half of the peak minimum width}
584+
#' \item{`SNR.Th`: We keep the default (3) but we explicitly specify it because we consider this a sensitive parameter}
585+
#' \item{`minNoiseLevel`: [MassSpecWavelet::identifyMajorPeaks()] uses a default of 0.001 and we keep that default}
586+
#' \item{`scales`: We provide here the [MassSpecWavelet::prepareWavelets()]
587+
#' output, created with scales representative of the peak widths. You can
588+
#' provide just a numeric vector with scales, but if you provide the prepared
589+
#' wavelets you get some extra performance.}
590+
#'
591+
#' }
592+
#'
593+
#' @keywords internal
594+
#' @param x The x axis, to determine units
595+
#' @param peakwidth_range_xunits A vector of length 2 with the minimum and maximum peak width.
596+
#'
597+
#' @returns
598+
#' A list with the default parameters we will use with [MassSpecWavelet::peakDetectionCWT()].
599+
#' @export
600+
#'
601+
#' @examples
602+
#' chromat1 <- GCIMSChromatogram(
603+
#' retention_time = seq(from = 0, to = 10, length.out = 200),
604+
#' intensity = 1:200
605+
#' )
606+
#'
607+
#' peakDetectionCWTParams <- defaultPeakDetectionCWTParams(
608+
#' x = rtime(chromat1),
609+
#' peakwidth_range_xunits = c(5, 25)
610+
#' )
611+
#' peakDetectionCWTParams$SNR.Th <- 2.0
612+
#' peakDetectionCWTParams$scales <- MassSpecWavelet::prepareWavelets(
613+
#' x = length(rtime(chromat1)),
614+
#' scales = c(1, seq(2, 30, 2), seq(32, 64, 4))
615+
#' )
616+
#' peakDetectionCWTParams
617+
defaultPeakDetectionCWTParams <- function(x, peakwidth_range_xunits) {
618+
step <- x[2] - x[1]
619+
peakwidth_min_pts <- units_to_points(peakwidth_range_xunits[1L], step)
620+
peakwidth_max_pts <- units_to_points(peakwidth_range_xunits[2L], step)
621+
622+
prep_wavelets <- prep_wav_from_peakwidths(
623+
x,
624+
peakwidth_min = peakwidth_range_xunits[1L],
625+
peakwidth_max = peakwidth_range_xunits[2L]
626+
)
627+
628+
peakDetectionCWTParams <- list(
629+
exclude0scaleAmpThresh = TRUE,
630+
nearbyPeak=TRUE, # This is already the default
631+
nearbyWinSize=peakwidth_max_pts,
632+
ridgeLength=prep_wavelets$scales[2],
633+
excludeBoundariesSize=max(1, floor(peakwidth_min_pts/2)),
634+
SNR.Th=3,
635+
minNoiseLevel = 0.001,
636+
scales = prep_wavelets
637+
)
638+
attr(peakDetectionCWTParams, "peakwidth_range_xunits") <- peakwidth_range_xunits
639+
peakDetectionCWTParams
640+
}
641+
642+
570643
#---------------#
571644
# FUNCTIONS #
572645
#---------------#

R/findPeaksImpl1D.R

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,20 @@ rois_to_peaklist_1D <- function(ROIs, x, y, deriv2) {
9292
#' 4. We merge similar ROIs using a threshold on the 1D-intersection over union
9393
#' 5. Get some ROI metrics and return.
9494
#'
95-
#' For the MassSpecWavelet-based peak detection, the `scales` are computed based on
96-
#' the requested peak widths. Besides, the scales, further tuning beyond the
97-
#' MassSpecWavelet defaults is possible through the `peakDetectionCWTParams` argument.
98-
#' By default, the only change we introduce is the `exclude0scaleAmpThresh = TRUE`
99-
#' which is a reasonable peak detection setting not enabled in MassSpecWavelet
100-
#' for backwards compatibility reasons.
95+
#' For the MassSpecWavelet-based peak detection, the `scales` and some other parameters
96+
#' are customized based on the requested peak widths. Still, if you want to further
97+
#' customize other peak detection sensitivity parameters, you can start calling
98+
#' [defaultPeakDetectionCWTParams()] and modify its output. Then pass that output
99+
#' to this function using `peakDetectionCWTParams=`.
100+
#'
101101
#'
102102
#' @keywords internal
103103
#' @param x The x axis, to determine units
104104
#' @param y The vector where to find peaks, of dimensions `length(x)`
105105
#' @param verbose If `TRUE` information will be printed on screen
106106
#' @param length_in_xunits Length of the filter used to compute the second derivative. See details.
107107
#' @param peakwidth_range_xunits A vector of length 2 with the minimum and maximum peak width. See details.
108-
#' @param peakDetectionCWTParams Additional parameters to [MassSpecWavelet::peakDetectionCWT()]. See details.
108+
#' @param peakDetectionCWTParams Parameters to [MassSpecWavelet::peakDetectionCWT()]. See details.
109109
#' @param extension_factor A number to extend the ROIs beyond their default size
110110
#' @param iou_overlap_threshold A number, between 0 and 1. Pairs of ROIs with an intersection over union larger than this threshold are merged.
111111
#' @param debug If TRUE, return as well the debug information
@@ -115,11 +115,32 @@ findPeaksImpl1D <- function(
115115
verbose = FALSE,
116116
length_in_xunits = 0.07,
117117
peakwidth_range_xunits = c(0.15, 0.4),
118-
peakDetectionCWTParams = list(exclude0scaleAmpThresh = TRUE),
118+
peakDetectionCWTParams = defaultPeakDetectionCWTParams(x=x, peakwidth_range_xunits = peakwidth_range_xunits),
119119
extension_factor = 0,
120120
iou_overlap_threshold = 0.2,
121121
debug = FALSE
122122
) {
123+
124+
#### Handle older peakDetectionCWTParams. Remove in 2026-01-01.
125+
old_peakDetectionCWTParams <- list(exclude0scaleAmpThresh = TRUE)
126+
if (identical(peakDetectionCWTParams, old_peakDetectionCWTParams)) {
127+
cli::cli_warn(c(
128+
"Deprecated peakDetectionCWTParams value",
129+
"!" = "Passing the argument {.code peakDetectionCWTParams = list(exclude0scaleAmpThresh = TRUE)} is deprecated since GCIMS v0.2.1.",
130+
"i" = "Please remove the {.arg peakDetectionCWTParams} argument. The new default value is better."
131+
))
132+
peakDetectionCWTParams = defaultPeakDetectionCWTParams(
133+
x=x,
134+
peakwidth_range_xunits = peakwidth_range_xunits
135+
)
136+
}
137+
#### Code handling older peakDetectionCWTParams ends here.
138+
139+
140+
if (!is.null(peakwidth_range_xunits) && !identical(attr(peakDetectionCWTParams, "peakwidth_range_xunits"), peakwidth_range_xunits)) {
141+
stop("Please provide a new peakDetectionCWTParams created with the given peakwidth_range_xunits")
142+
}
143+
123144
x_step <- x[2L] - x[1L]
124145
x_length_pts <- units_to_points(length_in_xunits, x_step, must_odd = TRUE)
125146
spec_length <- length(y)
@@ -129,24 +150,10 @@ findPeaksImpl1D <- function(
129150
sg_filt <- signal::sgolay(p = 2L, n = x_length_pts, m = 2)
130151
deriv2 <- sgolayfilt(y, sg_filt)
131152
# 5. Peaks and Zero-crossings
132-
scales <- prep_wav_from_peakwidths(
133-
x,
134-
peakwidth_min = peakwidth_range_xunits[1L],
135-
peakwidth_max = peakwidth_range_xunits[2L]
136-
)
137-
138-
if (verbose) {
139-
cli_inform(
140-
message = c(
141-
"i" = "Using the following scales: {glue::glue_collapse(scales$scales, sep = ', ')}"
142-
)
143-
)
144-
}
145153

146154
peaks_and_zeros_extra <- detect_peaks_and_zeros(
147155
xmat = matrix(y, ncol = 1),
148156
rowwise = FALSE,
149-
scales = scales,
150157
peakDetectionCWTParams = peakDetectionCWTParams,
151158
signals_to_save_extra = if (debug) 1L else NULL,
152159
xmat_zeros = matrix(deriv2, ncol = 1)

0 commit comments

Comments
 (0)