-
Notifications
You must be signed in to change notification settings - Fork 2
/
SpokeModel.m
3020 lines (2348 loc) · 149 KB
/
SpokeModel.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
classdef SpokeModel < most.Model
%SPIKEGRID Application class for Spoke; all its functionality is implemplemented here
% See project wiki for both user & developer overview documentation
% See end-of-file for nitty-gritty developer notes
%% PUBLIC PROPERTIES
properties (SetObservable)
refreshRate = 5; %Refresh rate, in Hz, at which data is reviewed and newly detected spikes are plotted
thresholdType = 'rmsMultiple'; %One of {'volts' 'rmsMultiple'}. If rmsMultiple is specified, it is assumed that signals' DC is already filtered.
thresholdVal = 5; %Threshold, in volts or SD units, to use for spike detection
thresholdAbsolute = false; % Logical indicating whether threshold should be considered an absolute value
baselineStatsRefreshPeriod = 2; %Period, in seconds, at which signal baseline stats (mean & RMS) values are recomputed, when needed (e.g. for 'rmsMultiple' threshold detection)
%baselineStatsRefreshOnRetrigger = true; %Logical indicating whether RMS value should be recomputed on SpikeGL 'retriggers' (applies for 'rmsMultiple' threshold detection)
globalMeanSubtraction = false; %Logical indicating whether to compute/apply global mean subtraction
dataReadMode = 'file'; %One of {'file' 'spikeGL'}. Specifies whether to read data from file (faster, but assumes only one file) or via spikeGL function.
filterWindow = [0 inf]; %Frequency range in Hz allowed to pass, using a 1-pole Butterworth bandpass filter
tabDisplayed = 1; %Number of 'tab' -- a batch of (PLOTS_PER_TAB) channels -- currently diplayed in the spike waveform grid.
displayMode = 'waveform'; %One of {'waveform' 'raster'}. Specifies type of information to display on plot grid.
%Spike waveform display properties
horizontalRange = [-1 2] * 1e-3; %2 element vector ([pre post]) indicating times, in seconds, to display before and after threshold crossing
waveformAmpUnits = 'volts'; %One of {'volts' 'rmsMultiple'} indicating units to use for waveform plot display. Value of 'rmsMultiple' only available if thresholdType='rmsMultiple'
waveformsPerPlot = 100; %Number of waveforms to display (overlay) in each channel subplot before clearing
waveformsPerPlotClearMode = 'all'; %One of {'all' 'oldest'}. Specify waveform-clearing behavior when the waveformsPerPlot limit is reached.
waveformPlotClearPeriod = inf; % (TODO) Time, in seconds, after which to clear all or oldest waveform if no waveforms have been received
%Raster/PSTH display properties
stimStartChannel = []; %Channel number (zero-indexed) to use for signaling start of stim
stimStartThreshold = 0.5; %Value, in volts, to use for stim start signal
horizontalRangeRaster = [-1 1]; %2 element vector ([pre post]) indicating times, in seconds, to display before and after stim start threshold crossing in raster/psth plots
verticalRangeRaster = [1 inf]; %2 element vector indicating which stim numbers to display, counted from last start/reset. Set second element to Inf to specify all stimuli should be displayed.
verticalRangeRasterInfIncrement = 20; %When verticalRangeRaster(2)=Inf, specify the increment in which the number of stimuli displayed are incremented by.
stimEventTypesDisplayed = {}; %String or string cell array indicating which event type(s) to display raster/PSTH data for.
spikeRefractoryPeriod = 2e-3; %Time, in seconds, to prevent detection of spike following previously detected spike. When displayMode='waveform', this value is given by second element of horizontalRange.
psthTimeBin = 10e-3; %Time, in seconds, over which to bin spikes for PSTH summary plot
psthAmpRange = [0 120]; %Amplitude range to display, in units of spikes/second
psthTimerPeriod = inf; %Period, in seconds, at which plotPSTH() method is called automatically when displayMode='raster'
%channelSubset = inf; %DEPRECATED BY HIDDEN DISPLAYCHANNELS PROP Subset of channels to acquire from
% The following are properties that were SetAccess=protected, but
% have been moved out of protected to allow config file saves with
% most.
refreshPeriodMaxWaveformRate = inf; %Maximum waveform rate (Hz) to detect/plot for each refresh period; spikes/stimuli above this rate are discarded (in spike-/stim-triggered waveform modes, respectively)
% The following are properties that back up dependent properties.
% This is for properties that need to be saved to disk.
verticalRange_; % backs up property verticalRange
end
properties (SetObservable, Transient)
stimEventClassifyFcn = []; %Function handle specifying function used to classify stimuli into event types. See class notes for further information.
end
properties (SetObservable, Dependent)
verticalRange; %2 element vector ([min max]) indicating voltage bounds or RMSMultiple (depending on thresholdType) for each spike plot
refreshPeriodMaxNumWaveforms = inf; %Maximum number of spikes to detect/plot during a given refresh period
numAuxChans; %Number of auxiliary
end
properties (Dependent)
numTabs;
stimEventCount; %Count of stimuli that have been detected since start/restart/rollover at current stimEventTypesDisplayed (when displayMode='raster')
end
%Following are determined at start of acquisition, via the
%stimEventClassifyFcn (no arg case). This function must return a structure
%with fields stimEventTypes & stimEventClassifyTime.
properties (SetAccess=protected)
stimEventTypes; %String cell array indicating names of distinct event types which can be signaled by stimulus start pulse. If empty, event Classify is not used
stimEventClassifyNumScans = 0; %Number of scans required to classify event type using stimulus start signal.
stimEventClassifyChannel = []; %SpikeGL (demuxed) channel number containing signal used for stimulus event classification (immediately following stimulus detection). Must be an auxiliary channel. If empty, the stimStartChannel is used.
stimTotalCount = 0; %Count of stimuli that have been detected since start/restart/rollover, for /all/ stimEventTypes (when displayMode='raster')
configFileName = ''; %Full filename (incl. path) of last-saved/loaded config file, if any
end
properties (SetAccess=private,SetObservable)
running = false;
end
%% HIDDEN PROPERTIES
properties (Hidden)
maxBufSizeSeconds = 1;
end
properties (Hidden,SetAccess=protected)
hSGL; %Handle to SpikeGLX object
sglParamCache; %cached SpikeGLX parameters
hTimer; %Timer to use for periodically checking Spoke's input data stream
hPSTHTimer; %Timer to periodically call plotPSTH() method
%Following structs have 3 keys {'waveform' 'raster' 'psth'}
hFigs; %Struct of handles to grid figure
hPanels; %Struct of handles to subpanels in grid figure, one for each channel per tab
hChanLabels; %Struct of arrays of text labels, identifying each channel
hButtons; %Struct of handles to buttons on the waveform and raster grids.
%Handle graphics specific to waveform display
hPlots; %Array of axes handles, one for each axes in grid
hThresholdLines; %Cell array of line handles, marking threshold level for each plot in grid
hWaveforms; %Array of animated line objects for each plotted waveform
%Handle graphics specific to raster display
hRasters; %Array of axes handles, one for each axes in grid
hPSTHs; %Array of axes handles, one for each axes in grid
hRasterLines; %Cell array of arrays, one per stimEventType, containing animated line objects for each raster plot in grid
numActiveTabs;
%neuralChans; %Acquisition channel numbers corresponding to neural inputs. Ordered as in SpikeGLX connection.
%auxChans; %Acquisition channel numbers corresponding to auxiliary inputs, i.e. suited for gating/stimulus. These are /not/ displayed. Ordered as in SpikeGLX connection.
sglChanSubset; %subset of SpikeGLX channels (includes neural and non-neural channels)
neuralChanDispOrder; %Order in which neuralChans should be displayed. Includes all acquired neural chans, whether included in subset or not.
neuralChanAcqList; % List of neural chans included in SpikeGLX chan subset
neuralChanDispList; %Ordered list of neural chans displayed. Display ordering applied. Channel subset, if any, is applied.
auxChanProcList; %List of auxiliary chans processed. Channel subset, if any, is applied.
baselineRMS; %Array of RMS values, one per channel, computed from all non-spike-window scans from within last baselineRMSTime
baselineMean; %Array of mean values, one per channel, computed from all non-spike-window scans from within last baselineRMSTime
baselineRMSLastScan = 0; %Last scan number at which threshold RMS value was updated
reducedData = {}; %Cell array, one cell per available neural channel, of structures storing reduced data from the full data (timestamps, waveform snippets, stimulus-tagged timestamps), based on detected stimuli and/or spike(s)
lastPlottedWaveformCount = []; %Scalar array specifying the spike count, for each channel that's been last plotted
lastPlottedWaveformCountSinceClear = []; %Scalar array specificying the spike count, for each channel that's been last plotted (cleared every time a channel is cleared) - only used for 'all' plot clear mode.
%globalMean; %Global mean value across acquisition channels, computed from all non-spike-window scan from within last baselineRMSTime, if globalMeanSubtraction=true is nonempty
%baselineRMSExcludedScans; %Array of numbers, one per channel, indicating number of scans included into RMS calculation, from within last baselineRMSTime
%baselineRMSNumSamples;
bufScanNumEnd; %Scan number of the last element in the fullDataBuffer
fullDataBuffer; %Array to cache full channel data to process during each timer cycle. Grows & contracts each cycle.
voltsPerBitAux; %Scaling factor between A/D values and voltage for Auxiliary Channels
voltsPerBitNeural; %Scaling factor between A/D values and voltage for Neural Channels
filterCoefficients = {}; %Cell array of a,b filter coefficients for current filterWindow
filterCondition; %Maintain 'initial condition' of filter between calls to filter()
tabChanNumbers; %Array of pad channel numbers (one-based) for currently specified tabDisplayed value
blockTimer = false; %Flag used to block timer callback actions during certain operations
maxReadableScanNum = 0; %Maximum scan number in currently logged-to file counted since start of acquisition (count uninterrupted across start & retrigger gaps)
lastMaxReadableScanNum = 0; %Value of maximum scan number in currently logged-to file, measured from start of file, at /last/ timer period
priorfileMaxReadableScanNum = 0; %Value of maximum scan number, measured from start of acquisition (uninterrupted count), reached for /prior/ file
%Following arrays are all the same length (could be a structure, but kept separate for ease of access/indexing)
stimScanNums; %Array of scan numbers corresponding to detected stimulus edges
stimWindowStartScanNums; %Array of scan numbers marking starting edge of stimulus display events
stimWindowEndScanNums; %Array of scan numbers marking ending edge of stimulus display events
stimEventTypeNames = {}; %Cell string array of event type names
stimNumsPlotted; %Structure array, of size neuralChansAvailable and with fields given by stimEventTypeNames, indicating number of stims that have been plotted so far for each event
stimLastEventScanNumWindow; %1x2 array indicating start/end scan numbers for last stimulus trial
stimEventCount_; %Struct var containing stimEventCount value for each of the stimEventTypes_
bmarkReadTimeStats = zeros(3,1); %Array of [mean std n]
bmarkPreProcessTimeStats = zeros(3,1);; %Array of [mean std n]
bmarkPlotTimeStats = zeros(3,1);; %Array of [mean std n]
bmarkPostProcessTimeStats = zeros(3,1);; %Array of [mean std n]
%waveformWrap/partialWaveformBuffer props handle edge-cases in stim-triggered waveform mode. they could potentially be used for spike-triggered waveform mode.
waveformWrap = []; %waveform detected towards end of timer processing period; specifies number of samples in the next processing period needed to complete the waveform
partialWaveformBuffer = {}; % Buffer that holds part of a waveform from prior processing period. Used when waveformWrap is true.
verticalRangeCache; % A struct used to persist the last value used for verticalRange in each mode.
% Debug related properties.
diagramm; %Temporary figure for debugging.
diagrammm; %Temporary figure 2 for debugging.
debug = false; % Set to true to enable debugging.
end
properties (Hidden, SetAccess=immutable)
sglIPAddress; %IP Address for remote control connection to SpikeGLX
%Number of logical chans of each of the sub-types available, as configured via the SpikeGLX NI Configuration
neuralChansAvailable;
analogMuxChansAvailable;
analogSoloChansAvailable;
end
properties (SetAccess=protected,Hidden,SetObservable,AbortSet)
maxNumWaveformsApplied = false; %Logical indicating if the refreshPeriodMaxNumWaveforms clamp was applied for any channel on the last refresh period
end
properties (Hidden, Dependent)
horizontalRangeScans; %horizontalRange in 'scan' units, which correspond to A/D 'scans' (a sample for each channel). Note that the window includes this # of pre & post scan, PLUS one additional scan for the spike itself
baselineStatsRefreshPeriodScans; %baselineStatsRefreshPeriod in scan units
maxBufSizeScans; %Maximum number of scans to process during a refresh period
refreshPeriodAvgScans; %Average number of scans to process at each refresh period
displayModeAxes; %Returns either hPlots/hRaster, depending on displayMode value
stimEventTypes_; %Returns {'allstim'} in case that stimEventTypes is empty
gridFigPosition; %Figure position of raster/waveform grid figures (same position for both..only one shown at a time)
psthFigPosition; %Figure position of PSTH grid figure
maxPointsPerAnimatedLine; %Used to set the number of max points per animated line.
%neuralChanAcqList; %Used to get the number of neural channels (used instead of sglChanSubset, which returns all channels, not just MN chans)
end
%Constants
% ********** NOTES *******
% 1. Is it possible to change plot order in tab to 2x16 (column)
% 2. Can we do 2x64?
% 3.
properties (Hidden,Constant)
PLOTS_PER_TAB = 32;
MAX_NUM_TABS = 8;
INIT_RMS_THRESHOLD = 10; %Initial rmsMultiple to use when detecting spikes to exclude from initial RMS value determination
RASTER_DISP_STIM_RANGE_INCREMENT_FRACTION = 0.85; %fraction of verticalRangeRasterInfIncrement which must be reached before display range is auto-incremented.
SGL_BITS_PER_SAMPLE = 16; %A constant from SpikeGLX currently; should perhaps update SpikeGL to pull this from the DAQmx API
end
%% CONSTRUCTOR/DESTRUCTOR
methods
function obj = SpokeModel(sglIPAddress)
%Process inputs
obj.sglIPAddress = sglIPAddress;
obj.hSGL = SpikeGL(sglIPAddress);
obj.sglParamCache = GetParams(obj.hSGL); %initialize SpikeGL param cache
%Create class-data file
s.lastConfigFileName = '';
obj.ensureClassDataFile(s,mfilename('class'));
%Initialize resources
obj.ziniCreateGrids(); %Create spike waveform grid figure
obj.hTimer = timer('Name','Spoke Waveform Grid Timer','ExecutionMode','fixedRate','TimerFcn',@obj.zcbkTimerFcn,'BusyMode','queue','StartDelay',0.1);
obj.hPSTHTimer = timer('Name','Spoke Plot PSTH Timer','ExecutionMode','fixedRate','TimerFcn',@(src,evnt)obj.plotPSTH,'BusyMode','drop','StartDelay',0.1);
%Immutable prop initializations
[obj.neuralChansAvailable, obj.analogMuxChansAvailable, obj.analogSoloChansAvailable] = obj.zprvGetAvailAcqChans();
%Programmatic prop intializations
aiRangeMax = obj.sglParamCache.niAiRangeMax;
niMNGain = obj.sglParamCache.niMNGain;
niMAGain = obj.sglParamCache.niMAGain;
%obj.voltsPerBitNeural = aiRangeMax / 2^(obj.SGL_BITS_PER_SAMPLE - 1) / niMNGain;
obj.voltsPerBitNeural = (aiRangeMax / niMNGain) / ( 2^(obj.SGL_BITS_PER_SAMPLE - 1));
obj.voltsPerBitAux = (aiRangeMax / niMAGain) / ( 2^(obj.SGL_BITS_PER_SAMPLE - 1));
obj.refreshRate = obj.refreshRate; %apply default value
obj.zprvResetReducedData();
%Initialize a default display for appearances (some aspects gets overridden by processing on start())
% obj.sglChanSubset = GetChannelSubset(obj.hSGL); %channel subset as specified in SpikeGLX. Wierd - this /has/ to be done here, outside of zprvZpplyChanOrderAndSubset() to avoid a hang.
% - MOVED TO zprvApplyChanOrderAndSubset. obj.sglChanSubset = GetSaveChansNi(obj.hSGL); %channel subset as specified in SpikeGLX. Wierd - this /has/ to be done here, outside of zprvZpplyChanOrderAndSubset() to avoid a hang.
obj.zprvApplyChanOrderAndSubset();
numNeuralChans = numel(obj.neuralChansAvailable);
obj.hThresholdLines = repmat({ones(numNeuralChans,1) * -1},2,1);
%Allocate spike waveforms & raster plots
obj.hWaveforms = gobjects(numNeuralChans,1);
for i=1:obj.PLOTS_PER_TAB
obj.hWaveforms(i) = animatedline('Parent',obj.hPlots(i),'Color','k','MaximumNumPoints',Inf,'Marker','.','MarkerSize',3,'LineStyle','-');
end
obj.zprvInitializeRasterGridLines();
obj.verticalRange = [-aiRangeMax aiRangeMax] / 1000; % Use millivolts for spikes.
obj.tabDisplayed = 1;
%Clean-up
Close(obj.hSGL);
obj.hSGL = [];
end
function initialize(obj)
initialize@most.Model(obj);
if ~isempty(obj.getClassDataVar('lastConfigFileName'))
obj.loadConfig();
end
end
function delete(obj)
%delete(obj.hFigs); %Why doesn't this happen automatically?
% Can't do the above because Matlab won't let you delete handle struct.
obj.quit();
end
end
methods (Hidden)
%Helper methods
function ziniCreateGrids(obj)
numNeuralChans = numel(obj.neuralChansAvailable);
obj.numActiveTabs = ceil(numNeuralChans/obj.PLOTS_PER_TAB);
assert(obj.numActiveTabs <= obj.MAX_NUM_TABS,'Exceeded maximum number of tabs (%d)',obj.MAX_NUM_TABS); %TODO: Deal more gracefully
%This is a good idea...but let's keep it simple for now
% if obj.numActiveTabs > 1
% numPlots = obj.PLOTS_PER_TAB;
% else
% numPlots = numDispChans;
% end
numPlots = obj.PLOTS_PER_TAB;
gridDimension = ceil(sqrt(numPlots));
gridPanelSize = 1/gridDimension;
%Create waveform & raster grids
obj.hFigs.waveform = most.idioms.figureScaled(1.6,'Name','Spoke Waveform Grid','CloseRequestFcn',@(src,evnt)set(src,'Visible','off'));
obj.hFigs.raster = most.idioms.figureScaled(1.6,'Name','Spoke Raster Grid','CloseRequestFcn',@(src,evnt)set(src,'Visible','off'));
obj.hFigs.psth = most.idioms.figureScaled(1.6,'Name','Spoke PSTH Grid','Visible','off','CloseRequestFcn',@(src,evnt)set(src,'Visible','off'));
structfun(@(hFig)set(hFig,'NumberTitle','off','Menubar','none','Toolbar','none'),obj.hFigs);
set([obj.hFigs.waveform obj.hFigs.raster obj.hFigs.psth],'Units','normalized');
%Add pushbuttons to the waveform and raster grids.
obj.hButtons.toolbar = uitoolbar(obj.hFigs.waveform);
% Read an image
[img,map] = imread(fullfile(matlabroot, 'toolbox','matlab','icons','matlabicon.gif'));
% Convert image from indexed to truecolor
icon = ind2rgb(img,map);
obj.hButtons.screenshot = uipushtool(obj.hButtons.toolbar,'TooltipString','Click to Save a Screenshot',...
'ClickedCallback',...
'hGrid.zprvSnapshot');
obj.hButtons.pause = uipushtool(obj.hButtons.toolbar,'TooltipString','Click to Pause or Unpause Operation',...
'ClickedCallback',...
'hGrid.zprvPause');
% Set the button icon
obj.hButtons.screenshot.CData = icon;
obj.hButtons.pause.CData = icon;
%TODO: Use gobjects
for i=1:numPlots
%Place panel from top-left, counting right and then down
rowCount = ceil(i/gridDimension);
colCount = mod(i-1,gridDimension) + 1;
panelPosn = [(colCount-1)*gridPanelSize (gridDimension-rowCount)*gridPanelSize gridPanelSize gridPanelSize];
obj.hPanels.waveform(i) = uipanel(obj.hFigs.waveform,'Position',panelPosn);
obj.hPanels.raster(i) = uipanel(obj.hFigs.raster,'Position',panelPosn);
obj.hPanels.psth(i) = uipanel(obj.hFigs.psth,'Position',panelPosn);
%Places axes in panel and configure
obj.hPlots(i) = axes('Parent',obj.hPanels.waveform(i),'Position',[0 0 1 1],'XLim',obj.horizontalRange); %,'YLim',obj.verticalRange);
obj.hRasters(i) = axes('Parent',obj.hPanels.raster(i),'Position',[0 0 1 1],'XLim',obj.horizontalRangeRaster);
obj.hPSTHs(i) = axes('Parent',obj.hPanels.psth(i),'Position',[0 0 1 1]);
%TODO: Remove this function & just set here
obj.zprvSetAxesProps(obj.hPlots(i));
obj.zprvSetAxesProps(obj.hRasters(i));
obj.zprvSetAxesProps(obj.hPSTHs(i));
end
obj.hChanLabels = struct('waveform',[],'raster',[],'psth',[]);
end
end
%% PROPERTY ACCESS
methods
function set.dataReadMode(obj,val)
obj.zprpAssertNotRunning('dataReadMode');
obj.validatePropArg('dataReadMode',val);
obj.dataReadMode = val;
end
function set.displayMode(obj,val)
obj.zprpAssertNotRunning('displayMode');
obj.validatePropArg('displayMode',val);
%Impose requirements before allowing switch to 'raster' mode
disallowRaster = false;
if strcmpi(val,'raster')
if isempty(obj.stimStartChannel)
fprintf(2,'WARNING: Must specify stimStartChannel in order to use spike raster display mode\n');
disallowRaster = true;
end
if ~isempty(obj.stimEventTypes) && isempty(obj.stimEventTypesDisplayed)
fprintf(2,'WARNING: A valid stimEventTypesDisplayed value must be specified in order to use spike raster display mode\n');
disallowRaster = true;
end
if disallowRaster
val = 'waveform';
end
end
obj.displayMode = val;
obj.zprvShowDisplayFig();
%Side-effects
obj.zprvResetReducedData();
obj.tabDisplayed = obj.tabDisplayed;
end
function set.filterWindow(obj,val)
obj.validatePropArg('filterWindow',val);
assert(val(2) > val(1),'Specified filter window must contain 2 elements in ascending order, specifying lower and upper frequency bound');
obj.filterWindow = val;
%Side-effects
if isequal(val(:),[0;inf])
obj.filterCoefficients = {};
else
if val(1) == 0
[b,a] = butter(1,obj.filterWindow(2) * 2 / obj.sglParamCache.niSampRate,'low');
elseif isinf(val(2)) %high-pass filter
[b,a] = butter(1,obj.filterWindow(1) * 2 / obj.sglParamCache.niSampRate,'high');
else
[b,a] = butter(1,obj.filterWindow * 2 / obj.sglParamCache.niSampRate,'bandpass');
end
obj.filterCoefficients = {a b};
end
end
function set.globalMeanSubtraction(obj,val)
obj.validatePropArg('globalMeanSubtraction',val);
obj.globalMeanSubtraction = val;
end
function val = get.displayModeAxes(obj)
switch obj.displayMode
case 'raster'
val = obj.hRasters;
case 'waveform'
val = obj.hPlots;
end
end
function val = get.gridFigPosition(obj)
val = get(obj.hFigs.(obj.displayMode),'Position');
end
function set.gridFigPosition(obj,val)
set(obj.hFigs.(obj.displayMode),'Position',val);
end
function val = get.maxBufSizeScans(obj)
val = round(obj.maxBufSizeSeconds * obj.sglParamCache.niSampRate);
end
function val = get.psthFigPosition(obj)
val = get(obj.hFigs.psth,'Position');
end
function set.psthFigPosition(obj,val)
set(obj.hFigs.psth,'Position',val);
end
function set.psthTimeBin(obj,val)
obj.validatePropArg('psthTimeBin',val);
obj.psthTimeBin = val;
end
function set.psthTimerPeriod(obj,val)
obj.validatePropArg('psthTimerPeriod',val);
changeWhileRunning = strcmpi(get(obj.hPSTHTimer,'Running'),'on');
if changeWhileRunning
stop(obj.hPSTHTimer);
end
if ~isinf(val)
set(obj.hPSTHTimer,'Period',val,'StartDelay',val);
if changeWhileRunning
start(obj.hPSTHTimer);
end
end
obj.psthTimerPeriod = val;
end
function set.psthAmpRange(obj,val)
obj.validatePropArg('psthAmpRange',val);
set(obj.hPSTHs,'YLim',val);
obj.psthAmpRange = get(obj.hPSTHs(1),'YLim');
end
function val = get.numTabs(obj)
numNeuralChans = numel(obj.neuralChansAvailable);
val = ceil(numNeuralChans/obj.PLOTS_PER_TAB);
end
function val = get.refreshPeriodMaxNumWaveforms(obj)
val = obj.refreshPeriodMaxWaveformRate / obj.refreshRate;
end
function set.refreshPeriodMaxWaveformRate(obj,val)
obj.validatePropArg('refreshPeriodMaxWaveformRate',val);
lclVar = ceil(val / obj.refreshRate);
obj.refreshPeriodMaxWaveformRate = lclVar * obj.refreshRate;
end
function val = get.refreshPeriodAvgScans(obj)
val = round(get(obj.hTimer,'Period') * obj.sglParamCache.niSampRate);
end
%
% function val = get.neuralChanAcqList(obj)
% % obj.sglChanSubset does not discriminate between neural, aux, and other types of channels.
% % return obj.sglChanSubset's neural chans only. hGrid.sglParamCache.niMNChans1, hGrid.sglParamCache.niMNChans2
% lclMNChans = str2num(num2str(obj.sglParamCache.niMNChans1));
% lclChanLim = (max(lclMNChans) + 1) * obj.sglParamCache.niMuxFactor; % DO NOT HARDCODE THIS TO NUMBER OF WAVEFORMS PER TAB.
% %val = obj.sglChanSubset;
% val = obj.sglChanSubset(obj.sglChanSubset < lclChanLim);
% end
function set.refreshRate(obj,val)
obj.zprpAssertNotRunning('refreshRate');
obj.validatePropArg('refreshRate',val);
%Ensure value does not exceed the processing refresh period
hrng = diff(obj.horizontalRange);
f_samp = obj.sglParamCache.niSampRate;
assert(ceil(hrng * f_samp) < floor(f_samp/val),'horizontalRange must be shorter than the processing refresh period');
refreshPeriodRounded = round(1e3 * 1/val) * 1e-3; %Make an integer number of milliseconds
set(obj.hTimer,'Period',refreshPeriodRounded);
currMaxSpikeRate = obj.refreshPeriodMaxWaveformRate;
obj.refreshRate = 1/get(obj.hTimer,'Period');
%Side-effects
obj.refreshPeriodMaxWaveformRate = currMaxSpikeRate;
end
function val = get.horizontalRangeScans(obj)
val = round(obj.horizontalRange * obj.sglParamCache.niSampRate);
end
function val = get.stimEventTypes_(obj)
if isempty(obj.stimEventTypes)
val = {'allstim'};
else
val = obj.stimEventTypes;
end
end
function val = get.stimEventClassifyChannel(obj)
if isempty(obj.stimEventClassifyChannel)
val = obj.stimStartChannel;
else
val = obj.stimEventClassifyChannel;
end
end
function val = get.stimEventCount(obj)
if isempty(obj.stimEventCount_) %Acq hasn't yet been started
for i=1:length(obj.stimEventTypes_)
obj.stimEventCount_.(obj.stimEventTypes_{i}) = 0;
end
end
if isempty(obj.stimEventTypes)
val = obj.stimEventCount_.allstim;
else
val = sum(cellfun(@(eventType)obj.stimEventCount_.(eventType),obj.stimEventTypesDisplayed));
end
end
function set.waveformAmpUnits(obj,val)
obj.validatePropArg('waveformAmpUnits',val);
% Save old value of vertical range to cache.
oldVal = obj.waveformAmpUnits;
obj.verticalRangeCache.(obj.waveformAmpUnits) = obj.verticalRange;
% Update the property value.
obj.waveformAmpUnits = val;
% Check to see if cached value exists in struct.
if isfield(obj.verticalRangeCache,(val))
%Restore cached value of vertical range if it exists.
obj.verticalRange = obj.verticalRangeCache.(val);
end
end
function val = get.verticalRange(obj)
val = get(obj.hPlots(1),'YLim');
end
function set.verticalRange(obj,val)
obj.validatePropArg('verticalRange',val);
if strcmpi(obj.waveformAmpUnits,'volts');
aiRangeMax = obj.sglParamCache.niAiRangeMax;
if any(abs(val) > 1.1 * aiRangeMax)
warning('Specified range exceeded input channel voltage range by greater than 10% -- spike amplitude axis limits clamped');
val = min(val,1.1 * aiRangeMax);
val = max(val,-1.1 * aiRangeMax);
end
end
set(obj.hPlots,'YLim',val);
%Set real property
obj.verticalRange_ = val;
end
function set.verticalRange_(obj,val)
%force recalc of dependent property
obj.verticalRange_ = val;
end
function set.horizontalRange(obj,val)
obj.zprpAssertNotRunning('horizontalRange');
obj.validatePropArg('horizontalRange',val);
%Ensure value does not exceed processing refresh period
dval = diff(val);
f_samp = obj.sglParamCache.niSampRate;
assert(dval > 0,'Horizontal range must be specified from minimum to maximum');
assert(ceil(dval * f_samp) < floor(f_samp/obj.refreshRate),'horizontalRange must be shorter than the processing refresh period');
obj.horizontalRange = val;
%Side-effects
set(obj.hPlots,'XLim',val);
end
function val = get.maxPointsPerAnimatedLine(obj)
%Calculate MaximumNumPoints
if strcmp(obj.waveformsPerPlotClearMode,'oldest')
spikeSampleRate = obj.sglParamCache.niSampRate;
numPointsPerWindow = spikeSampleRate * (obj.horizontalRange(2)-obj.horizontalRange(1));
val = ceil(obj.waveformsPerPlot * numPointsPerWindow);
else
val = Inf;
end
end
% function val = get.sglChanSubset(obj)
% val = GetSaveChansNi(obj.hSGL); %channel subset as specified in SpikeGLX. Wierd - this /has/ to be done here, outside of zprvZpplyChanOrderAndSubset() to avoid a hang.
% end
% Why does setting refreshPeriodMaxWaveformRate > refreshRate cause
% waveformsPerPlot to be bypassed? Even if I set the
% waveformsPerPlot to 1, if refreshPeriodMaxWaveformRate = 16 and
% refreshRate = 4, I get 4 lines per plot - and also my waveform
% processing gets screwed up.
% well, apparently, it has to do with some kind of max waveform
% processing in zlcldetectspikes.
function set.waveformsPerPlot(obj,val)
obj.validatePropArg('waveformsPerPlot',val);
obj.waveformsPerPlot = val;
end
function set.waveformsPerPlotClearMode(obj,val)
obj.zprpAssertNotRunning('waveformsPerPlotClearMode');
obj.validatePropArg('waveformsPerPlotClearMode',val);
obj.waveformsPerPlotClearMode = val;
%side-effects
%TODO: add check here to reset max points in animatedline when changed to anything but 'oldest'.
%TODO: add code to recompute max points on animatedlines when changed to 'oldest'.
obj.zprvClearPlots('waveform');
end
function val = get.spikeRefractoryPeriod(obj)
switch obj.displayMode
case 'waveform'
val = obj.horizontalRange(2);
case 'raster'
val = obj.spikeRefractoryPeriod;
end
end
function set.spikeRefractoryPeriod(obj,val)
obj.validatePropArg('spikeRefractoryPeriod',val);
oldVal = obj.spikeRefractoryPeriod;
switch obj.displayMode
case 'waveform'
if obj.mdlInitialized && ~isequal(oldVal,val)
fprintf(2,'WARNING: When displayMode = ''waveform'', the ''spikeRefractoryPeriod'' cannot be directly set. Value specified ignored.\n')
end
case 'raster'
obj.spikeRefractoryPeriod = val;
end
end
% function val = get.stimEventTypes(obj)
% if isempty(obj.stimEventClassifyFcn)
% val = '';
% else
% val = feval(obj.stimEventClassifyFcn);
% end
% end
function set.stimEventTypesDisplayed(obj,val)
assert(ischar(val) || iscellstr(val),'Value of ''stimEventTypesDisplayed'' must be either a string or string cell array');
if isempty(obj.stimEventTypes)
assert(isempty(val),'Value of ''stimEventTypesDisplayed'' must be empty when ''stimEventTypes'' is empty');
return;
else
assert(~isempty(val),'Valid value of ''stimEventTypesDisplayed'' must be supplied.');
end
if ~iscell(val)
val = {val};
end
assert(all(ismember(val,obj.stimEventTypes)),'One or more of the specified stim event types not recognized');
oldVal = obj.stimEventTypesDisplayed;
obj.stimEventTypesDisplayed = val;
%Side-effects
if ~isequal(oldVal,val) && strcmpi(obj.displayMode,'raster')
obj.zprvClearPlots('raster');
if obj.running
obj.zprvUpdateRasterPlot();
end
end
end
function set.stimEventClassifyFcn(obj,val)
obj.zprpAssertNotRunning('stimEventClassifyFcn');
%Custom validation
isFcn = isscalar(val) && isa(val,'function_handle');
assert(isempty(val) || isFcn, 'Property ''stimEventClassifyFcn'' must be a function handle (or empty)');
errorString = '';
if isFcn
try
s = feval(val);
obj.stimEventTypes = s.stimEventTypes;
obj.stimEventClassifyNumScans = s.stimEventClassifyNumScans;
if isfield(s,'stimEventClassifyChannel')
obj.stimEventClassifyChannel = s.stimEventClassifyChannel;
else
obj.stimEventClassifyChannel = [];
end
if isempty(obj.stimEventTypes)
obj.stimEventTypesDisplayed = '';
elseif isempty(obj.stimEventTypesDisplayed) || ~all(ismember(obj.stimEventTypesDisplayed,obj.stimEventTypes))
obj.stimEventTypesDisplayed = obj.stimEventTypes{1};
end
obj.stimEventCount_ = struct();
for i=1:length(obj.stimEventTypes)
obj.stimEventCount_.(obj.stimEventTypes{i}) = 0;
end
catch ME
errorString = 'Specified ''stimEventClassifyFcn'' must return structure with fields ''stimEventTypes'' and ''stimEventClassifyNumScans'' when called with no arguments.';
end
end
if ~isFcn || ~isempty(errorString)
val = [];
obj.stimEventTypes = '';
obj.stimEventClassifyNumScans = 0;
obj.stimEventCount_ = struct();
obj.stimEventCount_.allstim = 0;
end
if errorString
error(errorString);
else
obj.stimEventClassifyFcn = val;
end
%Side-effects
obj.zprvInitializeRasterGridLines(); %Initialize raster grid animated line objects
end
function set.verticalRangeRaster(obj,val)
obj.validatePropArg('verticalRangeRaster',val);
ylim = obj.zprvverticalRangeRaster2YLim(val);
set(obj.hRasters,'YLim',ylim);
obj.verticalRangeRaster = val;
end
function set.verticalRangeRasterInfIncrement(obj,val)
obj.validatePropArg('verticalRangeRasterInfIncrement',val);
obj.verticalRangeRasterInfIncrement = val;
end
function set.stimStartChannel(obj,val)
obj.zprpAssertNotRunning('stimStartChannel');
obj.validatePropArg('stimStartChannel',val);
obj.zprpAssertAuxChan(val,'stimStartChannel'); %Assert any stimulus channel specified is a valid auxiliary channel
obj.stimStartChannel = val;
end
function set.stimStartThreshold(obj,val)
obj.validatePropArg('stimStartThreshold',val);
obj.stimStartThreshold = val;
end
function set.horizontalRangeRaster(obj,val)
obj.zprpAssertNotRunning('horizontalRangeRaster');
obj.validatePropArg('horizontalRangeRaster',val);
obj.zprvClearPlots('raster');
set(obj.hRasters,'XLim',val);
obj.zprvClearPlots('psth');
obj.horizontalRangeRaster = val;
end
function set.tabDisplayed(obj,val)
obj.validatePropArg('tabDisplayed',val);
assert(val <= obj.numTabs,'Value specified (%d) exceeds the number of available tabs (%d)',val,obj.numTabs);
obj.zprvAssertAvailChansConstant();
obj.blockTimer = true;
try
obj.tabDisplayed = val;
dispType = obj.displayMode;
%Update tabChanNumbers
numNeuralChans = numel(obj.neuralChansAvailable); %#ok<*MCSUP>
tcn = (1:obj.PLOTS_PER_TAB) + (val-1)*obj.PLOTS_PER_TAB;
tcn(tcn > numNeuralChans) = [];
obj.tabChanNumbers = tcn; %One-based channel index
%Clear grid waveform/raster plots
obj.zprvClearPlots({obj.displayMode 'psth'},false); %Don't reuse existing threshold lines
%Update plot channel labels
most.idioms.deleteHandle(obj.hChanLabels.(dispType));
most.idioms.deleteHandle(obj.hChanLabels.psth);
hAxes = obj.displayModeAxes;
for i=1:length(tcn)
actualChannelNumber = tcn(i) - 1;
if ~isequal(obj.neuralChanDispOrder, obj.neuralChansAvailable)
%Use channel mapping numbers (if they exist)
tempChannelNumber = obj.neuralChanDispOrder( tcn(i) );
if tempChannelNumber ~= tcn(i) - 1
channelNumberString = strcat(num2str(tempChannelNumber),' (',num2str(tcn(i) - 1),')');
textPosition = [.13 .92];
else
channelNumberString = num2str(tcn(i) - 1);
textPosition = [.08 .92];
end
else
%Display with 0-based channel index
tempChannelNumber = tcn(i) - 1;
channelNumberString = num2str(tcn(i) - 1);
textPosition = [.08 .92];
end
obj.hChanLabels.(dispType)(i) = text('Parent',hAxes(i),'String',channelNumberString,'HandleVisibility','off','FontWeight','bold','Color','k','Units','normalized','Position',textPosition,'HorizontalAlignment','center');
obj.hChanLabels.psth(i) = text('Parent',obj.hPSTHs(i),'String',channelNumberString,'HandleVisibility','off','FontWeight','bold','Color','k','Units','normalized','Position',textPosition,'HorizontalAlignment','center');
end
%Update threshold lines/raster plots, as appropriate
if strcmpi(obj.displayMode,'raster')
obj.zprvUpdateRasterPlot();
end
drawnow expose update;
obj.blockTimer = false;
catch ME
obj.blockTimer = false;
ME.rethrow();
end
end
function set.thresholdAbsolute(obj,val)
obj.validatePropArg('thresholdAbsolute',val);
obj.thresholdAbsolute = val;
%Side-effects
obj.zprvDrawThresholdLines();
end
function val = get.baselineStatsRefreshPeriodScans(obj)
val = round(obj.baselineStatsRefreshPeriod * obj.sglParamCache.niSampRate);
end
function set.thresholdType(obj,val)
obj.zprpAssertNotRunning('thresholdType');
obj.validatePropArg('thresholdType',val);
oldVal = obj.thresholdType;
obj.thresholdType = val;
%Side Effects
if ~strcmpi(oldVal,val)
%Adjust thresholdVal & verticalRange
%TODO(?): A smarter adjustment based on the last-cached RMS values, somehow handlign the variety across channels
switch val
case 'volts'
aiRangeMax = obj.sglParamCache.niAiRangeMax;
obj.thresholdVal = .1 * aiRangeMax;
case 'rmsMultiple'
obj.thresholdVal = 5;
end
%Redraw threshold lines
obj.zprvDrawThresholdLines();
end
end
function set.thresholdVal(obj,val)
%obj.zprpAssertNotRunning('thresholdVal');
obj.validatePropArg('thresholdVal',val);
obj.thresholdVal = val;
%Side-effects
obj.zprvDrawThresholdLines();
end
function set.baselineStatsRefreshPeriod(obj,val)
obj.zprpAssertNotRunning('baselineStatsRefreshPeriod');
obj.validatePropArg('baselineStatsRefreshPeriod',val);
obj.baselineStatsRefreshPeriod = val;
end