Architecture¶
A tour of how processFNIRS2 is put together — enough to understand the data flow, find your way around the packages, and know where new code belongs. For the public API see API_REFERENCE.md; for the processing methods see PROCESSING_PIPELINE.md.
Two layers¶
processFNIRS2 is organized as two layers with a deliberately simple boundary between them:
flowchart TB
files[("Device files<br/>.nir · .snirf · .oxy3 · Hitachi MES")]
subgraph L1["LAYER 1: Single-subject processing (+pf2)"]
direction TB
imp["pf2.import.*<br/><i>importNIR · importSNIRF · importOxy3</i>"]
proc["processFNIRS2<br/><i>3-stage pipeline</i>"]
epoch["pf2.data.*<br/><i>defineBlocks → extractBlocks → blockAverage</i>"]
qc["pf2.qc.*<br/><i>assess / apply · ChannelCheck</i>"]
imp --> proc
proc --> epoch
proc -.-> qc
end
struct{{"data struct: the interface<br/><b>raw · HbO/HbR · markers · device · info</b>"}}
subgraph L2["LAYER 2: Group analysis (+exploreFNIRS)"]
direction TB
ex["Experiment / GLMExperiment"]
agg["groupby → aggregate"]
ana["+stats LME · contrasts · FDR<br/>+connectivity · +coupling · +hyperscanning"]
viz["plot* · +probe.plot<br/><i>topo · 3D · connectome</i>"]
ex --> agg --> ana
agg --> viz
end
exp["pf2.export.*<br/><i>SNIRF · BIDS · tensor (.h5)</i>"]
files --> imp
proc --> struct
epoch --> struct
struct -->|"cell array of<br/>processed structs"| ex
struct --> exp
classDef contract fill:#eef,stroke:#557,stroke-width:2px;
class struct contract;
The contract between the layers is a plain MATLAB struct (one per recording) — not a class hierarchy or a database. Layer 1 produces it; Layer 2 consumes a cell array of them. This is what makes the whole toolbox scriptable and testable, and lets external tools interoperate by producing/consuming the same struct.
End-to-end call flow¶
A single recording travels through the toolbox like this:
-
Import —
pf2.import.importNIR/importSNIRF/importOxy3/ … reads a device file and returns the data struct:raw,time,fs,fchMask, amarkerstable,info, and adeviceobject (auto-attached viapf2_base.loadDeviceCfg). SNIRF import also folds BIDSevents.tsvinto the marker dictionary. -
Process —
processFNIRS2(data)runs three stages (below) and returns the same struct withHbO,HbR,HbTotal,HbDiff,CBSI, plusunits,DPF_factor, and aprocessingInforecord for reproducibility. -
Epoch (single-subject) —
pf2.data.defineBlocksturns marker codes into block definitions;pf2.data.extractBlockscuts time-locked segments;pf2.data.blockAverage/grandAverageproduce trial-averaged waveforms. -
Group analysis (Layer 2) — a cell array of processed structs becomes an
exploreFNIRS.core.Experiment;groupby+aggregatebuild the group tensors;plot*and the+statsengine produce figures and LME results. -
Export —
pf2.export.asSNIRF/asBIDS/asTensorserialize the struct for sharing or downstream ML.
flowchart LR
imp[import] --> ds[("data<br/>struct")]
ds --> proc[processFNIRS2] --> ps[("processed<br/>struct")]
ps --> db[defineBlocks] --> eb[extractBlocks]
eb --> ba[blockAverage] --> wf["averaged waveform<br/>(single subject)"]
eb -->|segments| exptl["Experiment"]
ps -.->|"cell array of<br/>processed structs"| exptl
ps --> exp["export<br/>SNIRF · BIDS · tensor"]
exptl --> gb[groupby] --> ag[aggregate] --> ps2["plot / stats"]
The data struct is the interface¶
Because every stage reads and writes the same struct, its fields are the most important contract in the codebase. Treat them as stable.
| Field | Meaning |
|---|---|
raw [T×C] |
Raw light intensity (input). |
time [T×1], fs |
Time vector (s) and sampling rate (Hz). |
fchMask [1×C] |
Channel mask (1 = good, 0 = bad). |
markers (table) |
Time, Code, Duration, Amplitude (+ any extra columns you add). Read by name. |
info |
Metadata; info.markerDict (code→label), info.eventTypes (BIDS), subject fields. |
device |
pf2.Device value object — geometry, wavelengths, saturation bounds. |
Aux |
Optional typed auxiliary signals (HR, EKG, accel, …). |
HbO HbR HbTotal HbDiff CBSI [T×C] |
Hemoglobin biomarkers (output). |
units, DPF_factor, processingInfo |
Units, DPF used, and the full processing record. |
Two sub-contracts worth calling out:
- Markers are a table, not a matrix —
data.markers.Code, never column indexing. Extra columns (e.g.RT,Label) survivesetT0,split,extractBlocks, and processing. Helpers:pf2_base.normalizeMarkers,markersToArray,mergeMarkers. - The marker dictionary
info.markerDictgives codes meaning and is the unifying target for source formats (BIDSevents.tsv, COBI logs).defineBlocksandlabelMarkersread it.
The three-stage processing pipeline¶
processFNIRS2 converts raw intensity to filtered hemoglobin in three stages,
implemented in +pf2_base/+fnirs:
| Stage | Engine | Transform |
|---|---|---|
| 1 | processStageRaw2OD |
Raw intensity → optical density (motion correction, filtering, CAR — the configurable raw method chain). |
| 2 | processStageOD2Hb / bvoxy |
Optical density → HbO/HbR/… via the modified Beer-Lambert law, with DPF correction (None / Fixed / age-dependent Calc). |
| 3 | processStageFilterHb |
Hemoglobin → filtered hemoglobin (the configurable oxy method chain). |
Stages 1 and 3 are method chains: ordered lists of step functions (from
functions/) whose arguments are bound by name from the processing context
(x, fs, fTime, fchMask, …). The same chains are also expressible as
first-class RawPipeline / OxyPipeline value objects (see below).
Package map¶
flowchart TB
subgraph PF2["+pf2: user-facing API (Layer 1)"]
direction LR
p_import["+import"]
p_data["+data<br/>(+plot)"]
p_process["+process"]
p_methods["+methods<br/>+raw/+oxy/+seeds"]
p_probe["+probe<br/>+plot/+roi/+project<br/>+forward/+dot"]
p_qc["+qc"]
p_export["+export"]
p_dev["Device"]
end
subgraph BASE["+pf2_base: infrastructure & algorithms"]
direction LR
b_ctx["ProcessingContext"]
b_pipe["Pipeline ·<br/>Raw/Oxy/<br/>PipelineFunction"]
b_fnirs["+fnirs<br/>stage engines<br/>bvoxy · GLM"]
b_sig["+signal<br/>+wavelet<br/>+accel"]
b_io["+bids · +dot<br/>+plot · +external"]
b_init["pf2_initialize<br/>loadDeviceCfg<br/>normalize*"]
end
subgraph EX["+exploreFNIRS: group analysis (Layer 2)"]
direction LR
e_core["+core<br/>Experiment ·<br/>GLMExperiment"]
e_conn["+connectivity<br/>+coupling<br/>+hyperscanning"]
e_stats["+stats · LME<br/>contrasts · FDR<br/>+graph"]
e_rep["+report · +dataset<br/>+export · +fx"]
end
FUNS["functions/: step implementations<br/>TDDR · SMAR · wavelet · Butterworth · Takizawa · CAR · SSR · GSR"]
DEVS[("devices/*.cfg")]
%% invisible chains keep each package's nodes on one row
p_import ~~~ p_data ~~~ p_process ~~~ p_methods ~~~ p_probe ~~~ p_qc ~~~ p_export ~~~ p_dev
b_ctx ~~~ b_pipe ~~~ b_fnirs ~~~ b_sig ~~~ b_io ~~~ b_init
e_core ~~~ e_conn ~~~ e_stats ~~~ e_rep
PF2 ==>|builds on| BASE
BASE ==> FUNS
PF2 ==> DEVS
PF2 -.->|processed structs| EX
FUNS ~~~ EX
+pf2/ — user-facing API (Layer 1)¶
| Subpackage | Responsibility |
|---|---|
+import |
Device readers (importNIR, importSNIRF, importOxy3, …), importDirectory, fromTable, sampleData. |
+data |
Struct manipulation (setT0, resample, split), epoching (defineBlocks, extractBlocks, blockAverage), markers, metadata; +plot for time series. |
+process |
Stage-level entry points (processRaw, processOxy). |
+methods |
Method registry — +raw, +oxy, +seeds (list/set/create/edit). |
+probe |
Anatomy & spatial viz — +plot (topo, 3D, movies, connectome), +roi, +project, +forward & +dot (diffuse optical tomography), canonicalize, montage. |
+qc |
Quality control — pipeline.assess/apply, snapshot, ChannelCheck GUI. |
+export |
asNIR, asSNIRF, asBIDS, asTensor, export. |
+settings, +GUI |
Processing settings and GUI glue. Device.m (top level) is the device value class. |
+pf2_base/ — advanced and internal infrastructure & algorithms¶
This package serves two roles: documented low-level interfaces for advanced
users, and implementation machinery used by the primary pf2 workflow. Those
roles are distinguished by the API catalog and documentation tier; package
membership alone is not a support promise.
Top-level: ProcessingContext, the pipeline classes (Pipeline,
RawPipeline, OxyPipeline, PipelineFunction), pf2_initialize,
loadDeviceCfg, normalizeMarkers/normalizeAux, hierarchicalAverage.
Subpackages include +fnirs (the stage engines, bvoxy, GLM), +dot, +bids,
+accel, +signal, +wavelet (first-party transforms), +plot, +external
(vendored helpers), and +tests.
+exploreFNIRS/ — group analysis (Layer 2)¶
+core holds the scriptable Experiment and GLMExperiment classes plus their
plotting methods. Analysis subpackages: +connectivity, +coupling,
+hyperscanning, +stats (LME, contrasts, FDR), +graph, +report,
+dataset, +export, +fx.
Supporting directories¶
functions/ — flat signal-processing step implementations (TDDR, SMAR,
wavelet, Butterworth, Takizawa, …), dispatched by name from the method chains.
devices/ — device .cfg files. sampledata/ — bundled datasets.
examples/scripts/ — runnable tutorials.
Entry points & legacy zones¶
processFNIRS2.m— the processing engine (handles cell arrays,parfor, and theContextbypass).pf2.m— convenience wrapper that self-heals the path.exploreFNIRS.m— the GUIDE-based group-analysis GUI.base_functions/,GUI/,compat_shims/— legacy / compatibility code outside the package structure. Kept working, but new code should not be added here.
Analysis approaches¶
Once a recording is processed, the choice of analysis approach depends on the
experimental design. They share the same processed struct and converge on the
Layer-2 Experiment for group statistics.
flowchart TB
proc["Processed recording<br/>HbO/HbR · markers"]
proc --> q{"Analysis goal?"}
q -->|"Event-related amplitude<br/>(clean, spaced trials)"| EPOCH
q -->|"Continuous / overlapping<br/>or irregular events"| GLM
q -->|"Resting / dynamic FC<br/>(no events)"| SLIDE
q -->|"Functional connectivity"| CONN
subgraph EPOCH["Epoch / block-averaging"]
direction TB
e1["defineBlocks(code, dur)"] --> e2["extractBlocks<br/>(PreTime / PostTime)"]
end
subgraph GLM["GLM (continuous)"]
direction TB
g1["buildDesignMatrix<br/>HRF ⊛ boxcar + drift"] --> g2["fitGLM (OLS / AR-IRLS)"] --> g3["betas /<br/>first-level contrasts"]
end
subgraph SLIDE["Sliding windows"]
direction TB
s1["slidingWindows<br/>(Length, Overlap)"] --> s2["extractBlocks"]
end
subgraph CONN["Connectivity"]
direction TB
c1["computeMatrix<br/>pearson · partial · ..."]
end
e2 -->|segments| GRP
s2 -->|segments| GRP
g3 -->|"betas (betasToSegments)"| GRP
GRP["exploreFNIRS.core.Experiment<br/>groupby → aggregate<br/>· group-level averaging ·"] --> OUT["Group stats (LME / FDR)<br/>plots · export"]
CONN --> COUT["connectome · plotChord<br/>group connectivity"]
GLM pipeline¶
GLMExperiment automates the manual chain below (process → design → fit →
contrasts → package → group). Reach for the manual path when you need control
over the design matrix or first-level contrasts.
flowchart TB
rec["Continuous recordings<br/>(raw subjects)"]
blk["block definitions<br/>(markers → conditions)"]
rec --> proc["processFNIRS2<br/><i>GLM convention: skip bandpass:<br/>drift regressors model trends</i>"]
blk --> ev["blocksToEvents"]
proc --> dm
ev --> dm["buildDesignMatrix<br/>HRF ⊛ boxcar · drift (Legendre/DCT)<br/>± derivative/dispersion · short-channels"]
dm --> fit["fitGLM <i>per subject × biomarker</i><br/>OLS (default) / AR-IRLS · betas · t / p · R²"]
fit --> con["First-level contrasts<br/>C·β (e.g. Hard > Easy)"]
con --> pack["betasToSegments<br/><i>β as pseudo-segments</i>"]
pack --> grp["Experiment (group)<br/>aggregate"]
grp --> lme["statsFitLME · plotLME · plotTopoLME"]
grp --> tbl["betaTable <i>(GLMExperiment)</i><br/>R / Python export"]
subgraph AUTO["GLMExperiment: wraps the whole chain"]
direction TB
a1["GLMExperiment(subjects, blockDefs) → fit() → plot/stats"]
a2["betaSeriesConnectivity · ppi / ppiTable / ppiLME"]
end
Hyperscanning (inter-brain synchrony)¶
Paired (or grouped) recordings are coupled channel-by-channel, then tested for inter-brain synchrony and modeled at the group level.
flowchart TB
A["Subject A<br/>processed"]
B["Subject B<br/>processed"]
A --> pair["pairSubjects<br/><i>align dyads / groups</i>"]
B --> pair
pair --> dyad["computeDyad<br/><i>ChannelPairing: same / all</i>"]
pair --> group["computeGroup<br/><i>n-way coupling</i>"]
subgraph COUP["+coupling: per-pair metric"]
direction LR
m1["wcoherence ·<br/>coherence"]
m2["pearson · spearman ·<br/>xcorr"]
m3["granger · transferEntropy ·<br/>mutualInfo"]
m4["partialCoherence /<br/>partialCorr<br/><i>control shared physio</i>"]
end
dyad --> COUP
group --> COUP
pair --> qc["physioConfoundQC<br/><i>LFO/VLFO shared-aux flag</i>"]
COUP --> perm["permutationTest<br/><i>vs pseudo-pairs</i>"]
perm --> viz["plotDualBrain · plotDyadMatrix<br/>plotInterBrainTopo · plotGroup"]
perm --> stats
pair --> xppi["cross-brain PPI (gPPI)<br/><i>computePPI · seed = other brain</i>"]
xppi --> stats["Group LME<br/><i>Experiment · GLMExperiment.ppi /<br/>ppiTable / ppiLME</i>"]
A2["HB-ICA<br/><i>hbica · shared inter-brain components</i>"]
pair --> A2 --> hviz["plotHBICA"]
Key abstractions¶
pf2.Device— an immutable value object describing a probe (geometry, wavelengths, MNI positions, saturation bounds), loaded from a.cfgand attached asdata.device.- Method / Pipeline system — a method is a named, ordered chain of step
functions.
RawPipeline/OxyPipelineexpose this as value objects (every mutating call returns a copy);.toMethod()/.save()convert to the registry format,.fromMethod()reloads. ProcessingContext— bypasses thePF2/setFglobals so settings, methods, and device are threaded as locals. This is what makes processing isolated, reproducible, andparfor-safe (processFNIRS2(data, 'Context', ctx)).Experiment/GLMExperiment— the Layer-2 group objects: ingest processed structs,groupby/aggregateinto group tensors, and exposeplot*and statistics.GLMExperimentwraps processing + GLM + group analysis.
Where does X go?¶
| You want to add… | Put it here |
|---|---|
| A processing algorithm / step | functions/ (a plain function bound by name), then register it in a method chain or add it to a RawPipeline/OxyPipeline. |
| A device | A .cfg in devices/ (or generate one with pf2.probe.saveCfg). |
| An importer / exporter | +pf2/+import / +pf2/+export. |
| A plot or spatial visualization | +pf2/+probe/+plot (or +project for cortical projections). |
| A QC check | +pf2/+qc (wire it into pipeline.assess). |
| Group-level analysis or statistics | +exploreFNIRS/+core (Experiment methods) or the relevant analysis subpackage (+connectivity, +stats, +graph, …). |
| Internal infrastructure / shared utility | +pf2_base (the right subpackage). |
| Tests | +pf2_base/+tests. |
See CONTRIBUTING.md for setup, tests, and coding conventions.