Skip to content

Commit b8b69d7

Browse files
authored
Merge pull request #76 from JamesHWade/feat/baseline-algorithms
feat: Add advanced baseline correction algorithms
2 parents 69273de + 85a0703 commit b8b69d7

29 files changed

+2260
-13
lines changed

.beads/.sync.lock

Whitespace-only changes.

NAMESPACE

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@ S3method(bake,step_measure_augment_shift)
3030
S3method(bake,step_measure_baseline_airpls)
3131
S3method(bake,step_measure_baseline_als)
3232
S3method(bake,step_measure_baseline_arpls)
33+
S3method(bake,step_measure_baseline_aspls)
3334
S3method(bake,step_measure_baseline_auto)
3435
S3method(bake,step_measure_baseline_custom)
36+
S3method(bake,step_measure_baseline_fastchrom)
3537
S3method(bake,step_measure_baseline_gpc)
38+
S3method(bake,step_measure_baseline_iarpls)
3639
S3method(bake,step_measure_baseline_minima)
3740
S3method(bake,step_measure_baseline_morph)
41+
S3method(bake,step_measure_baseline_morphological)
3842
S3method(bake,step_measure_baseline_poly)
3943
S3method(bake,step_measure_baseline_py)
4044
S3method(bake,step_measure_baseline_rf)
@@ -192,11 +196,15 @@ S3method(prep,step_measure_augment_shift)
192196
S3method(prep,step_measure_baseline_airpls)
193197
S3method(prep,step_measure_baseline_als)
194198
S3method(prep,step_measure_baseline_arpls)
199+
S3method(prep,step_measure_baseline_aspls)
195200
S3method(prep,step_measure_baseline_auto)
196201
S3method(prep,step_measure_baseline_custom)
202+
S3method(prep,step_measure_baseline_fastchrom)
197203
S3method(prep,step_measure_baseline_gpc)
204+
S3method(prep,step_measure_baseline_iarpls)
198205
S3method(prep,step_measure_baseline_minima)
199206
S3method(prep,step_measure_baseline_morph)
207+
S3method(prep,step_measure_baseline_morphological)
200208
S3method(prep,step_measure_baseline_poly)
201209
S3method(prep,step_measure_baseline_py)
202210
S3method(prep,step_measure_baseline_rf)
@@ -316,11 +324,15 @@ S3method(print,step_measure_augment_shift)
316324
S3method(print,step_measure_baseline_airpls)
317325
S3method(print,step_measure_baseline_als)
318326
S3method(print,step_measure_baseline_arpls)
327+
S3method(print,step_measure_baseline_aspls)
319328
S3method(print,step_measure_baseline_auto)
320329
S3method(print,step_measure_baseline_custom)
330+
S3method(print,step_measure_baseline_fastchrom)
321331
S3method(print,step_measure_baseline_gpc)
332+
S3method(print,step_measure_baseline_iarpls)
322333
S3method(print,step_measure_baseline_minima)
323334
S3method(print,step_measure_baseline_morph)
335+
S3method(print,step_measure_baseline_morphological)
324336
S3method(print,step_measure_baseline_poly)
325337
S3method(print,step_measure_baseline_py)
326338
S3method(print,step_measure_baseline_rf)
@@ -480,11 +492,15 @@ S3method(tidy,step_measure_augment_shift)
480492
S3method(tidy,step_measure_baseline_airpls)
481493
S3method(tidy,step_measure_baseline_als)
482494
S3method(tidy,step_measure_baseline_arpls)
495+
S3method(tidy,step_measure_baseline_aspls)
483496
S3method(tidy,step_measure_baseline_auto)
484497
S3method(tidy,step_measure_baseline_custom)
498+
S3method(tidy,step_measure_baseline_fastchrom)
485499
S3method(tidy,step_measure_baseline_gpc)
500+
S3method(tidy,step_measure_baseline_iarpls)
486501
S3method(tidy,step_measure_baseline_minima)
487502
S3method(tidy,step_measure_baseline_morph)
503+
S3method(tidy,step_measure_baseline_morphological)
488504
S3method(tidy,step_measure_baseline_poly)
489505
S3method(tidy,step_measure_baseline_py)
490506
S3method(tidy,step_measure_baseline_rf)
@@ -568,7 +584,10 @@ S3method(tunable,step_measure_align_shift)
568584
S3method(tunable,step_measure_baseline_airpls)
569585
S3method(tunable,step_measure_baseline_als)
570586
S3method(tunable,step_measure_baseline_arpls)
587+
S3method(tunable,step_measure_baseline_aspls)
571588
S3method(tunable,step_measure_baseline_custom)
589+
S3method(tunable,step_measure_baseline_fastchrom)
590+
S3method(tunable,step_measure_baseline_iarpls)
572591
S3method(tunable,step_measure_baseline_poly)
573592
S3method(tunable,step_measure_baseline_py)
574593
S3method(tunable,step_measure_baseline_rf)
@@ -599,11 +618,13 @@ export(align_segment_length)
599618
export(all_pass)
600619
export(assess_deconv_quality)
601620
export(augment)
621+
export(baseline_alpha)
602622
export(baseline_asymmetry)
603623
export(baseline_degree)
604624
export(baseline_half_window)
605625
export(baseline_lambda)
606626
export(baseline_span)
627+
export(baseline_window)
607628
export(bigaussian_peak_model)
608629
export(bin_width)
609630
export(check_axis_consistency)
@@ -741,11 +762,15 @@ export(step_measure_augment_shift)
741762
export(step_measure_baseline_airpls)
742763
export(step_measure_baseline_als)
743764
export(step_measure_baseline_arpls)
765+
export(step_measure_baseline_aspls)
744766
export(step_measure_baseline_auto)
745767
export(step_measure_baseline_custom)
768+
export(step_measure_baseline_fastchrom)
746769
export(step_measure_baseline_gpc)
770+
export(step_measure_baseline_iarpls)
747771
export(step_measure_baseline_minima)
748772
export(step_measure_baseline_morph)
773+
export(step_measure_baseline_morphological)
749774
export(step_measure_baseline_poly)
750775
export(step_measure_baseline_py)
751776
export(step_measure_baseline_rf)

R/baseline-morphological.R

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# ==============================================================================
2+
# Morphological Baseline Correction Methods
3+
#
4+
# This file contains the morphological baseline correction algorithm:
5+
# - step_measure_baseline_morphological: Erosion/dilation
6+
#
7+
# Note: step_measure_baseline_tophat and step_measure_baseline_minima
8+
# are defined in baseline-extended.R
9+
# ==============================================================================
10+
11+
# ==============================================================================
12+
# step_measure_baseline_morphological
13+
# ==============================================================================
14+
15+
#' Morphological Baseline Correction (Erosion/Dilation)
16+
#'
17+
#' `step_measure_baseline_morphological()` creates a *specification* of a recipe
18+
#' step that applies morphological erosion followed by dilation for baseline
19+
#' estimation.
20+
#'
21+
#' @param recipe A recipe object.
22+
#' @param measures An optional character vector of measure column names.
23+
#' @param window_size Size of the structuring element. Default is 50.
24+
#' @param iterations Number of erosion iterations. Default is 1.
25+
#' @param role Not used.
26+
#' @param trained Logical indicating if the step has been trained.
27+
#' @param skip Logical. Should the step be skipped when baking?
28+
#' @param id Unique step identifier.
29+
#'
30+
#' @return An updated recipe with the new step added.
31+
#'
32+
#' @details
33+
#' This morphological approach uses erosion (local minimum) to push the
34+
#' baseline down below peaks, followed by dilation (local maximum) to
35+
#' smooth the result.
36+
#'
37+
#' Multiple erosion iterations can be used for signals with tall peaks
38+
#' that require more aggressive baseline estimation.
39+
#'
40+
#' @family measure-baseline
41+
#' @export
42+
#'
43+
#' @examples
44+
#' library(recipes)
45+
#'
46+
#' \donttest{
47+
#' rec <- recipe(water + fat + protein ~ ., data = meats_long) |>
48+
#' update_role(id, new_role = "id") |>
49+
#' step_measure_input_long(transmittance, location = vars(channel)) |>
50+
#' step_measure_baseline_morphological(window_size = 50) |>
51+
#' prep()
52+
#'
53+
#' bake(rec, new_data = NULL)
54+
#' }
55+
step_measure_baseline_morphological <- function(
56+
recipe,
57+
measures = NULL,
58+
window_size = 50L,
59+
iterations = 1L,
60+
role = NA,
61+
trained = FALSE,
62+
skip = FALSE,
63+
id = recipes::rand_id("measure_baseline_morphological")
64+
) {
65+
recipes::add_step(
66+
recipe,
67+
step_measure_baseline_morphological_new(
68+
measures = measures,
69+
window_size = as.integer(window_size),
70+
iterations = as.integer(iterations),
71+
role = role,
72+
trained = trained,
73+
skip = skip,
74+
id = id
75+
)
76+
)
77+
}
78+
79+
step_measure_baseline_morphological_new <- function(
80+
measures,
81+
window_size,
82+
iterations,
83+
role,
84+
trained,
85+
skip,
86+
id
87+
) {
88+
recipes::step(
89+
subclass = "measure_baseline_morphological",
90+
measures = measures,
91+
window_size = window_size,
92+
iterations = iterations,
93+
role = role,
94+
trained = trained,
95+
skip = skip,
96+
id = id
97+
)
98+
}
99+
100+
#' @export
101+
prep.step_measure_baseline_morphological <- function(
102+
x,
103+
training,
104+
info = NULL,
105+
...
106+
) {
107+
check_for_measure(training)
108+
109+
# Validate parameters
110+
if (
111+
!is.numeric(x$window_size) ||
112+
length(x$window_size) != 1 ||
113+
x$window_size < 3
114+
) {
115+
cli::cli_abort(
116+
"{.arg window_size} must be a single integer >= 3, not {.val {x$window_size}}."
117+
)
118+
}
119+
if (
120+
!is.numeric(x$iterations) || length(x$iterations) != 1 || x$iterations < 1
121+
) {
122+
cli::cli_abort(
123+
"{.arg iterations} must be a positive integer, not {.val {x$iterations}}."
124+
)
125+
}
126+
127+
if (is.null(x$measures)) {
128+
measure_cols <- find_measure_cols(training)
129+
} else {
130+
measure_cols <- x$measures
131+
}
132+
133+
step_measure_baseline_morphological_new(
134+
measures = measure_cols,
135+
window_size = as.integer(x$window_size),
136+
iterations = as.integer(x$iterations),
137+
role = x$role,
138+
trained = TRUE,
139+
skip = x$skip,
140+
id = x$id
141+
)
142+
}
143+
144+
#' Morphological baseline algorithm
145+
#' @noRd
146+
.morphological_baseline <- function(y, window_size, iterations) {
147+
n <- length(y)
148+
149+
# Validate input
150+
if (n < 3) {
151+
cli::cli_warn("Input vector has fewer than 3 points, returning original.")
152+
return(y)
153+
}
154+
if (anyNA(y)) {
155+
cli::cli_warn(
156+
"Input contains {sum(is.na(y))} NA value{?s}. NA values will propagate."
157+
)
158+
}
159+
if (any(!is.finite(y) & !is.na(y))) {
160+
cli::cli_abort("Input contains Inf/-Inf values. Cannot compute baseline.")
161+
}
162+
163+
baseline <- y
164+
half_window <- window_size %/% 2
165+
166+
# Multiple erosion iterations (local minimum)
167+
for (iter in seq_len(iterations)) {
168+
eroded <- numeric(n)
169+
for (i in seq_len(n)) {
170+
start <- max(1, i - half_window)
171+
end <- min(n, i + half_window)
172+
eroded[i] <- min(baseline[start:end])
173+
}
174+
baseline <- eroded
175+
}
176+
177+
# Dilation (local maximum) to recover baseline level after erosion
178+
dilated <- numeric(n)
179+
for (i in seq_len(n)) {
180+
start <- max(1, i - half_window)
181+
end <- min(n, i + half_window)
182+
dilated[i] <- max(baseline[start:end])
183+
}
184+
185+
dilated
186+
}
187+
188+
#' @export
189+
bake.step_measure_baseline_morphological <- function(object, new_data, ...) {
190+
window_size <- object$window_size
191+
iterations <- object$iterations
192+
193+
for (col in object$measures) {
194+
result <- purrr::map(new_data[[col]], function(m) {
195+
baseline <- .morphological_baseline(m$value, window_size, iterations)
196+
new_measure_tbl(
197+
location = m$location,
198+
value = m$value - baseline
199+
)
200+
})
201+
new_data[[col]] <- new_measure_list(result)
202+
}
203+
204+
tibble::as_tibble(new_data)
205+
}
206+
207+
#' @export
208+
print.step_measure_baseline_morphological <- function(
209+
x,
210+
width = max(20, options()$width - 30),
211+
...
212+
) {
213+
title <- paste0(
214+
"Morphological baseline (window=",
215+
x$window_size,
216+
", iter=",
217+
x$iterations,
218+
")"
219+
)
220+
if (x$trained) {
221+
cat(title, " on <internal measurements>", sep = "")
222+
} else {
223+
cat(title)
224+
}
225+
cat("\n")
226+
invisible(x)
227+
}
228+
229+
#' @rdname tidy.recipe
230+
#' @export
231+
#' @keywords internal
232+
tidy.step_measure_baseline_morphological <- function(x, ...) {
233+
tibble::tibble(
234+
terms = if (recipes::is_trained(x)) x$measures else "<all measure columns>",
235+
window_size = x$window_size,
236+
iterations = x$iterations,
237+
id = x$id
238+
)
239+
}

0 commit comments

Comments
 (0)