Skip to content

Commit ed51d8d

Browse files
authored
Merge pull request #39 from cortex-lab/tl-as-class_outputClasses
Timeline is now an object.
2 parents bed3a79 + 9621ddc commit ed51d8d

File tree

139 files changed

+4260
-17177
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

139 files changed

+4260
-17177
lines changed

+dat/addLogEntry.m

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
function e = addLogEntry(subject, timestamp, type, value, comments)
1+
function e = addLogEntry(subject, timestamp, type, value, comments, AlyxInstance)
22
%DAT.ADDLOGENTRY Adds a new entry to the experiment log
33
% e = DAT.ADDLOGENTRY(subject, timestamp, type, value, comments) files a
44
% new log entry for 'subject' with the corresponding info.
@@ -26,6 +26,7 @@
2626
%% create and store entry
2727
e = entry(nextidx);
2828
log(nextidx) = e;
29+
if nargin > 5; e.AlyxInstance = AlyxInstance; end
2930

3031
%% store updated log to *all* repos locations
3132
superSave(dat.logPath(subject, 'all'), struct('log', log));

+dat/listSubjects.m

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,43 @@
1-
function subjects = listSubjects()
1+
function subjects = listSubjects(varargin)
22
%DAT.LISTSUBJECTS Lists recorded subjects
3-
% subjects = DAT.LISTSUBJECTS() Lists the experimental subjects present
3+
% subjects = DAT.LISTSUBJECTS([alyxInstance]) Lists the experimental subjects present
44
% in experiment info repository ('expInfo').
55
%
6+
% Optional input argument of an alyx instance will enable generating this
7+
% list from alyx rather than from the directory structure on zserver
8+
%
69
% Part of Rigbox
710

811
% 2013-03 CB created
12+
% 2018-01 NS added alyx compatibility
913

10-
% The master 'expInfo' repository is the reference for the existence of
11-
% experiments, as given by the folder structure
12-
expInfoPath = dat.reposPath('expInfo', 'master');
13-
14-
dirs = file.list(expInfoPath, 'dirs');
15-
subjects = setdiff(dirs, {'misc'}); %exclude the misc directory
16-
14+
if nargin>0 && ~isempty(varargin{1}) % user provided an alyx instance
15+
ai = varargin{1}; % an alyx instance
16+
17+
% get list of all living, non-stock mice from alyx
18+
s = alyx.getData(ai, 'subjects?stock=False&alive=True');
19+
20+
% determine the user for each mouse
21+
respUser = cellfun(@(x)x.responsible_user, s, 'uni', false);
22+
23+
% get cell array of subject names
24+
subjNames = cellfun(@(x)x.nickname, s, 'uni', false);
25+
26+
% determine which subjects belong to this user
27+
thisUserSubs = sort(subjNames(strcmp(respUser, ai.username)));
28+
29+
% all the subjects
30+
otherUserSubs = sort(subjNames(~strcmp(respUser, ai.username)));
31+
32+
% the full, ordered list
33+
subjects = [{'default'}, thisUserSubs, otherUserSubs]';
34+
else
35+
36+
% The master 'expInfo' repository is the reference for the existence of
37+
% experiments, as given by the folder structure
38+
expInfoPath = dat.reposPath('expInfo', 'master');
39+
40+
dirs = file.list(expInfoPath, 'dirs');
41+
subjects = setdiff(dirs, {'misc'}); %exclude the misc directory
42+
end
1743
end

+dat/loadBlock.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
function block = loadBlock(varargin)
2-
%loadBlock Load the designated experiment block(s)
2+
%DAT.LOADBLOCK Load the designated experiment block(s)
33
% BLOCK = loadBlock(EXPREF, [EXPTYPE])
44
%
55
% BLOCK = loadBlock(SUBJECTREF, EXPDATE, EXPSEQ, [EXPTYPE])

+dat/newExp.m

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
1-
function [expRef, expSeq] = newExp(subject, expDate, expParams)
1+
function [expRef, expSeq, url] = newExp(subject, expDate, expParams, AlyxInstance)
22
%DAT.NEWEXP Create a new unique experiment in the database
3-
% [ref, seq] = DAT.NEWEXP(subject, expDate, expParams) TODO
3+
% [ref, seq, url] = DAT.NEWEXP(subject, expDate, expParams[, AlyxInstance])
4+
% Create a new experiment by creating the relevant folder tree in the
5+
% local and main data repositories in the following format:
6+
%
7+
% subject/
8+
% |_ YYYY-MM-DD/
9+
% |_ expSeq/
10+
%
11+
% If experiment parameters are passed into the function, they are saved
12+
% here, as a mat and in JSON (if possible). If an instance of Alyx is
13+
% passed and a base session for the experiment date is not found, one is
14+
% created in the Alyx database. A corresponding subsession is also
15+
% created and the parameters file is registered with the sub-session.
16+
%
17+
% See also DAT.PATHS
418
%
519
% Part of Rigbox
620

@@ -16,6 +30,11 @@
1630
expParams = [];
1731
end
1832

33+
if (nargin < 4 || isempty(AlyxInstance)) && ~strcmp(subject, 'default')
34+
% no instance of Alyx, don't create session on Alyx
35+
AlyxInstance = alyx.loginWindow;
36+
end
37+
1938
if ischar(expDate)
2039
% if the passed expDate is a string, parse it into a datenum
2140
expDate = datenum(expDate, 'yyyy-mm-dd');
@@ -29,8 +48,7 @@
2948
[~, dateList, seqList] = dat.listExps(subject);
3049

3150
% filter the list by expdate
32-
expDate = floor(expDate);
33-
filterIdx = dateList == expDate;
51+
filterIdx = dateList == floor(expDate);
3452

3553
% find the next sequence number
3654
expSeq = max(seqList(filterIdx)) + 1;
@@ -40,14 +58,61 @@
4058
end
4159

4260
% expInfo repository is the reference location for which experiments exist
43-
[expPath, expRef] = dat.expPath(subject, expDate, expSeq, 'expInfo');
61+
[expPath, expRef] = dat.expPath(subject, floor(expDate), expSeq, 'expInfo');
4462
% ensure nothing went wrong in making a "unique" ref and path to hold
4563
assert(~any(file.exists(expPath)), ...
4664
sprintf('Something went wrong as experiment folders already exist for "%s".', expRef));
4765

4866
% now make the folder(s) to hold the new experiment
4967
assert(all(cellfun(@(p) mkdir(p), expPath)), 'Creating experiment directories failed');
5068

69+
if ~strcmp(subject, 'default') % Ignore fake subject
70+
% if the Alyx Instance is set, find or create BASE session
71+
expDate = alyx.datestr(expDate); % date in Alyx format
72+
% Get list of base sessions
73+
sessions = alyx.getData(AlyxInstance,...
74+
['sessions?type=Base&subject=' subject]);
75+
76+
%If the date of this latest base session is not the same date as
77+
%today, then create a new base session for today
78+
if isempty(sessions) || ~strcmp(sessions{end}.start_time(1:10), expDate(1:10))
79+
d = struct;
80+
d.subject = subject;
81+
d.procedures = {'Behavior training/tasks'};
82+
d.narrative = 'auto-generated session';
83+
d.start_time = expDate;
84+
d.type = 'Base';
85+
% d.users = {AlyxInstance.username};
86+
87+
base_submit = alyx.postData(AlyxInstance, 'sessions', d);
88+
assert(isfield(base_submit,'subject'),...
89+
'Submitted base session did not return appropriate values');
90+
91+
%Now retrieve the sessions again
92+
sessions = alyx.getData(AlyxInstance,...
93+
['sessions?type=Base&subject=' subject]);
94+
end
95+
latest_base = sessions{end};
96+
97+
%Now create a new SUBSESSION, using the same experiment number
98+
d = struct;
99+
d.subject = subject;
100+
d.procedures = {'Behavior training/tasks'};
101+
d.narrative = 'auto-generated session';
102+
d.start_time = expDate;
103+
d.type = 'Experiment';
104+
d.parent_session = latest_base.url;
105+
d.number = expSeq;
106+
% d.users = {AlyxInstance.username};
107+
108+
subsession = alyx.postData(AlyxInstance, 'sessions', d);
109+
assert(isfield(subsession,'subject'),...
110+
'Failed to create new sub-session in Alyx for %s', subject);
111+
url = subsession.url;
112+
else
113+
url = [];
114+
end
115+
51116
% if the parameters had an experiment definition function, save a copy in
52117
% the experiment's folder
53118
if isfield(expParams, 'defFunction')
@@ -61,5 +126,29 @@
61126
% now save the experiment parameters variable
62127
superSave(dat.expFilePath(expRef, 'parameters'), struct('parameters', expParams));
63128

64-
129+
if ~isempty(expParams)
130+
try % save a copy of parameters in json
131+
% First, change all functions to strings
132+
f_idx = structfun(@(s)isa(s, 'function_handle'), expParams);
133+
fields = fieldnames(expParams);
134+
paramCell = struct2cell(expParams);
135+
paramCell(f_idx) = cellfun(@func2str, paramCell(f_idx),'UniformOutput', false);
136+
expParams = cell2struct(paramCell, fields);
137+
% Generate JSON path and save
138+
jsonPath = fullfile(fileparts(dat.expFilePath(expRef, 'parameters', 'master')),...
139+
[expRef, '_parameters.json']);
140+
savejson('parameters', expParams, jsonPath);
141+
% Register our JSON parameter set to Alyx
142+
if ~strcmp(subject, 'default')
143+
alyx.registerFile(jsonPath, 'json', url, 'Parameters', [], AlyxInstance);
144+
end
145+
catch ex
146+
warning(ex.identifier, 'Failed to save paramters as JSON: %s.\n Registering mat file instead', ex.message)
147+
% Register our parameter set to Alyx
148+
if ~strcmp(subject, 'default')
149+
alyx.registerFile(dat.expFilePath(expRef, 'parameters', 'master'), 'mat',...
150+
url, 'Parameters', [], AlyxInstance);
151+
end
152+
end
153+
end
65154
end

+dat/parseAlyxInstance.m

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,42 @@
1-
function varargout = parseAlyxInstance(varargin)
2-
%DATA.PARSEALYXINSTANCE Converts input to string for UDP message and back
3-
% [UDP_string] = DATA.PARSEALYXINSTANCE(AlyxInstance, ref)
1+
function [ref, AlyxInstance] = parseAlyxInstance(varargin)
2+
%DAT.PARSEALYXINSTANCE Converts input to string for UDP message and back
3+
% [UDP_string] = DATA.PARSEALYXINSTANCE(ref, AlyxInstance)
4+
% [ref, AlyxInstance] = DATA.PARSEALYXINSTANCE(UDP_string)
45
%
56
% The pattern for 'ref' should be '{date}_{seq#}_{subject}', with two
67
% date formats accepted, either 'yyyy-mm-dd' or 'yyyymmdd'.
78
%
89
% AlyxInstance should be a struct with the following fields, all
9-
% containing strings: 'baseURL', 'token', 'username'.
10+
% containing strings: 'baseURL', 'token', 'username'[, 'subsessionURL'].
1011
%
1112
% Part of Rigbox
1213

1314
% 2017-10 MW created
1415

15-
if nargin > 1 % in [AlyxInstance, ref]
16-
ai = varargin{1}; % extract AlyxInstance struct
17-
ref = varargin(2); % extract expRef
16+
if nargin > 1 % in [ref, AlyxInstance]
17+
ref = varargin{1}; % extract expRef
18+
ai = varargin{2}; % extract AlyxInstance struct
1819
if isstruct(ai) % if there is an AlyxInstance
20+
ai = orderfields(ai); % alphabetize fields
21+
% remove water requirement remaining field
22+
if isfield(ai, 'water_requirement_remaining')
23+
ai = rmfield(ai, 'water_requirement_remaining');
24+
end
25+
fname = fieldnames(ai); % get fieldnames
26+
emp = structfun(@isempty, ai); % find empty fields
27+
if any(emp); ai = rmfield(ai, fname(emp)); end % remove the empty fields
1928
c = cellfun(@(fn) ai.(fn), fieldnames(ai), 'UniformOutput', false); % get fieldnames
20-
varargout = strjoin([c; ref],'\'); % join into single string for UDP
21-
else % otherwise just output the expRef
22-
varargout = ref;
29+
ref = strjoin([ref; c],'\'); % join into single string for UDP, otherwise just output the expRef
2330
end
2431
else % in [UDP_string]
2532
C = strsplit(varargin{1},'\'); % split string
26-
varargout{1} = struct('baseURL', C{1}, 'token', C{2}, 'username', C{3}); % reconstruct AlyxInstance
27-
varargout{2} = C{4}; % output expRef
33+
ref = C{1}; % output expRef
34+
if numel(C)>4 % if UDP string included AlyxInstance
35+
AlyxInstance = struct('baseURL', C{2}, 'subsessionURL', C{3},...
36+
'token', C{4}, 'username', C{5}); % reconstruct AlyxInstance
37+
elseif numel(C)>1 % if AlyxInstance has no subsession set
38+
AlyxInstance = struct('baseURL', C{2}, 'token', C{3}, 'username', C{4}); % reconstruct AlyxInstance
39+
else
40+
AlyxInstance = []; % if input was just an expRef, output empty AlyxInstance
41+
end
2842
end

+dat/parseExpRef.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
function [subjectRef, expDate, expSequence] = parseExpRef(ref)
2-
%DATA.PARSEEXPREF Extracts subject, date and seq from an experiment ref
2+
%DAT.PARSEEXPREF Extracts subject, date and seq from an experiment ref
33
% [subject, date, seq] = DATA.PARSEEXPREF(ref)
44
%
55
% The pattern for 'ref' should be '{date}_{seq#}_{subject}', with two

+dat/paths.m

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
% server3Name = '\\zserver3.cortexlab.net'; % 2017-02-18 MW - Currently
1919
% unused by Rigbox
2020
server4Name = '\\zserver4.cortexlab.net';
21+
basketName = '\\basket.cortexlab.net'; % for working analyses
22+
lugaroName = '\\lugaro.cortexlab.net'; % for tape backup
2123

2224
%% defaults
2325
% path containing rigbox config folders
@@ -51,6 +53,18 @@
5153
% repository for all experiment definitions
5254
p.expDefinitions = fullfile(server1Name, 'Code', 'Rigging', 'ExpDefinitions');
5355

56+
% repository for working analyses that are not meant to be stored
57+
% permanently
58+
p.workingAnalysisRepository = fullfile(basketName, 'data');
59+
60+
% for tape backups, first files go here:
61+
p.tapeStagingRepository = fullfile(lugaroName, 'bigdrive', 'staging');
62+
63+
% then they go here:
64+
p.tapeArchiveRepository = fullfile(lugaroName, 'bigdrive', 'toarchive');
65+
66+
67+
5468
%% load rig-specific overrides from config file, if any
5569
customPathsFile = fullfile(p.rigConfig, 'paths.mat');
5670
if file.exists(customPathsFile)

+dat/updateLogEntry.m

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
function updateLogEntry(subject, id, newEntry)
22
%DAT.UPDATELOGENTRY Updates an existing experiment log entry
3-
% DAT.UPDATELOGENTRY(subject, id, newEntry)
4-
% TODO
3+
% DAT.UPDATELOGENTRY(subject, id, newEntry) The server copy of the log is
4+
% loaded and the relevant record overwritten. If an AlyxInstance is set,
5+
% any session comments are saved in the session narrative in Alyx.
6+
%
7+
% See also DAT.ADDLOGENTRY
58
%
69
% Part of Rigbox
710

811
% 2013-03 CB created
912

13+
if isfield(newEntry, 'AlyxInstance')&&~isempty(newEntry.comments)
14+
data = struct('subject', dat.parseExpRef(newEntry.value.ref),...
15+
'narrative', strrep(mat2DStrTo1D(newEntry.comments),newline,'\n'));
16+
alyx.putData(newEntry.AlyxInstance,...
17+
newEntry.AlyxInstance.subsessionURL, data);
18+
newEntry = rmfield(newEntry, 'AlyxInstance');
19+
end
20+
1021
%load existing log from central repos
1122
log = pick(load(dat.logPath(subject, 'master')), 'log');
1223
%find the entry with specified id
@@ -17,5 +28,4 @@ function updateLogEntry(subject, id, newEntry)
1728
log(idx) = newEntry;
1829
%store new log to all repos locations
1930
superSave(dat.logPath(subject), struct('log', log));
20-
2131
end

0 commit comments

Comments
 (0)