rpipe 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +23 -0
- data/LICENSE +20 -0
- data/README +0 -0
- data/README.rdoc +33 -0
- data/Rakefile +78 -0
- data/VERSION +1 -0
- data/bin/create_driver.rb +79 -0
- data/bin/rpipe +131 -0
- data/bin/swallow_batch_run.rb +21 -0
- data/lib/core_additions.rb +5 -0
- data/lib/custom_methods/JohnsonMerit220Visit1Preproc.m +26 -0
- data/lib/custom_methods/JohnsonMerit220Visit1Preproc.rb +43 -0
- data/lib/custom_methods/JohnsonMerit220Visit1Preproc_job.m +80 -0
- data/lib/custom_methods/JohnsonMerit220Visit1Stats.m +74 -0
- data/lib/custom_methods/JohnsonMerit220Visit1Stats.rb +63 -0
- data/lib/custom_methods/JohnsonMerit220Visit1Stats_job.m +63 -0
- data/lib/custom_methods/JohnsonTbiLongitudinalSnodPreproc.m +26 -0
- data/lib/custom_methods/JohnsonTbiLongitudinalSnodPreproc.rb +41 -0
- data/lib/custom_methods/JohnsonTbiLongitudinalSnodPreproc_job.m +69 -0
- data/lib/custom_methods/JohnsonTbiLongitudinalSnodStats.m +76 -0
- data/lib/custom_methods/JohnsonTbiLongitudinalSnodStats.rb +67 -0
- data/lib/custom_methods/JohnsonTbiLongitudinalSnodStats_job.m +59 -0
- data/lib/custom_methods/ReconWithHello.rb +7 -0
- data/lib/default_logger.rb +13 -0
- data/lib/default_methods/default_preproc.rb +76 -0
- data/lib/default_methods/default_recon.rb +80 -0
- data/lib/default_methods/default_stats.rb +94 -0
- data/lib/default_methods/recon/physionoise_helper.rb +69 -0
- data/lib/default_methods/recon/raw_sequence.rb +109 -0
- data/lib/generators/job_generator.rb +36 -0
- data/lib/generators/preproc_job_generator.rb +31 -0
- data/lib/generators/recon_job_generator.rb +76 -0
- data/lib/generators/stats_job_generator.rb +70 -0
- data/lib/generators/workflow_generator.rb +128 -0
- data/lib/global_additions.rb +18 -0
- data/lib/logfile.rb +310 -0
- data/lib/matlab_helpers/CreateFunctionalVolumeStruct.m +6 -0
- data/lib/matlab_helpers/import_csv.m +32 -0
- data/lib/matlab_helpers/matlab_queue.rb +37 -0
- data/lib/matlab_helpers/prepare_onsets_xls.m +30 -0
- data/lib/rpipe.rb +254 -0
- data/rpipe.gemspec +177 -0
- data/spec/generators/preproc_job_generator_spec.rb +27 -0
- data/spec/generators/recon_job_generator_spec.rb +33 -0
- data/spec/generators/stats_job_generator_spec.rb +50 -0
- data/spec/generators/workflow_generator_spec.rb +97 -0
- data/spec/helper_spec.rb +40 -0
- data/spec/integration/johnson.merit220.visit1_spec.rb +47 -0
- data/spec/integration/johnson.tbi.longitudinal.snod_spec.rb +48 -0
- data/spec/logfile_spec.rb +96 -0
- data/spec/matlab_queue_spec.rb +40 -0
- data/spec/merit220_stats_spec.rb +81 -0
- data/spec/physio_spec.rb +98 -0
- data/test/drivers/merit220_workflow_sample.yml +15 -0
- data/test/drivers/mrt00000.yml +65 -0
- data/test/drivers/mrt00015.yml +62 -0
- data/test/drivers/mrt00015_hello.yml +41 -0
- data/test/drivers/mrt00015_withphys.yml +81 -0
- data/test/drivers/tbi000.yml +129 -0
- data/test/drivers/tbi000_separatevisits.yml +137 -0
- data/test/drivers/tmp.yml +58 -0
- data/test/fixtures/faces3_recognitionA.mat +0 -0
- data/test/fixtures/faces3_recognitionA.txt +86 -0
- data/test/fixtures/faces3_recognitionA_equal.csv +25 -0
- data/test/fixtures/faces3_recognitionA_unequal.csv +21 -0
- data/test/fixtures/faces3_recognitionB_incmisses.txt +86 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CPd3R_40.txt +13360 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CPd3_40.txt +13360 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CPttl_40.txt +13360 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CRTd3R_40.txt +13360 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CRTd3_40.txt +13360 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CRTttl_40.txt +13360 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_CRTd3R_40.txt +334 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_CRTd3_40.txt +334 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_CRTttl_40.txt +334 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_RRT_40.txt +334 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_RVT_40.txt +334 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_card_spline_40.txt +334 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_resp_spline_40.txt +334 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_RRT_40.txt +9106 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_RVT_40.txt +9106 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_CRTd3R_40.txt +167 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_CRTd3_40.txt +167 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_CRTttl_40.txt +167 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_RRT_40.txt +167 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_RVT_40.txt +167 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_card_spline_40.txt +167 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_resp_spline_40.txt +167 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_card_spline_40.txt +13360 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_resp_spline_40.txt +9106 -0
- data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_resp_spline_downsampled_40.txt +9106 -0
- data/test/fixtures/ruport_summary.yml +123 -0
- data/test/fixtures/valid_scans.yaml +35 -0
- data/test/helper.rb +10 -0
- data/test/test_dynamic_method_inclusion.rb +10 -0
- data/test/test_includes.rb +11 -0
- data/test/test_integrative_johnson.merit220.visit1.rb +31 -0
- data/test/test_preproc.rb +11 -0
- data/test/test_recon.rb +11 -0
- data/test/test_rpipe.rb +19 -0
- data/vendor/output_catcher.rb +93 -0
- data/vendor/trollop.rb +781 -0
- metadata +260 -0
data/lib/logfile.rb
ADDED
@@ -0,0 +1,310 @@
|
|
1
|
+
require 'pp'
|
2
|
+
require 'ruport'
|
3
|
+
require 'default_logger'
|
4
|
+
require 'matlab_helpers/matlab_queue'
|
5
|
+
|
6
|
+
############################################### START OF CLASS ######################################################
|
7
|
+
# An object that helps parse and format Behavioral Logfiles.
|
8
|
+
class Logfile
|
9
|
+
include DefaultLogger
|
10
|
+
|
11
|
+
# An array of rows raw text data
|
12
|
+
attr_accessor :textfile_data
|
13
|
+
# Conditions to extract time vectors from
|
14
|
+
attr_accessor :conditions
|
15
|
+
# Filename for output csv
|
16
|
+
attr_accessor :csv_filename
|
17
|
+
# Original Presentation Processed Logfile (.txt)
|
18
|
+
attr_reader :textfile
|
19
|
+
# A hash of Onset Vectors keyed by condition
|
20
|
+
attr_reader :vectors
|
21
|
+
|
22
|
+
def initialize(path, *conditions)
|
23
|
+
@textfile = path
|
24
|
+
@textfile_data = []
|
25
|
+
@conditions = conditions.select {|cond| cond.respond_to? 'to_sym' }
|
26
|
+
|
27
|
+
# Add the keys of any hashes (the combined conditions) to the list of
|
28
|
+
# conditions and add the separate vectors to the list of combined_conditions
|
29
|
+
@combined_conditions = []
|
30
|
+
conditions.select {|cond| cond.respond_to? 'keys' }.each do |cond|
|
31
|
+
cond.keys.collect {|key| @conditions << key }
|
32
|
+
@combined_conditions << cond
|
33
|
+
end
|
34
|
+
|
35
|
+
raise IOError, "Can't find file #{path}" unless File.exist?(path)
|
36
|
+
File.open(path, 'r').each do |line|
|
37
|
+
@textfile_data << line.split(/[\,\:\n\r]+/).each { |val| val.strip }
|
38
|
+
end
|
39
|
+
|
40
|
+
raise IOError, "Problem reading #{@textfile} - no data found." if @textfile_data.empty?
|
41
|
+
|
42
|
+
if @conditions.empty?
|
43
|
+
raise ScriptError, "Could not set conditions #{conditions}" unless conditions.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
setup_logger
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
def condition_vectors
|
51
|
+
return @vectors if @vectors
|
52
|
+
raise ScriptError, "Conditions must be set to extract vectors" if @conditions.empty?
|
53
|
+
vectors = extract_condition_vectors(@conditions)
|
54
|
+
@vectors = zero_and_convert_to_reps(vectors)
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_csv
|
58
|
+
rows = condition_vectors.values
|
59
|
+
rows = pad_array(rows)
|
60
|
+
|
61
|
+
output = ""
|
62
|
+
output << vectors.keys.join(', ') + "\n"
|
63
|
+
vectors.values.transpose.each do |row|
|
64
|
+
output << row.join(', ') + "\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
return output
|
68
|
+
end
|
69
|
+
|
70
|
+
def write_csv(filename)
|
71
|
+
File.open(filename, 'w') { |f| f.puts to_csv }
|
72
|
+
raise ScriptError, "Unable to write #{filename}" unless File.exist?(filename)
|
73
|
+
@csv_filename = filename
|
74
|
+
end
|
75
|
+
|
76
|
+
def write_mat(prefix)
|
77
|
+
queue = MatlabQueue.new
|
78
|
+
queue.paths << [
|
79
|
+
Pathname.new(File.join(File.dirname(__FILE__), 'matlab_helpers'))
|
80
|
+
]
|
81
|
+
|
82
|
+
raise ScriptError, "Can't find #{@csv_filename}" unless File.exist?(@csv_filename)
|
83
|
+
|
84
|
+
queue << "prepare_onsets_xls( \
|
85
|
+
'#{@csv_filename}', \
|
86
|
+
'#{prefix}', \
|
87
|
+
{ #{@conditions.collect {|c| "'#{c}'"}.join(' ') } } \
|
88
|
+
)"
|
89
|
+
|
90
|
+
queue.run!
|
91
|
+
|
92
|
+
raise ScriptError, "Problem writing #{prefix}.mat" unless File.exist?(prefix + '.mat')
|
93
|
+
|
94
|
+
return prefix + '.mat'
|
95
|
+
end
|
96
|
+
|
97
|
+
# Sort Logfiles by their Modification Time
|
98
|
+
def <=>(other_logfile)
|
99
|
+
File.stat(@textfile).mtime <=> File.stat(other_logfile.textfile).mtime
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.write_summary(filename = 'tmp.csv', directory = Dir.pwd, grouping = 'version')
|
103
|
+
table = self.summarize_directory(directory)
|
104
|
+
File.open(filename, 'w') do |f|
|
105
|
+
f.puts Ruport::Data::Grouping(table, :by => grouping).to_csv
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.summarize_directory(directory)
|
110
|
+
table = Ruport::Data::Table.new
|
111
|
+
Dir.glob(File.join(directory, '*.txt')).each do |logfile|
|
112
|
+
# Intialize a logfile without any conditions.
|
113
|
+
lf = Logfile.new(logfile)
|
114
|
+
table << lf.ruport_summary
|
115
|
+
table.column_names = lf.summary_attributes if table.column_names.empty?
|
116
|
+
end
|
117
|
+
return table
|
118
|
+
end
|
119
|
+
|
120
|
+
# Create an item analysis table containing average response time and
|
121
|
+
# accuracy for each item over all logfiles.
|
122
|
+
def self.item_analysis(directory)
|
123
|
+
logfiles = Dir.glob(File.join(directory, '*.txt')).collect { |lf| lf = Logfile.new(lf) }
|
124
|
+
all_items = self.item_table(logfiles)
|
125
|
+
item_groups = self.group_items_by(all_items, ['code1', 'pair_type'])
|
126
|
+
summary = self.item_summary(item_groups)
|
127
|
+
|
128
|
+
return summary
|
129
|
+
end
|
130
|
+
|
131
|
+
# Create a Ruport::Data::Table of all the response pairs for a given logfile.
|
132
|
+
def response_pairs_table
|
133
|
+
response_pairs = []
|
134
|
+
|
135
|
+
@textfile_data.each do |line|
|
136
|
+
next if line.empty?
|
137
|
+
header = line.first.gsub(/(\(|\))/, '_').downcase.chomp("_").to_sym
|
138
|
+
if line[1] =~ /(o|n)_\w\d{2}/
|
139
|
+
response_pairs << Ruport::Data::Record.new(response_pair_data(line), :attributes => response_pair_attributes)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
return response_pairs
|
144
|
+
end
|
145
|
+
|
146
|
+
# Fields to be included in a Response Pair Table
|
147
|
+
def response_pair_attributes
|
148
|
+
['enum', 'version', 'ctime', 'pair_type', 'code1', 'time1', 'code2', 'time2', 'time_diff']
|
149
|
+
end
|
150
|
+
|
151
|
+
def ruport_summary
|
152
|
+
Ruport::Data::Record.new(summary_data, :attributes => summary_attributes )
|
153
|
+
end
|
154
|
+
|
155
|
+
def summary_attributes
|
156
|
+
['enum', 'task', 'version', 'ctime'] + @textfile_data[0]
|
157
|
+
end
|
158
|
+
|
159
|
+
# Combine vectors into a new one (new_misses + old_misses = misses)
|
160
|
+
def combine_vectors(combined_vector_title, original_vector_titles)
|
161
|
+
# Add the combined vectors to the vectors instance variable.
|
162
|
+
condition_vectors[combined_vector_title] = combined_vectors(original_vector_titles)
|
163
|
+
|
164
|
+
# Add the new condition to @conditions
|
165
|
+
@conditions << combined_vector_title
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
def combined_vectors(titles)
|
171
|
+
combined_vector = []
|
172
|
+
puts titles
|
173
|
+
titles.each do |title|
|
174
|
+
if condition_vectors[title]
|
175
|
+
combined_vector << condition_vectors[title]
|
176
|
+
else
|
177
|
+
pp condition_vectors
|
178
|
+
raise "Couldn't find vector called #{title}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
pp combined_vector
|
182
|
+
return combined_vector.flatten.sort
|
183
|
+
end
|
184
|
+
|
185
|
+
# Create a table of all responses to all items.
|
186
|
+
# Pass in an array of #Logfiles
|
187
|
+
def self.item_table(logfiles)
|
188
|
+
table = Ruport::Data::Table.new
|
189
|
+
|
190
|
+
logfiles.each do |logfile|
|
191
|
+
logfile.response_pairs_table.collect { |pair| table << pair }
|
192
|
+
table.column_names = logfile.response_pair_attributes if table.column_names.empty?
|
193
|
+
end
|
194
|
+
|
195
|
+
return table
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.item_summary(grouping)
|
199
|
+
item_summary_table = Ruport::Data::Table.new
|
200
|
+
grouping.data.keys.each do |code|
|
201
|
+
# Create a new copy of the grouping, because the act of summarizing a
|
202
|
+
# grouping does something destructive to it.
|
203
|
+
current_group = grouping.dup;
|
204
|
+
samples = current_group.data.values.first.length
|
205
|
+
|
206
|
+
|
207
|
+
# Return the summary of codes for each item (in the current case, that's
|
208
|
+
# <condition>_correct, <condition>_incorrect, and misses)
|
209
|
+
item_records = current_group.subgrouping(code).summary(:code1,
|
210
|
+
:response_time_average => lambda {|sub| sub.sigma("time_diff").to_f / sub.length},
|
211
|
+
:count => lambda {|sub| sub.length},
|
212
|
+
:accuracy => lambda {|sub| sub.length / samples.to_f },
|
213
|
+
:version => lambda {|sub| sub.data[0][1] }
|
214
|
+
);
|
215
|
+
|
216
|
+
# Extract just the first record (correct responses) for the item analysis
|
217
|
+
# as incorrect can be inferred. Currently, ditch the misses.
|
218
|
+
item_record = item_records.to_a.first
|
219
|
+
|
220
|
+
# Add the item to the record as a column (the summary doesn't give this
|
221
|
+
# by default because it assumes we already know what we're summarizing.)
|
222
|
+
item_record['code1'] = code;
|
223
|
+
item_summary_table << item_record
|
224
|
+
end
|
225
|
+
|
226
|
+
return item_summary_table
|
227
|
+
end
|
228
|
+
|
229
|
+
def self.group_items_by(table, groupings)
|
230
|
+
Ruport::Data::Grouping.new(table, :by => groupings)
|
231
|
+
end
|
232
|
+
|
233
|
+
def response_pair_data(line)
|
234
|
+
enum, task, version = File.basename(@textfile).split("_").values_at(0,3,4)
|
235
|
+
enum = File.basename(enum) unless enum.nil?
|
236
|
+
version = File.basename(version, '.txt') unless version.nil?
|
237
|
+
ctime = File.stat(@textfile).ctime
|
238
|
+
|
239
|
+
[enum, version, ctime] + line
|
240
|
+
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
|
245
|
+
def summary_data
|
246
|
+
enum, task, version = File.basename(@textfile).split("_").values_at(0,3,4)
|
247
|
+
enum = File.basename(enum) unless enum.nil?
|
248
|
+
version = File.basename(version, '.txt') unless version.nil?
|
249
|
+
ctime = File.stat(@textfile).ctime
|
250
|
+
|
251
|
+
[[enum, task, version, ctime], @textfile_data[1]].flatten
|
252
|
+
end
|
253
|
+
|
254
|
+
def extract_condition_vectors(conditions)
|
255
|
+
vectors = {}
|
256
|
+
@conditions
|
257
|
+
@textfile_data.each do |line|
|
258
|
+
next if line.empty?
|
259
|
+
# Headers are written in the Textfile as "New(Correct)".
|
260
|
+
# Convert them to proper condition names - downcase separated by underscores
|
261
|
+
header = pretty_condition(line.first)
|
262
|
+
vector = line[2..-1].collect {|val| val.to_f } if line[2..-1]
|
263
|
+
|
264
|
+
# Make sure this isn't a column line inside the logfile.
|
265
|
+
if line[1] =~ /time/
|
266
|
+
# Check if this vector matches any combined conditions.
|
267
|
+
@combined_conditions.each do |vector_group|
|
268
|
+
vector_group.each_pair do |key, vals|
|
269
|
+
if vals.include?(header)
|
270
|
+
vectors[key] = vectors[key] ? vector.collect{ |timepoint| vectors[key] << timepoint} : vector
|
271
|
+
vectors[key].flatten!
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# Check if this vector matches a single condition.
|
277
|
+
vectors[header] = vector if @conditions.include?(header);
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Ensure that the vecotors are in order.
|
282
|
+
vectors.each_value { |vector| vector.sort! }
|
283
|
+
|
284
|
+
raise ScriptError, "Unable to read vectors for #{@textfile}" if vectors.empty?
|
285
|
+
|
286
|
+
return vectors
|
287
|
+
end
|
288
|
+
|
289
|
+
def zero_and_convert_to_reps(vectors)
|
290
|
+
minimum = vectors.values.flatten.min
|
291
|
+
vectors.values.each do |row|
|
292
|
+
row.collect! { |val| (val - minimum) / 2000 }
|
293
|
+
end
|
294
|
+
|
295
|
+
return vectors
|
296
|
+
end
|
297
|
+
|
298
|
+
def pad_array(rows)
|
299
|
+
max_length = rows.inject(0) { |max, row| max >= row.length ? max : row.length }
|
300
|
+
rows.each do |row|
|
301
|
+
row[max_length] = nil
|
302
|
+
row.pop
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def pretty_condition(condition)
|
307
|
+
condition.gsub(/(\(|\))/, '_').downcase.chomp("_").to_sym
|
308
|
+
end
|
309
|
+
|
310
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
function import_csv(fileToRead1)
|
2
|
+
%IMPORTFILE(FILETOREAD1)
|
3
|
+
% Imports data from the specified file
|
4
|
+
% FILETOREAD1: file to read
|
5
|
+
|
6
|
+
% Auto-generated by MATLAB on 24-May-2010 11:01:12
|
7
|
+
|
8
|
+
if ~exist(fileToRead1)
|
9
|
+
error('File %s does not exist.', fileToRead1)
|
10
|
+
end
|
11
|
+
|
12
|
+
% Import the file
|
13
|
+
newData1 = importdata(fileToRead1);
|
14
|
+
|
15
|
+
% Split Headers
|
16
|
+
headers = regexp(newData1.textdata, ',', 'split');
|
17
|
+
% headers = headers{1}
|
18
|
+
|
19
|
+
% If Matlab FAILED to read an empty vector, assign it.
|
20
|
+
% if size(newData1.textdata, 2) ~= size(headers, 2)
|
21
|
+
% newData1.data(:, size(headers,2)) = NaN
|
22
|
+
% end
|
23
|
+
|
24
|
+
% Create an array of NaN's of proper dimensions (as many rows as data and as many columns as header) and insert the data on top of it.
|
25
|
+
data = zeros(size(newData1.data, 1), size(headers,2)) + nan;
|
26
|
+
data(1:size(newData1.data,1),1:size(newData1.data,2)) = newData1.data;
|
27
|
+
|
28
|
+
% Create new variables in the base workspace from those fields.
|
29
|
+
for i = 1:size(headers, 2)
|
30
|
+
assignin('caller', genvarname(headers{i}{1}), data(:,i));
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'default_logger'
|
2
|
+
|
3
|
+
# Maintain and run matlab commands and paths.
|
4
|
+
class MatlabQueue
|
5
|
+
include DefaultLogger
|
6
|
+
|
7
|
+
attr_accessor :paths, :commands, :ml_command
|
8
|
+
attr_reader :success
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@paths = []
|
12
|
+
@commands = []
|
13
|
+
@ml_command = "matlab -nosplash -nodesktop"
|
14
|
+
setup_logger
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
[
|
19
|
+
@paths.flatten.collect {|path| "addpath(genpath('#{path}'))"},
|
20
|
+
@commands
|
21
|
+
].flatten.join('; ')
|
22
|
+
end
|
23
|
+
|
24
|
+
def run!
|
25
|
+
cmd = @ml_command + " -r \"#{ to_s }; exit\" "
|
26
|
+
@success = run(cmd)
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(m, *args, &block)
|
30
|
+
@commands.send(m, *args, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_to_path(*args)
|
34
|
+
args.each { |arg| @paths << arg }
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
function [] = prepare_onsets_xls(csvfile, matfileprefix, conditions)
|
2
|
+
%IMPORTFILE(FILETOREAD1)
|
3
|
+
% Imports data from the specified file
|
4
|
+
% FILETOREAD1: file to read
|
5
|
+
|
6
|
+
import_csv(csvfile);
|
7
|
+
|
8
|
+
for i = 1:length(conditions)
|
9
|
+
condition = conditions{i};
|
10
|
+
condition_onsets = eval(condition);
|
11
|
+
|
12
|
+
% Strip NaN's, but leave one nan if vector is empty (SPM's preference).
|
13
|
+
condition_onsets = condition_onsets(find(~isnan(condition_onsets)));
|
14
|
+
|
15
|
+
% Allow for conditions called 'misses' to be dropped from onsets.
|
16
|
+
if length(condition_onsets) == 0;
|
17
|
+
if ~strcmp(condition, 'misses')
|
18
|
+
condition_onsets=[nan];
|
19
|
+
else
|
20
|
+
continue
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
% Format cell array for SPM's multiple conditions
|
25
|
+
names{i} = condition;
|
26
|
+
onsets{i} = condition_onsets;
|
27
|
+
durations{i} = [0];
|
28
|
+
end
|
29
|
+
|
30
|
+
save([matfileprefix,'.mat'],'names','onsets', 'durations');
|
data/lib/rpipe.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'custom_methods'))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'yaml'
|
6
|
+
require 'ftools'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'pathname'
|
9
|
+
require 'tmpdir'
|
10
|
+
require 'erb'
|
11
|
+
require 'log4r'
|
12
|
+
require 'popen4'
|
13
|
+
require 'core_additions'
|
14
|
+
require 'metamri/core_additions'
|
15
|
+
|
16
|
+
# prevent zipping in FSL programs
|
17
|
+
ENV['FSLOUTPUTTYPE'] = 'NIFTI'
|
18
|
+
|
19
|
+
require 'default_logger'
|
20
|
+
require 'global_additions'
|
21
|
+
|
22
|
+
class JobStep
|
23
|
+
|
24
|
+
COLLISION_POLICY = :panic # options -- :panic, :destroy, :overwrite
|
25
|
+
|
26
|
+
attr_accessor :subid, :rawdir, :origdir, :procdir, :statsdir, :spmdir, :collision_policy, :libdir
|
27
|
+
|
28
|
+
# Intialize with two configuration option hashes - workflow_spec and job_spec
|
29
|
+
def initialize(workflow_spec, job_spec)
|
30
|
+
# allow jobspec to override the workflow spec
|
31
|
+
@subid = job_spec['subid'] || workflow_spec['subid']
|
32
|
+
@rawdir = job_spec['rawdir'] || workflow_spec['rawdir']
|
33
|
+
@origdir = job_spec['origdir'] || workflow_spec['origdir']
|
34
|
+
@procdir = job_spec['procdir'] || workflow_spec['procdir']
|
35
|
+
@statsdir = job_spec['statsdir'] || workflow_spec['statsdir']
|
36
|
+
@spmdir = job_spec['spmdir'] || workflow_spec['spmdir']
|
37
|
+
@scans = job_spec['scans'] || workflow_spec['scans']
|
38
|
+
@scan_labels = job_spec['scan_labels'] || workflow_spec['scan_labels']
|
39
|
+
@collision_policy = (job_spec['collision'] || workflow_spec['collision'] || COLLISION_POLICY).to_sym
|
40
|
+
@method = job_spec['method']
|
41
|
+
include_custom_methods(@method)
|
42
|
+
@libdir = File.dirname(Pathname.new(__FILE__).realpath)
|
43
|
+
|
44
|
+
job_requires 'subid'
|
45
|
+
end
|
46
|
+
|
47
|
+
# Dynamically load custom methods for advanced processing.
|
48
|
+
def include_custom_methods(module_name)
|
49
|
+
if module_name.nil? or ['default','wadrc'].include?(module_name)
|
50
|
+
# do nothing, use default implementation
|
51
|
+
else
|
52
|
+
require module_name
|
53
|
+
extend self.class.const_get(module_name.dot_camelize)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Setup directory path according to collision policy.
|
58
|
+
def setup_directory(path, logging_tag)
|
59
|
+
if File.exist?(path)
|
60
|
+
if @collision_policy == :destroy
|
61
|
+
puts "#{logging_tag} :: Deleting directory #{path}"
|
62
|
+
FileUtils.rm_rf(path)
|
63
|
+
FileUtils.mkdir_p(path)
|
64
|
+
elsif @collision_policy == :overwrite
|
65
|
+
puts "#{logging_tag} :: Overwriting inside directory #{path}"
|
66
|
+
else
|
67
|
+
raise(IOError, "Directory already exists, exiting: #{path}")
|
68
|
+
end
|
69
|
+
else
|
70
|
+
puts "#{logging_tag} :: Creating new directory #{path}"
|
71
|
+
FileUtils.mkdir_p(path)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check for required keys in instance variables.
|
76
|
+
def job_requires(*args)
|
77
|
+
check_instance_vars *args do |missing_vars|
|
78
|
+
error = "
|
79
|
+
Warning: Misconfiguration detected.
|
80
|
+
You are missing the following required variables from your spec file:
|
81
|
+
#{missing_vars.collect { |var| " - #{var} \n" } }
|
82
|
+
"
|
83
|
+
|
84
|
+
puts error
|
85
|
+
raise ScriptError, "Missing Vars: #{missing_vars.join(", ")}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def check_instance_vars(*args)
|
90
|
+
undefined_vars = []
|
91
|
+
args.each do |arg|
|
92
|
+
unless instance_variable_defined?("@" + arg) && eval("@" + arg).nil? == false
|
93
|
+
undefined_vars << arg
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
unless undefined_vars.size == 0
|
98
|
+
yield undefined_vars
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
########################################################################################################################
|
109
|
+
# A class for performing initial reconstruction of both functional and anatomical MRI scan acquisitions.
|
110
|
+
# Uses AFNI to convert from dicoms to 3D or 4D nifti files, initial volume stripping, and slice timing correction.
|
111
|
+
# Currently, supports dicoms or P-Files.
|
112
|
+
class Reconstruction < JobStep
|
113
|
+
require 'default_methods/default_recon'
|
114
|
+
include DefaultRecon
|
115
|
+
|
116
|
+
VOLUME_SKIP = 3 # number of volumes to strip from beginning of functional scans.
|
117
|
+
|
118
|
+
attr_accessor :scans, :volume_skip
|
119
|
+
|
120
|
+
# Instances are initialized with a properly configured hash containing all the information needed to drive
|
121
|
+
# reconstruction tasks. This hash is normally generated with a Pipe object.
|
122
|
+
def initialize(workflow_spec, recon_spec)
|
123
|
+
super(workflow_spec, recon_spec)
|
124
|
+
raise ScriptError, "At least one scan must be specified." if @scans.nil?
|
125
|
+
@volume_skip = recon_spec['volume_skip'] || VOLUME_SKIP
|
126
|
+
|
127
|
+
job_requires 'rawdir', 'origdir', 'scans'
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
############################################### END OF CLASS #########################################################
|
132
|
+
|
133
|
+
|
134
|
+
|
135
|
+
|
136
|
+
########################################################################################################################
|
137
|
+
# A class for performing spatial preprocessing steps on functional MRI data in preparation for first level stats.
|
138
|
+
# Preprocessing includes setting up output directories, linking all appropriate data, customizing a preconfigured spm
|
139
|
+
# job, running the job, calculating withing scan motion derivatives, and finally checking for excessive motion.
|
140
|
+
# The spm job should normally include tasks for realignment, normalization, and smoothing.
|
141
|
+
class Preprocessing < JobStep
|
142
|
+
require 'default_methods/default_preproc'
|
143
|
+
include DefaultPreproc
|
144
|
+
|
145
|
+
MOTION_THRESHOLD = 1 # maximum allowable realignment displacement in any direction
|
146
|
+
|
147
|
+
attr_accessor :tspec, :motion_threshold, :bold_reps
|
148
|
+
|
149
|
+
# Initialize instances with a hash normally generated by a Pipe object.
|
150
|
+
def initialize(workflow_spec, preproc_spec)
|
151
|
+
super(workflow_spec, preproc_spec)
|
152
|
+
@tspec = preproc_spec['template_spec']
|
153
|
+
@motion_threshold = preproc_spec['motion_threshold'] || MOTION_THRESHOLD
|
154
|
+
@bold_reps = preproc_spec['bold_reps']
|
155
|
+
|
156
|
+
job_requires 'origdir', 'procdir'
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
############################################### END OF CLASS #########################################################
|
161
|
+
|
162
|
+
|
163
|
+
|
164
|
+
|
165
|
+
|
166
|
+
########################################################################################################################
|
167
|
+
# A class used to compute the first level stats for a functional MRI visit data set.
|
168
|
+
# Currently very incomplete, any ideas for other data/attributes we need here?
|
169
|
+
class Stats < JobStep
|
170
|
+
require 'default_methods/default_stats'
|
171
|
+
include DefaultStats
|
172
|
+
|
173
|
+
attr_accessor :statsdir, :tspec, :onsetsfiles, :responses, :regressorsfiles, :bold_reps, :conditions
|
174
|
+
|
175
|
+
# Initialize instances with a hash normally generated by a Pipe object.
|
176
|
+
def initialize(workflow_spec, stats_spec)
|
177
|
+
super(workflow_spec, stats_spec)
|
178
|
+
@tspec = stats_spec['template_spec']
|
179
|
+
@onsetsfiles = stats_spec['onsetsfiles']
|
180
|
+
@responses = stats_spec['responses']
|
181
|
+
@regressorsfiles = stats_spec['regressorsfiles']
|
182
|
+
@bold_reps = stats_spec['bold_reps']
|
183
|
+
@conditions = stats_spec['conditions'] || workflow_spec['conditions']
|
184
|
+
|
185
|
+
job_requires 'bold_reps', 'conditions', 'procdir'
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
############################################### END OF CLASS #########################################################
|
190
|
+
|
191
|
+
|
192
|
+
|
193
|
+
|
194
|
+
|
195
|
+
########################################################################################################################
|
196
|
+
# An object that drives all the other pipeline classes in this module. Once initialized, an array of instances of each
|
197
|
+
# class are available to run segments of preprocessing.
|
198
|
+
class RPipe
|
199
|
+
|
200
|
+
include Log4r
|
201
|
+
include DefaultLogger
|
202
|
+
|
203
|
+
attr_accessor :recon_jobs, :preproc_jobs, :stats_jobs, :workflow_spec
|
204
|
+
|
205
|
+
libdir = File.expand_path(File.dirname(__FILE__))
|
206
|
+
|
207
|
+
# Initialize an RPipe instance by passing it a pipeline configuration driver.
|
208
|
+
# Drivers contain a list of entries, each of which contains all the
|
209
|
+
# information necessary to create an instance of the proper object that
|
210
|
+
# executes the job. Details on the formatting of the yaml drivers including examples will be
|
211
|
+
# provided in other documentation.
|
212
|
+
#
|
213
|
+
# The driver may be either a Hash or a yaml configuration file.
|
214
|
+
|
215
|
+
def initialize(driver)
|
216
|
+
@recon_jobs = []
|
217
|
+
@preproc_jobs = []
|
218
|
+
@stats_jobs = []
|
219
|
+
|
220
|
+
# A driver may be either a properly configured hash or a Yaml file containing
|
221
|
+
# the configuration.
|
222
|
+
@workflow_spec = driver.kind_of?(Hash) ? driver : read_driver_file(driver)
|
223
|
+
|
224
|
+
setup_logger
|
225
|
+
|
226
|
+
jobs = @workflow_spec['jobs']
|
227
|
+
jobs.each do |job_params|
|
228
|
+
@recon_jobs << Reconstruction.new(@workflow_spec, job_params) if job_params['step'] == 'reconstruct'
|
229
|
+
@preproc_jobs << Preprocessing.new(@workflow_spec, job_params) if job_params['step'] == 'preprocess'
|
230
|
+
@stats_jobs << Stats.new(@workflow_spec, job_params) if job_params['step'] == 'stats'
|
231
|
+
end
|
232
|
+
|
233
|
+
def jobs
|
234
|
+
[@recon_jobs, @preproc_jobs, @stats_jobs].flatten
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
239
|
+
# Reads a YAML driver file, parses it with ERB and returns the Configuration Hash.
|
240
|
+
# Raises an error if the file is not found in the file system.
|
241
|
+
def read_driver_file(driver_file)
|
242
|
+
raise(IOError, "Driver file not found: #{driver_file}") unless File.exist?(driver_file)
|
243
|
+
YAML.load(ERB.new(File.read(driver_file)).result)
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
# To compare jobs look at their configuration, not ruby object identity.
|
249
|
+
def ==(other_rpipe)
|
250
|
+
@workflow_spec == other_rpipe.workflow_spec
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
############################################### END OF CLASS #########################################################
|