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.
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 #########################################################