Repository navigation
NIRx import, SNIRF compliance and gaps, and the .nir counter time base (v1.2.2) - #12
Merged
Merged
Conversation
importNIRX takes positions, the 2D layout, optode labels and the head model from <root>_probeInfo.mat (the source of NIRx's own SNIRF), and landmarks from digpts.txt. t0 comes from the first .roh frame, read from inside the .zip. A 'Model' option names the device when the .hdr only has a serial Device ID. NIRStar recordings without a .nirs file import from the .wl files, the .hdr S-D-Key/S-D-Mask and the .evt triggers instead of erroring. .hdr Events no longer read the frame number as the duration. asSNIRF writes optode labels, landmarks and the coordinate system, and writes MeasurementDate/MeasurementTime in UTC with the zone in MeasurementTimeZone, since MNE drops the UTC offset. importSNIRF converts UnixTime to the recording's zone instead of relabelling the UTC clock time, parses a MeasurementTime offset, keeps a time with no zone zone-less, and reads fixed-width landmark labels, which made NIRx's own SNIRF files fail to import. importNIR keeps the marker file index in a MarkerSource column, only when markers come from more than one .mrk file, instead of in Amplitude, and documents the .mrk columns as read.
Device setup warned "Time column could not be found" for every recording from a device whose raw data has no time column, such as NIRx. Both copies of the warning now fire once per session and carry the pf2:updateCurrentDevice:noTimeColumn identifier.
…ing metadata asSNIRF writes the measurementList index and type fields as int32 (doubles fail the SNIRF validator) and no longer writes dataUnit 'au', which MNE 1.13 rejects, or the non-standard dataTypeLabel 'raw-DC'. Non-BIDS coordinate systems such as MNI go to the CoordinateSystem metadata tags instead of probe/coordinateSystem 'Other', which drew a validator warning. .info fields that identify or describe a person (names, date of birth, age, sex, experimenter, contact details, notes) are written only with the new IncludeIdentifying option, and path values are reduced to the file name; the .nir filename included the full local path. importSNIRF reads integer measurementList fields as doubles, finds raw channels by dataType when de-normalizing, and restores the coordinate system from the metadata tags.
importNIR read the COBI Studio Modern sample counter as 1 kHz. On the 2000M it ticks at 32768/33 Hz, so time ran 0.70% fast and fs came out 5.025 Hz, and segments between counter resets were stitched with a PC time difference that includes the delivery delay before each dropout. pf2_base.nirCounterTimebase splits the recording at counter resets, PC clock restarts, sustained PC clock steps and counter pauses at dropouts. It fits the counter tick per segment as the lower envelope of PC time against the counter (Moon, Skelly & Towsley, 1999), anchors each segment to its PC latency floor, carries time across PC restarts with the counter, and sets fs from the tick and the mean counts per sample. Segment, gap and latency QC goes to info.timebase, and device setup in processFNIRS2 keeps that fs instead of 1/median(diff(time)). Short 2000M, 2000S and 3000C recordings are recognised by their counter step; 1200W files stay on the PC time column.
…lection Time base: 1 ms counters (2000S, 3000C) keep exactly 1 ms when the fit is within 50 ppm, and the first piece of each counter segment is anchored at its first sample with the earlier arithmetic, so their times and rates are unchanged without counter resets. fs is the median step except on the 2000M. Step and pause detection uses a provisional fitted tick, a counter pause needs a level raised floor over 30 s, and short split pieces are merged back without steering the neighbouring fit or anchor. Short recordings check their fit against the default, runs of rows without a counter stay together, and fewer than two rows return no rate. Frames, offsets and t0Shift are returned. importNIR moves .mrk markers and t0 into the final PC frame, placing each marker file in a consistent sequence of frames and warning on ambiguous markers. Counter detection moves to pf2_base.isNirCounterColumn (zero-padded baselines, early resets, short counters, 1200W excluded), samples at PC time 0 or below are kept, PC-timed files get a regularity check (pf2_base.nirTimingCheck), and per-sample QC arrays stay out of info. Device setup keeps the counter fs only while the time vector matches (pf2_base.timebaseMatches). asNIR does not write MarkerSource. asSNIRF strips dark channels by default, writes time from 0 at the first sample with TimeOrigin, marks every gap with a BAD_gap stim and fills only gaps of up to MaxFillSamples (default 1) whole missing samples, writes SamplingRate at full precision, labels gap stims, maps optode labels on per-optode devices, describes BIDS coordinate systems and omits more identifying fields. importSNIRF restores the time origin for samples, markers, aux and t0, drops filled samples, takes fs from SamplingRate and RawMax from its tag, and keeps labels, landmarks and the coordinate system for re-export. asBIDS writes events.tsv onsets from the first sample. importNIRX reads only the files of the selected recording, keeps .hdr Events codes over the .nirs stim matrix, accepts 0-based S-D-Key indices and defaults Aurora wavelengths to 760/850 nm, and handles dotted file names, space-separated wavelengths, string option names and Octave line splitting. Release date moves to 2026-11-04.
asSNIRF 'Target','mne' (also through batch export and asBIDS) prepares files for MNE-Python: dark channels are stripped, every gap is filled unless MaxFillSamples is given, landmarks are left out (MNE 1.13 reads landmarkPos3D 1000 times too large), and irregular sample steps are warned about. The default target, 'snirf', writes the data as recorded. importNIR moves manual <name>_C.mrk markers by the first PC frame's offset after PC clock restarts: they are timed by the PC QPC timer from the session start, which the restarts do not reset.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved defects can import the wrong recording, expose identifying metadata despite opt-out, and produce incorrect timestamps and sampling rates.
8 open issues
Recording prefix match includes neighboring recordings · New Invalid counters produce an incorrect sampling rate · New MaxFillSamples=Inf does not bypass whole-step restriction · New Imported sampling rate is lost during device setup · New Column-oriented HDF5 optode labels are discarded · New Aux signals without explicit time receive origin twice · New BIDS origin ignores mismatched time-vector length · New Stale counter metadata overrides resampled sampling rate · New
What changed in this PR
This v1.2.2 update improves recording import, timing accuracy, and SNIRF interoperability across processFNIRS2.
Changes:
- Adds recording-specific NIRx loading, vendor geometry, and NIRStar wavelength-file support.
- Updates SNIRF compliance, metadata privacy, gap handling, and time-origin round trips.
- Introduces counter-based COBI
.nirtiming with diagnostic metadata and regression tests.
| File | Description |
|---|---|
| README.md | Updates the version badge and citation. |
| docs/README.md | Updates the release citation. |
| docs/CITING.md | Updates APA and BibTeX versions. |
| CITATION.cff | Updates release metadata. |
| CHANGELOG.md | Documents v1.2.2 changes. |
| +pf2/+import/importSNIRF.m | Restores timing, sampling rate, labels, and geometry metadata. |
| +pf2/+import/importNIRX.m | Adds vendor geometry, recording selection, and wavelength-file imports. |
| +pf2/+import/importNIR.m | Integrates counter timing and marker-frame alignment. |
| +pf2/+export/asSNIRF.m | Updates compliance, privacy, timestamps, and gap handling. |
| +pf2/+export/asNIR.m | Excludes marker-source bookkeeping from exported markers. |
| +pf2_base/timebaseMatches.m | Checks whether stored timing metadata still applies. |
| +pf2_base/pf2version.m | Updates the runtime version and release date. |
| +pf2_base/pf2_updateCurrentDevice.m | Preserves matching counter rates and limits repeated warnings. |
| +pf2_base/nirTimingCheck.m | Reports irregular PC-based sampling. |
| +pf2_base/nirCounterTimebase.m | Fits counter timing and handles clock discontinuities. |
| +pf2_base/isNirCounterColumn.m | Detects supported device counter columns. |
| +pf2_base/+tests/+unit/NirCounterTimebaseTest.m | Tests counter timing, resets, markers, and rate preservation. |
| +pf2_base/+tests/+unit/ExportTest.m | Expands SNIRF and NIRx regression coverage. |
| +pf2_base/+tests/+integration/RoundtripTest.m | Supplies explicit time zones for export checks. |
| +pf2_base/+gui/updateCurrentDevice.m | Preserves matching counter rates in shared device setup. |
| +pf2_base/+bids/writeEventsTsv.m | Makes event onsets relative to the first sample. |
Review effort: Balanced
- importNIRX reads only files named with the selected recording's exact root and a NIRx file suffix, so rec_2.* no longer leaks into rec. - A recording without a usable counter keeps its PC times and their rate. - timebaseMatches compares sample count, span and median step, so a same-length resample onto a regular grid no longer revives a stale rate; setT0 still keeps it. - importSNIRF records the SamplingRate tag with its time vector, so the imported rate survives processFNIRS2 until the data are resampled or cut. - importSNIRF reads fixed-width optode labels stored one per column, and shifts only aux signals with their own time by the time origin. - asSNIRF with MaxFillSamples Inf fills every gap, including steps that are not a whole number of sample periods. - writeEventsTsv uses origin 0 when the time vector does not match the data, as asSNIRF does.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Follow-ups to the v1.2.1 NIRx and SNIRF fixes, released as v1.2.2. NIRx imports use the vendor coordinate set and start time and read only the selected recording, NIRStar recordings without a
.nirsfile import, SNIRF exports pass the official validator and open in MNE with annotations aligned, and COBI Studio Modern.nirfiles are timed from their device sample counter with a fitted tick.NIRx import
<root>_probeInfo.mat, the source of the positions in NIRx's own SNIRF: 3D positions in mm, the vendor 2D layout, 10-10 optode labels, and the head model as the coordinate system (ICBM152 is declared as MNI). Landmarks (Nz, Iz, LPA, RPA, Cz) come fromdigpts.txt.t0is the first frame of the raw.rohstream, read from inside<root>.zip. A'Model'option names the device when the.hdronly has a serialDevice ID. The AuroraSampling ratekey is read..nirsfile. Data from.wl1/.wl2, channels from the.hdrS-D-Key/S-D-Mask(0-based keys accepted), geometry from_probeInfo.mat, markers from.hdrEventsor.evttriggers. Missing wavelengths default to 760/850 nm with a warning..hdrEventscodes are kept when a.nirsfile is also present.SNIRF export and import
dataUnitand the non-standardraw-DClabel are not written,dataLabels,wavelengthsandtimeare 1-D, and BIDS coordinate systems carry a description; other tags such asMNIgo to metadata tags thatimportSNIRFrestores.TimeOrigin;MeasurementDate/MeasurementTimeare in UTC when the zone is known ('TimeZone'option), since MNE drops the offset.BAD_gapstim. A gap of one whole missing sample is filled by interpolation; longer gaps are written as recorded with a warning ('MaxFillSamples'sets the limit;Inffills all). In MNE, filling with 0 or NaN instead would make optical density non-finite for the whole recording.'Target','mne'(off by default; also in batch export andasBIDS) fills every gap unless'MaxFillSamples'is given, strips dark channels, leaves out landmarks, and warns about irregular sample steps.'IncludeIdentifying', and path values are reduced to the file name.importSNIRFrestores the time origin for samples, markers, aux andt0, drops filled samples andBAD_gapstims, takes fs fromSamplingRate(written at full precision) and keepsRawMax, optode labels, landmarks and the coordinate system for re-export.asBIDSwritesevents.tsvonsets from the first sample.landmarkPos3D1000 times too large; MNE 1.11 reads it correctly..nir time base
.mrkmarkers and the header start time move into the final PC frame. Manual_C.mrkmarkers use the PC QPC timer from the session start, so they move by the first frame's offset like the start time; automatic marker files are placed frame by frame, and markers that could belong to more than one frame are warned about.info.timebase; PC-timed files get a regularity check ininfo.timing. Device setup keeps the counter fs only while the time vector still matches.Other
importNIRkeeps the marker file index inMarkerSource(only with more than one.mrkfile) instead ofAmplitude;asNIRdoes not write it.Testing
ExportTest,NirCounterTimebaseTest,CharacterizationTest,RoundtripTest,ImportNIRTest,FullPipelineTest,BidsExportTest,AuxRoundtripTest,MarkerPropagationTestandTensorContractTestpass. The GNU Octave subset (pf2_base.tests.runOctaveSubset) passes under Octave 11.3.0.ImportDirectoryTest/testMalformedBIDSDuplicateEntityWarns,MarkerTableTest/testSnirfTwoColumnStimReadsCodeFromCol2andPipelineFunctionTest/testSetParamsSpecialKeyWarnsfail the same way onmaster: an earlier test leaves warnings off.ExportTest/testExportSNIRFMultipleRunsstays skipped; multi-run export is a separate, existing limitation.