Skip to content

Commit 17c757b

Browse files
authored
ExpPanel updates (#221)
* Returned access restriction; started docs; Hiding info fields and comments box; cleaner subclassing * Rethrow Signals update mismatch as warning, #72 * SqueakExpPanel -> SignalsExpPanel * Removed code for very specific experiment that is no longer used * Completed using_ExpPanel docs * Formatting docs * Issue #79 * Cleaned up UpdatesFilter * documentation; changed plot colours
1 parent e19976a commit 17c757b

16 files changed

+1180
-1678
lines changed

+eui/ChoiceExpPanel.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ function refresh(obj)
9595
end
9696
end
9797

98-
methods %(Access = protected)
98+
methods (Access = protected)
9999
function newTrial(obj, num, condition)
100100
%attempt num is red when on higher than third
101101
attemptColour = iff(condition.repeatNum > 3, [1 0 0], [0 0 0]);

+eui/ExpPanel.m

Lines changed: 281 additions & 98 deletions
Large diffs are not rendered by default.

+eui/MControl.m

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -384,18 +384,28 @@ function rigConnected(obj, rig, ~)
384384
% If rig is connected check no experiments are running...
385385
expRef = rig.ExpRunning; % returns expRef if running
386386
if expRef
387-
% error('Experiment %s already running of %s', expDef, rig.Name)
388387
choice = questdlg(['Attention: An experiment is already running on ', rig.Name], ...
389388
upper(rig.Name), 'View', 'Cancel', 'Cancel');
390389
switch choice
391390
case 'View'
392391
% Load the parameters from file
393-
paramStruct = load(dat.expFilePath(expRef, 'parameters', 'master'));
392+
paramsPath = dat.expFilePath(expRef, 'parameters', 'master');
393+
paramStruct = load(paramsPath);
394394
if ~isfield(paramStruct.parameters, 'type')
395395
paramStruct.type = 'custom'; % override type name with preferred
396396
end
397+
% Determine the experiment start time
398+
try % Try getting data from Alyx
399+
ai = obj.AlyxPanel.AlyxInstance;
400+
assert(ai.IsLoggedIn)
401+
meta = ai.getSessions(expRef);
402+
startedTime = ai.datenum(meta.start_time);
403+
catch % Fall back on parameter file's system mod date
404+
startedTime = file.modDate(paramsPath);
405+
end
397406
% Instantiate an ExpPanel and pass it the expRef and parameters
398-
panel = eui.ExpPanel.live(obj.ActiveExpsGrid, expRef, rig, paramStruct.parameters);
407+
panel = eui.ExpPanel.live(obj.ActiveExpsGrid, expRef, rig,...
408+
paramStruct.parameters, 'StartedTime', startedTime);
399409
obj.LastExpPanel = panel;
400410
% Add a listener for the new panel
401411
panel.Listeners = [panel.Listeners

+eui/MappingExpPanel.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
end
2020
end
2121

22-
methods %(Access = protected)
22+
methods (Access = protected)
2323
function event(obj, name, t)
2424
event@eui.ExpPanel(obj, name, t); %call superclass method
2525
switch name

+eui/SignalsExpPanel.m

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
classdef SignalsExpPanel < eui.ExpPanel
2+
%EUI.SIGNALSEXPPANEL Basic UI control for monitoring a Signals experiment
3+
% Displays all values of events, inputs and outputs signals as they
4+
% arrive from the remote stimulus server. These events arrive as
5+
% 'signals' ExpEvents which are added to the SignalUpdates queue by
6+
% expUpdate and processed by processUpdates upon calling the update
7+
% method.
8+
%
9+
%
10+
% Part of Rigbox
11+
12+
% 2015-03 CB created
13+
14+
properties
15+
% Structure of signals event updates from SignalExp. As new updates
16+
% come in, previous updates in the list are overwritten
17+
SignalUpdates = struct('name', cell(500,1), 'value', cell(500,1), 'timestamp', cell(500,1))
18+
% List of updates to exclude (when Exclude == true) or to exclusively
19+
% show (Exclude == false) in the InfoGrid.
20+
UpdatesFilter = {'inputs.wheel', 'pars'}
21+
% Flag for excluding updates in UpdatesFilter list from InfoGrid. When
22+
% false only those in the list are shown, when false those in the list
23+
% are hidden
24+
Exclude = true
25+
% The total number of
26+
NumSignalUpdates = 0
27+
% containers.Map of InfoGrid ui labels mapped to their corresponding
28+
% Signal name
29+
LabelsMap
30+
% The colour of recently updated Signals update events in the InfoGrid
31+
RecentColour = [0 1 0]
32+
end
33+
34+
35+
methods
36+
function obj = SignalsExpPanel(parent, ref, params, logEntry)
37+
% Subclasses must chain a call to this.
38+
obj = obj@eui.ExpPanel(parent, ref, params, logEntry);
39+
obj.LabelsMap = containers.Map(); % Initialize labels map
40+
end
41+
42+
function update(obj)
43+
% UPDATE Update the panel
44+
% Processes any new updates via a call to the processUpdates method
45+
% and changes colours of info field labels based on how recently
46+
% they updated. This method is the callback to the RefreshTimer in
47+
% MC. Subclasses must chain a call to this.
48+
%
49+
% See also eui.ExpPanel/update
50+
update@eui.ExpPanel(obj); % Elapsed timer updated by superclass
51+
processUpdates(obj); % Update labels with latest signal values
52+
labelsMapVals = values(obj.LabelsMap)';
53+
labels = deal([labelsMapVals{:}]);
54+
if ~isempty(labels) % Colour decay by recency on labels
55+
dt = cellfun(@(t)etime(clock,t),...
56+
ensureCell(get(labels, 'UserData')));
57+
c = num2cell(exp(-dt/1.5)*obj.RecentColour, 2);
58+
set(labels, {'ForegroundColor'}, c);
59+
end
60+
end
61+
end
62+
63+
methods (Access = protected)
64+
function newTrial(obj, num, condition)
65+
% NEWTRIAL Process new trial conditions
66+
% Do nothing, this is for subclasses to override and react to, e.g.
67+
% to update plots, etc. based on a new trial's conditional
68+
% parameters. For a SignalsExp experiment, this may be called by
69+
% the processUpdates method upon an events.newTrial signal update.
70+
% In the future SignalsExp may send newTrial events (i.e.
71+
% independant of the 'signals' event updates)
72+
%
73+
% Inputs:
74+
% num (int) : The new trial number. May be used to index into
75+
% Block property
76+
% condition (struct) : Condition data for the new trial
77+
%
78+
% See also processUpdates, expUpdate, trialCompleted
79+
end
80+
81+
function trialCompleted(obj, num, data)
82+
% TRIALCOMPLETED Process completed trial data
83+
% Do nothing, this is for subclasses to override and react to, e.g.
84+
% to update plots, etc. based on a complete trial's data. Called by
85+
% expUpdate method upon 'trialData' event (currently not used by
86+
% exp.SignalsExp).
87+
%
88+
% Inputs:
89+
% num (int) : The new trial number. May be used to index into
90+
% Block property
91+
% data (struct) : Completed trial data
92+
%
93+
% See also expUpdate, processUpdates, trialCompleted
94+
end
95+
96+
function event(obj, name, t)
97+
% EVENT Process none-signals experiment event
98+
% Called by expUpdate callback to process all miscellaneous events,
99+
% i.e. experiment phases. This method is downstream of srv.ExpEvent
100+
% events. Updates ActivePhases list as well as the panel title
101+
% colour and, upon phase changes, the Status info field.
102+
%
103+
% Inputs:
104+
% name (char) : The event name
105+
% t (date vec) : The time the event occured
106+
%
107+
% Example:
108+
% if strcmp(evt.Data{1}, 'event') % srv.ExpEvent object
109+
% % Pass event info to be processed
110+
% obj.event(evt.Data{2}, evt.Data{3})
111+
% end
112+
113+
%called when an experiment event occurs
114+
phaseChange = false;
115+
if strEndsWith(name, 'Started')
116+
if strcmp(name, 'experimentStarted')
117+
obj.Root.TitleColor = [0 0.8 0.05]; % green title area
118+
else
119+
%phase has started, add it to active phases
120+
phase = name;
121+
phase(strfind(name, 'Started'):end) = [];
122+
obj.ActivePhases = [obj.ActivePhases; phase];
123+
phaseChange = true;
124+
end
125+
elseif strEndsWith(name, 'Ended')
126+
if strcmp(name, 'experimentEnded')
127+
obj.Root.TitleColor = [0.98 0.65 0.22]; %amber title area
128+
obj.ActivePhases = {};
129+
phaseChange = true;
130+
else
131+
%phase has ended, remove it from active phases
132+
phase = name;
133+
phase(strfind(name, 'Ended'):end) = [];
134+
obj.ActivePhases(strcmp(obj.ActivePhases, phase)) = [];
135+
phaseChange = true;
136+
end
137+
% else
138+
% disp(name);
139+
end
140+
if phaseChange % only update if there was a change for efficiency
141+
%update status with list of running phases
142+
phasesStr = ['[' strJoin(obj.ActivePhases, ',') ']'];
143+
set(obj.StatusLabel, 'String', sprintf('Running %s', phasesStr));
144+
end
145+
end
146+
147+
function processUpdates(obj)
148+
% PROCESSUPDATES Process all accumulated signals event updates
149+
% Process the signals events that have occured since the method was
150+
% last called. Any new field labels are created and all fields are
151+
% updated with the most recent signal values.
152+
%
153+
% This function is downstream of the update method, which is
154+
%
155+
% See also expUpdate, update
156+
updates = obj.SignalUpdates(1:obj.NumSignalUpdates);
157+
obj.NumSignalUpdates = 0;
158+
% fprintf('processing %i signal updates\n', length(updates));
159+
for ui = 1:length(updates)
160+
signame = updates(ui).name;
161+
switch signame
162+
case 'events.trialNum'
163+
set(obj.TrialCountLabel, ...
164+
'String', num2str(updates(ui).value));
165+
otherwise
166+
% Check whether to display update using UpdatesFilter
167+
onList = any(ismember(signame, obj.UpdatesFilter));
168+
if (obj.Exclude && ~onList) || (~obj.Exclude && onList)
169+
if ~isKey(obj.LabelsMap, signame) % If new update, add field
170+
obj.LabelsMap(signame) = obj.addInfoField(signame, '');
171+
end
172+
str = toStr(updates(ui).value); % Convert the value to string
173+
set(obj.LabelsMap(signame), 'String', str, 'UserData', clock,...
174+
'ForegroundColor', obj.RecentColour); % Update value
175+
end
176+
end
177+
end
178+
end
179+
180+
function expUpdate(obj, rig, evt)
181+
% EXPUPDATE Callback to the remote rig ExpUpdate event
182+
% Processes a new experiment event. Signals events are added to the
183+
% SignalUpdates queue for processing by the processUpdates method.
184+
%
185+
% Inputs:
186+
% rig (srv.StimulusControl) : The source of the event
187+
% evt (srv.ExpEvent) : The experiment event object
188+
%
189+
% See also live, event, srv.StimulusControl, srv.ExpEvent
190+
if strcmp(evt.Name, 'signals')
191+
type = 'signals';
192+
else
193+
type = evt.Data{1};
194+
end
195+
switch type
196+
case 'signals' %queue signal updates
197+
updates = evt.Data;
198+
newNUpdates = obj.NumSignalUpdates + length(updates);
199+
if newNUpdates > length(obj.SignalUpdates)
200+
%grow message queue to accommodate
201+
obj.SignalUpdates(2*newNUpdates).value = [];
202+
end
203+
try
204+
obj.SignalUpdates(obj.NumSignalUpdates+1:newNUpdates) = updates;
205+
catch % see github.com/cortex-lab/Rigbox/issues/72
206+
id = 'Rigbox:eui:SignalsExpPanel:signalsUpdateMismatch';
207+
msg = 'Error caught in signals updates: length of updates = %g, length newNUpdates = %g';
208+
warning(id, msg, length(updates), newNUpdates-(obj.NumSignalUpdates+1))
209+
end
210+
obj.NumSignalUpdates = newNUpdates;
211+
case 'newTrial'
212+
cond = evt.Data{2}; %condition data for the new trial
213+
trialCount = obj.Block.numCompletedTrials;
214+
%add the trial condition to a new trial in the block
215+
obj.mergeTrialData(trialCount + 1, struct('condition', cond));
216+
obj.newTrial(trialCount + 1, cond);
217+
case 'trialData'
218+
%a trial just completed
219+
data = evt.Data{2}; %the final data from that trial
220+
nTrials = obj.Block.numCompletedTrials + 1;
221+
obj.Block.numCompletedTrials = nTrials; %inc trial number in block
222+
%merge the new data with the rest of the trial data in the block
223+
obj.mergeTrialData(nTrials, data);
224+
obj.trialCompleted(nTrials, data);
225+
set(obj.TrialCountLabel, 'String', sprintf('%i', nTrials));
226+
case 'event'
227+
% disp(evt.Data);
228+
obj.event(evt.Data{2}, evt.Data{3});
229+
end
230+
end
231+
232+
end
233+
234+
end
235+

0 commit comments

Comments
 (0)