Optimized Analytic Single-pass Inverse Solution (OASIS) extends Least Squares Separate (LSS) estimation through algebraic reformulation that enables single-pass computation of all trial estimates. For a single-basis HRF (K = 1) without ridge, OASIS reduces exactly to the closed-form LSS solution; the per‑trial 2x2 normal equations are the same. The value of OASIS is in batching those solves efficiently and generalizing the same algebra to multi‑basis HRFs (2Kx2K) with optional ridge and diagnostics. This document provides the mathematical foundation and implementation details.
Read this after vignette("oasis_method") if you want the
linear algebra behind the user-facing API. The code chunks below are
there to make the main scaling claims executable rather than purely
narrative.
This vignette assumes familiarity with: - QR decomposition and orthogonal projection matrices - Ridge regression and regularization - Matrix calculus and linear algebra - The standard LSS formulation
Standard LSS requires N separate GLM fits for N trials, each involving: 1. Matrix assembly: O(T²) operations 2. QR decomposition: O(T³) operations 3. Back-substitution: O(T²) operations
Total complexity: O(NT³) for N trials
OASIS recognizes that these N models share substantial structure. By factoring out common computations, OASIS reduces complexity to: 1. Single QR decomposition: O(T³) 2. Shared projections: O(NT²) 3. Per-trial solutions: O(N)
Total complexity: O(T³ + NT²), a significant reduction when N is large.
# Demonstrate computational scaling
N_trials <- c(10, 50, 100, 200, 500, 1000)
T_points <- 200 # Fixed number of timepoints
# Simplified complexity models (arbitrary units)
classical_ops <- N_trials * T_points^3 / 1e6 # O(NT³)
oasis_ops <- (T_points^3 + N_trials * T_points^2) / 1e6 # O(T³ + NT²)
# Create comparison plot
plot(N_trials, classical_ops, type='l', col='red', lwd=2,
xlab='Number of Trials', ylab='Computational Operations (millions)',
main='Computational Complexity: Classical LSS vs OASIS',
ylim=c(0, max(classical_ops)))
lines(N_trials, oasis_ops, col='blue', lwd=2)
legend('topleft', c('Classical LSS', 'OASIS'),
col=c('red', 'blue'), lwd=2, bty='n')
# Add shaded region showing computational savings
polygon(c(N_trials, rev(N_trials)),
c(classical_ops, rev(oasis_ops)),
col=rgb(0.2, 0.8, 0.2, 0.3), border=NA)
text(500, mean(c(classical_ops[4], oasis_ops[4])),
'Computational\nSavings', col='darkgreen')Computational complexity: Classical LSS vs OASIS
complexity_summary <- data.frame(
Trials = N_trials,
Classical = classical_ops,
OASIS = oasis_ops,
SpeedupRatio = classical_ops / oasis_ops
)
complexity_summary
#> Trials Classical OASIS SpeedupRatio
#> 1 10 80 8.4 9.52381
#> 2 50 400 10.0 40.00000
#> 3 100 800 12.0 66.66667
#> 4 200 1600 16.0 100.00000
#> 5 500 4000 28.0 142.85714
#> 6 1000 8000 48.0 166.66667Code references point to R/oasis_glue.R and
src/oasis_core.cpp implementations.
Notation used throughout:
We first treat the single-basis case (one regressor per trial) before generalizing to multi-basis HRFs.
Classical LSS fits, for each trial \(j\), a GLM with design \([x_j, b_j, Z]\), where \(b_j = \sum_{i \neq j} x_i\). Solving each model independently costs \(\mathcal{O}(N)\) QR factorizations. Algebraically, the trial-specific beta can be expressed as
\[ \hat{\beta}_j = \frac{\langle Rx_j, RY \rangle - \frac{\langle Rx_j, Rb_j \rangle}{\|Rb_j\|^2} \langle Rb_j, RY \rangle}{\|Rx_j\|^2 - \frac{\langle Rx_j, Rb_j \rangle^2}{\|Rb_j\|^2}}. \]
OASIS extracts and reuses the common computational components (projections, norms, cross-products) across all trials, computing each only once.
After residualising against nuisance regressors we define:
Let \(n_{jv} = \langle a_j, RY_{\cdot v} \rangle\) and \(m_v = \langle s, RY_{\cdot v} \rangle\). The pair \((\beta_j, \gamma_j)\) solving the 2x2 system for trial \(j\) and voxel \(v\) is obtained from
\[ G_j \begin{bmatrix} \beta_{jv} \\ \gamma_{jv} \end{bmatrix} = \begin{bmatrix} n_{jv} \\ m_v - n_{jv} \end{bmatrix}, \quad G_j = \begin{bmatrix} d_j + \lambda_x & \alpha_j \\ \alpha_j & s_j + \lambda_b \end{bmatrix}, \]
with ridge penalties \(\lambda_x, \lambda_b
\ge 0\). The inverse of \(G_j\)
is analytic, so
\[
\beta_{jv} = \frac{(s_j + \lambda_b) n_{jv} - \alpha_j (m_v -
n_{jv})}{(d_j + \lambda_x)(s_j + \lambda_b) - \alpha_j^2}.
\]
This is exactly what oasis_betas_closed_form()
implements (C++ file src/oasis_core.cpp). The
precomputation step oasis_precompute_design() produces
\(a_j, s, d_j, \alpha_j, s_j\) once,
while oasis_AtY_SY_blocked() streams through voxels to
obtain \(n_{jv}\) and \(m_v\).
oasis$ridge_mode = "fractional" sets \(\lambda_x = \eta_x \cdot \bar{d}\) and
\(\lambda_b = \eta_b \cdot \bar{s}\),
where \(\bar{d}\) and \(\bar{s}\) are means of \(d_j\) and \(s_j\). The helper
.oasis_resolve_ridge() implements this scaling. Absolute
ridge uses the supplied values directly.
Given \(G_j^{-1}\) and residual norm \(\|RY\|^2\), the variance of \(\beta_{jv}\) is
\[ \operatorname{Var}(\hat{\beta}_{jv}) = \sigma_{jv}^2 \left( G_j^{-1} \right)_{11}, \quad \sigma_{jv}^2 = \frac{\text{SSE}_{jv}}{\text{dof}}, \]
with
\[ \text{SSE}_{jv} = \|RY_{\cdot v}\|^2 - 2 (\beta_{jv} n_{jv} + \gamma_{jv} (m_v - n_{jv})) + d_j \beta_{jv}^2 + s_j \gamma_{jv}^2 + 2 \alpha_j \beta_{jv} \gamma_{jv}. \]
.oasis_se_from_norms() realises this computation,
reusing \(n_{jv}\), \(m_v\) and the cached design scalars.
When the HRF contributes \(K > 1\) basis functions, each trial has columns \(A_j \in \mathbb{R}^{T \times K}\). Define
Per voxel we need \(N1 = A^T RY\) (stacked \(N\) blocks of size \(K\)) and \(SY = S^T RY\). The block system is
\[ \begin{bmatrix} D_j + \lambda_x I & C_j \\ C_j^T & E_j + \lambda_b I \end{bmatrix} \begin{bmatrix} B_{jv} \\ \Gamma_{jv} \end{bmatrix} = \begin{bmatrix} N1_{jv} \\ SY_v - N1_{jv} \end{bmatrix}, \]
where \(B_{jv} \in \mathbb{R}^K\).
oasisk_betas() solves this 2Kx2K system via Cholesky
factorisation. Ridge again adds \(\lambda_x
I\) and \(\lambda_b I\) to the
block diagonals. Compared to the single-basis path, only the shapes of
the cached matrices differ; the solve is still analytic per trial/voxel
block.
The companion oasisk_betas_se() extends the SSE/variance
calculation to the multi-basis case, using the same building blocks.
OASIS can construct \(X\) on the fly
from event specifications. .oasis_build_X_from_events()
uses fmrihrf::regressor_set() to generate trial-wise
columns (and optional other-condition aggregates) given:
cond$onsets: per-trial onset timescond$hrf: HRF object (canonical, FIR, multi-basis,
user-defined)cond$span, precision, method:
convolution controlsThis design is then residualised against nuisance regressors and fed
into the algebra above. Because the HRF definition enters directly,
switching HRFs or running grid searches automatically regenerates a
matching design. When you provide an explicit X, OASIS
skips this step and assumes you have already encoded the HRF in the
matrix.
oasis$whiten = "ar1" estimates a common AR(1)
coefficient from residualised data. .oasis_ar1_whitener()
computes \(\rho\) and applies
Toeplitz-safe differencing:
\[ \tilde{y}_t = \begin{cases} \sqrt{1 - \rho^2} y_1 & t = 1, \\ y_t - \rho y_{t-1} & t > 1. \end{cases} \]
The same transformation is applied to \(X\) and nuisance regressors before the standard OASIS algebra runs. Whitening preserves the single-pass benefits because the transformed data are treated exactly like the original inputs.
When oasis$return_diag = TRUE, OASIS returns the
precomputed design scalars:
oasis_precompute_design())oasisk_precompute_design())These matrices are useful for checking trial collinearity, energy, and the effect of ridge scaling.
Putting everything together, the single-basis solver proceeds as follows:
oasis_precompute_design).oasis_AtY_SY_blocked).The multi-basis path swaps steps 2–5 for their block equivalents. In both cases, the cost is dominated by the single projection of \(Y\) and the matrix–vector multiplies in step 3, giving \(\mathcal{O}(T V)\) complexity with a small trial-dependent overhead.
Compared to classical LSS (\(N\) separate regressions), OASIS shaves off repeated projections and linear solves, yielding substantial speedups when \(N\) or \(V\) is large.
vignette("oasis_method") — practical OASIS usage with
ridge, multi-basis HRFs, and standard errorsvignette("fmrilss") — foundational LSS conceptsvignette("sbhm") — library-constrained voxel-specific
HRFsR/oasis_glue.R and
src/oasis_core.cpp (for implementation alignment).