Back to the main page.

Bug 3229 - ft_apply_montage generates NaN elec.chanpos

Status CLOSED FIXED
Reported 2017-01-11 08:19:00 +0100
Modified 2019-08-10 12:40:57 +0200
Product: FieldTrip
Component: forward
Version: unspecified
Hardware: PC
Operating System: Mac OS
Importance: P5 normal
Assigned to: Arjen Stolk
URL:
Tags:
Depends on:
Blocks:
See also: http://bugzilla.fieldtriptoolbox.org/show_bug.cgi?id=1481http://bugzilla.fieldtriptoolbox.org/show_bug.cgi?id=1990http://bugzilla.fieldtriptoolbox.org/show_bug.cgi?id=2634

Arjen Stolk - 2017-01-11 08:19:38 +0100

Is there a reason why ft_apply_montage produces NaNs in elec.chanpos in stead of taking the spatial average of the positions of the to-be-rereferenced electrodes? Or am I using it incorrectly? Code to replicate: elec.label = {'1';'2';'3';'4';'5';'6';'7';'8'}; elec.elecpos = randn(8,3); elec.chanpos = elec.elecpos; elec.tra = eye(8); montage.labelold = elec.label; montage.labelnew = strcat(elec.label(1:end-1), '-', elec.label(2:end)); % bipolar labels M1 = diag(-ones(numel(elec.label)-1,1),1); M2 = diag(ones(numel(elec.label)-1,1),-1); montage.tra = M1(1:end-1,:)+M2(2:end,:); % bipolar montage sens = ft_apply_montage(elec, montage); sens.chanpos ans = NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN Line 377 and onward in ft_apply_montage (note the sens.chanpos = nan..): % The montage operates on the coil weights in sens.tra, but the output channels % can be different. If possible, we want to keep the original channel positions % and orientations. [sel1, sel2] = match_str(montage.labelnew, inputlabel); keepchans = length(sel1)==length(montage.labelnew); if isfield(sens, 'chanpos') if keepchans sens.chanpos = sens.chanpos(sel2,:); else if ~isfield(sens, 'chanposold') % add a chanposold only if it is not there yet sens.chanposold = sens.chanpos; end sens.chanpos = nan(numel(montage.labelnew),3); end end


Robert Oostenveld - 2017-01-11 11:24:47 +0100

Chanpos is not only used for visualization, but also for other stuff. Better have it invalid in case it is unknown rather than weird side effects happening. It would be nice to have an overview of where in the FT code chanpos is being used. The less, the better. The rationale for the NANs is that you don't want EEG channels to be projected into the head. The position of a channel that consists of two electrodes is not peruse in between the two electrodes. For plotting (using ft_prepare_layout) you may want to make that assumption. That is also why the abs-weighted montage is used there to compute channel locations in the layout. Also for an ICA cleaned dataset you don't want the channel positions to be shifted relative to the original ones. Please check whether ft_prepare_layout solves your problem


Arjen Stolk - 2017-01-11 17:52:22 +0100

(In reply to Robert Oostenveld from comment #1) Thanks for the elaboration. "The rationale for the NANs is that you don't want EEG channels to be projected into the head. The position of a channel that consists of two electrodes is not peruse in between the two electrodes. For plotting (using ft_prepare_layout) you may want to make that assumption. That is also why the abs-weighted montage is used there to compute channel locations in the layout." > I understand, just wouldn't that make ft_apply_montage obsolete for sens input, if not to adjust chanpos? If one wanted achieve one of your examples, one would specify a identity matrix for montage.tra. On a related note, wasn't it channel position adjustment that justified the existence of chanpos, besides elecpos, in the first place? One would use either one depending on the purpose, e.g. visualization vs. algorithmic. All in all, in the case of bipolar electrode derivation (in say intracranial EEG), ft_apply_montage then would only be used for applying to the functional data, and not channel position adjustment?


Arjen Stolk - 2017-01-12 08:05:11 +0100

ft_prepare_layout with cfg.montage (and cfg.elec) flattens the 3D elecpos into a 2D layout. It doesn't calculate the spatial average between two electrode locations (that are used for a bipolar derivation). This is not a problem since it's so trivial, just I would assume that ft_apply_montage was meant to do that. cfg = []; cfg.montage.labelorg = elec.label; cfg.montage.labelnew = strcat(elec.label(1:end-1), '-', elec.label(2:end)); M1 = diag(-ones(numel(elec.label)-1,1),1); M2 = diag(ones(numel(elec.label)-1,1),-1); cfg.montage.tra = M1(1:end-1,:)+M2(2:end,:); cfg.elec = elec; >> [layout, cfg] = ft_prepare_layout(cfg, []) creating layout from cfg.elec creating layout for eeg system the call to "ft_prepare_layout" took 0 seconds and required the additional allocation of an estimated 1 MB layout = pos: [9x2 double] width: [9x1 double] height: [9x1 double] label: {9x1 cell} outline: {[101x2 double] [3x2 double] [10x2 double] [10x2 double]} mask: {[101x2 double]} cfg: [1x1 struct] cfg = montage: [1x1 struct] elec: [1x1 struct] outputfilepresent: 'overwrite' callinfo: [1x1 struct] version: [1x1 struct] rotate: [] style: '2d' projection: 'polar' layout: [] grad: [] opto: [] gradfile: [] elecfile: [] optofile: [] output: [] feedback: 'no' image: [] mesh: [] bw: 0 channel: {8x1 cell} skipscale: 'no' skipcomnt: 'no' overlap: 'shift' previous: {}


Arjen Stolk - 2017-01-12 08:05:36 +0100

..


Robert Oostenveld - 2017-01-17 11:29:42 +0100

closed multiple bugs that were resolved some time ago


Arjen Stolk - 2017-02-27 19:45:01 +0100

Re-opening this one as it does not (yet) satisfy re-weighting channel positions (e.g. for bipolar derivation). In short, it fails two-fold. 1) When creating new channel names (e.g. 1-2, 2-3) that do not match the original labels causes NaNs in chanpos (see situation #1). 2) Even when creating new labels that do match the originals, montage.tra does not cause channel positions to be averaged (see situation #2). The reason it produces NaNs originates from (l. 380): [sel1, sel2] = match_str(montage.labelnew, inputlabel); keepchans = length(sel1)==length(montage.labelnew); where keepchans = 0 in case labelnew are labels not occurring in inputlabel, as with combined labels (e.g. 1-2, 2-3). SITUATION #1 (using combined labels): elec.label = {'1';'2';'3';'4';'5';'6';'7';'8'}; elec.elecpos = randn(8,3); elec.chanpos = elec.elecpos; elec.tra = eye(8); montage.labelold = elec.label; montage.labelnew = strcat(elec.label(1:end-1), '-', elec.label(2:end)); % bipolar labels M1 = diag(-ones(numel(elec.label)-1,1),1); M2 = diag(ones(numel(elec.label)-1,1),-1); montage.tra = M1(1:end-1,:)+M2(2:end,:); % bipolar montage sens = ft_apply_montage(elec, montage); sens.chanpos ans = NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN Note that using original labels to get keepchans = 1 also does not seem to update the elecpos according to weights specified in montage.tra. SITUATION #2 (using original labels): elec.label = {'1';'2';'3';'4';'5';'6';'7';'8'}; elec.elecpos = randn(8,3); elec.chanpos = elec.elecpos; elec.tra = eye(8); montage.labelold = elec.label; montage.labelnew = elec.label(1:end-1); % original labels M1 = diag(-ones(numel(elec.label)-1,1),1); M2 = diag(ones(numel(elec.label)-1,1),-1); montage.tra = M1(1:end-1,:)+M2(2:end,:); % bipolar montage sens = ft_apply_montage(elec, montage); sens.chanpos ans = 0.5377 3.5784 -0.1241 1.8339 2.7694 1.4897 -2.2588 -1.3499 1.4090 0.8622 3.0349 1.4172 0.3188 0.7254 0.6715 -1.3077 -0.0631 -1.2075 -0.4336 0.7147 0.7172 where sens.chanpos equals elec.chanpos(1:7), thus implying that the new tra matrix including +1 -1 weights on neighboring channels has not been applied.


Arjen Stolk - 2017-02-27 20:44:42 +0100

Simplifying the issue: elec.label = {'1';'2';'3';'4'}; elec.elecpos = [1 1 1; 2 2 2; 3 3 3; 4 4 4]; elec.chanpos = elec.elecpos; elec.tra = eye(4); bipolar.labelold = {'1', '2', '3', '4'}; bipolar.labelnew = {'1', '2', '3'}; bipolar.tra = [ +1 -1 0 0 0 +1 -1 0 0 0 +1 -1 ]; dat = ft_apply_montage(elec, bipolar); dat.chanpos: 1 1 1 2 2 2 3 3 3 rather than the expected: 1.5 1.5 1.5 2.5 2.5 2.5 3.5 3.5 3.5 adding the same transformation used for datatype raw (data.trial{i} = montage.tra * data.trial{i}) to the code (e.g. at line 375: sens.chanpos = montage.tra * sens.chanpos), produces: -1 -1 -1 -1 -1 -1 -1 -1 -1 which is also not correct, and makes me wonder whether it works correctly for raw data in the first place (given that both sens and data.trial are chan by something)


Robert Oostenveld - 2017-03-07 21:07:35 +0100

(In reply to Arjen Stolk from comment #7) in your first case you have the same channel names in the output as the input, therefore it is assumed that the channels remain on the same position. See line 381 in ft_apply_montage. Please note that positions are always weighted with an absolute number, so [+1 -1] is in between the two channels (+1/2 times one and +1/2 times the other), not +1 times the one position -1 times the other position. In the following case you could expect the new channel positions to be in-between. bipolar.labelold = {'1', '2', '3', '4'}; bipolar.labelnew = {'-2', '2-3', '3-4'}; bipolar.tra = [ +1 -1 0 0 0 +1 -1 0 0 0 +1 -1 ]; elec_bi = ft_apply_montage(elec, bipolar); However, they are all nan. That is something which could be fixed.


Arjen Stolk - 2017-03-07 23:46:36 +0100

Agreed with the first case. Also agreed with the purpose of the second case: The issue is that (temporarily) adding the tra matrix multiplication - similarly to how it's done with the EEG/MEG/ECoG signals - does neither lead to averaged positions nor the output of the first case. That is, and perhaps I'm doing this wrong, adding the following line sens.chanpos = montage.tra * sens.chanpos; to line 374/375 would produce: output.chanpos: 1 1 1 2 2 2 3 3 3 rather than the expected: 1.5 1.5 1.5 2.5 2.5 2.5 3.5 3.5 3.5


Arjen Stolk - 2017-03-07 23:49:18 +0100

Created attachment 830 sens.chanpos = montage.tra * sens.chanpos;


Arjen Stolk - 2017-03-07 23:50:06 +0100

>> elec.label = {'1';'2';'3';'4'}; elec.elecpos = [1 1 1; 2 2 2; 3 3 3; 4 4 4]; elec.chanpos = elec.elecpos; elec.tra = eye(4); bipolar.labelold = {'1', '2', '3', '4'}; bipolar.labelnew = {'1', '2', '3'}; bipolar.tra = [ +1 -1 0 0 0 +1 -1 0 0 0 +1 -1 ]; dat = ft_apply_montage(elec, bipolar) dat = label: {'1' '2' '3'} elecpos: [4x3 double] chanpos: [3x3 double] tra: [3x4 double] labelold: {4x1 cell} >> dat.chanpos ans = -1 -1 -1 -1 -1 -1 -1 -1 -1 rather than the expected: 1.5 1.5 1.5 2.5 2.5 2.5 3.5 3.5 3.5


Arjen Stolk - 2017-03-07 23:58:59 +0100

Sorry, I copied the wrong code. This code creates the following output, having added sens.chanpos = montage.tra * sens.chanpos; to line 375 and having commented out the nan-matrix generation at line 391/392. As you can see the new chanpos all are -1, -1, -1 and not 1.5, 1.5, 1.5, etc. elec.label = {'1';'2';'3';'4'}; elec.elecpos = [1 1 1; 2 2 2; 3 3 3; 4 4 4]; elec.chanpos = elec.elecpos; elec.tra = eye(4); bipolar.labelold = {'1', '2', '3', '4'}; bipolar.labelnew = {'1-2', '2-3', '3-4'}; bipolar.tra = [ +1 -1 0 0 0 +1 -1 0 0 0 +1 -1 ]; elec_bi = ft_apply_montage(elec, bipolar); elec_bi.chanpos ans = -1 -1 -1 -1 -1 -1 -1 -1 -1


Arjen Stolk - 2017-03-08 05:39:04 +0100

Fast forwarding: I now see you calculate the weights differently than is done for raw data farther down in ft_apply_montage (data.trial{i} = montage.tra * data.trial{i};): posweight = abs(montage.tra); posweight = diag(1./sum(posweight,2)) * posweight; sens.chanpos = posweight * sens.chanpos; This produces the expected result: ans = 1.5000 1.5000 1.5000 2.5000 2.5000 2.5000 3.5000 3.5000 3.5000 Would the same posweight matrix not also be used for (bipolar) derivation of the neural data?


Jan-Mathijs Schoffelen - 2017-03-08 08:16:44 +0100

(In reply to Arjen Stolk from comment #13) NO!


Arjen Stolk - 2017-03-08 08:19:36 +0100

(In reply to Jan-Mathijs Schoffelen from comment #14) Naturally Of_course?


Robert Oostenveld - 2017-03-08 08:44:35 +0100

(In reply to Arjen Stolk from comment #15) has been addressed in the corresponding github issue at https://github.com/fieldtrip/fieldtrip/pull/361#issuecomment-284968536 let's keep the discussions on either one of bugzilla or github, but not both.


Arjen Stolk - 2017-03-08 08:46:48 +0100

/me taps JM's fingers


Robert Oostenveld - 2017-03-08 12:21:25 +0100

I made a test script (in a branch) that reveals a problem with ft_componentanalysis followed by ft_rejectcomponent. This touches upon the handling of balance, which is not part of this bug per see. But it would be good to use this bug to also improve the overall handling of elec/grad/topo structures. I will add some bugs to the "see also" list.


Arjen Stolk - 2017-04-24 18:56:46 +0200

NaNs no more


Robert Oostenveld - 2019-08-10 12:34:47 +0200

This closes a whole series of bugs that have been resolved (either FIXED/WONTFIX/INVALID) for quite some time. If you disagree, please file a new issue on https://github.com/fieldtrip/fieldtrip/issues.


Robert Oostenveld - 2019-08-10 12:40:57 +0200

This closes a whole series of bugs that have been resolved (either FIXED/WONTFIX/INVALID) for quite some time. If you disagree, please file a new issue on https://github.com/fieldtrip/fieldtrip/issues.