rpipe 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. data/.document +5 -0
  2. data/.gitignore +23 -0
  3. data/LICENSE +20 -0
  4. data/README +0 -0
  5. data/README.rdoc +33 -0
  6. data/Rakefile +78 -0
  7. data/VERSION +1 -0
  8. data/bin/create_driver.rb +79 -0
  9. data/bin/rpipe +131 -0
  10. data/bin/swallow_batch_run.rb +21 -0
  11. data/lib/core_additions.rb +5 -0
  12. data/lib/custom_methods/JohnsonMerit220Visit1Preproc.m +26 -0
  13. data/lib/custom_methods/JohnsonMerit220Visit1Preproc.rb +43 -0
  14. data/lib/custom_methods/JohnsonMerit220Visit1Preproc_job.m +80 -0
  15. data/lib/custom_methods/JohnsonMerit220Visit1Stats.m +74 -0
  16. data/lib/custom_methods/JohnsonMerit220Visit1Stats.rb +63 -0
  17. data/lib/custom_methods/JohnsonMerit220Visit1Stats_job.m +63 -0
  18. data/lib/custom_methods/JohnsonTbiLongitudinalSnodPreproc.m +26 -0
  19. data/lib/custom_methods/JohnsonTbiLongitudinalSnodPreproc.rb +41 -0
  20. data/lib/custom_methods/JohnsonTbiLongitudinalSnodPreproc_job.m +69 -0
  21. data/lib/custom_methods/JohnsonTbiLongitudinalSnodStats.m +76 -0
  22. data/lib/custom_methods/JohnsonTbiLongitudinalSnodStats.rb +67 -0
  23. data/lib/custom_methods/JohnsonTbiLongitudinalSnodStats_job.m +59 -0
  24. data/lib/custom_methods/ReconWithHello.rb +7 -0
  25. data/lib/default_logger.rb +13 -0
  26. data/lib/default_methods/default_preproc.rb +76 -0
  27. data/lib/default_methods/default_recon.rb +80 -0
  28. data/lib/default_methods/default_stats.rb +94 -0
  29. data/lib/default_methods/recon/physionoise_helper.rb +69 -0
  30. data/lib/default_methods/recon/raw_sequence.rb +109 -0
  31. data/lib/generators/job_generator.rb +36 -0
  32. data/lib/generators/preproc_job_generator.rb +31 -0
  33. data/lib/generators/recon_job_generator.rb +76 -0
  34. data/lib/generators/stats_job_generator.rb +70 -0
  35. data/lib/generators/workflow_generator.rb +128 -0
  36. data/lib/global_additions.rb +18 -0
  37. data/lib/logfile.rb +310 -0
  38. data/lib/matlab_helpers/CreateFunctionalVolumeStruct.m +6 -0
  39. data/lib/matlab_helpers/import_csv.m +32 -0
  40. data/lib/matlab_helpers/matlab_queue.rb +37 -0
  41. data/lib/matlab_helpers/prepare_onsets_xls.m +30 -0
  42. data/lib/rpipe.rb +254 -0
  43. data/rpipe.gemspec +177 -0
  44. data/spec/generators/preproc_job_generator_spec.rb +27 -0
  45. data/spec/generators/recon_job_generator_spec.rb +33 -0
  46. data/spec/generators/stats_job_generator_spec.rb +50 -0
  47. data/spec/generators/workflow_generator_spec.rb +97 -0
  48. data/spec/helper_spec.rb +40 -0
  49. data/spec/integration/johnson.merit220.visit1_spec.rb +47 -0
  50. data/spec/integration/johnson.tbi.longitudinal.snod_spec.rb +48 -0
  51. data/spec/logfile_spec.rb +96 -0
  52. data/spec/matlab_queue_spec.rb +40 -0
  53. data/spec/merit220_stats_spec.rb +81 -0
  54. data/spec/physio_spec.rb +98 -0
  55. data/test/drivers/merit220_workflow_sample.yml +15 -0
  56. data/test/drivers/mrt00000.yml +65 -0
  57. data/test/drivers/mrt00015.yml +62 -0
  58. data/test/drivers/mrt00015_hello.yml +41 -0
  59. data/test/drivers/mrt00015_withphys.yml +81 -0
  60. data/test/drivers/tbi000.yml +129 -0
  61. data/test/drivers/tbi000_separatevisits.yml +137 -0
  62. data/test/drivers/tmp.yml +58 -0
  63. data/test/fixtures/faces3_recognitionA.mat +0 -0
  64. data/test/fixtures/faces3_recognitionA.txt +86 -0
  65. data/test/fixtures/faces3_recognitionA_equal.csv +25 -0
  66. data/test/fixtures/faces3_recognitionA_unequal.csv +21 -0
  67. data/test/fixtures/faces3_recognitionB_incmisses.txt +86 -0
  68. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CPd3R_40.txt +13360 -0
  69. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CPd3_40.txt +13360 -0
  70. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CPttl_40.txt +13360 -0
  71. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CRTd3R_40.txt +13360 -0
  72. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CRTd3_40.txt +13360 -0
  73. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_CRTttl_40.txt +13360 -0
  74. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_CRTd3R_40.txt +334 -0
  75. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_CRTd3_40.txt +334 -0
  76. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_CRTttl_40.txt +334 -0
  77. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_RRT_40.txt +334 -0
  78. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_RVT_40.txt +334 -0
  79. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_card_spline_40.txt +334 -0
  80. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_HalfTR_resp_spline_40.txt +334 -0
  81. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_RRT_40.txt +9106 -0
  82. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_RVT_40.txt +9106 -0
  83. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_CRTd3R_40.txt +167 -0
  84. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_CRTd3_40.txt +167 -0
  85. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_CRTttl_40.txt +167 -0
  86. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_RRT_40.txt +167 -0
  87. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_RVT_40.txt +167 -0
  88. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_card_spline_40.txt +167 -0
  89. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_TR_resp_spline_40.txt +167 -0
  90. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_card_spline_40.txt +13360 -0
  91. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_resp_spline_40.txt +9106 -0
  92. data/test/fixtures/physionoise_regressors/EPI__fMRI_Task1_resp_spline_downsampled_40.txt +9106 -0
  93. data/test/fixtures/ruport_summary.yml +123 -0
  94. data/test/fixtures/valid_scans.yaml +35 -0
  95. data/test/helper.rb +10 -0
  96. data/test/test_dynamic_method_inclusion.rb +10 -0
  97. data/test/test_includes.rb +11 -0
  98. data/test/test_integrative_johnson.merit220.visit1.rb +31 -0
  99. data/test/test_preproc.rb +11 -0
  100. data/test/test_recon.rb +11 -0
  101. data/test/test_rpipe.rb +19 -0
  102. data/vendor/output_catcher.rb +93 -0
  103. data/vendor/trollop.rb +781 -0
  104. 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,6 @@
1
+ function volume_struct = CreateFunctionalVolumeStruct(studypath, image, bold_reps)
2
+
3
+ volume_struct = cell(bold_reps,1);
4
+ for vol = 1:bold_reps
5
+ volume_struct{vol} = strcat(studypath, image, ',', int2str(vol));
6
+ 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 #########################################################