From 10aaaf6d27c14f94b052da12e6ea1dabb4370a47 Mon Sep 17 00:00:00 2001 From: Nick Steinmetz Date: Fri, 22 Feb 2019 14:39:12 -0800 Subject: [PATCH] implement channel map creation and saving --- .../neuropixPhase3A_kilosortChanMap.mat | Bin 2001 -> 2069 bytes .../neuropixPhase3B1_kilosortChanMap.mat | Bin 1996 -> 2075 bytes .../neuropixPhase3B2_kilosortChanMap.mat | Bin 2043 -> 2073 bytes gui/createValidChanMap.m | 70 ++++++++++ gui/ksGUI.m | 124 +++++++++++++----- gui/loadChanMaps.m | 21 +++ gui/saveNewChanMap.m | 21 +++ 7 files changed, 203 insertions(+), 33 deletions(-) create mode 100644 gui/createValidChanMap.m create mode 100644 gui/loadChanMaps.m create mode 100644 gui/saveNewChanMap.m diff --git a/configFiles/neuropixPhase3A_kilosortChanMap.mat b/configFiles/neuropixPhase3A_kilosortChanMap.mat index 43cc1e161296926df9ff6b3b6596bd9f2fc04220..e10b44fbd563f1ae61ab31a410eacfc6fe2cd955 100644 GIT binary patch delta 120 zcmV-;0Ehq450wy*G#Ey5X&^>rVjwayATcsJGc!6fIUq7HF*%V@BavVQv2<%tQb8%vT|H%qhqVgvv? CEe&=6 diff --git a/configFiles/neuropixPhase3B1_kilosortChanMap.mat b/configFiles/neuropixPhase3B1_kilosortChanMap.mat index 012de13de1fa441430cda330608037e24421317d..48b9b594bf1ecf376cf63e253e9727347994f07d 100644 GIT binary patch delta 122 zcmV-=0EPd|51SB>G#Ey5X&^>rVjwayATcsJGc-ChF(5K9F*%V@BavVQv2FKFOsVSNOKsgQZv(5$n0aMH?=l}o! delta 46 zcmbO&aE5<^i9|?gs)BD~k%E!Af}y#Uv6+>zp@NZtp~b{N<%tQb8%yF?H!HIJVgvv= C9}Q*z diff --git a/configFiles/neuropixPhase3B2_kilosortChanMap.mat b/configFiles/neuropixPhase3B2_kilosortChanMap.mat index 109f3d68c6e30bd4b242f67d60ee750b42beadfa..f6460d113517ec09b5696271b95ee74103475521 100644 GIT binary patch delta 125 zcmV-@0D}Mf519~rVjwayATcsJGch_fIUq7HF*%V@BavVQk#rETwN3>~ zM*si-c%0*7U|2>DrA7G#nH8xy#R>r# fiN&eLPDTnEi8-0+d8sLy3=9B0Ck+g<{{{R3-BBwA delta 95 zcmV-l0HFVw5c?01G#FHMWgtyqav(A{ATc*OGdDUiH6SuDF*uP?BavVQk#rC(DF6Tf zc%0*7U|=u+Vm2V=fYKnq3gxo^F(Vi=GbH8}GcW-~xTUqN0U@9QJ+XjN1+yUr_yJ+{ B9u@!q diff --git a/gui/createValidChanMap.m b/gui/createValidChanMap.m new file mode 100644 index 00000000..d7d11bac --- /dev/null +++ b/gui/createValidChanMap.m @@ -0,0 +1,70 @@ + + +function cm = createValidChanMap(q, varargin) +cm = []; +if isfield(q, 'chanMap') && ... + isfield(q, 'xcoords') && ... + isfield(q, 'ycoords') && ... + numel(q.chanMap)==numel(q.xcoords) &&... + numel(q.chanMap)==numel(q.ycoords) + + if min(q.chanMap)==0 + q.chanMap = q.chanMap+1; + end + + % has required fields - we accept it + + cm.chanMap = q.chanMap(:); + cm.xcoords = q.xcoords(:); + cm.ycoords = q.ycoords(:); + cm.chanMap0ind = q.chanMap(:)-1; + + if isfield(q, 'connected') + if numel(q.connected)==numel(cm.chanMap) + cm.connected = q.connected(:); + else + warning('Invalid chanMap variable ''connected'': must be the same size as chanMap. Using default.'); + end + + else + cm.connected = true(size(cm.chanMap)); + end + + cm.kcoords = ones(size(cm.chanMap)); + if isfield(q, 'kcoords') + if numel(q.kcoords)==numel(cm.chanMap) + cm.kcoords = q.kcoords(:); + else + warning('Invalid chanMap variable ''kcoords'': must be the same size as chanMap. Using default.'); + end + elseif isfield(q, 'shankInd') + if numel(q.shankInd)==numel(cm.chanMap) + cm.kcoords = q.shankInd(:); + else + warning('Invalid chanMap variable ''shankInd'': must be the same size as chanMap. Using default.'); + end + end + + if isfield(q, 'name') + cm.name = q.name; + else + if nargin>1 + filename = varargin{1}; + x = strfind(filename, 'kilosortChanMap'); + if ~isempty(x) + cm.name = filename(1:x-2); + else + cm.name = filename; + end + else + cm.name = 'unknown'; + end + end + + if isfield(q, 'siteSize') + cm.siteSize = siteSize; + end + +else + warning('Invalid channel map: A valid channel map must have chanMap, xcoords, and ycoords, and they must all be the same size.') +end \ No newline at end of file diff --git a/gui/ksGUI.m b/gui/ksGUI.m index 6aed191b..713e47b9 100644 --- a/gui/ksGUI.m +++ b/gui/ksGUI.m @@ -10,14 +10,12 @@ % GUI by N. Steinmetz % % TODO: (* = before release) - % - test that path adding and compilation work on a fresh install % - allow better setting of probe site shape/size % - auto-load number of channels from meta file when possible % - update time plot when scrolling in dataview % - show RMS noise level of channels to help selecting ones to drop? % - implement builder for new probe channel maps (cm, xc, yc, name, % site size) - % - *advanced option setting % - saving of probe layouts % - plotting bug: zoom out on probe view should allow all the way out % in x @@ -28,9 +26,6 @@ % - find way to run ks in the background so gui is still usable(?) % - quick way to set working/output directory when selecting a new file % - when selecting a new file, reset everything - % - when re-loading old file and whitening matrix already exists, use - % it rather than re-compute - % - button to easily match folders to the source % - why doesn't computeWhitening run on initial load? properties @@ -85,12 +80,12 @@ function init(obj) fprintf(1, 'Success!\n'); cd(oldDir); catch ex - fprintf(1, 'Compilation failed. Check installation instructions at https://github.com/cortex-lab/Kilosort\n'); + fprintf(1, 'Compilation failed. Check installation instructions at https://github.com/MouseLand/Kilosort2\n'); rethrow(ex); end end - + obj.P.allChanMaps = loadChanMaps(); end @@ -134,7 +129,13 @@ function build(obj, f) 'String', 'Help', 'FontSize', 24,... 'Callback', @(~,~)obj.help); - obj.H.titleHBox.Sizes = [-5 -1]; + obj.H.resetButton = uicontrol(... + 'Parent', obj.H.titleHBox,... + 'Style', 'pushbutton', ... + 'String', 'Reset GUI', 'FontSize', 24,... + 'Callback', @(~,~)obj.reset); + + obj.H.titleHBox.Sizes = [-5 -1 -1]; % -- Main section obj.H.setRunVBox = uiextras.VBox(... @@ -260,16 +261,13 @@ function build(obj, f) 'Style', 'edit', 'HorizontalAlignment', 'left', ... 'String', '...', 'Callback', @(~,~)obj.updateFileSettings()); - % TODO: get list of probes + probeNames = {obj.P.allChanMaps.name}; + probeNames{end+1} = '[new]'; + probeNames{end+1} = 'other...'; obj.H.settings.setProbeEdt = uicontrol(... 'Parent', obj.H.settingsGrid,... 'Style', 'popupmenu', 'HorizontalAlignment', 'left', ... - 'String', {... - 'Neuropixels Phase3A', ... - 'Neuropixels Phase3B1 (staggered)',... - 'Neuropixels Phase3B2 (aligned)',... - '[new]', ... - 'other...'}, ... + 'String', probeNames, ... 'Callback', @(~,~)obj.updateProbeView('reset')); obj.H.settings.setnChanEdt = uicontrol(... 'Parent', obj.H.settingsGrid,... @@ -449,8 +447,9 @@ function initPars(obj) obj.restoreGUIsettings(); obj.updateProbeView('new'); obj.updateFileSettings(); - catch + catch ex obj.log('Failed to initialize last file.'); + keyboard end end else @@ -1087,30 +1086,69 @@ function updateProbeView(obj, varargin) cm = []; switch selProbe - case 'Neuropixels Phase3A' - cm = load('neuropixPhase3A_kilosortChanMap.mat'); - case 'Neuropixels Phase3B1 (staggered)' - cm = load('neuropixPhase3B1_kilosortChanMap.mat'); - case 'Neuropixels Phase3B2 (aligned)' - cm = load('neuropixPhase3B2_kilosortChanMap.mat'); case '[new]' - obj.log('New probe creator not yet implemented.'); - return; + %obj.log('New probe creator not yet implemented.'); + answer = inputdlg({'Name for new channel map:', ... + 'X-coordinates of each site (can use matlab expressions):',... + 'Y-coordinates of each site:',... + 'Shank index (''kcoords'') for each site (blank for single shank):',... + 'Channel map (the list of rows in the data file for each site):',... + 'List of disconnected/bad site numbers (blank for none):'}); + if isempty(answer) + return; + else + cm.name = answer{1}; + cm.xcoords = str2num(answer{2}); + cm.ycoords = str2num(answer{3}); + if ~isempty(answer{4}) + cm.kcoords = str2num(answer{4}); + end + cm.chanMap = str2num(answer{5}); + if ~isempty(answer{6}) + q = str2num(answer{6}); + if numel(q) == numel(cm.chanMap) + cm.connected = q; + else + cm.connected = true(size(cm.chanMap)); + cm.connected(q) = false; + end + end + cm = createValidChanMap(cm); + if ~isempty(cm) + answer = questdlg('Save this channel map for later?'); + if strcmp(answer, 'Yes') + saveNewChanMap(cm, obj); + end + else + obj.log('Channel map invalid. Must have chanMap, xcoords, and ycoords of same length'); + end + end case 'other...' [filename, pathname] = uigetfile('*.mat', 'Pick a channel map file.'); if filename~=0 % 0 when cancel %obj.log('choosing a different channel map not yet implemented.'); - cm = load(fullfile(pathname, filename)); - cm.chanMap = cm.chanMap(:); - cm.xcoords = cm.xcoords(:); - cm.ycoords = cm.ycoords(:); - cm.connected = logical(cm.connected(:)); - % ** check for all the right data, get a name for - % it, add to defaults + cm = load(fullfile(pathname, filename)); + cm = createValidChanMap(cm, filename); + if ~isempty(cm) + obj.P.allChanMaps(end+1) = cm; + currProbeList = obj.H.settings.setProbeEdt.String; + newProbeList = [{cm.name} currProbeList]; + obj.H.settings.setProbeEdt.String = newProbeList; + answer = questdlg('Save this channel map for later?'); + if strcmp(answer, 'Yes') + saveNewChanMap(cm, obj); + end + else + obj.log('Channel map invalid. Must have chanMap, xcoords, and ycoords of same length'); + return; + end else return; end + otherwise + probeNames = {obj.P.allChanMaps.name}; + cm = obj.P.allChanMaps(strcmp(probeNames, selProbe)); end nSites = numel(cm.chanMap); @@ -1429,7 +1467,10 @@ function restoreGUIsettings(obj) obj.H.settings.setProbeEdt.Value = saveDat.settings.setProbeEdt.Value; obj.H.settings.setnChanEdt.String = saveDat.settings.setnChanEdt.String; obj.H.settings.setFsEdt.String = saveDat.settings.setFsEdt.String; - obj.H.settings.setNfiltEdt.String = saveDat.settings.setNfiltEdt.String; + obj.H.settings.setThEdt.String = saveDat.settings.setThEdt.String; + obj.H.settings.setLambdaEdt.String = saveDat.settings.setLambdaEdt.String; + obj.H.settings.setCcsplitEdt.String = saveDat.settings.setCcsplitEdt.String; + obj.H.settings.setMinfrEdt.String = saveDat.settings.setMinfrEdt.String; obj.ops = saveDat.ops; obj.rez = saveDat.rez; @@ -1462,8 +1503,25 @@ function help(obj) end + function reset(obj) + % full reset: delete userSettings.mat and the settings file + % for current file. re-launch. + + delete(obj.P.settingsPath); + + [p,fn] = fileparts(obj.H.settings.ChooseFileEdt.String); + savePath = fullfile(p, [fn '_ksSettings.mat']); + delete(savePath); + + obj.P.skipSave = true; + kilosort; + + end + function cleanup(obj) - obj.saveGUIsettings(); + if ~isfield(obj.P, 'skipSave') + obj.saveGUIsettings(); + end fclose('all'); end diff --git a/gui/loadChanMaps.m b/gui/loadChanMaps.m new file mode 100644 index 00000000..d66ef890 --- /dev/null +++ b/gui/loadChanMaps.m @@ -0,0 +1,21 @@ + + +function chanMaps = loadChanMaps() + + +ksroot = fileparts(fileparts(mfilename('fullpath'))); +chanMapFiles = dir(fullfile(ksroot, 'configFiles', '*.mat')); + +idx = 1; +chanMaps = []; +for c = 1:numel(chanMapFiles) + + q = load(fullfile(ksroot, 'configFiles', chanMapFiles(c).name)); + + cm = createValidChanMap(q, chanMapFiles(c).name); + if ~isempty(cm) + if idx==1; chanMaps = cm; else; chanMaps(idx) = cm; end; + idx = idx+1; + end + +end diff --git a/gui/saveNewChanMap.m b/gui/saveNewChanMap.m new file mode 100644 index 00000000..40e1cd84 --- /dev/null +++ b/gui/saveNewChanMap.m @@ -0,0 +1,21 @@ + + +function saveNewChanMap(cm, obj) + +newName = cm.name; +if strcmp(cm.name, 'unknown') + answer = inputdlg('Name for this channel map:'); + if ~isempty(answer) && ~isempty(answer{1}) + newName = answer{1}; + else + newName = ''; + end +end +if ~isempty(newName) + ksRoot = fileparts(fileparts(mfilename('fullpath'))); + newFn = [answer{1} '_kilosortChanMap.mat']; + save(fullfile(ksRoot, 'configFiles', newFn), '-struct', 'cm'); + obj.log(['Saved new channel map: ' fullfile(ksRoot, 'configFiles', newFn)]); +else + obj.log('Could not save new channel map without a name'); +end \ No newline at end of file