oceanproc 1.2.2__tar.gz → 1.3.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.
Files changed (21) hide show
  1. {oceanproc-1.2.2 → oceanproc-1.3.1}/PKG-INFO +1 -1
  2. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/bids_wrapper.py +40 -18
  3. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/events_long.py +6 -1
  4. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/group_series.py +8 -34
  5. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/ocean_proc.py +54 -27
  6. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/preprocessing_wrapper.py +51 -15
  7. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/segmentation_wrapper.py +33 -8
  8. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/utils.py +38 -7
  9. {oceanproc-1.2.2 → oceanproc-1.3.1}/pyproject.toml +1 -1
  10. {oceanproc-1.2.2 → oceanproc-1.3.1}/README.md +0 -0
  11. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/__init__.py +0 -0
  12. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/firstlevel/__init__.py +0 -0
  13. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/firstlevel/run_glm.py +0 -0
  14. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/firstlevel/tests/__init__.py +0 -0
  15. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/firstlevel/tests/test_run_glm.py +0 -0
  16. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/grouplevel/__init__.py +0 -0
  17. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/grouplevel/computations.py +0 -0
  18. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/grouplevel/design_mat_utils.py +0 -0
  19. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/grouplevel/dscalar_utils.py +0 -0
  20. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/grouplevel/utils.py +0 -0
  21. {oceanproc-1.2.2 → oceanproc-1.3.1}/oceanproc/oceanparse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: oceanproc
3
- Version: 1.2.2
3
+ Version: 1.3.1
4
4
  Summary: MRI preprocessing for WUSTL Ocean labs
5
5
  Author-Email: Ramone Agard <rhagard@wustl.edu>, Joey Scanga <joeyscanga92@gmail.com>
6
6
  Requires-Python: <3.13,>=3.10
@@ -4,14 +4,12 @@ from argparse import ArgumentParser
4
4
  from pathlib import Path
5
5
  import json
6
6
  import os
7
- import re
8
7
  import shutil
9
- import subprocess
10
8
  from textwrap import dedent
11
9
  import xml.etree.ElementTree as et
12
- from .utils import exit_program_early, prompt_user_continue, prepare_subprocess_logging, debug_logging, log_linebreak, flags, run_subprocess
10
+ from .utils import exit_program_early, prompt_user_continue, prepare_subprocess_logging, debug_logging, log_linebreak, flags, run_subprocess, update_permissions
13
11
  import logging
14
- from bids import BIDSLayout, BIDSLayoutIndexer
12
+ from bids import BIDSLayout
15
13
  from bisect import bisect
16
14
  module_logger = logging.getLogger("fsspec")
17
15
  module_logger.setLevel(logging.CRITICAL)
@@ -136,16 +134,16 @@ def run_dcm2bids(subject:str,
136
134
  logger.debug(f"deleting file: {f}")
137
135
  os.remove(f)
138
136
 
139
- except RuntimeError or subprocess.CalledProcessError as e:
137
+ except (Exception, KeyboardInterrupt) as e:
140
138
  prepare_subprocess_logging(logger, stop=True)
141
139
  logger.exception(e, stack_info=True)
142
- exit_program_early(f"Problem running '{title}'.")
140
+ return False
143
141
 
142
+ return True
144
143
 
145
144
  @debug_logging
146
145
  def run_dcm2niix(source_dir:Path,
147
- tmp_nifti_dir:Path,
148
- clean_up_func=None):
146
+ tmp_nifti_dir:Path):
149
147
  """
150
148
  Run dcm2niix with the given input and output directories.
151
149
 
@@ -176,11 +174,12 @@ def run_dcm2niix(source_dir:Path,
176
174
  log_linebreak()
177
175
  logger.info("####### Converting DICOM files into NIFTI #######\n")
178
176
  run_subprocess(helper_command, title=title)
179
- except RuntimeError or subprocess.CalledProcessError as e:
177
+ except (Exception, KeyboardInterrupt) as e:
180
178
  prepare_subprocess_logging(logger, stop=True)
181
179
  logger.exception(e, stack_info=True)
182
- exit_program_early(f"Problem running '{title}'.",
183
- exit_func=clean_up_func if clean_up_func else None)
180
+ return False
181
+
182
+ return True
184
183
 
185
184
 
186
185
  def get_usability_from_file(usability_file:Path):
@@ -322,7 +321,8 @@ def dicom_to_bids(subject:str,
322
321
  if not p.exists():
323
322
  exit_program_early(f"Path {str(p)} does not exist.")
324
323
 
325
- if (path_that_exists := bids_dir / f"sub-{subject}/ses-{session}").exists() and (not skip_prompt):
324
+ sub_ses_bids_dir = bids_dir / f"sub-{subject}/ses-{session}"
325
+ if sub_ses_bids_dir.exists() and (not skip_prompt):
326
326
  ans = prompt_user_continue(dedent("""
327
327
  A raw data bids path for this subject and session already exists.
328
328
  Would you like to delete its contents and rerun dcm2bids? If not,
@@ -330,7 +330,7 @@ def dicom_to_bids(subject:str,
330
330
  """))
331
331
  if ans:
332
332
  logger.debug("removing the old BIDS raw data directory and its contents")
333
- shutil.rmtree(path_that_exists)
333
+ shutil.rmtree(sub_ses_bids_dir)
334
334
  else:
335
335
  return
336
336
 
@@ -345,16 +345,27 @@ def dicom_to_bids(subject:str,
345
345
  logger.warning(f"There was a problem deleting the temporary directory at {tmp_nifti_path}")
346
346
 
347
347
  clean_up(quiet=True)
348
+ nii_success = True
348
349
  if not nifti:
349
- run_dcm2niix(source_dir=source_dir,
350
- tmp_nifti_dir=tmp_nifti_path)
350
+ nii_success = run_dcm2niix(source_dir=source_dir,
351
+ tmp_nifti_dir=tmp_nifti_path)
351
352
  else:
352
353
  # copy current niftis to tmp_nifti_path
353
354
  logger.info(f"copying the source NIFTI files to tmp directory, {tmp_nifti_path}")
354
355
  shutil.copytree(source_dir.resolve(), tmp_nifti_path.resolve())
355
356
 
356
-
357
- # filter out know 'weird' files that can be created during conversion
357
+ # update the permissions on the nifti files
358
+ update_permissions(
359
+ permissions=flags.file_permissions,
360
+ path=tmp_nifti_path,
361
+ recursive=True,
362
+ group=flags.permissions_group
363
+ )
364
+
365
+ if not nii_success:
366
+ exit_program_early(f"Problem running 'dcm2niix'")
367
+
368
+ # filter out known 'weird' files that can be created during conversion
358
369
  if not keep_all_niftis:
359
370
  files_to_remove = []
360
371
  short_runs = list(tmp_nifti_path.glob("[!.]*[a-z].nii*")) + list(tmp_nifti_path.glob("[!.]*[a-z].json"))
@@ -396,13 +407,24 @@ def dicom_to_bids(subject:str,
396
407
  usability_file=usability_file,
397
408
  session_number=session_index)
398
409
 
399
- run_dcm2bids(subject=subject,
410
+ dcm2bids_success = run_dcm2bids(subject=subject,
400
411
  session=session,
401
412
  nifti_dir=tmp_nifti_path,
402
413
  bids_output_dir=bids_dir,
403
414
  config_file=bids_config,
404
415
  nordic_config=nordic_config,
405
416
  skip_validate=skip_validate)
417
+
418
+ update_permissions(
419
+ permissions=flags.file_permissions,
420
+ path=sub_ses_bids_dir,
421
+ recursive=True,
422
+ group=flags.permissions_group,
423
+ quiet=True
424
+ )
425
+
426
+ if not dcm2bids_success:
427
+ exit_program_early(f"Problem running 'dcm2bids'")
406
428
 
407
429
  if not flags.debug:
408
430
  clean_up()
@@ -2,7 +2,7 @@ import pandas as pd
2
2
  import numpy as np
3
3
  import nibabel as nib
4
4
  import json
5
- from .utils import exit_program_early, debug_logging, log_linebreak
5
+ from .utils import exit_program_early, debug_logging, log_linebreak, update_permissions, flags
6
6
  from pathlib import Path
7
7
  import logging
8
8
 
@@ -162,6 +162,11 @@ def create_events_and_confounds(bids_path:Path, derivs_path:Path, sub:str, ses:s
162
162
  output_file=event_file_out)
163
163
  append_to_confounds(confounds_file=confounds_file,
164
164
  fd_thresh=fd_thresh)
165
+ update_permissions(
166
+ permissions=flags.file_permissions,
167
+ path=event_file_out,
168
+ group=flags.permissions_group
169
+ )
165
170
 
166
171
 
167
172
 
@@ -3,53 +3,20 @@
3
3
  import json
4
4
  from datetime import datetime, timedelta
5
5
  import argparse
6
- import re
7
6
  from pathlib import Path
8
- import xml.etree.ElementTree as et
9
7
  from bids import BIDSLayout
10
8
  import logging
11
9
 
12
10
  from bids.layout import BIDSFile
13
- from .utils import exit_program_early, debug_logging, log_linebreak, flags
11
+ from .utils import exit_program_early, debug_logging, log_linebreak, flags, update_permissions
14
12
  import nibabel as nib
15
13
  import numpy as np
16
- import shutil
17
14
 
18
15
  module_logger = logging.getLogger("fsspec")
19
16
  module_logger.setLevel(logging.CRITICAL)
20
17
 
21
18
  logger = logging.getLogger(__name__)
22
19
 
23
-
24
- # @debug_logging
25
- # def get_locals_from_xml(xml_path: Path) -> tuple[set, str]:
26
- # """
27
- # Read in the xml file to find the localizers.
28
-
29
- # :param xml_path: Path to XML generated by XNAT
30
- # :type xml_path: pathlib.Path
31
- # :return: Set containing all localizer IDs
32
- # :rtype: set
33
- # """
34
- # tree = et.parse(xml_path)
35
- # prefix = "{" + str(tree.getroot()).split("{")[-1].split("}")[0] + "}"
36
- # scan_element_list = list(tree.iter(f"{prefix}scans"))
37
- # exp_element_list = list(tree.iter(f"{prefix}experiments"))
38
- # study_id = exp_element_list[0][0].get("study_id")
39
-
40
- # if len(scan_element_list) != 1:
41
- # exit_program_early(f"Error parsing the xml file provided. Found none or more than one scan groups")
42
-
43
- # scans = scan_element_list[0]
44
-
45
- # localizers = set()
46
- # for s in scans:
47
- # if re.match(r"Localizer.*", s.get("type")) and s.find(f'{prefix}quality').text == "usable":
48
- # acq_time = datetime.strptime(s.find(f"{prefix}startTime").text, "%H:%M:%S")
49
- # series_id = s.get("ID")
50
- # localizers.add((acq_time, series_id))
51
- # return (sorted(localizers), study_id)
52
-
53
20
  DEFAULT_LOCALIZER_LABEL = "00"
54
21
  EXTRACTED_ACQ_VALUE = "bestVol"
55
22
 
@@ -331,6 +298,13 @@ def extract_best_acqusition(fmap_file: str|BIDSFile, bids_layout: BIDSLayout) ->
331
298
  with open(best_acq_json, "w") as out_js:
332
299
  json.dump(jd, out_js, indent=4)
333
300
 
301
+ for fmap_out_path in [best_acq_fmap, best_acq_json]:
302
+ update_permissions(
303
+ permissions=flags.file_permissions,
304
+ path=fmap_out_path,
305
+ group=flags.permissions_group,
306
+ )
307
+
334
308
  return (BIDSFile(best_acq_fmap), BIDSFile(best_acq_json))
335
309
 
336
310
 
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
  import argparse
4
+ from ast import arg
4
5
  import sys
5
6
  from pathlib import Path
6
7
  import logging
@@ -10,10 +11,12 @@ from oceanproc.group_series import map_fmap_to_func, map_fmap_to_func_with_pairi
10
11
  from oceanproc.preprocessing_wrapper import process_data, adult_defaults, infant_defaults, mount_opts, insert_dummy_frames
11
12
  from oceanproc.segmentation_wrapper import segmentation_args, segment_anatomical
12
13
  from oceanproc.events_long import create_events_and_confounds
13
- from oceanproc.utils import exit_program_early, prompt_user_continue, default_log_format, add_file_handler, export_args_to_file, flags, debug_logging, log_linebreak, extract_options, make_option
14
+ from oceanproc.utils import *
14
15
  from oceanproc.oceanparse import OceanParser
15
16
  from bids import BIDSLayout, BIDSLayoutIndexer
16
17
  import shutil
18
+ import grp
19
+ import os
17
20
  from textwrap import dedent
18
21
 
19
22
  logging.basicConfig(level=logging.INFO,
@@ -65,6 +68,32 @@ def main():
65
68
  f"path string <{path}> does not represent an existing file"
66
69
  )
67
70
  return p
71
+
72
+ def ValidPermissions(permissions):
73
+ try:
74
+ validate_permissions_string(permissions)
75
+ except ValueError:
76
+ raise argparse.ArgumentTypeError(
77
+ f"permissions string <{permissions}> does not represent a valid permissions string"
78
+ )
79
+ return permissions
80
+
81
+ def ValidGroup(group):
82
+ try:
83
+ group_id = grp.getgrnam(group).gr_gid
84
+ valid_groups = os.getgroups()
85
+ if group_id not in valid_groups:
86
+ raise argparse.ArgumentTypeError(
87
+ f"current user does not belong to group <{group}>"
88
+ )
89
+ except KeyError:
90
+ raise argparse.ArgumentTypeError(
91
+ f"group name <{group}> is not a valid system user group"
92
+ )
93
+ return group
94
+
95
+ default_user_group = grp.getgrgid(os.getgid()).gr_name
96
+ default_file_permissions = "771"
68
97
 
69
98
  parser = OceanParser(
70
99
  prog="oceanproc",
@@ -74,10 +103,9 @@ def main():
74
103
  epilog="An arguments file can be accepted with @FILEPATH.\nAny additional arguments not listed in the help message will be passed to the preprocessing subprocess.",
75
104
  allow_abbrev=False
76
105
  )
77
- parser.add_argument("--version", action="version", version='%(prog)s v1.1.8')
106
+ parser.add_argument("--version", action="version", version='%(prog)s v1.3.1')
78
107
  session_args = parser.add_argument_group("Session Specific")
79
108
  config_args = parser.add_argument_group("Configuration Arguments", "These arguments are saved to a file if the '--export_args' option is used")
80
- parser.register("type", "Full-Path", lambda p: Path(p).resolve())
81
109
 
82
110
  session_args.add_argument("--subject", "-su", required=True,
83
111
  help="The identifier of the subject to preprocess")
@@ -138,7 +166,7 @@ def main():
138
166
  config_args.add_argument("--dscans_path", type=ExistingDir,
139
167
  help="""The path to the directory containing 'dscans' files for inserting dummy scans into bold runs. Must be named exactly as corresponding bold image except with 'dscans' as
140
168
  the suffix and '.tsv' as the extension. Contents of the file must be a column, titled 'dummy_scan', with ones and zeros. Ones denote which frames should be dummy scans.""")
141
- config_args.add_argument("--precomputed_derivatives", "-pd", type="Full-Path", dest="derivatives", nargs="*",
169
+ config_args.add_argument("--precomputed_derivatives", "-pd", type=ExistingDir, dest="derivatives", nargs="+",
142
170
  help="A list of paths to any BIDS-style precomputed derivatives that should be used in preprocessing. (Ex. /path/to/bibsnet)")
143
171
  config_args.add_argument("--work_dir", "-w", type=ExistingDir, required=True,
144
172
  help="The path to the working directory used to store intermediate files")
@@ -155,19 +183,16 @@ def main():
155
183
  help="Path to the BIBSnet apptainer image to use for segmentation. If provided, BIBSnet segmentation will be run and the outputs will be used for preprocessing. (Must be used with the '--infant' flag)")
156
184
  config_args.add_argument("--bibsnet_work", "-bw", type=ExistingDir,
157
185
  help="The path to the working directory used to store intermediate files for BIBSnet")
186
+ config_args.add_argument("--permissions_group", "-g", type=ValidGroup, default=default_user_group,
187
+ help=f"The user group that should own all of the created files. (Default for the current user is '{default_user_group}')")
188
+ config_args.add_argument("--file_permissions", "-fp", type=ValidPermissions, default=default_file_permissions,
189
+ help=f"The file permissions that all output files should have, represented in numeric format. (Default permissions is '{default_file_permissions}')")
158
190
  args, unknown_args = parser.parse_known_args()
159
191
 
160
- # try:
161
- # assert args.derivs_path.is_dir(), "Derivatives directory must exist but it cannot be found"
162
- # assert args.bids_path.is_dir(), "Raw Bids directory must exist but it cannot be found"
163
- # if not args.skip_dcm2bids:
164
- # assert args.source_data.is_dir(), "Source data directory must exist but it cannot be found"
165
- # assert args.work_dir.is_dir(), "Work directroy must exist but it cannot be found"
166
- # except AssertionError as e:
167
- # logger.exception(e)
168
- # parser.error(e)
169
-
170
- defaults = infant_defaults if args.infant else adult_defaults
192
+ if args.infant:
193
+ flags.infant = True
194
+
195
+ defaults = infant_defaults if flags.infant else adult_defaults
171
196
  for k,v in defaults.__dict__.items():
172
197
  if k in args.__dict__ and args.__dict__[k] is None:
173
198
  args.__dict__[k] = v
@@ -175,15 +200,12 @@ def main():
175
200
  unknown_args = extract_options(unknown_args)
176
201
 
177
202
  bibsnet_path = (args.derivs_path / "bibsnet").resolve()
178
- if args.infant and args.bibsnet_image_path:
203
+ if flags.infant and args.bibsnet_image_path:
179
204
  if args.derivatives is None:
180
205
  args.derivatives = [bibsnet_path]
181
206
  elif bibsnet_path not in args.derivatives:
182
207
  args.derivatives.append(bibsnet_path)
183
208
 
184
- # if args.bibsnet_work and (not args.bibsnet_work.exists()):
185
- # parser.error("BIBSnet working directory must exist but it cannot be found")
186
-
187
209
  if args.longitudinal:
188
210
  for lg_group in args.longitudinal:
189
211
  if not lg_group[0].is_dir():
@@ -195,11 +217,10 @@ def main():
195
217
  parser.error(f"usability file must be a .xml or .json file, and it must exist: {lg_group[1]}")
196
218
  else:
197
219
  parser.error(f"longitudinal argument cannot have more than 2 elements: {lg_group}")
220
+ flags.longitudinal = True
198
221
 
199
222
  preproc_image = f"{defaults.image_name}:{args.image_version}"
200
223
  if args.preproc_image_path:
201
- # if not args.preproc_image_path.exists():
202
- # parser.error(f"Cannot find the preprocessing apptainer image at the path: {args.preproc_image_path}")
203
224
  flags.apptainer = True
204
225
  preproc_image = args.preproc_image_path
205
226
 
@@ -212,6 +233,11 @@ def main():
212
233
  if args.debug_mode:
213
234
  flags.debug = True
214
235
  logger.setLevel(logging.DEBUG)
236
+
237
+ flags.file_permissions = args.file_permissions
238
+ flags.permissions_group = args.permissions_group
239
+ flags.gid = grp.getgrnam(flags.permissions_group).gr_gid
240
+ flags.uid = os.getuid()
215
241
 
216
242
  logger.info("Starting oceanproc...")
217
243
  logger.info(f"Log will be stored at {log_path}")
@@ -222,14 +248,11 @@ def main():
222
248
  assert args.export_args.parent.exists() and args.export_args.suffix, "Argument export path must be a file path in a directory that exists"
223
249
  logger.info(f"####### Exporting Configuration Arguments to: '{args.export_args}' #######")
224
250
  export_args_to_file(args, config_args, args.export_args, extra_args=unknown_args)
251
+ update_permissions(permissions=flags.file_permissions, path=args.export_args, group=flags.permissions_group)
225
252
  except Exception as e:
226
253
  logger.exception(e)
227
254
  exit_program_early(e)
228
255
 
229
- if args.longitudinal:
230
- flags.longitudinal = True
231
- if args.infant:
232
- flags.infant = True
233
256
 
234
257
  # log the input arguments
235
258
  for k,v in (dict(args._get_kwargs())).items():
@@ -275,6 +298,11 @@ def main():
275
298
  with open(bids_path / ".bidsignore", "r") as f:
276
299
  ignore_list.extend(f.readlines())
277
300
  ignore_list = list(set(ignore_list))
301
+ other_sub_paths = [
302
+ p.name for p in bids_path.glob("sub-*")
303
+ if not f"sub-{args.subject}".startswith(p.name) and p.is_dir() # 'startswith' to account for pybids regexifying after the end of subject name
304
+ ]
305
+ ignore_list.extend(other_sub_paths)
278
306
  bids_layout = BIDSLayout(args.bids_path, validate=False, indexer=BIDSLayoutIndexer(ignore=ignore_list, validate=False))
279
307
 
280
308
  ##### Remove the scans marked as 'unusable' #####
@@ -299,7 +327,6 @@ def main():
299
327
  subject=args.subject,
300
328
  session=args.session,
301
329
  layout=bids_layout,
302
- # xml_path=xml_data_path,
303
330
  allow_uneven_fmap_groups=args.allow_uneven_fmap_groups,
304
331
  extract_best_fmap=args.extract_best_fmap
305
332
  )
@@ -359,7 +386,7 @@ def main():
359
386
  )
360
387
 
361
388
  ##### Create long formatted event files #####
362
- if not args.skip_event_files:
389
+ if not args.skip_event_files and not args.anat_only:
363
390
  create_events_and_confounds(
364
391
  bids_path=args.bids_path,
365
392
  derivs_path=preproc_derivs_path,
@@ -3,9 +3,8 @@
3
3
  from pathlib import Path
4
4
  import logging
5
5
  from bids import BIDSLayout
6
- from .utils import exit_program_early, make_option, prepare_subprocess_logging, flags, debug_logging, log_linebreak, run_subprocess
6
+ from .utils import exit_program_early, make_option, prepare_subprocess_logging, flags, debug_logging, log_linebreak, run_subprocess, update_permissions
7
7
  import shutil
8
- from subprocess import Popen, PIPE
9
8
  from matplotlib import pyplot as plt
10
9
  from bs4 import BeautifulSoup as bsoup
11
10
  import pandas as pd
@@ -85,10 +84,8 @@ def run_preprocessing(subject:str,
85
84
  if flags.apptainer:
86
85
  cmd_prelude = f"apptainer run --nv --cleanenv --no-mount cwd --pwd {work_mount}"
87
86
  mount_flag = "-B"
88
- else:
89
- uid = Popen(["id", "-u"], stdout=PIPE).stdout.read().decode("utf-8").strip()
90
- gid = Popen(["id", "-g"], stdout=PIPE).stdout.read().decode("utf-8").strip()
91
- cmd_prelude = f"docker run --rm -i -u {uid}:{gid}"
87
+ else:
88
+ cmd_prelude = f"docker run --rm -i -u {flags.uid}:{flags.gid}"
92
89
  mount_flag = "-v"
93
90
 
94
91
  additional_mount_paths = []
@@ -122,16 +119,18 @@ def run_preprocessing(subject:str,
122
119
  {additional_mount_args}
123
120
  {option_chain}"""
124
121
 
122
+ success = True
125
123
  try:
126
124
  run_subprocess(preproc_command, title=title)
127
- except RuntimeError as e:
125
+ except (Exception, KeyboardInterrupt) as e:
128
126
  prepare_subprocess_logging(logger, stop=True)
129
127
  logger.exception(e, stack_info=True)
130
- exit_program_early(f"Program '{title}' has run into an error.",
131
- None if flags.debug else clean_up)
128
+ success = False
129
+
132
130
  if not flags.debug:
133
131
  clean_up()
134
132
 
133
+ return success
135
134
 
136
135
  @debug_logging
137
136
  def add_fd_plot_to_report(subject:str,
@@ -356,7 +355,7 @@ def process_data(subject:str,
356
355
  option_chain = " ".join([make_option(v, key=k, delimeter=" ", convert_underscore=True) for k,v in kwargs.items()])
357
356
  preproc_title = "NiBabies" if flags.infant else "fMRIPrep"
358
357
 
359
- run_preprocessing(subject=subject,
358
+ preproc_success = run_preprocessing(subject=subject,
360
359
  session=session if flags.infant else None,
361
360
  bids_path=bids_path,
362
361
  derivs_path=derivs_path,
@@ -367,8 +366,45 @@ def process_data(subject:str,
367
366
  option_chain=option_chain,
368
367
  additional_mounts=additional_mounts,
369
368
  remove_work_folder=remove_work_folder)
370
-
371
- add_fd_plot_to_report(subject=subject,
372
- session=session,
373
- derivs_path=derivs_path,
374
- title=preproc_title)
369
+
370
+ if preproc_success:
371
+ add_fd_plot_to_report(subject=subject,
372
+ session=session,
373
+ derivs_path=derivs_path,
374
+ title=preproc_title)
375
+
376
+ paths_to_update = []
377
+ paths_to_update.extend(sorted(derivs_path.glob(f"sub-{subject}*")))
378
+ paths_to_update.extend(sorted(derivs_path.glob(f"sourcedata/*/sub-{subject}*")))
379
+ for out_path in paths_to_update:
380
+ if out_path.exists():
381
+ # update file permissions for preprocessing outputs
382
+ update_permissions(
383
+ permissions=flags.file_permissions,
384
+ path=out_path,
385
+ recursive=True,
386
+ group=flags.permissions_group
387
+ )
388
+
389
+ # check other top-level files
390
+ other_paths = [derivs_path/"dataset_description.json", derivs_path/"logs"]
391
+ for op in other_paths:
392
+ if op.exists() and (op.stat().st_uid == flags.uid):
393
+ update_permissions(
394
+ permissions=flags.file_permissions,
395
+ path=op,
396
+ recursive=True,
397
+ group=flags.permissions_group
398
+ )
399
+
400
+ # update file permisions for working directory files
401
+ update_permissions(
402
+ permissions=flags.file_permissions,
403
+ path=work_path,
404
+ recursive=True,
405
+ group=flags.permissions_group,
406
+ quiet=True
407
+ )
408
+
409
+ if not preproc_success:
410
+ exit_program_early(f"Program '{preproc_title}' has run into an error.")
@@ -2,7 +2,8 @@
2
2
 
3
3
  from pathlib import Path
4
4
  import logging
5
- from .utils import exit_program_early, make_option, prepare_subprocess_logging, flags, debug_logging, log_linebreak, run_subprocess
5
+ from sre_constants import SUCCESS
6
+ from .utils import exit_program_early, make_option, prepare_subprocess_logging, flags, debug_logging, log_linebreak, run_subprocess, update_permissions
6
7
  import shutil
7
8
  from pathlib import Path
8
9
 
@@ -59,9 +60,6 @@ def run_bibsnet(subject:str,
59
60
  elif not bibsnet_image.exists():
60
61
  exit_program_early(f"BIBSnet image path {derivs_path} does not exist.")
61
62
 
62
- # uid = Popen(["id", "-u"], stdout=PIPE).stdout.read().decode("utf-8").strip()
63
- # gid = Popen(["id", "-g"], stdout=PIPE).stdout.read().decode("utf-8").strip()
64
-
65
63
  bids_mount = "/data"
66
64
  derivs_mount = "/out"
67
65
  work_mount = "/work"
@@ -75,16 +73,18 @@ def run_bibsnet(subject:str,
75
73
  -w {work_mount} --verbose
76
74
  {option_chain}"""
77
75
 
76
+ success = True
78
77
  try:
79
78
  run_subprocess(bibsnet_command, title="BIBSnet")
80
- except RuntimeError as e:
79
+ except (Exception, KeyboardInterrupt) as e:
81
80
  prepare_subprocess_logging(logger, stop=True)
82
81
  logger.exception(e, stack_info=True)
83
- exit_program_early(f"Program 'BIBSnet' has run into an error.",
84
- None if flags.debug else clean_up)
82
+ success = False
83
+
85
84
  if not flags.debug:
86
85
  clean_up()
87
86
 
87
+ return success
88
88
 
89
89
  @debug_logging
90
90
  def segment_anatomical(subject:str,
@@ -116,7 +116,7 @@ def segment_anatomical(subject:str,
116
116
  """
117
117
  option_chain = " ".join([make_option(v, key=k, delimeter=" ", convert_underscore=True) for k,v in kwargs.items()])
118
118
 
119
- run_bibsnet(subject=subject,
119
+ segment_success = run_bibsnet(subject=subject,
120
120
  session=session,
121
121
  bids_path=bids_path,
122
122
  derivs_path=derivs_path,
@@ -124,5 +124,30 @@ def segment_anatomical(subject:str,
124
124
  bibsnet_image=bibsnet_image,
125
125
  option_chain=option_chain,
126
126
  remove_work_folder=remove_work_folder)
127
+
128
+ paths_to_update = sorted(derivs_path.glob(f"bibsnet/sub-{subject}*"))
129
+ paths_to_update.extend(sorted(work_path.glob(f"*/sub-{subject}*")))
130
+ for out_path in paths_to_update:
131
+ if out_path.exists():
132
+ # update file permissions for segmentation outputs and working files
133
+ update_permissions(
134
+ permissions=flags.file_permissions,
135
+ path=out_path,
136
+ recursive=True,
137
+ group=flags.permissions_group)
138
+
139
+ # check other top-level files
140
+ other_paths = [derivs_path/"dataset_description.json"]
141
+ for op in other_paths:
142
+ if op.exists() and (op.stat().st_uid == flags.uid):
143
+ update_permissions(
144
+ permissions=flags.file_permissions,
145
+ path=op,
146
+ recursive=True,
147
+ group=flags.permissions_group
148
+ )
149
+
150
+ if not segment_success:
151
+ exit_program_early(f"Program 'BIBSnet' has run into an error.")
127
152
 
128
153
 
@@ -239,20 +239,24 @@ def prepare_subprocess_logging(this_logger,
239
239
 
240
240
 
241
241
  @debug_logging
242
- def run_subprocess(cmd: str, title: str):
242
+ def run_subprocess(cmd: str, title: str, log_output=True, quiet=False):
243
243
  split_cmd = shlex.split(cmd)
244
244
  logger.info(f"running '{title}' with the following command: \n{' '.join(split_cmd)}\n")
245
245
  prepare_subprocess_logging(logger)
246
+ ecode = 0
246
247
  with Popen(split_cmd, stdout=PIPE, stderr=PIPE) as p:
247
248
  while p.poll() == None:
248
- for line in p.stdout:
249
- logger.info(line.decode("utf-8", "ignore"))
250
- for line in p.stderr:
251
- logger.info(line.decode("utf-8", "ignore"))
249
+ if log_output:
250
+ for line in p.stdout:
251
+ logger.info(line.decode("utf-8", "ignore"))
252
+ for line in p.stderr:
253
+ logger.info(line.decode("utf-8", "ignore"))
252
254
  prepare_subprocess_logging(logger, stop=True)
255
+ ecode = p.poll()
253
256
  p.kill()
254
- if p.poll() != 0:
255
- raise RuntimeError(f"process -'{title}'- has ended with a non-zero exit code.")
257
+ if (ecode != 0) and (not quiet):
258
+ raise RuntimeError(f"process -'{title}'- has ended with a non-zero exit code <{ecode}>.")
259
+ return ecode
256
260
 
257
261
 
258
262
  def log_linebreak():
@@ -389,3 +393,30 @@ def parcellate_dtseries(dtseries_path: Path,
389
393
  exit_program_early(f"Program '{title}' has run into an error.")
390
394
 
391
395
  return ptseries_path
396
+
397
+ def validate_permissions_string(permissions:str):
398
+ valid = True
399
+ if (len(permissions) != 3) or not permissions.isnumeric():
400
+ valid = False
401
+ if valid:
402
+ for character in permissions:
403
+ if int(character) > 7 or int(character) < 0:
404
+ valid = False
405
+
406
+ if not valid:
407
+ raise ValueError(f"Did not receive a valid permissions string: <{permissions}>")
408
+
409
+
410
+ def update_permissions(permissions:str, path:Path, recursive=False, group=None, quiet=False):
411
+ validate_permissions_string(permissions)
412
+ if not path.exists():
413
+ if quiet:
414
+ return
415
+ raise ValueError(f"Did not receive a valid path: <{path}>")
416
+
417
+ chmod_cmd = f"chmod {'-R' if recursive else ''} {permissions} {path.resolve()}"
418
+ run_subprocess(cmd=chmod_cmd, title="chmod", log_output=False, quiet=True)
419
+
420
+ if group is not None:
421
+ chgrp_cmd = f"chgrp {'-R' if recursive else ''} {group} {path.resolve()}"
422
+ run_subprocess(cmd=chgrp_cmd, title="chgrp", log_output=False, quiet=True)
@@ -34,7 +34,7 @@ dependencies = [
34
34
  "pybids>=0.16.3",
35
35
  ]
36
36
  name = "oceanproc"
37
- version = "1.2.2"
37
+ version = "1.3.1"
38
38
  description = "MRI preprocessing for WUSTL Ocean labs"
39
39
  readme = "README.md"
40
40
 
File without changes