-
Notifications
You must be signed in to change notification settings - Fork 14
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
Add support for reference channel specification #75
Add support for reference channel specification #75
Conversation
f91beb3
to
8a6a864
Compare
I will extend this to support different references for all channels too, as is permitted by the file format spec. |
Ok I think this is ready for review |
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.
thanks @hoechenberger, this LGTM in general except for the thing with "None=CAR" (see comment on that).
And of course checking against MNE, FT, EEGLAB, Brainstorm ... and BVAnalyzer (which we can hopefully spread across several people)
pybv/io.py
Outdated
def write_brainvision(*, data, sfreq, ch_names, ref_ch_names=None, fname_base, | ||
folder_out, events=None, resolution=0.1, unit='µV', |
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.
honest question: is it good practice to have default args and non-default args intermixed? It seems to work here due to the leading *
arg, but it looks a bit odd and without the leading *
it would raise a SyntaxError.
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.
honest question: is it good practice to have default args and non-default args intermixed? It seems to work here due to the leading
*
arg, but it looks a bit odd and without the leading*
it would raise a SyntaxError.
I don't know if it's considered good practice, but I think it's one of the big advantages of using kw-only params to be able to do this :) I think it makes a lot of sense to have ref_ch_names
directly after ch_names
in both the signature and the docstring, even though it's not a required parameter.
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.
agreed, let's do it this way then 👍
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've never seen this either, it will certainly cause some confusion. Is there any alternative other than putting all default args at the end? And yes, because of the *
there is no "end" because these are all kw-only args.
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 there any alternative other than putting all default args at the end?
None that wouldn't be even more confusing: assign a default value like None
to parameters that need to be set. 🤷♂️
pybv/io.py
Outdated
The name of the channel used as a reference during the recording. If | ||
references differed between channels, you may supply a list of | ||
reference channel names corresponding to each channel in ``ch_names``. | ||
If ``None`` (default), assume the common average reference. |
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
None
(default), assume the common average reference.
I don't think we can do that without breaking compatibility. Specifying None
and writing nothing is not the same as the CAR - it just means that the information is not present.
For example, This is the beginning from a VHDR file I recorded using the standard BrainVision equipment, software, and workflow (Reference is on position FCz, but does not occur as a channel in the data):
[Channel Infos]
; Each entry: Ch<Channel number>=<Name>,<Reference channel name>,
; <Resolution in "Unit">,<Unit>, Future extensions..
; Fields are delimited by commas, some fields might be omitted (empty).
; Commas in channel names are coded as "\1".
Ch1=Fp1,,0.1,µV
Ch2=Fp2,,0.1,µV
Ch3=F7,,0.1,µV
The ref channel is FCz, not a CAR.
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.
Specifying
None
and writing nothing is not the same as the CAR - it just means that the information is not present.
But I do see where the misunderstanding comes from, I shouldn't have written "common average" but "reference-free" (although my understanding of looking at the circuits of the ActiCHamp amplifier we had in our old lab was that internally it would reference to the mean across all channels):
In any case, I propose to change the wording to "If None, assumes reference-free recording", WDYT?
This is the beginning from a VHDR file I recorded using the standard BrainVision equipment, software, and workflow (Reference is on position FCz, but does not occur as a channel in the data):
This seems to be violating their own file spec 🙈😱
Unless … I believe in BV Recorder there's the option to do a reference-free recording, BUT still select a reference electrode merely for online data viz. Could that have been the case 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.
Good point you make in #74 though, maybe we should reach out to BP for clarification.
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 am pretty sure BV recorder doesn't have a "reference free" button 🤔 but yes, there seems to be some sort of conflict :-(
I am going to write to BP support about this and will put you in cc / post the history 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.
btw: history is posted in #74
pybv/io.py
Outdated
.. note:: The reference channel name specified here does not need to | ||
appear in ``ch_names``; that is, it is okay to specify the | ||
name of a reference channel that was not saved along with | ||
the data. |
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 agree in principle, but before the next release, I'd like to check what BrainVision analyzer ... and mne/ft/eeglab/... do when they find a ref channel in the VHDR that is not present in the data.
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 have some data that looks exactly like this and was recorded with PyCorder in our old lab: for all channels, it specifies REF
as the reference, and REF
doesn't appear anywhere in the data.
(I believe we simply renamed Cz
to REF
in the settings)
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.
okay, that's already good to know.
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 believe MNE ignores this information anyway? AFAIK the reference is not stored anywhere within the Raw
object.
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 believe MNE ignores this information anyway? AFAIK the reference is not stored anywhere within the
Raw
object.
Yep. MNE doesn't do anything with this, as we cannot store the reference anyway.
pybv/io.py
Outdated
@@ -147,6 +157,33 @@ def write_brainvision(*, data, sfreq, ch_names, fname_base, folder_out, | |||
if len(set(ch_names)) != nchan: | |||
raise ValueError("Channel names must be unique, found duplicate name.") | |||
|
|||
# Ensure we have a list of strings as reference channel names | |||
if ref_ch_names is None: | |||
ref_ch_names = [''] * nchan # common average reference |
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.
see my comment above, I don't think we can/should do this.
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 we should proceed as follows (copied from #74 (comment))
- If user passes
reference_channel=None
(the default), pybv's old behavior is retained: The docstring will say that this corresponds to assuming a reference channel that is common to all channels, but which is not further specified. - If user passes
reference_channel=<some channel name that is in the data>
, we error if the channel is not all zeros ... else we accept it - If user passes
reference_channel=<some channel name that is **NOT** in the data>
, we accept it. In future extensions we might think about extending our API so that such cases can be documented in the[Comment]
section - but for now I feel that this is overkill. - If a list of reference channels is passed, the entries MUST NOT be
None
- we simply don't allow it. Otherwise, the rules from above apply
Sounds great! |
@hoechenberger do you want to finish this? |
Yes … when I have time 😅😅😅 |
no worries, I just intended it as showing that I still think it's worth it 👍 I'll be patient :) |
Please feel free to ping me again in a few weeks should you not see any activity here! |
Codecov Report
@@ Coverage Diff @@
## main #75 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 3 3
Lines 386 421 +35
=========================================
+ Hits 386 421 +35
Continue to review full report at Codecov.
|
to clarify: my last 3 commits were intended only to resolve the conflicts I introduced by making some commits to |
Can you maybe rebase and force push? That way changes from other PRs (namely #78) won't show up. |
Maybe I need to get another coffee, I'm pretty sure I was seeing diffs related to that other PR. Anyway, now it seems to work! |
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.
@hoechenberger I took some time to implement my requests from #75 (review)
I think this is ready to be merged now, feel free to check if my commits were in line with your ideas as well.
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.
LGTM!
In it goes, thanks for the work @hoechenberger |
Amazing, thanks @sappelhoff! |
Closes #74