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
@@ -0,0 +1,94 @@
1
+ module DefaultStats
2
+
3
+ # runs the complete set of tasks using data in a subject's "proc" directory and a preconfigured template spm job.
4
+ def run_first_level_stats
5
+ flash "Highway to the dangerzone..."
6
+ setup_directory(@statsdir, "STATS")
7
+
8
+ Dir.chdir(@statsdir) do
9
+ link_files_from_proc_directory
10
+ link_onsets_files
11
+ customize_template_job
12
+ run_stats_spm_job
13
+ end
14
+
15
+ end
16
+
17
+ alias_method :perform, :run_first_level_stats
18
+
19
+ # Links all the files necessary from the "proc" directory. Links written to current working directory.
20
+ def link_files_from_proc_directory(images_wildcard = File.join(@procdir, "swq*.nii"), motion_regressors_wildcard = File.join(@procdir, "md_rp*.txt"))
21
+ puts Dir.glob(images_wildcard), @statsdir
22
+ raise IOError, "No images to link with #{images_wildcard}" if Dir.glob(images_wildcard).empty?
23
+ system("ln -s #{images_wildcard} #{@statsdir}")
24
+
25
+ raise IOError, "No motion_regressors to link with #{motion_regressors_wildcard}" if Dir.glob(motion_regressors_wildcard).empty?
26
+ system("ln -s #{motion_regressors_wildcard} #{@statsdir}")
27
+ end
28
+
29
+ # Links to a preconfigured onsets file, a matlab file that contains three cell arrays: names, onsets, durations.
30
+ # This file is used by SPM to configure the conditions of the function task and the onset times to use in the model.
31
+ # Link is written to current working directory.
32
+ def link_onsets_files
33
+ @onsetsfiles.each do |ofile|
34
+ # Check if File Path is Absolute. If not link from procdir/onsets
35
+ opath = Pathname.new(ofile).absolute? ? ofile : File.join(@procdir, 'onsets', ofile)
36
+ system("ln -s #{File.expand_path(opath)} #{Dir.pwd}")
37
+ end
38
+ end
39
+
40
+ # Copies the template job to the current working directory, then customizes it by performing a set of recursive
41
+ # string replacements specified in the template_spec.
42
+ def customize_template_job
43
+ # TODO
44
+ end
45
+
46
+ # Finally runs the stats job and writes output to current working directory.
47
+ def run_stats_spm_job
48
+ # TODO
49
+ end
50
+
51
+ # Create onsets files using logfile responses for given conditions.
52
+ #
53
+ # Responses is a hash containing a directory and filenames of the .txt
54
+ # button-press response logfiles formatted by Presentation's SDF.
55
+ #
56
+ # responses = { 'directory' => '/path/to/files', 'logfiles' => ['subid_taskB.txt', 'subid_taskA.txt']}
57
+ #
58
+ # Conditions is an array of vector labels to extract from the .txt file
59
+ # Each label may be either an individual condition or a hash of multiple
60
+ # conditions, with the combined label as the key and the separate labels as
61
+ # values.
62
+ #
63
+ # conditions = [:new, :old, {:misses => [:new_misses, :old_misses]} ]
64
+ def create_onsets_files(responses, conditions)
65
+ onsets_csv_files = []
66
+ onsets_mat_files = []
67
+ wd = Dir.pwd
68
+ matching_directories = Dir.glob(responses['directory'])
69
+ raise IOError, "Response directory #{responses['directory']} doesn't exist." unless File.directory?(responses['directory'])
70
+ raise IOError, "Only one response directory currently accepted (matched directories: #{matching_directories.join(', ')})" unless matching_directories.length == 1
71
+ Dir.chdir matching_directories.first do
72
+ responses['logfiles'].each do |logfile|
73
+ # Either Strip off the prefix directly without changing the name...
74
+ # prefix = File.basename(logfile, '.txt')
75
+ # Or create a new name based on standard logfile naming scheme:
76
+ # mrt00000_abc_021110_faces3_recognitionA.txt
77
+ prefix = File.basename(logfile, '.txt').split("_").values_at(0,3,4).join("_")
78
+ log = Logfile.new(logfile, *conditions)
79
+
80
+ # puts log.to_csv
81
+ onsets_csv_files << log.write_csv(prefix + '.csv')
82
+ onsets_mat_files << log.write_mat(prefix)
83
+ end
84
+
85
+ [onsets_csv_files, onsets_mat_files].flatten.each do |response_file|
86
+ FileUtils.move response_file, wd
87
+ end
88
+ end
89
+
90
+ return onsets_mat_files
91
+ end
92
+
93
+
94
+ end
@@ -0,0 +1,69 @@
1
+ module DefaultRecon
2
+ # Create Physionoise Regressors for Inclusion in GLM
3
+ def create_physiosnoise_regressors(scan_spec)
4
+ runs = build_physionoise_run_spec(scan_spec)
5
+ Physionoise.run_physionoise_on(runs, ["--saveFiles"])
6
+ end
7
+
8
+ # Generate a Physionoise Spec
9
+ def generate_physiospec
10
+ physiospec = Physiospec.new(@rawdir, File.join(@rawdir, '..', 'cardiac'))
11
+ physiospec.epis_and_associated_phys_files
12
+ end
13
+
14
+ # Build a Run Spec from a Scan Spec
15
+ # This should be moved to the generators and shouldn't be used here.
16
+ def build_physionoise_run_spec(rpipe_scan_spec)
17
+ run = rpipe_scan_spec['physio_files'].dup
18
+ flash "Physionoise Regressors: #{run[:phys_directory]}"
19
+ run[:bold_reps] = rpipe_scan_spec['bold_reps']
20
+ run[:rep_time] = rpipe_scan_spec['rep_time']
21
+ unless Pathname.new(run[:phys_directory]).absolute?
22
+ run[:phys_directory] = File.join(@rawdir, run[:phys_directory])
23
+ end
24
+ run[:run_directory] = @rawdir
25
+ runs = [run]
26
+ end
27
+
28
+ # Runs 3dRetroicor for a scan.
29
+ # Returns the output filename if successful or raises an error if there was an error.
30
+ def run_retroicor(physio_files, file)
31
+ icor_cmd, outfile = build_retroicor_cmd(physio_files, file)
32
+ flash "3dRetroicor: #{file} \n #{icor_cmd}"
33
+ if run(icor_cmd)
34
+ return outfile
35
+ else
36
+ raise ScriptError, "Problem running #{icor_cmd}"
37
+ end
38
+ end
39
+
40
+ # Builds a properly formed 3dRetroicor command and returns the command and
41
+ # output filename.
42
+ #
43
+ # Input a physio_files hash with keys:
44
+ # :respiration_signal: RESPData_epiRT_0303201014_46_27_463
45
+ # :respiration_trigger: RESPTrig_epiRT_0303201014_46_27_463
46
+ # :phys_directory: cardiac/
47
+ # :cardiac_signal: PPGData_epiRT_0303201014_46_27_463
48
+ # :cardiac_trigger: PPGTrig_epiRT_0303201014_46_27_463
49
+ def build_retroicor_cmd(physio_files, file)
50
+ [:cardiac_signal, :respiration_signal].collect {|req| raise ScriptError, "Missing physio config: #{req}" unless physio_files.include?(req)}
51
+
52
+ prefix = 'p'
53
+ unless Pathname.new(physio_files[:cardiac_signal]).absolute?
54
+ cardiac_signal = File.join(@rawdir, physio_files[:phys_directory], physio_files[:cardiac_signal])
55
+ end
56
+
57
+ unless Pathname.new(physio_files[:respiration_signal]).absolute?
58
+ respiration_signal = File.join(@rawdir, physio_files[:phys_directory], physio_files[:respiration_signal])
59
+ end
60
+
61
+ outfile = prefix + file
62
+
63
+ icor_format = "3dretroicor -prefix %s -card %s -resp %s %s"
64
+ icor_options = [outfile, cardiac_signal, respiration_signal, file]
65
+ icor_cmd = icor_format % icor_options
66
+ return icor_cmd, outfile
67
+ end
68
+
69
+ end
@@ -0,0 +1,109 @@
1
+ module DefaultRecon
2
+ # An abstract class for Raw Image Sequences
3
+ # The Recon job will calls prepare on Raw Sequence Instances to process
4
+ # them from their raw state (dicoms or pfiles) to Nifti files suitable for
5
+ # processing.
6
+ class RawSequence
7
+ def initialize(scan_spec, rawdir)
8
+ @scan_spec = scan_spec
9
+ @rawdir = rawdir
10
+ end
11
+ end
12
+
13
+ # Manage a folder of Raw Dicoms for Nifti file conversion
14
+ class DicomRawSequence < RawSequence
15
+ # Locally copy and unzip a folder of Raw Dicoms and call convert_sequence on them
16
+ def prepare_and_convert_sequence(outfile)
17
+ scandir = File.join(@rawdir, @scan_spec['dir'])
18
+ $Log.info "Dicom Reconstruction: #{scandir}"
19
+ Pathname.new(scandir).all_dicoms do |dicoms|
20
+ convert_sequence(dicoms, outfile)
21
+ end
22
+ end
23
+
24
+ alias_method :prepare, :prepare_and_convert_sequence
25
+
26
+ private
27
+
28
+ # Convert a folder of unzipped Dicom files to outfile
29
+ def convert_sequence(dicoms, outfile)
30
+ local_scandir = File.dirname(dicoms.first)
31
+ second_file = Dir.glob( File.join(local_scandir, "*0002*") )
32
+ wildcard = File.join(local_scandir, "*.[0-9]*")
33
+
34
+ recon_cmd_format = 'to3d -skip_outliers %s -prefix tmp.nii "%s"'
35
+
36
+ timing_opts = timing_options(@scan_spec, second_file)
37
+
38
+ unless run(recon_cmd_format % [timing_opts, wildcard])
39
+ raise(IOError,"Failed to reconstruct scan: #{scandir}")
40
+ end
41
+ end
42
+
43
+ # Determines the proper timing options to pass to to3d for functional scans.
44
+ # Must pass a static path to the second file in the series to determine zt
45
+ # versus tz ordering. Assumes 2sec TR's. Returns the options as a string
46
+ # that may be empty if the scan is an anatomical.
47
+ def timing_options(scan_spec, second_file)
48
+ return "" if scan_spec['type'] == "anat"
49
+ instance_offset = scan_spec['z_slices'] + 1
50
+ if system("dicom_hdr #{second_file} | grep .*REL.Instance.*#{instance_offset}")
51
+ return "-epan -time:tz #{scan_spec['bold_reps']} #{scan_spec['z_slices']} 2000 alt+z"
52
+ else
53
+ return "-epan -time:zt #{scan_spec['z_slices']} #{scan_spec['bold_reps']} 2000 alt+z"
54
+ end
55
+ end
56
+ end
57
+
58
+ # Reconstucts a PFile from Raw to Nifti File
59
+ class PfileRawSequence < RawSequence
60
+ # Create a local unzipped copy of the Pfile and prepare Scanner Reference Data for reconstruction
61
+ def initialize(scan_spec, rawdir)
62
+ super(scan_spec, rawdir)
63
+
64
+ base_pfile_path = File.join(@rawdir, @scan_spec['pfile'])
65
+ pfile_path = File.exist?(base_pfile_path) ? base_pfile_path : base_pfile_path + '.bz2'
66
+
67
+ raise IOError, "#{pfile_path} does not exist." unless File.exist?(pfile_path)
68
+
69
+ flash "Pfile Reconstruction: #{pfile_path}"
70
+ @pfile_data = Pathname.new(pfile_path).local_copy
71
+
72
+ @refdat_file = @scan_spec['refdat_stem'] ||= search_for_refdat_file
73
+ setup_refdat(@refdat_file)
74
+ end
75
+
76
+ # Reconstructs a single pfile using epirecon
77
+ # Outfile may include a '.nii' extension - a nifti file will be constructed
78
+ # directly in this case.
79
+ def reconstruct_sequence(outfile)
80
+ volumes_to_skip = @scan_spec['volumes_to_skip'] ||= 3
81
+ epirecon_cmd_format = "epirecon_ex -f %s -NAME %s -skip %d -scltype=0"
82
+ epirecon_cmd_options = [@pfile_data, outfile, volumes_to_skip]
83
+ epirecon_cmd = epirecon_cmd_format % epirecon_cmd_options
84
+ raise ScriptError, "Problem running #{epirecon_cmd}" unless run(epirecon_cmd)
85
+ end
86
+
87
+ alias_method :prepare, :reconstruct_sequence
88
+
89
+ private
90
+
91
+ # Find an appropriate ref.dat file if not provided in the scan spec.
92
+ def search_for_refdat_file
93
+ Dir.new(@rawdir).each do |file|
94
+ return file if file =~ /ref.dat/
95
+ end
96
+ raise ScriptError, "No candidate ref.dat file found in #{@rawdir}"
97
+ end
98
+
99
+ # Create a new unzipped local copy of the ref.dat file and link it into
100
+ # pwd for reconstruction.
101
+ def setup_refdat(refdat_stem)
102
+ base_refdat_path = File.join(@rawdir, refdat_stem)
103
+ refdat_path = File.exist?(base_refdat_path) ? base_refdat_path : base_refdat_path + ".bz2"
104
+ raise IOError, "#{refdat_path} does not exist." unless File.exist?(refdat_path)
105
+ local_refdat_file = Pathname.new(refdat_path).local_copy
106
+ FileUtils.ln_s(local_refdat_file, Dir.pwd, :force => true)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,36 @@
1
+ ########################################################################################################################
2
+ # A class for parsing a data directory and creating Job Specs
3
+ class JobGenerator
4
+ # Configuration details are put in a spec hash and used to drive processing.
5
+ attr_reader :spec
6
+ # The spec hash of a previous step (i.e. the recon hash to build a preprocessing hash on.)
7
+ attr_reader :previous_step
8
+
9
+ # Intialize spec and previous step and set job defaults.
10
+ def initialize(config = {})
11
+ @spec = {}
12
+ config_defaults = {}
13
+ @config = config_defaults.merge(config)
14
+
15
+ @previous_step = @config['previous_step']
16
+ @spec['method'] = @config['method'] if @config['method']
17
+ end
18
+
19
+ def config_requires(*args)
20
+ missing_args = [*args.collect { |arg| arg unless @config.has_key?(arg) }].flatten.compact
21
+ unless missing_args.empty?
22
+ raise DriverConfigError, "Missing Configuration for: #{missing_args.join(', ')}"
23
+ end
24
+ end
25
+
26
+ def spec_validates(*args)
27
+ invalid_args = [*args.collect{ |arg| arg if eval("@spec['#{arg}'].nil?") }].flatten.compact
28
+ unless invalid_args.empty?
29
+ raise DriverConfigError, "Job could not create: #{invalid_args.join(', ')}"
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ # Raised when a JobGenerator class is missing required information.
36
+ class DriverConfigError < ScriptError; end
@@ -0,0 +1,31 @@
1
+ require 'generators/job_generator'
2
+
3
+ ########################################################################################################################
4
+ # A class for parsing a data directory and creating a default Preprocessing Job
5
+ # Intialize a PreprocJobGenerator with a config hash including the following optional keys:
6
+ #
7
+ # - scans : A hash of scans upon which to base preprocessing.
8
+ # Used to extract the correct bold reps for SPM.
9
+ # See #ReconJobGenerator for options for the scans hash.
10
+ #
11
+
12
+ class PreprocJobGenerator < JobGenerator
13
+ def initialize(config = {})
14
+ config_defaults = {}
15
+ super config_defaults.merge(config)
16
+
17
+ @spec['step'] = 'preprocess'
18
+
19
+ config_requires 'scans'
20
+ end
21
+
22
+ # Build a job spec and return it.
23
+ def build
24
+ bold_reps = []
25
+ @spec['bold_reps'] = @config['scans'].collect do |scan|
26
+ scan['bold_reps'] - scan['volumes_to_skip']
27
+ end
28
+
29
+ return @spec
30
+ end
31
+ end
@@ -0,0 +1,76 @@
1
+ gem 'activeresource', '<=2.3.8'
2
+ $LOAD_PATH.unshift('~/projects/metamri/lib').unshift('~/code/metamri/lib')
3
+ require 'metamri'
4
+ require 'generators/job_generator'
5
+
6
+ ########################################################################################################################
7
+ # A class for parsing a data directory and creating a default Reconstruction Job
8
+ # Intialize the ReconJobGenerator with the following options in a config hash.
9
+ #
10
+ # Required Options:
11
+ # - rawdir : The directory containing EPI runs
12
+ #
13
+ # Raises an IOError if the Raw Directory cannot be read, and a
14
+ # DriverConfigError if the Raw Directory is not specified.
15
+
16
+ class ReconJobGenerator < JobGenerator
17
+ def initialize(config)
18
+ # Add job-specific config defaults to config and initialize teh JobGenerator with them.
19
+ config_defaults = {}
20
+ config_defaults['epi_pattern'] = /fMRI/i
21
+ config_defaults['ignore_patterns'] = [/pcasl/i]
22
+ config_defaults['volumes_to_skip'] = 3
23
+ super config_defaults.merge(config)
24
+
25
+ @spec['step'] = 'reconstruct'
26
+
27
+ config_requires 'rawdir'
28
+ @rawdir = @config['rawdir']
29
+ raise IOError, "Can't find raw directory #{@rawdir}" unless File.readable?(@rawdir)
30
+ end
31
+
32
+ def build
33
+ scans = Array.new
34
+
35
+ visit = VisitRawDataDirectory.new(@rawdir)
36
+ # Scan the datasets, ignoring unwanted (very large unused) directories.
37
+ visit.scan(:ignore_patterns => [@config['ignore_patterns']].flatten)
38
+
39
+ visit.datasets.each do |dataset|
40
+ # Only build hashes for EPI datasets
41
+ next unless dataset.series_description =~ @config['epi_pattern']
42
+
43
+ scans << build_scan_hash(dataset)
44
+ end
45
+
46
+ @spec['scans'] = scans
47
+
48
+ return @spec
49
+ end
50
+
51
+ # Returns a hash describing how to reconstruct the dataset.
52
+ def build_scan_hash(dataset)
53
+ scan = {}
54
+ raw_image_file = dataset.raw_image_files.first
55
+ # phys = Physionoise.new(@rawdir, File.join(@rawdir, '..', 'cardiac' ))
56
+
57
+ scan['dir'] = dataset.relative_dataset_path
58
+ scan['type'] = 'func'
59
+ scan['z_slices'] = raw_image_file.num_slices
60
+ scan['bold_reps'] = raw_image_file.bold_reps
61
+ scan['volumes_to_skip'] = @config['volumes_to_skip']
62
+ scan['rep_time'] = raw_image_file.rep_time.in_seconds
63
+ scan['label'] = dataset.series_description.escape_filename
64
+ # scan['task'] = '?'
65
+ # scan['physio_files'] = "#TODO"
66
+
67
+ return scan
68
+ end
69
+ end
70
+
71
+ # Convert Milliseconds to Seconds for TRs
72
+ class Float
73
+ def in_seconds
74
+ self / 1000.0
75
+ end
76
+ end
@@ -0,0 +1,70 @@
1
+ require 'generators/job_generator'
2
+ require 'logfile'
3
+
4
+ ########################################################################################################################
5
+ # A class for parsing a data directory and creating a default Stats Job
6
+ # Intialize a StatsJobGenerator with a config hash including the following optional keys:
7
+ #
8
+ # - subid : SubjectID (i.e. 'mrt00015')
9
+ # - conditions : An array of condition names for analysis.
10
+ # - scans : A hash containing scan information (labels, bold_reps, etc.)
11
+
12
+ class StatsJobGenerator < JobGenerator
13
+ def initialize(config = {})
14
+ config_defaults = {}
15
+ config_defaults['epi_task_pattern'] = /Task/i
16
+ config_defaults['regressors_prefix'] = 'rp_a'
17
+ super config_defaults.merge(config)
18
+
19
+ @spec['step'] = 'stats'
20
+
21
+ config_requires 'scans', 'subid', 'conditions', 'responses_dir'
22
+
23
+ @scans = []
24
+ @config['scans'].each { |scan| @scans << scan if scan['label'] =~ @config['epi_task_pattern'] }
25
+
26
+ end
27
+
28
+ def build
29
+ @spec['bold_reps'] = bold_reps
30
+ @spec['responses'] = responses
31
+ @spec['conditions'] = @config['conditions']
32
+ @spec['regressorsfiles'] = regressorsfiles
33
+ return @spec
34
+ end
35
+
36
+ def bold_reps
37
+ return @bold_reps if @bold_reps
38
+ bold_reps = []
39
+ @scans.collect {|scan| scan['bold_reps'] - scan['volumes_to_skip']}
40
+ end
41
+
42
+ # A getter/builder method for behavioural responses.
43
+ def responses
44
+ return @responses if @responses
45
+ @responses = {}
46
+ @responses['directory'] = @config['responses_dir']
47
+ @responses['logfiles'] = logfiles
48
+
49
+ return @responses
50
+ end
51
+
52
+ def logfiles
53
+ return @logfiles if @logfiles
54
+ logfiles = Dir.glob(File.join(@responses['directory'], @config['subid'] + "*.txt"))
55
+ raise IOError, "No logfiles found in #{@responses['directory']} matching #{@config['subid']}" if logfiles.empty?
56
+ logfiles = logfiles.collect! {|file| Logfile.new(file)}.sort
57
+ @logfiles = logfiles.collect! {|file| File.basename(file.textfile) }
58
+ end
59
+
60
+ def regressorsfiles
61
+ return @regressorsfiles if @regressorsfiles
62
+ regressorsfiles = []
63
+ @regressorsfiles = @scans.collect {|scan| "%s%s_%s.txt" % [ @config['regressors_prefix'], @config['subid'], scan['label'] ]}
64
+ end
65
+
66
+ def valid?
67
+ spec_validates 'regressorsfiles', 'responses'
68
+ end
69
+
70
+ end
@@ -0,0 +1,128 @@
1
+ require 'tmpdir'
2
+ require 'pathname'
3
+
4
+ require 'core_additions'
5
+ require 'generators/recon_job_generator'
6
+ require 'generators/preproc_job_generator'
7
+ require 'generators/stats_job_generator'
8
+
9
+ ########################################################################################################################
10
+ # A class for parsing a data directory and creating default Driver Configurations
11
+ # Intialize a WorkflowGenerator with a Raw Directory containing Scans
12
+ # and with the following optional keys in a config hash:
13
+ #
14
+ # Directory Options:
15
+ # - processing_dir : A directory common to orig, proc and stats directories, if they are not explicitly specified..
16
+ # - origdir : A directory where dicoms will be converted to niftis and basic preprocessing occurs.
17
+ # - procdir : A directory for detailed preprocessing (normalization and smoothing)
18
+ # - statsdir : A directory where stats will be saved. (This should be a final directory.)
19
+
20
+ class WorkflowGenerator < JobGenerator
21
+ attr_reader :spec
22
+
23
+ def initialize(rawdir, config = Hash.new)
24
+ config_defaults = {}
25
+ config_defaults['conditions'] = ['new_correct', 'new_incorrect', 'old_correct', 'old_incorrect']
26
+ config_defaults['processing_dir'] = Dir.mktmpdir
27
+ super config_defaults.merge(config)
28
+
29
+ @rawdir = rawdir
30
+ @spec['rawdir'] = @rawdir
31
+ @spec['subid'] = parse_subid
32
+ @spec['study_procedure'] = @config['study_procedure'] ||= guess_study_procedure_from(@rawdir)
33
+
34
+ config_requires 'responses_dir'
35
+ end
36
+
37
+ # Create and return a workflow spec to drive processing
38
+ def build
39
+ configure_directories
40
+
41
+ @spec['collision'] = 'destroy'
42
+
43
+
44
+ jobs = []
45
+
46
+ # Recon
47
+ recon_options = {'rawdir' => @rawdir, 'epi_pattern' => /(Resting|Task)/i, }
48
+ config_step_method(recon_options, 'recon') if @config['custom_methods']
49
+ jobs << ReconJobGenerator.new(recon_options).build
50
+
51
+ # Preproc
52
+ preproc_options = {'scans' => jobs.first['scans']}
53
+ config_step_method(preproc_options, 'preproc') if @config['custom_methods']
54
+ jobs << PreprocJobGenerator.new(preproc_options).build
55
+
56
+ # Stats
57
+ stats_options = {
58
+ 'scans' => jobs.first['scans'],
59
+ 'conditions' => @config['conditions'],
60
+ 'responses_dir' => @config['responses_dir'],
61
+ 'subid' => @spec['subid']
62
+ }
63
+ config_step_method(stats_options, 'stats') if @config['custom_methods']
64
+ jobs << StatsJobGenerator.new(stats_options).build
65
+
66
+ @spec['jobs'] = jobs
67
+
68
+ return @spec
69
+ end
70
+
71
+ # Guesses a Subject Id from @rawdir
72
+ # Takes the split basename of rawdir itself if rawdir includes subdir, or
73
+ # the basename of its parent.
74
+ def parse_subid
75
+ subject_path = File.basename(@rawdir) == 'dicoms' ?
76
+ Pathname.new(File.join(@rawdir, '..')).realpath : Pathname.new(@rawdir).realpath
77
+
78
+ subject_path.basename.to_s.split('_').first
79
+ end
80
+
81
+ # Handle Directory Configuration and Defaults for orig, proc and stats dirs.
82
+ def configure_directories
83
+ processing_dir = @config['processing_dir']
84
+ @spec['origdir'] = @config['origdir'] || parse_directory_format(@config['directory_formats']['origdir']) || File.join(processing_dir, @spec['subid'] + '_orig')
85
+ @spec['procdir'] = @config['procdir'] || parse_directory_format(@config['directory_formats']['procdir']) || File.join(processing_dir, @spec['subid'] + '_proc')
86
+ @spec['statsdir'] = @config['statsdir'] || parse_directory_format(@config['directory_formats']['statsdir']) || File.join(processing_dir, @spec['subid'] + '_stats')
87
+ end
88
+
89
+ # Replace a directory format string with respective values from the spec.
90
+ # For example, replace the string "/Data/<study_procedure>/<subid>/stats" from
91
+ # a workflow_driver['directory_formats']['statsdir'] with
92
+ # "/Data/johnson.merit220.visit1/mrt00000/stats"
93
+ def parse_directory_format(fmt)
94
+ dir = fmt.dup
95
+ dir.scan(/<\w*>/).each do |replacement|
96
+ key = replacement.to_s.gsub(/(<|>)/, '')
97
+ dir.sub!(/<\w*>/, @spec[key])
98
+ end
99
+ return dir
100
+ end
101
+
102
+ # Guess a StudyProcedure from the data's raw directory.
103
+ # A properly formed study procdure should be: <PI>.<Study>.<Description or Visit>
104
+ # Raises a ScriptError if it couldn't guess a reasonable procedure.
105
+ def guess_study_procedure_from(dir)
106
+ dirs = dir.split("/")
107
+ while dirs.empty? == false do
108
+ current_dir = dirs.pop
109
+ return current_dir if current_dir =~ /\w*\.\w*\.\w*/
110
+ end
111
+ raise ScriptError, "Could not guess study procedure from #{dir}"
112
+ end
113
+
114
+ # Configure Custom Methods from the Workflow Driver
115
+ #
116
+ # Custom methods may be simply set to true for a given job or listed
117
+ # explicitly. If true, they will set the method to the a camelcased version
118
+ # of the study_procedure and step, i.e. JohnsonMerit220Visit1Stats
119
+ # If listed explicitly, it will set the step to the value listed.
120
+ def config_step_method(options, step)
121
+ if @config['custom_methods'][step].class == String
122
+ options['method'] = @config['custom_methods'][step]
123
+ elsif @config['custom_methods'][step] == true
124
+ options['method'] = [@config['study_procedure'], step.capitalize].join("_").dot_camelize
125
+ end
126
+ end
127
+
128
+ end
@@ -0,0 +1,18 @@
1
+ require 'popen4'
2
+
3
+ # Global Method to Log and Run system commands.
4
+ def run(command)
5
+ $CommandLog.info command
6
+
7
+ status = POpen4::popen4(command) do |stdout, stderr|
8
+ puts stdout.read.strip
9
+ puts stderr.read.strip
10
+ end
11
+
12
+ status && status.exitstatus == 0 ? true : false
13
+ end
14
+
15
+ # Global Method to display a message and the date/time to standard output.
16
+ def flash(msg)
17
+ $Log.info msg
18
+ end