boldqc-bu 0.0.1__tar.gz

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.
@@ -0,0 +1,26 @@
1
+ Copyright 2021 The President and Fellows of Harvard College. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification,
4
+ are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this
7
+ list of conditions and the following disclaimer.
8
+
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ 3. Neither the name of the copyright holder nor the names of its contributors
14
+ may be used to endorse or promote products derived from this software without
15
+ specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
21
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
24
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: boldqc-bu
3
+ Version: 0.0.1
4
+ Summary: BOLD QC
5
+ Home-page: https://github.com/kakurk/boldqc
6
+ Author: Kyle Kurkela
7
+ Author-email: kkurkela@bu.edu
8
+ License-File: LICENSE
9
+ Requires-Dist: selfie
10
+ Requires-Dist: executors
11
+ Requires-Dist: yaxil
12
+ Requires-Dist: matplotlib
13
+ Requires-Dist: nibabel
14
+ Requires-Dist: scipy
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: home-page
18
+ Dynamic: license-file
19
+ Dynamic: requires-dist
20
+ Dynamic: summary
@@ -0,0 +1,11 @@
1
+ # BOLD Quality Control (beta)
2
+ BOLDQC is an automated quality control pipeline for functional MRI scans. BOLDQC
3
+ is built on top of the excellent
4
+ [`dcm2niix`](https://github.com/rordenlab/dcm2niix),
5
+ [`FreeSurfer`](https://surfer.nmr.mgh.harvard.edu/),
6
+ and
7
+ [`FSL`](https://fsl.fmrib.ox.ac.uk/fsl)
8
+ software packages.
9
+
10
+ For the latest documentation please head over to [boldqc.readthedocs.io](https://boldqc.readthedocs.io).
11
+
@@ -0,0 +1,35 @@
1
+ import os
2
+ import json
3
+ import tarfile
4
+ import logging
5
+ from . import __version__
6
+
7
+ logger = logging.getLogger()
8
+
9
+ def version():
10
+ return __version__.__version__
11
+
12
+ def archive(indir, output):
13
+ with tarfile.open(output, 'w:gz') as tar:
14
+ tar.add(indir, os.path.basename(indir))
15
+
16
+ def get_mask_threshold(sidecar):
17
+ with open(sidecar, 'r') as fo:
18
+ js = json.load(fo)
19
+ bits_stored = js.get('BitsStored', None)
20
+ receive_coil = js.get('ReceiveCoilName', None)
21
+ if bits_stored == 12:
22
+ logger.info(f'scan has "{bits_stored}" bits and receive coil "{receive_coil}", setting mask threshold to 150.0')
23
+ return 150.0
24
+ if bits_stored == 16:
25
+ if receive_coil in ['Head_32']:
26
+ logger.info(f'scan has "{bits_stored}" bits and receive coil "{receive_coil}", setting mask threshold to 1500.0')
27
+ return 1500.0
28
+ if receive_coil in ['Head_64', 'HeadNeck_64']:
29
+ logger.info(f'scan has "{bits_stored}" bits and receive coil "{receive_coil}", setting mask threshold to 3000.0')
30
+ return 3000.0
31
+ raise MaskThresholdError(f'unexpected bits stored "{bits_stored}" + receive coil "{receive_coil}"')
32
+
33
+ class MaskThresholdError(Exception):
34
+ pass
35
+
@@ -0,0 +1,6 @@
1
+ __title__ = 'boldqc-bu'
2
+ __description__ = 'BOLD QC'
3
+ __url__ = 'https://github.com/kakurk/boldqc'
4
+ __version__ = '0.0.1'
5
+ __author__ = 'Kyle Kurkela'
6
+ __author_email__ = 'kkurkela@bu.edu'
@@ -0,0 +1,153 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import json
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class BIDS(object):
10
+ def __init__(self, base, sub, ses=None):
11
+ self._base = base
12
+ self._sub = sub
13
+ self._ses = ses
14
+
15
+ @staticmethod
16
+ def basename(sub, mod, ses=None, acq=None, ce=None, rec=None, run=None, defacemask=False):
17
+ # add sub (required)
18
+ b = 'sub-' + sub.replace('sub-', '')
19
+ if ses:
20
+ b += '_ses-' + ses.replace('ses-', '')
21
+ if acq:
22
+ b += '_acq-' + acq.replace('acq-', '')
23
+ if ce:
24
+ b += '_ce-' + ce.replace('ce-', '')
25
+ if rec:
26
+ b += '_rec-' + rec.replace('rec-', '')
27
+ if run:
28
+ b += '_run-' + str(run).replace('run-', '')
29
+ if defacemask:
30
+ b += '_mod-' + mod.replace('mod-', '')
31
+ b += '_defacemask'
32
+ else:
33
+ b += '_' + mod.replace('mod-', '')
34
+ return b
35
+
36
+ @staticmethod
37
+ def sidecar_for_image(image):
38
+ logger.info(image)
39
+ base = re.sub('\.nii.*$', '', image)
40
+ logger.info(base)
41
+ return base + '.json'
42
+
43
+ def derivatives_dir(self, name):
44
+ parts = [
45
+ self._sub
46
+ ]
47
+ if self._ses:
48
+ parts.append(self._ses)
49
+ return os.path.join(
50
+ self._base,
51
+ 'derivatives',
52
+ name,
53
+ *parts
54
+ )
55
+
56
+ def raw_anat(self, mod, sourcedata=False, **kwargs):
57
+ parts = [
58
+ self._sub
59
+ ]
60
+ if self._ses:
61
+ parts.append(self._ses)
62
+ if sourcedata:
63
+ dirname = os.path.join(self._base, 'sourcedata', *parts, 'anat')
64
+ else:
65
+ dirname = os.path.join(self._base, *parts, 'anat')
66
+ acq = kwargs.get('acq', None)
67
+ ce = kwargs.get('ce', None)
68
+ rec = kwargs.get('rec', None)
69
+ run = kwargs.get('run', None)
70
+ brainmask = kwargs.get('brainmask', None)
71
+ if acq:
72
+ parts.append('acq-' + acq)
73
+ if ce:
74
+ parts.append('ce-' + ce)
75
+ if rec:
76
+ parts.append('rec-' + rec)
77
+ if run:
78
+ parts.append('run-' + str(run))
79
+ if brainmask:
80
+ parts.append('mod-' + mod + '_brainmask')
81
+ else:
82
+ parts.append(mod)
83
+ basename = '_'.join(parts)
84
+ return dirname, basename
85
+
86
+ def raw_bold(self, mod, sourcedata=False, **kwargs):
87
+ parts = [
88
+ self._sub
89
+ ]
90
+ if self._ses:
91
+ parts.append(self._ses)
92
+ if sourcedata:
93
+ dirname = os.path.join(self._base, 'sourcedata', *parts, 'func')
94
+ else:
95
+ dirname = os.path.join(self._base, *parts, 'func')
96
+ acq = kwargs.get('acq', None)
97
+ ce = kwargs.get('ce', None)
98
+ rec = kwargs.get('rec', None)
99
+ run = kwargs.get('run', None)
100
+ brainmask = kwargs.get('brainmask', None)
101
+ if acq:
102
+ parts.append('acq-' + acq)
103
+ if ce:
104
+ parts.append('ce-' + ce)
105
+ if rec:
106
+ parts.append('rec-' + rec)
107
+ if run:
108
+ parts.append('run-' + str(run))
109
+ if brainmask:
110
+ parts.append('mod-' + mod + '_brainmask')
111
+ else:
112
+ parts.append(mod)
113
+ basename = '_'.join(parts)
114
+ return dirname, basename
115
+
116
+ @staticmethod
117
+ def parse(filename):
118
+ result = {
119
+ 'filename': filename
120
+ }
121
+ # get extension
122
+ match = re.match('^.*(.dicom|.json|.nii|.nii.gz)$', filename)
123
+ result['ext'] = match.group(1) if match else None
124
+ # get sub component
125
+ match = re.match('^sub-([^\W_]+).*\.(dicom|json|nii|nii.gz)$', filename)
126
+ result['sub'] = match.group(1) if match else None
127
+ # get ses component
128
+ match = re.match('.*_ses-([^\W_]+).*\.(dicom|json|nii|nii.gz)$', filename)
129
+ result['ses'] = match.group(1) if match else None
130
+ # get acq component
131
+ match = re.match('.*_acq-([^\W_]+).*\.(dicom|json|nii|nii.gz)$', filename)
132
+ result['acq'] = match.group(1) if match else None
133
+ # get run component
134
+ match = re.match('.*_run-(\d+).*\.(dicom|json|nii|nii.gz)$', filename)
135
+ result['run'] = match.group(1) if match else None
136
+ # get rec component
137
+ match = re.match('.*_rec-(\d+).*\.(dicom|json|nii|nii.gz)$', filename)
138
+ result['rec'] = match.group(1) if match else None
139
+ # get ce component
140
+ match = re.match('.*_ce-(\d+).*\.(dicom|json|nii|nii.gz)$', filename)
141
+ result['ce'] = match.group(1) if match else None
142
+ # get defacemask component
143
+ match = re.match('.*(_defacemask)\.(dicom|json|nii|nii.gz)$', filename)
144
+ result['defacemask'] = True if match else None
145
+ # get modality component
146
+ if result['defacemask']:
147
+ match = re.match('.*_mod-([^\W_]+)_defacemask\.(dicom|json|nii|nii.gz)$', filename)
148
+ result['mod'] = match.group(1) if match else None
149
+ else:
150
+ match = re.match('.*_([^\W_]+)\.(dicom|json|nii|nii.gz)$', filename)
151
+ result['mod'] = match.group(1) if match else None
152
+ return result
153
+
@@ -0,0 +1,3 @@
1
+ from . import get
2
+ from . import process
3
+ from . import tandem
@@ -0,0 +1,83 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import json
5
+ import yaml
6
+ import yaxil
7
+ import logging
8
+ import argparse as ap
9
+ import subprocess as sp
10
+ import collections as col
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ def do(args):
15
+ if args.insecure:
16
+ logger.warning('disabling ssl certificate verification')
17
+ yaxil.CHECK_CERTIFICATE = False
18
+
19
+ # load authentication data and set environment variables for ArcGet.py
20
+ auth = yaxil.auth2(
21
+ args.xnat_alias,
22
+ args.xnat_host,
23
+ args.xnat_user,
24
+ args.xnat_pass
25
+ )
26
+ os.environ['XNAT_HOST'] = auth.url
27
+ os.environ['XNAT_USER'] = auth.username
28
+ os.environ['XNAT_PASS'] = auth.password
29
+
30
+ # query BOLD scans
31
+ with yaxil.session(auth) as ses:
32
+ scans = col.defaultdict(dict)
33
+ for scan in ses.scans(label=args.label, project=args.project):
34
+ note = scan['note']
35
+ bold_match = re.match('.*(^|\s)#BOLD(?P<run>_\d+)?(\s|$).*', note, flags=re.IGNORECASE)
36
+ if bold_match:
37
+ run = bold_match.group('run')
38
+ run = re.sub('[^0-9]', '', run or '1')
39
+ run = int(run)
40
+ scans[run]['bold'] = scan['id']
41
+ logger.info(json.dumps(scans, indent=2))
42
+ for run,scansr in scans.items():
43
+ logger.info('getting bold run=%s, scan=%s', run, scansr['bold'])
44
+ get_bold(args, auth, run, scansr['bold'], verbose=args.verbose)
45
+
46
+ def get_bold(args, auth, run, scan, verbose=False):
47
+ config = {
48
+ 'func': {
49
+ 'bold': [
50
+ {
51
+ 'run': int(run),
52
+ 'scan': scan
53
+ }
54
+ ]
55
+ }
56
+ }
57
+ config = yaml.safe_dump(config)
58
+ cmd = [
59
+ 'ArcGet.py',
60
+ '--label', args.label,
61
+ '--output-dir', args.bids_dir,
62
+ '--output-format', 'bids',
63
+ ]
64
+ if args.project:
65
+ cmd.extend([
66
+ '--project', args.project
67
+ ])
68
+ if args.insecure:
69
+ cmd.extend([
70
+ '--insecure'
71
+ ])
72
+ if args.in_mem:
73
+ cmd.extend([
74
+ '--in-mem'
75
+ ])
76
+ cmd.extend([
77
+ '--config', '-'
78
+ ])
79
+ if verbose:
80
+ cmd.append('--debug')
81
+ logger.info(sp.list2cmdline(cmd))
82
+ sp.check_output(cmd, input=config.encode('utf-8'))
83
+
@@ -0,0 +1,110 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import json
5
+ import yaml
6
+ import yaxil
7
+ import glob
8
+ import math
9
+ import boldqc
10
+ import logging
11
+ import tarfile
12
+ import executors
13
+ import tempfile as tf
14
+ import subprocess as sp
15
+ import boldqc.tasks.stackcheck_ext as stackcheck_ext
16
+ import boldqc.tasks.niftiqa_wrapper as niftiqa_wrapper
17
+ from executors.models import Job, JobArray
18
+ from boldqc.bids import BIDS
19
+ from boldqc.xnat import Report
20
+ from boldqc.state import State
21
+
22
+ logger = logging.getLogger(__name__)
23
+ LIBEXEC = os.path.realpath(os.path.join(os.path.dirname(boldqc.__file__), 'libexec'))
24
+ os.environ['PATH'] = LIBEXEC + ':' + os.environ['PATH']
25
+
26
+ def do(args):
27
+ if args.insecure:
28
+ logger.warning('disabling ssl certificate verification')
29
+ yaxil.CHECK_CERTIFICATE = False
30
+
31
+ # create job executor and job array
32
+ if args.scheduler:
33
+ E = executors.get(args.scheduler, partition=args.partition)
34
+ else:
35
+ E = executors.probe(args.partition)
36
+ jarray = JobArray(E)
37
+
38
+ # create BIDS
39
+ B = BIDS(args.bids_dir, args.sub, ses=args.ses)
40
+ raw = B.raw_bold('bold', run=args.run)
41
+ logger.debug('BOLD raw: %s', raw)
42
+
43
+ # get repetition time from T1w sidecar for vNav processing
44
+ sidecar = os.path.join(*raw) + '.json'
45
+ logger.debug('sidecar: %s', sidecar)
46
+ with open(sidecar) as fo:
47
+ js = json.load(fo)
48
+ tr = js['RepetitionTime']
49
+ logger.debug('TR: %s', tr)
50
+
51
+ boldqc_outdir = None
52
+ infile = os.path.join(*raw) + '.nii.gz'
53
+ boldqc_outdir = B.derivatives_dir('boldqc')
54
+ boldqc_outdir = os.path.join(boldqc_outdir, 'func', raw[1])
55
+
56
+ # niftiqa job
57
+ task = niftiqa_wrapper.Task(
58
+ infile,
59
+ boldqc_outdir
60
+ )
61
+ logger.info(json.dumps(task.command, indent=1))
62
+ jarray.add(task.job)
63
+
64
+ # stackcheck_ext job
65
+ task = stackcheck_ext.Task(
66
+ infile,
67
+ boldqc_outdir
68
+ )
69
+ logger.info(json.dumps(task.command, indent=1))
70
+ jarray.add(task.job)
71
+
72
+ # submit jobs and wait for them to finish
73
+ if not args.dry_run:
74
+ logger.info('submitting jobs')
75
+ jarray.submit(limit=1)
76
+ logger.info('waiting for all jobs to finish')
77
+ jarray.wait()
78
+ numjobs = len(jarray.array)
79
+ failed = len(jarray.failed)
80
+ complete = len(jarray.complete)
81
+ if failed:
82
+ logger.info('%s/%s jobs failed', failed, numjobs)
83
+ for pid,job in iter(jarray.failed.items()):
84
+ logger.error('%s exited with returncode %s', job.name, job.returncode)
85
+ with open(job.output, 'r') as fp:
86
+ logger.error('standard output\n%s', fp.read())
87
+ with open(job.error, 'r') as fp:
88
+ logger.error('standard error\n%s', fp.read())
89
+ logger.info('%s/%s jobs completed', complete, numjobs)
90
+ if failed > 0:
91
+ sys.exit(1)
92
+
93
+ # artifacts directory
94
+ if not args.artifacts_dir:
95
+ args.artifacts_dir = os.path.join(
96
+ boldqc_outdir,
97
+ 'xnat-artifacts'
98
+ )
99
+
100
+ # build data to upload to XNAT
101
+ R = Report(args.bids_dir, args.sub, args.ses, args.run)
102
+ logger.info('building xnat artifacts to %s', args.artifacts_dir)
103
+ R.build_assessment(args.artifacts_dir)
104
+
105
+ # upload data to xnat over rest api
106
+ if args.xnat_upload:
107
+ logger.info('Uploading artifacts to XNAT')
108
+ auth = yaxil.auth2(args.xnat_alias)
109
+ yaxil.storerest(auth, args.artifacts_dir, 'boldqc-resource')
110
+
@@ -0,0 +1,66 @@
1
+ import os
2
+ import re
3
+ import json
4
+ import yaml
5
+ import yaxil
6
+ import logging
7
+ import yaxil.bids
8
+ import argparse as ap
9
+ import subprocess as sp
10
+ import boldqc.cli.get
11
+ import boldqc.cli.process
12
+ import collections as col
13
+ import yaxil.bids
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ def do(args):
18
+ if args.insecure:
19
+ logger.warning('disabling ssl certificate verification')
20
+ yaxil.CHECK_CERTIFICATE = False
21
+
22
+ # load authentication data and set environment variables for ArcGet.py
23
+ auth = yaxil.auth2(
24
+ args.xnat_alias,
25
+ args.xnat_host,
26
+ args.xnat_user,
27
+ args.xnat_pass
28
+ )
29
+ os.environ['XNAT_HOST'] = auth.url
30
+ os.environ['XNAT_USER'] = auth.username
31
+ os.environ['XNAT_PASS'] = auth.password
32
+
33
+ # query BOLD scans
34
+ with yaxil.session(auth) as ses:
35
+ scans = col.defaultdict(dict)
36
+ for scan in ses.scans(label=args.label, project=args.project):
37
+ note = scan['note']
38
+ bold_match = re.match('.*(^|\s)#BOLD(?P<run>_\d+)?(\s|$).*', note, flags=re.IGNORECASE)
39
+ if bold_match:
40
+ run = bold_match.group('run')
41
+ run = re.sub('[^0-9]', '', run or '1')
42
+ run = int(run)
43
+ scans[run]['bold'] = scan['id']
44
+
45
+ subject_label = scan['subject_label']
46
+ logger.info(json.dumps(scans, indent=2))
47
+
48
+ for run,scansr in scans.items():
49
+ if run != args.run:
50
+ continue
51
+ logger.info('getting bold run=%s, scan=%s', run, scansr['bold'])
52
+ boldqc.cli.get.get_bold(
53
+ args,
54
+ auth,
55
+ run,
56
+ scansr['bold'],
57
+ verbose=args.verbose
58
+ )
59
+ args.run = int(run)
60
+ args.run = int(run)
61
+ bids_ses_label = yaxil.bids.legal.sub('', args.label)
62
+ bids_sub_label = yaxil.bids.legal.sub('', subject_label)
63
+ args.sub = 'sub-' + bids_sub_label
64
+ args.ses = 'ses-' + bids_ses_label
65
+ logger.debug('sub=%s, ses=%s', args.sub, args.ses)
66
+ boldqc.cli.process.do(args)
@@ -0,0 +1,97 @@
1
+ #!/bin/bash
2
+
3
+ function isInt {
4
+ num="$1"
5
+ if ! [[ "$num" =~ ^-?[0-9]+$ ]]; then
6
+ return 0
7
+ fi
8
+ return 1
9
+ }
10
+
11
+ function isFloat {
12
+ num="$1"
13
+ if ! [[ "$num" =~ ^-?[0-9]+([.][0-9]+)?$ ]]; then
14
+ return 0
15
+ fi
16
+ return 1
17
+ }
18
+
19
+ function checkMode {
20
+ file="$1"
21
+ mode="$2"
22
+ if [ ! -e "$file" -a "$mode" != "c" ]; then
23
+ error "file does not exist '$file'"
24
+ fi
25
+ case $mode in
26
+ r)
27
+ if [ ! -r "$file" ]; then
28
+ error "file is unreadable '$file'"
29
+ fi
30
+ ;;
31
+ w)
32
+ if [ ! -r "$file" ]; then
33
+ error "file is not writable '$file'"
34
+ fi
35
+ ;;
36
+ x)
37
+ if [ ! -x "$file" ]; then
38
+ error "file is not executable '$file'"
39
+ fi
40
+ ;;
41
+ c)
42
+ parent=`dirname "$file"`
43
+ if [ ! -w "$parent" ]; then
44
+ error "cannot create file in '$parent'"
45
+ fi
46
+ ;;
47
+ esac
48
+ return 1;
49
+ }
50
+
51
+ function dirBackup {
52
+ dir="$1"
53
+ if [ -e "$dir" ]; then
54
+ newdir="$dir-`date +'%Y-%M-%d_%H.%M.%S'`"
55
+ echo -n "Moving directory $dir to $newdir... "
56
+ if [ ! -w "$dir" ]; then
57
+ error "permission denied."
58
+ else
59
+ mv "$dir" "$newdir" || error "failed."
60
+ fi
61
+ else
62
+ return 0
63
+ fi
64
+ echo "Done." && return 1
65
+ }
66
+
67
+ function error {
68
+ echo "[ERROR]: $1"
69
+ exit 1
70
+ }
71
+
72
+ function warn {
73
+ caller=`caller 1`
74
+ if [ $caller ]; then
75
+ echo -n "$caller ";
76
+ fi
77
+ echo "[WARN]: $1"
78
+ }
79
+
80
+ function check {
81
+ if [ -z `which "$1"` ]; then
82
+ [ -n "$2" ] && error "Could not find command $1 (hint: $2)"
83
+ error "Could not find command $1"
84
+ fi
85
+ }
86
+
87
+ function execute {
88
+ cmd="$1"
89
+ verbose="$2"
90
+ [ -n "$cmd" ] || error "No command supplied to execute function."
91
+ if [ "$verbose" = "0" ]; then
92
+ cmd="$cmd &> /dev/null"
93
+ else
94
+ echo "$cmd"
95
+ fi
96
+ eval "$cmd" && return $?
97
+ }