Repository navigation
Auxiliary Signals
fNIRS recordings often come with companion physiological signals — heart
rate, ECG/PPG waveforms, accelerometer/IMU, skin conductance, EEG,
respiration, temperature. processFNIRS2 treats these as first-class typed
Aux signals: recognized by family, alignable to the fNIRS time grid, and
usable as nuisance regressors, covariates, motion gates, and event-related
overlays. This page is for anyone with a data.Aux container who wants to
align, extract features from, or model with those signals.
Every named signal in data.Aux (e.g. data.Aux.heartRate,
data.Aux.accelerometer) is normalized to a canonical struct with these
fields:
sig.data % [T x C] numeric
sig.time % [T x 1] seconds
sig.unit % e.g. 'bpm', 'g', 'uS', 'uV'
sig.varNames % {1 x C} channel labels, e.g. {'X','Y','Z'}
sig.type % inferred family: 'HR'|'EKG'|'PPG'|'ACCEL'|'GSR'|'EEG'|'RESP'|'TEMP'|''
sig.kind % 'feature' | 'waveform' | ''| Family | Meaning | Typical role |
|---|---|---|
HR |
Heart rate (bpm) | Covariate of arousal (derived feature) |
EKG |
Electrocardiogram waveform | Cardiac nuisance regressor + HR source |
PPG |
Photoplethysmography waveform | Cardiac nuisance regressor + HR source |
ACCEL |
Accelerometer / IMU | Motion quality gate + nuisance regressor |
GSR / EDA
|
Electrodermal activity | Covariate of arousal |
EEG |
Electroencephalography | Covariate / fusion modality, own per-band typing |
RESP |
Respiration (belt / RIP) | Low-frequency nuisance conditioning signal |
TEMP |
Skin/ambient temperature | Covariate |
% Coerce a whole Aux container (any mix of struct/table/numeric signals)
% into the canonical form above
data.Aux = pf2_base.normalizeAux(data.Aux);
% Resolve a signal name (and optional unit) to its type descriptor
info = pf2_base.auxSignalType('heartRate'); % info.type == 'HR'
info = pf2_base.auxSignalType('chan1', 'uS'); % info.type == 'GSR' (resolved by unit)
% info also carries: .kind, .defaultUnit, .role, .roles, .band, .bands (EEG), .matchedBynormalizeAux is idempotent (re-normalizing a canonical container is a
no-op) and recognizes common field-name synonyms (values/signal/y →
data; t/timestamps → time; units → unit; names/labels/
channels → varNames). Type is inferred from the signal's field name and
falls back to its unit; unrecognized signals get type = '' and still flow
through the Aux machinery normally.
pf2.data.auxOnGrid is the single alignment primitive everything else in
this page builds on — it resamples a named Aux signal (nested or flattened
form) onto an arbitrary target time base, handling anti-aliasing, clock
offset, and NaN gaps.
hr = pf2.data.auxOnGrid(proc, 'heartRate'); % onto proc.time
acc = pf2.data.auxOnGrid(proc, 'accelerometer', 'Channels', {'X','Y'});
eda = pf2.data.auxOnGrid(proc, 'gsr', 'Offset', 0.25, 'MaxGap', 2); % device clock skew + gap handlingKey parameters: 'Time' (target grid, default data.time), 'Channels'
(subset by index or name), 'Offset' (seconds, corrects device clock skew),
'AntiAlias' (default true, ignored when upsampling), 'MaxGap' (seconds —
target samples inside a source gap wider than this come back NaN rather
than being interpolated across).
These turn a raw waveform into a derived feature series (or scalar summary). All are self-contained (no Signal Processing Toolbox dependency).
% PPG/EKG waveform -> instantaneous heart rate (bpm)
[hr, info] = pf2.data.aux.heartRateFrom(ppg, fs);
% 'Band' (default [0.5 5]), 'MinBPM' (30), 'MaxBPM' (220)
% GSR/EDA -> tonic (SCL) / phasic (SCR) decomposition
[scl, scr, info] = pf2.data.aux.edaDecompose(gsr, fs);
% 'TonicCutoff' (default 0.05 Hz); reference: Boucsein (2012), DOI: 10.1007/978-1-4614-1126-0
% Accelerometer/IMU -> motion features
[feat, info] = pf2.data.aux.accelFeatures(acc, fs);
% feat.norm (gravity-removed vector magnitude), feat.jerk (its derivative)
% EEG -> canonical band-power envelopes (delta..gamma)
bp = pf2.data.aux.eegBandPower(eeg, fs);
% bp.delta, bp.theta, bp.alpha, bp.beta, bp.gamma — each [T x C]
% Respiration -> rate (breaths/min) and RVT nuisance regressor
rf = pf2.data.aux.respFeatures(resp, fs);
% rf.rate, rf.rvt; reference: Birn et al. (2006), DOI: 10.1016/j.neuroimage.2006.02.048
% PPG/EKG (or beat times, or NN intervals) -> scalar HRV metrics
hrv = pf2.data.aux.hrvFeatures(ppg, fs);
% hrv.meanHR, .SDNN, .RMSSD, .pNN50, .LF, .HF, .LFHF
% 'Input': 'waveform' (default) | 'beats' | 'ibi'
% reference: Task Force ESC/NASPE (1996), DOI: 10.1161/01.CIR.93.5.1043[hr, ~] = pf2.data.aux.heartRateFrom(proc.Aux.ppg.data, proc.Aux.ppg.fs);
proc = pf2.data.aux.addFeature(proc, 'heartRate', hr, 'Unit', 'bpm');
% -> proc.Aux.heartRate as a canonical {data,time,unit,varNames,type,kind} signal,
% typed HR from the field name, and it survives SNIRF export/re-import.An independent motion reference disambiguates true head movement from neural transients, rather than inferring motion from the optical signal itself.
% Flag motion samples/windows from an accelerometer aux signal
[mask, mi] = pf2_base.fnirs.accelMotionDetect(proc);
% 'Metric': 'norm' (default) | 'jerk'
% 'Threshold' (default [] -> adaptive median + MADScale*MAD)
% mi.windows [W x 2] start/end times (s), mi.fractionFlagged
% Regress accelerometer-explained variance out of HbO/HbR (mean preserved)
proc = pf2_base.fnirs.accelRegress(proc);
% 'IncludeAxes'/'IncludeNorm'/'IncludeJerk' (all default true)
% Reference: Metz et al. (2015), DOI: 10.3390/a8041052
% Drop epochs that overlap accelerometer-flagged motion during block extraction
seg = pf2.data.extractBlocks(proc, blocks, 'PreTime', 5, 'PostTime', 15, ...
'RejectByAux', 'accelerometer');
% 'RejectByAux': signal name, or true to auto-detect an ACCEL-type signal
% 'AuxMotionFraction' (default 0.2): max tolerated flagged-sample fraction
% 'AuxMotionThresh' (default [] -> adaptive)When both HbO and HbR are corrected, accelRegress recomputes the derived
HbTotal/HbDiff/CBSI fields so they stay consistent, and records a
.accelRegressInfo field on the output.
pf2_base.fnirs.physioRegress removes cardiac oscillations using the
measured heart rate (from an HR feature signal, or derived from PPG/EKG)
rather than a fixed notch band — useful when heart rate drifts or is
elevated (e.g. aroused participants).
proc = pf2_base.fnirs.physioRegress(proc, 'Harmonics', 2);
% 'Signal': auto-detects HR, then PPG, then EKG if unspecified
% 'Biomarkers' (default {'HbO','HbR'})
% Reference: Glover, Li & Ress (2000), DOI: 10.1002/1522-2594(200007)44:1<162::AID-MRM23>3.0.CO;2-EBuilds sine/cosine regressors at the instantaneous cardiac phase and its harmonics, then removes them by least squares (channel means preserved). If no cardiac-type signal is present, the input is returned unchanged with a warning (graceful no-op).
% GLMExperiment: declare aux signals as nuisance regressors in the design
gx = exploreFNIRS.core.GLMExperiment(allData);
gx.glm.auxNuisance = {'heartRate'};
% Group LME: promote an aux_* signal to a model covariate
ex.statsAutoLME('AuxCovariates', {'heartRate'});See Group Analysis for the full GLMExperiment/LME
workflow these plug into.
exploreFNIRS.core.Experiment.plotTemporal can overlay an aux signal
(right-hand y-axis), time-locked to the same epoch grid as the biomarker
trace:
ex.plotTemporal('Biomarkers', {'HbO'}, 'AuxOverlay', {'heartRate'});Shared physiology (e.g. cardiac/respiratory coupling) can masquerade as inter-brain synchrony; the Aux model plugs into hyperscanning coupling and QC to control for it — see Group Analysis for the full hyperscanning workflow:
r = exploreFNIRS.coupling.partialCoherence(A_ch, B_ch, sharedAux, fs); % control shared physio
qc = exploreFNIRS.hyperscanning.physioConfoundQC(A, B); % LFO/VLFO shared-aux flagAligned (and derived) aux signals can be written into the self-describing HDF5 tensor export alongside HbO/HbR:
pf2.export.asTensor(proc, 'out.h5', 'Features', {'HbO','HbR'}, 'Aux', 'all');
% 'Aux': cellstr of aux base names, or true/'all' for every present signal
% -> written to /aux, aligned to /time, with auxNames/auxUnits attributesSee Export and Interoperability for the full tensor/BIDS/SNIRF export contract.
data.Aux.heartRate.data % [T x 1]
data.Aux.heartRate.time % [T x 1]
data.Aux.heartRate.unit % 'bpm'
data.Aux.heartRate.varNames % {'HR'}
data.Aux.accelerometer.data % [T x 3]
data.Aux.accelerometer.time % [T x 1]
data.Aux.accelerometer.unit % 'g'
data.Aux.accelerometer.varNames % {'X','Y','Z'}varNames propagates through pf2.data.split/pf2.data.resample as table
column names and through grand-averaging to plot subplot titles.
processFNIRS2
Getting Started
Core Workflow
Group Analysis
Visualization & Export
Reference