diff --git a/fsl/utils/filetree/__init__.py b/fsl/utils/filetree/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b5ffec3ef7d33eb05b20f6e0b2a4ae2b2b26f538 --- /dev/null +++ b/fsl/utils/filetree/__init__.py @@ -0,0 +1,6 @@ +"""mc_filetree - Easy format to define intput/output files in a python pipeline""" + +__author__ = 'Michiel Cottaar <Michiel.Cottaar@ndcn.ox.ac.uk>' + +from .filetree import FileTree, register_tree, MissingVariable +from .parse import tree_directories diff --git a/fsl/utils/filetree/filetree.py b/fsl/utils/filetree/filetree.py new file mode 100644 index 0000000000000000000000000000000000000000..10afe1e2a344382f801c9e8251f38e3532b320a3 --- /dev/null +++ b/fsl/utils/filetree/filetree.py @@ -0,0 +1,399 @@ +from pathlib import Path, PurePath +from typing import Tuple, Optional, Dict, Any, Set +from copy import deepcopy +from . import parse +import pickle +import os.path as op +from . import utils + + +class MissingVariable(KeyError): + """ + Returned when the variables of a tree or its parents do not contain a given variable + """ + pass + + +class FileTree(object): + """ + Contains the input/output filename tree + + Properties + - ``templates``: dictionary mapping short names to filename templates + - ``variables``: dictionary mapping variables in the templates to specific values (variables set to None are explicitly unset) + - ``sub_trees``: filename trees describing specific sub-directories + - ``parent``: parent FileTree, of which this sub-tree is a sub-directory + """ + def __init__(self, + templates: Dict[str, str], + variables: Dict[str, Any], + sub_trees: Dict[str, "FileTree"]=None, + parent: Optional["FileTree"]=None): + """ + Creates a new filename tree. + """ + self.templates = templates + self.variables = variables + if sub_trees is None: + sub_trees = {} + self.sub_trees = sub_trees + self._parent = parent + + @property + def parent(self, ): + return self._parent + + @property + def all_variables(self, ): + """ + All tree variables including those from the parent tree + """ + if self.parent is None: + return self.variables + res = self.parent.all_variables + res.update(self.variables) + return res + + def get_variable(self, name: str, default=None) -> str: + """ + Gets a variable used to fill out the template + + :param name: variable name + :param default: default variables (if not set an error is raised for a missing variable) + :return: value of the variable + """ + variables = self.all_variables + if name in variables and variables[name] is not None: + return variables[name] + if default is None: + raise MissingVariable('Variable {} not found in sub-tree or parents'.format(name)) + return default + + def _get_template_tree(self, short_name: str) -> Tuple["FileTree", str]: + """ + Retrieves template text from this tree, parent tree or sub_tree + + :param short_name: filename reference name + :return: tuple with the containing tree and the template text + """ + if '/' in short_name: + sub_tree, sub_name = short_name.split('/', maxsplit=1) + if sub_tree == '..': + if self.parent is None: + raise KeyError("Tried to access the parent of the top-level tree") + return self.parent._get_template_tree(sub_name) + return self.sub_trees[sub_tree]._get_template_tree(sub_name) + return self, self.templates[short_name] + + def get_template(self, short_name: str) -> Tuple[str, Dict[str, str]]: + """ + Returns the sub-tree that defines a given short name + + - '/' characters in short_name refer to sub-trees + - '../' characters in short_name refer to parents + + For example: + - eddy/output refers to the output in the eddy sub_tree (i.e. self.sub_trees['eddy'].templates['output'] + - ../other/name refers to the 'other' sub-tree of the parent tree (i.e., self.parent.sub_trees['other'].templates['name'] + + :param short_name: name of the template + :return: tuple with the template and the variables corresponding to the template + """ + tree, text = self._get_template_tree(short_name) + return text, tree.all_variables + + def template_variables(self, short_name: Optional[str]=None, optional=True, required=True) -> Set[str]: + """ + Returns the variables needed to define a template + + :param short_name: name of the template (defaults to all) + :param optional: if set to False don't include the optional variables + :param required: if set to False don't include the required variables + :return: set of variable names + """ + if not optional and not required: + return set() + if short_name is None: + all_vars = set() + required_vars = set() + for short_name in self.templates.keys(): + all_vars.update(self.template_variables(short_name)) + if required or optional: + required_vars.update(self.template_variables(short_name, optional=False)) + for sub_tree in self.sub_trees.values(): + all_vars.update(sub_tree.template_variables()) + if required or optional: + required_vars.update(sub_tree.template_variables(optional=False)) + if optional and required: + return all_vars + if required: + return required_vars + if optional: + return all_vars.difference(required_vars) + else: + _, text = self._get_template_tree(short_name) + all_vars = set(utils.find_variables(text)) + if optional and required: + return all_vars + opt_vars = set(utils.optional_variables(text)) + if optional: + return opt_vars + if required: + return all_vars.difference(opt_vars) + + def get(self, short_name, make_dir=False) -> str: + """ + Gets a full filename based on its short name + + :param short_name: identifier in the tree + :param make_dir: if True make sure that the directory leading to this file exists + :return: full filename + """ + text, variables = self.get_template(short_name) + res = Path(utils.resolve(text, variables)) + if make_dir: + res.parents[0].mkdir(parents=True, exist_ok=True) + return str(res) + + def get_all(self, short_name: str, glob_vars=()) -> Tuple[str]: + """ + Gets all existing directory/file names matching a specific pattern + + :param short_name: short name of the path template + :param glob_vars: sequence of undefined variables that can take any possible values when looking for matches on the disk + Any defined variables in `glob_vars` will be ignored. + If glob_vars is set to 'all', all undefined variables will be used to look up matches + :return: sorted sequence of paths + """ + text, variables = self.get_template(short_name) + return tuple(utils.get_all(text, variables, glob_vars=glob_vars)) + + def get_all_vars(self, short_name: str, glob_vars=()) -> Tuple[Dict[str, str]]: + """ + Gets all the parameters that generate existing filenames + + :param short_name: short name of the path template + :param glob_vars: sequence of undefined variables that can take any possible values when looking for matches on the disk + Any defined variables in `glob_vars` will be ignored. + If glob_vars is set to 'all', all undefined variables will be used to look up matches + :return: sequence of dictionaries with the variables settings used to generate each filename + """ + return tuple(self.extract_variables(short_name, fn) for fn in self.get_all(short_name, glob_vars=glob_vars)) + + def update(self, **variables) -> "FileTree": + """ + Creates a new filetree with updated variables + + :arg variables: new values for the variables + Setting variables to None will explicitly unset them + """ + new_tree = deepcopy(self) + new_tree.variables.update(variables) + return new_tree + + def extract_variables(self, short_name: str, filename: str) -> Dict[str, str]: + """ + Extracts the variables from the given filename + + :param short_name: short name of the path template + :param filename: filename matching the template + :return: variables needed to get to the given filename + Variables with None value are optional variables in the template that were not used + """ + text, _ = self.get_template(short_name) + return utils.extract_variables(text, filename, self.variables) + + def save_pickle(self, filename): + """ + Saves the Filetree to a pickle file + + :param filename: filename to store the file tree (usually ending with .pck) + """ + with open(filename, 'wb') as f: + pickle.dump(self, f) + + @classmethod + def load_pickle(cls, filename): + """ + Loads the Filetree from a pickle file + + :param filename: filename produced from Filetree.save + :return: stored Filetree + """ + with open(filename, 'rb') as f: + res = pickle.load(f) + if not isinstance(res, cls): + raise IOError("Pickle file did not contain %s object" % cls) + return res + + def exists(self, short_names, on_disk=False, error=False, glob_vars=()): + """ + Checks whether the short_names are defined in the tree (and optional exist on the disk) + + :param short_names: list of expected short names to exist in the tree + :param on_disk: if True checks whether the files exist on disk + :param error: if True raises a helpful error when the check fails + :param glob_vars: sequence of undefined variables that can take any possible values when looking for matches on the disk + If `glob_vars` contains any defined variables, it will be ignored. + :return: True if short names exist and optionally exist on disk (False otherwise) + :raise: + - ValueError if error is set and the tree is incomplete + - IOError if error is set and any files are missing from the disk + """ + if isinstance(short_names, str): + short_names = (short_names, ) + + def single_test(short_name): + try: + self._get_template_tree(short_name) + except KeyError: + return True + return False + + missing = tuple(name for name in short_names if single_test(name)) + if len(missing) > 0: + if error: + raise ValueError("Provided Filetree is missing file definitions for {}".format(missing)) + return False + if on_disk: + try: + missing = tuple(name for name in short_names if len(self.get_all(name, glob_vars=glob_vars)) == 0) + except KeyError: + if error: + raise + return False + if len(missing) > 0: + if error: + raise IOError("Failed to find any files existing for {}".format(missing)) + return False + return True + + @classmethod + def read(cls, tree_name: str, directory='.', **variables) -> "FileTree": + """ + Reads a FileTree from a specific file + + The return type is ``cls`` unless the tree_name has been previously registered + The return type of any sub-tree is FileTree unless the tree_name has been previously registered + + :param tree_name: file containing the filename tree. + Can provide the filename of a tree file or the name for a tree in the ``filetree.tree_directories``. + :param directory: parent directory of the full tree (defaults to current directory) + :param variables: variable settings + :return: dictionary from specifier to filename + """ + if op.exists(tree_name): + filename = tree_name + elif op.exists(tree_name + '.tree'): + filename = tree_name + '.tree' + else: + filename = parse.search_tree(tree_name) + filename = Path(filename) + + templates = {} + nspaces_level = [] + sub_trees = {} + + file_variables = {} + + with open(str(filename), 'r') as f: + for full_line in f: + # ignore anything behind the first #-character + line = full_line.split('#')[0] + + if len(line.strip()) == 0: + continue + + if line.strip()[:2] == '->': + nspaces = line.index('->') + + if len(nspaces_level) == 0: + sub_dir = directory + elif nspaces > nspaces_level[-1]: + sub_dir = current_filename + elif nspaces < nspaces_level[-1]: + if nspaces not in nspaces_level: + raise ValueError('line %s dropped to a non-existent level' % line) + new_level = nspaces_level.index(nspaces) + current_filename = current_filename.parents[len(nspaces_level) - new_level - 1] / filename + nspaces_level = nspaces_level[:new_level + 1] + sub_dir = current_filename.parents[0] + else: + sub_dir = current_filename.parents[0] + + _, sub_tree, short_name = parse.read_subtree_line(line, sub_dir) + if short_name in sub_trees: + raise ValueError("Name of sub_tree {short_name} used multiple times in {tree_name}.tree".format(**locals())) + + sub_trees[short_name] = sub_tree + elif '=' in line: + key, value = line.split('=') + if len(key.split()) != 1: + raise ValueError("Variable assignment could not be parsed: {line}".format(**locals())) + file_variables[key.strip()] = value.strip() + else: + nspaces, filename, short_name = parse.read_line(line) + if short_name in templates: + raise ValueError("Name of directory/file {short_name} used multiple times in {tree_name}.tree".format(**locals())) + + if len(nspaces_level) == 0: + current_filename = PurePath(directory) / filename + nspaces_level.append(nspaces) + elif nspaces > nspaces_level[-1]: + # increase the level + current_filename = current_filename / filename + nspaces_level.append(nspaces) + elif nspaces < nspaces_level[-1]: + # decreased the level + if nspaces not in nspaces_level: + raise ValueError('line %s dropped to a non-existent level' % full_line) + new_level = nspaces_level.index(nspaces) + current_filename = current_filename.parents[len(nspaces_level) - new_level - 1] / filename + nspaces_level = nspaces_level[:new_level + 1] + else: + current_filename = current_filename.parents[0] / filename + templates[short_name] = str(current_filename) + + file_variables.update(variables) + res = get_registered(tree_name, cls)(templates, variables=file_variables, sub_trees=sub_trees) + for tree in sub_trees.values(): + tree._parent = res + return res + + +_registered_subtypes = {} + + +def register_tree(name: str, tree_subtype: type): + """ + Registers a tree_subtype under name + + Loading a tree with given name will lead to the `tree_subtype` rather than FileTree to be returned + + :param name: name of tree filename + :param tree_subtype: tree subtype + """ + global _registered_subtypes + if not issubclass(tree_subtype, FileTree): + raise ValueError("Only sub-classes of FileTree can be registered") + _registered_subtypes[name] = tree_subtype + + +def get_registered(name, default=FileTree) -> type: + """ + Get the previously registered subtype for ``name`` + + :param name: name of the sub-tree + :param default: type to return if the name has not been registered + :return: FileTree or sub-type thereof + """ + if name in _registered_subtypes: + return _registered_subtypes[name] + name = op.split(name)[1] + if name in _registered_subtypes: + return _registered_subtypes[name] + while name.endswith('.tree'): + name = name[:-5] + if name in _registered_subtypes: + return _registered_subtypes[name] + return default diff --git a/fsl/utils/filetree/parse.py b/fsl/utils/filetree/parse.py new file mode 100644 index 0000000000000000000000000000000000000000..2e5195361b2fe1b51bb0dcb60219949adaff9a50 --- /dev/null +++ b/fsl/utils/filetree/parse.py @@ -0,0 +1,76 @@ +import os.path as op +from . import filetree +from pathlib import PurePath +from typing import Tuple +import re + + +tree_directories = ['.', op.join(op.split(__file__)[0], 'trees')] + + +def search_tree(name: str) -> str: + """ + Searches for the file defining the specific tree + + Iteratively searches through the directories in ``tree_directories`` till a file named ${name}.tree is found + + :param name: Name of the tree + :return: path to the file defining the tree + """ + for directory in tree_directories: + filename = op.join(directory, name) + if op.exists(filename): + return filename + elif op.exists(filename + '.tree'): + return filename + '.tree' + raise ValueError("No file tree found for %s" % name) + + +def read_line(line: str) -> Tuple[int, PurePath, str]: + """ + Parses line from the tree file + + :param line: input line from a *.tree file + :return: Tuple with: + - number of spaces in front of the name + - name of the file or the sub_tree + - short name of the file + """ + if line.strip()[:1] == '->': + return read_subtree_line(line) + match = re.match(r'^(\s*)(\S*)\s*\((\S*)\)\s*$', line) + if match is not None: + gr = match.groups() + return len(gr[0]), PurePath(gr[1]), gr[2] + match = re.match(r'^(\s*)(\S*)\s*$', line) + if match is not None: + gr = match.groups() + short_name = gr[1].split('.')[0] + return len(gr[0]), PurePath(gr[1]), short_name + raise ValueError('Unrecognized line %s' % line) + + +def read_subtree_line(line: str, directory: str) -> Tuple[int, "filetree.FileTree", str]: + """ + Parses the line defining a sub_tree + + :param line: input line from a *.tree file + :param directory: containing directory + :return: Tuple with + - number of spaces in front of the name + - sub_tree + - short name of the sub_tree + """ + match = re.match(r'^(\s*)->\s*(\S*)(.*)\((\S*)\)', line) + if match is None: + raise ValueError("Sub-tree line could not be parsed: {}".format(line.strip())) + spaces, type_name, variables_str, short_name = match.groups() + + variables = {} + if len(variables_str.strip()) != 0: + for single_variable in variables_str.split(','): + key, value = single_variable.split('=') + variables[key.strip()] = value.strip() + + sub_tree = filetree.FileTree.read(type_name, directory, **variables) + return len(spaces), sub_tree, short_name diff --git a/fsl/utils/filetree/trees/BedpostX.tree b/fsl/utils/filetree/trees/BedpostX.tree new file mode 100644 index 0000000000000000000000000000000000000000..4bc58e42b8802334ddfe0635ea72875b76cdd2f4 --- /dev/null +++ b/fsl/utils/filetree/trees/BedpostX.tree @@ -0,0 +1,40 @@ +bvals +bvecs +commands.txt +dyads1.nii.gz +dyads1_dispersion.nii.gz +dyads2.nii.gz +dyads2_dispersion.nii.gz +dyads2_thr0.05.nii.gz (dyads2_thr0.05) +dyads2_thr0.05_modf2.nii.gz (dyads2_thr0.05_modf2) +dyads3.nii.gz +dyads3_dispersion.nii.gz +dyads3_thr0.05.nii.gz (dyads3_thr0.05) +dyads3_thr0.05_modf3.nii.gz (dyads3_thr0.05_modf3) +mean_Rsamples.nii.gz +mean_S0samples.nii.gz +mean_d_stdsamples.nii.gz +mean_dsamples.nii.gz +mean_f1samples.nii.gz +mean_f2samples.nii.gz +mean_f3samples.nii.gz +mean_fsumsamples.nii.gz +mean_ph1samples.nii.gz +mean_ph2samples.nii.gz +mean_ph3samples.nii.gz +mean_tausamples.nii.gz +mean_th1samples.nii.gz +mean_th2samples.nii.gz +mean_th3samples.nii.gz +merged_f1samples.nii.gz +merged_f2samples.nii.gz +merged_f3samples.nii.gz +merged_ph1samples.nii.gz +merged_ph2samples.nii.gz +merged_ph3samples.nii.gz +merged_th1samples.nii.gz +merged_th2samples.nii.gz +merged_th3samples.nii.gz +nodif_brain_mask.nii.gz +xfms + eye.mat diff --git a/fsl/utils/filetree/trees/Diffusion.tree b/fsl/utils/filetree/trees/Diffusion.tree new file mode 100644 index 0000000000000000000000000000000000000000..76c3ee16cf3f4456ab43159d2116dce763ba1a20 --- /dev/null +++ b/fsl/utils/filetree/trees/Diffusion.tree @@ -0,0 +1,5 @@ +data.nii.gz +nodif_brain_mask.nii.gz +bvals +bvecs +grad_dev.nii.gz diff --git a/fsl/utils/filetree/trees/HCP_Surface.tree b/fsl/utils/filetree/trees/HCP_Surface.tree new file mode 100644 index 0000000000000000000000000000000000000000..7850b8db7ff488c38674ab6e6d9d5514258fe980 --- /dev/null +++ b/fsl/utils/filetree/trees/HCP_Surface.tree @@ -0,0 +1,26 @@ +{subject}.{hemi}.sulc.{space}.shape.gii (sulc_gifti) +{subject}.{hemi}.atlasroi.{space}.shape.gii (atlasroi_gifti) +{subject}.{hemi}.thickness.{space}.shape.gii (thick_gifti) +{subject}.{hemi}.curvature.{space}.shape.gii (curv_gifti) +{subject}.{hemi}.white.{space}.surf.gii (white) +{subject}.{hemi}.midthickness.{space}.surf.gii (mid) +{subject}.{hemi}.pial.{space}.surf.gii (pial) +{subject}.{hemi}.inflated.{space}.surf.gii (inflated) +{subject}.{hemi}.very_inflated.{space}.surf.gii (very_inflated) +{subject}.{hemi}.white_MSMAll.{space}.surf.gii (white_msm) +{subject}.{hemi}.midthickness_MSMAll.{space}.surf.gii (mid_msm) +{subject}.{hemi}.pial_MSMAll.{space}.surf.gii (pial_msm) +{subject}.{hemi}.inflated_MSMAll.{space}.surf.gii (inflated_msm) +{subject}.{hemi}.very_inflated_MSMAll.{space}.surf.gii (very_inflated_msm) +{subject}.{hemi}.sphere.{space}.surf.gii (sphere) +{subject}.{hemi}.sphere.reg.native.surf.gii (sphere_reg) +{subject}.{hemi}.sphere.reg.reg_LR.native.surf.gii (sphere_reg_LR) +{subject}.{hemi}.sphere.MSMSulc.native.surf.gii (sphere_MSMSulc) +{subject}.{hemi}.sphere.MSMAll.native.surf.gii (sphere_msm) +{subject}.{space}.wb.spec (spec) +{subject}.sulc.{space}.dscalar.nii (sulc_cifti) +{subject}.thickness.{space}.dscalar.nii (thick_cifti) +{subject}.curvature.{space}.dscalar.nii (curv_cifti) +{subject}.sulc_MSMAll.{space}.dscalar.nii (sulc_msm_cifti) +{subject}.thickness_MSMAll.{space}.dscalar.nii (thick_msm_cifti) +{subject}.curvature_MSMAll.{space}.dscalar.nii (curv_msm_cifti) diff --git a/fsl/utils/filetree/trees/HCP_directory.tree b/fsl/utils/filetree/trees/HCP_directory.tree new file mode 100644 index 0000000000000000000000000000000000000000..11c1ec5eb00767c802dd98daede9f5fe2a193123 --- /dev/null +++ b/fsl/utils/filetree/trees/HCP_directory.tree @@ -0,0 +1,53 @@ +{subject} (directory) + MNINonLinear + ->HCP_Surface space=164k_fs_LR (mni_164k) + BiasField.nii.gz + Native (mni_native) + ->HCP_Surface space=native (mni_native) + ROIs + T1w.nii.gz + T1w_restore.2.nii.gz (T1w_restore_2) + T1w_restore.nii.gz + T1w_restore_brain.nii.gz + T2w.nii.gz + T2w_restore.2.nii.gz (T2w_restore_2) + T2w_restore.nii.gz + T2w_restore_brain.nii.gz + aparc+aseg.nii.gz (mni_aparc) + aparc.a2009s+aseg.nii.gz (min_aparc_a2009s) + brainmask_fs.nii.gz (mni_brainmask) + fsaverage_LR32k (mni_32k) + ->HCP_Surface space=32k_fs_LR (mni_32k) + ribbon.nii.gz (mni_ribbon) + wmparc.nii.gz (mni_wmparc) + xfms + NonlinearRegJacobians.nii.gz + acpc_dc2standard.nii.gz + standard2acpc_dc.nii.gz + T1w (acpc_dc) + Diffusion + ->Diffusion (Diffusion) + Diffusion.bedpostX (bedpost) + ->BedpostX (bedpost) + {subject}_3T.csv + BiasField_acpc_dc.nii.gz + Native (T1w_native) + ->HCP_Surface space=native (T1w_native) + T1wDividedByT2w.nii.gz + T1wDividedByT2w_ribbon.nii.gz + T1w_acpc_dc.nii.gz + T1w_acpc_dc_restore.nii.gz + T1w_acpc_dc_restore_brain.nii.gz + T1w_acpc_dc_restore_1.25.nii.gz (T1w_diffusion) + T2w_acpc_dc.nii.gz + T2w_acpc_dc_restore.nii.gz + T2w_acpc_dc_restore_brain.nii.gz + aparc+aseg.nii.gz (T1w_aparc) + aparc.a2009s+aseg.nii.gz (T1w_apard_a2009s) + brainmask_fs.nii.gz (T1w_brainmask) + fsaverage_LR32k (T1w_32k) + ->HCP_Surface space=32k_fs_LR (T1w_32k) + ribbon.nii.gz (T1w_ribbon) + wmparc.nii.gz (T1w_wmparc) + release-notes + diff --git a/fsl/utils/filetree/trees/ProbtrackX.tree b/fsl/utils/filetree/trees/ProbtrackX.tree new file mode 100644 index 0000000000000000000000000000000000000000..38db04c26930365b9046b8171ba28bfbd116ff39 --- /dev/null +++ b/fsl/utils/filetree/trees/ProbtrackX.tree @@ -0,0 +1,46 @@ +basename=fdt + +probtrackx.log (log_cmd) +{basename}.log (log_settings) +waytotal + +# single seed tracking +coords = {x}_{y}_{z} # default format for coords +{basename}_{coords}.nii.gz (single_path) # --opd output +{basename}_{coords}_length.nii.gz (single_path_length) # --ompl output +{basename}_{coords}_alt.nii.gz (single_alt_path) # --opd/--fopd output +{basename}_{coords}_alt_length.nii.gz (single_alt_path_length) # --ompl/--fopd output + +# output paths +{basename}.nii.gz (path) # --opd output +{basename}_length.nii.gz (path_length) # --ompl output + +{basename}_alt.nii.gz (alt_path) # --opd/--fopd output +{basename}_alt_length.nii.gz (alt_path_length) # --ompl/--fopd output + +# target mask connectivity +seeds_to_{target}.nii.gz (seed2target) # --os2t/--targetmasks output +seeds_{seed_id}_to_{target}.nii.gz (multi_seed2target) # --os2t/--targetmasks with multiple seeds +target_paths_{target}.nii.gz (target_path) # --otargetpaths + +# ROIxROI connectivity (--network) +fdt_network_matrix (network) +tmpnetmaskfile (network_masks) + +# matrix output +# matrix 1, 2, or 3 +coords_for_fdt_matrix{mat_id} (mat_seed_coord) # matrix 1, 2, or3 +fdt_matrix{mat_id}.dot (mat) # matrix 1, 2, or 3 +lookup_tractspace_fdt_matrix{mat_id}.nii.gz (mat_target_space) # only matrix 2 +tract_space_coords_for_fdt_matrix{mat_id} (mat_target_coord) # matrix 2 or 3 + +# matrix4 +fdt_matrix4_{part}.mtx (mat4) +lookup_tractspace_fdt_matrix4.nii.gz (mat4_space) +tract_space_coords_for_fdt_matrix4 (mat4_target_coord) + + +# other +{basename}_localdir.nii.gz (localdir) # --opathdir + +saved_paths.txt # --savepaths diff --git a/fsl/utils/filetree/trees/bet.tree b/fsl/utils/filetree/trees/bet.tree new file mode 100644 index 0000000000000000000000000000000000000000..b2c8f1b23dd05e9efbd88f199443867bb01752b0 --- /dev/null +++ b/fsl/utils/filetree/trees/bet.tree @@ -0,0 +1,3 @@ +{name}.nii.gz (input) +{name}_brain.nii.gz (output) +{name}_brain_mask.nii.gz (mask) \ No newline at end of file diff --git a/fsl/utils/filetree/trees/bids_raw.tree b/fsl/utils/filetree/trees/bids_raw.tree new file mode 100644 index 0000000000000000000000000000000000000000..d07f190047a01670e77b74475f7caeadf5df22a1 --- /dev/null +++ b/fsl/utils/filetree/trees/bids_raw.tree @@ -0,0 +1,30 @@ +ext=.nii.gz + +dataset_description.json +participants.tsv +README +CHANGES +sub-{participant} + [ses-{session}] + sub-{participant}_sessions.tsv (sessions_tsv) + anat (anat_dir) + sub-{participant}[_ses-{session}][_acq-{acq}][_rec-{rec}][_run-{run_index}]_{modality}{ext} (anat_image) + func (func_dir) + sub-{participant}[_ses-{session}]_task-{task}[_acq-{acq}][_rec-{rec}][_run-{run_index}]_bold{ext} (task_image) + sub-{participant}[_ses-{session}]_task-{task}[_acq-{acq}][_rec-{rec}][_run-{run_index}]_sbref{ext} (task_sbref) + sub-{participant}[_ses-{session}]_task-{task}[_acq-{acq}][_rec-{rec}][_run-{run_index}]_events.tsv (task_events) + sub-{participant}[_ses-{session}]_task-{task}[_acq-{acq}][_rec-{rec}][_run-{run_index}][_recording-{recording}]_physio{ext} (task_physio) + sub-{participant}[_ses-{session}]_task-{task}[_acq-{acq}][_rec-{rec}][_run-{run_index}][_recording-{recording}]_stim{ext} (task_stim) + dwi (dwi_dir) + sub-{participant}[_ses-{session}][_acq-{acq}][_run-{run_index}]_dwi{ext} (dwi_image) + sub-{participant}[_ses-{session}][_acq-{acq}][_run-{run_index}]_dwi.bval (bval) + sub-{participant}[_ses-{session}][_acq-{acq}][_run-{run_index}]_dwi.bvec (bvec) + fmap (fmap_dir) + sub-{participant}[_ses-{session}][_acq-{acq}][_run-{run_index}]_phasediff{ext} (fmap_phasediff) + sub-{participant}[_ses-{session}][_acq-{acq}][_run-{run_index}]_magnitude{ext} (fmap_mag) + sub-{participant}[_ses-{session}][_acq-{acq}][_run-{run_index}]_magnitude1{ext} (fmap_mag1) + sub-{participant}[_ses-{session}][_acq-{acq}][_run-{run_index}]_magnitude2{ext} (fmap_mag2) + sub-{participant}[_ses-{session}][_acq-{acq}][_run-{run_index}]_phase1{ext} (fmap_phase1) + sub-{participant}[_ses-{session}][_acq-{acq}][_run-{run_index}]_phase2{ext} (fmap_phase2) + sub-{participant}[_ses-{session}][_acq-{acq}][_run-{run_index}]_fieldmap{ext} (fmap) + sub-{participant}[_ses-{session}][_acq-{acq}]_dir-{dir}[_run-{run_index}]_epi{ext} (fmap_epi) diff --git a/fsl/utils/filetree/trees/dti.tree b/fsl/utils/filetree/trees/dti.tree new file mode 100644 index 0000000000000000000000000000000000000000..bac4c29ae37e26302a4c30c2ab9ca0ba2d5efc1d --- /dev/null +++ b/fsl/utils/filetree/trees/dti.tree @@ -0,0 +1,14 @@ +basename = dti + +{basename} (basename) +{basename}_MD.nii.gz (MD) +{basename}_FA.nii.gz (FA) +{basename}_MO.nii.gz (MO) +{basename}_V1.nii.gz (V1) +{basename}_V2.nii.gz (V2) +{basename}_V3.nii.gz (V3) +{basename}_L1.nii.gz (L1) +{basename}_L2.nii.gz (L2) +{basename}_L3.nii.gz (L3) + +{basename}_MK.nii.gz (MK) diff --git a/fsl/utils/filetree/trees/eddy.tree b/fsl/utils/filetree/trees/eddy.tree new file mode 100644 index 0000000000000000000000000000000000000000..323abfd78a65ec65761ef7886d091105a0896b1a --- /dev/null +++ b/fsl/utils/filetree/trees/eddy.tree @@ -0,0 +1,14 @@ +basename = eddy_output + +{basename} (basename) +{basename}.eddy_movement_rms (movement_rms) +{basename}.eddy_outlier_map (outlier_map) +{basename}.eddy_outlier_n_sqr_stdev_map (outlier_n_sqr_stdev_map) +{basename}.eddy_outlier_n_stdev_map (outlier_n_stdev_map) +{basename}.eddy_outlier_report (outlier_report) +{basename}.eddy_parameters (paramaters) +{basename}.eddy_post_eddy_shell_PE_translation_parameters (shell_PE_translation_parameters) +{basename}.eddy_post_eddy_shell_alignment_parameters (shell_alignment_parameters) +{basename}.eddy_restricted_movement_rms (restricted_movement_rms) +{basename}.eddy_rotated_bvecs (rotated_bvecs) +{basename}.nii.gz (image) diff --git a/fsl/utils/filetree/trees/epi_reg.tree b/fsl/utils/filetree/trees/epi_reg.tree new file mode 100644 index 0000000000000000000000000000000000000000..729d87859e38f688e3f9462adb3c7da4243dfa80 --- /dev/null +++ b/fsl/utils/filetree/trees/epi_reg.tree @@ -0,0 +1,6 @@ +{basename} (basename) +{basename}.mat (transform) +{basename}.nii.gz (output) +{basename}_fast_wmedge.nii.gz (wmedge) +{basename}_fast_wmseg.nii.gz (wmseg) +{basename}_init.mat (init_transform) \ No newline at end of file diff --git a/fsl/utils/filetree/trees/fast.tree b/fsl/utils/filetree/trees/fast.tree new file mode 100644 index 0000000000000000000000000000000000000000..f39d407484924f7358ad95bed359b79d370e1e4f --- /dev/null +++ b/fsl/utils/filetree/trees/fast.tree @@ -0,0 +1,11 @@ +{basename} (basename) +{basename}.nii.gz (input) +{basename}_mixeltype.nii.gz (mixeltype) +{basename}_pve_{tissue_idx}.nii.gz (pve) +{basename}_pveseg.nii.gz (pveseg) +{basename}_seg.nii.gz (seg) + +# for T1-weighted images and 3 classes: +{basename}_pve_0.nii.gz (csf_pve) +{basename}_pve_1.nii.gz (gm_pve) +{basename}_pve_2.nii.gz (wm_pve) diff --git a/fsl/utils/filetree/trees/freesurfer.tree b/fsl/utils/filetree/trees/freesurfer.tree new file mode 100644 index 0000000000000000000000000000000000000000..8a20ac2110861bd31205235bfee3ce0715e40240 --- /dev/null +++ b/fsl/utils/filetree/trees/freesurfer.tree @@ -0,0 +1,76 @@ +{subject} (directory) + orig (struct_dir) + 001.mgz (vol1) + 002.mgz (vol2) + T2raw.mgz + FLAIRraw.mgz + rawavg.mgz + orig.mgz + orig_nu.mgz + transforms + talairach.auto.xfm (talairach_auto) + talairach.xfm (talairach_xfm) + talairach_avi.log + talairach_with_skull.lta + talairach.lta (talairach_lta) + talairach.m3z (talairach_m3z) + T2raw.lta (T2raw_lta) + nu.mgz + T1.mgz + brainmask.auto.mgz (brainmask_auto) + brainmask.mgz + norm.mgz + brain.mgz + brain.finalsurfs.mgz (final_surfs) + wm.seg.mgz (wm_seg) + wm.asegedit.mgz (wm_asegedit) + wm.mgz + filled.mgz + filled-pretess255.mgz + filled-pretess127.mgz + ribbon.mgz (vol_ribbon) + wmparc.mgz + + T2.prenorm.mgz (T2_prenorm) + T2.norm.mgz (T2_norm) + T2.mgz + + {hemi}h.orig.nofix (orig_nofix) + {hemi}h.smoothwm.nofix (smoothwm_nofix) + {hemi}h.inflated.nofix (inflated_nofix) + {hemi}h.qsphere.nofix (qsphere_nofix) + {hemi}h.orig (white) + {hemi}h.inflated (inflated) + {hemi}h.sphere (sphere) + {hemi}h.sphere.reg (sphere_reg) + {hemi}h.white.preaparc (white_preaparc) + {hemi}h.curv (curv) + {hemi}h.sulc (sulc) + {hemi}h.area (area) + {hemi}h.jacobian_white (jacobian_white) + {hemi}h.avg_curv (avg_curv) + {hemi}h.cortex.label (cortex_label) + {hemi}h.smoothwm (smoothwm) + {hemi}h.qsphere (qsphere) + + {hemi}h.pial (pial) + {hemi}h.woT2.pial (woT2_pial) + {hemi}h.curv.pial (curv_pial) + {hemi}h.area.pial (area_pial) + {hemi}h.thickness (thickness) + {hemi}h.ribbon.mgz (surf_ribbon) + + aseg.auto_noCCseg.mgz (aseg_auto_noCCseg) + aseg.auto.mgz (aseg_auto) + aseg.presurf.mgz (aseg_presurf) + aparc+aseg.mgz (aparc_aseg) + aparc.a2009s+aseg.mgz (aparc_a2009s_aseg) + aseg.mgz + + label/ + {hemi}h.aparc.annot (aparc.annot) + + stats/ + aseg.stats (aseg_stats) + wmparc.stats (wmparc_stats) + diff --git a/fsl/utils/filetree/trees/tbss.tree b/fsl/utils/filetree/trees/tbss.tree new file mode 100644 index 0000000000000000000000000000000000000000..4e89f0d43e390a05fa7ad5b6b1a7e4229b954a52 --- /dev/null +++ b/fsl/utils/filetree/trees/tbss.tree @@ -0,0 +1,21 @@ +FA + {subject}_FA.nii.gz (eroded) + {subject}_FA_mask.nii.gz (mask) + {subject}_FA_to_target.mat (affine2std) + {subject}_FA_to_target.nii.gz (eroded_std) + {subject}_FA_to_target_warp.msf (warp2std_msf) + {subject}_FA_to_target_warp.nii.gz (warp2std) + {subject}_FA_to_target_warp_inv.nii.gz (warp2std_inv) + slicesdir + index.html (eroded_html) +origdata + {subject}.nii.gz (input) +stats + all_FA.nii.gz + all_FA_skeletonised.nii.gz + mean_FA.nii.gz + mean_FA_mask.nii.gz + mean_FA_skeleton.nii.gz + mean_FA_skeleton_mask.nii.gz + mean_FA_skeleton_mask_dst.nii.gz + thresh.txt diff --git a/fsl/utils/filetree/trees/topup.tree b/fsl/utils/filetree/trees/topup.tree new file mode 100644 index 0000000000000000000000000000000000000000..9a6702dd65a3da0b36e8d012d9e75585340f6c50 --- /dev/null +++ b/fsl/utils/filetree/trees/topup.tree @@ -0,0 +1,5 @@ +basename = topup_output + +{basename} (basename) +{basename}_fieldcoef.nii.gz (fieldcoef) +{basename}_movpar.txt (movement) diff --git a/fsl/utils/filetree/utils.py b/fsl/utils/filetree/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f78046fff8f6878309225b55ae22d6c8dab519ff --- /dev/null +++ b/fsl/utils/filetree/utils.py @@ -0,0 +1,178 @@ +import re +import itertools +import glob +from . import filetree + + +def resolve(template, variables): + """ + Resolves the template given a set of variables + + :param template: template + :param variables: mapping of variable names to values + :return: cleaned string + """ + filled = fill_known(template, variables) + filename = resolve_optionals(filled) + remaining = find_variables(filename) + if len(remaining) > 0: + raise filetree.MissingVariable('Variables %s not defined' % set(remaining)) + return filename + + +def get_all(template, variables, glob_vars=()): + """ + Gets all variables matching the templates given the variables + + :param template: template + :param variables: (incomplete) mapping of variable names to values + :param glob_vars: sequence of undefined variables that can take any possible values when looking for matches on the disk + If `glob_vars` contains any defined variables, it will be ignored. + :return: sequence of filenames + """ + filled = fill_known(template, variables) + remaining = set(find_variables(filled)) + optional = optional_variables(filled) + res = set() + if glob_vars == 'all': + glob_vars = remaining + glob_vars = set(glob_vars).difference(variables.keys()) + + undefined_vars = remaining.difference(glob_vars).difference(optional) + if len(undefined_vars) > 0: + raise KeyError("Required variables {} were not defined".format(undefined_vars)) + + for keep in itertools.product(*[(True, False) for _ in optional.intersection(glob_vars)]): + sub_variables = {var: '*' for k, var in zip(keep, optional) if k} + for var in remaining.difference(optional).intersection(glob_vars): + sub_variables[var] = '*' + sub_filled = fill_known(filled, sub_variables) + + pattern = resolve_optionals(sub_filled) + assert len(find_variables(pattern)) == 0 + + for filename in glob.glob(pattern): + try: + extract_variables(filled, filename) + except ValueError: + continue + res.add(filename) + return sorted(res) + + +def fill_known(template, variables): + """ + Fills in the known variables filling the other variables with {<variable_name>} + + :param template: template + :param variables: mapping of variable names to values (ignoring any None) + :return: cleaned string + """ + prev = '' + while prev != template: + prev = template + settings = {} + for name in set(find_variables(template)): + if name in variables and variables[name] is not None: + settings[name] = variables[name] + else: + settings[name] = '{' + name + '}' + template = template.format(**settings) + return template + + +def resolve_optionals(text): + """ + Resolves the optional sections + + :param text: template after filling in the known variables + :return: cleaned string + """ + def resolve_single_optional(part): + if len(part) == 0: + return part + if part[0] != '[' or part[-1] != ']': + return part + elif len(find_variables(part)) == 0: + return part[1:-1] + else: + return '' + + res = [resolve_single_optional(text) for text in re.split('(\[.*?\])', text)] + return ''.join(res) + + +def find_variables(template): + """ + Finds all the variables in the template + + :param template: full template + :return: sequence of variables + """ + return tuple(var.split(':')[0] for var in re.findall("\{(.*?)\}", template)) + + +def optional_variables(template): + """ + Finds the variables that can be skipped + + :param template: full template + :return: set of variables that are only present in optional parts of the string + """ + include = set() + exclude = set() + for text in re.split('(\[.*?\])', template): + if len(text) == 0: + continue + variables = find_variables(text) + if text[0] == '[' and text[-1] == ']': + include.update(variables) + else: + exclude.update(variables) + return include.difference(exclude) + + +def extract_variables(template, filename, known_vars=None): + """ + Extracts the variable values from the filename + + :param template: template matching the given filename + :param filename: filename + :param known_vars: already known variables + :return: dictionary from variable names to string representations (unused variables set to None) + """ + if known_vars is None: + known_vars = {} + template = fill_known(template, known_vars) + while '//' in filename: + filename = filename.replace('//', '/') + remaining = set(find_variables(template)) + optional = optional_variables(template) + for keep in itertools.product(*[(True, False) for _ in optional]): + sub_re = resolve_optionals(fill_known( + template, + dict( + **{var: '(\S+)' for k, var in zip(keep, optional) if k}, + **{var: '(\S+)' for var in remaining.difference(optional)} + ) + )) + while '//' in sub_re: + sub_re = sub_re.replace('//', '/') + if re.match(sub_re, filename) is None: + continue + + extracted_value = {} + kept_vars = [var for var in find_variables(template) + if var not in optional or keep[list(optional).index(var)]] + for var, value in zip(kept_vars, re.match(sub_re, filename).groups()): + if var in extracted_value: + if value != extracted_value[var]: + raise ValueError('Multiple values found for {}'.format(var)) + else: + extracted_value[var] = value + for name in find_variables(template): + if name not in extracted_value: + extracted_value[name] = None + extracted_value.update(known_vars) + return extracted_value + raise ValueError("{} did not match {}".format(filename, template)) diff --git a/setup.py b/setup.py index 63a6da5b801304ac8f3867c7ab3832eb198aac5e..1383c0e1228c3093d0de4433853e5f1adf2ec26d 100644 --- a/setup.py +++ b/setup.py @@ -111,6 +111,7 @@ setup( install_requires=install_requires, extras_require=extra_requires, + package_data={'fsl': ['utils/filetree/trees/*']}, test_suite='tests', diff --git a/tests/test_filetree/custom.tree b/tests/test_filetree/custom.tree new file mode 100644 index 0000000000000000000000000000000000000000..ee08e30a03d47f0433f46fe24e3c234e6c654f41 --- /dev/null +++ b/tests/test_filetree/custom.tree @@ -0,0 +1,4 @@ +parent + [opt_layer_{opt}] + sub_file.nii.gz + opt_file_{opt}.nii.gz (opt_file) \ No newline at end of file diff --git a/tests/test_filetree/extract_vars.tree b/tests/test_filetree/extract_vars.tree new file mode 100644 index 0000000000000000000000000000000000000000..18209f62f75293a46c07ea7b988c348a3b7c518a --- /dev/null +++ b/tests/test_filetree/extract_vars.tree @@ -0,0 +1,4 @@ +parent + opt_layer_test + {p1}_{p2}.nii.gz (fn) + diff --git a/tests/test_filetree/format.tree b/tests/test_filetree/format.tree new file mode 100644 index 0000000000000000000000000000000000000000..3162cde68003e4b8cf44663905624a9c4eb31915 --- /dev/null +++ b/tests/test_filetree/format.tree @@ -0,0 +1,3 @@ +{var:.0f} (int) +{var:.1f} (f1) +{var:.2f} (f2) diff --git a/tests/test_filetree/parent.tree b/tests/test_filetree/parent.tree new file mode 100644 index 0000000000000000000000000000000000000000..2558ed21630812025ec0d9f1d4bdbb0ae9bd42ce --- /dev/null +++ b/tests/test_filetree/parent.tree @@ -0,0 +1,7 @@ +dir1 + ->eddy basename=1 (sub1) + dir2 + ->eddy basename=2 (sub2) + ->eddy basename=1b (sub1b) +->eddy basename=0 (sub0) +->eddy basename=subvar_{subvar} (subvar) diff --git a/tests/test_filetree/parent/opt_file_test.nii.gz b/tests/test_filetree/parent/opt_file_test.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_filetree/parent/opt_layer_test/opt_file_test.nii.gz b/tests/test_filetree/parent/opt_layer_test/opt_file_test.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_filetree/parent/opt_layer_test/opt_file_test2.nii.gz b/tests/test_filetree/parent/opt_layer_test/opt_file_test2.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_filetree/parent/opt_layer_test/sub_file.nii.gz b/tests/test_filetree/parent/opt_layer_test/sub_file.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_filetree/parent/sub_file.nii.gz b/tests/test_filetree/parent/sub_file.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_filetree/test_read.py b/tests/test_filetree/test_read.py new file mode 100644 index 0000000000000000000000000000000000000000..b27984477218cd69f757199948865c2a06e6f8d1 --- /dev/null +++ b/tests/test_filetree/test_read.py @@ -0,0 +1,148 @@ +# Sample Test passing with nose and pytest +from fsl.utils import filetree +from pathlib import PurePath +import os.path as op +import pytest +from glob import glob + + +def same_path(p1, p2): + assert PurePath(p1) == PurePath(p2) + + +def test_simple_tree(): + tree = filetree.FileTree.read('eddy') + assert tree.variables['basename'] == 'eddy_output' + same_path(tree.get('basename'), './eddy_output') + same_path(tree.get('image'), './eddy_output.nii.gz') + + tree = filetree.FileTree.read('eddy.tree', basename='out') + same_path(tree.get('basename'), './out') + same_path(tree.update(basename='test').get('basename'), './test') + same_path(tree.get('basename'), './out') + + with pytest.raises(ValueError): + filetree.FileTree.read('non_existing') + + +def test_complicated_tree(): + tree = filetree.FileTree.read('HCP_directory').update(subject='100307') + + same_path(tree.get('T1w_acpc_dc'), '100307/T1w/T1w_acpc_dc.nii.gz') + + L_white = '100307/T1w/fsaverage_LR32k/100307.L.white.32k_fs_LR.surf.gii' + same_path(tree.update(hemi='L').get('T1w_32k/white'), L_white) + same_path(tree.sub_trees['T1w_32k'].update(hemi='L').get('white'), L_white) + + assert tree.exists(('T1w_32k/white', )) + assert tree.exists('T1w_32k/white') + assert not tree.exists(('T1w_32k/white_misspelled', )) + assert not tree.exists(('T1w_32k/white', 'T1w_32k/white_misspelled', )) + assert not tree.exists(('T1w_32k_err/white', )) + assert not tree.exists(('../test')) + with pytest.raises(ValueError): + assert not tree.exists(('../test'), error=True) + with pytest.raises(ValueError): + tree.exists(('T1w_32k_err/white', ), error=True) + assert tree.exists(('T1w_32k/white', ), error=True) + + +def test_parent_tree(): + directory = op.split(__file__)[0] + tree = filetree.FileTree.read(op.join(directory, 'parent.tree')) + same_path(tree.get('sub0/basename'), '0') + same_path(tree.get('sub1/basename'), 'dir1/1') + same_path(tree.get('sub1b/basename'), 'dir1/1b') + same_path(tree.get('sub2/basename'), 'dir1/dir2/2') + same_path(tree.update(subvar='grot').get('subvar/basename'), 'subvar_grot') + with pytest.raises(KeyError): + tree.get('sub_var/basename') + + +def test_custom_tree(): + directory = op.split(__file__)[0] + tree = filetree.FileTree.read(op.join(directory, 'custom.tree'), directory=directory) + same_path(tree.get('sub_file'), op.join(directory, 'parent/sub_file.nii.gz')) + same_path(tree.update(opt='test').get('sub_file'), op.join(directory, 'parent/opt_layer_test/sub_file.nii.gz')) + + with pytest.raises(KeyError): + tree.get('opt_file') + same_path(tree.update(opt='test').get('opt_file'), op.join(directory, 'parent/opt_layer_test/opt_file_test.nii.gz')) + + assert len(tree.update(opt='test').get_all('sub_file')) == 1 + assert len(tree.update(opt='test').get_all_vars('sub_file')) == 1 + assert len(tree.update(opt='test2').get_all('sub_file')) == 0 + assert len(tree.update(opt='test2').get_all_vars('sub_file')) == 0 + assert len(tree.get_all('sub_file', glob_vars=['opt'])) == 2 + assert len(tree.get_all('sub_file', glob_vars='all')) == 2 + assert len(tree.get_all('sub_file')) == 1 + assert len(tree.update(opt=None).get_all('sub_file')) == 1 + assert len(tree.update(opt=None).get_all('sub_file', glob_vars=['opt'])) == 1 + assert len(tree.update(opt=None).get_all('sub_file', glob_vars='all')) == 1 + + for fn, settings in zip(tree.get_all('sub_file', glob_vars='all'), + tree.get_all_vars('sub_file', glob_vars='all')): + same_path(fn, tree.update(**settings).get('sub_file')) + + assert len(tree.update(opt='test2').get_all('opt_file')) == 0 + assert len(tree.update(opt='test').get_all('opt_file')) == 1 + with pytest.raises(KeyError): + tree.get_all('opt_file') + assert len(tree.get_all('opt_file', glob_vars=['opt'])) == 1 + + for vars in ({'opt': None}, {'opt': 'test'}): + filename = tree.update(**vars).get('sub_file') + assert vars == tree.extract_variables('sub_file', filename) + assert {'opt': None} == tree.extract_variables('sub_file', tree.get('sub_file')) + + assert tree.exists(('sub_file', 'opt_file'), error=True, on_disk=True, glob_vars=['opt']) + assert tree.exists(('sub_file', 'opt_file'), on_disk=True, glob_vars=['opt']) + assert not tree.exists(('sub_file', 'opt_file'), error=False, on_disk=True) + with pytest.raises(KeyError): + assert tree.exists(('sub_file', 'opt_file'), error=True, on_disk=True) + assert not tree.update(opt='test2').exists(('sub_file', 'opt_file'), on_disk=True) + with pytest.raises(IOError): + tree.update(opt='test2').exists(('sub_file', 'opt_file'), on_disk=True, error=True) + + assert tree.template_variables() == {'opt'} + assert tree.template_variables(optional=False) == {'opt'} + assert tree.template_variables(required=False) == set() + assert tree.template_variables(required=False, optional=False) == set() + + assert tree.template_variables('sub_file') == {'opt'} + assert tree.template_variables('sub_file', required=False) == {'opt'} + assert tree.template_variables('sub_file', optional=False) == set() + assert tree.template_variables('sub_file', optional=False, required=False) == set() + + assert tree.template_variables('opt_file') == {'opt'} + assert tree.template_variables('opt_file', required=False) == set() + assert tree.template_variables('opt_file', optional=False) == {'opt'} + assert tree.template_variables('opt_file', optional=False, required=False) == set() + + +def test_format(): + directory = op.split(__file__)[0] + tree = filetree.FileTree.read(op.join(directory, 'format.tree'), var=1.23) + same_path(tree.get('int'), '1') + same_path(tree.get('f1'), '1.2') + same_path(tree.get('f2'), '1.23') + + +def test_read_all(): + for directory in filetree.tree_directories: + if directory != '.': + for filename in glob(op.join(directory, '*.tree')): + filetree.FileTree.read(filename) + + +def test_extract_vars_but(): + """ + Reproduces a bug where the already provided variables are ignored + """ + directory = op.split(__file__)[0] + tree = filetree.FileTree.read(op.join(directory, 'extract_vars.tree')) + fn = 'parent/opt_layer_test/opt_file_test.nii.gz' + assert {'p1': 'opt_file', 'p2': 'test'} == tree.extract_variables('fn', fn) + assert {'p1': 'opt_file', 'p2': 'test'} == tree.update(p1='opt_file').extract_variables('fn', fn) + assert {'p1': 'opt', 'p2': 'file_test'} == tree.update(p1='opt').extract_variables('fn', fn) + assert {'p1': 'opt_{p3}', 'p2': 'test', 'p3': 'file'} == tree.update(p1='opt_{p3}').extract_variables('fn', fn) diff --git a/tests/test_filetree/test_registration.py b/tests/test_filetree/test_registration.py new file mode 100644 index 0000000000000000000000000000000000000000..aa7171ee81d30accb2f55068a387a214557a8965 --- /dev/null +++ b/tests/test_filetree/test_registration.py @@ -0,0 +1,73 @@ +from fsl.utils.filetree import register_tree, FileTree +import os.path as op + + +class SubFileTree(FileTree): + pass + + +def test_register_parent(): + directory = op.split(__file__)[0] + filename = op.join(directory, 'parent.tree') + + # call from sub-type + tree = SubFileTree.read(filename) + assert isinstance(tree, FileTree) + assert isinstance(tree, SubFileTree) + for child in tree.sub_trees.values(): + assert isinstance(child, FileTree) + assert not isinstance(child, SubFileTree) + + # call from FileTree + tree = FileTree.read(filename) + assert isinstance(tree, FileTree) + assert not isinstance(tree, SubFileTree) + for child in tree.sub_trees.values(): + assert isinstance(child, FileTree) + assert not isinstance(child, SubFileTree) + + # register + call from FileTree + register_tree('parent', SubFileTree) + tree = FileTree.read(filename) + assert isinstance(tree, FileTree) + assert isinstance(tree, SubFileTree) + for child in tree.sub_trees.values(): + assert isinstance(child, FileTree) + assert not isinstance(child, SubFileTree) + + # register + call from SubFileTree + register_tree('parent', FileTree) + tree = SubFileTree.read(filename) + assert isinstance(tree, FileTree) + assert not isinstance(tree, SubFileTree) + for child in tree.sub_trees.values(): + assert isinstance(child, FileTree) + assert not isinstance(child, SubFileTree) + + +def test_children(): + directory = op.split(__file__)[0] + filename = op.join(directory, 'parent.tree') + + tree = SubFileTree.read(filename) + assert isinstance(tree, FileTree) + assert not isinstance(tree, SubFileTree) + for child in tree.sub_trees.values(): + assert isinstance(child, FileTree) + assert not isinstance(child, SubFileTree) + + register_tree('eddy', SubFileTree) + tree = SubFileTree.read(filename) + assert isinstance(tree, FileTree) + assert not isinstance(tree, SubFileTree) + for child in tree.sub_trees.values(): + assert isinstance(child, FileTree) + assert isinstance(child, SubFileTree) + + register_tree('eddy', FileTree) + tree = SubFileTree.read(filename) + assert isinstance(tree, FileTree) + assert not isinstance(tree, SubFileTree) + for child in tree.sub_trees.values(): + assert isinstance(child, FileTree) + assert not isinstance(child, SubFileTree) diff --git a/tests/test_filetree/test_template.py b/tests/test_filetree/test_template.py new file mode 100644 index 0000000000000000000000000000000000000000..96ecc18f00adb1add2b191c8d3744460f1c02bc6 --- /dev/null +++ b/tests/test_filetree/test_template.py @@ -0,0 +1,23 @@ +from fsl.utils.filetree import utils +import pytest + + +def test_variables(): + assert ('var', 'other_var', 'var') == tuple(utils.find_variables('some{var}_{other_var}_{var}')) + assert ('var', 'other_var', 'var') == tuple(utils.find_variables('some{var}_[{other_var}_{var}]')) + assert {'other_var'} == utils.optional_variables('some{var}_[{other_var}_{var}]') + + +def test_get_variables(): + assert {'var': 'test'} == utils.extract_variables('{var}', 'test') + assert {'var': 'test'} == utils.extract_variables('2{var}', '2test') + assert {'var': 'test', 'other_var': None} == utils.extract_variables('{var}[_{other_var}]', 'test') + assert {'var': 'test', 'other_var': 'foo'} == utils.extract_variables('{var}[_{other_var}]', 'test_foo') + assert {'var': 'test', 'other_var': 'foo'} == utils.extract_variables('{var}[_{other_var}]_{var}', 'test_foo_test') + assert {'var': 'test', 'other_var': None} == utils.extract_variables('{var}[_{other_var}]_{var}', 'test_test') + with pytest.raises(ValueError): + utils.extract_variables('{var}[_{other_var}]_{var}', 'test_foo') + with pytest.raises(ValueError): + utils.extract_variables('{var}[_{other_var}]_{var}', 'test_foo_bar') + with pytest.raises(ValueError): + utils.extract_variables('bar{var}[_{other_var}]_{var}', 'test')