                     Changes in version 0.6.1.9000                      

Changes that affect results

  - hrf(..., basis = "fir", nbasis = k) with k other than 12 now builds
    a true FIR basis (k indicator bins over 24 s, as
    fmrihrf::hrf_fir_generator()). It used to build overlapping tent
    functions, so design matrices for such terms change.
  - When nbasis is omitted, hrf(), trialwise() and feature() 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 explicit
    nbasis, 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 constant task
    gives one column per cond level (it gave a single column of ones),
    and hrf(cond, rt) with a one-level cond is modulated by rt (it was
    all ones).
  - pair_contrast(), oneway_contrast() and poly_contrast() apply
    basis_weights across all basis functions when basis is NULL or
    "all", as documented (e.g. basis_weights = c(1, 0, 0, 0, 0) is
    equivalent to basis = 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() and
    poly_contrast() used to fail with "No conditions were selected", and
    oneway_contrast(), interaction_contrast() and formula contrasts
    returned all-zero weights with only a warning.
  - contrast_weights() and Fcontrasts() on an event_model no longer give
    zero weights when the model renames a term to keep names unique
    (e.g. hrf(Ident(RT1, RT2)) + hrf(RT1, RT2, contrasts = ...)).
  - where clauses 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 == lvl inside a function). They used to fail or pick up
    package-internal objects.
  - design_colmap() and design_meta() record basis_name as the basis
    family spelled as in hrf(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 proper basis_labels
    (canonical / derivative / dispersion, lag_00, ...) instead of
    component_01, ..., which also changes their pretty names and plot
    labels. spmg1 terms get the same basis_name with 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 public fmrihrf::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() and weighted_hrf_gen() call fmrihrf::hrf_boxcar()
    and fmrihrf::hrf_weighted() directly instead of looking them up in
    fmrihrf's namespace.

  - fmridesign no longer registers its own print() method for fmrihrf's
    sampling_frame class, 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() and nbasis() are now true
    re-exports of fmrihrf's generics. fmridesign previously defined its
    own generics with the same names, so dispatch was split: for example
    fmrihrf::onsets(<event_term>) found no method and
    fmridesign::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() and shortnames()
    for event_model, convolved_term, feature_term and bare events
    (event_seq); columns() and cells() for event_model; conditions(),
    event_table(), nbasis() and design_matrix() for convolved_term; and
    conditions() for baseline_model. They previously lived in fmrireg.
    longnames() uses fmridesign's canonical variable.level form
    (condition.A), which is the design-matrix column name without its
    <term>_ prefix (condition_condition.A); ?longnames documents the
    relationship. Design-matrix column names are unchanged.

  - longnames(), shortnames() and condition_map() now honour drop.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 = FALSE lists the full grid
    of factor levels, which is what conditions() 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 returned column_name = NA for 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 from
    blockids(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>) gains within_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 = FALSE gives 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 accept label_values as an
    alias for annotate, and the event_model method accepts within_run
    too (default FALSE).

  - correlation_map() now errors on arguments that geom_tile() does not
    accept, instead of passing them on to be dropped with a warning. The
    deprecated size is still accepted.

Bug fixes

  - contrast_set() now combines individual specifications with nested
    contrast sets, including those returned by
    one_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 ignored summate and always produced the
    summate = TRUE design.
  - An event with a negative onset in the first run now fails with
    fmrihrf's "onsets must be non-negative" error, as the legacy path
    and fmrihrf::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, with NULL for 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 functions
    basis01, 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, and conditions() 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 as hrf(Ident(RT1, RT2)) with ~ RT1 - RT2.
  - poly_contrast() reports a value_map that misses factor levels
    instead of failing with "subscript out of bounds".
  - validate_contrasts() accepts a list of contrast vectors, names
    unnamed entries contrast1, contrast2, ..., and returns a zero-row
    data frame when nothing can be validated.
  - hrf() no longer errors when basis is an HRF object or function and
    hrf_fun is also given; it warns that hrf_fun takes 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
    subsetted Poly and BSpline bases.
  - baseline_model(nuisance_check = "drop") no longer fails when every
    nuisance column of a run is dropped, and zero-column nuisance
    matrices are accepted by baseline_model() and check_nuisance().
  - construct() on a nuisance() specification works (it always failed),
    and nuisance() terms no longer trigger tibble's name-repair warning.
  - Runwise intercept columns no longer carry stray assign / contrasts
    attributes from model.matrix(); their values are unchanged.
  - requires_external_processing() accepts a registered class name, and
    get_all_external_hrf_functions() returns character(0) rather than
    NULL when 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, with label_mode =
    "compact", about non-ASCII characters; per-run TRs in its subtitle
    are no longer padded.
  - correlation_map() no longer errors with annotate = FALSE when 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 with event_term() name
    the term, and event_term(list()) fails with a clear message.
  - The event_model, event_factor, event_matrix and labels.event help
    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() and check_nuisance() share their drift construction
    and block assembly (about 25% faster), and validate_contrasts()
    computes the design rank once (about twice as fast with many
    contrasts).

Packaging and internal changes

  - Remotes: tracks bbuchsbaum/fmrihrf instead 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() and Fcontrasts() 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.

                        Changes in version 0.6.1                        

Plotting overhaul

  - All plotting methods share one visual system: the new exported
    theme_fmridesign() and fmridesign_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. New style, show_events, y_scale, time_range, title and
    subtitle arguments.
  - 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 an events = 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, custom nbasis > 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_b01 held 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-major
    term_condition_b## layout, matching design_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 use f01, f02, ... suffixes, and final
    names follow the standard <term_tag>_<condition_tag> grammar (cov_x
    by default, or motion_x with id = "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. With intercept = "global", requesting several runs used
    to return one duplicate constant_global column per run (so blockid
    = 1:3 gave 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. A basis = "constant",
    intercept = "global" drift term likewise returned zero columns for
    any run but the first. Terms without block structure now error on
    blockid instead 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 to kronecker(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 labelled task tested
    load and vice versa. Rows now follow conditions() 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 in A: previously
    unit_contrast(~ cond == "A") ignored A and 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 bare event_term objects without
    a term_tag, which previously returned an empty list.
  - baseline_model(nuisance_list = ...) now keeps the user's nuisance
    column names (#28). Columns are named nuis_<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 when
    nuisance_check = "drop" removes columns. This replaces the previous
    nuis#<run>_<col> names, so code that matched those names must be
    updated. The original names are kept in the nuisance term's
    source_colnames field.
  - design_colmap(<baseline_model>) now reports nuisance columns with
    role "nuisance" (they were reported as "intercept"), takes their run
    from 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 in basis_label.

                        Changes in version 0.6.0                        

New features

  - baseline_model() now checks nuisance_list inputs 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() and clean_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-term tibble::tibble() / dplyr::bind_rows() calls used to
    assemble column metadata with a lightweight, validated tibble
    constructor. 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 empty fmrihrf regressor
    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 containing NA/NaN fall 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-column Reg construction / prep_reg_inputs overhead.
    Combined with a single global output matrix (no per-block zero-alloc
    + rbind) and deferred tibble materialization in
    build_event_model_design_matrix(), this is ~2–3.5× faster end-to-end
    on trialwise/LSS and multi-term workloads while remaining
    bit-identical to fmrihrf::evaluate(regressor(...)). Per-onset
    hrf_fun lists 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 with bash
    bench/run_compare.sh; see bench/RESULTS.md and
    bench/OPTIMIZATION_NOTES.md.

Bug fixes

  - contrast_weights() now removes rows for factor levels excluded by an
    hrf(..., subset = ) term from the returned term-local weights,
    keeping them consistent with the reconciled full-design
    offset_weights (#17).
  - convolve_design() now extracts each condition column with dmat[[i]],
    so it produces correct regressors for base data.frame inputs (its
    documented example). The previous dmat[, i][[1]] collapsed a data
    frame column to its first element; the tibble-based internal call
    path was unaffected.
  - contrast_weights() and Fcontrasts() for event_model objects now name
    interaction-term contrasts with the same term tags used by
    design-matrix col_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 so hrf(..., 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 fmrihrf 0.3.0, while
    continuing to surface unrelated warnings.
