Skip to content
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

Merged
merged 15 commits into from
Aug 14, 2021

Conversation

hoechenberger
Copy link
Collaborator

Closes #74

pybv/io.py Outdated Show resolved Hide resolved
@hoechenberger hoechenberger marked this pull request as draft June 20, 2021 08:00
@hoechenberger
Copy link
Collaborator Author

I will extend this to support different references for all channels too, as is permitted by the file format spec.

@hoechenberger hoechenberger marked this pull request as ready for review June 20, 2021 08:33
pybv/io.py Outdated Show resolved Hide resolved
pybv/io.py Outdated Show resolved Hide resolved
@hoechenberger
Copy link
Collaborator Author

Ok I think this is ready for review

Copy link
Member

@sappelhoff sappelhoff left a 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
Comment on lines 36 to 37
def write_brainvision(*, data, sfreq, ch_names, ref_ch_names=None, fname_base,
folder_out, events=None, resolution=0.1, unit='µV',
Copy link
Member

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.

Copy link
Collaborator Author

@hoechenberger hoechenberger Jun 21, 2021

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.

Copy link
Member

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 👍

Copy link
Collaborator

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.

Copy link
Collaborator Author

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 Show resolved Hide resolved
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.
Copy link
Member

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.

Copy link
Collaborator Author

@hoechenberger hoechenberger Jun 21, 2021

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.

The specs say otherwise:
Screen Shot 2021-06-21 at 11 13 43

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):

Screen Shot 2021-06-21 at 11 16 21

Screen Shot 2021-06-21 at 11 15 31

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?

Copy link
Collaborator Author

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.

Copy link
Member

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.

Copy link
Member

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
Comment on lines 57 to 60
.. 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.
Copy link
Member

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.

Copy link
Collaborator Author

@hoechenberger hoechenberger Jun 21, 2021

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)

Copy link
Member

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.

Copy link
Collaborator

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.

Copy link
Collaborator Author

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
Copy link
Member

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.

@sappelhoff sappelhoff added the enhancement New feature or request label Jun 21, 2021
@sappelhoff sappelhoff added this to the 0.6.0 milestone Jun 21, 2021
Copy link
Member

@sappelhoff sappelhoff left a 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

@hoechenberger
Copy link
Collaborator Author

Sounds great!

@sappelhoff
Copy link
Member

@hoechenberger do you want to finish this?

@hoechenberger
Copy link
Collaborator Author

hoechenberger commented Jul 20, 2021

Yes … when I have time 😅😅😅
Are you asking because you'd want to take over otherwise? Feel free to move ahead and I can help test & review. Otherwise you'll need to be a little more patient with me 😁

@sappelhoff
Copy link
Member

Are you asking because you'd want to take over otherwise? Feel free to move ahead and I can help test & review. Otherwise you'll need to be a little more patient with me

no worries, I just intended it as showing that I still think it's worth it 👍 I'll be patient :)

@hoechenberger
Copy link
Collaborator Author

I'll be patient :)

Please feel free to ping me again in a few weeks should you not see any activity here!

@codecov
Copy link

codecov bot commented Aug 7, 2021

Codecov Report

Merging #75 (f24fb87) into main (f80452c) will not change coverage.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff            @@
##              main       #75   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            3         3           
  Lines          386       421   +35     
=========================================
+ Hits           386       421   +35     
Impacted Files Coverage Δ
pybv/io.py 100.00% <100.00%> (ø)
pybv/tests/test_bv_writer.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f80452c...f24fb87. Read the comment docs.

@sappelhoff
Copy link
Member

to clarify: my last 3 commits were intended only to resolve the conflicts I introduced by making some commits to main in the meantime.

@cbrnr
Copy link
Collaborator

cbrnr commented Aug 9, 2021

Can you maybe rebase and force push? That way changes from other PRs (namely #78) won't show up.

@sappelhoff
Copy link
Member

Can you maybe rebase and force push? That way changes from other PRs (namely #78) won't show up.

I think they aren't 🤔 the diff of this PR is just relatively big itself

see for example this screenshot:

image

The change from #78 does not show up in the diff

@cbrnr
Copy link
Collaborator

cbrnr commented Aug 9, 2021

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!

Copy link
Member

@sappelhoff sappelhoff left a 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.

Copy link
Collaborator

@cbrnr cbrnr left a comment

Choose a reason for hiding this comment

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

LGTM!

@sappelhoff
Copy link
Member

In it goes, thanks for the work @hoechenberger

@sappelhoff sappelhoff merged commit 456d014 into bids-standard:main Aug 14, 2021
@hoechenberger
Copy link
Collaborator Author

Amazing, thanks @sappelhoff!

@hoechenberger hoechenberger deleted the hoechenberger/issue74 branch August 17, 2021 09:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Specify reference channel(s)
3 participants