Skip to content

Conversation

@jackz314
Copy link
Contributor

Reference issue

"Spin-off" from #9192.

What does this implement/fix?

Add support for importing channel location from XYZ/CSV files as an extra XYZ option (to be finalized) for read_custom_montage as per suggestion from here.

Additional information

The naming (XYZ?) & formatting of this feature isn't fully finalized yet, I'm still waiting for input from more qualified people on that.

Copy link
Member

@agramfort agramfort left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tell us when you added test and a what's new entry and it's good to go from your end.

@jackz314
Copy link
Contributor Author

jackz314 commented Mar 31, 2021

I think feature-wise it's done, but I'm not sure what's good enough for the tests. Should I just import from a CSV and compare the resulting montage to the CSV data? That's how I "tested" it locally, but I'm not sure if it's good and comprehensive enough.

If that's the case then I think the existing test_montage_readers function might be good enough, all I need to do would be add some fake CSV data and expected digs/montages.

Copy link
Member

@agramfort agramfort left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need help to add some unit tests?



def _read_csv(fname, delimiter=','):
"""Import eeg channel locations from CSV files into MNE instance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Import eeg channel locations from CSV files into MNE instance.
"""Import EEG channel locations from CSV files into MNE instance.

delimiter : str
Delimiter used by the CSV file
include_ch_names : bool
Whether the CSV file include channel names as the first column
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this parameter does not exist

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I forgot to remove this after I removed the actual parameter. Would you say it's beneficial to add these parameters (delimiter and ch_names) to read_custom_montage so the CSV files can be more flexible? Or should I just remove both of them?

@jackz314
Copy link
Contributor Author

do you need help to add some unit tests?

Some help would be great since I'm still not really familiar with the MNE testing standards.

Whether the CSV file include channel names as the first column
"""
include_ch_names = True
import csv
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python standard library imports should go top-of-file, not nested inside function. We only nest optional dependencies (or internal imports to avoid circular import errors)

Comment on lines 369 to 379
# chs = inst.info['chs']
# if not include_ch_names:
# for ch, coord in zip(chs, coords):
# ch['loc'][:3] = coord
# else:
# for ch in chs:
# try:
# coord = coords[ch_names.index(ch['ch_name'])]
# except ValueError: # ch_name not in channel list, default to 0
# coord = (0, 0, 0)
# ch['loc'][:3] = coord
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cruft?

include_ch_names : bool
Whether the CSV file include channel names as the first column
"""
include_ch_names = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is now always true. remove it and eliminate the conditionals that depend on it.

Comment on lines 356 to 367
f = open(fname, "r")
f.readline()
ch_names = []
pos = []
for row in csv.reader(f, delimiter=delimiter):
if include_ch_names:
ch_name, x, y, z, *_ = row
ch_names.append(ch_name)
else:
x, y, z, *_ = row
pos.append((float(x), float(y), float(z)))
f.close()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we typically use the with open(fname, 'r') as fid: context manager, so we don't have to expressly close the file afterward. For consistency we should do the same here.

Comment on lines 342 to 343
CSV files should have columns x, y, and z, each row represents one channel.
Optionally the first column can contain the channel names.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the way the function is now written, channel names as first column are not in fact optional. Revise this text to reflect that.

ch_names.append(ch_name)
else:
x, y, z, *_ = row
pos.append((float(x), float(y), float(z)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nitpick) you could omit the three float() calls here, and just add dtype=float when you later convert pos to a numpy array. Seems cleaner to me that way.

return make_dig_montage(ch_pos=_check_dupes_odict(ch_names, pos))


def _read_csv(fname, delimiter=','):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not clear to me why delimiter is exposed here. It's a private function (so users can't actually change it) and it's only called once in the codebase and there's no triaging based on file extension (csv vs tsv). Do you know there to be XYZ channel location text files that use a different delimiter? If so, we should expose delimiter in the public API somehow. If not, we should assume comma-delimiter and remove this param.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delimiter is a leftover parameter, the only different ones are probably tabs in TSV files. Should this be checked in read_custom_montage or in _read_csv? As for the channel names, I've seen files with channel names (mainly CSV files) and ones without (mainly XYZ files).

@agramfort
Copy link
Member

agramfort commented Mar 31, 2021 via email

Copy link
Member

@drammock drammock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just 2 small docstring fixes.


CSV files should have columns 4 columns containing
ch_name, x, y, and z. Each row represents one channel.
The first column can contain the channel names.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The first column can contain the channel names.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(cruft)

Parameters
----------
fname : str
Name of the csv file to read channel locations from
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Name of the csv file to read channel locations from
Name of the csv file to read channel locations from.

@agramfort
Copy link
Member

@jackz314 do you want to take over?

@jackz314
Copy link
Contributor Author

I'm finalizing the changes now. Should we change _read_csv to _read_xyz? Since it should handle all three formats (csv, tsv, and xyz)? Also, it seems like the norm for XYZ files is that channel names are in the last column, but I've also seen some, where it's the first column, what should we do about this?

@agramfort
Copy link
Member

agramfort commented Mar 31, 2021 via email

@jackz314
Copy link
Contributor Author

jackz314 commented Mar 31, 2021

I pushed a commit with some code to adapt for TSV, CSV, and XYZ files. Since CSV isn't a standard, I just specified and used the ch_name, x, y, z format, which follows the TSV standard used elsewhere. For XYZ files I used the EEGLAB standard (or just the standard EEGLAB uses), which is row_count, x, y, z, ch_name.

@agramfort
Copy link
Member

@jackz314 I added some tests. See my commit

To run the tests you can use:

pytest mne/channels/tests/test_montage.py

@jackz314
Copy link
Contributor Author

jackz314 commented Apr 1, 2021

Thanks for the tests, I ran them locally and it seems like they are passing. I think this should be ready unless there's anything else needed.

@jackz314 jackz314 marked this pull request as ready for review April 1, 2021 09:04
Copy link
Member

@agramfort agramfort left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jackz314 you need now to add a what's new entry in the file latest.inc in the doc folder

also I feel that we should update the page https://mne.tools/dev/auto_tutorials/intro/plot_40_sensor_locations.html

@drammock you confirm it would be the right place?

@drammock
Copy link
Member

drammock commented Apr 1, 2021

also I feel that we should update the page https://mne.tools/dev/auto_tutorials/intro/plot_40_sensor_locations.html
@drammock you confirm it would be the right place?

Not sure I agree. read_custom_montage isn't really covered anywhere in the tutorials (it is used once in passing in https://mne.tools/dev/auto_tutorials/source-modeling/plot_eeg_mri_coords.html). I think it might make more sense to create a new how-to example: "how to load a custom EEG sensor montage" in the input/output category.

@jackz314
Copy link
Contributor Author

jackz314 commented Apr 1, 2021

Just added the what's new entry.

I think it would make a lot of sense to add read_custom_montage to a tutorial somewhere since otherwise, it might be confusing for people just learning about MNE. I think it would make sense to add it to the existing page https://mne.tools/dev/auto_tutorials/intro/plot_40_sensor_locations.html since it's titled "Working with sensor locations", which seems like a natural place for adding/reading custom sensor locations.

@drammock
Copy link
Member

drammock commented Apr 1, 2021

Just added the what's new entry.

I think it would make a lot of sense to add read_custom_montage to a tutorial somewhere since otherwise, it might be confusing for people just learning about MNE. I think it would make sense to add it to the existing page https://mne.tools/dev/auto_tutorials/intro/plot_40_sensor_locations.html since it's titled "Working with sensor locations", which seems like a natural place for adding/reading custom sensor locations.

I guess it would be OK to add a section between "working with built-in montages" and "controlling channel projection" called "working with custom montages" that shows a quick example of read_custom_montage. It should probably list the various formats that we support (not just XYZ).

Copy link
Member

@agramfort agramfort left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

anyone merge if happy

thx @jackz314

@drammock
Copy link
Member

drammock commented Apr 2, 2021

@jackz314 are you planning to add to the tutorial in this PR, or in a separate one?

@jackz314
Copy link
Contributor Author

jackz314 commented Apr 3, 2021

@jackz314 are you planning to add to the tutorial in this PR, or in a separate one?

I wasn't planning on adding the tutorials myself, mostly because I'm not really familiar with how the tutorial part or the other kinds of montages work yet. But if no one else is working on it, I can look into it and open another PR later if I have time.

@drammock
Copy link
Member

drammock commented Apr 3, 2021

Ok I'll merge this one now then.

@drammock drammock merged commit 2897d4b into mne-tools:main Apr 3, 2021
@welcome
Copy link

welcome bot commented Apr 3, 2021

🎉 Congrats on merging your first pull request! 🥳 Looking forward to seeing more from you in the future! 💪

@jackz314 jackz314 deleted the chan_loc branch April 3, 2021 17:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants