Skip to content

Commit 6bbc670

Browse files
authored
Singularity (#41)
* update petdeface to handle environment supplied freesurfer license * updated singuarity run to work * moved brainlife app to it's own repo * fixed version string fetching * bump version * update docs
1 parent 029b954 commit 6bbc670

File tree

5 files changed

+166
-22
lines changed

5 files changed

+166
-22
lines changed

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,10 @@ freesurfer_binaries/*
3939

4040
# ignore html files that are generated by sphinx "make html"
4141
docs/_build/
42-
*.doctree
42+
*.doctree
43+
44+
# ignore freesurfer license
45+
license.txt
46+
47+
# ignore derivatives folder
48+
data/derivatives/

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ This software can be installed via source or via pip from PyPi with `pip install
3737
```bash
3838
usage: petdeface.py [-h] [--output_dir OUTPUT_DIR] [--anat_only]
3939
[--subject SUBJECT] [--session SESSION] [--docker]
40-
[--n_procs N_PROCS] [--skip_bids_validator] [--version]
41-
[--placement PLACEMENT] [--remove_existing] [--excludesubject]
42-
input_dir
40+
[--singularity] [--n_procs N_PROCS] [--skip_bids_validator]
41+
[--version] [--placement PLACEMENT] [--remove_existing]
42+
[--excludesubject] input_dir
4343

4444
PetDeface
4545

@@ -56,6 +56,7 @@ options:
5656
--session SESSION, -ses SESSION
5757
The label of the session to be processed.
5858
--docker, -d Run in docker container
59+
--singularity, -si Run in singularity container
5960
--n_procs N_PROCS Number of processors to use when running the workflow
6061
--skip_bids_validator
6162
--version, -v show programs version number and exit
@@ -109,6 +110,29 @@ docker run --user=$UID:$GID -a stderr -a stdout --rm \
109110
petdeface:latest /input --output_dir /output --n_procs 16 --skip_bids_validator --placement adjacent --user=$UID:$GID system_platform=Linux
110111
```
111112

113+
### Singularity Usage
114+
115+
Requirements:
116+
- Singularity must be installed
117+
- `openneuropet/petdeface` must be present or reachable at dockerhub
118+
119+
One can execute petdeface in singularity either directly via:
120+
121+
```bash
122+
singularity exec -e --bind license.txt:/opt/freesurfer/license.txt docker://openneuropet/petdeface:0.1.1 petdeface
123+
```
124+
125+
Input and Output directories don't need to be bound, but one does need to bind a freesurfer license to the image before they can proceed with defacing.
126+
Otherwise, one can run and execute petdeface with the same syntax as calling it from the command line, the only difference being that petdeface is prepended
127+
with `singularity exec -e`
128+
129+
```bash
130+
singularity exec -e --bind license.txt:/opt/freesurfer/license.txt docker://openneuropet/petdeface:0.1.1 petdeface /input --output-dir /output --n_procs 10
131+
```
132+
133+
**_NOTE_**: Testing with singularity has been limited to version singularity-ce 4.2.0, please let us know in the issues section of this repo if you have
134+
trouble running this container in singularity/apptainer.
135+
112136
## Development
113137

114138
This project uses poetry to package and build, to create a pip installable version of the package run:

docs/usage.rst

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,23 @@ One needs to create 3 bind mounts to the docker container when running PETdeface
147147
If one is running PETdeface on a linux machine and desires non-root execution of the container,
148148
the ``--user`` flag needs to be set to the UID and GID of the user running the container.
149149

150-
Of course all of the above is done automatically when running PETdeface using the ``--docker`` flag.
150+
Of course all of the above is done automatically when running PETdeface using the ``--docker`` flag.
151+
152+
Singularity Based
153+
-----------------
154+
155+
PETdeface can also be run using singularity, however one will need access to the internet/dockerhub as
156+
it relies on being able to retrieve the docker image from dockerhub. The syntax is as follows::
157+
158+
petdeface /inputfolder --output_dir /outputfolder --singularity
159+
160+
Running petdeface in singularity will generate then execute a singularity command that will pull the
161+
docker image from dockerhub and run the pipeline.
162+
163+
singularity exec -e --bind license.txt:/opt/freesurfer/license.txt docker://openneuropet/petdeface:latest petdeface /inputfolder --output_dir /outputfolder --n_procs 2 --placement adjacent
164+
165+
PETdeface will do it's best to locate a valid FreeSurfer license file on the host machine and bind it
166+
to the container by checking `FREESURFER_HOME` and `FREESURFER_LICENSE` environment variables. If you
167+
receive an error message relating to the FreeSurfer license file, try setting and exporting the
168+
`FREESURFER_LICENSE` environment variable to the location of the FreeSurfer license file on the host
169+
machine.

petdeface/petdeface.py

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
import shutil
88
from bids import BIDSLayout
9+
import importlib
910
import glob
1011
from platform import system
1112

@@ -51,7 +52,7 @@
5152
for place in places_to_look:
5253
for root, folders, files in os.walk(place):
5354
for file in files:
54-
if file.endswith("pyproject.toml"):
55+
if file.endswith("pyproject.toml") and "petdeface" in os.path.join(root, file):
5556
toml_file = os.path.join(root, file)
5657

5758
with open(toml_file, "r") as f:
@@ -66,7 +67,16 @@
6667
__bids_version__ = (
6768
line.split("=")[1].strip().replace('"', "")
6869
)
69-
break
70+
# if the version number is found and is formatted with major.minor.patch formating we can break
71+
# we check the version with a regex expression to see if all of the parts are there
72+
if re.match(r"\d+\.\d+\.\d+", __version__):
73+
break
74+
75+
if __version__ != "unable to locate version number in pyproject.toml":
76+
# we try to load the version using import lib
77+
__version__ = importlib.metadata.version("petdeface")
78+
if re.match(r"\d+\.\d+\.\d+", __version__):
79+
break
7080

7181

7282
def locate_freesurfer_license():
@@ -79,20 +89,32 @@ def locate_freesurfer_license():
7989
:return: full path to Freesurfer license file
8090
:rtype: pathlib.Path
8191
"""
82-
# collect freesurfer home environment variable
83-
fs_home = pathlib.Path(os.environ.get("FREESURFER_HOME", ""))
84-
if not fs_home:
85-
raise ValueError(
86-
"FREESURFER_HOME environment variable is not set, unable to determine location of license file"
87-
)
92+
93+
# check to see if FREESURFER_LICENSE variable is set, if so we can skip the rest of this function
94+
if os.environ.get("FREESURFER_LICENSE", ""):
95+
fs_license_env_var = pathlib.Path(os.environ.get("FREESURFER_LICENSE", ""))
96+
if not fs_license_env_var.exists():
97+
raise ValueError(
98+
f"Freesurfer license file does not exist at {fs_license_env_var}, but is set under $FREESURFER_LICENSE variable."
99+
f"Update or unset this varible to use the license.txt at $FREESURFER_HOME"
100+
)
101+
else:
102+
return fs_license_env_var
88103
else:
89-
fs_license = fs_home / pathlib.Path("license.txt")
90-
if not fs_license.exists():
104+
# collect freesurfer home environment variable and look there instead
105+
fs_home = pathlib.Path(os.environ.get("FREESURFER_HOME", ""))
106+
if not fs_home:
91107
raise ValueError(
92-
"Freesurfer license file does not exist at {}".format(fs_license)
108+
"FREESURFER_HOME environment variable is not set, unable to determine location of license file"
93109
)
94110
else:
95-
return fs_license
111+
fs_license = fs_home / pathlib.Path("license.txt")
112+
if not fs_license.exists():
113+
raise ValueError(
114+
"Freesurfer license file does not exist at {}".format(fs_license)
115+
)
116+
else:
117+
return fs_license
96118

97119

98120
def check_docker_installed():
@@ -198,7 +220,7 @@ def deface(args: Union[dict, argparse.Namespace]) -> None:
198220
else:
199221
args = args
200222

201-
if not check_valid_fs_license():
223+
if not check_valid_fs_license() and not locate_freesurfer_license().exists():
202224
raise Exception("You need a valid FreeSurfer license to proceed!")
203225

204226
if args.subject:
@@ -721,7 +743,11 @@ def __init__(
721743
# check if freesurfer license is valid
722744
self.fs_license = check_valid_fs_license()
723745
if not self.fs_license:
724-
raise ValueError("Freesurfer license is not valid")
746+
self.fs_license = locate_freesurfer_license()
747+
if not self.fs_license.exists():
748+
raise ValueError("Freesurfer license is not valid")
749+
else:
750+
print(f"Using freesurfer license at {self.fs_license} found in system env at $FREESURFER_LICENSE")
725751

726752
def run(self):
727753
"""
@@ -800,6 +826,13 @@ def cli():
800826
default=False,
801827
help="Run in docker container",
802828
),
829+
parser.add_argument(
830+
"--singularity",
831+
"-si",
832+
action="store_true",
833+
default=False,
834+
help="Run in singularity container",
835+
),
803836
parser.add_argument(
804837
"--n_procs",
805838
help="Number of processors to use when running the workflow",
@@ -943,8 +976,16 @@ def main(): # noqa: max-complexity: 12
943976
docker_command += f"-v {code_dir}:/petdeface "
944977

945978
# collect location of freesurfer license if it's installed and working
946-
if check_valid_fs_license():
947-
license_location = locate_freesurfer_license()
979+
try:
980+
check_valid_fs_license()
981+
except:
982+
if locate_freesurfer_license().exists():
983+
license_location = locate_freesurfer_license()
984+
else:
985+
raise FileNotFoundError(
986+
"Freesurfer license not found, please set FREESURFER_LICENSE environment variable or place license.txt in FREESURFER_HOME"
987+
)
988+
948989
if license_location:
949990
docker_command += f"-v {license_location}:/opt/freesurfer/license.txt "
950991

@@ -960,6 +1001,60 @@ def main(): # noqa: max-complexity: 12
9601001

9611002
subprocess.run(docker_command, shell=True)
9621003

1004+
elif args.singularity:
1005+
singularity_command = f"singularity exec -e"
1006+
1007+
if args.output_dir == "None" or args.output_dir is None or args.output_dir == "":
1008+
args.output_dir = args.input_dir / "derivatives" / "petdeface"
1009+
1010+
# create output directory if it doesn't exist
1011+
if not args.output_dir.exists():
1012+
args.output_dir.mkdir(parents=True, exist_ok=True)
1013+
1014+
# convert args to dictionary
1015+
args_dict = vars(args)
1016+
for key, value in args_dict.items():
1017+
if isinstance(value, pathlib.PosixPath):
1018+
args_dict[key] = str(value)
1019+
1020+
args_dict.pop("singularity")
1021+
1022+
# remove False boolean keys and values, and set true boolean keys to empty string
1023+
args_dict = {key: value for key, value in args_dict.items() if value}
1024+
set_to_empty_str = [key for key, value in args_dict.items() if value == True]
1025+
for key in set_to_empty_str:
1026+
args_dict[key] = "empty_str"
1027+
1028+
args_string = " ".join(
1029+
["--{} {}".format(key, value) for key, value in args_dict.items() if value]
1030+
)
1031+
args_string = args_string.replace("empty_str", "")
1032+
1033+
# remove --input_dir from args_string as input dir is positional, we
1034+
# we're simply removing an artifact of argparse
1035+
args_string = args_string.replace("--input_dir", "")
1036+
1037+
# collect location of freesurfer license if it's installed and working
1038+
try:
1039+
check_valid_fs_license()
1040+
except:
1041+
if locate_freesurfer_license().exists():
1042+
license_location = locate_freesurfer_license()
1043+
else:
1044+
raise FileNotFoundError(
1045+
"Freesurfer license not found, please set FREESURFER_LICENSE environment variable or place license.txt in FREESURFER_HOME"
1046+
)
1047+
1048+
singularity_command += f" --bind {str(license_location)}:/opt/freesurfer/license.txt"
1049+
singularity_command += f" docker://openneuropet/petdeface:{__version__}"
1050+
singularity_command += f" petdeface"
1051+
singularity_command += args_string
1052+
1053+
print("Running singularity command: \n{}".format(singularity_command))
1054+
1055+
subprocess.run(singularity_command, shell=True)
1056+
1057+
9631058
else:
9641059
petdeface = PetDeface(
9651060
bids_dir=args.input_dir,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "petdeface"
3-
version = "0.1.2"
3+
version = "0.2.1"
44
description = "A nipype PET and MR defacing pipeline for BIDS datasets utilizing FreeSurfer's MiDeFace."
55
authors = ["Martin Nørgaard <[email protected]>", "Anthony Galassi <[email protected]>", "Murat Bilgel <[email protected]>"]
66
license = "MIT"

0 commit comments

Comments
 (0)