Skip to content

Commit db0451c

Browse files
Merge pull request #31 from UNCDEPENdLab/codex/add-project-status-indexing-functions
Add project-wide processing status utilities
2 parents b3e1208 + 815f99e commit db0451c

6 files changed

Lines changed: 163 additions & 0 deletions

File tree

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Generated by roxygen2: do not edit by hand
22

33
S3method(summary,bg_project_cfg)
4+
S3method(summary,bg_status_df)
45
export(add_tracked_job_parent)
56
export(butterworth_filter_4d)
67
export(cluster_job_submit)
@@ -9,6 +10,8 @@ export(edit_project)
910
export(extract_bids_info)
1011
export(filtfilt_cpp)
1112
export(get_fmriprep_outputs)
13+
export(get_project_status)
14+
export(get_subject_status)
1215
export(get_tracked_job_status)
1316
export(image_quantile)
1417
export(lmfit_residuals_4d)
@@ -76,6 +79,7 @@ importFrom(lubridate,day)
7679
importFrom(lubridate,dhours)
7780
importFrom(lubridate,hour)
7881
importFrom(lubridate,minute)
82+
importFrom(lubridate,parse_date_time)
7983
importFrom(lubridate,second)
8084
importFrom(lubridate,seconds_to_period)
8185
importFrom(reticulate,py_install)

R/status_functions.R

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#' Parse completion time from a .complete file
2+
#'
3+
#' @param file Path to .complete file.
4+
#' @return POSIXct time or NA if parsing fails.
5+
#' @keywords internal
6+
#' @importFrom lubridate parse_date_time
7+
parse_complete_time <- function(file) {
8+
if (!file.exists(file)) return(as.POSIXct(NA))
9+
tm <- tryCatch(readLines(file, n = 1L, warn = FALSE), error = function(e) NULL)
10+
if (is.null(tm) || length(tm) == 0L) return(as.POSIXct(NA))
11+
parsed <- suppressWarnings(lubridate::parse_date_time(tm[1L], orders = c("mdy@HM", "mdy@HMS", "ymd HMS", "ymd HM", "mdy HM", "mdy HMS")))
12+
if (length(parsed) == 0L || is.na(parsed[1L])) return(as.POSIXct(NA))
13+
as.POSIXct(parsed)
14+
}
15+
16+
#' Get processing status for a single subject
17+
#'
18+
#' @param scfg Study configuration list.
19+
#' @param sub_id Subject identifier.
20+
#' @param ses_id Optional session identifier. When `NULL`, all sessions found in the log directory are returned.
21+
#' @return A data.frame with columns indicating completion status and times for each enabled step.
22+
#' @export
23+
#' @importFrom checkmate assert_class assert_string
24+
get_subject_status <- function(scfg, sub_id, ses_id = NULL) {
25+
checkmate::assert_class(scfg, "bg_project_cfg")
26+
checkmate::assert_string(sub_id)
27+
checkmate::assert_string(ses_id, null.ok = TRUE)
28+
29+
steps <- c()
30+
if (isTRUE(scfg$bids_conversion$enable)) steps <- c(steps, "bids_conversion")
31+
if (isTRUE(scfg$mriqc$enable)) steps <- c(steps, "mriqc")
32+
if (isTRUE(scfg$fmriprep$enable)) steps <- c(steps, "fmriprep")
33+
if (isTRUE(scfg$aroma$enable)) steps <- c(steps, "aroma")
34+
if (isTRUE(scfg$postprocess$enable)) steps <- c(steps, "postprocess")
35+
36+
pp_streams <- if ("postprocess" %in% steps) get_postprocess_stream_names(scfg) else character(0)
37+
38+
log_dir <- scfg$metadata$log_directory
39+
sub_log_dir <- file.path(log_dir, paste0("sub-", sub_id))
40+
comp_files <- list.files(sub_log_dir, pattern = "_complete$", full.names = FALSE)
41+
ses_ids <- if (!is.null(ses_id)) ses_id else {
42+
sids <- unique(sub("^.*_ses-([^_]+)_complete$", "\\1", comp_files[grepl("_ses-", comp_files)]))
43+
sids <- sids[!is.na(sids) & sids != "^.*_ses-([^_]+)_complete$"]
44+
if (length(sids) == 0) NA_character_ else sids
45+
}
46+
47+
res <- lapply(ses_ids, function(ss) {
48+
row <- list(sub_id = sub_id, ses_id = ifelse(is.na(ss), NA_character_, ss))
49+
for (st in steps) {
50+
if (st != "postprocess") {
51+
chk <- is_step_complete(scfg, sub_id, ses_id = if (st == "bids_conversion" && !is.na(ss)) ss else NULL, step_name = st)
52+
row[[paste0(st, "_complete")]] <- chk$complete
53+
row[[paste0(st, "_time")]] <- if (chk$complete) parse_complete_time(chk$complete_file) else as.POSIXct(NA)
54+
} else {
55+
for (stream in pp_streams) {
56+
chk <- is_step_complete(scfg, sub_id, ses_id = if (!is.na(ss)) ss else NULL, step_name = "postprocess", pp_stream = stream)
57+
row[[paste0(stream, "_complete")]] <- chk$complete
58+
row[[paste0(stream, "_time")]] <- if (chk$complete) parse_complete_time(chk$complete_file) else as.POSIXct(NA)
59+
}
60+
}
61+
}
62+
row
63+
})
64+
65+
df <- do.call(rbind.data.frame, res)
66+
class(df) <- c("bg_status_df", class(df))
67+
df
68+
}
69+
70+
#' Get processing status for all subjects
71+
#'
72+
#' @param scfg Study configuration list.
73+
#' @return Data.frame with one row per subject/session containing completion status columns.
74+
#' @export
75+
#' @importFrom checkmate assert_class
76+
get_project_status <- function(scfg) {
77+
checkmate::assert_class(scfg, "bg_project_cfg")
78+
log_dir <- scfg$metadata$log_directory
79+
sub_dirs <- list.dirs(log_dir, recursive = FALSE, full.names = FALSE)
80+
sub_ids <- sub("^sub-", "", sub_dirs[grepl("^sub-", sub_dirs)])
81+
res <- lapply(sub_ids, function(id) get_subject_status(scfg, id))
82+
df <- do.call(rbind.data.frame, res)
83+
class(df) <- c("bg_status_df", class(df))
84+
df
85+
}
86+
87+
#' Summarize project status
88+
#'
89+
#' @param object A data.frame produced by `get_project_status()`.
90+
#' @return Data.frame summarizing number of subjects completed for each step.
91+
#' @export
92+
summary.bg_status_df <- function(object, ...) {
93+
step_cols <- grep("_complete$", names(object), value = TRUE)
94+
counts <- vapply(step_cols, function(x) sum(object[[x]], na.rm = TRUE), numeric(1))
95+
data.frame(step = step_cols, n_complete = counts, row.names = NULL, stringsAsFactors = FALSE)
96+
}
97+

man/get_project_status.Rd

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
\name{get_project_status}
2+
\alias{get_project_status}
3+
\title{Get processing status for all subjects}
4+
\usage{get_project_status(scfg)}
5+
\arguments{\item{scfg}{Study configuration list.}}
6+
\value{Data.frame with one row per subject/session containing completion status columns.}
7+
\description{Uses \code{is_step_complete()} to check whether pipeline steps have completed for each subject.}
8+
\seealso{\code{\link{get_subject_status}}}

man/get_subject_status.Rd

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
\name{get_subject_status}
2+
\alias{get_subject_status}
3+
\title{Get processing status for a subject}
4+
\usage{get_subject_status(scfg, sub_id, ses_id = NULL)}
5+
\arguments{\item{scfg}{Study configuration list.}
6+
\item{sub_id}{Subject identifier.}
7+
\item{ses_id}{Optional session identifier.}}
8+
\value{Data.frame summarizing completion status and times for the requested subject/session.}
9+
\description{Checks the log directory for \code{.complete} files and returns completion information for each enabled processing step.}
10+
\seealso{\code{\link{get_project_status}}}

man/summary.bg_status_df.Rd

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
\name{summary.bg_status_df}
2+
\alias{summary.bg_status_df}
3+
\title{Summarize project status}
4+
\usage{\method{summary}{bg_status_df}(object, ...)}
5+
\arguments{\item{object}{A data.frame from \code{get_project_status}.}\item{...}{Additional arguments (unused).}}
6+
\value{Data.frame with counts of subjects that have completed each processing step.}
7+
\description{Provides a tabular summary of completion counts for each step in the pipeline.}
8+
\seealso{\code{\link{get_project_status}}}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
test_that("get_project_status reports completion", {
2+
root <- tempdir()
3+
log_dir <- file.path(root, "logs"); dir.create(log_dir)
4+
bids_dir <- file.path(root, "bids"); dir.create(bids_dir)
5+
fmriprep_dir <- file.path(root, "fmriprep"); dir.create(fmriprep_dir)
6+
mriqc_dir <- file.path(root, "mriqc"); dir.create(mriqc_dir)
7+
8+
sub <- "01"; ses <- "A"
9+
dir.create(file.path(log_dir, paste0("sub-", sub)))
10+
dir.create(file.path(bids_dir, paste0("sub-", sub), paste0("ses-", ses)), recursive = TRUE)
11+
dir.create(file.path(fmriprep_dir, paste0("sub-", sub)), recursive = TRUE)
12+
dir.create(file.path(fmriprep_dir, paste0("sub-", sub), paste0("ses-", ses)), recursive = TRUE)
13+
14+
cat("2024-05-04 10:00:00", file = file.path(log_dir, paste0("sub-", sub), paste0(".bids_conversion_sub-", sub, "_ses-", ses, "_complete")))
15+
cat("2024-05-04 11:00:00", file = file.path(log_dir, paste0("sub-", sub), paste0(".fmriprep_sub-", sub, "_complete")))
16+
cat("2024-05-04 12:00:00", file = file.path(log_dir, paste0("sub-", sub), paste0(".postprocess_stream1_sub-", sub, "_ses-", ses, "_complete")))
17+
18+
scfg <- list(
19+
metadata = list(log_directory = log_dir, bids_directory = bids_dir, fmriprep_directory = fmriprep_dir, mriqc_directory = mriqc_dir),
20+
bids_conversion = list(enable = TRUE),
21+
mriqc = list(enable = FALSE),
22+
fmriprep = list(enable = TRUE),
23+
aroma = list(enable = FALSE),
24+
postprocess = list(enable = TRUE, stream1 = list())
25+
)
26+
class(scfg) <- "bg_project_cfg"
27+
28+
res <- get_project_status(scfg)
29+
expect_equal(nrow(res), 1)
30+
expect_true(res$bids_conversion_complete)
31+
expect_true(res$fmriprep_complete)
32+
expect_true(res$stream1_complete)
33+
34+
sm <- summary(res)
35+
expect_equal(sm$n_complete[sm$step == "bids_conversion_complete"], 1)
36+
})

0 commit comments

Comments
 (0)