Skip to content

Commit

Permalink
Provide option for explicit time vector in SNIRF output, fix #47 (#48)
Browse files Browse the repository at this point in the history
* Provide option for explicit time vector in SNIRF output
* Add optional explicit time vector to aux data
* Export MPU data in ms
* Bump version
  • Loading branch information
samuelpowell authored Nov 13, 2023
1 parent 4fee19e commit a345ecd
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 15 deletions.
2 changes: 1 addition & 1 deletion +lumofile/read_lufr.m
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,7 @@
'nframes', size(chdat, 2), ...
'nchns', size(chdat, 1), ...
'node_temp', tmpdat, ...
'node_mpu_dt', 10e-3, ...
'node_mpu_dt', 10e-3*1e3, ...
'node_mpu_fps', 100, ...
'node_acc', accdat, ...
'node_gyr', gyrdat);
Expand Down
35 changes: 25 additions & 10 deletions +lumofile/write_SNIRF.m
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
% See also LUMO_READ
%
%
% (C) Gowerlabs Ltd., 2022
% (C) Gowerlabs Ltd., 2023
%

%%% TODO
Expand All @@ -69,8 +69,11 @@
p = inputParser;
expected_styles_meta = {'standard', 'extended'};
addOptional(p, 'meta', 'standard', @(x) any(validatestring(x, expected_styles_meta)));
expected_styles_time = {'standard', 'explicit'};
addOptional(p, 'time', 'standard', @(x) any(validatestring(x, expected_styles_time)));
parse(p, varargin{:})
meta_style = p.Results.meta;
time_style = p.Results.time;


ng = length(enum.groups);
Expand Down Expand Up @@ -111,6 +114,18 @@

% Build permutation for ordering
chn_perm = [];

% Construct time vectors
if strcmp(time_style, 'explicit')
tt_chn = (0:((data(gidx).nframes)-1))*data(gidx).chn_dt;
tt_mpu = (0:((size(data(gidx).node_acc, 3))-1))*data(gidx).node_mpu_dt;
else
tt_chn = [0 data(gidx).chn_dt];
tt_mpu = [0 data(gidx).node_mpu_dt];
end




% Create NIRS root
% /nirs{i}
Expand Down Expand Up @@ -242,7 +257,7 @@
%
nirs_data_group = create_group(nirs_group, 'data1'); % Note: single data block
dt.dataTimeSeries = write_chn_dat_block(nirs_data_group, chn_perm, data(gidx).chn_dat); % Write data /nirs{i}/data1
dt.time = write_double(nirs_data_group, 'time', [0 data(gidx).chn_dt]); % Write /nirs{i}/time
dt.time = write_double(nirs_data_group, 'time', tt_chn); % Write /nirs{i}/time
dt.measurementList = write_measlist(nirs_data_group, chn_perm, gidx, enum, glch); % Write measurementList

% Assign data block
Expand Down Expand Up @@ -296,7 +311,7 @@
if size(data(gidx).chn_sat, 2) > 1
nirs_aux_sat = create_group(nirs_group, ['aux' num2str(auxi)]);
snirf.nirs(gidx).aux(auxi).name = write_var_string(nirs_aux_sat, 'name', 'saturationFlags');
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_sat, 'time', [0 data(gidx).chn_dt]);
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_sat, 'time', tt_chn);
snirf.nirs(gidx).aux(auxi).dataTimeSeries = write_single_compressed(nirs_aux_sat, 'dataTimeSeries', data(gidx).chn_sat.');
H5G.close(nirs_aux_sat);
auxi = auxi+1;
Expand All @@ -307,7 +322,7 @@
if isfield(data(gidx), 'node_temp')
nirs_aux_temp = create_group(nirs_group, ['aux' num2str(auxi)]);
snirf.nirs(gidx).aux(auxi).name = write_var_string(nirs_aux_temp, 'name', 'temperature');
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_temp, 'time', [0 data(gidx).chn_dt]);
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_temp, 'time', tt_chn);
snirf.nirs(gidx).aux(auxi).dataTimeSeries = write_single(nirs_aux_temp, 'dataTimeSeries', data(gidx).node_temp.');
H5G.close(nirs_aux_temp);
auxi = auxi+1;
Expand All @@ -319,42 +334,42 @@

nirs_aux_mpu = create_group(nirs_group, ['aux' num2str(auxi)]);
snirf.nirs(gidx).aux(auxi).name = write_var_string(nirs_aux_mpu, 'name', 'accel_x');
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', [0 data(gidx).node_mpu_dt]);
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', tt_mpu);
snirf.nirs(gidx).aux(auxi).dataTimeSeries = write_single(nirs_aux_mpu, 'dataTimeSeries', squeeze(data(gidx).node_acc(:,1,:)).');
H5G.close(nirs_aux_mpu);
auxi = auxi+1;

nirs_aux_mpu = create_group(nirs_group, ['aux' num2str(auxi)]);
snirf.nirs(gidx).aux(auxi).name = write_var_string(nirs_aux_mpu, 'name', 'accel_y');
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', [0 data(gidx).node_mpu_dt]);
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', tt_mpu);
snirf.nirs(gidx).aux(auxi).dataTimeSeries = write_single(nirs_aux_mpu, 'dataTimeSeries', squeeze(data(gidx).node_acc(:,2,:)).');
H5G.close(nirs_aux_mpu);
auxi = auxi+1;

nirs_aux_mpu = create_group(nirs_group, ['aux' num2str(auxi)]);
snirf.nirs(gidx).aux(auxi).name = write_var_string(nirs_aux_mpu, 'name', 'accel_z');
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', [0 data(gidx).node_mpu_dt]);
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', tt_mpu);
snirf.nirs(gidx).aux(auxi).dataTimeSeries = write_single(nirs_aux_mpu, 'dataTimeSeries', squeeze(data(gidx).node_acc(:,3,:)).');
H5G.close(nirs_aux_mpu);
auxi = auxi+1;

nirs_aux_mpu = create_group(nirs_group, ['aux' num2str(auxi)]);
snirf.nirs(gidx).aux(auxi).name = write_var_string(nirs_aux_mpu, 'name', 'gyro_x');
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', [0 data(gidx).node_mpu_dt]);
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', tt_mpu);
snirf.nirs(gidx).aux(auxi).dataTimeSeries = write_single(nirs_aux_mpu, 'dataTimeSeries', squeeze(data(gidx).node_gyr(:,1,:)).');
H5G.close(nirs_aux_mpu);
auxi = auxi+1;

nirs_aux_mpu = create_group(nirs_group, ['aux' num2str(auxi)]);
snirf.nirs(gidx).aux(auxi).name = write_var_string(nirs_aux_mpu, 'name', 'gyro_y');
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', [0 data(gidx).node_mpu_dt]);
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', tt_mpu);
snirf.nirs(gidx).aux(auxi).dataTimeSeries = write_single(nirs_aux_mpu, 'dataTimeSeries', squeeze(data(gidx).node_gyr(:,2,:)).');
H5G.close(nirs_aux_mpu);
auxi = auxi+1;

nirs_aux_mpu = create_group(nirs_group, ['aux' num2str(auxi)]);
snirf.nirs(gidx).aux(auxi).name = write_var_string(nirs_aux_mpu, 'name', 'gyro_z');
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', [0 data(gidx).node_mpu_dt]);
snirf.nirs(gidx).aux(auxi).time = write_double(nirs_aux_mpu, 'time', tt_mpu);
snirf.nirs(gidx).aux(auxi).dataTimeSeries = write_single(nirs_aux_mpu, 'dataTimeSeries', squeeze(data(gidx).node_gyr(:,3,:)).');
H5G.close(nirs_aux_mpu);
auxi = auxi+1;
Expand Down
2 changes: 1 addition & 1 deletion +lumomat/ver.m
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
function [ver] = ver()
%LMVER Return the hard coded version of the package
ver = '1.7.0-dev';
ver = '1.7.0';
end

13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# lumomat

## v1.7.0
- provide optional explicit time vector in SNIRF output

## v1.6.0
- fix loading of saturation flags from LUMO file

## v1.5.1
- filter non printable ASCII event marker characters on LUFR read

## v1.5.0
- SNIRF output version validated against v1.1 specification
- anatomical landmarks exported in NIRS SD3D output

## v1.4.1
- do not use the memory command on platforms upon which it is not supported
- update documentation on MPU data output ordering, extended metadata
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,13 @@ ans =
nframes: 1503
nchns: 24
node_temp: [1×1503 single]
node_mpu_dt: 0.0100
node_mpu_dt: 10
node_mpu_fps: 100
node_acc: [1×3×12024 double]
node_gyr: [1×3×12024 double]
```

When present, the accelerometer and gyroscope data has dimensions of `<no. tiles x 3 x no. time>` where the second dimension is indexed over the x, y, and z-axes. The time vector for such motion data can be computed using the `node_mpu_dt` field.
When present, the accelerometer and gyroscope data has dimensions of `<no. tiles x 3 x no. time>` where the second dimension is indexed over the x, y, and z-axes. The time vector for such motion data can be computed using the `node_mpu_dt` field, which is specified in ms.

## SNIRF output

Expand Down Expand Up @@ -453,11 +453,15 @@ Each auxiliary measurement is located in an individual `/nirs(i)/aux(j)` field.

*All auxiliary fields are optional and only recorded by more recent software versions of LUMO. To update your software in order to capture this data, contact Gowerlabs.*


### Notes

- When a layout field contains physiological landmarks, these will be stored in the appropriate `landmarkLabels` and `landmarkPos3D` datasets, but `landmarkPos2D` is optional and may not be present.

### Package support
- [Homer3](https://github.com/BUNPC/Homer3) is [not compliant](https://github.com/BUNPC/Homer3/issues/180) with the SNIRF specification and will fail to load files using the abbreviated time vector format. To circumvent this bug, export an explicit time vector using the options:
```
snirf = data.write_SNIRF(snirf_filename, 'time', 'explicit');
```

## NIRS output

Expand Down

0 comments on commit a345ecd

Please sign in to comment.