--- title: Working With LatentNeuroVec Objects output: rmarkdown::html_vignette: toc: yes toc_depth: 2.0 css: albers.css includes: in_header: albers-header.html resource_files: - albers.css - albers.js - albers-header.html params: family: red preset: homage vignette: | %\VignetteIndexEntry{Working With LatentNeuroVec Objects} %\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 = FALSE ) ``` ```{r albers-classes, echo=FALSE, results='asis'} cat(sprintf( paste0( '' ), params$family, params$preset )) ``` ```{r library} library(fmrilatent) ``` A `LatentNeuroVec` behaves like a neuroimaging time series, but it stores a factorization instead of a dense time-by-voxel matrix. This vignette shows the basic access pattern: encode data, inspect the factors, pull out a voxel time series, and reconstruct only when you need the dense matrix. ## What do we start with? The examples use a small 4 x 4 x 4 mask and a 24-time-point matrix. Rows are time points and columns are in-mask voxels. ```{r make-data} mask <- array(TRUE, dim = c(4, 4, 4)) mask_vol <- neuroim2::LogicalNeuroVol(mask, neuroim2::NeuroSpace(dim(mask))) set.seed(7) X <- matrix(rnorm(24 * sum(mask)), nrow = 24) dim(X) ``` ## How is the object encoded? A temporal DCT basis stores shared time functions in `basis()` and voxel weights in `loadings()`. ```{r encode} lat <- encode(X, spec_time_dct(k = 8), mask = mask_vol, materialize = "matrix") data.frame( time_points = nrow(basis(lat)), components = ncol(basis(lat)), voxels = nrow(loadings(lat)) ) ``` ```{r validate-encoding, include = FALSE} stopifnot( nrow(basis(lat)) == nrow(X), nrow(loadings(lat)) == ncol(X), all(is.finite(as.matrix(lat))) ) ``` The dense reconstruction has the same shape as the input, but it is lossy because eight DCT components cannot represent every 24-point time course. ```{r reconstruction-summary} recon <- as.matrix(lat) data.frame( rows = nrow(recon), cols = ncol(recon), rmse = sqrt(mean((recon - X)^2)) ) ``` ## How do you read one voxel? Use `series()` when you want a time course for one voxel or a small group of voxels without thinking about the full matrix. ```{r series-access} voxel_one <- series(lat, 1L) head(voxel_one) ``` Coordinate access is available when you know the voxel location in the image space. ```{r coordinate-access} same_voxel <- series(lat, 1L, 1L, 1L) all.equal(as.numeric(voxel_one), as.numeric(same_voxel)) ``` ## How do you slice it like an array? A `LatentNeuroVec` behaves like the 4D `(x, y, z, time)` array it stands in for, so the usual `neuroim2` indexing verbs work — each one reconstructs only the slice you ask for. Use `[[` to pull one time point as a 3D volume: ```{r single-volume} vol1 <- lat[[1]] class(vol1) dim(vol1) ``` Use `[` with four indices to extract a spatial-by-time sub-array: ```{r array-slice} block <- lat[1:2, 1:2, 1:2, 1:5] dim(block) ``` You never materialize the full dense array to get a corner of it; the slice is reconstructed on demand from the factors. ## When should handles be used? The default handle mode stores the basis as a lightweight `BasisHandle` reference instead of a dense matrix, and only materializes it the first time you ask for it. Matrix mode materializes up front, which is useful when you plan to reconstruct or access many series repeatedly. To see the difference you have to look at the stored slot directly: `basis()` *materializes* a handle on access, so it always hands back a dense matrix in either mode. The slot is where the two representations diverge. ```{r handle-comparison} lat_handle <- encode(X, spec_time_dct(k = 8), mask = mask_vol, materialize = "handle") data.frame( eager_slot = class(lat@basis)[1], handle_slot = class(lat_handle@basis)[1], same_reconstruction = isTRUE(all.equal(as.matrix(lat), as.matrix(lat_handle))) ) ``` The eager object stores a materialized matrix; the handle object stores a `BasisHandle`. Both reconstruct to the same values — the handle just defers the cost until you call `basis()`, `series()`, or `as.matrix()`. Use `vignette("compression-diagnostics")` when you want to compare error and storage budgets across component counts.