Skip to content

Commit

Permalink
merge pull request #78 from shawntschwartz/69-bug-add-optional-flag-i…
Browse files Browse the repository at this point in the history
…n-glassbox-to-turn-off-detrend

megafix: various issues with `glassbox()` and `plot()`
  • Loading branch information
shawntz authored Sep 27, 2024
2 parents c3b3886 + c450bbb commit 468c47e
Show file tree
Hide file tree
Showing 13 changed files with 237 additions and 97 deletions.
3 changes: 2 additions & 1 deletion .lintr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
linters: linters_with_defaults(
line_length_linter(80),
object_usage_linter = NULL
object_usage_linter = NULL,
cyclocomp_linter = NULL
)
encoding: "UTF-8"
91 changes: 63 additions & 28 deletions R/glassbox.R
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,30 @@
#'
#' @param file An SR Research EyeLink `.asc` file generated by the official
#' EyeLink `edf2asc` command.
#' @param interactive A flag to indicate whether to run the `glassbox` pipeline
#' autonomously all the way through (set to `FALSE`, the default), or to
#' @param confirm A flag to indicate whether to run the `glassbox` pipeline
#' autonomously all the way through (set to `FALSE` by default), or to
#' interactively provide a visualization after each pipeline step, where you
#' must also indicate "(y)es" or "(n)o" to either proceed or cancel the
#' current `glassbox` pipeline operation (set to `TRUE`).
#' @param n_epochs Number of random epochs to generate for visualization.
#' @param duration Time in seconds of each randomly selected epoch.
#' @param time_range The start and stop raw timestamps used to subset the
#' preprocessed data from each step of the `eyeris` pipeline for visualization.
#' Defaults to NULL, meaning random epochs as defined by `n_epochs` and
#' `duration` will be plotted. To override the random epochs, set `time_range`
#' here to a vector with relative start and stop times (e.g., `c(5000, 6000)`
#' to indicate the raw data from 5-6 seconds on data that were recorded at
#' 1000 Hz).
#' @param detrend_data A flag to indicate whether to run the `detrend` step (set
#' to `FALSE` by default). Detrending your pupil timeseries can have unintended
#' consequences; we thus recommend that users understand the implications of
#' detrending -- in addition to whether detrending is appropriate for the
#' research design and question(s) -- before using this function.
#' @param num_previews Number of random example "epochs" to generate for
#' previewing the effect of each preprocessing step on the pupil timeseries.
#' @param preview_duration Time in seconds of each randomly selected preview.
#' @param preview_window The start and stop raw timestamps used to subset the
#' preprocessed data from each step of the `eyeris` workflow for visualization.
#' Defaults to NULL, meaning random epochs as defined by `num_previews` and
#' `preview_duration` will be plotted. To override the random epochs, set
#' `preview_window` here to a vector with relative start and stop times
#' (e.g., `c(5000, 6000)` to indicate the raw data from 5-6 seconds on data that
#' were recorded at 1000 Hz). Note, the start/stop time values indicated here
#' relate to the raw index position of each pupil sample from 1 to n (which
#' will need to be specified manually by the user depending on the sampling rate
#' of the recording; i.e., 5000-6000 for the epoch positioned from 5-6 seconds
#' after the start of the timeseries, sampled at 1000 Hz).
#' @param ... Additional arguments to override the default, prescribed settings.
#'
#' @examples
Expand All @@ -52,33 +62,32 @@
#' ## (a) run an automated pipeline with no real-time inspection of parameters
#' output <- eyeris::glassbox(demo_data)
#'
#' ## (b) run an interactive pipeline
#' output <- eyeris::glassbox(demo_data, interactive = TRUE)
#' ## (b) run a interactive workflow (with confirmation prompts after each step)
#' output <- eyeris::glassbox(demo_data, confirm = TRUE)
#'
#' # (2) examples overriding the default parameters
#' output <- eyeris::glassbox(demo_data,
#' interactive = TRUE,
#' confirm = TRUE,
#' deblink = list(extend = 40),
#' lpfilt = list(plot_freqz = FALSE)
#' )
#' }
#'
#' @export
glassbox <- function(file, interactive = FALSE, n_epochs = 3, duration = 5,
time_range = NULL, ...) {

glassbox <- function(file, confirm = FALSE, detrend_data = FALSE,
num_previews = 3, preview_duration = 5,
preview_window = NULL, ...) {
# the default parameters
params <- list(
deblink = list(extend = 50),
detransient = list(n = 16),
lpfilt = list(wp = 4, ws = 8, rp = 1, rs = 35, plot_freqz = TRUE),
zscore = list(groups = NULL)
lpfilt = list(wp = 4, ws = 8, rp = 1, rs = 35, plot_freqz = TRUE)
)

params <- utils::modifyList(params, list(...))

pipeline <- list(
load = function(data, params) {
load_asc = function(data, params) {
return(eyeris::load_asc(data))
},
deblink = function(data, params) {
Expand All @@ -100,16 +109,34 @@ glassbox <- function(file, interactive = FALSE, n_epochs = 3, duration = 5,
))
},
detrend = function(data, params) {
return(eyeris::detrend(data))
if (detrend_data) {
return(eyeris::detrend(data))
} else {
return(data)
}
},
zscore = function(data, params) {
return(eyeris::zscore(data, groups = params$zscore$groups))
return(eyeris::zscore(data))
}
)

seed <- sample.int(.Machine$integer.max, 1)
step_counter <- 1

for (step_name in names(pipeline)) {
cli::cli_alert(
paste("Running", step_name, "...")
action <- "Running "
skip_plot <- FALSE

if (!detrend_data) {
if (step_name == "detrend") {
action <- "Skipping "
step_counter <- step_counter - 1
skip_plot <- TRUE
}
}

cli::cli_alert_success(
paste0("[ OK ] - ", action, "eyeris::", step_name, "()")
)

step_to_run <- pipeline[[step_name]]
Expand All @@ -120,17 +147,23 @@ glassbox <- function(file, interactive = FALSE, n_epochs = 3, duration = 5,
},
error = function(e) {
cli::cli_alert_info(
paste("Skipping", step_name, ":", e$message, "\n")
paste0("[ INFO ] - ", "Skipping eyeris::", step_name, "(): ",
e$message)
)
cat("\n")
err_thrown <<- TRUE
step_counter <<- step_counter - 1
return(file)
}
)

if (interactive) {
if (confirm) {
if (!err_thrown) {
plot(file, time_range = time_range)
if (!skip_plot) {
plot(file,
steps = step_counter, num_previews = num_previews, seed = seed,
preview_duration = preview_duration, preview_window = preview_window
)
}
if (step_name != "zscore") {
if (!prompt_user()) {
cli::cli_alert_info(
Expand All @@ -147,6 +180,8 @@ glassbox <- function(file, interactive = FALSE, n_epochs = 3, duration = 5,
}
}
}

step_counter <- step_counter + 1
}

return(file)
Expand Down
120 changes: 96 additions & 24 deletions R/plot.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,29 @@
#'
#' @param x An object of class `eyeris` derived from [eyeris::load()].
#' @param ... Additional arguments to be passed to `plot`.
#' @param n_epochs Number of random epochs to generate for visualization.
#' @param duration Time in seconds of each randomly selected epoch.
#' @param steps Which steps to plot; defaults to `all` (i.e., plot all steps).
#' Otherwise, pass in a vector containing the index of the step(s) you want to
#' plot, with index `1` being the original raw pupil timeseries.
#' @param time_range The start and stop raw timestamps used to subset the
#' preprocessed data from each step of the `eyeris` pipeline for visualization.
#' Defaults to NULL, meaning random epochs as defined by `n_epochs` and
#' `duration` will be plotted. To override the random epochs, set `time_range`
#' here to a vector with relative start and stop times (e.g., `c(5000, 6000)`
#' to indicate the raw data from 5-6 seconds on data that were recorded at
#' 1000 Hz).
#' @param num_previews Number of random example "epochs" to generate for
#' previewing the effect of each preprocessing step on the pupil timeseries.
#' @param preview_duration Time in seconds of each randomly selected preview.
#' @param preview_window The start and stop raw timestamps used to subset the
#' preprocessed data from each step of the `eyeris` workflow for visualization.
#' Defaults to NULL, meaning random epochs as defined by `num_examples` and
#' `example_duration` will be plotted. To override the random epochs, set
#' `example_timelim` here to a vector with relative start and stop times
#' (e.g., `c(5000, 6000)` to indicate the raw data from 5-6 seconds on data that
#' were recorded at 1000 Hz). Note, the start/stop time values indicated here
#' relate to the raw index position of each pupil sample from 1 to n (which
#' will need to be specified manually by the user depending on the sampling rate
#' of the recording; i.e., 5000-6000 for the epoch positioned from 5-6 seconds
#' after the start of the timeseries, sampled at 1000 Hz).
#' @param seed Random seed for current plotting session. Leave NULL to select
#' `num_previews` number of random preview "epochs" (of `preview_duration`) each
#' time. Otherwise, choose any seed-integer as you would normally select for
#' [base::set.seed()], and you will be able to continue re-plotting the same
#' random example pupil epochs each time -- which is helpful when adjusting
#' parameters within and across `eyeris` workflow steps.
#'
#' @return No return value; iteratively plots a subset of the pupil timeseries
#' from each preprocessing step run.
Expand All @@ -30,14 +41,15 @@
#' plot(eyeris_data)
#'
#' # using a custom time subset (i.e., 1 to 500 ms)
#' plot(eyeris_data, time_range = c(1, 500))
#' plot(eyeris_data, preview_window = c(1, 500))
#' }
#'
#' @rdname plot.eyeris
#'
#' @export
plot.eyeris <- function(x, ..., n_epochs = 3, duration = 5, steps = "all",
time_range = NULL) {
plot.eyeris <- function(x, ..., steps = NULL, num_previews = NULL,
preview_duration = NULL, preview_window = NULL,
seed = NULL) {
# tests
tryCatch(
{
Expand All @@ -57,6 +69,51 @@ plot.eyeris <- function(x, ..., n_epochs = 3, duration = 5, steps = "all",
}
)

# set param defaults outside of function declaration
if (!is.null(preview_window)) {
if (!is.null(num_previews) || !is.null(preview_duration)) {
cli::cli_alert_warning(
paste(
"num_previews and/or preview_duration will be ignored,",
"since preview_window was specified here."
)
)
}
}

if (is.null(steps)) {
steps <- "all"
}

if (is.null(num_previews)) {
num_previews <- 3
}

if (is.null(preview_duration)) {
preview_duration <- 5
}

# handle random seed for this plotting session
if (is.null(seed)) {
seed <- sample.int(.Machine$integer.max, 1)
}

# nolint start
current_seed <- .Random.seed
# nolint end

set.seed(seed)

if (!is.null(current_seed)) { # restore global seed
# nolint start
.Random.seed <- current_seed
# nolint end
} else {
# nolint start
rm(.Random.seed)
# nolint end
}

pupil_data <- x$timeseries
pupil_steps <- grep("^pupil_", names(pupil_data), value = TRUE)
colors <- c("black", rainbow(length(pupil_steps) - 1))
Expand All @@ -69,42 +126,57 @@ plot.eyeris <- function(x, ..., n_epochs = 3, duration = 5, steps = "all",
pupil_steps <- pupil_steps[steps]
colors <- colors[steps]
}
} else if (length(steps) > 1 && !is.null(time_range)) {
} else if (length(steps) > 1 && !is.null(preview_window)) {
pupil_steps <- pupil_steps[steps]
colors <- colors[steps]
} else {
pupil_steps <- pupil_steps
colors <- colors
}

if (is.null(time_range)) {
if (is.null(preview_window)) {
hz <- x$info$sample.rate
random_epochs <- draw_random_epochs(pupil_data, n_epochs, duration, hz)
par(mfrow = c(1, n_epochs))
random_epochs <- draw_random_epochs(
pupil_data, num_previews,
preview_duration, hz
)
par(mfrow = c(1, num_previews))
for (i in seq_along(pupil_steps)) {
for (n in 1:n_epochs) {
for (n in 1:num_previews) {
st <- min(random_epochs[[n]]$time_orig)
et <- max(random_epochs[[n]]$time_orig)

main_panel <- ceiling(n_epochs / 2)
main_panel <- ceiling(num_previews / 2)

if (n == main_panel) {
title <- paste0(pupil_steps[i], "\n[", st, " - ", et, "]")
} else {
title <- paste0("\n[", st, " - ", et, "]")
}

if (grepl("z", pupil_steps[i])) {
y_units <- "(z)"
} else {
y_units <- "(a.u.)"
}

if (n == 1) {
y_label <- paste("pupil size", y_units)
} else {
y_label <- ""
}

plot(random_epochs[[n]][[pupil_steps[i]]],
type = "l", col = colors[i], lwd = 2,
main = title, xlab = "Time", ylab = "Pupil Size"
main = title, xlab = "time (ms)", ylab = y_label
)
}
}

par(mfrow = c(1, n_epochs))
par(mfrow = c(1, num_previews))
} else {
start_index <- time_range[1]
end_index <- min(time_range[2], nrow(pupil_data))
start_index <- preview_window[1]
end_index <- min(preview_window[2], nrow(pupil_data))
sliced_pupil_data <- pupil_data[start_index:end_index, ]
par(mfrow = c(1, 1))
for (i in seq_along(pupil_steps)) {
Expand All @@ -114,7 +186,7 @@ plot.eyeris <- function(x, ..., n_epochs = 3, duration = 5, steps = "all",
type = "l", col = colors[i], lwd = 2,
main = paste0(
pupil_steps[i], "\n[", st, " - ", et, "] | ",
"[", time_range[1], " - ", time_range[2], "]"
"[", preview_window[1], " - ", preview_window[2], "]"
),
xlab = "Time", ylab = "Pupil Size"
)
Expand All @@ -132,7 +204,7 @@ draw_random_epochs <- function(x, n, d, hz) {
max_timestamp <- max(x$time_orig)

if ((max_timestamp - min_timestamp) < d) {
cli::cli_abort("Epoch duration is longer than available duration of data.")
cli::cli_abort("Example duration is longer than the duration of data.")
}

drawn_epochs <- list()
Expand Down
4 changes: 2 additions & 2 deletions README.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ library(eyeris)
demo_data <- system.file("extdata", "assocret.asc", package = "eyeris")
eyeris_preproc <- glassbox(demo_data, lpfilt = list(plot_freqz = TRUE))
eyeris_preproc <- glassbox(demo_data, detrend_data = F, lpfilt = list(plot_freqz = T))
```

### step-wise correction of pupillary signal
Expand All @@ -72,7 +72,7 @@ plot(eyeris_preproc)
### final pre-post correction of pupillary signal (raw -> preprocessed)

```{r timeseries-plot, echo=TRUE, fig.width=8, fig.height=5, fig.dpi=300}
plot(eyeris_preproc, steps = c(1, 6), time_range = c(0, 100000))
plot(eyeris_preproc, steps = c(1, 5), preview_window = c(0, 100000))
```

# Comments, suggestions, questions, issues
Expand Down
Loading

0 comments on commit 468c47e

Please sign in to comment.