--- title: Plugin Development output: rmarkdown::html_vignette: toc: yes toc_depth: 2.0 css: albers.css includes: in_header: albers-header.html params: family: red preset: homage resource_files: - albers.css - albers.js - albers-header.html vignette: | %\VignetteIndexEntry{Plugin Development} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} if (requireNamespace("ggplot2", quietly = TRUE) && requireNamespace("albersdown", quietly = TRUE)) ggplot2::theme_set(albersdown::theme_albers(family = params$family, preset = params$preset)) knitr::opts_chunk$set( collapse = TRUE, comment = "#>", message = FALSE, warning = TRUE ) library(fmrireg) set.seed(42) ``` ```{r albers-classes, echo=FALSE, results='asis'} cat(sprintf( paste0( '' ), params$family, params$preset )) ``` ## Why use the plugin API? The plugin API is for advanced users who want to extend `fmrireg` without forking the package. Two extension points are available: - `register_basis()` adds new basis names that can be used inside `hrf(...)`. - `register_engine()` adds new fitting engines that still return standard `fmri_lm` objects. This vignette shows how to register both kinds of extensions, how engine capabilities interact with global `fmri_lm()` options, and how to inspect the engine metadata that drives dispatch. ## How do you build a minimal dataset? We will work with a small matrix-backed dataset so every example is fast and fully reproducible. ```{r make-dataset} n_time <- 80L n_voxels <- 24L events <- data.frame( onsets = c(8, 20, 32, 44, 56), condition = factor(c("A", "B", "A", "B", "A")), run = 1L ) Y <- matrix(rnorm(n_time * n_voxels), nrow = n_time, ncol = n_voxels) dset <- matrix_frame( Y, TR = 2, run_length = n_time, event_table = events ) stopifnot(inherits(dset, "fmri_frame")) dim(fmridataset::collect_assay(dset)) ``` ## How do you register a custom basis? `register_basis()` maps a string name to a constructor. That name can then be used inside `hrf(...)` just like built-in bases. ```{r register-basis} basis_name <- "vignette_bspline_basis" register_basis( basis_name, function(span = 18, ...) { fmrihrf::gen_hrf(fmrihrf::hrf_bspline, N = 4, span = span) } ) model_with_basis <- create_fmri_model( formula = onsets ~ hrf(condition, basis = basis_name, span = 16), block = ~run, dataset = dset ) stopifnot(inherits(model_with_basis, "fmri_model")) plugin_design <- design_matrix(model_with_basis) stopifnot(qr(plugin_design)$rank == ncol(plugin_design)) head(colnames(plugin_design)) ``` The important rule is that the constructor must return an object that `fmrihrf` understands as a valid basis. ## How do you register a custom engine? An engine receives `(model, dataset, args, cfg)` and should return an `fmri_lm` object. In many cases the easiest path is: 1. extract or transform the response matrix, 2. call one of the helper constructors, 3. return the resulting `fmri_lm` object. The example below centers each voxel time series before fitting and then delegates to `fit_glm_on_transformed_series()`. ```{r register-engine} engine_name <- "vignette_centered_engine" register_engine( name = engine_name, capabilities = list( robust = FALSE, preprocessing = FALSE ), fit = function(model, dataset, args, cfg) { Y_raw <- as.matrix(fmridataset::collect_assay(dataset)) Y_centered <- if (isTRUE(args$center)) { sweep(Y_raw, 2, colMeans(Y_raw), FUN = "-") } else { Y_raw } fit_glm_on_transformed_series( model, Y_centered, cfg = cfg, dataset = dataset, engine = engine_name, strategy = "engine" ) } ) fit_plugin <- fmri_lm( onsets ~ hrf(condition, basis = basis_name, span = 16), block = ~run, dataset = dset, engine = engine_name, engine_args = list(center = TRUE), control = fmri_lm_control(robust = robust_spec("none", max_iter = 10L)) ) stopifnot(inherits(fit_plugin, "fmri_lm")) stopifnot(identical(attr(fit_plugin, "engine"), engine_name)) stopifnot(identical(attr(fit_plugin, "strategy"), "engine")) list( engine = attr(fit_plugin, "engine"), strategy = attr(fit_plugin, "strategy"), n_beta_rows = nrow(fit_plugin$result$betas) ) ``` ## Which helper should you use? Use `fit_glm_on_transformed_series()` when your engine has already produced a time-by-voxel matrix and you want standard OLS-style inference. Use `fit_glm_with_config()` when your engine still wants the integrated AR/robust solver applied to a transformed response matrix. Use `fit_glm_from_suffstats()` when your engine works from streamed cross-products and never materializes the transformed series directly. ## How do capabilities affect the executed configuration? The central dispatcher validates unsupported options before engine code runs. It also keeps two configurations on the fitted object: - `requested_config`: what the caller supplied to `fmri_lm()` - `executed_config`: the engine-scoped configuration that the engine actually received after unsupported sections were normalized away ```{r config-metadata} requested_cfg <- attr(fit_plugin, "requested_config") executed_cfg <- attr(fit_plugin, "executed_config") value_or_na <- function(x) if (is.null(x) || length(x) == 0) NA else x data.frame( config = c("requested", "executed"), robust_type = c( as.character(value_or_na(requested_cfg$robust$type)), as.character(value_or_na(executed_cfg$robust$type)) ), robust_max_iter = c( value_or_na(requested_cfg$robust$max_iter), value_or_na(executed_cfg$robust$max_iter) ), normalized = c( FALSE, !isTRUE(all.equal( value_or_na(requested_cfg$robust$max_iter), value_or_na(executed_cfg$robust$max_iter) )) ) ) ``` If a caller enables an unsupported robust method, the engine is rejected before `fit()` is called. ```{r capability-error-check, include = FALSE} unsupported_message <- NULL tryCatch( fmri_lm( onsets ~ hrf(condition), block = ~run, dataset = dset, engine = engine_name, control = fmri_lm_control(robust = robust_spec("huber")) ), error = function(e) { unsupported_message <<- conditionMessage(e) } ) stopifnot(grepl("does not support robust fitting", unsupported_message)) ``` ```{r capability-error} unsupported_message ``` ## How can you inspect engine specs? Built-in and plugin engines share the same spec object. The exported accessors `engine_spec()` and `engine_specs()` give you the metadata you need without touching internal registries. ```{r inspect-engine-specs} spec_row <- function(name) { spec <- engine_spec(name) data.frame( name = spec$name, source = spec$source, strategy = spec$strategy, aliases = paste(spec$aliases, collapse = ", "), robust = spec$capabilities$robust, preprocessing = spec$capabilities$preprocessing, stringsAsFactors = FALSE ) } rbind( spec_row(engine_name), spec_row("rrr_gls") ) ``` The full registry is available as a named list of spec objects. ```{r list-engine-specs} sort(names(engine_specs())) ``` ## What should extension authors remember? - Keep engine inputs small and explicit: `model`, `dataset`, `args`, `cfg`. - Declare unsupported global features in `capabilities` so the dispatcher can reject them before your engine runs. - Use `requested_config` versus `executed_config` when you need to explain what was requested versus what your engine actually used. - Prefer the helper constructors over hand-building `fmri_lm` objects unless your engine truly needs custom result assembly. ```{r cleanup, include = FALSE} if (exists(basis_name, envir = fmrireg:::.fmrireg_basis_registry, inherits = FALSE)) { rm(list = basis_name, envir = fmrireg:::.fmrireg_basis_registry) } if (exists(engine_name, envir = fmrireg:::.fmrireg_engine_registry, inherits = FALSE)) { rm(list = engine_name, envir = fmrireg:::.fmrireg_engine_registry) } ```