-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Broader support of the SNIRF file format and enable reading GowerLab data #10555
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
6f57595
08fc60f
f4fb273
6b89d3e
3b78561
ffa38db
7e55e66
7edec29
9fb918c
d28ecb7
b1486d6
5c0b057
07c4c9b
df66ca5
a8d6b91
59a5c27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -459,6 +459,9 @@ def __init__(self, fname, saturated, preload=False, verbose=None): | |
| annot = Annotations(onset, duration, description, ch_names=ch_names) | ||
| self.set_annotations(annot) | ||
|
|
||
| sort_idx = np.argsort(self.ch_names) | ||
| self.pick(picks=sort_idx) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hum. This will make a copy of the data buffer but more importantly this is not consistent with the other readers which read the channels in the order they appear in the file on disk. I prefer to avoid doing magic in readers. Relevant functions down the road should deal with files with channels present in different orders, eg combine_evoked grand_average etc.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If
I agree with this idea in principle, but it will require a big refactoring of our existing code, which assumes and requires that the frequency pairings are essentially properly "interlaced" to work correctly. So to me I would like to add a comment
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @rob-luke I realized this is problematic. Let's say we have this arrangement with the more-or-less standard "logical channel numbering": The native file will be written as Rather than this Eventually like we said before we could/should relax this by making the channel logic smarter everywhere, but that's a bigger undertaking. All we need to do to fix this immediate/lpressing "logical channel" reordering problem I think is
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I disagree that there is a standard logical channel ordering. Even for the trivial example which you illustrate I would assign the channel numbers differently to you. You have assigned
I would have thought the logical ordering was
I also don't think that the channel number has any meaning. And it's the pair naming that should be used. That's why I went for a simple sort. It will always be the same and can be easily described.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure it can differ by system or setup, but to me the acquisition system sets the "standard order" when it writes data to disk in some order. So the idea is just to respect the order as written to disk as much as possible until we can remove any ordering restrictions whatsoever. FWIW we used to have this order problem with MEEG data, too, and removing it made the code cleaner in the end. So I'm looking forward to removing the paired-order restriction for fNIRS, as the cleanest long term solution is just to keep the order each manufacturer uses in the first place, or a user does with reorder_channels, etc. Currently reorder_chaanels is problematic for fNIRS and it shouldn't be! |
||
|
|
||
| def _read_segment_file(self, data, idx, fi, start, stop, cals, mult): | ||
| """Read a segment of data from a file. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,16 +50,25 @@ | |
| ft_od = op.join(testing_path, 'SNIRF', 'FieldTrip', | ||
| '220307_opticaldensity.snirf') | ||
|
|
||
| # GowerLabs | ||
| lumo110 = op.join(testing_path, 'SNIRF', 'GowerLabs', 'lumomat-1-1-0.snirf') | ||
|
|
||
|
|
||
| def _get_loc(raw, ch_name): | ||
| return raw.copy().pick(ch_name).info['chs'][0]['loc'] | ||
|
|
||
|
|
||
| @requires_testing_data | ||
| @pytest.mark.filterwarnings('ignore:.*contains 2D location.*:') | ||
| @pytest.mark.filterwarnings('ignore:.*measurement date.*:') | ||
| @pytest.mark.parametrize('fname', ([sfnirs_homer_103_wShort, | ||
| nirx_nirsport2_103, | ||
| sfnirs_homer_103_153, | ||
| nirx_nirsport2_103, | ||
| nirx_nirsport2_103_2, | ||
| nirx_nirsport2_103_2, | ||
| kernel_hb | ||
| kernel_hb, | ||
| lumo110 | ||
| ])) | ||
| def test_basic_reading_and_min_process(fname): | ||
| """Test reading SNIRF files and minimum typical processing.""" | ||
|
|
@@ -73,6 +82,18 @@ def test_basic_reading_and_min_process(fname): | |
| assert 'hbr' in raw | ||
|
|
||
|
|
||
| @requires_testing_data | ||
| @pytest.mark.filterwarnings('ignore:.*measurement date.*:') | ||
| def test_snirf_gowerlabs(): | ||
| """Test reading SNIRF files.""" | ||
| raw = read_raw_snirf(lumo110, preload=True) | ||
|
|
||
| assert raw._data.shape == (216, 274) | ||
| assert raw.info['dig'][0]['coord_frame'] == FIFF.FIFFV_COORD_HEAD | ||
| assert len(raw.ch_names) == 216 | ||
| assert_allclose(raw.info['sfreq'], 10.0) | ||
|
|
||
|
|
||
| @requires_testing_data | ||
| def test_snirf_basic(): | ||
| """Test reading SNIRF files.""" | ||
|
|
@@ -85,33 +106,33 @@ def test_snirf_basic(): | |
| # Test channel naming | ||
| assert raw.info['ch_names'][:4] == ["S1_D1 760", "S1_D1 850", | ||
| "S1_D9 760", "S1_D9 850"] | ||
| assert raw.info['ch_names'][24:26] == ["S5_D13 760", "S5_D13 850"] | ||
| # assert raw.info['ch_names'][24:26] == ["S5_D8 760", "S5_D8 850"] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cruft, remove?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gross 😞 , thanks for finding this mess. |
||
|
|
||
| # Test frequency encoding | ||
| assert raw.info['chs'][0]['loc'][9] == 760 | ||
| assert raw.info['chs'][1]['loc'][9] == 850 | ||
|
|
||
| # Test source locations | ||
| assert_allclose([-8.6765 * 1e-2, 0.0049 * 1e-2, -2.6167 * 1e-2], | ||
| raw.info['chs'][0]['loc'][3:6], rtol=0.02) | ||
| _get_loc(raw, 'S1_D1 760')[3:6], rtol=0.02) | ||
| assert_allclose([7.9579 * 1e-2, -2.7571 * 1e-2, -2.2631 * 1e-2], | ||
| raw.info['chs'][4]['loc'][3:6], rtol=0.02) | ||
| _get_loc(raw, 'S2_D3 760')[3:6], rtol=0.02) | ||
| assert_allclose([-2.1387 * 1e-2, -8.8874 * 1e-2, 3.8393 * 1e-2], | ||
| raw.info['chs'][8]['loc'][3:6], rtol=0.02) | ||
| _get_loc(raw, 'S3_D2 760')[3:6], rtol=0.02) | ||
| assert_allclose([1.8602 * 1e-2, 9.7164 * 1e-2, 1.7539 * 1e-2], | ||
| raw.info['chs'][12]['loc'][3:6], rtol=0.02) | ||
| _get_loc(raw, 'S4_D4 760')[3:6], rtol=0.02) | ||
| assert_allclose([-0.1108 * 1e-2, 0.7066 * 1e-2, 8.9883 * 1e-2], | ||
| raw.info['chs'][16]['loc'][3:6], rtol=0.02) | ||
| _get_loc(raw, 'S5_D5 760')[3:6], rtol=0.02) | ||
|
|
||
| # Test detector locations | ||
| assert_allclose([-8.0409 * 1e-2, -2.9677 * 1e-2, -2.5415 * 1e-2], | ||
| raw.info['chs'][0]['loc'][6:9], rtol=0.02) | ||
| _get_loc(raw, 'S1_D1 760')[6:9], rtol=0.02) | ||
| assert_allclose([-8.7329 * 1e-2, 0.7577 * 1e-2, -2.7980 * 1e-2], | ||
| raw.info['chs'][3]['loc'][6:9], rtol=0.02) | ||
| _get_loc(raw, 'S1_D9 850')[6:9], rtol=0.02) | ||
| assert_allclose([9.2027 * 1e-2, 0.0161 * 1e-2, -2.8909 * 1e-2], | ||
| raw.info['chs'][5]['loc'][6:9], rtol=0.02) | ||
| _get_loc(raw, 'S2_D3 850')[6:9], rtol=0.02) | ||
| assert_allclose([7.7548 * 1e-2, -3.5901 * 1e-2, -2.3179 * 1e-2], | ||
| raw.info['chs'][7]['loc'][6:9], rtol=0.02) | ||
| _get_loc(raw, 'S2_D10 850')[6:9], rtol=0.02) | ||
|
|
||
| assert 'fnirs_cw_amplitude' in raw | ||
|
|
||
|
|
@@ -186,9 +207,9 @@ def test_snirf_nirsport2(): | |
| assert_almost_equal(raw.info['sfreq'], 7.6, decimal=1) | ||
|
|
||
| # Test channel naming | ||
| assert raw.info['ch_names'][:4] == ['S1_D1 760', 'S1_D1 850', | ||
| 'S1_D3 760', 'S1_D3 850'] | ||
| assert raw.info['ch_names'][24:26] == ['S6_D4 760', 'S6_D4 850'] | ||
| assert raw.info['ch_names'][:4] == ['S10_D3 760', 'S10_D3 850', | ||
| 'S10_D9 760', 'S10_D9 850'] | ||
| assert raw.info['ch_names'][24:26] == ['S15_D11 760', 'S15_D11 850'] | ||
|
|
||
| # Test frequency encoding | ||
| assert raw.info['chs'][0]['loc'][9] == 760 | ||
|
|
@@ -226,7 +247,7 @@ def test_snirf_nirsport2_w_positions(): | |
| # Test channel naming | ||
| assert raw.info['ch_names'][:4] == ['S1_D1 760', 'S1_D1 850', | ||
| 'S1_D6 760', 'S1_D6 850'] | ||
| assert raw.info['ch_names'][24:26] == ['S6_D4 760', 'S6_D4 850'] | ||
| assert raw.info['ch_names'][24:26] == ['S6_D14 760', 'S6_D14 850'] | ||
|
|
||
| # Test frequency encoding | ||
| assert raw.info['chs'][0]['loc'][9] == 760 | ||
|
|
@@ -238,11 +259,12 @@ def test_snirf_nirsport2_w_positions(): | |
| # nirsite https://github.com/mne-tools/mne-testing-data/pull/86 | ||
| # figure 3 | ||
| allowed_distance_error = 0.005 | ||
| distances = source_detector_distances(raw.info) | ||
| assert_allclose(distances[::2][:14], | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the ordering of the channels is now changed, I test specific source detector pairs, rather than an indexed channel. |
||
| [0.0304, 0.0411, 0.008, 0.0400, 0.008, 0.0310, 0.0411, | ||
| 0.008, 0.0299, 0.008, 0.0370, 0.008, 0.0404, 0.008], | ||
| atol=allowed_distance_error) | ||
| assert_allclose(source_detector_distances(raw.copy(). | ||
| pick("S1_D1 760").info), | ||
| [0.0304], atol=allowed_distance_error) | ||
| assert_allclose(source_detector_distances(raw.copy(). | ||
| pick("S2_D2 760").info), | ||
| [0.0400], atol=allowed_distance_error) | ||
|
|
||
| # Test location of detectors | ||
| # The locations of detectors can be seen in the first | ||
|
|
@@ -261,10 +283,6 @@ def test_snirf_nirsport2_w_positions(): | |
| assert_allclose( | ||
| mni_locs[2], [-0.0841, -0.0138, 0.0248], atol=allowed_dist_error) | ||
|
|
||
| assert raw.info['ch_names'][34][3:5] == 'D5' | ||
| assert_allclose( | ||
| mni_locs[34], [0.0845, -0.0451, -0.0123], atol=allowed_dist_error) | ||
|
|
||
| # Test location of sensors | ||
| # The locations of sensors can be seen in the second | ||
| # figure on this page... | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@samuelpowell are you ok with acknowledging your assistance here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that’s fine, thank you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rob-luke this should be
:newcontrib:and moved to the top of the new list