diff --git a/nibabies/config.py b/nibabies/config.py index 43b885ee..bd4933d4 100644 --- a/nibabies/config.py +++ b/nibabies/config.py @@ -202,7 +202,10 @@ def load(cls, settings, init=True, ignore=None): if k in ignore or v is None: continue if k in cls._paths: - setattr(cls, k, Path(v).absolute()) + if isinstance(v, (list, tuple)): # Multiple paths + setattr(cls, k, [Path(p).absolute() for p in v]) + else: + setattr(cls, k, Path(v).absolute()) elif hasattr(cls, k): setattr(cls, k, v) @@ -221,7 +224,10 @@ def get(cls): if callable(getattr(cls, k)): continue if k in cls._paths: - v = str(v) + if isinstance(v, (list, tuple)): # Multiple paths + v = [str(p) for p in v] + else: + v = str(v) if isinstance(v, SpatialReferences): v = " ".join([str(s) for s in v.references]) or None if isinstance(v, Reference): @@ -412,6 +418,7 @@ class execution(_Config): "anat_derivatives", "bids_dir", "bids_database_dir", + "derivatives", "fs_license_file", "fs_subjects_dir", "layout", @@ -609,6 +616,8 @@ class seeds(_Config): """Master random seed to initialize the Pseudorandom Number Generator (PRNG)""" ants = None """Seed used for antsRegistration, antsAI, antsMotionCorr""" + numpy = None + """Seed used by NumPy""" @classmethod def init(cls): @@ -619,6 +628,7 @@ def init(cls): random.seed(cls.master) # initialize the PRNG # functions to set program specific seeds cls.ants = _set_ants_seed() + cls.numpy = _set_numpy_seed() def _set_ants_seed(): @@ -628,6 +638,15 @@ def _set_ants_seed(): return val +def _set_numpy_seed(): + """NumPy's random seed is independant from Python's `random` module""" + import numpy as np + + val = random.randint(1, 65536) + np.random.seed(val) + return val + + def from_dict(settings): """Read settings from a flat dictionary.""" nipype.load(settings) @@ -707,7 +726,7 @@ def init_spaces(checkpoint=True): from .utils.misc import cohort_by_months cohort = cohort_by_months("MNIInfant", workflow.age_months) - spaces.add(Reference, {"cohort": cohort}) + spaces.add(Reference("MNIInfant", {"cohort": cohort})) # Ensure user-defined spatial references for outputs are correctly parsed. # Certain options require normalization to a space not explicitly defined by users. diff --git a/nibabies/data/tests/config.toml b/nibabies/data/tests/config.toml new file mode 100644 index 00000000..b4b398f8 --- /dev/null +++ b/nibabies/data/tests/config.toml @@ -0,0 +1,80 @@ +[environment] +cpu_count = 8 +exec_docker_version = "20.10.12" +exec_env = "nibabies-docker" +free_mem = 0.4 +overcommit_policy = "heuristic" +overcommit_limit = "50%" +nipype_version = "1.6.1" +templateflow_version = "0.7.2" +version = "21.1.0" + +[execution] +bids_dir = "/data" +bids_database_dir = "/tmp/bids_db" +bids_description_hash = "c47e9ebb943ca662556808b2aeac3f6c8bb2a242696c32850c64ec47aba80d9e" +boilerplate_only = false +sloppy = true +debug = [] +derivatives = [ "/opt/derivatives/precomputed",] +fs_license_file = "/opt/freesurfer/license.txt" +fs_subjects_dir = "/opt/subjects" +layout = "BIDS Layout: .../data | Subjects: 1 | Sessions: 1 | Runs: 1" +log_dir = "/tmp/logs" +log_level = 20 +low_mem = false +md_only_boilerplate = false +nibabies_dir = "/out" +notrack = false +output_dir = "/out" +me_output_echos = false +output_layout = "bids" +output_spaces = "MNIInfant:cohort-1:res-native" +reports_only = false +run_uuid = "20220323-202555_01a7d80d-7ff4-4b13-a99c-ec399045e9ff" +segmentation_atlases_dir = "/opt/segmentations" +participant_label = [ "01",] +templateflow_home = "/home/nibabies/.cache/templateflow" +work_dir = "/scratch" +write_graph = false + +[workflow] +anat_only = false +aroma_err_on_warn = false +aroma_melodic_dim = -200 +bold2t1w_dof = 6 +bold2t1w_init = "register" +cifti_output = false +fd_radius = 45 +fmap_bspline = false +force_syn = false +hires = true +ignore = [ "slicetiming",] +longitudinal = false +medial_surface_nan = false +regressors_all_comps = false +regressors_dvars_th = 1.5 +regressors_fd_th = 0.5 +run_reconall = true +skull_strip_fixed_seed = false +skull_strip_template = "UNCInfant:cohort-1" +skull_strip_t1w = "force" +slice_time_ref = 0.5 +spaces = "MNIInfant:cohort-1:res-native MNIInfant:cohort-1" +use_aroma = false +use_bbr = false +use_syn_sdc = false + +[nipype] +crashfile_format = "txt" +get_linked_libs = false +memory_gb = 8 +nprocs = 4 +omp_nthreads = 2 +plugin = "MultiProc" +resource_monitor = false +stop_on_first_crash = false + +[nipype.plugin_args] +maxtasksperchild = 1 +raise_insufficient = false diff --git a/nibabies/tests/__init__.py b/nibabies/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nibabies/tests/test_config.py b/nibabies/tests/test_config.py new file mode 100644 index 00000000..6dc8c32f --- /dev/null +++ b/nibabies/tests/test_config.py @@ -0,0 +1,136 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2021 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Check the configuration module and file.""" +import os +from pathlib import Path +from pkg_resources import resource_filename as pkgrf +from unittest.mock import patch + +import pytest +from toml import loads +from niworkflows.utils.spaces import format_reference + +from .. import config + + +def _reset_config(): + """ + Forcibly reload the configuration module to restore defaults. + .. caution:: + `importlib.reload` creates new sets of objects, but will not remove + previous references to those objects.""" + import importlib + + importlib.reload(config) + + +def test_reset_config(): + execution = config.execution + setattr(execution, 'bids_dir', 'TESTING') + assert config.execution.bids_dir == 'TESTING' + _reset_config() + assert config.execution.bids_dir is None + # Even though the config module was reset, + # previous references to config classes + # have not been touched. + assert execution.bids_dir == 'TESTING' + + +def test_config_spaces(): + """Check that all necessary spaces are recorded in the config.""" + filename = Path(pkgrf('nibabies', 'data/tests/config.toml')) + settings = loads(filename.read_text()) + for sectionname, configs in settings.items(): + if sectionname != 'environment': + section = getattr(config, sectionname) + section.load(configs, init=False) + config.nipype.init() + config.loggers.init() + config.init_spaces() + + spaces = config.workflow.spaces + assert "MNI152NLin6Asym:res-2" not in [str(s) for s in spaces.get_standard(full_spec=True)] + + assert "MNI152NLin6Asym_res-2" not in [ + format_reference((s.fullname, s.spec)) + for s in spaces.references + if s.standard and s.dim == 3 + ] + + config.workflow.use_aroma = True + config.init_spaces() + spaces = config.workflow.spaces + + assert "MNI152NLin6Asym:res-2" in [str(s) for s in spaces.get_standard(full_spec=True)] + + assert "MNI152NLin6Asym_res-2" in [ + format_reference((s.fullname, s.spec)) + for s in spaces.references + if s.standard and s.dim == 3 + ] + + config.execution.output_spaces = None + config.workflow.use_aroma = False + config.workflow.age_months = None + config.init_spaces() + spaces = config.workflow.spaces + + assert [str(s) for s in spaces.get_standard(full_spec=True)] == [] + assert [ + format_reference((s.fullname, s.spec)) + for s in spaces.references + if s.standard and s.dim == 3 + ] == [] + + # but adding age will populate output spaces with a default + config.execution.output_spaces = None + config.workflow.use_aroma = False + config.workflow.age_months = 1 + config.init_spaces() + spaces = config.workflow.spaces + + assert [str(s) for s in spaces.get_standard(full_spec=True)] == [] + assert [ + format_reference((s.fullname, s.spec)) + for s in spaces.references + if s.standard and s.dim == 3 + ] == ['MNIInfant_cohort-1'] + _reset_config() + + +@pytest.mark.parametrize( + "master_seed,ants_seed,numpy_seed", [(1, 17612, 8272), (100, 19094, 60232)] +) +def test_prng_seed(master_seed, ants_seed, numpy_seed): + """Ensure seeds are properly tracked""" + seeds = config.seeds + with patch.dict(os.environ, {}): + seeds.load({'_random_seed': master_seed}, init=True) + assert getattr(seeds, 'master') == master_seed + assert seeds.ants == ants_seed + assert seeds.numpy == numpy_seed + assert os.getenv("ANTS_RANDOM_SEED") == str(ants_seed) + + _reset_config() + for seed in ('_random_seed', 'master', 'ants', 'numpy'): + assert getattr(config.seeds, seed) is None diff --git a/wrapper/nibabies_wrapper.py b/wrapper/nibabies_wrapper.py index 51e30040..6368c68f 100755 --- a/wrapper/nibabies_wrapper.py +++ b/wrapper/nibabies_wrapper.py @@ -26,7 +26,7 @@ MISSING = """ Image '{}' is missing Would you like to download? [Y/n] """ -PKG_PATH = "/usr/local/miniconda/lib/python3.8/site-packages" +PKG_PATH = "/opt/conda/lib/python3.8/site-packages" TF_TEMPLATES = ( "MNI152Lin", "MNI152NLin2009cAsym",