Repository navigation
Block Averaging and Epoching
This page covers the single-subject recipe for turning a continuous processed recording into event-related epochs and a trial/grand-averaged waveform — marker lookup, block definition, epoch extraction, and averaging — plus marker-free sliding-window epoching for resting-state and connectivity work. It is for anyone going from a processed recording to a per-condition response curve.
data = pf2.import.sampleData(); % recording WITH markers (fNIR1200, marker code 50)
proc = processFNIRS2(data); % -> HbO/HbR/...
% 1. Define blocks from a marker code (Embed=false returns the block ARRAY;
% the default Embed=true returns the data struct with .blocks embedded)
blocks = pf2.data.defineBlocks(proc, 50, 15, 'Embed', false); % code 50, 15 s blocks
% 2. Cut time-locked epochs
segments = pf2.data.extractBlocks(proc, blocks, ...
'PreTime', 5, 'PostTime', 15, 'SetT0', true); % onset at t = 0
% 3. Trial/grand average onto a common grid (one call)
ga = pf2.data.blockAverage(segments); % or pf2.data.grandAverage (alias)
plot(ga.time, ga.HbO.Mean(:, 1)); % averaged HbO, channel 1pf2.import.sampleData() (fNIR1200, marker code 50) and pf2.import.sampleData.fNIR2000() (synthesized TaskA/TaskB blocks, codes 1/2) both carry markers, so either can be used to run this recipe with no external files.
pf2.data.getMarkers searches data.markers (or a marker table/matrix directly) for codes or code sequences, and is a useful way to inspect a recording before committing to a block definition:
times = pf2.data.getMarkers(proc, 50); % all onset times of code 50
times = pf2.data.getMarkers(proc, [50; 51]); % column vector = code 50 OR 51 (any match)
times = pf2.data.getMarkers(proc, 50, 51); % paired start/end onsets
times = pf2.data.getMarkers(proc, {25, [50 51]}); % pattern: code 25 AND sequence 50 then 51
idx = pf2.data.getMarkers(proc, 50, 'returnIndices', true); % row indices instead of timesA row vector like [50, 51] means a sequential pattern (50 followed by 51); a column vector like [50; 51] means "either code" (OR logic).
pf2.data.defineBlocks turns marker codes into a block definition — one struct per epoch, with startTime, endTime, duration, markerCode, markerIndex, and an .info struct for condition labels and per-trial metadata.
% Fixed duration from a marker code
blocks = pf2.data.defineBlocks(data, [49, 50], 30, 'Embed', false);
% Auto-detect duration from the marker's own .Duration column (omit duration)
blocks = pf2.data.defineBlocks(data, 49, 'Embed', false);
% Marker + end marker (duration derived from the terminating marker)
blocks = pf2.data.defineBlocks(data, [49, 50], 'EndMarker', 51, 'Embed', false);
% Start/end pairs with condition labels
blocks = pf2.data.defineBlocks(data, ...
'StartMarker', [50; 51], 'EndMarker', [52; 53], ...
'ConditionMap', {50, 'Natural'; 51, 'Synthetic'}, 'Embed', false);'Embed' controls the return value: false returns just the block struct array; the default true returns the data struct with the blocks stored on data.blocks, so a subsequent extractBlocks(data) call needs no second argument. Both forms accept a cell array of subjects (with 'Embed', true) to stamp blocks onto every element in one call.
When data was imported from SNIRF with a companion BIDS events.tsv, ConditionMap is auto-populated from data.info.eventTypes — no manual mapping is required:
data = pf2.import.importSNIRF('sub-01_nirs.snirf'); % reads events.tsv
blocks = pf2.data.defineBlocks(data, [1, 2, 3]); % auto-labeled conditionspf2.data.extractBlocks cuts each block (with padding) out of the continuous recording and returns a cell array of fNIRS structs — one per epoch, ready for blockAverage or exploreFNIRS.core.Experiment.
segments = pf2.data.extractBlocks(proc, blocks, ...
'PreTime', 5, 'PostTime', 15, 'BaselineWindow', [-5, 0], 'SetT0', true);Always set PreTime/PostTime explicitly. If neither PreTime, PostTime, nor Buffer is given, each side falls back to a small default Buffer of 2 s (a one-time pf2:extractBlocks:defaultBuffer note is emitted the first time this default path is taken in a session) — set them deliberately to size the epoch you actually want. 'Buffer' applies the same padding symmetrically to both sides when the corresponding PreTime/PostTime is not given.
Other useful options:
| Parameter | Purpose |
|---|---|
'SetT0' |
Shift time so block start = 0 (default true) |
'BaselineWindow' |
[start, end] relative to block start for baseline subtraction, e.g. [-5, 0]
|
'OverwriteInfo' |
Allow block.info to overwrite parent data.info fields of the same name (default true) |
'SkipInvalid' |
Skip blocks that fall outside the recording's time range (default true) |
'RejectByAux' |
Drop epochs overlapping accelerometer-flagged motion — pass an Aux signal name, or true to auto-detect an ACCEL-type signal |
If you accidentally pass an Embed=true data struct (rather than a block array) as the second argument, extractBlocks detects it and uses its .blocks field.
% Using embedded blocks — no second argument needed
data = pf2.data.defineBlocks(data, [49, 50], 30, 'Embed', true);
segments = pf2.data.extractBlocks(data, 'PreTime', 5, 'PostTime', 15);
% Cell array of subjects, each with blocks already embedded
segments = pf2.data.extractBlocks(allData, 'PreTime', 5, 'PostTime', 15);Marker-locked segments share a sampling rate but generally start at different sub-sample phases, so their time vectors do not line up sample-for-sample — averaging them directly would yield an almost entirely NaN result. pf2.data.blockAverage (aliased as pf2.data.grandAverage) first resamples every segment onto one shared time grid anchored at t = 0, then averages:
ga = pf2.data.blockAverage(segments);
% or, identically:
ga = pf2.data.grandAverage(segments);The output ga gives, for each biomarker HbO, HbR, HbTotal, HbDiff, CBSI:
ga.HbO.Mean % [T x C] mean across segments
ga.HbO.SEM % [T x C] standard error of the mean
ga.HbO.SD % [T x C] standard deviation
ga.HbO.N % [T x C] count of contributing segments (per timepoint/channel)
ga.HbO.Median % [T x C]
ga.HbO.Max % [T x C]
ga.HbO.Min % [T x C]
ga.HbO.data % [T x C x N] per-segment aligned data
ga.time % [T x 1] shared time grid
ga.units % biomarker units'ResampleInterval' overrides the shared grid's sample interval (default: the median of the segments' native intervals). 'AverageAux' also averages any auxiliary signals present on the segments.
For dynamic functional connectivity, resting-state analysis, windowed quality control, or fixed-length model input, pf2.data.slidingWindows tiles the whole recording on a regular grid instead of locking to markers. It produces defineBlocks-compatible blocks that feed straight into extractBlocks:
blocks = pf2.data.slidingWindows(proc, 'Length', 10, 'Overlap', 0.5, 'Embed', false);
windows = pf2.data.extractBlocks(proc, blocks, 'PreTime', 0, 'PostTime', 0);Set PreTime/PostTime to 0 so each extracted segment is exactly the window (otherwise extractBlocks applies its own default padding). Key options:
| Parameter | Purpose |
|---|---|
'Length' |
Window length in seconds (required) |
'Step' |
Step between window starts in seconds (default: Length, i.e. contiguous, non-overlapping); mutually exclusive with 'Overlap'
|
'Overlap' |
Fractional overlap in [0, 1); sets Step = Length*(1-Overlap)
|
'Start' / 'End'
|
Bound the tiled span in seconds (default: the full recording) |
'Partial' |
Keep a trailing short window when the grid doesn't divide evenly (default false) |
'Condition' |
Label stored in each window's .info.Condition (or 'ConditionField') |
'Embed' |
Store blocks on data.blocks and return the data struct (default true) |
% Contiguous 30 s windows, embedded directly on the struct
proc = pf2.data.slidingWindows(proc, 'Length', 30);
windows = pf2.data.extractBlocks(proc, 'PreTime', 0, 'PostTime', 0);fNIRS hemodynamic/neural fluctuations of interest sit near 0.01–0.04 Hz, which implies windows of roughly 30–100 s (Length >= 1/f_low); a much shorter window mostly captures Mayer-wave/systemic activity rather than neural co-fluctuation. Use pf2.data.defineBlocks instead for event-related/task designs.
blockAverage/grandAverage are the single-subject, flat-average endpoint. For multi-condition comparisons, between-group statistics, or averaging across multiple subjects, feed the extracted segments to exploreFNIRS.core.Experiment instead:
ex = exploreFNIRS.core.Experiment(segments);
ex.groupby({'Condition'});
ex.aggregate(); % default 'hierarchy' mode avoids pseudoreplication
ex.plotTemporal('Biomarkers', {'HbO'});See Group Analysis for groupby/aggregate semantics (including the 'hierarchy' vs 'flat' vs 'none' averaging modes) and group-level plotting and statistics.
processFNIRS2
Getting Started
Core Workflow
Group Analysis
Visualization & Export
Reference