Changelog
Source:NEWS.md
fmridesign (development version)
Changes that affect results
- Character condition variables now get their levels in sorted order, as base
factor()does, instead of in order of first appearance (#54). Regressor (column) order therefore depends only on the set of conditions, so it is the same across subjects and runs whatever the first trial is. Factor input keeps its levels as given. Designs built from character columns whose first-appearance order was not alphabetical will have their condition columns reordered; named contrasts are unaffected. -
hrf(..., basis = "fir", nbasis = k)withkother than 12 now builds a true FIR basis (kindicator bins over 24 s, asfmrihrf::hrf_fir_generator()). It used to build overlapping tent functions, so design matrices for such terms change. - When
nbasisis omitted,hrf(),trialwise()andfeature()give basis sets their standard size: 12 bins for"fir"and 5 functions for"bspline","tent"and"fourier". Previously"fir"and"fourier"got a single function and"bspline"failed to evaluate. An explicitnbasis, and the spmg/gamma/gaussian bases, are unaffected. - Event terms that cross a single-level factor with other variables now get the right columns:
hrf(cond, task)with a constanttaskgives one column percondlevel (it gave a single column of ones), andhrf(cond, rt)with a one-levelcondis modulated byrt(it was all ones). -
pair_contrast(),oneway_contrast()andpoly_contrast()applybasis_weightsacross all basis functions whenbasisisNULLor"all", as documented (e.g.basis_weights = c(1, 0, 0, 0, 0)is equivalent tobasis = 1). The weights used to be ignored silently; a length mismatch is now an error. -
interaction_contrast(~ A * B * C)tests the full highest-order interaction. It silently used only the first two factors. Two-factor results are unchanged. - Contrasts on factor-by-modulator terms such as
hrf(cond, RT)now work: cell weights go on the modulator columns, repeated per basis or modulator column.pair_contrast(),unit_contrast()andpoly_contrast()used to fail with “No conditions were selected”, andoneway_contrast(),interaction_contrast()and formula contrasts returned all-zero weights with only a warning. -
contrast_weights()andFcontrasts()on anevent_modelno longer give zero weights when the model renames a term to keep names unique (e.g.hrf(Ident(RT1, RT2)) + hrf(RT1, RT2, contrasts = ...)). -
whereclauses and the condition-selecting formulas of the contrast constructors are evaluated in the formula’s own environment, so they can use variables from the code that created them (e.g.where = ~ cond == lvlinside a function). They used to fail or pick up package-internal objects. -
design_colmap()anddesign_meta()recordbasis_nameas the basis family spelled as inhrf(basis = ...)("spmg1","spmg3","fir","bspline", …; a plain function is named after its argument, e.g."my_fun", or"custom"), instead of the HRF’s first class ("SPMG3_HRF","FIR_HRF", or just"HRF"). Lagged, blocked and normalised SPMG2, SPMG3 and FIR terms now get properbasis_labels (canonical/derivative/dispersion,lag_00, …) instead ofcomponent_01, …, which also changes their pretty names and plot labels. spmg1 terms get the samebasis_namewith every fmrihrf version. - Within-run
correlation_map()draws every cell one cell wide. Baseline models with two columns per run (e.g.basis = "poly", degree = 2) used to show no cells. -
design_colmap()reports the single global constant drift column (basis = "constant", intercept = "global") as shared across runs (is_block_diagonal = FALSE).
New features
- Added
check_estimability()for full-design rank, exact condition numbers, named weak directions, and contrast-specific estimability and variance factors. Event models include run intercepts by default and report event coverage per run; an explicit baseline/nuisance design can also be supplied.check_collinearity()now documents the limitations of pairwise checks (#32).
CRAN compliance
Design-column convolution no longer reaches into fmrihrf’s unexported
evaluate_regressor_cpp(). The shared-HRF fast path now evaluates each live column with the publicfmrihrf::regressor()/fmrihrf::evaluate()API. Results are identical to the previous path on every design built by the test suite; building a design matrix is roughly 1.3-2x slower (tens of milliseconds on a 4-run, 1200-scan design).boxcar_hrf_gen()andweighted_hrf_gen()callfmrihrf::hrf_boxcar()andfmrihrf::hrf_weighted()directly instead of looking them up in fmrihrf’s namespace.fmridesign no longer registers its own
print()method for fmrihrf’ssampling_frameclass, which overwrote fmrihrf’s method on load (“Registered S3 method overwritten by ‘fmridesign’”). Sampling frames now print with fmrihrf’s method.plot(<sampling_frame>)is unchanged and is documented under?plot.sampling_frame.onsets(),durations(),blockids()andnbasis()are now true re-exports of fmrihrf’s generics. fmridesign previously defined its own generics with the same names, so dispatch was split: for examplefmrihrf::onsets(<event_term>)found no method andfmridesign::nbasis(HRF_SPMG3)failed, and which one a bare call reached depended on attach order. Attaching fmridesign no longer masks these four names from fmrihrf.fmridesign now supplies every name and accessor method for its own classes, so downstream packages can use plain dispatch on the exported generics instead of defining duplicates or reaching into fmridesign’s namespace. New methods:
longnames()andshortnames()forevent_model,convolved_term,feature_termand bare events (event_seq);columns()andcells()forevent_model;conditions(),event_table(),nbasis()anddesign_matrix()forconvolved_term; andconditions()forbaseline_model. They previously lived in fmrireg.longnames()uses fmridesign’s canonicalvariable.levelform (condition.A), which is the design-matrix column name without its<term>_prefix (condition_condition.A);?longnamesdocuments the relationship. Design-matrix column names are unchanged.longnames(),shortnames()andcondition_map()now honourdrop.empty = TRUE(the default): an interaction cell with no events is left out, as it is from the design matrix, so the names line up one-to-one with the columns.drop.empty = FALSElists the full grid of factor levels, which is whatconditions()always returns (documented).condition_map(<event_model>)finds each condition’s column by its exact name,<term tag>_<canonical>, instead of by position. It previously returnedcolumn_name = NAfor every row of a term with an empty cell or a multi-basis HRF.blockids(<event_model>)is documented (?blockids.event_model): it returns one run id per event. Per-scan ids come fromblockids(x$sampling_frame);blocklens(x)gives scans per run. Because fmrireg previously overrode this method with per-scan ids, the first call in a session prints a one-time message. The message will be removed in the next release.correlation_map(<baseline_model>)gainswithin_run = TRUE: run intercepts are dropped, columns are centred within each run, and run-specific columns are correlated on their own run, with pairs from different runs left out. This reproduces fmrireg’s former method exactly;within_run = FALSEgives the previous fmridesign behaviour (runs concatenated). Cells are labelled by default when there are at most 12 columns, as in fmrireg’s method (event models keep the 20-column default). Both methods acceptlabel_valuesas an alias forannotate, and theevent_modelmethod acceptswithin_runtoo (defaultFALSE).correlation_map()now errors on arguments thatgeom_tile()does not accept, instead of passing them on to be dropped with a warning. The deprecatedsizeis still accepted.
Bug fixes
-
contrast_set()now combines individual specifications with nested contrast sets, including those returned byone_against_all_contrast(). It preserves leaf names and order, and reports invalid entries by argument path (#33). -
hrf(..., summate = FALSE)is honoured again for sustained events. The shared-HRF fast path ignoredsummateand always produced thesummate = TRUEdesign. - An event with a negative onset in the first run now fails with fmrihrf’s “
onsetsmust be non-negative” error, as the legacy path andfmrihrf::regressor()always did; the fast path had silently accepted it. The out-of-frame warning is still raised first. -
term_matrices()returns one entry per term, withNULLfor a term without design columns, for both event and baseline models; such a term used to shift every later matrix onto the wrong term name. -
cells()on multi-basis convolved terms lists only observed cells with correctly aligned counts, labels ten or more basis functionsbasis01,basis02, … in order, and works for terms with continuous variables. -
split_onsets()labels each condition’s onsets correctly when a parametric modulator is zero for every event of a condition. -
Fcontrasts()no longer fails for terms crossing a single-level factor, andconditions()no longer errors on a multi-variable term containing a factor without levels. -
condition_basis_list()names its result by condition for terms with a single continuous variable (the name was empty). - Formula contrasts (
contrast()) work on continuous terms with several variables, such ashrf(Ident(RT1, RT2))with~ RT1 - RT2. -
poly_contrast()reports avalue_mapthat misses factor levels instead of failing with “subscript out of bounds”. -
validate_contrasts()accepts a list of contrast vectors, names unnamed entriescontrast1,contrast2, …, and returns a zero-row data frame when nothing can be validated. -
hrf()no longer errors whenbasisis an HRF object or function andhrf_funis also given; it warns thathrf_funtakes precedence. - The basis names
"bs"and"tent", documented in?hrf, are accepted. - Degree-1 polynomial modulators (
hrf(Poly(x, 1))), and polynomial terms whose subset keeps a single event, no longer fail with “Processed event value is not a matrix”.predict()works on subsettedPolyandBSplinebases. -
baseline_model(nuisance_check = "drop")no longer fails when every nuisance column of a run is dropped, and zero-column nuisance matrices are accepted bybaseline_model()andcheck_nuisance(). -
construct()on anuisance()specification works (it always failed), andnuisance()terms no longer trigger tibble’s name-repair warning. - Runwise intercept columns no longer carry stray
assign/contrastsattributes frommodel.matrix(); their values are unchanged. -
requires_external_processing()accepts a registered class name, andget_all_external_hrf_functions()returnscharacter(0)rather thanNULLwhen nothing is registered. -
print()on baseline models and contrast specifications returns the object invisibly. -
plot()for event models no longer warns “Ignoring unknown parameters: inherit.aes” (ggplot2 < 3.5) or, withlabel_mode = "compact", about non-ASCII characters; per-run TRs in its subtitle are no longer padded. -
correlation_map()no longer errors withannotate = FALSEwhen there is nothing to correlate, can be drawn with ggplot2 < 3.5.0 when it has more than 10 columns, and says when no two regressors share a run instead of claiming that no regressor varies. -
convolve()warnings for terms built directly withevent_term()name the term, andevent_term(list())fails with a clear message. - The
event_model,event_factor,event_matrixandlabels.eventhelp pages no longer contain misplaced text or wrong example output.
Performance
- Building event models is 5-40 times faster with the current fmrihrf, whose convolution engine no longer starts OpenMP threads and computes only the samples the scan grid needs. Results are unchanged.
-
baseline_model()andcheck_nuisance()share their drift construction and block assembly (about 25% faster), andvalidate_contrasts()computes the design rank once (about twice as fast with many contrasts).
Packaging and internal changes
-
Remotes:tracksbbuchsbaum/fmrihrfinstead of a pre-release commit, so GitHub installs and CI use the same fmrihrf as r-universe. fmridesign still requires only fmrihrf (>= 0.4.0) and is tested against 0.4.0. - stringr is no longer imported; tidyr and Matrix move to Suggests (only the vignettes and tests use them); ggplot2 (>= 3.4.0) is declared.
- Large internal simplification: the contrast constructors and
contrast_weights()methods share validation and packaging helpers,contrast_weights()andFcontrasts()on event models share one weight-embedding helper, the scaling bases share one z-score helper, and the plotting code shares its colour, scale and glyph helpers. Dead code (dctbasis(),translate_legacy_pattern(), never-hit caches, duplicate%||%definitions, an fmrihrf <= 0.4.0 warning shim, …) and unused imports are gone.
fmridesign 0.6.1
Plotting overhaul
- All plotting methods share one visual system: the new exported
theme_fmridesign()andfmridesign_palette(), a colour-vision-checked categorical palette, one diverging palette centred on zero, readable column labels (e.g.face × high,drift 3,tx) and one way of marking runs. -
plot(<event_model>)draws one row per condition with event onset and duration marks. Rows of a term share an amplitude scale (y_scale). Basis sets get one row per basis function, and trialwise or long-basis designs switch to a heatmap with a per-trial overlap column. Newstyle,show_events,y_scale,time_range,titleandsubtitlearguments. -
design_map()is an SPM-style image grouped by term, with raw column ranges and a near-flat (dead) column check. Run boundaries are now placed correctly; they were previously computed from per-event block ids. -
correlation_map()shows the lower triangle with each column’s variance inflation factor (run means removed) on the diagonal and flags aliased columns. -
plot_contrasts()shows every design column, printed weights, the sum of weights and each contrast’s standard error per unit noise given the design. -
plot(<baseline_model>)shows every time-varying term (motion regressors were previously dropped), splitting translations and rotations into separate lanes. User-supplied nuisance column names are kept for display. -
plot(<sampling_frame>)no longer draws an empty panel; it adds"lane"and"grid"styles and anevents =overlay for checking coverage.
Bug fixes
-
User-visible correction: multi-basis column names change. For
hrf()terms with more than one basis function ("spmg2","spmg3", FIR, B-spline, tent, customnbasis > 1) and two or more conditions, design columns were filled condition-major (A_b01, A_b02, B_b01, ...) but named basis-major (A_b01, B_b01, A_b02, ...), so every column except the first and last was mislabelled. For example,task_task.scene_b01held face’s temporal derivative. The data layout is unchanged; column names,conditions(term, expand_basis = TRUE), and every name-based contrast (pair_contrast(),oneway_contrast(),column_contrast(),unit_contrast(),contrast(),interaction_contrast()) now follow the documented condition-majorterm_condition_b##layout, matchingdesign_colmap(). Contrast weights and coefficient labels from earlier versions for such terms were attached to the wrong columns and should be recomputed. Code that indexed multi-basis columns by position assuming basis-major order must be updated (#23). -
covariate()now expands matrix/data-frame arguments into one non-convolved regressor per column. Named inputs preserve sanitized column names, unnamed matrices usef01,f02, … suffixes, and final names follow the standard<term_tag>_<condition_tag>grammar (cov_xby default, ormotion_xwithid = "motion"). Covariate condition accessors and per-column metadata now expose the individual regressor identities instead of a concatenated multi-variable term name (#19). -
column_contrast()patterns now match the design-matrix column names, as documented (term_tag_condition_tag[_b##], e.g."^cond_cond\\.A$"or"^cond_cond\\.A_b01$"). Previously they were matched only against term-level condition names (cond.A), so documented patterns selected nothing. Term-level patterns still work: they are tried only when a pattern matches no design-matrix column, and select the same columns. A pattern that selects different columns in the two namespaces is now an error, and a pattern that matches nothing warns with the available column names (#24). -
design_matrix(<baseline_term>, blockid = )now returns each active column once. Withintercept = "global", requesting several runs used to return one duplicateconstant_globalcolumn per run (soblockid = 1:3gave three identical columns rather than one); the result now always equals the requested rows and non-zero columns of the full term matrix, in the term’s column order. Abasis = "constant", intercept = "global"drift term likewise returned zero columns for any run but the first. Terms without block structure now error onblockidinstead of returning an empty matrix. -
Fcontrasts(<event_model>)no longer returns an all-zero matrix with an “unmatched row names” warning for multi-basis terms. The term-level contrast is expanded tokronecker(C, diag(nbasis)), testing the condition effect jointly in every basis function (#25). -
Fcontrasts()for multi-factor terms (e.g.hrf(task, load)) assigned row names in the wrong order, so the matrix labelledtasktestedloadand vice versa. Rows now followconditions()order. -
interaction_contrast()now names its rows with the term’s canonical condition tags (e.g.task.face_load.low) and expands them across basis functions. It previously used raw cell labels (face_low) that matched no design column, producing all-zero weights with a warning. -
unit_contrast()and formula contrasts (contrast(~ face - obj)), and differences of contrasts built from them, now expand their weights across the basis functions of a multi-basis term. They previously returned zero-row weights with an “unmatched row names” warning. -
unit_contrast()now applies a logical selector inA: previouslyunit_contrast(~ cond == "A")ignoredAand averaged over every cell (weights 0.5/0.5 for two levels); it now selects level A only (weight 1). A bare factor (~ cond) still averages over all cells. -
Fcontrasts(<event_model>)skips terms with no categorical variable (e.g.hrf(rt), covariates) instead of failing for the whole model; a model with only such terms returns an empty list. -
condition_basis_list()now works for bareevent_termobjects without aterm_tag, which previously returned an empty list. -
baseline_model(nuisance_list = ...)now keeps the user’s nuisance column names (#28). Columns are namednuis_<name>_block_<run>(e.g.nuis_trans_x_block_1), matching the drift columns (base_poly1_block_1); names are sanitised to syntactic tokens and made unique within a run, and unnamed columns fall back to their original column index (nuis_2_block_1), which is preserved whennuisance_check = "drop"removes columns. This replaces the previousnuis#<run>_<col>names, so code that matched those names must be updated. The original names are kept in the nuisance term’ssource_colnamesfield. -
design_colmap(<baseline_model>)now reports nuisance columns with role"nuisance"(they were reported as"intercept"), takes theirrunfrom the block structure (it was parsed from the column index, so a 3-run model with 6 regressors per run reported runs 1 to 6), and labels them with the user’s column names inbasis_label.
fmridesign 0.6.0
New features
-
baseline_model()now checksnuisance_listinputs during construction for zero-variance columns, duplicate or near-duplicate columns, non-finite values, nuisance rank deficiency, and columns aliased with baseline terms. - Added
nuisance_check = c("warn", "error", "drop", "none")to control whether nuisance problems warn, stop, are dropped with an audit warning, or are skipped. - Added
check_nuisance()andclean_nuisance()helpers for inspecting and repairing block-wise nuisance regressors before model construction.
Performance
- Sped up
event_model()design-matrix construction by replacing the per-termtibble::tibble()/dplyr::bind_rows()calls used to assemble column metadata with a lightweight, validatedtibbleconstructor. This removes the metadata-building hotspot (~15% faster end-to-end on a representative multi-term, multi-run model) with byte-identical design matrices, column names,col_indices/term_spans, and metadata values. -
convolve.event_term()now skips columns that are all-zero within a block instead of building and evaluating an emptyfmrihrfregressor for each. For block-diagonal-ish designs (trialwise/LSS single-trial models, or factor levels present only in some runs) this is a large speedup (~2.2x faster on a representative 360-column trialwise model) while producing bit-identical output. Designs where every column is populated in every block are unaffected (a fast-exit keeps the original path), and blocks containingNA/NaNfall back to the previous full-column path so filtering semantics are unchanged. - Convolution hot path now shares one fine-grid HRF matrix across all columns and blocks and calls
fmrihrf’s C++ evaluator directly, skipping per-columnRegconstruction /prep_reg_inputsoverhead. Combined with a single global output matrix (no per-block zero-alloc +rbind) and deferred tibble materialization inbuild_event_model_design_matrix(), this is ~2–3.5× faster end-to-end on trialwise/LSS and multi-term workloads while remaining bit-identical tofmrihrf::evaluate(regressor(...)). Per-onsethrf_funlists and NA-misaligned designs keep the previous path. - Added
bench/cross-library design-matrix benchmarks against nilearn (the FitLins first-level design-matrix hot path). Run withbash bench/run_compare.sh; seebench/RESULTS.mdandbench/OPTIMIZATION_NOTES.md.
Bug fixes
-
contrast_weights()now removes rows for factor levels excluded by anhrf(..., subset = )term from the returned term-localweights, keeping them consistent with the reconciled full-designoffset_weights(#17). -
convolve_design()now extracts each condition column withdmat[[i]], so it produces correct regressors for basedata.frameinputs (its documented example). The previousdmat[, i][[1]]collapsed a data frame column to its first element; the tibble-based internal call path was unaffected. -
contrast_weights()andFcontrasts()forevent_modelobjects now name interaction-term contrasts with the same term tags used by design-matrixcol_indices, preventing downstream consumers from dropping crossed-term contrasts because of:/_key mismatches (#9). -
event_model()now warns when continuous parametric modulators are all-zero or have zero variance, catching degenerate design columns before model fitting while preserving the existing design-matrix shape (#8). - Fixed list-based
event_model()specifications sohrf(..., subset = )expressions can use base operators and helper functions from the calling environment, matching formula-interface subset behavior. - Fixed event-model column metadata construction for factor and multi-basis HRF terms that expand to multiple design columns.
- Event terms whose subsets select zero events now retain their canonical condition-column names and metadata on the resulting all-zero design matrix, rather than falling back to generic
col_1,col_2, … names. - Suppressed exact, known false-positive metadata warnings produced when decorated HRFs are reconstructed by
fmrihrf0.3.0, while continuing to surface unrelated warnings.