rpipe 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 #########################################################
|