-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
WIP: Adds support for fNIRS data to set_montage and new standard artinis montage #9141
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
Conversation
| @@ -0,0 +1,58 @@ | |||
| MNI Coordinates | |||
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.
Once we are sure the elc file is correct we can delete this. Must be done before merge.
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.
For those playing along at home...
In MNE we use the terminology source (S) and detector (D). These artinis files use the terminology transmitter (Tx) and receiver (Rx).
| @@ -0,0 +1,35 @@ | |||
| MNI Coordinates | |||
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.
Once we are sure the elc file is correct we can delete these. Must be done before merge.
|
Hi @HanBnrd, I cant make the sprint in your time zone today so I have tidied up our fork a little so I could share my thoughts with you. I have added a temporary change to one of the docs so we can discuss some questions I had. You can view the render of the doc change here: https://26156-1301584-gh.circle-artifacts.com/0/dev/auto_tutorials/io/plot_30_reading_fnirs_data.html#view-location-of-sensors-over-brain-surface Now to my questions to you... Is this the API/workflow that you were thinking about?We have ended up with this workflow... raw_intensity = load_some_data()
montage = make_standard_montage("artinis-octamon")
raw_intensity.set_montage(montage)
# ... processing as usualwill this work for you? If not, what's an alternative API we could use? Locations look wrongCurrently the sources and detectors in 3d look like... I think we have a coordinate system mismatch going on somewhere, either in the raw files or in the loading, storage, or plotting. Could you dig in to this a bit further? If we can get the sensor positions plotting correctly we can then go back and clean up our functions, make them more generic, add tests etc. @larsoner has helped me with coord system issues before, but he isn't on your timezone either. You might need to ask around in the available mentors room for someone who can help. |
|
Hi @rob-luke,
Yes the API looks good to me :)
At the moment I'm having a look at
|
|
It's probably not an issue related to |
mne/channels/montage.py
Outdated
|
|
||
| fnirs_picks = _picks_to_idx(info, 'fnirs', allow_empty=True) | ||
| if len(fnirs_picks) > 0: | ||
| info = _set_montage_fnirs(info, montage) |
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.
To avoid the pain of the next person errantly using montage instead of mnt_head, after conversion above we could del montage so that it's no longer in the namespace as a strong hint that the montage-in-head should be used by the subsequent code.
|
This is how it looks now with the fix thanks to @larsoner's help :) |
|
Nice! I had a feeling it was the coord frame 🤔 Now that things are correct we just need to make the code nice and add tests, docs etc. But before that, can you try this on your local data and confirm it helps to load your labs files, that's why we are doing this :) |
Yes, I'll do that tomorrow :) |
|
Hi @rob-luke, So it works with data from the actual OctaMon. I used a file from an experiment where I exported data in real time from OxySoft (Artinis' software) into a CSV thanks to TCP. import mne
import numpy as np
from mne.channels import make_standard_montage
from mne.channels.tests.test_standard_montage import _simulate_artinis_octamon
data = []
f = open(PATH + 'raw.csv', 'r', encoding='utf8')
f.readline() # skip headers
for line in f:
data.append(line.split('\n')[0].split(',')[1:]) # first col is timestamp
data = np.array(data, dtype='float64')
data = data * 1e-6 # uM to M
data = np.swapaxes(data, 0, 1)
ch_names = ['D1_S1 hbo', 'D1_S1 hbr', 'D1_S2 hbo', 'D1_S2 hbr',
'D1_S3 hbo', 'D1_S3 hbr', 'D1_S4 hbo', 'D1_S4 hbr',
'D2_S5 hbo', 'D2_S5 hbr', 'D2_S6 hbo', 'D2_S6 hbr',
'D2_S7 hbo', 'D2_S7 hbr', 'D2_S8 hbo', 'D2_S8 hbr']
ch_types = ['hbo', 'hbr', 'hbo', 'hbr',
'hbo', 'hbr', 'hbo', 'hbr',
'hbo', 'hbr', 'hbo', 'hbr',
'hbo', 'hbr', 'hbo', 'hbr']
sfreq = 10. # Hz
info = mne.create_info(ch_names=ch_names, ch_types=ch_types, sfreq=sfreq)
raw = mne.io.RawArray(data, info, verbose=True)
raw.set_montage('artinis-octamon')
raw.plot_sensors()
subjects_dir = mne.datasets.sample.data_path() + '/subjects'
mne.datasets.fetch_fsaverage(subjects_dir=subjects_dir, verbose=True)
fig = mne.viz.create_3d_figure(size=(800, 600), bgcolor='white')
fig = mne.viz.plot_alignment(raw.info, show_axes=True,
subject='fsaverage', coord_frame='mri',
trans='fsaverage', surfaces=['brain', 'head'],
fnirs=['channels', 'pairs',
'sources', 'detectors'],
dig=True, mri_fiducials=True,
subjects_dir=subjects_dir, fig=fig)
mne.viz.set_3d_view(figure=fig, azimuth=70, elevation=100, distance=0.4,
focalpoint=(0., -0.01, 0.02))Unfortunately I cannot share the data used here with the above code. Also, once there are loaders for oxy3 and oxy4 files (the proprietary format) it'll be more straight forward hopefully. I'll try to see if I can make some artificial evoked topo like this. |
|
Just so we have this somewhere: |
rob-luke
left a comment
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.
If we are to demonstrate how to generate raw and info from unsuported data, should we somewhere put in a description of the mandatory formatting? I described the mandatory rules at d42951d
And I assume the issue with the missaligned brite23 montage is fixed?
And I cant see a render of the docs in the jobs list, is it missing or have I lost my mind?
| _check_channels_ordered) | ||
| # Modify info['chs'][#]['loc'] in place | ||
| num_ficiduals = len(montage.dig) - len(montage.ch_names) | ||
| freqs = np.unique(_channel_frequencies(info)) |
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.
| freqs = np.unique(_channel_frequencies(info)) | |
| # First we must validate that the fNIRS info is correctly formatted | |
| freqs = np.unique(_channel_frequencies(info)) |
mne/channels/montage.py
Outdated
|
|
||
| for name, use in zip(info_names, info_names_use): | ||
| _loc_view = info['chs'][info['ch_names'].index(name)]['loc'] | ||
| # XXX info['chs'][#]['loc'] modified in place |
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.
Is this related to a change we made?
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.
It is not directly related but I wrote those comments to pinpoint all the lines where the in-place modifications were happening as it wasn't always obvious at first glance for example for this line 907.
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.
Should I remove them or is it okay?
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.
such comments are useful, but it would be clearer if it didn't start with XXX as this is usually a signal for "something to be fixed later / eventually". Can you change them to say "next line modifies info['chs'][#]['loc'] in-place" or similar?
mne/channels/montage.py
Outdated
| # in the old dig | ||
| if ref_dig_point in old_dig: | ||
| digpoints.append(ref_dig_point) | ||
| # XXX info['dig'] modified in place |
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.
As above
mne/channels/montage.py
Outdated
| info = _set_montage_fnirs(info, mnt_head) | ||
|
|
||
| else: # None case | ||
| # XXX info['dig'] modified in place |
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.
As above
| by MNE and cannot be converted easily into SNIRF. However if it is possible to | ||
| store this data in a legacy csv or tsv format, we show here a way to load it |
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.
| by MNE and cannot be converted easily into SNIRF. However if it is possible to | |
| store this data in a legacy csv or tsv format, we show here a way to load it | |
| by MNE and cannot be converted easily into SNIRF. However if it is possible to | |
| This legacy data is often in csv or tsv format, we show here a way to load it |
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.
this suggestion leaves the previous sentence beginning dangling... be sure to remove the "however if it is possible to" part
| standardisation of the file format (the naming and ordering of channels, the | ||
| type and scaling of data, and specification of sensor positions varies between | ||
| each vendor). You will likely have to adapt this depending on the system from | ||
| which your CSV originated. |
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.
sometimes its csv, other times CSV. which is correct?
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.
All changed to capital letters as they are acronyms.
Co-authored-by: Robert Luke <748691+rob-luke@users.noreply.github.com>
|
Hi @rob-luke, thanks for reviewing.
It was in an in-line comment but I just made it a warning now.
Yes it is solved by not using
I think it was because of the checks timing out, hopefully this will work now. Will paste the link here when it's built. |
|
Ok your turn @larsoner 😃 |
Actually CircleCI is not running it seems, I'm not sure where to find the built docs. |
|
Hi @rob-luke, an important point related to #9141 (comment): I just realised that it is actually possible to read any elc file (see #9309 (comment)) and our PR will make it work for fNIRS too: montage = read_custom_montage('path/to/artinis-brite23.elc', coord_frame='mri')
raw.set_montage(montage)(This is probably what has been tried in #8149) We just need to have trans = compute_native_head_t(montage)Should we write something about this in the example? I think you can add this in the PR description, and also that our PR will close #8149. |
|
I'm not sure why CircleCI isn't running here. The first thing I'd try is resovling the merge conflict on |
|
This recently happened to @cbrnr , I think because he revoked some permissions for CircleCI on his user (?). You could look at your settings. Also see if you can view https://app.circleci.com/pipelines/github/mne-tools/mne-python/7826/workflows/c965a22a-19cf-402a-b92f-633247c4b948/jobs/27243, I hope it asks you to log in or for permission, and then you can view it. This might fix things |
drammock
left a comment
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.
this is looking very good @HanBnrd. Just 2 comments.
|
|
||
|
|
||
| ############################################################################### | ||
| # Storing of optode locations |
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.
this whole section seems almost like a "note" that applies to all the other sections. Maybe this info belongs at the very top of the tutorial, before the NIRx section?
| # We simulate 16 channels with 100 samples of data and save this to a file | ||
| # called fnirs.csv. | ||
|
|
||
| pd.DataFrame(np.random.normal(size=(16, 100))).to_csv("fnirs.csv") |
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.
I wonder if it is really necessary to write out the CSV and then re-load it? Would it be just as good to say something like "here we'll make some fake data using pd.DataFrame; in practice you'd use pd.read_csv instead to load your file"?
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.
I think that some users won't know how to read a csv. And this will demonstrate it.
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.
fair enough.
|
I just pushed one more commit implementing @drammock's suggestion and @rob-luke's comment. Here is the new tutorial: https://27748-1301584-gh.circle-artifacts.com/0/dev/auto_tutorials/io/30_reading_fnirs_data.html That's ready for review now @larsoner. |
|
Thanks everybody! 🎉 |






Reference issue
Add montage for Artinis OctaMon and Brite23 Fixes #9093
What does this implement/fix?
Additional information
This PR has had feature creep and now contains a suggestion for importing data as csv files, to support the reading of unstructured data this PR now contains new code to check the naming of channels, etc. I have suggested reverting this PR back to its original goal of adding fNIRS support to
.set_montage()and providing two sample montage files.