From 87c7dc1a0e0a16cd84d95069c266bd32e35832a5 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Mon, 25 Jun 2018 14:06:11 +0100 Subject: [PATCH 01/57] ENH: Define Cifti2 Axes describing the rows/columns of the Cifti2 data --- nibabel/cifti2/__init__.py | 1 + nibabel/cifti2/cifti2.py | 40 +- nibabel/cifti2/cifti2_axes.py | 1224 +++++++++++++++++++++++++++++ nibabel/cifti2/tests/test_axes.py | 245 ++++++ nibabel/cifti2/tests/test_io.py | 176 +++++ nibabel/cifti2/tests/test_name.py | 19 + 6 files changed, 1704 insertions(+), 1 deletion(-) create mode 100644 nibabel/cifti2/cifti2_axes.py create mode 100644 nibabel/cifti2/tests/test_axes.py create mode 100644 nibabel/cifti2/tests/test_io.py create mode 100644 nibabel/cifti2/tests/test_name.py diff --git a/nibabel/cifti2/__init__.py b/nibabel/cifti2/__init__.py index 3025a6f991..0c80e4033b 100644 --- a/nibabel/cifti2/__init__.py +++ b/nibabel/cifti2/__init__.py @@ -25,3 +25,4 @@ Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ, Cifti2Vertices, Cifti2Volume, CIFTI_BRAIN_STRUCTURES, Cifti2HeaderError, CIFTI_MODEL_TYPES, load, save) +from .cifti2_axes import (Axis, BrainModel, Parcels, Series, Label, Scalar) \ No newline at end of file diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 67dab1d0c2..30bcbda73e 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1268,6 +1268,40 @@ def get_index_map(self, index): ''' return self.matrix.get_index_map(index) + def get_axis(self, index): + ''' + Generates the Cifti2 axis for a given dimension + + Parameters + ---------- + index : int + Dimension for which we want to obtain the mapping. + + Returns + ------- + axis : cifti2_axes.Axis + ''' + from . import cifti2_axes + return cifti2_axes.from_mapping(self.matrix.get_index_map(index)) + + @classmethod + def from_axes(cls, axes): + ''' + Creates a new Cifti2 header based on the Cifti2 axes + + Parameters + ---------- + axes : Tuple[cifti2_axes.Axis] + sequence of Cifti2 axes describing each row/column of the matrix to be stored + + Returns + ------- + header : Cifti2Header + new header describing the rows/columns in a format consistent with Cifti2 + ''' + from . import cifti2_axes + return cifti2_axes.to_header(axes) + class Cifti2Image(DataobjImage): """ Class for single file CIFTI2 format image @@ -1297,8 +1331,10 @@ def __init__(self, Object containing image data. It should be some object that returns an array from ``np.asanyarray``. It should have a ``shape`` attribute or property. - header : Cifti2Header instance + header : Cifti2Header instance or Sequence[cifti2_axes.Axis] Header with data for / from XML part of CIFTI2 format. + Alternatively a sequence of cifti2_axes.Axis objects can be provided + describing each dimension of the array. nifti_header : None or mapping or NIfTI2 header instance, optional Metadata for NIfTI2 component of this format. extra : None or mapping @@ -1306,6 +1342,8 @@ def __init__(self, file_map : mapping, optional Mapping giving file information for this image format. ''' + if not isinstance(header, Cifti2Header) and header: + header = Cifti2Header.from_axes(header) super(Cifti2Image, self).__init__(dataobj, header=header, extra=extra, file_map=file_map) self._nifti_header = Nifti2Header.from_header(nifti_header) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py new file mode 100644 index 0000000000..3125b58404 --- /dev/null +++ b/nibabel/cifti2/cifti2_axes.py @@ -0,0 +1,1224 @@ +import numpy as np +from nibabel.cifti2 import cifti2 +from six import string_types +from operator import xor + + +def from_mapping(mim): + """ + Parses the MatrixIndicesMap to find the appropriate CIFTI axis describing the rows or columns + + Parameters + ---------- + mim : cifti2.Cifti2MatrixIndicesMap + + Returns + ------- + subtype of Axis + """ + return_type = {'CIFTI_INDEX_TYPE_SCALARS': Scalar, + 'CIFTI_INDEX_TYPE_LABELS': Label, + 'CIFTI_INDEX_TYPE_SERIES': Series, + 'CIFTI_INDEX_TYPE_BRAIN_MODELS': BrainModel, + 'CIFTI_INDEX_TYPE_PARCELS': Parcels} + return return_type[mim.indices_map_to_data_type].from_mapping(mim) + + +def to_header(axes): + """ + Converts the axes describing the rows/columns of a CIFTI vector/matrix to a Cifti2Header + + Parameters + ---------- + axes : iterable[Axis] + one or more axes describing each dimension in turn + + Returns + ------- + cifti2.Cifti2Header + """ + axes = list(axes) + mims_all = [] + matrix = cifti2.Cifti2Matrix() + for dim, ax in enumerate(axes): + if ax in axes[:dim]: + dim_prev = axes.index(ax) + mims_all[dim_prev].applies_to_matrix_dimension.append(dim) + mims_all.append(mims_all[dim_prev]) + else: + mim = ax.to_mapping(dim) + mims_all.append(mim) + matrix.append(mim) + return cifti2.Cifti2Header(matrix) + + +class Axis(object): + """ + Generic object describing the rows or columns of a CIFTI vector/matrix + + Attributes + ---------- + arr : np.ndarray + (N, ) typed array with the actual information on each row/column + """ + _use_dtype = None + arr = None + + def __init__(self, arr): + self.arr = np.asarray(arr, dtype=self._use_dtype) + + def get_element(self, index): + """ + Extracts a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + Description of the row/column + """ + return self.arr[index] + + def __getitem__(self, item): + if isinstance(item, int): + return self.get_element(item) + if isinstance(item, string_types): + raise IndexError("Can not index an Axis with a string (except for Parcels)") + return type(self)(self.arr[item]) + + @property + def size(self, ): + return self.arr.size + + def __len__(self): + return self.size + + def __eq__(self, other): + return (type(self) == type(other) and + len(self) == len(other) and + (self.arr == other.arr).all()) + + def __add__(self, other): + """ + Concatenates two Axes of the same type + + Parameters + ---------- + other : Axis + axis to be appended to the current one + + Returns + ------- + Axis of the same subtype as self and other + """ + if type(self) == type(other): + return type(self)(np.append(self.arr, other.arr)) + return NotImplemented + + +class BrainModel(Axis): + """ + Each row/column in the CIFTI vector/matrix represents a single vertex or voxel + + This Axis describes which vertex/voxel is represented by each row/column. + + Attributes + ---------- + voxel : np.ndarray + (N, 3) array with the voxel indices + vertex : np.ndarray + (N, ) array with the vertex indices + name : np.ndarray + (N, ) array with the brain structure objects + """ + _use_dtype = np.dtype([('vertex', 'i4'), ('voxel', ('i4', 3)), + ('name', 'U%i' % max(len(name) for name in cifti2.CIFTI_BRAIN_STRUCTURES))]) + _affine = None + _volume_shape = None + + def __init__(self, arr, affine=None, volume_shape=None, nvertices=None): + """ + Creates a new BrainModel axis defining the vertices and voxels represented by each row/column + + Parameters + ---------- + arr : np.ndarray + (N, ) structured array with for every element a tuple with 3 elements: + - vertex index (-1 for voxels) + - 3 voxel indices (-1 for vertices) + - string (name of brain structure) + affine : np.ndarray + (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only covering the surface) + volume_shape : Tuple[int, int, int] + shape of the volume in which the voxels were defined (not needed for CIFTI files only covering the surface) + nvertices : dict[String -> int] + maps names of surface elements to integers + """ + super(BrainModel, self).__init__(arr) + self.name = self.name # correct names to CIFTI brain structures + if nvertices is None: + self.nvertices = {} + else: + self.nvertices = dict(nvertices) + for name in list(self.nvertices.keys()): + if name not in self.name: + del self.nvertices[name] + if self.is_surface.all(): + self.affine = None + self.volume_shape = None + else: + self.affine = affine + self.volume_shape = volume_shape + + @classmethod + def from_mapping(cls, mim): + """ + Creates a new BrainModel axis based on a CIFTI dataset + + Parameters + ---------- + mim : cifti2.Cifti2MatrixIndicesMap + + Returns + ------- + BrainModel + """ + nbm = np.sum([bm.index_count for bm in mim.brain_models]) + arr = np.zeros(nbm, dtype=cls._use_dtype) + arr['voxel'] = -1 + arr['vertex'] = -1 + nvertices = {} + affine, shape = None, None + for bm in mim.brain_models: + index_end = bm.index_offset + bm.index_count + is_surface = bm.model_type == 'CIFTI_MODEL_TYPE_SURFACE' + arr['name'][bm.index_offset: index_end] = bm.brain_structure + if is_surface: + arr['vertex'][bm.index_offset: index_end] = bm.vertex_indices + nvertices[bm.brain_structure] = bm.surface_number_of_vertices + else: + arr['voxel'][bm.index_offset: index_end, :] = bm.voxel_indices_ijk + if affine is None: + shape = mim.volume.volume_dimensions + affine = mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix + else: + if shape != mim.volume.volume_dimensions: + raise ValueError("All volume masks should be defined in the same volume") + if (affine != mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix).any(): + raise ValueError("All volume masks should have the same affine") + return cls(arr, affine, shape, nvertices) + + @classmethod + def from_mask(cls, mask, name='other', affine=None): + """ + Creates a new BrainModel axis describing the provided mask + + Parameters + ---------- + mask : np.ndarray + all non-zero voxels will be included in the BrainModel axis + should be (Nx, Ny, Nz) array for volume mask or (Nvertex, ) array for surface mask + name : str + Name of the brain structure (e.g. 'CortexRight', 'thalamus_left' or 'brain_stem') + affine : np.ndarray + (4, 4) array with the voxel to mm transformation (defaults to identity matrix) + Argument will be ignored for surface masks + + Returns + ------- + BrainModel which covers the provided mask + """ + if affine is None: + affine = np.eye(4) + if np.asarray(affine).shape != (4, 4): + raise ValueError("Affine transformation should be a 4x4 array or None, not %r" % affine) + if mask.ndim == 1: + return cls.from_surface(np.where(mask != 0)[0], mask.size, name=name) + elif mask.ndim == 3: + voxels = np.array(np.where(mask != 0)).T + arr = np.zeros(len(voxels), dtype=cls._use_dtype) + arr['vertex'] = -1 + arr['voxel'] = voxels + arr['name'] = cls.to_cifti_brain_structure_name(name) + return cls(arr, affine=affine, volume_shape=mask.shape) + else: + raise ValueError("Mask should be either 1-dimensional (for surfaces) or " + "3-dimensional (for volumes), not %i-dimensional" % mask.ndim) + + @classmethod + def from_surface(cls, vertices, nvertex, name='Cortex'): + """ + Creates a new BrainModel axis describing the vertices on a surface + + Parameters + ---------- + vertices : np.ndarray + indices of the vertices on the surface + nvertex : int + total number of vertices on the surface + name : str + Name of the brain structure (e.g. 'CortexLeft' or 'CortexRight') + + Returns + ------- + BrainModel which covers (part of) the surface + """ + arr = np.zeros(len(vertices), dtype=cls._use_dtype) + arr['voxel'] = -1 + arr['vertex'] = vertices + arr['name'] = cls.to_cifti_brain_structure_name(name) + return cls(arr, nvertices={arr['name'][0]: nvertex}) + + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 3 elements + - boolean, which is True if it is a surface element + - vertex index if it is a surface element, otherwise array with 3 voxel indices + - structure.BrainStructure object describing the brain structure the element was taken from + """ + elem = self.arr[index] + is_surface = elem['name'] in self.nvertices.keys() + name = 'vertex' if is_surface else 'voxel' + return is_surface, elem[name], elem['name'] + + def to_mapping(self, dim): + """ + Converts the brain model axis to a MatrixIndicesMap for storage in CIFTI format + + Parameters + ---------- + dim : int + which dimension of the CIFTI vector/matrix is described by this dataset (zero-based) + + Returns + ------- + cifti2.Cifti2MatrixIndicesMap + """ + mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_BRAIN_MODELS') + for name, to_slice, bm in self.iter_structures(): + is_surface = name in self.nvertices.keys() + if is_surface: + voxels = None + vertices = cifti2.Cifti2VertexIndices(bm.vertex) + nvertex = self.nvertices[name] + else: + voxels = cifti2.Cifti2VoxelIndicesIJK(bm.voxel) + vertices = None + nvertex = None + if mim.volume is None: + affine = cifti2.Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ(-3, matrix=self.affine) + mim.volume = cifti2.Cifti2Volume(self.volume_shape, affine) + cifti_bm = cifti2.Cifti2BrainModel(to_slice.start, len(bm), + 'CIFTI_MODEL_TYPE_SURFACE' if is_surface else 'CIFTI_MODEL_TYPE_VOXELS', + name, nvertex, voxels, vertices) + mim.append(cifti_bm) + return mim + + def iter_structures(self, ): + """ + Iterates over all brain structures in the order that they appear along the axis + + Yields + ------ + tuple with + - CIFTI brain structure name + - slice to select the data associated with the brain structure from the tensor + - brain model covering that specific brain structure + """ + idx_start = 0 + start_name = self.name[idx_start] + for idx_current, name in enumerate(self.name): + if start_name != name: + yield start_name, slice(idx_start, idx_current), self[idx_start: idx_current] + idx_start = idx_current + start_name = self.name[idx_start] + yield start_name, slice(idx_start, None), self[idx_start:] + + @property + def affine(self, ): + return self._affine + + @affine.setter + def affine(self, value): + if value is not None: + value = np.asarray(value) + if value.shape != (4, 4): + raise ValueError('Affine transformation should be a 4x4 array') + self._affine = value + + @property + def volume_shape(self, ): + return self._volume_shape + + @volume_shape.setter + def volume_shape(self, value): + if value is not None: + value = tuple(value) + if len(value) != 3: + raise ValueError("Volume shape should be a tuple of length 3") + self._volume_shape = value + + @property + def is_surface(self, ): + """True for any element on the surface + """ + return np.vectorize(lambda name: name in self.nvertices.keys())(self.name) + + @property + def voxel(self, ): + """The voxel represented by each row or column + """ + return self.arr['voxel'] + + @voxel.setter + def voxel(self, values): + self.arr['voxel'] = values + + @property + def vertex(self, ): + """The vertex represented by each row or column + """ + return self.arr['vertex'] + + @vertex.setter + def vertex(self, values): + self.arr['vertex'] = values + + @property + def name(self, ): + """The brain structure to which the voxel/vertices of belong + """ + return self.arr['name'] + + @name.setter + def name(self, values): + self.arr['name'] = [self.to_cifti_brain_structure_name(name) for name in values] + + @staticmethod + def to_cifti_brain_structure_name(name): + """ + Attempts to convert the name of an anatomical region in a format recognized by CIFTI + + This function returns: + * the name if it is in the CIFTI format already + * if the name is a tuple the first element is assumed to be the structure name while + the second is assumed to be the hemisphere (left, right or both). The latter will default + to both. + * names like left_cortex, cortex_left, LeftCortex, or CortexLeft will be converted to + CIFTI_STRUCTURE_CORTEX_LEFT + + see ``nibabel.cifti2.tests.test_name`` for examples of which conversions are possible + + Parameters + ---------- + name: (str, tuple) + input name of an anatomical region + + Returns + ------- + CIFTI2 compatible name + + Raises + ------ + ValueError: raised if the input name does not match a known anatomical structure in CIFTI + """ + if name in cifti2.CIFTI_BRAIN_STRUCTURES: + return name + if not isinstance(name, string_types): + if len(name) == 1: + structure = name[0] + orientation = 'both' + else: + structure, orientation = name + if structure.lower() in ('left', 'right', 'both'): + orientation, structure = name + else: + orient_names = ('left', 'right', 'both') + for poss_orient in orient_names: + idx = len(poss_orient) + if poss_orient == name.lower()[:idx]: + orientation = poss_orient + if name[idx] in '_ ': + structure = name[idx + 1:] + else: + structure = name[idx:] + break + if poss_orient == name.lower()[-idx:]: + orientation = poss_orient + if name[-idx - 1] in '_ ': + structure = name[:-idx - 1] + else: + structure = name[:-idx] + break + else: + orientation = 'both' + structure = name + if orientation.lower() == 'both': + proposed_name = 'CIFTI_STRUCTURE_%s' % structure.upper() + else: + proposed_name = 'CIFTI_STRUCTURE_%s_%s' % (structure.upper(), orientation.upper()) + if proposed_name not in cifti2.CIFTI_BRAIN_STRUCTURES: + raise ValueError('%s was interpreted as %s, which is not a valid CIFTI brain structure' % + (name, proposed_name)) + return proposed_name + + def __getitem__(self, item): + if isinstance(item, int): + return self.get_element(item) + if isinstance(item, string_types): + raise IndexError("Can not index an Axis with a string (except for Parcels)") + return type(self)(self.arr[item], self.affine, self.volume_shape, self.nvertices) + + def __eq__(self, other): + if type(self) != type(other) or len(self) != len(other): + return False + if xor(self.affine is None, other.affine is None): + return False + return (((self.affine is None and other.affine is None) or + (abs(self.affine - other.affine).max() < 1e-8 and + self.volume_shape == other.volume_shape)) and + (self.nvertices == other.nvertices) and + (self.arr == other.arr).all()) + + def __add__(self, other): + """ + Concatenates two BrainModels + + Parameters + ---------- + other : BrainModel + brain model to be appended to the current one + + Returns + ------- + BrainModel + """ + if type(self) == type(other): + if self.affine is None: + affine, shape = other.affine, other.volume_shape + else: + affine, shape = self.affine, self.volume_shape + if other.affine is not None and ((other.affine != affine).all() or + other.volume_shape != shape): + raise ValueError("Trying to concatenate two BrainModels defined in a different brain volume") + nvertices = dict(self.nvertices) + for name, value in other.nvertices.items(): + if name in nvertices.keys() and nvertices[name] != value: + raise ValueError("Trying to concatenate two BrainModels with inconsistent number of vertices for %s" + % name) + nvertices[name] = value + return type(self)(np.append(self.arr, other.arr), affine, shape, nvertices) + return NotImplemented + + +class Parcels(Axis): + """ + Each row/column in the CIFTI vector/matrix represents a parcel of voxels/vertices + + This Axis describes which parcel is represented by each row/column. + + Attributes + ---------- + name : np.ndarray + (N, ) string array with the parcel names + parcel : np.ndarray + (N, ) array with the actual get_parcels (each of which is a BrainModel object) + + Individual get_parcels can also be accessed based on their name, using + >>> parcel = parcel_axis[name] + """ + _use_dtype = np.dtype([('name', 'U60'), ('voxels', 'object'), ('vertices', 'object')]) + _affine = None + _volume_shape = None + + def __init__(self, arr, affine=None, volume_shape=None, nvertices=None): + """ + Creates a new BrainModel axis defining the vertices and voxels represented by each row/column + + Parameters + ---------- + arr : np.ndarray + (N, ) structured array with for every element a tuple with 3 elements: + - string (name of parcel) + - (M, 3) int array with the M voxel indices in the parcel + - Dict[String -> (K, ) int array] mapping surface brain structure names to vertex indices + affine : np.ndarray + (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only covering the surface) + volume_shape : Tuple[int, int, int] + shape of the volume in which the voxels were defined (not needed for CIFTI files only covering the surface) + nvertices : dict[String -> int] + maps names of surface elements to integers + """ + super(Parcels, self).__init__(arr) + self.affine = affine + self.volume_shape = volume_shape + if nvertices is None: + self.nvertices = {} + else: + self.nvertices = dict(nvertices) + + @classmethod + def from_brain_models(cls, named_brain_models): + """ + Creates a Parcel axis from a list of BrainModel axes with names + + Parameters + ---------- + named_brain_models : List[Tuple[String, BrainModel]] + list of (parcel name, brain model representation) pairs defining each parcel + + Returns + ------- + Parcels + """ + affine = None + volume_shape = None + arr = np.zeros(len(named_brain_models), dtype=cls._use_dtype) + nvertices = {} + for idx_parcel, (parcel_name, bm) in enumerate(named_brain_models): + voxels = bm.voxel[~bm.is_surface] + if voxels.shape[0] != 0: + if affine is None: + affine = bm.affine + volume_shape = bm.volume_shape + else: + if (affine != bm.affine).any() or (volume_shape != bm.volume_shape): + raise ValueError( + "Can not combine brain models defined in different volumes into a single Parcel axis") + vertices = {} + for name, _, bm_part in bm.iter_structures(): + if name in bm.nvertices.keys(): + if name in nvertices.keys() and nvertices[name] != bm.nvertices[name]: + raise ValueError("Got multiple conflicting number of vertices for surface structure %s" % name) + nvertices[name] = bm.nvertices[name] + vertices[name] = bm_part.vertex + arr[idx_parcel] = (parcel_name, voxels, vertices) + return Parcels(arr, affine, volume_shape, nvertices) + + @classmethod + def from_mapping(cls, mim): + """ + Creates a new Parcels axis based on a CIFTI dataset + + Parameters + ---------- + mim : cifti2.Cifti2MatrixIndicesMap + + Returns + ------- + Parcels + """ + nparcels = len(list(mim.parcels)) + arr = np.zeros(nparcels, dtype=cls._use_dtype) + volume_shape = None if mim.volume is None else mim.volume.volume_dimensions + affine = None if mim.volume is None else mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix + nvertices = {} + for surface in mim.surfaces: + nvertices[surface.brain_structure] = surface.surface_number_of_vertices + for idx_parcel, parcel in enumerate(mim.parcels): + nvoxels = 0 if parcel.voxel_indices_ijk is None else len(parcel.voxel_indices_ijk) + voxels = np.zeros((nvoxels, 3), dtype='i4') + if nvoxels != 0: + voxels[()] = parcel.voxel_indices_ijk + vertices = {} + for vertex in parcel.vertices: + name = vertex.brain_structure + vertices[vertex.brain_structure] = np.array(vertex) + if name not in nvertices.keys(): + raise ValueError("Number of vertices for surface structure %s not defined" % name) + arr[idx_parcel]['voxels'] = voxels + arr[idx_parcel]['vertices'] = vertices + arr[idx_parcel]['name'] = parcel.name + return cls(arr, affine, volume_shape, nvertices) + + def to_mapping(self, dim): + """ + Converts the get_parcels to a MatrixIndicesMap for storage in CIFTI format + + Parameters + ---------- + dim : int + which dimension of the CIFTI vector/matrix is described by this dataset (zero-based) + + Returns + ------- + cifti2.Cifti2MatrixIndicesMap + """ + mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_PARCELS') + if self.affine is not None: + affine = cifti2.Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ(-3, matrix=self.affine) + mim.volume = cifti2.Cifti2Volume(self.volume_shape, affine) + for name, nvertex in self.nvertices.items(): + mim.append(cifti2.Cifti2Surface(name, nvertex)) + for name, voxels, vertices in self.arr: + cifti_voxels = cifti2.Cifti2VoxelIndicesIJK(voxels) + element = cifti2.Cifti2Parcel(name, cifti_voxels) + for name, idx_vertices in vertices.items(): + element.vertices.append(cifti2.Cifti2Vertices(name, idx_vertices)) + mim.append(element) + return mim + + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 3 elements + - unicode name of the parcel + - (M, 3) int array with voxel indices + - Dict[String -> (K, ) int array] with vertex indices for a specific surface brain structure + """ + return self.name[index], self.voxels[index], self.vertices[index] + + @property + def affine(self, ): + return self._affine + + @affine.setter + def affine(self, value): + if value is not None: + value = np.asarray(value) + if value.shape != (4, 4): + raise ValueError('Affine transformation should be a 4x4 array') + self._affine = value + + @property + def volume_shape(self, ): + return self._volume_shape + + @volume_shape.setter + def volume_shape(self, value): + if value is not None: + value = tuple(value) + if len(value) != 3: + raise ValueError("Volume shape should be a tuple of length 3") + self._volume_shape = value + + @property + def name(self, ): + return self.arr['name'] + + @name.setter + def name(self, values): + self.arr['name'] = values + + @property + def voxels(self, ): + return self.arr['voxels'] + + @voxels.setter + def voxels(self, values): + self.arr['voxels'] = values + + @property + def vertices(self, ): + return self.arr['vertices'] + + @vertices.setter + def vertices(self, values): + self.arr['vertices'] = values + + def __getitem__(self, item): + if isinstance(item, string_types): + idx = np.where(self.name == item)[0] + if len(idx) == 0: + raise IndexError("Parcel %s not found" % item) + if len(idx) > 1: + raise IndexError("Multiple get_parcels with name %s found" % item) + return self.voxels[idx[0]], self.vertices[idx[0]] + if isinstance(item, int): + return self.get_element(item) + if isinstance(item, string_types): + raise IndexError("Can not index an Axis with a string (except for Parcels)") + return type(self)(self.arr[item], self.affine, self.volume_shape, self.nvertices) + + def __eq__(self, other): + if (type(self) != type(other) or len(self) != len(other) or + (self.name != other.name).all() or self.nvertices != other.nvertices or + any((vox1 != vox2).any() for vox1, vox2 in zip(self.voxels, other.voxels))): + return False + if self.affine is not None: + if ( other.affine is None or + abs(self.affine - other.affine).max() > 1e-8 or + self.volume_shape != other.volume_shape): + return False + elif other.affine is not None: + return False + for vert1, vert2 in zip(self.vertices, other.vertices): + if len(vert1) != len(vert2): + return False + for name in vert1.keys(): + if name not in vert2 or (vert1[name] != vert2[name]).all(): + return False + return True + + def __add__(self, other): + """ + Concatenates two Parcels + + Parameters + ---------- + other : Parcel + parcel to be appended to the current one + + Returns + ------- + Parcel + """ + if type(self) == type(other): + if self.affine is None: + affine, shape = other.affine, other.volume_shape + else: + affine, shape = self.affine, self.volume_shape + if other.affine is not None and ((other.affine != affine).all() or + other.volume_shape != shape): + raise ValueError("Trying to concatenate two Parcels defined in a different brain volume") + nvertices = dict(self.nvertices) + for name, value in other.nvertices.items(): + if name in nvertices.keys() and nvertices[name] != value: + raise ValueError("Trying to concatenate two Parcels with inconsistent number of vertices for %s" + % name) + nvertices[name] = value + return type(self)(np.append(self.arr, other.arr), affine, shape, nvertices) + return NotImplemented + + +class Scalar(Axis): + """ + Along this axis of the CIFTI vector/matrix each row/column has been given a unique name and optionally metadata + + Attributes + ---------- + name : np.ndarray + (N, ) string array with the parcel names + meta : np.ndarray + (N, ) array with a dictionary of metadata for each row/column + """ + _use_dtype = np.dtype([('name', 'U60'), ('meta', 'object')]) + + def __init__(self, arr): + """ + Creates a new Scalar axis from (name, meta-data) pairs + + Parameters + ---------- + arr : Iterable[Tuple[str, dict[str -> str]] + iterates over all rows/columns assigning a name and a dictionary of metadata to each + """ + super(Scalar, self).__init__(arr) + + @classmethod + def from_mapping(cls, mim): + """ + Creates a new get_scalar axis based on a CIFTI dataset + + Parameters + ---------- + mim : cifti2.Cifti2MatrixIndicesMap + + Returns + ------- + Scalar + """ + res = np.zeros(len(list(mim.named_maps)), dtype=cls._use_dtype) + res['name'] = [nm.map_name for nm in mim.named_maps] + res['meta'] = [{} if nm.metadata is None else dict(nm.metadata) for nm in mim.named_maps] + return cls(res) + + @classmethod + def from_names(cls, names): + """ + Creates a new get_scalar axis with the given row/column names + + Parameters + ---------- + names : List[str] + gives a unique name to every row/column in the matrix + + Returns + ------- + Scalar + """ + res = np.zeros(len(names), dtype=cls._use_dtype) + res['name'] = names + res['meta'] = [{} for _ in names] + return cls(res) + + def to_mapping(self, dim): + """ + Converts the hcp_labels to a MatrixIndicesMap for storage in CIFTI format + + Parameters + ---------- + dim : int + which dimension of the CIFTI vector/matrix is described by this dataset (zero-based) + + Returns + ------- + cifti2.Cifti2MatrixIndicesMap + """ + mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_SCALARS') + for elem in self.arr: + meta = None if len(elem['meta']) == 0 else elem['meta'] + named_map = cifti2.Cifti2NamedMap(elem['name'], cifti2.Cifti2MetaData(meta)) + mim.append(named_map) + return mim + + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 2 elements + - unicode name of the get_scalar + - dictionary with the element metadata + """ + return self.arr['name'][index], self.arr['meta'][index] + + def to_label(self, labels): + """ + Creates a new Label axis based on the Scalar axis + + Parameters + ---------- + labels : list[dict] + mapping from integers to (name, (R, G, B, A)), where `name` is a string and R, G, B, and A are floats + between 0 and 1 giving the colour and alpha (transparency) + + Returns + ------- + Label + """ + res = np.zeros(self.size, dtype=Label._use_dtype) + res['name'] = self.arr['name'] + res['meta'] = self.arr['meta'] + res['get_label'] = labels + return Label(res) + + @property + def name(self, ): + return self.arr['name'] + + @name.setter + def name(self, values): + self.arr['name'] = values + + @property + def meta(self, ): + return self.arr['meta'] + + @meta.setter + def meta(self, values): + self.arr['meta'] = values + + +class Label(Axis): + """ + Along this axis of the CIFTI vector/matrix each row/column has been given a unique name, + get_label table, and optionally metadata + + Attributes + ---------- + name : np.ndarray + (N, ) string array with the parcel names + meta : np.ndarray + (N, ) array with a dictionary of metadata for each row/column + get_label : sp.ndarray + (N, ) array with dictionaries mapping integer values to get_label names and RGBA colors + """ + _use_dtype = np.dtype([('name', 'U60'), ('get_label', 'object'), ('meta', 'object')]) + + def __init__(self, arr): + """ + Creates a new Scalar axis from (name, meta-data) pairs + + Parameters + ---------- + arr : Iterable[Tuple[str, dict[int -> (str, (float, float, float, float)), dict(str->str)]] + iterates over all rows/columns assigning a name, dictionary mapping integers to get_label names and rgba colours + and a dictionary of metadata to each + """ + super(Label, self).__init__(arr) + + @classmethod + def from_mapping(cls, mim): + """ + Creates a new get_scalar axis based on a CIFTI dataset + + Parameters + ---------- + mim : cifti2.Cifti2MatrixIndicesMap + + Returns + ------- + Scalar + """ + tables = [{key: (value.label, value.rgba) for key, value in nm.label_table.items()} + for nm in mim.named_maps] + return Scalar.from_mapping(mim).to_label(tables) + + def to_mapping(self, dim): + """ + Converts the hcp_labels to a MatrixIndicesMap for storage in CIFTI format + + Parameters + ---------- + dim : int + which dimension of the CIFTI vector/matrix is described by this dataset (zero-based) + + Returns + ------- + cifti2.Cifti2MatrixIndicesMap + """ + mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_LABELS') + for elem in self.arr: + label_table = cifti2.Cifti2LabelTable() + for key, value in elem['get_label'].items(): + label_table[key] = (value[0],) + tuple(value[1]) + meta = None if len(elem['meta']) == 0 else elem['meta'] + named_map = cifti2.Cifti2NamedMap(elem['name'], cifti2.Cifti2MetaData(meta), + label_table) + mim.append(named_map) + return mim + + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 2 elements + - unicode name of the get_scalar + - dictionary with the get_label table + - dictionary with the element metadata + """ + return self.arr['name'][index], self.arr['get_label'][index], self.arr['meta'][index] + + @property + def name(self, ): + return self.arr['name'] + + @name.setter + def name(self, values): + self.arr['name'] = values + + @property + def meta(self, ): + return self.arr['meta'] + + @meta.setter + def meta(self, values): + self.arr['meta'] = values + + @property + def label(self, ): + return self.arr['get_label'] + + @label.setter + def label(self, values): + self.arr['get_label'] = values + + +class Series(Axis): + """ + Along this axis of the CIFTI vector/matrix the rows/columns increase monotonously in time + + This Axis describes the time point of each row/column. + + Attributes + ---------- + start : float + starting time point + step : float + sampling time (TR) + size : int + number of time points + """ + size = None + _unit = None + + def __init__(self, start, step, size, unit="SECOND"): + """ + Creates a new Series axis + + Parameters + ---------- + start : float + Time of the first datapoint + step : float + Step size between data points + size : int + Number of data points + unit : str + Unit of the step size (one of 'second', 'hertz', 'meter', or 'radian') + """ + self.unit = unit + self.start = start + self.step = step + self.size = size + + @property + def unit(self, ): + return self._unit + + @unit.setter + def unit(self, value): + if value.upper() not in ("SECOND", "HERTZ", "METER", "RADIAN"): + raise ValueError("Series unit should be one of ('second', 'hertz', 'meter', or 'radian'") + self._unit = value.upper() + + @property + def arr(self, ): + return np.arange(self.size) * self.step + self.start + + @classmethod + def from_mapping(cls, mim): + """ + Creates a new Series axis based on a CIFTI dataset + + Parameters + ---------- + mim : cifti2.Cifti2MatrixIndicesMap + + Returns + ------- + Series + """ + start = mim.series_start * 10 ** mim.series_exponent + step = mim.series_step * 10 ** mim.series_exponent + return cls(start, step, mim.number_of_series_points, mim.series_unit) + + def to_mapping(self, dim): + """ + Converts the get_series to a MatrixIndicesMap for storage in CIFTI format + + Parameters + ---------- + dim : int + which dimension of the CIFTI vector/matrix is described by this dataset (zero-based) + + Returns + ------- + cifti2.Cifti2MatrixIndicesMap + """ + mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_SERIES') + mim.series_exponent = 0 + mim.series_start = self.start + mim.series_step = self.step + mim.number_of_series_points = self.size + mim.series_unit = self.unit + return mim + + def extend(self, other_axis): + """ + Concatenates two get_series + + Note: this will ignore the start point of the other axis + + Parameters + ---------- + other_axis : Series + other axis + + Returns + ------- + Series + """ + if other_axis.step != self.step: + raise ValueError('Can only concatenate get_series with the same step size') + if other_axis.unit != self.unit: + raise ValueError('Can only concatenate get_series with the same unit') + return Series(self.start, self.step, self.size + other_axis.size, self.unit) + + def __getitem__(self, item): + if isinstance(item, slice): + step = 1 if item.step is None else item.step + idx_start = ((self.size - 1 if step < 0 else 0) + if item.start is None else + (item.start if item.start >= 0 else self.size + item.start)) + idx_end = ((-1 if step < 0 else self.size) + if item.stop is None else + (item.stop if item.stop >= 0 else self.size + item.stop)) + if idx_start > self.size: + idx_start = self.size - 1 + if idx_end > self.size: + idx_end = self.size + nelements = (idx_end - idx_start) // step + if nelements < 0: + nelements = 0 + return Series(idx_start * self.step + self.start, self.step * step, nelements) + elif isinstance(item, int): + return self.get_element(item) + raise IndexError('Series can only be indexed with integers or slices without breaking the regular structure') + + def get_element(self, index): + """ + Gives the time point of a specific row/column + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + float + """ + if index < 0: + index = self.size + index + if index >= self.size: + raise IndexError("index %i is out of range for get_series with size %i" % (index, self.size)) + return self.start + self.step * index + + def __add__(self, other): + """ + Concatenates two Series + + Parameters + ---------- + other : Series + Time get_series to append at the end of the current time get_series. + Note that the starting time of the other time get_series is ignored. + + Returns + ------- + Series + New time get_series with the concatenation of the two + + Raises + ------ + ValueError + raised if the repetition time of the two time get_series is different + """ + if isinstance(other, Series): + return self.extend(other) + return NotImplemented diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py new file mode 100644 index 0000000000..64e94f2663 --- /dev/null +++ b/nibabel/cifti2/tests/test_axes.py @@ -0,0 +1,245 @@ +import numpy as np +from nose.tools import assert_raises +from .test_io import check_rewrite +import nibabel.cifti2.cifti2_axes as axes + + +rand_affine = np.random.randn(4, 4) +vol_shape = (5, 10, 3) + + +def get_brain_models(): + """ + Generates a set of practice BrainModel axes + + Yields + ------ + BrainModel axis + """ + mask = np.zeros(vol_shape) + mask[0, 1, 2] = 1 + mask[0, 4, 2] = True + mask[0, 4, 0] = True + yield axes.BrainModel.from_mask(mask, 'ThalamusRight', rand_affine) + mask[0, 0, 0] = True + yield axes.BrainModel.from_mask(mask, affine=rand_affine) + + yield axes.BrainModel.from_surface([0, 5, 10], 15, 'CortexLeft') + yield axes.BrainModel.from_surface([0, 5, 10, 13], 15) + + surface_mask = np.zeros(15, dtype='bool') + surface_mask[[2, 9, 14]] = True + yield axes.BrainModel.from_mask(surface_mask, name='CortexRight') + + +def get_parcels(): + """ + Generates a practice Parcel axis out of all practice brain models + + Returns + ------- + Parcel axis + """ + bml = list(get_brain_models()) + return axes.Parcels.from_brain_models([('mixed', bml[0] + bml[2]), ('volume', bml[1]), ('surface', bml[3])]) + + +def get_scalar(): + """ + Generates a practice Scalar axis with names ('one', 'two', 'three') + + Returns + ------- + Scalar axis + """ + return axes.Scalar.from_names(['one', 'two', 'three']) + + +def get_label(): + """ + Generates a practice Label axis with names ('one', 'two', 'three') and two labels + + Returns + ------- + Label axis + """ + return axes.Scalar.from_names(['one', 'two', 'three']).to_label({0: ('something', (0.2, 0.4, 0.1, 0.5)), + 1: ('even better', (0.3, 0.8, 0.43, 0.9))}) + +def get_series(): + """ + Generates a set of 4 practice Series axes with different starting times/lengths/time steps and units + + Yields + ------ + Series axis + """ + yield axes.Series(3, 10, 4) + yield axes.Series(8, 10, 3) + yield axes.Series(3, 2, 4) + yield axes.Series(5, 10, 5, "HERTZ") + + +def get_axes(): + """ + Iterates through all of the practice axes defined in the functions above + + Yields + ------ + Cifti2 axis + """ + yield get_parcels() + yield get_scalar() + yield get_label() + for elem in get_brain_models(): + yield elem + for elem in get_series(): + yield elem + + +def test_brain_models(): + """ + Tests the introspection and creation of CIFTI2 BrainModel axes + """ + bml = list(get_brain_models()) + assert len(bml[0]) == 3 + assert (bml[0].vertex == -1).all() + assert (bml[0].voxel == [[0, 1, 2], [0, 4, 0], [0, 4, 2]]).all() + assert bml[0][1][0] == False + assert (bml[0][1][1] == [0, 4, 0]).all() + assert bml[0][1][2] == axes.BrainModel.to_cifti_brain_structure_name('thalamus_right') + assert len(bml[1]) == 4 + assert (bml[1].vertex == -1).all() + assert (bml[1].voxel == [[0, 0, 0], [0, 1, 2], [0, 4, 0], [0, 4, 2]]).all() + assert len(bml[2]) == 3 + assert (bml[2].voxel == -1).all() + assert (bml[2].vertex == [0, 5, 10]).all() + assert bml[2][1] == (True, 5, 'CIFTI_STRUCTURE_CORTEX_LEFT') + assert len(bml[3]) == 4 + assert (bml[3].voxel == -1).all() + assert (bml[3].vertex == [0, 5, 10, 13]).all() + assert bml[4][1] == (True, 9, 'CIFTI_STRUCTURE_CORTEX_RIGHT') + assert len(bml[4]) == 3 + assert (bml[4].voxel == -1).all() + assert (bml[4].vertex == [2, 9, 14]).all() + + for bm, label in zip(bml, ['ThalamusRight', 'Other', 'cortex_left', 'cortex']): + structures = list(bm.iter_structures()) + assert len(structures) == 1 + name = structures[0][0] + assert name == axes.BrainModel.to_cifti_brain_structure_name(label) + if 'CORTEX' in name: + assert bm.nvertices[name] == 15 + else: + assert name not in bm.nvertices + assert (bm.affine == rand_affine).all() + assert bm.volume_shape == vol_shape + + bmt = bml[0] + bml[1] + bml[2] + bml[3] + assert len(bmt) == 14 + structures = list(bmt.iter_structures()) + assert len(structures) == 4 + for bm, (name, _, bm_split) in zip(bml, structures): + assert bm == bm_split + assert (bm_split.name == name).all() + assert bm == bmt[bmt.name == bm.name[0]] + assert bm == bmt[np.where(bmt.name == bm.name[0])] + + bmt = bmt + bml[3] + assert len(bmt) == 18 + structures = list(bmt.iter_structures()) + assert len(structures) == 4 + assert len(structures[-1][2]) == 8 + + +def test_parcels(): + """ + Test the introspection and creation of CIFTI2 Parcel axes + """ + prc = get_parcels() + assert isinstance(prc, axes.Parcels) + assert prc['mixed'][0].shape == (3, 3) + assert len(prc['mixed'][1]) == 1 + assert prc['mixed'][1]['CIFTI_STRUCTURE_CORTEX_LEFT'].shape == (3, ) + + assert prc['volume'][0].shape == (4, 3) + assert len(prc['volume'][1]) == 0 + + assert prc['surface'][0].shape == (0, 3) + assert len(prc['surface'][1]) == 1 + assert prc['surface'][1]['CIFTI_STRUCTURE_CORTEX'].shape == (4, ) + + prc2 = prc + prc + assert len(prc2) == 6 + assert (prc2.affine == prc.affine).all() + assert (prc2.nvertices == prc.nvertices) + assert (prc2.volume_shape == prc.volume_shape) + assert prc2[:3] == prc + assert prc2[3:] == prc + + assert prc2[3:]['mixed'][0].shape == (3, 3) + assert len(prc2[3:]['mixed'][1]) == 1 + assert prc2[3:]['mixed'][1]['CIFTI_STRUCTURE_CORTEX_LEFT'].shape == (3, ) + + +def test_scalar(): + """ + Test the introspection and creation of CIFTI2 Scalar axes + """ + sc = get_scalar() + assert len(sc) == 3 + assert isinstance(sc, axes.Scalar) + assert (sc.name == ['one', 'two', 'three']).all() + assert sc[1] == ('two', {}) + sc2 = sc + sc + assert len(sc2) == 6 + assert (sc2.name == ['one', 'two', 'three', 'one', 'two', 'three']).all() + assert sc2[:3] == sc + assert sc2[3:] == sc + + +def test_series(): + """ + Test the introspection and creation of CIFTI2 Series axes + """ + sr = list(get_series()) + assert sr[0].unit == 'SECOND' + assert sr[1].unit == 'SECOND' + assert sr[2].unit == 'SECOND' + assert sr[3].unit == 'HERTZ' + + assert (sr[0].arr == np.arange(4) * 10 + 3).all() + assert (sr[1].arr == np.arange(3) * 10 + 8).all() + assert (sr[2].arr == np.arange(4) * 2 + 3).all() + assert ((sr[0] + sr[1]).arr == np.arange(7) * 10 + 3).all() + assert ((sr[1] + sr[0]).arr == np.arange(7) * 10 + 8).all() + assert ((sr[1] + sr[0] + sr[0]).arr == np.arange(11) * 10 + 8).all() + assert sr[1][2] == 28 + assert sr[1][-2] == sr[1].arr[-2] + assert_raises(ValueError, lambda: sr[0] + sr[2]) + assert_raises(ValueError, lambda: sr[2] + sr[1]) + assert_raises(ValueError, lambda: sr[0] + sr[3]) + assert_raises(ValueError, lambda: sr[3] + sr[1]) + assert_raises(ValueError, lambda: sr[3] + sr[2]) + + # test slicing + assert (sr[0][1:3].arr == sr[0].arr[1:3]).all() + assert (sr[0][1:].arr == sr[0].arr[1:]).all() + assert (sr[0][:-2].arr == sr[0].arr[:-2]).all() + assert (sr[0][1:-1].arr == sr[0].arr[1:-1]).all() + assert (sr[0][1:-1:2].arr == sr[0].arr[1:-1:2]).all() + assert (sr[0][::2].arr == sr[0].arr[::2]).all() + assert (sr[0][:10:2].arr == sr[0].arr[::2]).all() + assert (sr[0][10::-1].arr == sr[0].arr[::-1]).all() + assert (sr[0][3:1:-1].arr == sr[0].arr[3:1:-1]).all() + assert (sr[0][1:3:-1].arr == sr[0].arr[1:3:-1]).all() + + +def test_writing(): + """ + Tests the writing and reading back in of custom created CIFTI2 axes + """ + for ax1 in get_axes(): + for ax2 in get_axes(): + arr = np.random.randn(len(ax1), len(ax2)) + check_rewrite(arr, (ax1, ax2)) diff --git a/nibabel/cifti2/tests/test_io.py b/nibabel/cifti2/tests/test_io.py new file mode 100644 index 0000000000..22f4c27253 --- /dev/null +++ b/nibabel/cifti2/tests/test_io.py @@ -0,0 +1,176 @@ +from nibabel.cifti2 import cifti2_axes, cifti2 +from nibabel.tests.nibabel_data import get_nibabel_data, needs_nibabel_data +import nibabel as nib +import os +import numpy as np +import tempfile + +test_directory = os.path.join(get_nibabel_data(), 'nitest-cifti2') + +hcp_labels = ['CortexLeft', 'CortexRight', 'AccumbensLeft', 'AccumbensRight', 'AmygdalaLeft', 'AmygdalaRight', + 'brain_stem', 'CaudateLeft', 'CaudateRight', 'CerebellumLeft', 'CerebellumRight', + 'Diencephalon_ventral_left', 'Diencephalon_ventral_right', 'HippocampusLeft', 'HippocampusRight', + 'PallidumLeft', 'PallidumRight', 'PutamenLeft', 'PutamenRight', 'ThalamusLeft', 'ThalamusRight'] + +hcp_n_elements = [29696, 29716, 135, 140, 315, 332, 3472, 728, 755, 8709, 9144, 706, + 712, 764, 795, 297, 260, 1060, 1010, 1288, 1248] + +hcp_affine = np.array([[ -2., 0., 0., 90.], + [ 0., 2., 0., -126.], + [ 0., 0., 2., -72.], + [ 0., 0., 0., 1.]]) + + +def check_hcp_grayordinates(brain_model): + """Checks that a BrainModel matches the expected 32k HCP grayordinates + """ + assert isinstance(brain_model, cifti2_axes.BrainModel) + structures = list(brain_model.iter_structures()) + assert len(structures) == len(hcp_labels) + idx_start = 0 + for idx, (name, _, bm), label, nel in zip(range(len(structures)), structures, hcp_labels, hcp_n_elements): + if idx < 2: + assert name in bm.nvertices.keys() + assert (bm.voxel == -1).all() + assert (bm.vertex != -1).any() + assert bm.nvertices[name] == 32492 + else: + assert name not in bm.nvertices.keys() + assert (bm.voxel != -1).any() + assert (bm.vertex == -1).all() + assert (bm.affine == hcp_affine).all() + assert bm.volume_shape == (91, 109, 91) + assert name == cifti2_axes.BrainModel.to_cifti_brain_structure_name(label) + assert len(bm) == nel + assert (bm.arr == brain_model.arr[idx_start:idx_start + nel]).all() + idx_start += nel + assert idx_start == len(brain_model) + + assert (brain_model.arr[:5]['vertex'] == np.arange(5)).all() + assert structures[0][2].vertex[-1] == 32491 + assert structures[1][2].vertex[0] == 0 + assert structures[1][2].vertex[-1] == 32491 + assert (structures[-1][2].arr[-1] == brain_model.arr[-1]).all() + assert (brain_model.arr[-1]['voxel'] == [38, 55, 46]).all() + assert (brain_model.arr[70000]['voxel'] == [56, 22, 19]).all() + + +def check_Conte69(brain_model): + """Checks that the BrainModel matches the expected Conte69 surface coordinates + """ + assert isinstance(brain_model, cifti2_axes.BrainModel) + structures = list(brain_model.iter_structures()) + assert len(structures) == 2 + assert structures[0][0] == 'CIFTI_STRUCTURE_CORTEX_LEFT' + assert structures[0][2].is_surface.all() + assert structures[1][0] == 'CIFTI_STRUCTURE_CORTEX_RIGHT' + assert structures[1][2].is_surface.all() + assert (brain_model.voxel == -1).all() + + assert (brain_model.arr[:5]['vertex'] == np.arange(5)).all() + assert structures[0][2].vertex[-1] == 32491 + assert structures[1][2].vertex[0] == 0 + assert structures[1][2].vertex[-1] == 32491 + + +def check_rewrite(arr, axes, extension='.nii'): + """ + Checks wheter writing the Cifti2 array to disc and reading it back in gives the same object + + Parameters + ---------- + arr : array + N-dimensional array of data + axes : Sequence[cifti2_axes.Axis] + sequence of length N with the meaning of the rows/columns along each dimension + extension : str + custom extension to use + """ + (fd, name) = tempfile.mkstemp(extension) + cifti2.Cifti2Image(arr, header=axes).to_filename(name) + img = nib.load(name) + arr2 = img.get_data() + assert (arr == arr2).all() + for idx in range(len(img.shape)): + assert (axes[idx] == img.header.get_axis(idx)) + return img + + +@needs_nibabel_data('nitest-cifti2') +def test_read_ones(): + img = nib.load(os.path.join(test_directory, 'ones.dscalar.nii')) + arr = img.get_data() + axes = [img.header.get_axis(dim) for dim in range(2)] + assert (arr == 1).all() + assert isinstance(axes[0], cifti2_axes.Scalar) + assert len(axes[0]) == 1 + assert axes[0].name[0] == 'ones' + assert axes[0].meta[0] == {} + check_hcp_grayordinates(axes[1]) + img = check_rewrite(arr, axes) + check_hcp_grayordinates(img.header.get_axis(1)) + + +@needs_nibabel_data('nitest-cifti2') +def test_read_conte69_dscalar(): + img = nib.load(os.path.join(test_directory, 'Conte69.MyelinAndCorrThickness.32k_fs_LR.dscalar.nii')) + arr = img.get_data() + axes = [img.header.get_axis(dim) for dim in range(2)] + assert isinstance(axes[0], cifti2_axes.Scalar) + assert len(axes[0]) == 2 + assert axes[0].name[0] == 'MyelinMap_BC_decurv' + assert axes[0].name[1] == 'corrThickness' + assert axes[0].meta[0] == {'PaletteColorMapping': '\n MODE_AUTO_SCALE_PERCENTAGE\n 98.000000 2.000000 2.000000 98.000000\n -100.000000 0.000000 0.000000 100.000000\n ROY-BIG-BL\n true\n true\n false\n true\n THRESHOLD_TEST_SHOW_OUTSIDE\n THRESHOLD_TYPE_OFF\n false\n -1.000000 1.000000\n -1.000000 1.000000\n -1.000000 1.000000\n \n PALETTE_THRESHOLD_RANGE_MODE_MAP\n'} + check_Conte69(axes[1]) + check_rewrite(arr, axes) + + +@needs_nibabel_data('nitest-cifti2') +def test_read_conte69_dtseries(): + img = nib.load(os.path.join(test_directory, 'Conte69.MyelinAndCorrThickness.32k_fs_LR.dtseries.nii')) + arr = img.get_data() + axes = [img.header.get_axis(dim) for dim in range(2)] + assert isinstance(axes[0], cifti2_axes.Series) + assert len(axes[0]) == 2 + assert axes[0].start == 0 + assert axes[0].step == 1 + assert axes[0].size == arr.shape[0] + assert (axes[0].arr == [0, 1]).all() + check_Conte69(axes[1]) + check_rewrite(arr, axes) + + +@needs_nibabel_data('nitest-cifti2') +def test_read_conte69_dlabel(): + img = nib.load(os.path.join(test_directory, 'Conte69.parcellations_VGD11b.32k_fs_LR.dlabel.nii')) + arr = img.get_data() + axes = [img.header.get_axis(dim) for dim in range(2)] + assert isinstance(axes[0], cifti2_axes.Label) + assert len(axes[0]) == 3 + assert (axes[0].name == ['Composite Parcellation-lh (FRB08_OFP03_retinotopic)', + 'Brodmann lh (from colin.R via pals_R-to-fs_LR)', 'MEDIAL WALL lh (fs_LR)']).all() + assert axes[0].label[1][70] == ('19_B05', (1.0, 0.867, 0.467, 1.0)) + assert (axes[0].meta == [{}] * 3).all() + check_Conte69(axes[1]) + check_rewrite(arr, axes) + + +@needs_nibabel_data('nitest-cifti2') +def test_read_conte69_ptseries(): + img = nib.load(os.path.join(test_directory, 'Conte69.MyelinAndCorrThickness.32k_fs_LR.ptseries.nii')) + arr = img.get_data() + axes = [img.header.get_axis(dim) for dim in range(2)] + assert isinstance(axes[0], cifti2_axes.Series) + assert len(axes[0]) == 2 + assert axes[0].start == 0 + assert axes[0].step == 1 + assert axes[0].size == arr.shape[0] + assert (axes[0].arr == [0, 1]).all() + + assert len(axes[1]) == 54 + voxels, vertices = axes[1]['ER_FRB08'] + assert voxels.shape == (0, 3) + assert len(vertices) == 2 + assert vertices['CIFTI_STRUCTURE_CORTEX_LEFT'].shape == (206 // 2, ) + assert vertices['CIFTI_STRUCTURE_CORTEX_RIGHT'].shape == (206 // 2, ) + check_rewrite(arr, axes) diff --git a/nibabel/cifti2/tests/test_name.py b/nibabel/cifti2/tests/test_name.py new file mode 100644 index 0000000000..a73c5e8c46 --- /dev/null +++ b/nibabel/cifti2/tests/test_name.py @@ -0,0 +1,19 @@ +from nibabel.cifti2 import cifti2_axes + +equivalents = [('CIFTI_STRUCTURE_CORTEX_LEFT', ('CortexLeft', 'LeftCortex', 'left_cortex', 'Left Cortex', + 'Cortex_Left', 'cortex left', 'CORTEX_LEFT', 'LEFT CORTEX', + ('cortex', 'left'), ('CORTEX', 'Left'), ('LEFT', 'coRTEX'))), + ('CIFTI_STRUCTURE_CORTEX', ('Cortex', 'CortexBOTH', 'Cortex_both', 'both cortex', + 'BOTH_CORTEX', 'cortex', 'CORTEX', ('cortex', ), + ('COrtex', 'Both'), ('both', 'cortex')))] + + +def test_name_conversion(): + """ + Tests the automatic name conversion to a format recognized by CIFTI2 + """ + func = cifti2_axes.BrainModel.to_cifti_brain_structure_name + for base_name, input_names in equivalents: + assert base_name == func(base_name) + for name in input_names: + assert base_name == func(name) \ No newline at end of file From 0e7566a1dff794b60cbbefb67bcb791e6bee22f5 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Mon, 25 Jun 2018 14:27:29 +0100 Subject: [PATCH 02/57] Clarified the test filenames distinguishes between io using the raw header or using the new Cifti2 axes --- nibabel/cifti2/tests/test_axes.py | 2 +- nibabel/cifti2/tests/{test_io.py => test_cifti2io_axes.py} | 0 .../cifti2/tests/{test_cifti2io.py => test_cifti2io_header.py} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename nibabel/cifti2/tests/{test_io.py => test_cifti2io_axes.py} (100%) rename nibabel/cifti2/tests/{test_cifti2io.py => test_cifti2io_header.py} (100%) diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index 64e94f2663..d7b24f03ec 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -1,6 +1,6 @@ import numpy as np from nose.tools import assert_raises -from .test_io import check_rewrite +from .test_cifti2io_axes import check_rewrite import nibabel.cifti2.cifti2_axes as axes diff --git a/nibabel/cifti2/tests/test_io.py b/nibabel/cifti2/tests/test_cifti2io_axes.py similarity index 100% rename from nibabel/cifti2/tests/test_io.py rename to nibabel/cifti2/tests/test_cifti2io_axes.py diff --git a/nibabel/cifti2/tests/test_cifti2io.py b/nibabel/cifti2/tests/test_cifti2io_header.py similarity index 100% rename from nibabel/cifti2/tests/test_cifti2io.py rename to nibabel/cifti2/tests/test_cifti2io_header.py From 671c1967c5d2ba56b30fc9a4c831ba7f3183fd3d Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Mon, 25 Jun 2018 14:31:16 +0100 Subject: [PATCH 03/57] BUG: removed failing doctest --- nibabel/cifti2/cifti2_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 3125b58404..9f1992b5d6 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -537,7 +537,7 @@ class Parcels(Axis): (N, ) array with the actual get_parcels (each of which is a BrainModel object) Individual get_parcels can also be accessed based on their name, using - >>> parcel = parcel_axis[name] + ``parcel = parcel_axis[name]`` """ _use_dtype = np.dtype([('name', 'U60'), ('voxels', 'object'), ('vertices', 'object')]) _affine = None From 1dd059de2da6593ba25d062ef0b6b527d5084ea1 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Mon, 28 Jan 2019 15:00:38 +0000 Subject: [PATCH 04/57] DOC: explained that Axis is an abstract class --- nibabel/cifti2/cifti2_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 9f1992b5d6..09faf0e341 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -54,7 +54,7 @@ def to_header(axes): class Axis(object): """ - Generic object describing the rows or columns of a CIFTI vector/matrix + Abstract class for any object describing the rows or columns of a CIFTI vector/matrix Attributes ---------- From 8660afdcc01601ccfe39763c24ce3d9dc428a450 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 14 Mar 2019 17:38:05 +0000 Subject: [PATCH 05/57] RF: removed the typed array underlying all axes Also added a few tests to increase coverage --- nibabel/cifti2/cifti2_axes.py | 579 ++++++++++++--------- nibabel/cifti2/tests/test_axes.py | 51 +- nibabel/cifti2/tests/test_cifti2io_axes.py | 16 +- 3 files changed, 387 insertions(+), 259 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 09faf0e341..fc970d5717 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -2,6 +2,7 @@ from nibabel.cifti2 import cifti2 from six import string_types from operator import xor +from abc import ABC, abstractmethod def from_mapping(mim): @@ -52,55 +53,38 @@ def to_header(axes): return cifti2.Cifti2Header(matrix) -class Axis(object): +class Axis(ABC): """ Abstract class for any object describing the rows or columns of a CIFTI vector/matrix - Attributes - ---------- - arr : np.ndarray - (N, ) typed array with the actual information on each row/column + Mainly used for type checking. """ - _use_dtype = None - arr = None - def __init__(self, arr): - self.arr = np.asarray(arr, dtype=self._use_dtype) + @property + def size(self, ): + return len(self) - def get_element(self, index): + @abstractmethod + def __len__(self): + pass + + @abstractmethod + def __eq__(self, other): """ - Extracts a single element from the axis + Compares whether two Axes are equal Parameters ---------- - index : int - Indexes the row/column of interest + other : Axis + other axis to compare to Returns ------- - Description of the row/column + False if the axes don't have the same type or if their content differs """ - return self.arr[index] - - def __getitem__(self, item): - if isinstance(item, int): - return self.get_element(item) - if isinstance(item, string_types): - raise IndexError("Can not index an Axis with a string (except for Parcels)") - return type(self)(self.arr[item]) - - @property - def size(self, ): - return self.arr.size - - def __len__(self): - return self.size - - def __eq__(self, other): - return (type(self) == type(other) and - len(self) == len(other) and - (self.arr == other.arr).all()) + pass + @abstractmethod def __add__(self, other): """ Concatenates two Axes of the same type @@ -114,9 +98,13 @@ def __add__(self, other): ------- Axis of the same subtype as self and other """ - if type(self) == type(other): - return type(self)(np.append(self.arr, other.arr)) - return NotImplemented + pass + + @abstractmethod + def __getitem__(self, item): + """ + Extracts definition of single row/column or new Axis describing a subset of the rows/columns + """ class BrainModel(Axis): @@ -127,45 +115,67 @@ class BrainModel(Axis): Attributes ---------- + name : np.ndarray + (N, ) array with the brain structure objects voxel : np.ndarray (N, 3) array with the voxel indices vertex : np.ndarray (N, ) array with the vertex indices - name : np.ndarray - (N, ) array with the brain structure objects """ - _use_dtype = np.dtype([('vertex', 'i4'), ('voxel', ('i4', 3)), - ('name', 'U%i' % max(len(name) for name in cifti2.CIFTI_BRAIN_STRUCTURES))]) _affine = None _volume_shape = None + _nvertices = None + _name = None - def __init__(self, arr, affine=None, volume_shape=None, nvertices=None): + def __init__(self, name, voxel=None, vertex=None, affine=None, volume_shape=None, nvertices=None): """ Creates a new BrainModel axis defining the vertices and voxels represented by each row/column + A more convenient way to create BrainModel axes is provided by the factory methods: + - `from_mask`: creates a surface or volumetric BrainModel axis from respectively 1D and 3D masks + - `from_surface`: creates a volumetric BrainModel axis + + The resulting BrainModel axes can be concatenated by adding them together. + Parameters ---------- - arr : np.ndarray - (N, ) structured array with for every element a tuple with 3 elements: - - vertex index (-1 for voxels) - - 3 voxel indices (-1 for vertices) - - string (name of brain structure) + name : str or np.ndarray + brain structure name or (N, ) array with the brain structure names + voxel : np.ndarray + (N, 3) array with the voxel indices (can be omitted for CIFTI files only covering the surface) + vertex : np.ndarray + (N, ) array with the vertex indices (can be omitted for volumetric CIFTI files) affine : np.ndarray (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only covering the surface) volume_shape : Tuple[int, int, int] shape of the volume in which the voxels were defined (not needed for CIFTI files only covering the surface) nvertices : dict[String -> int] - maps names of surface elements to integers + maps names of surface elements to integers (not needed for volumetric CIFTI files) """ - super(BrainModel, self).__init__(arr) - self.name = self.name # correct names to CIFTI brain structures + if voxel is None: + if vertex is None: + raise ValueError("Voxel and vertex indices not defined") + nelements = len(vertex) + self.voxel = -np.ones((nelements, 3), dtype=int) + else: + nelements = len(voxel) + self.voxel = np.asarray(voxel, dtype=int) + + self.vertex = -np.ones(nelements, dtype=int) if vertex is None else np.asarray(vertex, dtype=int) + + if isinstance(name, string_types): + name = [self.to_cifti_brain_structure_name(name)] * self.vertex.size + self.name = np.asarray(name, dtype='U') + if nvertices is None: self.nvertices = {} else: self.nvertices = dict(nvertices) + for name in list(self.nvertices.keys()): if name not in self.name: del self.nvertices[name] + if self.is_surface.all(): self.affine = None self.volume_shape = None @@ -173,6 +183,17 @@ def __init__(self, arr, affine=None, volume_shape=None, nvertices=None): self.affine = affine self.volume_shape = volume_shape + if (self.vertex[self.is_surface] < 0).any(): + raise ValueError('Undefined vertex indices found for surface elements') + if (self.voxel[~self.is_surface] < 0).any(): + raise ValueError('Undefined voxel indices found for volumetric elements') + + for check_name in ('name', 'voxel', 'vertex'): + shape = (self.size, 3) if check_name == 'voxel' else (self.size, ) + if getattr(self, check_name).shape != shape: + raise ValueError("Input {} has incorrect shape ({}) for Label axis".format( + check_name, getattr(self, check_name).shape)) + @classmethod def from_mapping(cls, mim): """ @@ -187,20 +208,21 @@ def from_mapping(cls, mim): BrainModel """ nbm = np.sum([bm.index_count for bm in mim.brain_models]) - arr = np.zeros(nbm, dtype=cls._use_dtype) - arr['voxel'] = -1 - arr['vertex'] = -1 + voxel = -np.ones((nbm, 3)) + vertex = -np.ones(nbm) + name = [] + nvertices = {} affine, shape = None, None for bm in mim.brain_models: index_end = bm.index_offset + bm.index_count is_surface = bm.model_type == 'CIFTI_MODEL_TYPE_SURFACE' - arr['name'][bm.index_offset: index_end] = bm.brain_structure + name.extend([bm.brain_structure] * bm.index_count) if is_surface: - arr['vertex'][bm.index_offset: index_end] = bm.vertex_indices + vertex[bm.index_offset: index_end] = bm.vertex_indices nvertices[bm.brain_structure] = bm.surface_number_of_vertices else: - arr['voxel'][bm.index_offset: index_end, :] = bm.voxel_indices_ijk + voxel[bm.index_offset: index_end, :] = bm.voxel_indices_ijk if affine is None: shape = mim.volume.volume_dimensions affine = mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix @@ -209,7 +231,7 @@ def from_mapping(cls, mim): raise ValueError("All volume masks should be defined in the same volume") if (affine != mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix).any(): raise ValueError("All volume masks should have the same affine") - return cls(arr, affine, shape, nvertices) + return cls(name, voxel, vertex, affine, shape, nvertices) @classmethod def from_mask(cls, mask, name='other', affine=None): @@ -239,11 +261,7 @@ def from_mask(cls, mask, name='other', affine=None): return cls.from_surface(np.where(mask != 0)[0], mask.size, name=name) elif mask.ndim == 3: voxels = np.array(np.where(mask != 0)).T - arr = np.zeros(len(voxels), dtype=cls._use_dtype) - arr['vertex'] = -1 - arr['voxel'] = voxels - arr['name'] = cls.to_cifti_brain_structure_name(name) - return cls(arr, affine=affine, volume_shape=mask.shape) + return cls(name, voxel=voxels, affine=affine, volume_shape=mask.shape) else: raise ValueError("Mask should be either 1-dimensional (for surfaces) or " "3-dimensional (for volumes), not %i-dimensional" % mask.ndim) @@ -266,11 +284,9 @@ def from_surface(cls, vertices, nvertex, name='Cortex'): ------- BrainModel which covers (part of) the surface """ - arr = np.zeros(len(vertices), dtype=cls._use_dtype) - arr['voxel'] = -1 - arr['vertex'] = vertices - arr['name'] = cls.to_cifti_brain_structure_name(name) - return cls(arr, nvertices={arr['name'][0]: nvertex}) + cifti_name = cls.to_cifti_brain_structure_name(name) + return cls(cifti_name, vertex=vertices, + nvertices={cifti_name: nvertex}) def get_element(self, index): """ @@ -288,10 +304,9 @@ def get_element(self, index): - vertex index if it is a surface element, otherwise array with 3 voxel indices - structure.BrainStructure object describing the brain structure the element was taken from """ - elem = self.arr[index] - is_surface = elem['name'] in self.nvertices.keys() + is_surface = self.name[index] in self.nvertices.keys() name = 'vertex' if is_surface else 'voxel' - return is_surface, elem[name], elem['name'] + return is_surface, getattr(self, name)[index], self.name[index] def to_mapping(self, dim): """ @@ -348,6 +363,9 @@ def iter_structures(self, ): @property def affine(self, ): + """ + Affine of the volumetric image in which the greyordinate voxels were defined + """ return self._affine @affine.setter @@ -360,6 +378,9 @@ def affine(self, value): @property def volume_shape(self, ): + """ + Shape of the volumetric image in which the greyordinate voxels were defined + """ return self._volume_shape @volume_shape.setter @@ -368,43 +389,26 @@ def volume_shape(self, value): value = tuple(value) if len(value) != 3: raise ValueError("Volume shape should be a tuple of length 3") + if not all(isinstance(v, int) for v in value): + raise ValueError("All elements of the volume shape should be integers") self._volume_shape = value @property def is_surface(self, ): - """True for any element on the surface - """ - return np.vectorize(lambda name: name in self.nvertices.keys())(self.name) - - @property - def voxel(self, ): - """The voxel represented by each row or column """ - return self.arr['voxel'] - - @voxel.setter - def voxel(self, values): - self.arr['voxel'] = values - - @property - def vertex(self, ): - """The vertex represented by each row or column + (N, ) boolean array which is true for any element on the surface """ - return self.arr['vertex'] - - @vertex.setter - def vertex(self, values): - self.arr['vertex'] = values + return np.vectorize(lambda name: name in self.nvertices.keys())(self.name) @property def name(self, ): """The brain structure to which the voxel/vertices of belong """ - return self.arr['name'] + return self._name @name.setter def name(self, values): - self.arr['name'] = [self.to_cifti_brain_structure_name(name) for name in values] + self._name = np.array([self.to_cifti_brain_structure_name(name) for name in values]) @staticmethod def to_cifti_brain_structure_name(name): @@ -474,23 +478,47 @@ def to_cifti_brain_structure_name(name): (name, proposed_name)) return proposed_name + def __len__(self ): + return self.name.size + def __getitem__(self, item): + """ + Extracts part of the brain structure + + Parameters + ---------- + item : anything that can index a 1D array + + Returns + ------- + If `item` is an integer returns a tuple with 3 elements: + - boolean, which is True if it is a surface element + - vertex index if it is a surface element, otherwise array with 3 voxel indices + - structure.BrainStructure object describing the brain structure the element was taken from + + Otherwise returns a new BrainModel + """ if isinstance(item, int): return self.get_element(item) if isinstance(item, string_types): raise IndexError("Can not index an Axis with a string (except for Parcels)") - return type(self)(self.arr[item], self.affine, self.volume_shape, self.nvertices) + return type(self)(self.name[item], self.voxel[item], self.vertex[item], + self.affine, self.volume_shape, self.nvertices) def __eq__(self, other): - if type(self) != type(other) or len(self) != len(other): + if not isinstance(other, BrainModel) or len(self) != len(other): return False if xor(self.affine is None, other.affine is None): return False - return (((self.affine is None and other.affine is None) or + return ( + ((self.affine is None and other.affine is None) or (abs(self.affine - other.affine).max() < 1e-8 and self.volume_shape == other.volume_shape)) and (self.nvertices == other.nvertices) and - (self.arr == other.arr).all()) + (self.name == other.name).all() and + (self.voxel[~self.is_surface] == other.voxel[~other.is_surface]).all() and + (self.vertex[~self.is_surface] == other.vertex[~other.is_surface]).all() + ) def __add__(self, other): """ @@ -505,7 +533,7 @@ def __add__(self, other): ------- BrainModel """ - if type(self) == type(other): + if isinstance(other, BrainModel): if self.affine is None: affine, shape = other.affine, other.volume_shape else: @@ -519,7 +547,12 @@ def __add__(self, other): raise ValueError("Trying to concatenate two BrainModels with inconsistent number of vertices for %s" % name) nvertices[name] = value - return type(self)(np.append(self.arr, other.arr), affine, shape, nvertices) + return type(self)( + np.append(self.name, other.name), + np.concatenate((self.voxel, other.voxel), 0), + np.append(self.vertex, other.vertex), + affine, shape, nvertices + ) return NotImplemented @@ -529,39 +562,34 @@ class Parcels(Axis): This Axis describes which parcel is represented by each row/column. - Attributes - ---------- - name : np.ndarray - (N, ) string array with the parcel names - parcel : np.ndarray - (N, ) array with the actual get_parcels (each of which is a BrainModel object) - - Individual get_parcels can also be accessed based on their name, using + Individual parcels can be accessed based on their name, using ``parcel = parcel_axis[name]`` """ - _use_dtype = np.dtype([('name', 'U60'), ('voxels', 'object'), ('vertices', 'object')]) _affine = None _volume_shape = None - def __init__(self, arr, affine=None, volume_shape=None, nvertices=None): + def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvertices=None): """ Creates a new BrainModel axis defining the vertices and voxels represented by each row/column Parameters ---------- - arr : np.ndarray - (N, ) structured array with for every element a tuple with 3 elements: - - string (name of parcel) - - (M, 3) int array with the M voxel indices in the parcel - - Dict[String -> (K, ) int array] mapping surface brain structure names to vertex indices + name : np.ndarray + (N, ) string array with the parcel names + voxels : np.ndarray + (N, ) object array each containing a sequence of voxels + vertices : np.ndarray + (N, ) object array each containing a sequence of vertices affine : np.ndarray (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only covering the surface) volume_shape : Tuple[int, int, int] shape of the volume in which the voxels were defined (not needed for CIFTI files only covering the surface) nvertices : dict[String -> int] - maps names of surface elements to integers + maps names of surface elements to integers (not needed for volumetric CIFTI files) """ - super(Parcels, self).__init__(arr) + self.name = np.asarray(name, dtype='U') + self.voxels = np.asarray(voxels, dtype='object') + self.vertices = np.asarray(vertices, dtype='object') self.affine = affine self.volume_shape = volume_shape if nvertices is None: @@ -569,6 +597,11 @@ def __init__(self, arr, affine=None, volume_shape=None, nvertices=None): else: self.nvertices = dict(nvertices) + for check_name in ('name', 'voxels', 'vertices'): + if getattr(self, check_name).shape != (self.size, ): + raise ValueError("Input {} has incorrect shape ({}) for Label axis".format( + check_name, getattr(self, check_name).shape)) + @classmethod def from_brain_models(cls, named_brain_models): """ @@ -585,9 +618,13 @@ def from_brain_models(cls, named_brain_models): """ affine = None volume_shape = None - arr = np.zeros(len(named_brain_models), dtype=cls._use_dtype) + all_names = [] + all_voxels = [] + all_vertices = [] nvertices = {} for idx_parcel, (parcel_name, bm) in enumerate(named_brain_models): + all_names.append(parcel_name) + voxels = bm.voxel[~bm.is_surface] if voxels.shape[0] != 0: if affine is None: @@ -597,6 +634,8 @@ def from_brain_models(cls, named_brain_models): if (affine != bm.affine).any() or (volume_shape != bm.volume_shape): raise ValueError( "Can not combine brain models defined in different volumes into a single Parcel axis") + all_voxels.append(voxels) + vertices = {} for name, _, bm_part in bm.iter_structures(): if name in bm.nvertices.keys(): @@ -604,8 +643,8 @@ def from_brain_models(cls, named_brain_models): raise ValueError("Got multiple conflicting number of vertices for surface structure %s" % name) nvertices[name] = bm.nvertices[name] vertices[name] = bm_part.vertex - arr[idx_parcel] = (parcel_name, voxels, vertices) - return Parcels(arr, affine, volume_shape, nvertices) + all_vertices.append(vertices) + return Parcels(all_names, all_voxels, all_vertices, affine, volume_shape, nvertices) @classmethod def from_mapping(cls, mim): @@ -621,7 +660,10 @@ def from_mapping(cls, mim): Parcels """ nparcels = len(list(mim.parcels)) - arr = np.zeros(nparcels, dtype=cls._use_dtype) + all_names = [] + all_voxels = np.zeros(nparcels, dtype='object') + all_vertices = np.zeros(nparcels, dtype='object') + volume_shape = None if mim.volume is None else mim.volume.volume_dimensions affine = None if mim.volume is None else mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix nvertices = {} @@ -638,10 +680,10 @@ def from_mapping(cls, mim): vertices[vertex.brain_structure] = np.array(vertex) if name not in nvertices.keys(): raise ValueError("Number of vertices for surface structure %s not defined" % name) - arr[idx_parcel]['voxels'] = voxels - arr[idx_parcel]['vertices'] = vertices - arr[idx_parcel]['name'] = parcel.name - return cls(arr, affine, volume_shape, nvertices) + all_voxels[idx_parcel] = voxels + all_vertices[idx_parcel] = vertices + all_names.append(parcel.name) + return cls(all_names, all_voxels, all_vertices, affine, volume_shape, nvertices) def to_mapping(self, dim): """ @@ -662,7 +704,7 @@ def to_mapping(self, dim): mim.volume = cifti2.Cifti2Volume(self.volume_shape, affine) for name, nvertex in self.nvertices.items(): mim.append(cifti2.Cifti2Surface(name, nvertex)) - for name, voxels, vertices in self.arr: + for name, voxels, vertices in zip(self.name, self.voxels, self.vertices): cifti_voxels = cifti2.Cifti2VoxelIndicesIJK(voxels) element = cifti2.Cifti2Parcel(name, cifti_voxels) for name, idx_vertices in vertices.items(): @@ -712,31 +754,16 @@ def volume_shape(self, value): raise ValueError("Volume shape should be a tuple of length 3") self._volume_shape = value - @property - def name(self, ): - return self.arr['name'] - - @name.setter - def name(self, values): - self.arr['name'] = values - - @property - def voxels(self, ): - return self.arr['voxels'] - - @voxels.setter - def voxels(self, values): - self.arr['voxels'] = values - - @property - def vertices(self, ): - return self.arr['vertices'] - - @vertices.setter - def vertices(self, values): - self.arr['vertices'] = values + def __len__(self, ): + return self.name.size def __getitem__(self, item): + """ + Extracts subset of the axes based on the type of ``item``: + - `int`: 3-element tuple of (parcel name, parcel voxels, parcel vertices) + - `string`: 2-element tuple of (parcel voxels, parcel vertices + - other object that can index 1D arrays: new Parcel axis + """ if isinstance(item, string_types): idx = np.where(self.name == item)[0] if len(idx) == 0: @@ -746,9 +773,8 @@ def __getitem__(self, item): return self.voxels[idx[0]], self.vertices[idx[0]] if isinstance(item, int): return self.get_element(item) - if isinstance(item, string_types): - raise IndexError("Can not index an Axis with a string (except for Parcels)") - return type(self)(self.arr[item], self.affine, self.volume_shape, self.nvertices) + return type(self)(self.name[item], self.voxels[item], self.vertices[item], + self.affine, self.volume_shape, self.nvertices) def __eq__(self, other): if (type(self) != type(other) or len(self) != len(other) or @@ -797,33 +823,41 @@ def __add__(self, other): raise ValueError("Trying to concatenate two Parcels with inconsistent number of vertices for %s" % name) nvertices[name] = value - return type(self)(np.append(self.arr, other.arr), affine, shape, nvertices) + return type(self)( + np.append(self.name, other.name), + np.append(self.voxels, other.voxels), + np.append(self.vertices, other.vertices), + affine, shape, nvertices + ) return NotImplemented class Scalar(Axis): """ Along this axis of the CIFTI vector/matrix each row/column has been given a unique name and optionally metadata - - Attributes - ---------- - name : np.ndarray - (N, ) string array with the parcel names - meta : np.ndarray - (N, ) array with a dictionary of metadata for each row/column """ _use_dtype = np.dtype([('name', 'U60'), ('meta', 'object')]) - def __init__(self, arr): + def __init__(self, name, meta=None): """ Creates a new Scalar axis from (name, meta-data) pairs Parameters ---------- - arr : Iterable[Tuple[str, dict[str -> str]] - iterates over all rows/columns assigning a name and a dictionary of metadata to each + name : np.ndarray + (N, ) string array with the parcel names + meta : np.ndarray + (N, ) object array with a dictionary of metadata for each row/column. Defaults to empty dictionary """ - super(Scalar, self).__init__(arr) + self.name = np.asarray(name, dtype='U') + if meta is None: + meta = [{} for _ in range(self.name.size)] + self.meta = np.asarray(meta, dtype='object') + + for check_name in ('name', 'meta'): + if getattr(self, check_name).shape != (self.size, ): + raise ValueError("Input {} has incorrect shape ({}) for Scalar axis".format( + check_name, getattr(self, check_name).shape)) @classmethod def from_mapping(cls, mim): @@ -838,29 +872,9 @@ def from_mapping(cls, mim): ------- Scalar """ - res = np.zeros(len(list(mim.named_maps)), dtype=cls._use_dtype) - res['name'] = [nm.map_name for nm in mim.named_maps] - res['meta'] = [{} if nm.metadata is None else dict(nm.metadata) for nm in mim.named_maps] - return cls(res) - - @classmethod - def from_names(cls, names): - """ - Creates a new get_scalar axis with the given row/column names - - Parameters - ---------- - names : List[str] - gives a unique name to every row/column in the matrix - - Returns - ------- - Scalar - """ - res = np.zeros(len(names), dtype=cls._use_dtype) - res['name'] = names - res['meta'] = [{} for _ in names] - return cls(res) + names = [nm.map_name for nm in mim.named_maps] + meta = [{} if nm.metadata is None else dict(nm.metadata) for nm in mim.named_maps] + return cls(names, meta) def to_mapping(self, dim): """ @@ -876,9 +890,9 @@ def to_mapping(self, dim): cifti2.Cifti2MatrixIndicesMap """ mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_SCALARS') - for elem in self.arr: - meta = None if len(elem['meta']) == 0 else elem['meta'] - named_map = cifti2.Cifti2NamedMap(elem['name'], cifti2.Cifti2MetaData(meta)) + for name, meta in zip(self.name, self.meta): + meta = None if len(meta) == 0 else meta + named_map = cifti2.Cifti2NamedMap(name, cifti2.Cifti2MetaData(meta)) mim.append(named_map) return mim @@ -897,7 +911,7 @@ def get_element(self, index): - unicode name of the get_scalar - dictionary with the element metadata """ - return self.arr['name'][index], self.arr['meta'][index] + return self.name[index], self.meta[index] def to_label(self, labels): """ @@ -905,7 +919,7 @@ def to_label(self, labels): Parameters ---------- - labels : list[dict] + labels : list[dict] or dict mapping from integers to (name, (R, G, B, A)), where `name` is a string and R, G, B, and A are floats between 0 and 1 giving the colour and alpha (transparency) @@ -913,56 +927,86 @@ def to_label(self, labels): ------- Label """ - res = np.zeros(self.size, dtype=Label._use_dtype) - res['name'] = self.arr['name'] - res['meta'] = self.arr['meta'] - res['get_label'] = labels - return Label(res) + if isinstance(labels, dict): + labels = [dict(labels) for _ in range(self.size)] + return Label(self.name, labels, self.meta) - @property - def name(self, ): - return self.arr['name'] + def __eq__(self, other): + """ + Compares two Scalars - @name.setter - def name(self, values): - self.arr['name'] = values + Parameters + ---------- + other : Scalar + scalar axis to be compared - @property - def meta(self, ): - return self.arr['meta'] + Returns + ------- + bool : False if type, length or content do not match + """ + if not isinstance(other, Scalar) or self.size != other.size: + return False + return (self.name == other.name).all() and (self.meta == other.meta).all() + + def __len__(self, ): + return self.name.size + + def __add__(self, other): + """ + Concatenates two Scalars + + Parameters + ---------- + other : Scalar + scalar axis to be appended to the current one + + Returns + ------- + Scalar + """ + if not isinstance(other, Scalar): + return NotImplemented + return Scalar( + np.append(self.name, other.name), + np.append(self.meta, other.meta), + ) - @meta.setter - def meta(self, values): - self.arr['meta'] = values + def __getitem__(self, item): + if isinstance(item, int): + return self.get_element(item) + return type(self)(self.name[item], self.meta[item]) class Label(Axis): """ Along this axis of the CIFTI vector/matrix each row/column has been given a unique name, get_label table, and optionally metadata - - Attributes - ---------- - name : np.ndarray - (N, ) string array with the parcel names - meta : np.ndarray - (N, ) array with a dictionary of metadata for each row/column - get_label : sp.ndarray - (N, ) array with dictionaries mapping integer values to get_label names and RGBA colors """ - _use_dtype = np.dtype([('name', 'U60'), ('get_label', 'object'), ('meta', 'object')]) - def __init__(self, arr): + def __init__(self, name, label, meta): """ - Creates a new Scalar axis from (name, meta-data) pairs + Creates a new Label axis from (name, meta-data) pairs Parameters ---------- - arr : Iterable[Tuple[str, dict[int -> (str, (float, float, float, float)), dict(str->str)]] - iterates over all rows/columns assigning a name, dictionary mapping integers to get_label names and rgba colours - and a dictionary of metadata to each + name : np.ndarray + (N, ) string array with the parcel names + meta : np.ndarray + (N, ) object array with a dictionary of metadata for each row/column + label : np.ndarray + (N, ) object array with dictionaries mapping integer values to get_label names and RGBA colors """ - super(Label, self).__init__(arr) + self.name = np.asarray(name, dtype='U') + self.meta = np.asarray(meta, dtype='object') + self.label = np.asarray(label, dtype='object') + + for check_name in ('name', 'meta', 'label'): + if getattr(self, check_name).shape != (self.size, ): + raise ValueError("Input {} has incorrect shape ({}) for Label axis".format( + check_name, getattr(self, check_name).shape)) + + def __len__(self, ): + return self.name.size @classmethod def from_mapping(cls, mim): @@ -995,12 +1039,13 @@ def to_mapping(self, dim): cifti2.Cifti2MatrixIndicesMap """ mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_LABELS') - for elem in self.arr: + for name, label, meta in zip(self.name, self.label, self.meta): label_table = cifti2.Cifti2LabelTable() - for key, value in elem['get_label'].items(): + for key, value in label.items(): label_table[key] = (value[0],) + tuple(value[1]) - meta = None if len(elem['meta']) == 0 else elem['meta'] - named_map = cifti2.Cifti2NamedMap(elem['name'], cifti2.Cifti2MetaData(meta), + if len(meta) == 0: + meta = None + named_map = cifti2.Cifti2NamedMap(name, cifti2.Cifti2MetaData(meta), label_table) mim.append(named_map) return mim @@ -1021,31 +1066,50 @@ def get_element(self, index): - dictionary with the get_label table - dictionary with the element metadata """ - return self.arr['name'][index], self.arr['get_label'][index], self.arr['meta'][index] + return self.name[index], self.label[index], self.meta[index] - @property - def name(self, ): - return self.arr['name'] + def __add__(self, other): + """ + Concatenates two Labels - @name.setter - def name(self, values): - self.arr['name'] = values + Parameters + ---------- + other : Label + scalar axis to be appended to the current one - @property - def meta(self, ): - return self.arr['meta'] + Returns + ------- + Label + """ + if not isinstance(other, Label): + return NotImplemented + return Label( + np.append(self.name, other.name), + np.append(self.label, other.label), + np.append(self.meta, other.meta), + ) - @meta.setter - def meta(self, values): - self.arr['meta'] = values + def __eq__(self, other): + """ + Compares two Labels - @property - def label(self, ): - return self.arr['get_label'] + Parameters + ---------- + other : Label + label axis to be compared + + Returns + ------- + bool : False if type, length or content do not match + """ + if not isinstance(other, Label) or self.size != other.size: + return False + return (self.name == other.name).all() and (self.meta == other.meta).all() and (self.label == other.label).all() - @label.setter - def label(self, values): - self.arr['get_label'] = values + def __getitem__(self, item): + if isinstance(item, int): + return self.get_element(item) + return type(self)(self.name[item], self.label[item], self.meta[item]) class Series(Axis): @@ -1154,9 +1218,9 @@ def extend(self, other_axis): Series """ if other_axis.step != self.step: - raise ValueError('Can only concatenate get_series with the same step size') + raise ValueError('Can only concatenate Series with the same step size') if other_axis.unit != self.unit: - raise ValueError('Can only concatenate get_series with the same unit') + raise ValueError('Can only concatenate Series with the same unit') return Series(self.start, self.step, self.size + other_axis.size, self.unit) def __getitem__(self, item): @@ -1175,7 +1239,7 @@ def __getitem__(self, item): nelements = (idx_end - idx_start) // step if nelements < 0: nelements = 0 - return Series(idx_start * self.step + self.start, self.step * step, nelements) + return Series(idx_start * self.step + self.start, self.step * step, nelements, self.unit) elif isinstance(item, int): return self.get_element(item) raise IndexError('Series can only be indexed with integers or slices without breaking the regular structure') @@ -1222,3 +1286,18 @@ def __add__(self, other): if isinstance(other, Series): return self.extend(other) return NotImplemented + + def __eq__(self, other): + """ + True if start, step, size, and unit are the same. + """ + return ( + isinstance(other, Series) and + self.start == other.start and + self.step == other.step and + self.size == other.size and + self.unit == other.unit + ) + + def __len__(self): + return self.size diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index d7b24f03ec..ee52cb160a 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -6,6 +6,7 @@ rand_affine = np.random.randn(4, 4) vol_shape = (5, 10, 3) +use_label = {0: ('something', (0.2, 0.4, 0.1, 0.5)), 1: ('even better', (0.3, 0.8, 0.43, 0.9))} def get_brain_models(): @@ -52,7 +53,7 @@ def get_scalar(): ------- Scalar axis """ - return axes.Scalar.from_names(['one', 'two', 'three']) + return axes.Scalar(['one', 'two', 'three']) def get_label(): @@ -63,8 +64,8 @@ def get_label(): ------- Label axis """ - return axes.Scalar.from_names(['one', 'two', 'three']).to_label({0: ('something', (0.2, 0.4, 0.1, 0.5)), - 1: ('even better', (0.3, 0.8, 0.43, 0.9))}) + return axes.Scalar(['one', 'two', 'three']).to_label(use_label) + def get_series(): """ @@ -190,14 +191,36 @@ def test_scalar(): assert len(sc) == 3 assert isinstance(sc, axes.Scalar) assert (sc.name == ['one', 'two', 'three']).all() + assert (sc.meta == [{}] * 3).all() assert sc[1] == ('two', {}) sc2 = sc + sc assert len(sc2) == 6 assert (sc2.name == ['one', 'two', 'three', 'one', 'two', 'three']).all() + assert (sc2.meta == [{}] * 6).all() assert sc2[:3] == sc assert sc2[3:] == sc +def test_label(): + """ + Test the introspection and creation of CIFTI2 Scalar axes + """ + lab = get_label() + assert len(lab) == 3 + assert isinstance(lab, axes.Label) + assert (lab.name == ['one', 'two', 'three']).all() + assert (lab.meta == [{}] * 3).all() + assert (lab.label == [use_label] * 3).all() + assert lab[1] == ('two', use_label, {}) + lab2 = lab + lab + assert len(lab2) == 6 + assert (lab2.name == ['one', 'two', 'three', 'one', 'two', 'three']).all() + assert (lab2.meta == [{}] * 6).all() + assert (lab2.label == [use_label] * 6).all() + assert lab2[:3] == lab + assert lab2[3:] == lab + + def test_series(): """ Test the introspection and creation of CIFTI2 Series axes @@ -243,3 +266,25 @@ def test_writing(): for ax2 in get_axes(): arr = np.random.randn(len(ax1), len(ax2)) check_rewrite(arr, (ax1, ax2)) + + +def test_common_interface(): + """ + Tests the common interface for all custom created CIFTI2 axes + """ + for axis1, axis2 in zip(get_axes(), get_axes()): + assert axis1 == axis2 + concatenated = axis1 + axis2 + assert axis1 != concatenated + print(type(axis1)) + if isinstance(axis1, axes.Series): + print(concatenated.start, axis1.start) + print(concatenated[:axis1.size].start, axis1.start) + assert axis1 == concatenated[:axis1.size] + if isinstance(axis1, axes.Series): + assert axis2 != concatenated[axis1.size:] + else: + assert axis2 == concatenated[axis1.size:] + + assert len(axis1) == axis1.size + diff --git a/nibabel/cifti2/tests/test_cifti2io_axes.py b/nibabel/cifti2/tests/test_cifti2io_axes.py index 22f4c27253..4a3aa8d111 100644 --- a/nibabel/cifti2/tests/test_cifti2io_axes.py +++ b/nibabel/cifti2/tests/test_cifti2io_axes.py @@ -42,17 +42,21 @@ def check_hcp_grayordinates(brain_model): assert bm.volume_shape == (91, 109, 91) assert name == cifti2_axes.BrainModel.to_cifti_brain_structure_name(label) assert len(bm) == nel - assert (bm.arr == brain_model.arr[idx_start:idx_start + nel]).all() + assert (bm.name == brain_model.name[idx_start:idx_start + nel]).all() + assert (bm.voxel == brain_model.voxel[idx_start:idx_start + nel]).all() + assert (bm.vertex == brain_model.vertex[idx_start:idx_start + nel]).all() idx_start += nel assert idx_start == len(brain_model) - assert (brain_model.arr[:5]['vertex'] == np.arange(5)).all() + assert (brain_model.vertex[:5] == np.arange(5)).all() assert structures[0][2].vertex[-1] == 32491 assert structures[1][2].vertex[0] == 0 assert structures[1][2].vertex[-1] == 32491 - assert (structures[-1][2].arr[-1] == brain_model.arr[-1]).all() - assert (brain_model.arr[-1]['voxel'] == [38, 55, 46]).all() - assert (brain_model.arr[70000]['voxel'] == [56, 22, 19]).all() + assert structures[-1][2].name[-1] == brain_model.name[-1] + assert (structures[-1][2].voxel[-1] == brain_model.voxel[-1]).all() + assert structures[-1][2].vertex[-1] == brain_model.vertex[-1] + assert (brain_model.voxel[-1] == [38, 55, 46]).all() + assert (brain_model.voxel[70000] == [56, 22, 19]).all() def check_Conte69(brain_model): @@ -67,7 +71,7 @@ def check_Conte69(brain_model): assert structures[1][2].is_surface.all() assert (brain_model.voxel == -1).all() - assert (brain_model.arr[:5]['vertex'] == np.arange(5)).all() + assert (brain_model.vertex[:5] == np.arange(5)).all() assert structures[0][2].vertex[-1] == 32491 assert structures[1][2].vertex[0] == 0 assert structures[1][2].vertex[-1] == 32491 From c2274c52967c80c4d1a93dbc4eedbadcf9271b7a Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 14 Mar 2019 17:56:10 +0000 Subject: [PATCH 06/57] RF: increased consistency of methods in the Axis objects - removed last reference of `arr` (replace with `time`) - removed spurious print statements in test --- nibabel/cifti2/cifti2_axes.py | 528 +++++++++++---------- nibabel/cifti2/tests/test_axes.py | 38 +- nibabel/cifti2/tests/test_cifti2io_axes.py | 4 +- 3 files changed, 285 insertions(+), 285 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index fc970d5717..e99507052a 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -122,10 +122,6 @@ class BrainModel(Axis): vertex : np.ndarray (N, ) array with the vertex indices """ - _affine = None - _volume_shape = None - _nvertices = None - _name = None def __init__(self, name, voxel=None, vertex=None, affine=None, volume_shape=None, nvertices=None): """ @@ -194,45 +190,6 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, volume_shape=None raise ValueError("Input {} has incorrect shape ({}) for Label axis".format( check_name, getattr(self, check_name).shape)) - @classmethod - def from_mapping(cls, mim): - """ - Creates a new BrainModel axis based on a CIFTI dataset - - Parameters - ---------- - mim : cifti2.Cifti2MatrixIndicesMap - - Returns - ------- - BrainModel - """ - nbm = np.sum([bm.index_count for bm in mim.brain_models]) - voxel = -np.ones((nbm, 3)) - vertex = -np.ones(nbm) - name = [] - - nvertices = {} - affine, shape = None, None - for bm in mim.brain_models: - index_end = bm.index_offset + bm.index_count - is_surface = bm.model_type == 'CIFTI_MODEL_TYPE_SURFACE' - name.extend([bm.brain_structure] * bm.index_count) - if is_surface: - vertex[bm.index_offset: index_end] = bm.vertex_indices - nvertices[bm.brain_structure] = bm.surface_number_of_vertices - else: - voxel[bm.index_offset: index_end, :] = bm.voxel_indices_ijk - if affine is None: - shape = mim.volume.volume_dimensions - affine = mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix - else: - if shape != mim.volume.volume_dimensions: - raise ValueError("All volume masks should be defined in the same volume") - if (affine != mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix).any(): - raise ValueError("All volume masks should have the same affine") - return cls(name, voxel, vertex, affine, shape, nvertices) - @classmethod def from_mask(cls, mask, name='other', affine=None): """ @@ -288,25 +245,44 @@ def from_surface(cls, vertices, nvertex, name='Cortex'): return cls(cifti_name, vertex=vertices, nvertices={cifti_name: nvertex}) - def get_element(self, index): + @classmethod + def from_mapping(cls, mim): """ - Describes a single element from the axis + Creates a new BrainModel axis based on a CIFTI dataset Parameters ---------- - index : int - Indexes the row/column of interest + mim : cifti2.Cifti2MatrixIndicesMap Returns ------- - tuple with 3 elements - - boolean, which is True if it is a surface element - - vertex index if it is a surface element, otherwise array with 3 voxel indices - - structure.BrainStructure object describing the brain structure the element was taken from + BrainModel """ - is_surface = self.name[index] in self.nvertices.keys() - name = 'vertex' if is_surface else 'voxel' - return is_surface, getattr(self, name)[index], self.name[index] + nbm = np.sum([bm.index_count for bm in mim.brain_models]) + voxel = -np.ones((nbm, 3)) + vertex = -np.ones(nbm) + name = [] + + nvertices = {} + affine, shape = None, None + for bm in mim.brain_models: + index_end = bm.index_offset + bm.index_count + is_surface = bm.model_type == 'CIFTI_MODEL_TYPE_SURFACE' + name.extend([bm.brain_structure] * bm.index_count) + if is_surface: + vertex[bm.index_offset: index_end] = bm.vertex_indices + nvertices[bm.brain_structure] = bm.surface_number_of_vertices + else: + voxel[bm.index_offset: index_end, :] = bm.voxel_indices_ijk + if affine is None: + shape = mim.volume.volume_dimensions + affine = mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix + else: + if shape != mim.volume.volume_dimensions: + raise ValueError("All volume masks should be defined in the same volume") + if (affine != mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix).any(): + raise ValueError("All volume masks should have the same affine") + return cls(name, voxel, vertex, affine, shape, nvertices) def to_mapping(self, dim): """ @@ -361,55 +337,6 @@ def iter_structures(self, ): start_name = self.name[idx_start] yield start_name, slice(idx_start, None), self[idx_start:] - @property - def affine(self, ): - """ - Affine of the volumetric image in which the greyordinate voxels were defined - """ - return self._affine - - @affine.setter - def affine(self, value): - if value is not None: - value = np.asarray(value) - if value.shape != (4, 4): - raise ValueError('Affine transformation should be a 4x4 array') - self._affine = value - - @property - def volume_shape(self, ): - """ - Shape of the volumetric image in which the greyordinate voxels were defined - """ - return self._volume_shape - - @volume_shape.setter - def volume_shape(self, value): - if value is not None: - value = tuple(value) - if len(value) != 3: - raise ValueError("Volume shape should be a tuple of length 3") - if not all(isinstance(v, int) for v in value): - raise ValueError("All elements of the volume shape should be integers") - self._volume_shape = value - - @property - def is_surface(self, ): - """ - (N, ) boolean array which is true for any element on the surface - """ - return np.vectorize(lambda name: name in self.nvertices.keys())(self.name) - - @property - def name(self, ): - """The brain structure to which the voxel/vertices of belong - """ - return self._name - - @name.setter - def name(self, values): - self._name = np.array([self.to_cifti_brain_structure_name(name) for name in values]) - @staticmethod def to_cifti_brain_structure_name(name): """ @@ -478,32 +405,63 @@ def to_cifti_brain_structure_name(name): (name, proposed_name)) return proposed_name - def __len__(self ): - return self.name.size + @property + def is_surface(self, ): + """ + (N, ) boolean array which is true for any element on the surface + """ + return np.vectorize(lambda name: name in self.nvertices.keys())(self.name) - def __getitem__(self, item): + _affine = None + + @property + def affine(self, ): """ - Extracts part of the brain structure + Affine of the volumetric image in which the greyordinate voxels were defined + """ + return self._affine - Parameters - ---------- - item : anything that can index a 1D array + @affine.setter + def affine(self, value): + if value is not None: + value = np.asarray(value) + if value.shape != (4, 4): + raise ValueError('Affine transformation should be a 4x4 array') + self._affine = value - Returns - ------- - If `item` is an integer returns a tuple with 3 elements: - - boolean, which is True if it is a surface element - - vertex index if it is a surface element, otherwise array with 3 voxel indices - - structure.BrainStructure object describing the brain structure the element was taken from + _volume_shape = None - Otherwise returns a new BrainModel + @property + def volume_shape(self, ): """ - if isinstance(item, int): - return self.get_element(item) - if isinstance(item, string_types): - raise IndexError("Can not index an Axis with a string (except for Parcels)") - return type(self)(self.name[item], self.voxel[item], self.vertex[item], - self.affine, self.volume_shape, self.nvertices) + Shape of the volumetric image in which the greyordinate voxels were defined + """ + return self._volume_shape + + @volume_shape.setter + def volume_shape(self, value): + if value is not None: + value = tuple(value) + if len(value) != 3: + raise ValueError("Volume shape should be a tuple of length 3") + if not all(isinstance(v, int) for v in value): + raise ValueError("All elements of the volume shape should be integers") + self._volume_shape = value + + _name = None + + @property + def name(self, ): + """The brain structure to which the voxel/vertices of belong + """ + return self._name + + @name.setter + def name(self, values): + self._name = np.array([self.to_cifti_brain_structure_name(name) for name in values]) + + def __len__(self ): + return self.name.size def __eq__(self, other): if not isinstance(other, BrainModel) or len(self) != len(other): @@ -555,6 +513,50 @@ def __add__(self, other): ) return NotImplemented + def __getitem__(self, item): + """ + Extracts part of the brain structure + + Parameters + ---------- + item : anything that can index a 1D array + + Returns + ------- + If `item` is an integer returns a tuple with 3 elements: + - boolean, which is True if it is a surface element + - vertex index if it is a surface element, otherwise array with 3 voxel indices + - structure.BrainStructure object describing the brain structure the element was taken from + + Otherwise returns a new BrainModel + """ + if isinstance(item, int): + return self.get_element(item) + if isinstance(item, string_types): + raise IndexError("Can not index an Axis with a string (except for Parcels)") + return type(self)(self.name[item], self.voxel[item], self.vertex[item], + self.affine, self.volume_shape, self.nvertices) + + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 3 elements + - boolean, which is True if it is a surface element + - vertex index if it is a surface element, otherwise array with 3 voxel indices + - structure.BrainStructure object describing the brain structure the element was taken from + """ + is_surface = self.name[index] in self.nvertices.keys() + name = 'vertex' if is_surface else 'voxel' + return is_surface, getattr(self, name)[index], self.name[index] + class Parcels(Axis): """ @@ -565,8 +567,6 @@ class Parcels(Axis): Individual parcels can be accessed based on their name, using ``parcel = parcel_axis[name]`` """ - _affine = None - _volume_shape = None def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvertices=None): """ @@ -633,7 +633,7 @@ def from_brain_models(cls, named_brain_models): else: if (affine != bm.affine).any() or (volume_shape != bm.volume_shape): raise ValueError( - "Can not combine brain models defined in different volumes into a single Parcel axis") + "Can not combine brain models defined in different volumes into a single Parcel axis") all_voxels.append(voxels) vertices = {} @@ -712,23 +712,7 @@ def to_mapping(self, dim): mim.append(element) return mim - def get_element(self, index): - """ - Describes a single element from the axis - - Parameters - ---------- - index : int - Indexes the row/column of interest - - Returns - ------- - tuple with 3 elements - - unicode name of the parcel - - (M, 3) int array with voxel indices - - Dict[String -> (K, ) int array] with vertex indices for a specific surface brain structure - """ - return self.name[index], self.voxels[index], self.vertices[index] + _affine = None @property def affine(self, ): @@ -742,6 +726,8 @@ def affine(self, value): raise ValueError('Affine transformation should be a 4x4 array') self._affine = value + _volume_shape = None + @property def volume_shape(self, ): return self._volume_shape @@ -757,25 +743,6 @@ def volume_shape(self, value): def __len__(self, ): return self.name.size - def __getitem__(self, item): - """ - Extracts subset of the axes based on the type of ``item``: - - `int`: 3-element tuple of (parcel name, parcel voxels, parcel vertices) - - `string`: 2-element tuple of (parcel voxels, parcel vertices - - other object that can index 1D arrays: new Parcel axis - """ - if isinstance(item, string_types): - idx = np.where(self.name == item)[0] - if len(idx) == 0: - raise IndexError("Parcel %s not found" % item) - if len(idx) > 1: - raise IndexError("Multiple get_parcels with name %s found" % item) - return self.voxels[idx[0]], self.vertices[idx[0]] - if isinstance(item, int): - return self.get_element(item) - return type(self)(self.name[item], self.voxels[item], self.vertices[item], - self.affine, self.volume_shape, self.nvertices) - def __eq__(self, other): if (type(self) != type(other) or len(self) != len(other) or (self.name != other.name).all() or self.nvertices != other.nvertices or @@ -783,8 +750,8 @@ def __eq__(self, other): return False if self.affine is not None: if ( other.affine is None or - abs(self.affine - other.affine).max() > 1e-8 or - self.volume_shape != other.volume_shape): + abs(self.affine - other.affine).max() > 1e-8 or + self.volume_shape != other.volume_shape): return False elif other.affine is not None: return False @@ -815,7 +782,7 @@ def __add__(self, other): else: affine, shape = self.affine, self.volume_shape if other.affine is not None and ((other.affine != affine).all() or - other.volume_shape != shape): + other.volume_shape != shape): raise ValueError("Trying to concatenate two Parcels defined in a different brain volume") nvertices = dict(self.nvertices) for name, value in other.nvertices.items(): @@ -831,12 +798,48 @@ def __add__(self, other): ) return NotImplemented + def __getitem__(self, item): + """ + Extracts subset of the axes based on the type of ``item``: + - `int`: 3-element tuple of (parcel name, parcel voxels, parcel vertices) + - `string`: 2-element tuple of (parcel voxels, parcel vertices + - other object that can index 1D arrays: new Parcel axis + """ + if isinstance(item, string_types): + idx = np.where(self.name == item)[0] + if len(idx) == 0: + raise IndexError("Parcel %s not found" % item) + if len(idx) > 1: + raise IndexError("Multiple get_parcels with name %s found" % item) + return self.voxels[idx[0]], self.vertices[idx[0]] + if isinstance(item, int): + return self.get_element(item) + return type(self)(self.name[item], self.voxels[item], self.vertices[item], + self.affine, self.volume_shape, self.nvertices) + + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 3 elements + - unicode name of the parcel + - (M, 3) int array with voxel indices + - Dict[String -> (K, ) int array] with vertex indices for a specific surface brain structure + """ + return self.name[index], self.voxels[index], self.vertices[index] + class Scalar(Axis): """ Along this axis of the CIFTI vector/matrix each row/column has been given a unique name and optionally metadata """ - _use_dtype = np.dtype([('name', 'U60'), ('meta', 'object')]) def __init__(self, name, meta=None): """ @@ -896,23 +899,6 @@ def to_mapping(self, dim): mim.append(named_map) return mim - def get_element(self, index): - """ - Describes a single element from the axis - - Parameters - ---------- - index : int - Indexes the row/column of interest - - Returns - ------- - tuple with 2 elements - - unicode name of the get_scalar - - dictionary with the element metadata - """ - return self.name[index], self.meta[index] - def to_label(self, labels): """ Creates a new Label axis based on the Scalar axis @@ -931,6 +917,9 @@ def to_label(self, labels): labels = [dict(labels) for _ in range(self.size)] return Label(self.name, labels, self.meta) + def __len__(self, ): + return self.name.size + def __eq__(self, other): """ Compares two Scalars @@ -948,9 +937,6 @@ def __eq__(self, other): return False return (self.name == other.name).all() and (self.meta == other.meta).all() - def __len__(self, ): - return self.name.size - def __add__(self, other): """ Concatenates two Scalars @@ -976,6 +962,23 @@ def __getitem__(self, item): return self.get_element(item) return type(self)(self.name[item], self.meta[item]) + def get_element(self, index): + """ + Describes a single element from the axis + + Parameters + ---------- + index : int + Indexes the row/column of interest + + Returns + ------- + tuple with 2 elements + - unicode name of the get_scalar + - dictionary with the element metadata + """ + return self.name[index], self.meta[index] + class Label(Axis): """ @@ -1005,9 +1008,6 @@ def __init__(self, name, label, meta): raise ValueError("Input {} has incorrect shape ({}) for Label axis".format( check_name, getattr(self, check_name).shape)) - def __len__(self, ): - return self.name.size - @classmethod def from_mapping(cls, mim): """ @@ -1050,23 +1050,25 @@ def to_mapping(self, dim): mim.append(named_map) return mim - def get_element(self, index): + def __len__(self, ): + return self.name.size + + def __eq__(self, other): """ - Describes a single element from the axis + Compares two Labels Parameters ---------- - index : int - Indexes the row/column of interest + other : Label + label axis to be compared Returns ------- - tuple with 2 elements - - unicode name of the get_scalar - - dictionary with the get_label table - - dictionary with the element metadata + bool : False if type, length or content do not match """ - return self.name[index], self.label[index], self.meta[index] + if not isinstance(other, Label) or self.size != other.size: + return False + return (self.name == other.name).all() and (self.meta == other.meta).all() and (self.label == other.label).all() def __add__(self, other): """ @@ -1089,27 +1091,28 @@ def __add__(self, other): np.append(self.meta, other.meta), ) - def __eq__(self, other): + def __getitem__(self, item): + if isinstance(item, int): + return self.get_element(item) + return type(self)(self.name[item], self.label[item], self.meta[item]) + + def get_element(self, index): """ - Compares two Labels + Describes a single element from the axis Parameters ---------- - other : Label - label axis to be compared + index : int + Indexes the row/column of interest Returns ------- - bool : False if type, length or content do not match + tuple with 2 elements + - unicode name of the get_scalar + - dictionary with the get_label table + - dictionary with the element metadata """ - if not isinstance(other, Label) or self.size != other.size: - return False - return (self.name == other.name).all() and (self.meta == other.meta).all() and (self.label == other.label).all() - - def __getitem__(self, item): - if isinstance(item, int): - return self.get_element(item) - return type(self)(self.name[item], self.label[item], self.meta[item]) + return self.name[index], self.label[index], self.meta[index] class Series(Axis): @@ -1128,7 +1131,6 @@ class Series(Axis): number of time points """ size = None - _unit = None def __init__(self, start, step, size, unit="SECOND"): """ @@ -1151,17 +1153,7 @@ def __init__(self, start, step, size, unit="SECOND"): self.size = size @property - def unit(self, ): - return self._unit - - @unit.setter - def unit(self, value): - if value.upper() not in ("SECOND", "HERTZ", "METER", "RADIAN"): - raise ValueError("Series unit should be one of ('second', 'hertz', 'meter', or 'radian'") - self._unit = value.upper() - - @property - def arr(self, ): + def time(self, ): return np.arange(self.size) * self.step + self.start @classmethod @@ -1202,6 +1194,18 @@ def to_mapping(self, dim): mim.series_unit = self.unit return mim + _unit = None + + @property + def unit(self, ): + return self._unit + + @unit.setter + def unit(self, value): + if value.upper() not in ("SECOND", "HERTZ", "METER", "RADIAN"): + raise ValueError("Series unit should be one of ('second', 'hertz', 'meter', or 'radian'") + self._unit = value.upper() + def extend(self, other_axis): """ Concatenates two get_series @@ -1223,6 +1227,45 @@ def extend(self, other_axis): raise ValueError('Can only concatenate Series with the same unit') return Series(self.start, self.step, self.size + other_axis.size, self.unit) + def __len__(self): + return self.size + + def __eq__(self, other): + """ + True if start, step, size, and unit are the same. + """ + return ( + isinstance(other, Series) and + self.start == other.start and + self.step == other.step and + self.size == other.size and + self.unit == other.unit + ) + + def __add__(self, other): + """ + Concatenates two Series + + Parameters + ---------- + other : Series + Time get_series to append at the end of the current time get_series. + Note that the starting time of the other time get_series is ignored. + + Returns + ------- + Series + New time get_series with the concatenation of the two + + Raises + ------ + ValueError + raised if the repetition time of the two time get_series is different + """ + if isinstance(other, Series): + return self.extend(other) + return NotImplemented + def __getitem__(self, item): if isinstance(item, slice): step = 1 if item.step is None else item.step @@ -1262,42 +1305,3 @@ def get_element(self, index): if index >= self.size: raise IndexError("index %i is out of range for get_series with size %i" % (index, self.size)) return self.start + self.step * index - - def __add__(self, other): - """ - Concatenates two Series - - Parameters - ---------- - other : Series - Time get_series to append at the end of the current time get_series. - Note that the starting time of the other time get_series is ignored. - - Returns - ------- - Series - New time get_series with the concatenation of the two - - Raises - ------ - ValueError - raised if the repetition time of the two time get_series is different - """ - if isinstance(other, Series): - return self.extend(other) - return NotImplemented - - def __eq__(self, other): - """ - True if start, step, size, and unit are the same. - """ - return ( - isinstance(other, Series) and - self.start == other.start and - self.step == other.step and - self.size == other.size and - self.unit == other.unit - ) - - def __len__(self): - return self.size diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index ee52cb160a..eccdf7aecc 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -231,14 +231,14 @@ def test_series(): assert sr[2].unit == 'SECOND' assert sr[3].unit == 'HERTZ' - assert (sr[0].arr == np.arange(4) * 10 + 3).all() - assert (sr[1].arr == np.arange(3) * 10 + 8).all() - assert (sr[2].arr == np.arange(4) * 2 + 3).all() - assert ((sr[0] + sr[1]).arr == np.arange(7) * 10 + 3).all() - assert ((sr[1] + sr[0]).arr == np.arange(7) * 10 + 8).all() - assert ((sr[1] + sr[0] + sr[0]).arr == np.arange(11) * 10 + 8).all() + assert (sr[0].time == np.arange(4) * 10 + 3).all() + assert (sr[1].time == np.arange(3) * 10 + 8).all() + assert (sr[2].time == np.arange(4) * 2 + 3).all() + assert ((sr[0] + sr[1]).time == np.arange(7) * 10 + 3).all() + assert ((sr[1] + sr[0]).time == np.arange(7) * 10 + 8).all() + assert ((sr[1] + sr[0] + sr[0]).time == np.arange(11) * 10 + 8).all() assert sr[1][2] == 28 - assert sr[1][-2] == sr[1].arr[-2] + assert sr[1][-2] == sr[1].time[-2] assert_raises(ValueError, lambda: sr[0] + sr[2]) assert_raises(ValueError, lambda: sr[2] + sr[1]) assert_raises(ValueError, lambda: sr[0] + sr[3]) @@ -246,16 +246,16 @@ def test_series(): assert_raises(ValueError, lambda: sr[3] + sr[2]) # test slicing - assert (sr[0][1:3].arr == sr[0].arr[1:3]).all() - assert (sr[0][1:].arr == sr[0].arr[1:]).all() - assert (sr[0][:-2].arr == sr[0].arr[:-2]).all() - assert (sr[0][1:-1].arr == sr[0].arr[1:-1]).all() - assert (sr[0][1:-1:2].arr == sr[0].arr[1:-1:2]).all() - assert (sr[0][::2].arr == sr[0].arr[::2]).all() - assert (sr[0][:10:2].arr == sr[0].arr[::2]).all() - assert (sr[0][10::-1].arr == sr[0].arr[::-1]).all() - assert (sr[0][3:1:-1].arr == sr[0].arr[3:1:-1]).all() - assert (sr[0][1:3:-1].arr == sr[0].arr[1:3:-1]).all() + assert (sr[0][1:3].time == sr[0].time[1:3]).all() + assert (sr[0][1:].time == sr[0].time[1:]).all() + assert (sr[0][:-2].time == sr[0].time[:-2]).all() + assert (sr[0][1:-1].time == sr[0].time[1:-1]).all() + assert (sr[0][1:-1:2].time == sr[0].time[1:-1:2]).all() + assert (sr[0][::2].time == sr[0].time[::2]).all() + assert (sr[0][:10:2].time == sr[0].time[::2]).all() + assert (sr[0][10::-1].time == sr[0].time[::-1]).all() + assert (sr[0][3:1:-1].time == sr[0].time[3:1:-1]).all() + assert (sr[0][1:3:-1].time == sr[0].time[1:3:-1]).all() def test_writing(): @@ -276,10 +276,6 @@ def test_common_interface(): assert axis1 == axis2 concatenated = axis1 + axis2 assert axis1 != concatenated - print(type(axis1)) - if isinstance(axis1, axes.Series): - print(concatenated.start, axis1.start) - print(concatenated[:axis1.size].start, axis1.start) assert axis1 == concatenated[:axis1.size] if isinstance(axis1, axes.Series): assert axis2 != concatenated[axis1.size:] diff --git a/nibabel/cifti2/tests/test_cifti2io_axes.py b/nibabel/cifti2/tests/test_cifti2io_axes.py index 4a3aa8d111..fee3605ce4 100644 --- a/nibabel/cifti2/tests/test_cifti2io_axes.py +++ b/nibabel/cifti2/tests/test_cifti2io_axes.py @@ -139,7 +139,7 @@ def test_read_conte69_dtseries(): assert axes[0].start == 0 assert axes[0].step == 1 assert axes[0].size == arr.shape[0] - assert (axes[0].arr == [0, 1]).all() + assert (axes[0].time == [0, 1]).all() check_Conte69(axes[1]) check_rewrite(arr, axes) @@ -169,7 +169,7 @@ def test_read_conte69_ptseries(): assert axes[0].start == 0 assert axes[0].step == 1 assert axes[0].size == arr.shape[0] - assert (axes[0].arr == [0, 1]).all() + assert (axes[0].time == [0, 1]).all() assert len(axes[1]) == 54 voxels, vertices = axes[1]['ER_FRB08'] From 52aff8ae37161685cba4483619c387e804b01ef1 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 14 Mar 2019 18:06:32 +0000 Subject: [PATCH 07/57] ENH: made it easier to create Label axis from constructor directly Removed Scalar.to_label, because it was no longer needed --- nibabel/cifti2/cifti2_axes.py | 34 ++++++++++--------------------- nibabel/cifti2/tests/test_axes.py | 2 +- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index e99507052a..ec8203b6ea 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -899,24 +899,6 @@ def to_mapping(self, dim): mim.append(named_map) return mim - def to_label(self, labels): - """ - Creates a new Label axis based on the Scalar axis - - Parameters - ---------- - labels : list[dict] or dict - mapping from integers to (name, (R, G, B, A)), where `name` is a string and R, G, B, and A are floats - between 0 and 1 giving the colour and alpha (transparency) - - Returns - ------- - Label - """ - if isinstance(labels, dict): - labels = [dict(labels) for _ in range(self.size)] - return Label(self.name, labels, self.meta) - def __len__(self, ): return self.name.size @@ -986,7 +968,7 @@ class Label(Axis): get_label table, and optionally metadata """ - def __init__(self, name, label, meta): + def __init__(self, name, label, meta=None): """ Creates a new Label axis from (name, meta-data) pairs @@ -994,14 +976,19 @@ def __init__(self, name, label, meta): ---------- name : np.ndarray (N, ) string array with the parcel names + label : np.ndarray + single dictionary or (N, ) object array with dictionaries mapping from integers to (name, (R, G, B, A)), + where name is a string and R, G, B, and A are floats between 0 and 1 giving the colour and alpha meta : np.ndarray (N, ) object array with a dictionary of metadata for each row/column - label : np.ndarray - (N, ) object array with dictionaries mapping integer values to get_label names and RGBA colors """ self.name = np.asarray(name, dtype='U') - self.meta = np.asarray(meta, dtype='object') + if isinstance(label, dict): + label = [label] * self.name.size self.label = np.asarray(label, dtype='object') + if meta is None: + meta = [{} for _ in range(self.name.size)] + self.meta = np.asarray(meta, dtype='object') for check_name in ('name', 'meta', 'label'): if getattr(self, check_name).shape != (self.size, ): @@ -1023,7 +1010,8 @@ def from_mapping(cls, mim): """ tables = [{key: (value.label, value.rgba) for key, value in nm.label_table.items()} for nm in mim.named_maps] - return Scalar.from_mapping(mim).to_label(tables) + rest = Scalar.from_mapping(mim) + return Label(rest.name, tables, rest.meta) def to_mapping(self, dim): """ diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index eccdf7aecc..4f7c9f1dea 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -64,7 +64,7 @@ def get_label(): ------- Label axis """ - return axes.Scalar(['one', 'two', 'three']).to_label(use_label) + return axes.Label(['one', 'two', 'three'], use_label) def get_series(): From dad74eee9c0fb3d5dcddc89d1d9312d684f9d06e Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 14 Mar 2019 18:27:26 +0000 Subject: [PATCH 08/57] REF: made flake8 happy --- nibabel/cifti2/cifti2_axes.py | 119 ++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 41 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index ec8203b6ea..97946e22cf 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -123,12 +123,14 @@ class BrainModel(Axis): (N, ) array with the vertex indices """ - def __init__(self, name, voxel=None, vertex=None, affine=None, volume_shape=None, nvertices=None): + def __init__(self, name, voxel=None, vertex=None, affine=None, + volume_shape=None, nvertices=None): """ - Creates a new BrainModel axis defining the vertices and voxels represented by each row/column + Creates a BrainModel axis defining the vertices and voxels represented by each row/column A more convenient way to create BrainModel axes is provided by the factory methods: - - `from_mask`: creates a surface or volumetric BrainModel axis from respectively 1D and 3D masks + - `from_mask`: creates surface or volumetric BrainModel axis from respectively + 1D or 3D masks - `from_surface`: creates a volumetric BrainModel axis The resulting BrainModel axes can be concatenated by adding them together. @@ -138,13 +140,16 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, volume_shape=None name : str or np.ndarray brain structure name or (N, ) array with the brain structure names voxel : np.ndarray - (N, 3) array with the voxel indices (can be omitted for CIFTI files only covering the surface) + (N, 3) array with the voxel indices (can be omitted for CIFTI files only + covering the surface) vertex : np.ndarray (N, ) array with the vertex indices (can be omitted for volumetric CIFTI files) affine : np.ndarray - (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only covering the surface) + (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only + covering the surface) volume_shape : Tuple[int, int, int] - shape of the volume in which the voxels were defined (not needed for CIFTI files only covering the surface) + shape of the volume in which the voxels were defined (not needed for CIFTI files only + covering the surface) nvertices : dict[String -> int] maps names of surface elements to integers (not needed for volumetric CIFTI files) """ @@ -157,7 +162,10 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, volume_shape=None nelements = len(voxel) self.voxel = np.asarray(voxel, dtype=int) - self.vertex = -np.ones(nelements, dtype=int) if vertex is None else np.asarray(vertex, dtype=int) + if vertex is None: + self.vertex = -np.ones(nelements, dtype=int) + else: + self.vertex = np.asarray(vertex, dtype=int) if isinstance(name, string_types): name = [self.to_cifti_brain_structure_name(name)] * self.vertex.size @@ -280,7 +288,10 @@ def from_mapping(cls, mim): else: if shape != mim.volume.volume_dimensions: raise ValueError("All volume masks should be defined in the same volume") - if (affine != mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix).any(): + if ( + affine != + mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix + ).any(): raise ValueError("All volume masks should have the same affine") return cls(name, voxel, vertex, affine, shape, nvertices) @@ -309,11 +320,13 @@ def to_mapping(self, dim): vertices = None nvertex = None if mim.volume is None: - affine = cifti2.Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ(-3, matrix=self.affine) + affine = cifti2.Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ(-3, self.affine) mim.volume = cifti2.Cifti2Volume(self.volume_shape, affine) - cifti_bm = cifti2.Cifti2BrainModel(to_slice.start, len(bm), - 'CIFTI_MODEL_TYPE_SURFACE' if is_surface else 'CIFTI_MODEL_TYPE_VOXELS', - name, nvertex, voxels, vertices) + cifti_bm = cifti2.Cifti2BrainModel( + to_slice.start, len(bm), + 'CIFTI_MODEL_TYPE_SURFACE' if is_surface else 'CIFTI_MODEL_TYPE_VOXELS', + name, nvertex, voxels, vertices + ) mim.append(cifti_bm) return mim @@ -401,8 +414,8 @@ def to_cifti_brain_structure_name(name): else: proposed_name = 'CIFTI_STRUCTURE_%s_%s' % (structure.upper(), orientation.upper()) if proposed_name not in cifti2.CIFTI_BRAIN_STRUCTURES: - raise ValueError('%s was interpreted as %s, which is not a valid CIFTI brain structure' % - (name, proposed_name)) + raise ValueError('%s was interpreted as %s, which is not a valid CIFTI brain structure' + % (name, proposed_name)) return proposed_name @property @@ -460,7 +473,7 @@ def name(self, ): def name(self, values): self._name = np.array([self.to_cifti_brain_structure_name(name) for name in values]) - def __len__(self ): + def __len__(self): return self.name.size def __eq__(self, other): @@ -496,14 +509,17 @@ def __add__(self, other): affine, shape = other.affine, other.volume_shape else: affine, shape = self.affine, self.volume_shape - if other.affine is not None and ((other.affine != affine).all() or - other.volume_shape != shape): - raise ValueError("Trying to concatenate two BrainModels defined in a different brain volume") + if other.affine is not None and ( + (other.affine != affine).all() or + other.volume_shape != shape + ): + raise ValueError("Trying to concatenate two BrainModels defined " + + "in a different brain volume") nvertices = dict(self.nvertices) for name, value in other.nvertices.items(): if name in nvertices.keys() and nvertices[name] != value: - raise ValueError("Trying to concatenate two BrainModels with inconsistent number of vertices for %s" - % name) + raise ValueError("Trying to concatenate two BrainModels with inconsistent " + + "number of vertices for %s" % name) nvertices[name] = value return type(self)( np.append(self.name, other.name), @@ -570,7 +586,7 @@ class Parcels(Axis): def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvertices=None): """ - Creates a new BrainModel axis defining the vertices and voxels represented by each row/column + Creates a Parcels axis defining the vertices and voxels represented by each row/column Parameters ---------- @@ -581,9 +597,11 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert vertices : np.ndarray (N, ) object array each containing a sequence of vertices affine : np.ndarray - (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only covering the surface) + (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only + covering the surface) volume_shape : Tuple[int, int, int] - shape of the volume in which the voxels were defined (not needed for CIFTI files only covering the surface) + shape of the volume in which the voxels were defined (not needed for CIFTI files only + covering the surface) nvertices : dict[String -> int] maps names of surface elements to integers (not needed for volumetric CIFTI files) """ @@ -632,15 +650,16 @@ def from_brain_models(cls, named_brain_models): volume_shape = bm.volume_shape else: if (affine != bm.affine).any() or (volume_shape != bm.volume_shape): - raise ValueError( - "Can not combine brain models defined in different volumes into a single Parcel axis") + raise ValueError("Can not combine brain models defined in different " + + "volumes into a single Parcel axis") all_voxels.append(voxels) vertices = {} for name, _, bm_part in bm.iter_structures(): if name in bm.nvertices.keys(): if name in nvertices.keys() and nvertices[name] != bm.nvertices[name]: - raise ValueError("Got multiple conflicting number of vertices for surface structure %s" % name) + raise ValueError("Got multiple conflicting number of " + + "vertices for surface structure %s" % name) nvertices[name] = bm.nvertices[name] vertices[name] = bm_part.vertex all_vertices.append(vertices) @@ -665,7 +684,9 @@ def from_mapping(cls, mim): all_vertices = np.zeros(nparcels, dtype='object') volume_shape = None if mim.volume is None else mim.volume.volume_dimensions - affine = None if mim.volume is None else mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix + affine = None + if mim.volume is not None: + affine = mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix nvertices = {} for surface in mim.surfaces: nvertices[surface.brain_structure] = surface.surface_number_of_vertices @@ -679,7 +700,8 @@ def from_mapping(cls, mim): name = vertex.brain_structure vertices[vertex.brain_structure] = np.array(vertex) if name not in nvertices.keys(): - raise ValueError("Number of vertices for surface structure %s not defined" % name) + raise ValueError("Number of vertices for surface structure %s not defined" % + name) all_voxels[idx_parcel] = voxels all_vertices[idx_parcel] = vertices all_names.append(parcel.name) @@ -749,9 +771,11 @@ def __eq__(self, other): any((vox1 != vox2).any() for vox1, vox2 in zip(self.voxels, other.voxels))): return False if self.affine is not None: - if ( other.affine is None or + if ( + other.affine is None or abs(self.affine - other.affine).max() > 1e-8 or - self.volume_shape != other.volume_shape): + self.volume_shape != other.volume_shape + ): return False elif other.affine is not None: return False @@ -783,11 +807,13 @@ def __add__(self, other): affine, shape = self.affine, self.volume_shape if other.affine is not None and ((other.affine != affine).all() or other.volume_shape != shape): - raise ValueError("Trying to concatenate two Parcels defined in a different brain volume") + raise ValueError("Trying to concatenate two Parcels defined " + + "in a different brain volume") nvertices = dict(self.nvertices) for name, value in other.nvertices.items(): if name in nvertices.keys() and nvertices[name] != value: - raise ValueError("Trying to concatenate two Parcels with inconsistent number of vertices for %s" + raise ValueError("Trying to concatenate two Parcels with inconsistent " + + "number of vertices for %s" % name) nvertices[name] = value return type(self)( @@ -838,7 +864,8 @@ def get_element(self, index): class Scalar(Axis): """ - Along this axis of the CIFTI vector/matrix each row/column has been given a unique name and optionally metadata + Along this axis of the CIFTI vector/matrix each row/column has been given + a unique name and optionally metadata """ def __init__(self, name, meta=None): @@ -850,7 +877,8 @@ def __init__(self, name, meta=None): name : np.ndarray (N, ) string array with the parcel names meta : np.ndarray - (N, ) object array with a dictionary of metadata for each row/column. Defaults to empty dictionary + (N, ) object array with a dictionary of metadata for each row/column. + Defaults to empty dictionary """ self.name = np.asarray(name, dtype='U') if meta is None: @@ -977,8 +1005,9 @@ def __init__(self, name, label, meta=None): name : np.ndarray (N, ) string array with the parcel names label : np.ndarray - single dictionary or (N, ) object array with dictionaries mapping from integers to (name, (R, G, B, A)), - where name is a string and R, G, B, and A are floats between 0 and 1 giving the colour and alpha + single dictionary or (N, ) object array with dictionaries mapping + from integers to (name, (R, G, B, A)), where name is a string and R, G, B, and A are + floats between 0 and 1 giving the colour and alpha (i.e., transparency) meta : np.ndarray (N, ) object array with a dictionary of metadata for each row/column """ @@ -1056,7 +1085,11 @@ def __eq__(self, other): """ if not isinstance(other, Label) or self.size != other.size: return False - return (self.name == other.name).all() and (self.meta == other.meta).all() and (self.label == other.label).all() + return ( + (self.name == other.name).all() and + (self.meta == other.meta).all() and + (self.label == other.label).all() + ) def __add__(self, other): """ @@ -1191,7 +1224,8 @@ def unit(self, ): @unit.setter def unit(self, value): if value.upper() not in ("SECOND", "HERTZ", "METER", "RADIAN"): - raise ValueError("Series unit should be one of ('second', 'hertz', 'meter', or 'radian'") + raise ValueError("Series unit should be one of " + + "('second', 'hertz', 'meter', or 'radian'") self._unit = value.upper() def extend(self, other_axis): @@ -1270,10 +1304,12 @@ def __getitem__(self, item): nelements = (idx_end - idx_start) // step if nelements < 0: nelements = 0 - return Series(idx_start * self.step + self.start, self.step * step, nelements, self.unit) + return Series(idx_start * self.step + self.start, self.step * step, + nelements, self.unit) elif isinstance(item, int): return self.get_element(item) - raise IndexError('Series can only be indexed with integers or slices without breaking the regular structure') + raise IndexError('Series can only be indexed with integers or slices ' + + 'without breaking the regular structure') def get_element(self, index): """ @@ -1291,5 +1327,6 @@ def get_element(self, index): if index < 0: index = self.size + index if index >= self.size: - raise IndexError("index %i is out of range for get_series with size %i" % (index, self.size)) + raise IndexError("index %i is out of range for get_series with size %i" % + (index, self.size)) return self.start + self.step * index From a0e8ff38b126ab5dd80d9240443d73d96a5a1862 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 14 Mar 2019 20:23:38 +0000 Subject: [PATCH 09/57] BF: fixed abstract class for python2 --- nibabel/cifti2/cifti2_axes.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 97946e22cf..c0d8dd86d5 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -1,8 +1,8 @@ import numpy as np from nibabel.cifti2 import cifti2 -from six import string_types +from six import string_types, add_metaclass from operator import xor -from abc import ABC, abstractmethod +import abc def from_mapping(mim): @@ -53,7 +53,8 @@ def to_header(axes): return cifti2.Cifti2Header(matrix) -class Axis(ABC): +@add_metaclass(abc.ABCMeta) +class Axis(object): """ Abstract class for any object describing the rows or columns of a CIFTI vector/matrix @@ -64,11 +65,11 @@ class Axis(ABC): def size(self, ): return len(self) - @abstractmethod + @abc.abstractmethod def __len__(self): pass - @abstractmethod + @abc.abstractmethod def __eq__(self, other): """ Compares whether two Axes are equal @@ -84,7 +85,7 @@ def __eq__(self, other): """ pass - @abstractmethod + @abc.abstractmethod def __add__(self, other): """ Concatenates two Axes of the same type @@ -100,7 +101,7 @@ def __add__(self, other): """ pass - @abstractmethod + @abc.abstractmethod def __getitem__(self, item): """ Extracts definition of single row/column or new Axis describing a subset of the rows/columns From 420dacbfb204510bb57647d3e39cae01de005ca1 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 15 Mar 2019 09:58:00 +0000 Subject: [PATCH 10/57] BF: allow any integer type, not just int --- nibabel/cifti2/cifti2_axes.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index c0d8dd86d5..9c01e2abd1 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -1,6 +1,6 @@ import numpy as np from nibabel.cifti2 import cifti2 -from six import string_types, add_metaclass +from six import string_types, add_metaclass, integer_types from operator import xor import abc @@ -156,7 +156,7 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, """ if voxel is None: if vertex is None: - raise ValueError("Voxel and vertex indices not defined") + raise ValueError("At least one of voxel or vertex indices should be defined") nelements = len(vertex) self.voxel = -np.ones((nelements, 3), dtype=int) else: @@ -458,7 +458,7 @@ def volume_shape(self, value): value = tuple(value) if len(value) != 3: raise ValueError("Volume shape should be a tuple of length 3") - if not all(isinstance(v, int) for v in value): + if not all(isinstance(v, integer_types) for v in value): raise ValueError("All elements of the volume shape should be integers") self._volume_shape = value @@ -547,7 +547,7 @@ def __getitem__(self, item): Otherwise returns a new BrainModel """ - if isinstance(item, int): + if isinstance(item, integer_types): return self.get_element(item) if isinstance(item, string_types): raise IndexError("Can not index an Axis with a string (except for Parcels)") @@ -839,7 +839,7 @@ def __getitem__(self, item): if len(idx) > 1: raise IndexError("Multiple get_parcels with name %s found" % item) return self.voxels[idx[0]], self.vertices[idx[0]] - if isinstance(item, int): + if isinstance(item, integer_types): return self.get_element(item) return type(self)(self.name[item], self.voxels[item], self.vertices[item], self.affine, self.volume_shape, self.nvertices) @@ -969,7 +969,7 @@ def __add__(self, other): ) def __getitem__(self, item): - if isinstance(item, int): + if isinstance(item, integer_types): return self.get_element(item) return type(self)(self.name[item], self.meta[item]) @@ -1114,7 +1114,7 @@ def __add__(self, other): ) def __getitem__(self, item): - if isinstance(item, int): + if isinstance(item, integer_types): return self.get_element(item) return type(self)(self.name[item], self.label[item], self.meta[item]) @@ -1307,7 +1307,7 @@ def __getitem__(self, item): nelements = 0 return Series(idx_start * self.step + self.start, self.step * step, nelements, self.unit) - elif isinstance(item, int): + elif isinstance(item, integer_types): return self.get_element(item) raise IndexError('Series can only be indexed with integers or slices ' + 'without breaking the regular structure') From 29448f134698a49fd2a61b7ee6400799c35a2eba Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 15 Mar 2019 10:59:56 +0000 Subject: [PATCH 11/57] DOC: fixed many issues with the documentation Visually checked that the compiled documentation looks okay (not great) --- nibabel/cifti2/cifti2_axes.py | 89 +++++++++++++++++------------------ 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 9c01e2abd1..2d0949bc5f 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -113,33 +113,25 @@ class BrainModel(Axis): Each row/column in the CIFTI vector/matrix represents a single vertex or voxel This Axis describes which vertex/voxel is represented by each row/column. - - Attributes - ---------- - name : np.ndarray - (N, ) array with the brain structure objects - voxel : np.ndarray - (N, 3) array with the voxel indices - vertex : np.ndarray - (N, ) array with the vertex indices """ def __init__(self, name, voxel=None, vertex=None, affine=None, volume_shape=None, nvertices=None): """ - Creates a BrainModel axis defining the vertices and voxels represented by each row/column + New BrainModel axes can be constructed by passing on the greyordinate brain-structure + names and voxel/vertex indices to the constructor or by one of the + factory methods: - A more convenient way to create BrainModel axes is provided by the factory methods: - - `from_mask`: creates surface or volumetric BrainModel axis from respectively + - :py:meth:`~BrainModel.from_mask`: creates surface or volumetric BrainModel axis from respectively 1D or 3D masks - - `from_surface`: creates a volumetric BrainModel axis + - :py:meth:`~BrainModel.from_surface`: creates a volumetric BrainModel axis The resulting BrainModel axes can be concatenated by adding them together. Parameters ---------- name : str or np.ndarray - brain structure name or (N, ) array with the brain structure names + brain structure name or (N, ) string array with the brain structure names voxel : np.ndarray (N, 3) array with the voxel indices (can be omitted for CIFTI files only covering the surface) @@ -337,7 +329,7 @@ def iter_structures(self, ): Yields ------ - tuple with + tuple with 3 elements: - CIFTI brain structure name - slice to select the data associated with the brain structure from the tensor - brain model covering that specific brain structure @@ -357,14 +349,16 @@ def to_cifti_brain_structure_name(name): Attempts to convert the name of an anatomical region in a format recognized by CIFTI This function returns: - * the name if it is in the CIFTI format already - * if the name is a tuple the first element is assumed to be the structure name while - the second is assumed to be the hemisphere (left, right or both). The latter will default - to both. - * names like left_cortex, cortex_left, LeftCortex, or CortexLeft will be converted to - CIFTI_STRUCTURE_CORTEX_LEFT - see ``nibabel.cifti2.tests.test_name`` for examples of which conversions are possible + - the name if it is in the CIFTI format already + - if the name is a tuple the first element is assumed to be the structure name while + the second is assumed to be the hemisphere (left, right or both). The latter will default + to both. + - names like left_cortex, cortex_left, LeftCortex, or CortexLeft will be converted to + CIFTI_STRUCTURE_CORTEX_LEFT + + see :py:func:`nibabel.cifti2.tests.test_name` for examples of + which conversions are possible Parameters ---------- @@ -587,8 +581,6 @@ class Parcels(Axis): def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvertices=None): """ - Creates a Parcels axis defining the vertices and voxels represented by each row/column - Parameters ---------- name : np.ndarray @@ -618,7 +610,7 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert for check_name in ('name', 'voxels', 'vertices'): if getattr(self, check_name).shape != (self.size, ): - raise ValueError("Input {} has incorrect shape ({}) for Label axis".format( + raise ValueError("Input {} has incorrect shape ({}) for Parcel axis".format( check_name, getattr(self, check_name).shape)) @classmethod @@ -710,7 +702,7 @@ def from_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the get_parcels to a MatrixIndicesMap for storage in CIFTI format + Converts the Parsel to a MatrixIndicesMap for storage in CIFTI format Parameters ---------- @@ -739,6 +731,9 @@ def to_mapping(self, dim): @property def affine(self, ): + """ + Affine of the volumetric image in which the greyordinate voxels were defined + """ return self._affine @affine.setter @@ -753,6 +748,9 @@ def affine(self, value): @property def volume_shape(self, ): + """ + Shape of the volumetric image in which the greyordinate voxels were defined + """ return self._volume_shape @volume_shape.setter @@ -828,6 +826,7 @@ def __add__(self, other): def __getitem__(self, item): """ Extracts subset of the axes based on the type of ``item``: + - `int`: 3-element tuple of (parcel name, parcel voxels, parcel vertices) - `string`: 2-element tuple of (parcel voxels, parcel vertices - other object that can index 1D arrays: new Parcel axis @@ -837,7 +836,7 @@ def __getitem__(self, item): if len(idx) == 0: raise IndexError("Parcel %s not found" % item) if len(idx) > 1: - raise IndexError("Multiple get_parcels with name %s found" % item) + raise IndexError("Multiple parcels with name %s found" % item) return self.voxels[idx[0]], self.vertices[idx[0]] if isinstance(item, integer_types): return self.get_element(item) @@ -871,8 +870,6 @@ class Scalar(Axis): def __init__(self, name, meta=None): """ - Creates a new Scalar axis from (name, meta-data) pairs - Parameters ---------- name : np.ndarray @@ -894,7 +891,7 @@ def __init__(self, name, meta=None): @classmethod def from_mapping(cls, mim): """ - Creates a new get_scalar axis based on a CIFTI dataset + Creates a new Scalar axis based on a CIFTI dataset Parameters ---------- @@ -985,7 +982,7 @@ def get_element(self, index): Returns ------- tuple with 2 elements - - unicode name of the get_scalar + - unicode name of the row/column - dictionary with the element metadata """ return self.name[index], self.meta[index] @@ -993,14 +990,14 @@ def get_element(self, index): class Label(Axis): """ + Defines CIFTI axis for label array. + Along this axis of the CIFTI vector/matrix each row/column has been given a unique name, - get_label table, and optionally metadata + label table, and optionally metadata """ def __init__(self, name, label, meta=None): """ - Creates a new Label axis from (name, meta-data) pairs - Parameters ---------- name : np.ndarray @@ -1028,7 +1025,7 @@ def __init__(self, name, label, meta=None): @classmethod def from_mapping(cls, mim): """ - Creates a new get_scalar axis based on a CIFTI dataset + Creates a new Label axis based on a CIFTI dataset Parameters ---------- @@ -1036,7 +1033,7 @@ def from_mapping(cls, mim): Returns ------- - Scalar + Label """ tables = [{key: (value.label, value.rgba) for key, value in nm.label_table.items()} for nm in mim.named_maps] @@ -1099,7 +1096,7 @@ def __add__(self, other): Parameters ---------- other : Label - scalar axis to be appended to the current one + label axis to be appended to the current one Returns ------- @@ -1130,8 +1127,8 @@ def get_element(self, index): Returns ------- tuple with 2 elements - - unicode name of the get_scalar - - dictionary with the get_label table + - unicode name of the row/column + - dictionary with the label table - dictionary with the element metadata """ return self.name[index], self.label[index], self.meta[index] @@ -1197,7 +1194,7 @@ def from_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the get_series to a MatrixIndicesMap for storage in CIFTI format + Converts the Series to a MatrixIndicesMap for storage in CIFTI format Parameters ---------- @@ -1231,7 +1228,7 @@ def unit(self, value): def extend(self, other_axis): """ - Concatenates two get_series + Concatenates two Series Note: this will ignore the start point of the other axis @@ -1272,18 +1269,18 @@ def __add__(self, other): Parameters ---------- other : Series - Time get_series to append at the end of the current time get_series. - Note that the starting time of the other time get_series is ignored. + Time Series to append at the end of the current time Series. + Note that the starting time of the other time Series is ignored. Returns ------- Series - New time get_series with the concatenation of the two + New time Series with the concatenation of the two Raises ------ ValueError - raised if the repetition time of the two time get_series is different + raised if the repetition time of the two time Series is different """ if isinstance(other, Series): return self.extend(other) @@ -1328,6 +1325,6 @@ def get_element(self, index): if index < 0: index = self.size + index if index >= self.size: - raise IndexError("index %i is out of range for get_series with size %i" % + raise IndexError("index %i is out of range for Series with size %i" % (index, self.size)) return self.start + self.step * index From 56842680b3d171ac3f1f674f6e4d373517c1746c Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 15 Mar 2019 11:30:50 +0000 Subject: [PATCH 12/57] RF: made flake8 happy again --- nibabel/cifti2/cifti2_axes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 2d0949bc5f..65fd10a744 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -122,9 +122,9 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, names and voxel/vertex indices to the constructor or by one of the factory methods: - - :py:meth:`~BrainModel.from_mask`: creates surface or volumetric BrainModel axis from respectively - 1D or 3D masks - - :py:meth:`~BrainModel.from_surface`: creates a volumetric BrainModel axis + - :py:meth:`~BrainModel.from_mask`: creates surface or volumetric BrainModel axis + from respectively 1D or 3D masks + - :py:meth:`~BrainModel.from_surface`: creates a surface BrainModel axis The resulting BrainModel axes can be concatenated by adding them together. From fbd28dc20543b2c275fe508abe1c218b9970c94a Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Tue, 19 Mar 2019 21:40:23 +0000 Subject: [PATCH 13/57] Apply suggestions from code review Co-Authored-By: MichielCottaar --- nibabel/cifti2/cifti2_axes.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 65fd10a744..af4524eaf6 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -62,7 +62,7 @@ class Axis(object): """ @property - def size(self, ): + def size(self): return len(self) @abc.abstractmethod @@ -150,13 +150,13 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, if vertex is None: raise ValueError("At least one of voxel or vertex indices should be defined") nelements = len(vertex) - self.voxel = -np.ones((nelements, 3), dtype=int) + self.voxel = np.full((nelements, 3), fill_value=-1, dtype=int) else: nelements = len(voxel) self.voxel = np.asarray(voxel, dtype=int) if vertex is None: - self.vertex = -np.ones(nelements, dtype=int) + self.vertex = np.full(nelements, fill_value=-1, dtype=int) else: self.vertex = np.asarray(vertex, dtype=int) @@ -173,16 +173,17 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, if name not in self.name: del self.nvertices[name] - if self.is_surface.all(): + is_surface = self.is_surface + if is_surface.all(): self.affine = None self.volume_shape = None else: self.affine = affine self.volume_shape = volume_shape - if (self.vertex[self.is_surface] < 0).any(): + if np.any(self.vertex[is_surface] < 0): raise ValueError('Undefined vertex indices found for surface elements') - if (self.voxel[~self.is_surface] < 0).any(): + if np.any(self.voxel[~is_surface] < 0): raise ValueError('Undefined voxel indices found for volumetric elements') for check_name in ('name', 'voxel', 'vertex'): @@ -259,9 +260,9 @@ def from_mapping(cls, mim): ------- BrainModel """ - nbm = np.sum([bm.index_count for bm in mim.brain_models]) - voxel = -np.ones((nbm, 3)) - vertex = -np.ones(nbm) + nbm = sum(bm.index_count for bm in mim.brain_models) + voxel = np.full((nbm, 3), fill_value=-1, dtype=int) + vertex = np.full(nbm, fill_value=-1, dtype=int) name = [] nvertices = {} @@ -323,7 +324,7 @@ def to_mapping(self, dim): mim.append(cifti_bm) return mim - def iter_structures(self, ): + def iter_structures(self): """ Iterates over all brain structures in the order that they appear along the axis @@ -414,7 +415,7 @@ def to_cifti_brain_structure_name(name): return proposed_name @property - def is_surface(self, ): + def is_surface(self): """ (N, ) boolean array which is true for any element on the surface """ @@ -499,7 +500,8 @@ def __add__(self, other): ------- BrainModel """ - if isinstance(other, BrainModel): + if not isinstance(other, BrainModel): + return NotImplemented if self.affine is None: affine, shape = other.affine, other.volume_shape else: @@ -516,7 +518,7 @@ def __add__(self, other): raise ValueError("Trying to concatenate two BrainModels with inconsistent " + "number of vertices for %s" % name) nvertices[name] = value - return type(self)( + return self.__class__( np.append(self.name, other.name), np.concatenate((self.voxel, other.voxel), 0), np.append(self.vertex, other.vertex), @@ -545,7 +547,7 @@ def __getitem__(self, item): return self.get_element(item) if isinstance(item, string_types): raise IndexError("Can not index an Axis with a string (except for Parcels)") - return type(self)(self.name[item], self.voxel[item], self.vertex[item], + return self.__class__(self.name[item], self.voxel[item], self.vertex[item], self.affine, self.volume_shape, self.nvertices) def get_element(self, index): @@ -565,8 +567,8 @@ def get_element(self, index): - structure.BrainStructure object describing the brain structure the element was taken from """ is_surface = self.name[index] in self.nvertices.keys() - name = 'vertex' if is_surface else 'voxel' - return is_surface, getattr(self, name)[index], self.name[index] + struct = self.vertex if is_surface else self.voxel + return is_surface, struct[index], self.name[index] class Parcels(Axis): From c33e0d17dad2711ddc89b49e4e8eb3dd9d62640b Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 19 Mar 2019 22:02:15 +0000 Subject: [PATCH 14/57] Added other reviewer suggestions Still need to to the tests of the validation code --- nibabel/cifti2/cifti2_axes.py | 70 ++++++++++++++++------------------- 1 file changed, 31 insertions(+), 39 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index af4524eaf6..a1c0de7165 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -279,14 +279,6 @@ def from_mapping(cls, mim): if affine is None: shape = mim.volume.volume_dimensions affine = mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix - else: - if shape != mim.volume.volume_dimensions: - raise ValueError("All volume masks should be defined in the same volume") - if ( - affine != - mim.volume.transformation_matrix_voxel_indices_ijk_to_xyz.matrix - ).any(): - raise ValueError("All volume masks should have the same affine") return cls(name, voxel, vertex, affine, shape, nvertices) def to_mapping(self, dim): @@ -478,13 +470,13 @@ def __eq__(self, other): if xor(self.affine is None, other.affine is None): return False return ( - ((self.affine is None and other.affine is None) or - (abs(self.affine - other.affine).max() < 1e-8 and - self.volume_shape == other.volume_shape)) and - (self.nvertices == other.nvertices) and - (self.name == other.name).all() and - (self.voxel[~self.is_surface] == other.voxel[~other.is_surface]).all() and - (self.vertex[~self.is_surface] == other.vertex[~other.is_surface]).all() + (self.affine is None or + np.allclose(self.affine, other.affine) and + self.volume_shape == other.volume_shape) and + self.nvertices == other.nvertices and + np.array_equal(self.name, other.name) and + np.array_equal(self.voxel[~self.is_surface], other.voxel[~other.is_surface]) and + np.array_equal(self.vertex[self.is_surface], other.vertex[other.is_surface]) ) def __add__(self, other): @@ -502,29 +494,29 @@ def __add__(self, other): """ if not isinstance(other, BrainModel): return NotImplemented - if self.affine is None: - affine, shape = other.affine, other.volume_shape - else: - affine, shape = self.affine, self.volume_shape - if other.affine is not None and ( - (other.affine != affine).all() or - other.volume_shape != shape - ): - raise ValueError("Trying to concatenate two BrainModels defined " + - "in a different brain volume") - nvertices = dict(self.nvertices) - for name, value in other.nvertices.items(): - if name in nvertices.keys() and nvertices[name] != value: - raise ValueError("Trying to concatenate two BrainModels with inconsistent " + - "number of vertices for %s" % name) - nvertices[name] = value - return self.__class__( - np.append(self.name, other.name), - np.concatenate((self.voxel, other.voxel), 0), - np.append(self.vertex, other.vertex), - affine, shape, nvertices - ) - return NotImplemented + if self.affine is None: + affine, shape = other.affine, other.volume_shape + else: + affine, shape = self.affine, self.volume_shape + if other.affine is not None and ( + not np.allclose(other.affine, affine) or + other.volume_shape != shape + ): + raise ValueError("Trying to concatenate two BrainModels defined " + + "in a different brain volume") + + nvertices = dict(self.nvertices) + for name, value in other.nvertices.items(): + if name in nvertices.keys() and nvertices[name] != value: + raise ValueError("Trying to concatenate two BrainModels with inconsistent " + + "number of vertices for %s" % name) + nvertices[name] = value + return self.__class__( + np.append(self.name, other.name), + np.concatenate((self.voxel, other.voxel), 0), + np.append(self.vertex, other.vertex), + affine, shape, nvertices + ) def __getitem__(self, item): """ @@ -548,7 +540,7 @@ def __getitem__(self, item): if isinstance(item, string_types): raise IndexError("Can not index an Axis with a string (except for Parcels)") return self.__class__(self.name[item], self.voxel[item], self.vertex[item], - self.affine, self.volume_shape, self.nvertices) + self.affine, self.volume_shape, self.nvertices) def get_element(self, index): """ From bc2064eb08c3208cd9542f521f6c4d89c80c520e Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 19 Mar 2019 22:03:15 +0000 Subject: [PATCH 15/57] RF: remove spurious ', ' from method definition --- nibabel/cifti2/cifti2_axes.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index a1c0de7165..357a6db910 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -416,7 +416,7 @@ def is_surface(self): _affine = None @property - def affine(self, ): + def affine(self): """ Affine of the volumetric image in which the greyordinate voxels were defined """ @@ -433,7 +433,7 @@ def affine(self, value): _volume_shape = None @property - def volume_shape(self, ): + def volume_shape(self): """ Shape of the volumetric image in which the greyordinate voxels were defined """ @@ -452,7 +452,7 @@ def volume_shape(self, value): _name = None @property - def name(self, ): + def name(self): """The brain structure to which the voxel/vertices of belong """ return self._name @@ -724,7 +724,7 @@ def to_mapping(self, dim): _affine = None @property - def affine(self, ): + def affine(self): """ Affine of the volumetric image in which the greyordinate voxels were defined """ @@ -741,7 +741,7 @@ def affine(self, value): _volume_shape = None @property - def volume_shape(self, ): + def volume_shape(self): """ Shape of the volumetric image in which the greyordinate voxels were defined """ @@ -755,7 +755,7 @@ def volume_shape(self, value): raise ValueError("Volume shape should be a tuple of length 3") self._volume_shape = value - def __len__(self, ): + def __len__(self): return self.name.size def __eq__(self, other): @@ -919,7 +919,7 @@ def to_mapping(self, dim): mim.append(named_map) return mim - def __len__(self, ): + def __len__(self): return self.name.size def __eq__(self, other): @@ -1059,7 +1059,7 @@ def to_mapping(self, dim): mim.append(named_map) return mim - def __len__(self, ): + def __len__(self): return self.name.size def __eq__(self, other): @@ -1166,7 +1166,7 @@ def __init__(self, start, step, size, unit="SECOND"): self.size = size @property - def time(self, ): + def time(self): return np.arange(self.size) * self.step + self.start @classmethod @@ -1210,7 +1210,7 @@ def to_mapping(self, dim): _unit = None @property - def unit(self, ): + def unit(self): return self._unit @unit.setter From 4c0f389130ad96ea61e72a27f0ca61d77d917c5f Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 19 Mar 2019 22:06:21 +0000 Subject: [PATCH 16/57] Replaced asarray with asanyarray Did not actually test array subclasses to see if it leads to errors down the line... --- nibabel/cifti2/cifti2_axes.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 357a6db910..92cd0203b7 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -153,16 +153,16 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, self.voxel = np.full((nelements, 3), fill_value=-1, dtype=int) else: nelements = len(voxel) - self.voxel = np.asarray(voxel, dtype=int) + self.voxel = np.asanyarray(voxel, dtype=int) if vertex is None: self.vertex = np.full(nelements, fill_value=-1, dtype=int) else: - self.vertex = np.asarray(vertex, dtype=int) + self.vertex = np.asanyarray(vertex, dtype=int) if isinstance(name, string_types): name = [self.to_cifti_brain_structure_name(name)] * self.vertex.size - self.name = np.asarray(name, dtype='U') + self.name = np.asanyarray(name, dtype='U') if nvertices is None: self.nvertices = {} @@ -214,7 +214,9 @@ def from_mask(cls, mask, name='other', affine=None): """ if affine is None: affine = np.eye(4) - if np.asarray(affine).shape != (4, 4): + else: + affine = np.asanyarray(affine) + if affine.shape != (4, 4): raise ValueError("Affine transformation should be a 4x4 array or None, not %r" % affine) if mask.ndim == 1: return cls.from_surface(np.where(mask != 0)[0], mask.size, name=name) @@ -425,7 +427,7 @@ def affine(self): @affine.setter def affine(self, value): if value is not None: - value = np.asarray(value) + value = np.asanyarray(value) if value.shape != (4, 4): raise ValueError('Affine transformation should be a 4x4 array') self._affine = value @@ -592,9 +594,9 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert nvertices : dict[String -> int] maps names of surface elements to integers (not needed for volumetric CIFTI files) """ - self.name = np.asarray(name, dtype='U') - self.voxels = np.asarray(voxels, dtype='object') - self.vertices = np.asarray(vertices, dtype='object') + self.name = np.asanyarray(name, dtype='U') + self.voxels = np.asanyarray(voxels, dtype='object') + self.vertices = np.asanyarray(vertices, dtype='object') self.affine = affine self.volume_shape = volume_shape if nvertices is None: @@ -733,7 +735,7 @@ def affine(self): @affine.setter def affine(self, value): if value is not None: - value = np.asarray(value) + value = np.asanyarray(value) if value.shape != (4, 4): raise ValueError('Affine transformation should be a 4x4 array') self._affine = value @@ -872,10 +874,10 @@ def __init__(self, name, meta=None): (N, ) object array with a dictionary of metadata for each row/column. Defaults to empty dictionary """ - self.name = np.asarray(name, dtype='U') + self.name = np.asanyarray(name, dtype='U') if meta is None: meta = [{} for _ in range(self.name.size)] - self.meta = np.asarray(meta, dtype='object') + self.meta = np.asanyarray(meta, dtype='object') for check_name in ('name', 'meta'): if getattr(self, check_name).shape != (self.size, ): @@ -1003,13 +1005,13 @@ def __init__(self, name, label, meta=None): meta : np.ndarray (N, ) object array with a dictionary of metadata for each row/column """ - self.name = np.asarray(name, dtype='U') + self.name = np.asanyarray(name, dtype='U') if isinstance(label, dict): label = [label] * self.name.size - self.label = np.asarray(label, dtype='object') + self.label = np.asanyarray(label, dtype='object') if meta is None: meta = [{} for _ in range(self.name.size)] - self.meta = np.asarray(meta, dtype='object') + self.meta = np.asanyarray(meta, dtype='object') for check_name in ('name', 'meta', 'label'): if getattr(self, check_name).shape != (self.size, ): From 72c089a7f6b03f15e9cafbca4e1c4807b916e473 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 19 Mar 2019 22:10:28 +0000 Subject: [PATCH 17/57] reverse guard for incorrect type when concatenating parcels --- nibabel/cifti2/cifti2_axes.py | 48 +++++++++++++++++------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 92cd0203b7..7a1e45fa66 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -788,36 +788,36 @@ def __add__(self, other): Parameters ---------- - other : Parcel + other : Parcels parcel to be appended to the current one Returns ------- Parcel """ - if type(self) == type(other): - if self.affine is None: - affine, shape = other.affine, other.volume_shape - else: - affine, shape = self.affine, self.volume_shape - if other.affine is not None and ((other.affine != affine).all() or - other.volume_shape != shape): - raise ValueError("Trying to concatenate two Parcels defined " + - "in a different brain volume") - nvertices = dict(self.nvertices) - for name, value in other.nvertices.items(): - if name in nvertices.keys() and nvertices[name] != value: - raise ValueError("Trying to concatenate two Parcels with inconsistent " + - "number of vertices for %s" - % name) - nvertices[name] = value - return type(self)( - np.append(self.name, other.name), - np.append(self.voxels, other.voxels), - np.append(self.vertices, other.vertices), - affine, shape, nvertices - ) - return NotImplemented + if not isinstance(other, Parcels): + return NotImplemented + if self.affine is None: + affine, shape = other.affine, other.volume_shape + else: + affine, shape = self.affine, self.volume_shape + if other.affine is not None and ((other.affine != affine).all() or + other.volume_shape != shape): + raise ValueError("Trying to concatenate two Parcels defined " + + "in a different brain volume") + nvertices = dict(self.nvertices) + for name, value in other.nvertices.items(): + if name in nvertices.keys() and nvertices[name] != value: + raise ValueError("Trying to concatenate two Parcels with inconsistent " + + "number of vertices for %s" + % name) + nvertices[name] = value + return type(self)( + np.append(self.name, other.name), + np.append(self.voxels, other.voxels), + np.append(self.vertices, other.vertices), + affine, shape, nvertices + ) def __getitem__(self, item): """ From e8bcaba0195c6040075b2f48fb8805276936877e Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 19 Mar 2019 22:12:34 +0000 Subject: [PATCH 18/57] Replaces type(self) with self.__class__ --- nibabel/cifti2/cifti2_axes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 7a1e45fa66..b3cfd80023 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -761,7 +761,7 @@ def __len__(self): return self.name.size def __eq__(self, other): - if (type(self) != type(other) or len(self) != len(other) or + if (self.__class__ != other.__class__ or len(self) != len(other) or (self.name != other.name).all() or self.nvertices != other.nvertices or any((vox1 != vox2).any() for vox1, vox2 in zip(self.voxels, other.voxels))): return False @@ -812,7 +812,7 @@ def __add__(self, other): "number of vertices for %s" % name) nvertices[name] = value - return type(self)( + return self.__class__( np.append(self.name, other.name), np.append(self.voxels, other.voxels), np.append(self.vertices, other.vertices), @@ -964,7 +964,7 @@ def __add__(self, other): def __getitem__(self, item): if isinstance(item, integer_types): return self.get_element(item) - return type(self)(self.name[item], self.meta[item]) + return self.__class__(self.name[item], self.meta[item]) def get_element(self, index): """ @@ -1109,7 +1109,7 @@ def __add__(self, other): def __getitem__(self, item): if isinstance(item, integer_types): return self.get_element(item) - return type(self)(self.name[item], self.label[item], self.meta[item]) + return self.__class__(self.name[item], self.label[item], self.meta[item]) def get_element(self, index): """ From ac2618b1d3bf46e151e98a11a47161e55224a1bd Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 19 Mar 2019 23:24:43 +0000 Subject: [PATCH 19/57] Tests many more fail conditions and edge cases --- nibabel/cifti2/cifti2_axes.py | 32 +++++-- nibabel/cifti2/tests/test_axes.py | 142 ++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 9 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index b3cfd80023..e2ed0b68cf 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -167,7 +167,8 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, if nvertices is None: self.nvertices = {} else: - self.nvertices = dict(nvertices) + self.nvertices = {self.to_cifti_brain_structure_name(name): number + for name, number in nvertices.items()} for name in list(self.nvertices.keys()): if name not in self.name: @@ -178,6 +179,9 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, self.affine = None self.volume_shape = None else: + if affine is None or volume_shape is None: + raise ValueError("Affine and volume shape should be defined " + + "for BrainModel containing voxels") self.affine = affine self.volume_shape = volume_shape @@ -189,7 +193,7 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, for check_name in ('name', 'voxel', 'vertex'): shape = (self.size, 3) if check_name == 'voxel' else (self.size, ) if getattr(self, check_name).shape != shape: - raise ValueError("Input {} has incorrect shape ({}) for Label axis".format( + raise ValueError("Input {} has incorrect shape ({}) for BrainModel axis".format( check_name, getattr(self, check_name).shape)) @classmethod @@ -577,14 +581,20 @@ class Parcels(Axis): def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvertices=None): """ + Use of this constructor is not recommended. New Parcels axes can be constructed more easily + from a sequence of BrainModel axes using :py:meth:`~Parcels.from_brain_models` + Parameters ---------- name : np.ndarray (N, ) string array with the parcel names voxels : np.ndarray - (N, ) object array each containing a sequence of voxels + (N, ) object array each containing a sequence of voxels. + For each parcel the voxels are represented by a (M, 3) index array vertices : np.ndarray - (N, ) object array each containing a sequence of vertices + (N, ) object array each containing a sequence of vertices. + For each parcel the vertices are represented by a mapping from brain structure name to + (M, ) index array affine : np.ndarray (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only covering the surface) @@ -602,7 +612,8 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert if nvertices is None: self.nvertices = {} else: - self.nvertices = dict(nvertices) + self.nvertices = {BrainModel.to_cifti_brain_structure_name(name): number + for name, number in nvertices.items()} for check_name in ('name', 'voxels', 'vertices'): if getattr(self, check_name).shape != (self.size, ): @@ -623,11 +634,12 @@ def from_brain_models(cls, named_brain_models): ------- Parcels """ + nparcels = len(named_brain_models) affine = None volume_shape = None all_names = [] - all_voxels = [] - all_vertices = [] + all_voxels = np.zeros(nparcels, dtype='object') + all_vertices = np.zeros(nparcels, dtype='object') nvertices = {} for idx_parcel, (parcel_name, bm) in enumerate(named_brain_models): all_names.append(parcel_name) @@ -641,7 +653,7 @@ def from_brain_models(cls, named_brain_models): if (affine != bm.affine).any() or (volume_shape != bm.volume_shape): raise ValueError("Can not combine brain models defined in different " + "volumes into a single Parcel axis") - all_voxels.append(voxels) + all_voxels[idx_parcel] = voxels vertices = {} for name, _, bm_part in bm.iter_structures(): @@ -651,7 +663,7 @@ def from_brain_models(cls, named_brain_models): "vertices for surface structure %s" % name) nvertices[name] = bm.nvertices[name] vertices[name] = bm_part.vertex - all_vertices.append(vertices) + all_vertices[idx_parcel] = vertices return Parcels(all_names, all_voxels, all_vertices, affine, volume_shape, nvertices) @classmethod @@ -755,6 +767,8 @@ def volume_shape(self, value): value = tuple(value) if len(value) != 3: raise ValueError("Volume shape should be a tuple of length 3") + if not all(isinstance(v, integer_types) for v in value): + raise ValueError("All elements of the volume shape should be integers") self._volume_shape = value def __len__(self): diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index 4f7c9f1dea..c38b5401f5 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -152,6 +152,101 @@ def test_brain_models(): assert len(structures) == 4 assert len(structures[-1][2]) == 8 + # break brain model + bmt.affine = np.eye(4) + with assert_raises(ValueError): + bmt.affine = np.eye(3) + with assert_raises(ValueError): + bmt.affine = np.eye(4).flatten() + + bmt.volume_shape = (5, 3, 1) + with assert_raises(ValueError): + bmt.volume_shape = (5., 3, 1) + with assert_raises(ValueError): + bmt.volume_shape = (5, 3, 1, 4) + + with assert_raises(IndexError): + bmt['thalamus_left'] + + # Test the constructor + bm_vox = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + print(bm_vox.name) + assert np.all(bm_vox.name == np.full(5, 'CIFTI_STRUCTURE_THALAMUS_LEFT')) + assert np.all(bm_vox.vertex == np.full(5, -1)) + assert np.all(bm_vox.voxel == np.full((5, 3), 1)) + with assert_raises(ValueError): + # no volume shape + axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4)) + with assert_raises(ValueError): + # no affine + axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), volume_shape=(2, 3, 4)) + with assert_raises(ValueError): + # incorrect name + axes.BrainModel('random_name', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + with assert_raises(ValueError): + # negative voxel indices + axes.BrainModel('thalamus_left', voxel=-np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + with assert_raises(ValueError): + # no voxels or vertices + axes.BrainModel('thalamus_left', affine=np.eye(4), volume_shape=(2, 3, 4)) + with assert_raises(ValueError): + # incorrect voxel shape + axes.BrainModel('thalamus_left', voxel=np.ones((5, 2), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + + bm_vertex = axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + assert np.all(bm_vertex.name == np.full(5, 'CIFTI_STRUCTURE_CORTEX_LEFT')) + assert np.all(bm_vertex.vertex == np.full(5, 1)) + assert np.all(bm_vertex.voxel == np.full((5, 3), -1)) + with assert_raises(ValueError): + axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int)) + with assert_raises(ValueError): + axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_right': 20}) + with assert_raises(ValueError): + axes.BrainModel('cortex_left', vertex=-np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + + # test from_mask errors + with assert_raises(ValueError): + # affine should be 4x4 matrix + axes.BrainModel.from_mask(np.arange(5) > 2, affine=np.ones(5)) + with assert_raises(ValueError): + # only 1D or 3D masks accepted + axes.BrainModel.from_mask(np.ones((5, 3))) + + # tests error in adding together or combining as Parcels + bm_vox = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4), volume_shape=(2, 3, 4)) + bm_vox + bm_vox + assert (bm_vertex + bm_vox)[:bm_vertex.size] == bm_vertex + assert (bm_vox + bm_vertex)[:bm_vox.size] == bm_vox + for bm_added in (bm_vox + bm_vertex, bm_vertex + bm_vox): + assert bm_added.nvertices == bm_vertex.nvertices + assert np.all(bm_added.affine == bm_vox.affine) + assert bm_added.volume_shape == bm_vox.volume_shape + + axes.Parcels.from_brain_models([('a', bm_vox), ('b', bm_vox)]) + with assert_raises(Exception): + bm_vox + get_label() + + bm_other_shape = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4), volume_shape=(4, 3, 4)) + with assert_raises(ValueError): + bm_vox + bm_other_shape + with assert_raises(ValueError): + axes.Parcels.from_brain_models([('a', bm_vox), ('b', bm_other_shape)]) + bm_other_affine = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4) * 2, volume_shape=(2, 3, 4)) + with assert_raises(ValueError): + bm_vox + bm_other_affine + with assert_raises(ValueError): + axes.Parcels.from_brain_models([('a', bm_vox), ('b', bm_other_affine)]) + + bm_vertex = axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + bm_other_number = axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 30}) + with assert_raises(ValueError): + bm_vertex + bm_other_number + with assert_raises(ValueError): + axes.Parcels.from_brain_models([('a', bm_vertex), ('b', bm_other_number)]) + def test_parcels(): """ @@ -159,13 +254,16 @@ def test_parcels(): """ prc = get_parcels() assert isinstance(prc, axes.Parcels) + assert prc[0] == ('mixed', ) + prc['mixed'] assert prc['mixed'][0].shape == (3, 3) assert len(prc['mixed'][1]) == 1 assert prc['mixed'][1]['CIFTI_STRUCTURE_CORTEX_LEFT'].shape == (3, ) + assert prc[1] == ('volume', ) + prc['volume'] assert prc['volume'][0].shape == (4, 3) assert len(prc['volume'][1]) == 0 + assert prc[2] == ('surface', ) + prc['surface'] assert prc['surface'][0].shape == (0, 3) assert len(prc['surface'][1]) == 1 assert prc['surface'][1]['CIFTI_STRUCTURE_CORTEX'].shape == (4, ) @@ -182,6 +280,45 @@ def test_parcels(): assert len(prc2[3:]['mixed'][1]) == 1 assert prc2[3:]['mixed'][1]['CIFTI_STRUCTURE_CORTEX_LEFT'].shape == (3, ) + with assert_raises(IndexError): + prc['non_existent'] + + prc['surface'] + with assert_raises(IndexError): + # parcel exists twice + prc2['surface'] + + # break parcels + prc.affine = np.eye(4) + with assert_raises(ValueError): + prc.affine = np.eye(3) + with assert_raises(ValueError): + prc.affine = np.eye(4).flatten() + + prc.volume_shape = (5, 3, 1) + with assert_raises(ValueError): + prc.volume_shape = (5., 3, 1) + with assert_raises(ValueError): + prc.volume_shape = (5, 3, 1, 4) + + # break adding of parcels + with assert_raises(Exception): + prc + get_label() + + prc = get_parcels() + other_prc = get_parcels() + prc + other_prc + + other_prc = get_parcels() + other_prc.affine = np.eye(4) * 2 + with assert_raises(ValueError): + prc + other_prc + + other_prc = get_parcels() + other_prc.volume_shape = (20, 3, 4) + with assert_raises(ValueError): + prc + other_prc + def test_scalar(): """ @@ -230,7 +367,12 @@ def test_series(): assert sr[1].unit == 'SECOND' assert sr[2].unit == 'SECOND' assert sr[3].unit == 'HERTZ' + sr[0].unit = 'hertz' + assert sr[0].unit == 'HERTZ' + with assert_raises(ValueError): + sr[0].unit = 'non_existent' + sr = list(get_series()) assert (sr[0].time == np.arange(4) * 10 + 3).all() assert (sr[1].time == np.arange(3) * 10 + 8).all() assert (sr[2].time == np.arange(4) * 2 + 3).all() From e00143b2eebe8faeca772587f8e11701fb8364db Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 19 Mar 2019 20:55:46 -0400 Subject: [PATCH 20/57] MNT: Bump minimum numpy version to 1.8 --- .travis.yml | 8 ++++---- doc/source/installation.rst | 2 +- nibabel/info.py | 2 +- requirements.txt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 367a105045..fe2cdf0b97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,11 +33,11 @@ matrix: # Absolute minimum dependencies - python: 2.7 env: - - DEPENDS="numpy==1.7.1" + - DEPENDS="numpy==1.8" # Absolute minimum dependencies - python: 2.7 env: - - DEPENDS="numpy==1.7.1" + - DEPENDS="numpy==1.8" - CHECK_TYPE="import" # Absolute minimum dependencies plus oldest MPL # Check these against: @@ -46,11 +46,11 @@ matrix: # requirements.txt - python: 2.7 env: - - DEPENDS="numpy==1.7.1 matplotlib==1.3.1" + - DEPENDS="numpy==1.8 matplotlib==1.3.1" # Minimum pydicom dependency - python: 2.7 env: - - DEPENDS="numpy==1.7.1 pydicom==0.9.9 pillow==2.6" + - DEPENDS="numpy==1.8 pydicom==0.9.9 pillow==2.6" # pydicom master branch - python: 3.5 env: diff --git a/doc/source/installation.rst b/doc/source/installation.rst index ec942bd043..c853de9619 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -87,7 +87,7 @@ Requirements .travis.yml * Python_ 2.7, or >= 3.4 -* NumPy_ 1.7.1 or greater +* NumPy_ 1.8 or greater * Six_ 1.3 or greater * SciPy_ (optional, for full SPM-ANALYZE support) * PyDICOM_ 0.9.9 or greater (optional, for DICOM support) diff --git a/nibabel/info.py b/nibabel/info.py index abe71735cd..36437ff5b9 100644 --- a/nibabel/info.py +++ b/nibabel/info.py @@ -186,7 +186,7 @@ def cmp_pkg_version(version_str, pkg_version_str=__version__): # doc/source/installation.rst # requirements.txt # .travis.yml -NUMPY_MIN_VERSION = '1.7.1' +NUMPY_MIN_VERSION = '1.8' PYDICOM_MIN_VERSION = '0.9.9' SIX_MIN_VERSION = '1.3' diff --git a/requirements.txt b/requirements.txt index 061fa37bef..6299333665 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,4 @@ # doc/source/installation.rst six>=1.3 -numpy>=1.7.1 +numpy>=1.8 From 436231c94f7cf86d731e096dbb80bbe6522a702c Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Fri, 22 Mar 2019 10:55:27 +0000 Subject: [PATCH 21/57] Apply suggestions from code review Co-Authored-By: MichielCottaar --- nibabel/cifti2/cifti2_axes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index e2ed0b68cf..18122904bd 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -1,5 +1,5 @@ import numpy as np -from nibabel.cifti2 import cifti2 +from . import cifti2 from six import string_types, add_metaclass, integer_types from operator import xor import abc @@ -650,7 +650,7 @@ def from_brain_models(cls, named_brain_models): affine = bm.affine volume_shape = bm.volume_shape else: - if (affine != bm.affine).any() or (volume_shape != bm.volume_shape): + if not np.allclose(affine, bm.affine) or (volume_shape != bm.volume_shape): raise ValueError("Can not combine brain models defined in different " + "volumes into a single Parcel axis") all_voxels[idx_parcel] = voxels @@ -695,7 +695,7 @@ def from_mapping(cls, mim): nvoxels = 0 if parcel.voxel_indices_ijk is None else len(parcel.voxel_indices_ijk) voxels = np.zeros((nvoxels, 3), dtype='i4') if nvoxels != 0: - voxels[()] = parcel.voxel_indices_ijk + voxels[:] = parcel.voxel_indices_ijk vertices = {} for vertex in parcel.vertices: name = vertex.brain_structure @@ -710,7 +710,7 @@ def from_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the Parsel to a MatrixIndicesMap for storage in CIFTI format + Converts the Parcel to a MatrixIndicesMap for storage in CIFTI format Parameters ---------- @@ -776,7 +776,7 @@ def __len__(self): def __eq__(self, other): if (self.__class__ != other.__class__ or len(self) != len(other) or - (self.name != other.name).all() or self.nvertices != other.nvertices or + not np.array_equal(self.name, other.name) or self.nvertices != other.nvertices or any((vox1 != vox2).any() for vox1, vox2 in zip(self.voxels, other.voxels))): return False if self.affine is not None: @@ -815,14 +815,14 @@ def __add__(self, other): affine, shape = other.affine, other.volume_shape else: affine, shape = self.affine, self.volume_shape - if other.affine is not None and ((other.affine != affine).all() or + if other.affine is not None and (not np.allclose(other.affine, affine) or other.volume_shape != shape): raise ValueError("Trying to concatenate two Parcels defined " + "in a different brain volume") nvertices = dict(self.nvertices) for name, value in other.nvertices.items(): if name in nvertices.keys() and nvertices[name] != value: - raise ValueError("Trying to concatenate two Parcels with inconsistent " + + raise ValueError("Trying to concatenate two Parcels with inconsistent " "number of vertices for %s" % name) nvertices[name] = value @@ -850,7 +850,7 @@ def __getitem__(self, item): return self.voxels[idx[0]], self.vertices[idx[0]] if isinstance(item, integer_types): return self.get_element(item) - return type(self)(self.name[item], self.voxels[item], self.vertices[item], + return self.__class__(self.name[item], self.voxels[item], self.vertices[item], self.affine, self.volume_shape, self.nvertices) def get_element(self, index): @@ -1334,7 +1334,7 @@ def get_element(self, index): """ if index < 0: index = self.size + index - if index >= self.size: + if index >= self.size or index < 0: raise IndexError("index %i is out of range for Series with size %i" % (index, self.size)) return self.start + self.step * index From d1bc7fea3b5fc2f7f957ff132c26a60ebece52c7 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 22 Mar 2019 10:57:46 +0000 Subject: [PATCH 22/57] BF: only set idx_start to size - 1 for negative step --- nibabel/cifti2/cifti2_axes.py | 2 +- nibabel/cifti2/tests/test_axes.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 18122904bd..a8ba5bce92 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -1305,7 +1305,7 @@ def __getitem__(self, item): idx_end = ((-1 if step < 0 else self.size) if item.stop is None else (item.stop if item.stop >= 0 else self.size + item.stop)) - if idx_start > self.size: + if idx_start > self.size and step < 0: idx_start = self.size - 1 if idx_end > self.size: idx_end = self.size diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index c38b5401f5..71b068779e 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -395,7 +395,9 @@ def test_series(): assert (sr[0][1:-1:2].time == sr[0].time[1:-1:2]).all() assert (sr[0][::2].time == sr[0].time[::2]).all() assert (sr[0][:10:2].time == sr[0].time[::2]).all() - assert (sr[0][10::-1].time == sr[0].time[::-1]).all() + assert (sr[0][10:].time == sr[0].time[10:]).all() + assert (sr[0][10:12].time == sr[0].time[10:12]).all() + assert (sr[0][10::-1].time == sr[0].time[10::-1]).all() assert (sr[0][3:1:-1].time == sr[0].time[3:1:-1]).all() assert (sr[0][1:3:-1].time == sr[0].time[1:3:-1]).all() From 949da0ef3ef73e02a0efe87d0c966c1dcc875a45 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 22 Mar 2019 11:28:44 +0000 Subject: [PATCH 23/57] TEST: add tests for Axis __eq__ methods Tests changing any individual part of the Axis leads to inequality. Some bugs found when adding tests --- nibabel/cifti2/cifti2_axes.py | 8 +- nibabel/cifti2/tests/test_axes.py | 148 ++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 4 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index a8ba5bce92..ce7c1062a8 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -777,12 +777,12 @@ def __len__(self): def __eq__(self, other): if (self.__class__ != other.__class__ or len(self) != len(other) or not np.array_equal(self.name, other.name) or self.nvertices != other.nvertices or - any((vox1 != vox2).any() for vox1, vox2 in zip(self.voxels, other.voxels))): + any(not np.array_equal(vox1, vox2) for vox1, vox2 in zip(self.voxels, other.voxels))): return False if self.affine is not None: if ( other.affine is None or - abs(self.affine - other.affine).max() > 1e-8 or + not np.allclose(self.affine, other.affine) or self.volume_shape != other.volume_shape ): return False @@ -792,7 +792,7 @@ def __eq__(self, other): if len(vert1) != len(vert2): return False for name in vert1.keys(): - if name not in vert2 or (vert1[name] != vert2[name]).all(): + if name not in vert2 or not np.array_equal(vert1[name], vert2[name]): return False return True @@ -1021,7 +1021,7 @@ def __init__(self, name, label, meta=None): """ self.name = np.asanyarray(name, dtype='U') if isinstance(label, dict): - label = [label] * self.name.size + label = [label.copy() for _ in range(self.name.size)] self.label = np.asanyarray(label, dtype='object') if meta is None: meta = [{} for _ in range(self.name.size)] diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index 71b068779e..452957128e 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -2,6 +2,7 @@ from nose.tools import assert_raises from .test_cifti2io_axes import check_rewrite import nibabel.cifti2.cifti2_axes as axes +from copy import deepcopy rand_affine = np.random.randn(4, 4) @@ -247,6 +248,55 @@ def test_brain_models(): with assert_raises(ValueError): axes.Parcels.from_brain_models([('a', bm_vertex), ('b', bm_other_number)]) + # test equalities + bm_vox = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4), volume_shape=(2, 3, 4)) + bm_other = deepcopy(bm_vox) + assert bm_vox == bm_other + bm_other.voxel[1, 0] = 0 + assert bm_vox != bm_other + + bm_other = deepcopy(bm_vox) + bm_other.vertex[1] = 10 + assert bm_vox == bm_other, 'vertices are ignored in volumetric BrainModel' + + bm_other = deepcopy(bm_vox) + bm_other.name[1] = 'BRAIN_STRUCTURE_OTHER' + assert bm_vox != bm_other + + bm_other = deepcopy(bm_vox) + bm_other.affine[0, 0] = 10 + assert bm_vox != bm_other + + bm_other = deepcopy(bm_vox) + bm_other.volume_shape = (10, 3, 4) + assert bm_vox != bm_other + + bm_vertex = axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + bm_other = deepcopy(bm_vertex) + assert bm_vertex == bm_other + bm_other.voxel[1, 0] = 0 + assert bm_vertex == bm_other, 'voxels are ignored in surface BrainModel' + + bm_other = deepcopy(bm_vertex) + bm_other.vertex[1] = 10 + assert bm_vertex != bm_other + + bm_other = deepcopy(bm_vertex) + bm_other.name[1] = 'BRAIN_STRUCTURE_CORTEX_RIGHT' + assert bm_vertex != bm_other + + bm_other = deepcopy(bm_vertex) + bm_other.nvertices['BRAIN_STRUCTURE_CORTEX_LEFT'] = 50 + assert bm_vertex != bm_other + + bm_other = deepcopy(bm_vertex) + bm_other.nvertices['BRAIN_STRUCTURE_CORTEX_RIGHT'] = 20 + assert bm_vertex != bm_other + + assert bm_vox != get_parcels() + assert bm_vertex != get_parcels() + def test_parcels(): """ @@ -319,6 +369,45 @@ def test_parcels(): with assert_raises(ValueError): prc + other_prc + # test parcel equalities + prc = get_parcels() + assert prc != get_scalar() + + prc_other = deepcopy(prc) + assert prc == prc_other + assert prc != prc_other[:2] + assert prc == prc_other[:] + prc_other.affine[0, 0] = 10 + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.volume_shape = (10, 3, 4) + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.nvertices['CIFTI_STRUCTURE_CORTEX_LEFT'] = 80 + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.voxels[0] = np.ones((2, 3), dtype='i4') + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.voxels[0] = prc_other.voxels * 2 + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.vertices[0]['CIFTI_STRUCTURE_CORTEX_LEFT'] = np.ones((8, ), dtype='i4') + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.vertices[0]['CIFTI_STRUCTURE_CORTEX_LEFT'] *= 2 + assert prc != prc_other + + prc_other = deepcopy(prc) + prc_other.name[0] = 'new_name' + assert prc != prc_other + def test_scalar(): """ @@ -337,6 +426,25 @@ def test_scalar(): assert sc2[:3] == sc assert sc2[3:] == sc + sc.meta[1]['a'] = 3 + assert 'a' not in sc.meta + + # test equalities + assert sc != get_parcels() + + sc_other = deepcopy(sc) + assert sc == sc_other + assert sc != sc_other[:2] + assert sc == sc_other[:] + sc_other.name[0] = 'new_name' + assert sc != sc_other + + sc_other = deepcopy(sc) + sc_other.meta[0]['new_key'] = 'new_entry' + assert sc != sc_other + sc.meta[0]['new_key'] = 'new_entry' + assert sc == sc_other + def test_label(): """ @@ -357,6 +465,30 @@ def test_label(): assert lab2[:3] == lab assert lab2[3:] == lab + # test equalities + lab = get_label() + assert lab != get_scalar() + + other_lab = deepcopy(lab) + assert lab != other_lab[:2] + assert lab == other_lab[:] + other_lab.name[0] = 'new_name' + assert lab != other_lab + + other_lab = deepcopy(lab) + other_lab.meta[0]['new_key'] = 'new_item' + assert 'new_key' not in other_lab.meta[1] + assert lab != other_lab + lab.meta[0]['new_key'] = 'new_item' + assert lab == other_lab + + other_lab = deepcopy(lab) + other_lab.label[0][20] = ('new_label', (0, 0, 0, 1)) + assert lab != other_lab + assert 20 not in other_lab.label[1] + lab.label[0][20] = ('new_label', (0, 0, 0, 1)) + assert lab == other_lab + def test_series(): """ @@ -401,6 +533,22 @@ def test_series(): assert (sr[0][3:1:-1].time == sr[0].time[3:1:-1]).all() assert (sr[0][1:3:-1].time == sr[0].time[1:3:-1]).all() + # test_equalities + sr = next(get_series()) + assert sr != sr[:2] + assert sr == sr[:] + + for key, value in ( + ('start', 20), + ('step', 7), + ('size', 14), + ('unit', 'HERTZ'), + ): + sr_other = deepcopy(sr) + assert sr == sr_other + setattr(sr_other, key, value) + assert sr != sr_other + def test_writing(): """ From ac0258c60f8125a38dc418fc7e601072f40cf00b Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 22 Mar 2019 11:31:08 +0000 Subject: [PATCH 24/57] DOC: removed Series attribute list as it is already listed in the __init__ --- nibabel/cifti2/cifti2_axes.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index ce7c1062a8..2411f9ac0f 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -1149,15 +1149,6 @@ class Series(Axis): Along this axis of the CIFTI vector/matrix the rows/columns increase monotonously in time This Axis describes the time point of each row/column. - - Attributes - ---------- - start : float - starting time point - step : float - sampling time (TR) - size : int - number of time points """ size = None @@ -1168,11 +1159,11 @@ def __init__(self, start, step, size, unit="SECOND"): Parameters ---------- start : float - Time of the first datapoint - step : float - Step size between data points + starting time point + step : float + sampling time (TR) size : int - Number of data points + number of time points unit : str Unit of the step size (one of 'second', 'hertz', 'meter', or 'radian') """ From 6726b00d6c7cfac050b8ece4170216742e9bc374 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 22 Mar 2019 11:33:42 +0000 Subject: [PATCH 25/57] RF: removed separate `extend` method Functionality is still available by adding two Series together --- nibabel/cifti2/cifti2_axes.py | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 2411f9ac0f..80220f459e 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -1227,27 +1227,6 @@ def unit(self, value): "('second', 'hertz', 'meter', or 'radian'") self._unit = value.upper() - def extend(self, other_axis): - """ - Concatenates two Series - - Note: this will ignore the start point of the other axis - - Parameters - ---------- - other_axis : Series - other axis - - Returns - ------- - Series - """ - if other_axis.step != self.step: - raise ValueError('Can only concatenate Series with the same step size') - if other_axis.unit != self.unit: - raise ValueError('Can only concatenate Series with the same unit') - return Series(self.start, self.step, self.size + other_axis.size, self.unit) - def __len__(self): return self.size @@ -1284,7 +1263,11 @@ def __add__(self, other): raised if the repetition time of the two time Series is different """ if isinstance(other, Series): - return self.extend(other) + if other.step != self.step: + raise ValueError('Can only concatenate Series with the same step size') + if other.unit != self.unit: + raise ValueError('Can only concatenate Series with the same unit') + return Series(self.start, self.step, self.size + other.size, self.unit) return NotImplemented def __getitem__(self, item): From 790aba4e378461ee8786f3dd4f41415544bc8749 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 22 Mar 2019 11:34:26 +0000 Subject: [PATCH 26/57] BF: report original index --- nibabel/cifti2/cifti2_axes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 80220f459e..899b8104cf 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -1306,9 +1306,10 @@ def get_element(self, index): ------- float """ + original_index = index if index < 0: index = self.size + index if index >= self.size or index < 0: raise IndexError("index %i is out of range for Series with size %i" % - (index, self.size)) + (original_index, self.size)) return self.start + self.step * index From e7302d669d56f6daf45316d6c211a078035a5cdb Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 22 Mar 2019 11:36:02 +0000 Subject: [PATCH 27/57] RF: removed spurious '+' when concatenating literal strings --- nibabel/cifti2/cifti2_axes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 899b8104cf..dd5fa7b6b4 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -180,7 +180,7 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, self.volume_shape = None else: if affine is None or volume_shape is None: - raise ValueError("Affine and volume shape should be defined " + + raise ValueError("Affine and volume shape should be defined " "for BrainModel containing voxels") self.affine = affine self.volume_shape = volume_shape @@ -508,13 +508,13 @@ def __add__(self, other): not np.allclose(other.affine, affine) or other.volume_shape != shape ): - raise ValueError("Trying to concatenate two BrainModels defined " + + raise ValueError("Trying to concatenate two BrainModels defined " "in a different brain volume") nvertices = dict(self.nvertices) for name, value in other.nvertices.items(): if name in nvertices.keys() and nvertices[name] != value: - raise ValueError("Trying to concatenate two BrainModels with inconsistent " + + raise ValueError("Trying to concatenate two BrainModels with inconsistent " "number of vertices for %s" % name) nvertices[name] = value return self.__class__( @@ -651,7 +651,7 @@ def from_brain_models(cls, named_brain_models): volume_shape = bm.volume_shape else: if not np.allclose(affine, bm.affine) or (volume_shape != bm.volume_shape): - raise ValueError("Can not combine brain models defined in different " + + raise ValueError("Can not combine brain models defined in different " "volumes into a single Parcel axis") all_voxels[idx_parcel] = voxels @@ -659,7 +659,7 @@ def from_brain_models(cls, named_brain_models): for name, _, bm_part in bm.iter_structures(): if name in bm.nvertices.keys(): if name in nvertices.keys() and nvertices[name] != bm.nvertices[name]: - raise ValueError("Got multiple conflicting number of " + + raise ValueError("Got multiple conflicting number of " "vertices for surface structure %s" % name) nvertices[name] = bm.nvertices[name] vertices[name] = bm_part.vertex @@ -817,7 +817,7 @@ def __add__(self, other): affine, shape = self.affine, self.volume_shape if other.affine is not None and (not np.allclose(other.affine, affine) or other.volume_shape != shape): - raise ValueError("Trying to concatenate two Parcels defined " + + raise ValueError("Trying to concatenate two Parcels defined " "in a different brain volume") nvertices = dict(self.nvertices) for name, value in other.nvertices.items(): @@ -1290,7 +1290,7 @@ def __getitem__(self, item): nelements, self.unit) elif isinstance(item, integer_types): return self.get_element(item) - raise IndexError('Series can only be indexed with integers or slices ' + + raise IndexError('Series can only be indexed with integers or slices ' 'without breaking the regular structure') def get_element(self, index): From b2c674f7502c7f3abc3a2e06e5b50926bbc3cff6 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 22 Mar 2019 12:11:43 +0000 Subject: [PATCH 28/57] Increased test coverage --- nibabel/cifti2/cifti2_axes.py | 14 +++++-- nibabel/cifti2/tests/test_axes.py | 67 ++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index dd5fa7b6b4..2ae19f1bbd 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -605,6 +605,13 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert maps names of surface elements to integers (not needed for volumetric CIFTI files) """ self.name = np.asanyarray(name, dtype='U') + as_array = np.asanyarray(voxels) + if as_array.ndim == 1: + voxels = as_array.astype('object') + else: + voxels = np.empty(len(voxels), dtype='object') + for idx in range(len(voxels)): + voxels[idx] = as_array[idx] self.voxels = np.asanyarray(voxels, dtype='object') self.vertices = np.asanyarray(vertices, dtype='object') self.affine = affine @@ -649,10 +656,9 @@ def from_brain_models(cls, named_brain_models): if affine is None: affine = bm.affine volume_shape = bm.volume_shape - else: - if not np.allclose(affine, bm.affine) or (volume_shape != bm.volume_shape): - raise ValueError("Can not combine brain models defined in different " - "volumes into a single Parcel axis") + elif not np.allclose(affine, bm.affine) or (volume_shape != bm.volume_shape): + raise ValueError("Can not combine brain models defined in different " + "volumes into a single Parcel axis") all_voxels[idx_parcel] = voxels vertices = {} diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index 452957128e..60454d2397 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -268,6 +268,11 @@ def test_brain_models(): bm_other.affine[0, 0] = 10 assert bm_vox != bm_other + bm_other = deepcopy(bm_vox) + bm_other.affine = None + assert bm_vox != bm_other + assert bm_other != bm_vox + bm_other = deepcopy(bm_vox) bm_other.volume_shape = (10, 3, 4) assert bm_vox != bm_other @@ -380,13 +385,24 @@ def test_parcels(): prc_other.affine[0, 0] = 10 assert prc != prc_other + prc_other = deepcopy(prc) + prc_other.affine = None + assert prc != prc_other + assert prc_other != prc + assert (prc + prc_other).affine is not None + assert (prc_other + prc).affine is not None + prc_other = deepcopy(prc) prc_other.volume_shape = (10, 3, 4) assert prc != prc_other + with assert_raises(ValueError): + prc + prc_other prc_other = deepcopy(prc) prc_other.nvertices['CIFTI_STRUCTURE_CORTEX_LEFT'] = 80 assert prc != prc_other + with assert_raises(ValueError): + prc + prc_other prc_other = deepcopy(prc) prc_other.voxels[0] = np.ones((2, 3), dtype='i4') @@ -408,6 +424,24 @@ def test_parcels(): prc_other.name[0] = 'new_name' assert prc != prc_other + # test direct initialisation + axes.Parcels( + voxels=[np.ones((3, 2), dtype=int)], + vertices=[{}], + name=['single_voxel'], + affine=np.eye(4), + volume_shape=(2, 3, 4), + ) + + with assert_raises(ValueError): + axes.Parcels( + voxels=[np.ones((3, 2), dtype=int)], + vertices=[{}], + name=[['single_voxel']], # wrong shape name array + affine=np.eye(4), + volume_shape=(2, 3, 4), + ) + def test_scalar(): """ @@ -430,7 +464,9 @@ def test_scalar(): assert 'a' not in sc.meta # test equalities - assert sc != get_parcels() + assert sc != get_label() + with assert_raises(Exception): + sc + get_label() sc_other = deepcopy(sc) assert sc == sc_other @@ -445,6 +481,15 @@ def test_scalar(): sc.meta[0]['new_key'] = 'new_entry' assert sc == sc_other + # test constructor + assert axes.Scalar(['scalar_name'], [{}]) == axes.Scalar(['scalar_name']) + + with assert_raises(ValueError): + axes.Scalar([['scalar_name']]) # wrong shape + + with assert_raises(ValueError): + axes.Scalar(['scalar_name'], [{}, {}]) # wrong size + def test_label(): """ @@ -468,6 +513,8 @@ def test_label(): # test equalities lab = get_label() assert lab != get_scalar() + with assert_raises(Exception): + lab + get_scalar() other_lab = deepcopy(lab) assert lab != other_lab[:2] @@ -489,6 +536,15 @@ def test_label(): lab.label[0][20] = ('new_label', (0, 0, 0, 1)) assert lab == other_lab + # test constructor + assert axes.Label(['scalar_name'], [{}], [{}]) == axes.Label(['scalar_name'], [{}]) + + with assert_raises(ValueError): + axes.Label([['scalar_name']], [{}]) # wrong shape + + with assert_raises(ValueError): + axes.Label(['scalar_name'], [{}, {}]) # wrong size + def test_series(): """ @@ -533,8 +589,17 @@ def test_series(): assert (sr[0][3:1:-1].time == sr[0].time[3:1:-1]).all() assert (sr[0][1:3:-1].time == sr[0].time[1:3:-1]).all() + with assert_raises(IndexError): + assert sr[0][[0, 1]] + with assert_raises(IndexError): + assert sr[0][20] + with assert_raises(IndexError): + assert sr[0][-20] + # test_equalities sr = next(get_series()) + with assert_raises(Exception): + sr + get_scalar() assert sr != sr[:2] assert sr == sr[:] From 459fa8855781860817f8a371e016c9756401d9c2 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 22 Mar 2019 12:15:17 +0000 Subject: [PATCH 29/57] RF: replaced last '.all()' with `array_equal` --- nibabel/cifti2/cifti2_axes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 2ae19f1bbd..5753ea9c00 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -959,7 +959,7 @@ def __eq__(self, other): """ if not isinstance(other, Scalar) or self.size != other.size: return False - return (self.name == other.name).all() and (self.meta == other.meta).all() + return np.array_equal(self.name, other.name) and np.array_equal(self.meta, other.meta) def __add__(self, other): """ @@ -1100,9 +1100,9 @@ def __eq__(self, other): if not isinstance(other, Label) or self.size != other.size: return False return ( - (self.name == other.name).all() and - (self.meta == other.meta).all() and - (self.label == other.label).all() + np.array_equal(self.name, other.name) and + np.array_equal(self.meta, other.meta) and + np.array_equal(self.label, other.label) ) def __add__(self, other): From 6b55e1128e95ab07c42184d16d283a8932d006c6 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 22 Mar 2019 13:33:18 +0000 Subject: [PATCH 30/57] RF: made flake8 happy again --- nibabel/cifti2/cifti2_axes.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 5753ea9c00..22ed322493 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -736,8 +736,8 @@ def to_mapping(self, dim): for name, voxels, vertices in zip(self.name, self.voxels, self.vertices): cifti_voxels = cifti2.Cifti2VoxelIndicesIJK(voxels) element = cifti2.Cifti2Parcel(name, cifti_voxels) - for name, idx_vertices in vertices.items(): - element.vertices.append(cifti2.Cifti2Vertices(name, idx_vertices)) + for name_vertex, idx_vertices in vertices.items(): + element.vertices.append(cifti2.Cifti2Vertices(name_vertex, idx_vertices)) mim.append(element) return mim @@ -783,7 +783,8 @@ def __len__(self): def __eq__(self, other): if (self.__class__ != other.__class__ or len(self) != len(other) or not np.array_equal(self.name, other.name) or self.nvertices != other.nvertices or - any(not np.array_equal(vox1, vox2) for vox1, vox2 in zip(self.voxels, other.voxels))): + any(not np.array_equal(vox1, vox2) + for vox1, vox2 in zip(self.voxels, other.voxels))): return False if self.affine is not None: if ( @@ -857,7 +858,7 @@ def __getitem__(self, item): if isinstance(item, integer_types): return self.get_element(item) return self.__class__(self.name[item], self.voxels[item], self.vertices[item], - self.affine, self.volume_shape, self.nvertices) + self.affine, self.volume_shape, self.nvertices) def get_element(self, index): """ From e4bc7b01c96e14c9faa23e76613d6d6adde33800 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 22 Mar 2019 18:05:42 +0000 Subject: [PATCH 31/57] BF: don't use np.full to create string array numpy 1.8 in python 2.7 does not like that --- nibabel/cifti2/tests/test_axes.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index 60454d2397..2ca0f7cd18 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -171,10 +171,9 @@ def test_brain_models(): # Test the constructor bm_vox = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) - print(bm_vox.name) - assert np.all(bm_vox.name == np.full(5, 'CIFTI_STRUCTURE_THALAMUS_LEFT')) - assert np.all(bm_vox.vertex == np.full(5, -1)) - assert np.all(bm_vox.voxel == np.full((5, 3), 1)) + assert np.all(bm_vox.name == ['CIFTI_STRUCTURE_THALAMUS_LEFT'] * 5) + assert np.array_equal(bm_vox.vertex, np.full(5, -1)) + assert np.array_equal(bm_vox.voxel, np.full((5, 3), 1)) with assert_raises(ValueError): # no volume shape axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4)) @@ -195,9 +194,9 @@ def test_brain_models(): axes.BrainModel('thalamus_left', voxel=np.ones((5, 2), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) bm_vertex = axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) - assert np.all(bm_vertex.name == np.full(5, 'CIFTI_STRUCTURE_CORTEX_LEFT')) - assert np.all(bm_vertex.vertex == np.full(5, 1)) - assert np.all(bm_vertex.voxel == np.full((5, 3), -1)) + assert np.array_equal(bm_vertex.name, ['CIFTI_STRUCTURE_CORTEX_LEFT'] * 5) + assert np.array_equal(bm_vertex.vertex, np.full(5, 1)) + assert np.array_equal(bm_vertex.voxel, np.full((5, 3), -1)) with assert_raises(ValueError): axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int)) with assert_raises(ValueError): From a5f88c2ae6a093d1aeb817ba4a9c1155daa75f37 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Sun, 24 Mar 2019 13:24:33 +0000 Subject: [PATCH 32/57] RF: set surface BrainModel default structure to Other --- nibabel/cifti2/cifti2_axes.py | 2 +- nibabel/cifti2/tests/test_axes.py | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 22ed322493..5c5e8290e9 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -232,7 +232,7 @@ def from_mask(cls, mask, name='other', affine=None): "3-dimensional (for volumes), not %i-dimensional" % mask.ndim) @classmethod - def from_surface(cls, vertices, nvertex, name='Cortex'): + def from_surface(cls, vertices, nvertex, name='Other'): """ Creates a new BrainModel axis describing the vertices on a surface diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index 2ca0f7cd18..c248c0c0f4 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -125,33 +125,34 @@ def test_brain_models(): assert (bml[4].voxel == -1).all() assert (bml[4].vertex == [2, 9, 14]).all() - for bm, label in zip(bml, ['ThalamusRight', 'Other', 'cortex_left', 'cortex']): + for bm, label, is_surface in zip(bml, ['ThalamusRight', 'Other', 'cortex_left', 'Other'], + (False, False, True, True)): structures = list(bm.iter_structures()) assert len(structures) == 1 name = structures[0][0] assert name == axes.BrainModel.to_cifti_brain_structure_name(label) - if 'CORTEX' in name: + if is_surface: assert bm.nvertices[name] == 15 else: assert name not in bm.nvertices assert (bm.affine == rand_affine).all() assert bm.volume_shape == vol_shape - bmt = bml[0] + bml[1] + bml[2] + bml[3] - assert len(bmt) == 14 + bmt = bml[0] + bml[1] + bml[2] + assert len(bmt) == 10 structures = list(bmt.iter_structures()) - assert len(structures) == 4 - for bm, (name, _, bm_split) in zip(bml, structures): + assert len(structures) == 3 + for bm, (name, _, bm_split) in zip(bml[:3], structures): assert bm == bm_split assert (bm_split.name == name).all() assert bm == bmt[bmt.name == bm.name[0]] assert bm == bmt[np.where(bmt.name == bm.name[0])] - bmt = bmt + bml[3] - assert len(bmt) == 18 + bmt = bmt + bml[2] + assert len(bmt) == 13 structures = list(bmt.iter_structures()) - assert len(structures) == 4 - assert len(structures[-1][2]) == 8 + assert len(structures) == 3 + assert len(structures[-1][2]) == 6 # break brain model bmt.affine = np.eye(4) @@ -320,7 +321,7 @@ def test_parcels(): assert prc[2] == ('surface', ) + prc['surface'] assert prc['surface'][0].shape == (0, 3) assert len(prc['surface'][1]) == 1 - assert prc['surface'][1]['CIFTI_STRUCTURE_CORTEX'].shape == (4, ) + assert prc['surface'][1]['CIFTI_STRUCTURE_OTHER'].shape == (4, ) prc2 = prc + prc assert len(prc2) == 6 From a31af61c9b189bd40f5f0f0c6ebcf51287d62ca6 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Sun, 24 Mar 2019 13:32:22 +0000 Subject: [PATCH 33/57] RF: adjusted some type desciptions --- nibabel/cifti2/cifti2_axes.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 5c5e8290e9..2dae100998 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -31,7 +31,7 @@ def to_header(axes): Parameters ---------- - axes : iterable[Axis] + axes : iterable of :py:class:`Axis` objects one or more axes describing each dimension in turn Returns @@ -140,10 +140,10 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, affine : np.ndarray (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only covering the surface) - volume_shape : Tuple[int, int, int] + volume_shape : tuple of three integers shape of the volume in which the voxels were defined (not needed for CIFTI files only covering the surface) - nvertices : dict[String -> int] + nvertices : dict from string to integer maps names of surface elements to integers (not needed for volumetric CIFTI files) """ if voxel is None: @@ -361,7 +361,7 @@ def to_cifti_brain_structure_name(name): Parameters ---------- - name: (str, tuple) + name: iterable of 2-element tuples of integer and string input name of an anatomical region Returns @@ -598,7 +598,7 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert affine : np.ndarray (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only covering the surface) - volume_shape : Tuple[int, int, int] + volume_shape : tuple of three integers shape of the volume in which the voxels were defined (not needed for CIFTI files only covering the surface) nvertices : dict[String -> int] @@ -634,7 +634,7 @@ def from_brain_models(cls, named_brain_models): Parameters ---------- - named_brain_models : List[Tuple[String, BrainModel]] + named_brain_models : iterable of 2-element tuples of string and BrainModel list of (parcel name, brain model representation) pairs defining each parcel Returns @@ -889,9 +889,9 @@ def __init__(self, name, meta=None): """ Parameters ---------- - name : np.ndarray + name : np.ndarray of string (N, ) string array with the parcel names - meta : np.ndarray + meta : np.ndarray of dict (N, ) object array with a dictionary of metadata for each row/column. Defaults to empty dictionary """ From 0e0b7f212c9bf9eafb97ef627312d447ddcc3e6b Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Sun, 24 Mar 2019 14:39:32 +0000 Subject: [PATCH 34/57] DOC: Added tutorial to docs adopted from https://github.com/MichielCottaar/cifti/blob/master/README.md Added CIfTI to list of supported file formats replaced CIFTI with CIfTI2 --- doc/source/api.rst | 1 + nibabel/cifti2/__init__.py | 3 +- nibabel/cifti2/cifti2.py | 7 +- nibabel/cifti2/cifti2_axes.py | 180 +++++++++++++++++++++++++++------- 4 files changed, 148 insertions(+), 43 deletions(-) diff --git a/doc/source/api.rst b/doc/source/api.rst index 1ae1bb416c..0f3cf1de26 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -23,6 +23,7 @@ File Formats analyze spm2analyze spm99analyze + cifti2 gifti freesurfer minc1 diff --git a/nibabel/cifti2/__init__.py b/nibabel/cifti2/__init__.py index 0c80e4033b..071f76bf58 100644 --- a/nibabel/cifti2/__init__.py +++ b/nibabel/cifti2/__init__.py @@ -6,7 +6,7 @@ # copyright and license terms. # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -"""CIfTI format IO +"""CIfTI2 format IO .. currentmodule:: nibabel.cifti2 @@ -14,6 +14,7 @@ :toctree: ../generated cifti2 + cifti2_axes """ from .parse_cifti2 import Cifti2Extension diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 30bcbda73e..06deb72fe4 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -12,12 +12,9 @@ http://www.nitrc.org/forum/message.php?msg_id=3738 -Definition of the CIFTI2 header format and file extensions attached to this -email: +Definition of the CIFTI2 header format and file extensions can be found at: - http://www.nitrc.org/forum/forum.php?thread_id=4380&forum_id=1955 - -Filename is ``CIFTI-2_Main_FINAL_1March2014.pdf``. + http://www.nitrc.org/projects/cifti ''' from __future__ import division, print_function, absolute_import import re diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 2dae100998..386fe3699f 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -1,3 +1,109 @@ +""" +Defines :class:`Axis` objects to create, read, and manipulate CIfTI2 files + +Each type of CIfTI2 axes describing the rows/columns in a CIfTI2 matrix is given a unique class: + +* :class:`BrainModel`: each row/column is a voxel or vertex +* :class:`Parcels`: each row/column is a group of voxels and/or vertices +* :class:`Scalar`: each row/column has a unique name (with optional meta-data) +* :class:`Label`: each row/column has a unique name and label table (with optional meta-data) +* :class:`Series`: each row/column is a timepoint, which increases monotonically + +All of these classes are derived from the :class:`Axis` class. + +After loading a CIfTI2 file a tuple of axes describing the rows and columns can be obtained +from the :meth:`.cifti2.Cifti2Header.get_axis` method on the header object +(e.g. ``nibabel.load().header.get_axis()``). Inversely, a new +:class:`.cifti2.Cifti2Header` object can be created from existing :class:`Axis` objects +using the :meth:`.cifti2.Cifti2Header.from_axes` factory method. + +CIfTI2 :class:`Axis` objects of the same type can be concatenated using the '+'-operator. +Numpy indexing also works on axes +(except for Series objects, which have to remain monotonically increasing or decreasing). + +Creating new CIfTI2 axes +----------------------- +New :class:`Axis` objects can be constructed by providing a description for what is contained +in each row/column of the described tensor. For each :class:`Axis` sub-class this descriptor is: + +* :class:`BrainModel`: a CIfTI2 structure name and a voxel or vertex index +* :class:`Parcels`: a name and a sequence of voxel and vertex indices +* :class:`Scalar`: a name and optionally a dict of meta-data +* :class:`Label`: a name, dict of label index to name and colour, + and optionally a dict of meta-data +* :class:`Series`: the time-point of each row/column is set by setting the start, stop, size, + and unit of the time-series + +Several helper functions exist to create new :class:`BrainModel` axes: + +* :meth:`BrainModel.from_mask` creates a new BrainModel volume covering the + non-zero values of a mask +* :meth:`BrainModel.from_surface` creates a new BrainModel surface covering the provided + indices of a surface + +A :class:`Parcels` axis can be created from a sequence of :class:`BrainModel` axes using +:meth:`Parcels.from_brain_models`. + +Examples +-------- +We can create brain models covering the left cortex and left thalamus using: + +>>> from nibabel import cifti2 +>>> bm_cortex = cifti2.BrainModel.from_mask(cortex_mask, brain_structure='cortex_left') +>>> bm_thal = cifti2.BrainModel.from_mask(thalamus_mask, affine=affine, + brain_structure='thalamus_left') + +Brain structure names automatically get converted to valid CIfTI2 indentifiers using +:meth:`BrainModel.to_cifti_brain_structure_name`. +A 1-dimensional mask will be automatically interpreted as a surface element and a 3-dimensional +mask as a volume element. + +These can be concatenated in a single brain model covering the left cortex and thalamus by +simply adding them together + +>>> bm_full = bm_cortex + bm_thal + +Brain models covering the full HCP grayordinate space can be constructed by adding all the +volumetric and surface brain models together like this (or by reading one from an already +existing HCP file). + +Getting a specific brain region from the full brain model is as simple as: + +>>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_CORTEX_LEFT'] == bm_cortex +>>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_THALAMUS_LEFT'] == bm_thal + +You can also iterate over all brain structures in a brain model: + +>>> for name, slc, bm in bm_full.iter_structures(): ... + +In this case there will be two iterations, namely: +('CIFTI_STRUCTURE_CORTEX_LEFT', slice(0, ), bm_cortex) +and +('CIFTI_STRUCTURE_THALAMUS_LEFT', slice(, None), bm_thal) + +Parcels can be constructed from selections of these brain models: + +>>> parcel = cifti2.Parcels.from_brain_models([ + ('surface_parcel', bm_cortex[:100]), # contains first 100 cortical vertices + ('volume_parcel', bm_thal), # contains thalamus + ('combined_parcel', bm_full[[1, 8, 10, 50, 120, 127]) # contains selected voxels/vertices + ]) + +Time series are represented by their starting time (typically 0), step size +(i.e. sampling time or TR), and number of elements: + +>>> series = cifti2.Series(start=0, step=100, size=5000) + +So a header for fMRI data with a TR of 100 ms covering the left cortex and thalamus with +5000 timepoints could be created with + +>>> cifti2.Cifti2Header.from_axes((series, bm_cortex + bm_thal)) + +Similarly the curvature and cortical thickness on the left cortex could be stored using a header +like: + +>>> cifti2.Cifti2Header.from_axes((cifti.Scalar(['curvature', 'thickness'], bm_cortex)) +""" import numpy as np from . import cifti2 from six import string_types, add_metaclass, integer_types @@ -7,7 +113,7 @@ def from_mapping(mim): """ - Parses the MatrixIndicesMap to find the appropriate CIFTI axis describing the rows or columns + Parses the MatrixIndicesMap to find the appropriate CIfTI2 axis describing the rows or columns Parameters ---------- @@ -27,7 +133,7 @@ def from_mapping(mim): def to_header(axes): """ - Converts the axes describing the rows/columns of a CIFTI vector/matrix to a Cifti2Header + Converts the axes describing the rows/columns of a CIfTI2 vector/matrix to a Cifti2Header Parameters ---------- @@ -56,7 +162,7 @@ def to_header(axes): @add_metaclass(abc.ABCMeta) class Axis(object): """ - Abstract class for any object describing the rows or columns of a CIFTI vector/matrix + Abstract class for any object describing the rows or columns of a CIfTI2 vector/matrix Mainly used for type checking. """ @@ -110,7 +216,7 @@ def __getitem__(self, item): class BrainModel(Axis): """ - Each row/column in the CIFTI vector/matrix represents a single vertex or voxel + Each row/column in the CIfTI2 vector/matrix represents a single vertex or voxel This Axis describes which vertex/voxel is represented by each row/column. """ @@ -133,18 +239,18 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, name : str or np.ndarray brain structure name or (N, ) string array with the brain structure names voxel : np.ndarray - (N, 3) array with the voxel indices (can be omitted for CIFTI files only + (N, 3) array with the voxel indices (can be omitted for CIfTI2 files only covering the surface) vertex : np.ndarray - (N, ) array with the vertex indices (can be omitted for volumetric CIFTI files) + (N, ) array with the vertex indices (can be omitted for volumetric CIfTI2 files) affine : np.ndarray - (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only + (4, 4) array mapping voxel indices to mm space (not needed for CIfTI2 files only covering the surface) volume_shape : tuple of three integers - shape of the volume in which the voxels were defined (not needed for CIFTI files only + shape of the volume in which the voxels were defined (not needed for CIfTI2 files only covering the surface) nvertices : dict from string to integer - maps names of surface elements to integers (not needed for volumetric CIFTI files) + maps names of surface elements to integers (not needed for volumetric CIfTI2 files) """ if voxel is None: if vertex is None: @@ -256,7 +362,7 @@ def from_surface(cls, vertices, nvertex, name='Other'): @classmethod def from_mapping(cls, mim): """ - Creates a new BrainModel axis based on a CIFTI dataset + Creates a new BrainModel axis based on a CIfTI2 dataset Parameters ---------- @@ -289,12 +395,12 @@ def from_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the brain model axis to a MatrixIndicesMap for storage in CIFTI format + Converts the brain model axis to a MatrixIndicesMap for storage in CIfTI2 format Parameters ---------- dim : int - which dimension of the CIFTI vector/matrix is described by this dataset (zero-based) + which dimension of the CIfTI2 vector/matrix is described by this dataset (zero-based) Returns ------- @@ -329,7 +435,7 @@ def iter_structures(self): Yields ------ tuple with 3 elements: - - CIFTI brain structure name + - CIfTI2 brain structure name - slice to select the data associated with the brain structure from the tensor - brain model covering that specific brain structure """ @@ -345,11 +451,11 @@ def iter_structures(self): @staticmethod def to_cifti_brain_structure_name(name): """ - Attempts to convert the name of an anatomical region in a format recognized by CIFTI + Attempts to convert the name of an anatomical region in a format recognized by CIfTI2 This function returns: - - the name if it is in the CIFTI format already + - the name if it is in the CIfTI2 format already - if the name is a tuple the first element is assumed to be the structure name while the second is assumed to be the hemisphere (left, right or both). The latter will default to both. @@ -366,11 +472,11 @@ def to_cifti_brain_structure_name(name): Returns ------- - CIFTI2 compatible name + CIfTI2 compatible name Raises ------ - ValueError: raised if the input name does not match a known anatomical structure in CIFTI + ValueError: raised if the input name does not match a known anatomical structure in CIfTI2 """ if name in cifti2.CIFTI_BRAIN_STRUCTURES: return name @@ -571,7 +677,7 @@ def get_element(self, index): class Parcels(Axis): """ - Each row/column in the CIFTI vector/matrix represents a parcel of voxels/vertices + Each row/column in the CIfTI2 vector/matrix represents a parcel of voxels/vertices This Axis describes which parcel is represented by each row/column. @@ -596,13 +702,13 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert For each parcel the vertices are represented by a mapping from brain structure name to (M, ) index array affine : np.ndarray - (4, 4) array mapping voxel indices to mm space (not needed for CIFTI files only + (4, 4) array mapping voxel indices to mm space (not needed for CIfTI2 files only covering the surface) volume_shape : tuple of three integers - shape of the volume in which the voxels were defined (not needed for CIFTI files only + shape of the volume in which the voxels were defined (not needed for CIfTI2 files only covering the surface) nvertices : dict[String -> int] - maps names of surface elements to integers (not needed for volumetric CIFTI files) + maps names of surface elements to integers (not needed for volumetric CIfTI2 files) """ self.name = np.asanyarray(name, dtype='U') as_array = np.asanyarray(voxels) @@ -675,7 +781,7 @@ def from_brain_models(cls, named_brain_models): @classmethod def from_mapping(cls, mim): """ - Creates a new Parcels axis based on a CIFTI dataset + Creates a new Parcels axis based on a CIfTI2 dataset Parameters ---------- @@ -716,12 +822,12 @@ def from_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the Parcel to a MatrixIndicesMap for storage in CIFTI format + Converts the Parcel to a MatrixIndicesMap for storage in CIfTI2 format Parameters ---------- dim : int - which dimension of the CIFTI vector/matrix is described by this dataset (zero-based) + which dimension of the CIfTI2 vector/matrix is described by this dataset (zero-based) Returns ------- @@ -881,7 +987,7 @@ def get_element(self, index): class Scalar(Axis): """ - Along this axis of the CIFTI vector/matrix each row/column has been given + Along this axis of the CIfTI2 vector/matrix each row/column has been given a unique name and optionally metadata """ @@ -908,7 +1014,7 @@ def __init__(self, name, meta=None): @classmethod def from_mapping(cls, mim): """ - Creates a new Scalar axis based on a CIFTI dataset + Creates a new Scalar axis based on a CIfTI2 dataset Parameters ---------- @@ -924,12 +1030,12 @@ def from_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the hcp_labels to a MatrixIndicesMap for storage in CIFTI format + Converts the hcp_labels to a MatrixIndicesMap for storage in CIfTI2 format Parameters ---------- dim : int - which dimension of the CIFTI vector/matrix is described by this dataset (zero-based) + which dimension of the CIfTI2 vector/matrix is described by this dataset (zero-based) Returns ------- @@ -1007,9 +1113,9 @@ def get_element(self, index): class Label(Axis): """ - Defines CIFTI axis for label array. + Defines CIfTI2 axis for label array. - Along this axis of the CIFTI vector/matrix each row/column has been given a unique name, + Along this axis of the CIfTI2 vector/matrix each row/column has been given a unique name, label table, and optionally metadata """ @@ -1042,7 +1148,7 @@ def __init__(self, name, label, meta=None): @classmethod def from_mapping(cls, mim): """ - Creates a new Label axis based on a CIFTI dataset + Creates a new Label axis based on a CIfTI2 dataset Parameters ---------- @@ -1059,12 +1165,12 @@ def from_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the hcp_labels to a MatrixIndicesMap for storage in CIFTI format + Converts the hcp_labels to a MatrixIndicesMap for storage in CIfTI2 format Parameters ---------- dim : int - which dimension of the CIFTI vector/matrix is described by this dataset (zero-based) + which dimension of the CIfTI2 vector/matrix is described by this dataset (zero-based) Returns ------- @@ -1153,7 +1259,7 @@ def get_element(self, index): class Series(Axis): """ - Along this axis of the CIFTI vector/matrix the rows/columns increase monotonously in time + Along this axis of the CIfTI2 vector/matrix the rows/columns increase monotonously in time This Axis describes the time point of each row/column. """ @@ -1186,7 +1292,7 @@ def time(self): @classmethod def from_mapping(cls, mim): """ - Creates a new Series axis based on a CIFTI dataset + Creates a new Series axis based on a CIfTI2 dataset Parameters ---------- @@ -1202,12 +1308,12 @@ def from_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the Series to a MatrixIndicesMap for storage in CIFTI format + Converts the Series to a MatrixIndicesMap for storage in CIfTI2 format Parameters ---------- dim : int - which dimension of the CIFTI vector/matrix is described by this dataset (zero-based) + which dimension of the CIfTI2 vector/matrix is described by this dataset (zero-based) Returns ------- From 594b22ff11cc439a9245dcecc97c954b451b9e68 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Sun, 24 Mar 2019 17:29:27 +0000 Subject: [PATCH 35/57] BUG: skip doctests in example code --- nibabel/cifti2/cifti2_axes.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 386fe3699f..8f36721af7 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -49,9 +49,10 @@ We can create brain models covering the left cortex and left thalamus using: >>> from nibabel import cifti2 ->>> bm_cortex = cifti2.BrainModel.from_mask(cortex_mask, brain_structure='cortex_left') +>>> bm_cortex = cifti2.BrainModel.from_mask(cortex_mask, +... brain_structure='cortex_left') # doctest: +SKIP >>> bm_thal = cifti2.BrainModel.from_mask(thalamus_mask, affine=affine, - brain_structure='thalamus_left') +... brain_structure='thalamus_left') # doctest: +SKIP Brain structure names automatically get converted to valid CIfTI2 indentifiers using :meth:`BrainModel.to_cifti_brain_structure_name`. @@ -61,7 +62,7 @@ These can be concatenated in a single brain model covering the left cortex and thalamus by simply adding them together ->>> bm_full = bm_cortex + bm_thal +>>> bm_full = bm_cortex + bm_thal # doctest: +SKIP Brain models covering the full HCP grayordinate space can be constructed by adding all the volumetric and surface brain models together like this (or by reading one from an already @@ -69,12 +70,12 @@ Getting a specific brain region from the full brain model is as simple as: ->>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_CORTEX_LEFT'] == bm_cortex ->>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_THALAMUS_LEFT'] == bm_thal +>>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_CORTEX_LEFT'] == bm_cortex # doctest: +SKIP +>>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_THALAMUS_LEFT'] == bm_thal # doctest: +SKIP You can also iterate over all brain structures in a brain model: ->>> for name, slc, bm in bm_full.iter_structures(): ... +>>> for name, slc, bm in bm_full.iter_structures(): ... # doctest: +SKIP In this case there will be two iterations, namely: ('CIFTI_STRUCTURE_CORTEX_LEFT', slice(0, ), bm_cortex) @@ -84,25 +85,26 @@ Parcels can be constructed from selections of these brain models: >>> parcel = cifti2.Parcels.from_brain_models([ - ('surface_parcel', bm_cortex[:100]), # contains first 100 cortical vertices - ('volume_parcel', bm_thal), # contains thalamus - ('combined_parcel', bm_full[[1, 8, 10, 50, 120, 127]) # contains selected voxels/vertices - ]) +... ('surface_parcel', bm_cortex[:100]), # contains first 100 cortical vertices +... ('volume_parcel', bm_thal), # contains thalamus +... ('combined_parcel', bm_full[[1, 8, 10, 50, 120, 127]) # contains selected voxels/vertices +... ]) # doctest: +SKIP Time series are represented by their starting time (typically 0), step size (i.e. sampling time or TR), and number of elements: ->>> series = cifti2.Series(start=0, step=100, size=5000) +>>> series = cifti2.Series(start=0, step=100, size=5000) # doctest: +SKIP So a header for fMRI data with a TR of 100 ms covering the left cortex and thalamus with 5000 timepoints could be created with ->>> cifti2.Cifti2Header.from_axes((series, bm_cortex + bm_thal)) +>>> cifti2.Cifti2Header.from_axes((series, bm_cortex + bm_thal)) # doctest: +SKIP Similarly the curvature and cortical thickness on the left cortex could be stored using a header like: ->>> cifti2.Cifti2Header.from_axes((cifti.Scalar(['curvature', 'thickness'], bm_cortex)) +>>> cifti2.Cifti2Header.from_axes((cifti.Scalar(['curvature', 'thickness'], +... bm_cortex)) # doctest: +SKIP """ import numpy as np from . import cifti2 From 29a52877120cd6d64fb5d5cd54207e037c8283e2 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Sun, 24 Mar 2019 17:30:21 +0000 Subject: [PATCH 36/57] BF: reduces line length in example code --- nibabel/cifti2/cifti2_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 8f36721af7..61fadb7ee7 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -87,7 +87,7 @@ >>> parcel = cifti2.Parcels.from_brain_models([ ... ('surface_parcel', bm_cortex[:100]), # contains first 100 cortical vertices ... ('volume_parcel', bm_thal), # contains thalamus -... ('combined_parcel', bm_full[[1, 8, 10, 50, 120, 127]) # contains selected voxels/vertices +... ('combined_parcel', bm_full[[1, 8, 10, 120, 127]) # contains selected voxels/vertices ... ]) # doctest: +SKIP Time series are represented by their starting time (typically 0), step size From 109fa9266ed65a2f2afcaf2d4d1c2c14e6554a2a Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 26 Mar 2019 17:12:12 +0000 Subject: [PATCH 37/57] RF: changes from @demianw not involving name changes --- nibabel/cifti2/cifti2_axes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 61fadb7ee7..00803f4cd1 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -1,6 +1,7 @@ """ Defines :class:`Axis` objects to create, read, and manipulate CIfTI2 files +These axes provide an alternative interface to the information in the CIFTI2 header. Each type of CIfTI2 axes describing the rows/columns in a CIfTI2 matrix is given a unique class: * :class:`BrainModel`: each row/column is a voxel or vertex @@ -146,7 +147,7 @@ def to_header(axes): ------- cifti2.Cifti2Header """ - axes = list(axes) + axes = tuple(axes) mims_all = [] matrix = cifti2.Cifti2Matrix() for dim, ax in enumerate(axes): @@ -214,6 +215,7 @@ def __getitem__(self, item): """ Extracts definition of single row/column or new Axis describing a subset of the rows/columns """ + pass class BrainModel(Axis): From f7c47bb389356a88f173fe9700fe7b621dbb58c1 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 26 Mar 2019 17:14:54 +0000 Subject: [PATCH 38/57] RF: rename from_mapping to from_index_mapping --- nibabel/cifti2/cifti2.py | 2 +- nibabel/cifti2/cifti2_axes.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 06deb72fe4..ddbc0550cd 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1279,7 +1279,7 @@ def get_axis(self, index): axis : cifti2_axes.Axis ''' from . import cifti2_axes - return cifti2_axes.from_mapping(self.matrix.get_index_map(index)) + return cifti2_axes.from_index_mapping(self.matrix.get_index_map(index)) @classmethod def from_axes(cls, axes): diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 00803f4cd1..c475baa30c 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -114,7 +114,7 @@ import abc -def from_mapping(mim): +def from_index_mapping(mim): """ Parses the MatrixIndicesMap to find the appropriate CIfTI2 axis describing the rows or columns @@ -131,7 +131,7 @@ def from_mapping(mim): 'CIFTI_INDEX_TYPE_SERIES': Series, 'CIFTI_INDEX_TYPE_BRAIN_MODELS': BrainModel, 'CIFTI_INDEX_TYPE_PARCELS': Parcels} - return return_type[mim.indices_map_to_data_type].from_mapping(mim) + return return_type[mim.indices_map_to_data_type].from_index_mapping(mim) def to_header(axes): @@ -364,7 +364,7 @@ def from_surface(cls, vertices, nvertex, name='Other'): nvertices={cifti_name: nvertex}) @classmethod - def from_mapping(cls, mim): + def from_index_mapping(cls, mim): """ Creates a new BrainModel axis based on a CIfTI2 dataset @@ -783,7 +783,7 @@ def from_brain_models(cls, named_brain_models): return Parcels(all_names, all_voxels, all_vertices, affine, volume_shape, nvertices) @classmethod - def from_mapping(cls, mim): + def from_index_mapping(cls, mim): """ Creates a new Parcels axis based on a CIfTI2 dataset @@ -1016,7 +1016,7 @@ def __init__(self, name, meta=None): check_name, getattr(self, check_name).shape)) @classmethod - def from_mapping(cls, mim): + def from_index_mapping(cls, mim): """ Creates a new Scalar axis based on a CIfTI2 dataset @@ -1150,7 +1150,7 @@ def __init__(self, name, label, meta=None): check_name, getattr(self, check_name).shape)) @classmethod - def from_mapping(cls, mim): + def from_index_mapping(cls, mim): """ Creates a new Label axis based on a CIfTI2 dataset @@ -1164,7 +1164,7 @@ def from_mapping(cls, mim): """ tables = [{key: (value.label, value.rgba) for key, value in nm.label_table.items()} for nm in mim.named_maps] - rest = Scalar.from_mapping(mim) + rest = Scalar.from_index_mapping(mim) return Label(rest.name, tables, rest.meta) def to_mapping(self, dim): @@ -1294,7 +1294,7 @@ def time(self): return np.arange(self.size) * self.step + self.start @classmethod - def from_mapping(cls, mim): + def from_index_mapping(cls, mim): """ Creates a new Series axis based on a CIfTI2 dataset From b88d51602810d32df3d395c067461a87ad0c6bb5 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 26 Mar 2019 17:25:21 +0000 Subject: [PATCH 39/57] RF: return CIFTI_MODEL_TYPE to distinguish surface and voxels --- nibabel/cifti2/cifti2_axes.py | 10 ++++++---- nibabel/cifti2/tests/test_axes.py | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index c475baa30c..1cbebb1e7c 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -670,13 +670,15 @@ def get_element(self, index): Returns ------- tuple with 3 elements - - boolean, which is True if it is a surface element + - str, 'CIFTI_MODEL_TYPE_SURFACE' for vertex or 'CIFTI_MODEL_TYPE_VOXELS' for voxel - vertex index if it is a surface element, otherwise array with 3 voxel indices - structure.BrainStructure object describing the brain structure the element was taken from """ - is_surface = self.name[index] in self.nvertices.keys() - struct = self.vertex if is_surface else self.voxel - return is_surface, struct[index], self.name[index] + element_type = 'CIFTI_MODEL_TYPE_' + ( + 'SURFACE' if self.name[index] in self.nvertices.keys() else 'VOXELS' + ) + struct = self.vertex if 'SURFACE' in element_type else self.voxel + return element_type, struct[index], self.name[index] class Parcels(Axis): diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index c248c0c0f4..b3ec45ebc4 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -107,7 +107,7 @@ def test_brain_models(): assert len(bml[0]) == 3 assert (bml[0].vertex == -1).all() assert (bml[0].voxel == [[0, 1, 2], [0, 4, 0], [0, 4, 2]]).all() - assert bml[0][1][0] == False + assert bml[0][1][0] == 'CIFTI_MODEL_TYPE_VOXELS' assert (bml[0][1][1] == [0, 4, 0]).all() assert bml[0][1][2] == axes.BrainModel.to_cifti_brain_structure_name('thalamus_right') assert len(bml[1]) == 4 @@ -116,11 +116,11 @@ def test_brain_models(): assert len(bml[2]) == 3 assert (bml[2].voxel == -1).all() assert (bml[2].vertex == [0, 5, 10]).all() - assert bml[2][1] == (True, 5, 'CIFTI_STRUCTURE_CORTEX_LEFT') + assert bml[2][1] == ('CIFTI_MODEL_TYPE_SURFACE', 5, 'CIFTI_STRUCTURE_CORTEX_LEFT') assert len(bml[3]) == 4 assert (bml[3].voxel == -1).all() assert (bml[3].vertex == [0, 5, 10, 13]).all() - assert bml[4][1] == (True, 9, 'CIFTI_STRUCTURE_CORTEX_RIGHT') + assert bml[4][1] == ('CIFTI_MODEL_TYPE_SURFACE', 9, 'CIFTI_STRUCTURE_CORTEX_RIGHT') assert len(bml[4]) == 3 assert (bml[4].voxel == -1).all() assert (bml[4].vertex == [2, 9, 14]).all() From fd8593eb57abbcf64a21e3bf59ed4c97a8967c5a Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 26 Mar 2019 17:37:33 +0000 Subject: [PATCH 40/57] RF: renamed is_surface to surface_mask and added volume_mask --- nibabel/cifti2/cifti2_axes.py | 29 ++++++++++++++-------- nibabel/cifti2/tests/test_axes.py | 1 + nibabel/cifti2/tests/test_cifti2io_axes.py | 4 +-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 1cbebb1e7c..734aa1bb0e 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -284,8 +284,8 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, if name not in self.name: del self.nvertices[name] - is_surface = self.is_surface - if is_surface.all(): + surface_mask = self.surface_mask + if surface_mask.all(): self.affine = None self.volume_shape = None else: @@ -295,9 +295,9 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, self.affine = affine self.volume_shape = volume_shape - if np.any(self.vertex[is_surface] < 0): + if np.any(self.vertex[surface_mask] < 0): raise ValueError('Undefined vertex indices found for surface elements') - if np.any(self.voxel[~is_surface] < 0): + if np.any(self.voxel[~surface_mask] < 0): raise ValueError('Undefined voxel indices found for volumetric elements') for check_name in ('name', 'voxel', 'vertex'): @@ -523,12 +523,19 @@ def to_cifti_brain_structure_name(name): return proposed_name @property - def is_surface(self): + def surface_mask(self): """ (N, ) boolean array which is true for any element on the surface """ return np.vectorize(lambda name: name in self.nvertices.keys())(self.name) + @property + def volume_mask(self): + """ + (N, ) boolean array which is true for any element on the surface + """ + return np.vectorize(lambda name: name not in self.nvertices.keys())(self.name) + _affine = None @property @@ -586,13 +593,13 @@ def __eq__(self, other): if xor(self.affine is None, other.affine is None): return False return ( - (self.affine is None or + (self.affine is None or np.allclose(self.affine, other.affine) and self.volume_shape == other.volume_shape) and - self.nvertices == other.nvertices and - np.array_equal(self.name, other.name) and - np.array_equal(self.voxel[~self.is_surface], other.voxel[~other.is_surface]) and - np.array_equal(self.vertex[self.is_surface], other.vertex[other.is_surface]) + self.nvertices == other.nvertices and + np.array_equal(self.name, other.name) and + np.array_equal(self.voxel[~self.surface_mask], other.voxel[~other.surface_mask]) and + np.array_equal(self.vertex[self.surface_mask], other.vertex[other.surface_mask]) ) def __add__(self, other): @@ -763,7 +770,7 @@ def from_brain_models(cls, named_brain_models): for idx_parcel, (parcel_name, bm) in enumerate(named_brain_models): all_names.append(parcel_name) - voxels = bm.voxel[~bm.is_surface] + voxels = bm.voxel[~bm.surface_mask] if voxels.shape[0] != 0: if affine is None: affine = bm.affine diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index b3ec45ebc4..150bb88300 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -127,6 +127,7 @@ def test_brain_models(): for bm, label, is_surface in zip(bml, ['ThalamusRight', 'Other', 'cortex_left', 'Other'], (False, False, True, True)): + assert np.all(bm.surface_mask == ~bm.volume_mask) structures = list(bm.iter_structures()) assert len(structures) == 1 name = structures[0][0] diff --git a/nibabel/cifti2/tests/test_cifti2io_axes.py b/nibabel/cifti2/tests/test_cifti2io_axes.py index fee3605ce4..e1025710ff 100644 --- a/nibabel/cifti2/tests/test_cifti2io_axes.py +++ b/nibabel/cifti2/tests/test_cifti2io_axes.py @@ -66,9 +66,9 @@ def check_Conte69(brain_model): structures = list(brain_model.iter_structures()) assert len(structures) == 2 assert structures[0][0] == 'CIFTI_STRUCTURE_CORTEX_LEFT' - assert structures[0][2].is_surface.all() + assert structures[0][2].surface_mask.all() assert structures[1][0] == 'CIFTI_STRUCTURE_CORTEX_RIGHT' - assert structures[1][2].is_surface.all() + assert structures[1][2].surface_mask.all() assert (brain_model.voxel == -1).all() assert (brain_model.vertex[:5] == np.arange(5)).all() From cdf57dba3be985c60f0b388f2640edc8a534325f Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Tue, 26 Mar 2019 21:36:05 +0000 Subject: [PATCH 41/57] STY: fixed indentation --- nibabel/cifti2/cifti2_axes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 734aa1bb0e..e3008518f6 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -594,8 +594,8 @@ def __eq__(self, other): return False return ( (self.affine is None or - np.allclose(self.affine, other.affine) and - self.volume_shape == other.volume_shape) and + np.allclose(self.affine, other.affine) and + self.volume_shape == other.volume_shape) and self.nvertices == other.nvertices and np.array_equal(self.name, other.name) and np.array_equal(self.voxel[~self.surface_mask], other.voxel[~other.surface_mask]) and From b51d5f114879a0b696912fdd6be1a1185a38b88f Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Wed, 27 Mar 2019 10:42:45 +0000 Subject: [PATCH 42/57] DOC: consistently use CIFTI-2 instead of CIfTI2 or CIFTI2 --- nibabel/cifti2/__init__.py | 2 +- nibabel/cifti2/cifti2.py | 36 ++++---- nibabel/cifti2/cifti2_axes.py | 90 ++++++++++---------- nibabel/cifti2/parse_cifti2.py | 35 ++++---- nibabel/cifti2/tests/test_axes.py | 14 +-- nibabel/cifti2/tests/test_cifti2.py | 2 +- nibabel/cifti2/tests/test_cifti2io_header.py | 2 +- nibabel/cifti2/tests/test_name.py | 2 +- nibabel/cifti2/tests/test_new_cifti2.py | 2 +- 9 files changed, 93 insertions(+), 92 deletions(-) diff --git a/nibabel/cifti2/__init__.py b/nibabel/cifti2/__init__.py index 071f76bf58..dc88a24b48 100644 --- a/nibabel/cifti2/__init__.py +++ b/nibabel/cifti2/__init__.py @@ -6,7 +6,7 @@ # copyright and license terms. # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -"""CIfTI2 format IO +"""CIFTI-2 format IO .. currentmodule:: nibabel.cifti2 diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index ddbc0550cd..7ca5584bb1 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -6,13 +6,13 @@ # copyright and license terms. # ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## -''' Read / write access to CIfTI2 image format +''' Read / write access to CIFTI-2 image format Format of the NIFTI2 container format described here: http://www.nitrc.org/forum/message.php?msg_id=3738 -Definition of the CIFTI2 header format and file extensions can be found at: +Definition of the CIFTI-2 header format and file extensions can be found at: http://www.nitrc.org/projects/cifti ''' @@ -39,7 +39,7 @@ def _float_01(val): class Cifti2HeaderError(Exception): - """ Error in CIFTI2 header + """ Error in CIFTI-2 header """ @@ -175,7 +175,7 @@ def _to_xml_element(self): class Cifti2LabelTable(xml.XmlSerializable, MutableMapping): - """ CIFTI2 label table: a sequence of ``Cifti2Label``s + """ CIFTI-2 label table: a sequence of ``Cifti2Label``s * Description - Used by NamedMap when IndicesMapToDataType is "CIFTI_INDEX_TYPE_LABELS" in order to associate names and display colors @@ -233,7 +233,7 @@ def _to_xml_element(self): class Cifti2Label(xml.XmlSerializable): - """ CIFTI2 label: association of integer key with a name and RGBA values + """ CIFTI-2 label: association of integer key with a name and RGBA values For all color components, value is floating point with range 0.0 to 1.0. @@ -311,7 +311,7 @@ def _to_xml_element(self): class Cifti2NamedMap(xml.XmlSerializable): - """CIFTI2 named map: association of name and optional data with a map index + """CIFTI-2 named map: association of name and optional data with a map index Associates a name, optional metadata, and possibly a LabelTable with an index in a map. @@ -429,7 +429,7 @@ def _to_xml_element(self): class Cifti2VoxelIndicesIJK(xml.XmlSerializable, MutableSequence): - """CIFTI2 VoxelIndicesIJK: Set of voxel indices contained in a structure + """CIFTI-2 VoxelIndicesIJK: Set of voxel indices contained in a structure * Description - Identifies the voxels that model a brain structure, or participate in a parcel. Note that when this is a child of BrainModel, @@ -511,7 +511,7 @@ def _to_xml_element(self): class Cifti2Vertices(xml.XmlSerializable, MutableSequence): - """CIFTI2 vertices - association of brain structure and a list of vertices + """CIFTI-2 vertices - association of brain structure and a list of vertices * Description - Contains a BrainStructure type and a list of vertex indices within a Parcel. @@ -577,7 +577,7 @@ def _to_xml_element(self): class Cifti2Parcel(xml.XmlSerializable): - """CIFTI2 parcel: association of a name with vertices and/or voxels + """CIFTI-2 parcel: association of a name with vertices and/or voxels * Description - Associates a name, plus vertices and/or voxels, with an index. @@ -692,7 +692,7 @@ def _to_xml_element(self): class Cifti2Volume(xml.XmlSerializable): - """CIFTI2 volume: information about a volume for mappings that use voxels + """CIFTI-2 volume: information about a volume for mappings that use voxels * Description - Provides information about the volume for any mappings that use voxels. @@ -735,7 +735,7 @@ def _to_xml_element(self): class Cifti2VertexIndices(xml.XmlSerializable, MutableSequence): - """CIFTI2 vertex indices: vertex indices for an associated brain model + """CIFTI-2 vertex indices: vertex indices for an associated brain model The vertex indices (which are independent for each surface, and zero-based) that are used in this brain model[.] The parent @@ -1078,7 +1078,7 @@ def _to_xml_element(self): class Cifti2Matrix(xml.XmlSerializable, MutableSequence): - """ CIFTI2 Matrix object + """ CIFTI-2 Matrix object This is a list-like container where the elements are instances of :class:`Cifti2MatrixIndicesMap`. @@ -1210,7 +1210,7 @@ def _to_xml_element(self): class Cifti2Header(FileBasedHeader, xml.XmlSerializable): - ''' Class for CIFTI2 header extension ''' + ''' Class for CIFTI-2 header extension ''' def __init__(self, matrix=None, version="2.0"): FileBasedHeader.__init__(self) @@ -1301,7 +1301,7 @@ def from_axes(cls, axes): class Cifti2Image(DataobjImage): - """ Class for single file CIFTI2 format image + """ Class for single file CIFTI-2 format image """ header_class = Cifti2Header valid_exts = Nifti2Image.valid_exts @@ -1329,7 +1329,7 @@ def __init__(self, returns an array from ``np.asanyarray``. It should have a ``shape`` attribute or property. header : Cifti2Header instance or Sequence[cifti2_axes.Axis] - Header with data for / from XML part of CIFTI2 format. + Header with data for / from XML part of CIFTI-2 format. Alternatively a sequence of cifti2_axes.Axis objects can be provided describing each dimension of the array. nifti_header : None or mapping or NIfTI2 header instance, optional @@ -1356,7 +1356,7 @@ def nifti_header(self): @classmethod def from_file_map(klass, file_map): - """ Load a CIFTI2 image from a file_map + """ Load a CIFTI-2 image from a file_map Parameters ---------- @@ -1376,7 +1376,7 @@ def from_file_map(klass, file_map): cifti_header = item.get_content() break else: - raise ValueError('NIfTI2 header does not contain a CIFTI2 ' + raise ValueError('NIfTI2 header does not contain a CIFTI-2 ' 'extension') # Construct cifti image. @@ -1435,7 +1435,7 @@ def to_file_map(self, file_map=None): img.to_file_map(file_map or self.file_map) def update_headers(self): - ''' Harmonize CIFTI2 and NIfTI headers with image data + ''' Harmonize CIFTI-2 and NIfTI headers with image data >>> import numpy as np >>> data = np.zeros((2,3,4)) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index e3008518f6..e0f8c0f7ea 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -1,8 +1,8 @@ """ -Defines :class:`Axis` objects to create, read, and manipulate CIfTI2 files +Defines :class:`Axis` objects to create, read, and manipulate CIFTI-2 files -These axes provide an alternative interface to the information in the CIFTI2 header. -Each type of CIfTI2 axes describing the rows/columns in a CIfTI2 matrix is given a unique class: +These axes provide an alternative interface to the information in the CIFTI-2 header. +Each type of CIFTI-2 axes describing the rows/columns in a CIFTI-2 matrix is given a unique class: * :class:`BrainModel`: each row/column is a voxel or vertex * :class:`Parcels`: each row/column is a group of voxels and/or vertices @@ -12,22 +12,22 @@ All of these classes are derived from the :class:`Axis` class. -After loading a CIfTI2 file a tuple of axes describing the rows and columns can be obtained +After loading a CIFTI-2 file a tuple of axes describing the rows and columns can be obtained from the :meth:`.cifti2.Cifti2Header.get_axis` method on the header object (e.g. ``nibabel.load().header.get_axis()``). Inversely, a new :class:`.cifti2.Cifti2Header` object can be created from existing :class:`Axis` objects using the :meth:`.cifti2.Cifti2Header.from_axes` factory method. -CIfTI2 :class:`Axis` objects of the same type can be concatenated using the '+'-operator. +CIFTI-2 :class:`Axis` objects of the same type can be concatenated using the '+'-operator. Numpy indexing also works on axes (except for Series objects, which have to remain monotonically increasing or decreasing). -Creating new CIfTI2 axes +Creating new CIFTI-2 axes ----------------------- New :class:`Axis` objects can be constructed by providing a description for what is contained in each row/column of the described tensor. For each :class:`Axis` sub-class this descriptor is: -* :class:`BrainModel`: a CIfTI2 structure name and a voxel or vertex index +* :class:`BrainModel`: a CIFTI-2 structure name and a voxel or vertex index * :class:`Parcels`: a name and a sequence of voxel and vertex indices * :class:`Scalar`: a name and optionally a dict of meta-data * :class:`Label`: a name, dict of label index to name and colour, @@ -55,7 +55,7 @@ >>> bm_thal = cifti2.BrainModel.from_mask(thalamus_mask, affine=affine, ... brain_structure='thalamus_left') # doctest: +SKIP -Brain structure names automatically get converted to valid CIfTI2 indentifiers using +Brain structure names automatically get converted to valid CIFTI-2 indentifiers using :meth:`BrainModel.to_cifti_brain_structure_name`. A 1-dimensional mask will be automatically interpreted as a surface element and a 3-dimensional mask as a volume element. @@ -116,7 +116,7 @@ def from_index_mapping(mim): """ - Parses the MatrixIndicesMap to find the appropriate CIfTI2 axis describing the rows or columns + Parses the MatrixIndicesMap to find the appropriate CIFTI-2 axis describing the rows or columns Parameters ---------- @@ -136,7 +136,7 @@ def from_index_mapping(mim): def to_header(axes): """ - Converts the axes describing the rows/columns of a CIfTI2 vector/matrix to a Cifti2Header + Converts the axes describing the rows/columns of a CIFTI-2 vector/matrix to a Cifti2Header Parameters ---------- @@ -165,7 +165,7 @@ def to_header(axes): @add_metaclass(abc.ABCMeta) class Axis(object): """ - Abstract class for any object describing the rows or columns of a CIfTI2 vector/matrix + Abstract class for any object describing the rows or columns of a CIFTI-2 vector/matrix Mainly used for type checking. """ @@ -220,7 +220,7 @@ def __getitem__(self, item): class BrainModel(Axis): """ - Each row/column in the CIfTI2 vector/matrix represents a single vertex or voxel + Each row/column in the CIFTI-2 vector/matrix represents a single vertex or voxel This Axis describes which vertex/voxel is represented by each row/column. """ @@ -243,18 +243,18 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, name : str or np.ndarray brain structure name or (N, ) string array with the brain structure names voxel : np.ndarray - (N, 3) array with the voxel indices (can be omitted for CIfTI2 files only + (N, 3) array with the voxel indices (can be omitted for CIFTI-2 files only covering the surface) vertex : np.ndarray - (N, ) array with the vertex indices (can be omitted for volumetric CIfTI2 files) + (N, ) array with the vertex indices (can be omitted for volumetric CIFTI-2 files) affine : np.ndarray - (4, 4) array mapping voxel indices to mm space (not needed for CIfTI2 files only + (4, 4) array mapping voxel indices to mm space (not needed for CIFTI-2 files only covering the surface) volume_shape : tuple of three integers - shape of the volume in which the voxels were defined (not needed for CIfTI2 files only + shape of the volume in which the voxels were defined (not needed for CIFTI-2 files only covering the surface) nvertices : dict from string to integer - maps names of surface elements to integers (not needed for volumetric CIfTI2 files) + maps names of surface elements to integers (not needed for volumetric CIFTI-2 files) """ if voxel is None: if vertex is None: @@ -366,7 +366,7 @@ def from_surface(cls, vertices, nvertex, name='Other'): @classmethod def from_index_mapping(cls, mim): """ - Creates a new BrainModel axis based on a CIfTI2 dataset + Creates a new BrainModel axis based on a CIFTI-2 dataset Parameters ---------- @@ -399,12 +399,12 @@ def from_index_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the brain model axis to a MatrixIndicesMap for storage in CIfTI2 format + Converts the brain model axis to a MatrixIndicesMap for storage in CIFTI-2 format Parameters ---------- dim : int - which dimension of the CIfTI2 vector/matrix is described by this dataset (zero-based) + which dimension of the CIFTI-2 vector/matrix is described by this dataset (zero-based) Returns ------- @@ -439,7 +439,7 @@ def iter_structures(self): Yields ------ tuple with 3 elements: - - CIfTI2 brain structure name + - CIFTI-2 brain structure name - slice to select the data associated with the brain structure from the tensor - brain model covering that specific brain structure """ @@ -455,11 +455,11 @@ def iter_structures(self): @staticmethod def to_cifti_brain_structure_name(name): """ - Attempts to convert the name of an anatomical region in a format recognized by CIfTI2 + Attempts to convert the name of an anatomical region in a format recognized by CIFTI-2 This function returns: - - the name if it is in the CIfTI2 format already + - the name if it is in the CIFTI-2 format already - if the name is a tuple the first element is assumed to be the structure name while the second is assumed to be the hemisphere (left, right or both). The latter will default to both. @@ -476,11 +476,11 @@ def to_cifti_brain_structure_name(name): Returns ------- - CIfTI2 compatible name + CIFTI-2 compatible name Raises ------ - ValueError: raised if the input name does not match a known anatomical structure in CIfTI2 + ValueError: raised if the input name does not match a known anatomical structure in CIFTI-2 """ if name in cifti2.CIFTI_BRAIN_STRUCTURES: return name @@ -690,7 +690,7 @@ def get_element(self, index): class Parcels(Axis): """ - Each row/column in the CIfTI2 vector/matrix represents a parcel of voxels/vertices + Each row/column in the CIFTI-2 vector/matrix represents a parcel of voxels/vertices This Axis describes which parcel is represented by each row/column. @@ -715,13 +715,13 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert For each parcel the vertices are represented by a mapping from brain structure name to (M, ) index array affine : np.ndarray - (4, 4) array mapping voxel indices to mm space (not needed for CIfTI2 files only + (4, 4) array mapping voxel indices to mm space (not needed for CIFTI-2 files only covering the surface) volume_shape : tuple of three integers - shape of the volume in which the voxels were defined (not needed for CIfTI2 files only + shape of the volume in which the voxels were defined (not needed for CIFTI-2 files only covering the surface) nvertices : dict[String -> int] - maps names of surface elements to integers (not needed for volumetric CIfTI2 files) + maps names of surface elements to integers (not needed for volumetric CIFTI-2 files) """ self.name = np.asanyarray(name, dtype='U') as_array = np.asanyarray(voxels) @@ -794,7 +794,7 @@ def from_brain_models(cls, named_brain_models): @classmethod def from_index_mapping(cls, mim): """ - Creates a new Parcels axis based on a CIfTI2 dataset + Creates a new Parcels axis based on a CIFTI-2 dataset Parameters ---------- @@ -835,12 +835,12 @@ def from_index_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the Parcel to a MatrixIndicesMap for storage in CIfTI2 format + Converts the Parcel to a MatrixIndicesMap for storage in CIFTI-2 format Parameters ---------- dim : int - which dimension of the CIfTI2 vector/matrix is described by this dataset (zero-based) + which dimension of the CIFTI-2 vector/matrix is described by this dataset (zero-based) Returns ------- @@ -1000,7 +1000,7 @@ def get_element(self, index): class Scalar(Axis): """ - Along this axis of the CIfTI2 vector/matrix each row/column has been given + Along this axis of the CIFTI-2 vector/matrix each row/column has been given a unique name and optionally metadata """ @@ -1027,7 +1027,7 @@ def __init__(self, name, meta=None): @classmethod def from_index_mapping(cls, mim): """ - Creates a new Scalar axis based on a CIfTI2 dataset + Creates a new Scalar axis based on a CIFTI-2 dataset Parameters ---------- @@ -1043,12 +1043,12 @@ def from_index_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the hcp_labels to a MatrixIndicesMap for storage in CIfTI2 format + Converts the hcp_labels to a MatrixIndicesMap for storage in CIFTI-2 format Parameters ---------- dim : int - which dimension of the CIfTI2 vector/matrix is described by this dataset (zero-based) + which dimension of the CIFTI-2 vector/matrix is described by this dataset (zero-based) Returns ------- @@ -1126,9 +1126,9 @@ def get_element(self, index): class Label(Axis): """ - Defines CIfTI2 axis for label array. + Defines CIFTI-2 axis for label array. - Along this axis of the CIfTI2 vector/matrix each row/column has been given a unique name, + Along this axis of the CIFTI-2 vector/matrix each row/column has been given a unique name, label table, and optionally metadata """ @@ -1161,7 +1161,7 @@ def __init__(self, name, label, meta=None): @classmethod def from_index_mapping(cls, mim): """ - Creates a new Label axis based on a CIfTI2 dataset + Creates a new Label axis based on a CIFTI-2 dataset Parameters ---------- @@ -1178,12 +1178,12 @@ def from_index_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the hcp_labels to a MatrixIndicesMap for storage in CIfTI2 format + Converts the hcp_labels to a MatrixIndicesMap for storage in CIFTI-2 format Parameters ---------- dim : int - which dimension of the CIfTI2 vector/matrix is described by this dataset (zero-based) + which dimension of the CIFTI-2 vector/matrix is described by this dataset (zero-based) Returns ------- @@ -1272,7 +1272,7 @@ def get_element(self, index): class Series(Axis): """ - Along this axis of the CIfTI2 vector/matrix the rows/columns increase monotonously in time + Along this axis of the CIFTI-2 vector/matrix the rows/columns increase monotonously in time This Axis describes the time point of each row/column. """ @@ -1305,7 +1305,7 @@ def time(self): @classmethod def from_index_mapping(cls, mim): """ - Creates a new Series axis based on a CIfTI2 dataset + Creates a new Series axis based on a CIFTI-2 dataset Parameters ---------- @@ -1321,12 +1321,12 @@ def from_index_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the Series to a MatrixIndicesMap for storage in CIfTI2 format + Converts the Series to a MatrixIndicesMap for storage in CIFTI-2 format Parameters ---------- dim : int - which dimension of the CIfTI2 vector/matrix is described by this dataset (zero-based) + which dimension of the CIFTI-2 vector/matrix is described by this dataset (zero-based) Returns ------- diff --git a/nibabel/cifti2/parse_cifti2.py b/nibabel/cifti2/parse_cifti2.py index f0df76ac7d..608636a446 100644 --- a/nibabel/cifti2/parse_cifti2.py +++ b/nibabel/cifti2/parse_cifti2.py @@ -94,7 +94,7 @@ def may_contain_header(klass, binaryblock): @staticmethod def _chk_qfac(hdr, fix=False): - # Allow qfac of 0 without complaint for CIFTI2 + # Allow qfac of 0 without complaint for CIFTI-2 rep = Report(HeaderDataError) if hdr['pixdim'][0] in (-1, 0, 1): return hdr, rep @@ -127,7 +127,7 @@ class _Cifti2AsNiftiImage(Nifti2Image): class Cifti2Parser(xml.XmlParser): - '''Class to parse an XML string into a CIFTI2 header object''' + '''Class to parse an XML string into a CIFTI-2 header object''' def __init__(self, encoding=None, buffer_size=3500000, verbose=0): super(Cifti2Parser, self).__init__(encoding=encoding, buffer_size=buffer_size, @@ -164,7 +164,7 @@ def StartElementHandler(self, name, attrs): parent = self.struct_state[-1] if not isinstance(parent, Cifti2Header): raise Cifti2HeaderError( - 'Matrix element can only be a child of the CIFTI2 Header element' + 'Matrix element can only be a child of the CIFTI-2 Header element' ) parent.matrix = matrix self.struct_state.append(matrix) @@ -175,7 +175,8 @@ def StartElementHandler(self, name, attrs): parent = self.struct_state[-1] if not isinstance(parent, (Cifti2Matrix, Cifti2NamedMap)): raise Cifti2HeaderError( - 'MetaData element can only be a child of the CIFTI2 Matrix or NamedMap elements' + 'MetaData element can only be a child of the CIFTI-2 Matrix ' + 'or NamedMap elements' ) self.struct_state.append(meta) @@ -207,7 +208,7 @@ def StartElementHandler(self, name, attrs): matrix = self.struct_state[-1] if not isinstance(matrix, Cifti2Matrix): raise Cifti2HeaderError( - 'MatrixIndicesMap element can only be a child of the CIFTI2 Matrix element' + 'MatrixIndicesMap element can only be a child of the CIFTI-2 Matrix element' ) matrix.append(mim) self.struct_state.append(mim) @@ -218,7 +219,7 @@ def StartElementHandler(self, name, attrs): mim = self.struct_state[-1] if not isinstance(mim, Cifti2MatrixIndicesMap): raise Cifti2HeaderError( - 'NamedMap element can only be a child of the CIFTI2 MatrixIndicesMap element' + 'NamedMap element can only be a child of the CIFTI-2 MatrixIndicesMap element' ) self.struct_state.append(named_map) mim.append(named_map) @@ -234,7 +235,7 @@ def StartElementHandler(self, name, attrs): lata = Cifti2LabelTable() if not isinstance(named_map, Cifti2NamedMap): raise Cifti2HeaderError( - 'LabelTable element can only be a child of the CIFTI2 NamedMap element' + 'LabelTable element can only be a child of the CIFTI-2 NamedMap element' ) self.fsm_state.append('LabelTable') self.struct_state.append(lata) @@ -244,7 +245,7 @@ def StartElementHandler(self, name, attrs): lata = self.struct_state[-1] if not isinstance(lata, Cifti2LabelTable): raise Cifti2HeaderError( - 'Label element can only be a child of the CIFTI2 LabelTable element' + 'Label element can only be a child of the CIFTI-2 LabelTable element' ) label = Cifti2Label() label.key = int(attrs["Key"]) @@ -260,7 +261,7 @@ def StartElementHandler(self, name, attrs): named_map = self.struct_state[-1] if not isinstance(named_map, Cifti2NamedMap): raise Cifti2HeaderError( - 'MapName element can only be a child of the CIFTI2 NamedMap element' + 'MapName element can only be a child of the CIFTI-2 NamedMap element' ) self.fsm_state.append('MapName') @@ -271,7 +272,7 @@ def StartElementHandler(self, name, attrs): mim = self.struct_state[-1] if not isinstance(mim, Cifti2MatrixIndicesMap): raise Cifti2HeaderError( - 'Surface element can only be a child of the CIFTI2 MatrixIndicesMap element' + 'Surface element can only be a child of the CIFTI-2 MatrixIndicesMap element' ) if mim.indices_map_to_data_type != "CIFTI_INDEX_TYPE_PARCELS": raise Cifti2HeaderError( @@ -287,7 +288,7 @@ def StartElementHandler(self, name, attrs): mim = self.struct_state[-1] if not isinstance(mim, Cifti2MatrixIndicesMap): raise Cifti2HeaderError( - 'Parcel element can only be a child of the CIFTI2 MatrixIndicesMap element' + 'Parcel element can only be a child of the CIFTI-2 MatrixIndicesMap element' ) parcel.name = attrs["Name"] mim.append(parcel) @@ -299,7 +300,7 @@ def StartElementHandler(self, name, attrs): parcel = self.struct_state[-1] if not isinstance(parcel, Cifti2Parcel): raise Cifti2HeaderError( - 'Vertices element can only be a child of the CIFTI2 Parcel element' + 'Vertices element can only be a child of the CIFTI-2 Parcel element' ) vertices.brain_structure = attrs["BrainStructure"] if vertices.brain_structure not in CIFTI_BRAIN_STRUCTURES: @@ -315,7 +316,7 @@ def StartElementHandler(self, name, attrs): parent = self.struct_state[-1] if not isinstance(parent, (Cifti2Parcel, Cifti2BrainModel)): raise Cifti2HeaderError( - 'VoxelIndicesIJK element can only be a child of the CIFTI2 ' + 'VoxelIndicesIJK element can only be a child of the CIFTI-2 ' 'Parcel or BrainModel elements' ) parent.voxel_indices_ijk = Cifti2VoxelIndicesIJK() @@ -325,7 +326,7 @@ def StartElementHandler(self, name, attrs): mim = self.struct_state[-1] if not isinstance(mim, Cifti2MatrixIndicesMap): raise Cifti2HeaderError( - 'Volume element can only be a child of the CIFTI2 MatrixIndicesMap element' + 'Volume element can only be a child of the CIFTI-2 MatrixIndicesMap element' ) dimensions = tuple([int(val) for val in attrs["VolumeDimensions"].split(',')]) @@ -339,7 +340,7 @@ def StartElementHandler(self, name, attrs): if not isinstance(volume, Cifti2Volume): raise Cifti2HeaderError( 'TransformationMatrixVoxelIndicesIJKtoXYZ element can only be a child ' - 'of the CIFTI2 Volume element' + 'of the CIFTI-2 Volume element' ) transform = Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ() transform.meter_exponent = int(attrs["MeterExponent"]) @@ -354,7 +355,7 @@ def StartElementHandler(self, name, attrs): if not isinstance(mim, Cifti2MatrixIndicesMap): raise Cifti2HeaderError( 'BrainModel element can only be a child ' - 'of the CIFTI2 MatrixIndicesMap element' + 'of the CIFTI-2 MatrixIndicesMap element' ) if mim.indices_map_to_data_type != "CIFTI_INDEX_TYPE_BRAIN_MODELS": raise Cifti2HeaderError( @@ -386,7 +387,7 @@ def StartElementHandler(self, name, attrs): if not isinstance(model, Cifti2BrainModel): raise Cifti2HeaderError( 'VertexIndices element can only be a child ' - 'of the CIFTI2 BrainModel element' + 'of the CIFTI-2 BrainModel element' ) self.fsm_state.append('VertexIndices') model.vertex_indices = index diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index 150bb88300..cd7682408d 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -101,7 +101,7 @@ def get_axes(): def test_brain_models(): """ - Tests the introspection and creation of CIFTI2 BrainModel axes + Tests the introspection and creation of CIFTI-2 BrainModel axes """ bml = list(get_brain_models()) assert len(bml[0]) == 3 @@ -306,7 +306,7 @@ def test_brain_models(): def test_parcels(): """ - Test the introspection and creation of CIFTI2 Parcel axes + Test the introspection and creation of CIFTI-2 Parcel axes """ prc = get_parcels() assert isinstance(prc, axes.Parcels) @@ -446,7 +446,7 @@ def test_parcels(): def test_scalar(): """ - Test the introspection and creation of CIFTI2 Scalar axes + Test the introspection and creation of CIFTI-2 Scalar axes """ sc = get_scalar() assert len(sc) == 3 @@ -494,7 +494,7 @@ def test_scalar(): def test_label(): """ - Test the introspection and creation of CIFTI2 Scalar axes + Test the introspection and creation of CIFTI-2 Scalar axes """ lab = get_label() assert len(lab) == 3 @@ -549,7 +549,7 @@ def test_label(): def test_series(): """ - Test the introspection and creation of CIFTI2 Series axes + Test the introspection and creation of CIFTI-2 Series axes """ sr = list(get_series()) assert sr[0].unit == 'SECOND' @@ -618,7 +618,7 @@ def test_series(): def test_writing(): """ - Tests the writing and reading back in of custom created CIFTI2 axes + Tests the writing and reading back in of custom created CIFTI-2 axes """ for ax1 in get_axes(): for ax2 in get_axes(): @@ -628,7 +628,7 @@ def test_writing(): def test_common_interface(): """ - Tests the common interface for all custom created CIFTI2 axes + Tests the common interface for all custom created CIFTI-2 axes """ for axis1, axis2 in zip(get_axes(), get_axes()): assert axis1 == axis2 diff --git a/nibabel/cifti2/tests/test_cifti2.py b/nibabel/cifti2/tests/test_cifti2.py index ce71b92bcc..6054c126b0 100644 --- a/nibabel/cifti2/tests/test_cifti2.py +++ b/nibabel/cifti2/tests/test_cifti2.py @@ -1,4 +1,4 @@ -""" Testing CIFTI2 objects +""" Testing CIFTI-2 objects """ import collections from xml.etree import ElementTree diff --git a/nibabel/cifti2/tests/test_cifti2io_header.py b/nibabel/cifti2/tests/test_cifti2io_header.py index 521e112847..e4970625a4 100644 --- a/nibabel/cifti2/tests/test_cifti2io_header.py +++ b/nibabel/cifti2/tests/test_cifti2io_header.py @@ -43,7 +43,7 @@ def test_read_nifti2(): - # Error trying to read a CIFTI2 image from a NIfTI2-only image. + # Error trying to read a CIFTI-2 image from a NIfTI2-only image. filemap = ci.Cifti2Image.make_file_map() for k in filemap: filemap[k].fileobj = io.open(NIFTI2_DATA) diff --git a/nibabel/cifti2/tests/test_name.py b/nibabel/cifti2/tests/test_name.py index a73c5e8c46..b656f88875 100644 --- a/nibabel/cifti2/tests/test_name.py +++ b/nibabel/cifti2/tests/test_name.py @@ -10,7 +10,7 @@ def test_name_conversion(): """ - Tests the automatic name conversion to a format recognized by CIFTI2 + Tests the automatic name conversion to a format recognized by CIFTI-2 """ func = cifti2_axes.BrainModel.to_cifti_brain_structure_name for base_name, input_names in equivalents: diff --git a/nibabel/cifti2/tests/test_new_cifti2.py b/nibabel/cifti2/tests/test_new_cifti2.py index 9ead1e3088..01bc742a22 100644 --- a/nibabel/cifti2/tests/test_new_cifti2.py +++ b/nibabel/cifti2/tests/test_new_cifti2.py @@ -1,4 +1,4 @@ -"""Tests the generation of new CIFTI2 files from scratch +"""Tests the generation of new CIFTI-2 files from scratch Contains a series of functions to create and check each of the 5 CIFTI index types (i.e. BRAIN_MODELS, PARCELS, SCALARS, LABELS, and SERIES). From 532bed968be43a853298e3f5ab432684ba0ab586 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Wed, 27 Mar 2019 10:53:20 +0000 Subject: [PATCH 43/57] RF: appended Axis to the different axes classes --- nibabel/cifti2/__init__.py | 2 +- nibabel/cifti2/cifti2_axes.py | 205 +++++++++++---------- nibabel/cifti2/tests/test_axes.py | 138 +++++++------- nibabel/cifti2/tests/test_cifti2io_axes.py | 20 +- nibabel/cifti2/tests/test_name.py | 2 +- 5 files changed, 184 insertions(+), 183 deletions(-) diff --git a/nibabel/cifti2/__init__.py b/nibabel/cifti2/__init__.py index dc88a24b48..9dc6dd68b8 100644 --- a/nibabel/cifti2/__init__.py +++ b/nibabel/cifti2/__init__.py @@ -26,4 +26,4 @@ Cifti2TransformationMatrixVoxelIndicesIJKtoXYZ, Cifti2Vertices, Cifti2Volume, CIFTI_BRAIN_STRUCTURES, Cifti2HeaderError, CIFTI_MODEL_TYPES, load, save) -from .cifti2_axes import (Axis, BrainModel, Parcels, Series, Label, Scalar) \ No newline at end of file +from .cifti2_axes import (Axis, BrainModelAxis, ParcelsAxis, SeriesAxis, LabelAxis, ScalarAxis) \ No newline at end of file diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index e0f8c0f7ea..90d3c6f4c4 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -4,11 +4,11 @@ These axes provide an alternative interface to the information in the CIFTI-2 header. Each type of CIFTI-2 axes describing the rows/columns in a CIFTI-2 matrix is given a unique class: -* :class:`BrainModel`: each row/column is a voxel or vertex -* :class:`Parcels`: each row/column is a group of voxels and/or vertices -* :class:`Scalar`: each row/column has a unique name (with optional meta-data) -* :class:`Label`: each row/column has a unique name and label table (with optional meta-data) -* :class:`Series`: each row/column is a timepoint, which increases monotonically +* :class:`BrainModelAxis`: each row/column is a voxel or vertex +* :class:`ParcelsAxis`: each row/column is a group of voxels and/or vertices +* :class:`ScalarAxis`: each row/column has a unique name (with optional meta-data) +* :class:`LabelAxis`: each row/column has a unique name and label table (with optional meta-data) +* :class:`SeriesAxis`: each row/column is a timepoint, which increases monotonically All of these classes are derived from the :class:`Axis` class. @@ -20,43 +20,43 @@ CIFTI-2 :class:`Axis` objects of the same type can be concatenated using the '+'-operator. Numpy indexing also works on axes -(except for Series objects, which have to remain monotonically increasing or decreasing). +(except for SeriesAxis objects, which have to remain monotonically increasing or decreasing). Creating new CIFTI-2 axes ----------------------- New :class:`Axis` objects can be constructed by providing a description for what is contained in each row/column of the described tensor. For each :class:`Axis` sub-class this descriptor is: -* :class:`BrainModel`: a CIFTI-2 structure name and a voxel or vertex index -* :class:`Parcels`: a name and a sequence of voxel and vertex indices -* :class:`Scalar`: a name and optionally a dict of meta-data -* :class:`Label`: a name, dict of label index to name and colour, +* :class:`BrainModelAxis`: a CIFTI-2 structure name and a voxel or vertex index +* :class:`ParcelsAxis`: a name and a sequence of voxel and vertex indices +* :class:`ScalarAxis`: a name and optionally a dict of meta-data +* :class:`LabelAxis`: a name, dict of label index to name and colour, and optionally a dict of meta-data -* :class:`Series`: the time-point of each row/column is set by setting the start, stop, size, +* :class:`SeriesAxis`: the time-point of each row/column is set by setting the start, stop, size, and unit of the time-series -Several helper functions exist to create new :class:`BrainModel` axes: +Several helper functions exist to create new :class:`BrainModelAxis` axes: -* :meth:`BrainModel.from_mask` creates a new BrainModel volume covering the +* :meth:`BrainModelAxis.from_mask` creates a new BrainModelAxis volume covering the non-zero values of a mask -* :meth:`BrainModel.from_surface` creates a new BrainModel surface covering the provided +* :meth:`BrainModelAxis.from_surface` creates a new BrainModelAxis surface covering the provided indices of a surface -A :class:`Parcels` axis can be created from a sequence of :class:`BrainModel` axes using -:meth:`Parcels.from_brain_models`. +A :class:`ParcelsAxis` axis can be created from a sequence of :class:`BrainModelAxis` axes using +:meth:`ParcelsAxis.from_brain_models`. Examples -------- We can create brain models covering the left cortex and left thalamus using: >>> from nibabel import cifti2 ->>> bm_cortex = cifti2.BrainModel.from_mask(cortex_mask, +>>> bm_cortex = cifti2.BrainModelAxis.from_mask(cortex_mask, ... brain_structure='cortex_left') # doctest: +SKIP ->>> bm_thal = cifti2.BrainModel.from_mask(thalamus_mask, affine=affine, +>>> bm_thal = cifti2.BrainModelAxis.from_mask(thalamus_mask, affine=affine, ... brain_structure='thalamus_left') # doctest: +SKIP Brain structure names automatically get converted to valid CIFTI-2 indentifiers using -:meth:`BrainModel.to_cifti_brain_structure_name`. +:meth:`BrainModelAxis.to_cifti_brain_structure_name`. A 1-dimensional mask will be automatically interpreted as a surface element and a 3-dimensional mask as a volume element. @@ -83,9 +83,9 @@ and ('CIFTI_STRUCTURE_THALAMUS_LEFT', slice(, None), bm_thal) -Parcels can be constructed from selections of these brain models: +ParcelsAxis can be constructed from selections of these brain models: ->>> parcel = cifti2.Parcels.from_brain_models([ +>>> parcel = cifti2.ParcelsAxis.from_brain_models([ ... ('surface_parcel', bm_cortex[:100]), # contains first 100 cortical vertices ... ('volume_parcel', bm_thal), # contains thalamus ... ('combined_parcel', bm_full[[1, 8, 10, 120, 127]) # contains selected voxels/vertices @@ -94,7 +94,7 @@ Time series are represented by their starting time (typically 0), step size (i.e. sampling time or TR), and number of elements: ->>> series = cifti2.Series(start=0, step=100, size=5000) # doctest: +SKIP +>>> series = cifti2.SeriesAxis(start=0, step=100, size=5000) # doctest: +SKIP So a header for fMRI data with a TR of 100 ms covering the left cortex and thalamus with 5000 timepoints could be created with @@ -104,7 +104,7 @@ Similarly the curvature and cortical thickness on the left cortex could be stored using a header like: ->>> cifti2.Cifti2Header.from_axes((cifti.Scalar(['curvature', 'thickness'], +>>> cifti2.Cifti2Header.from_axes((cifti.ScalarAxis(['curvature', 'thickness'], ... bm_cortex)) # doctest: +SKIP """ import numpy as np @@ -126,11 +126,11 @@ def from_index_mapping(mim): ------- subtype of Axis """ - return_type = {'CIFTI_INDEX_TYPE_SCALARS': Scalar, - 'CIFTI_INDEX_TYPE_LABELS': Label, - 'CIFTI_INDEX_TYPE_SERIES': Series, - 'CIFTI_INDEX_TYPE_BRAIN_MODELS': BrainModel, - 'CIFTI_INDEX_TYPE_PARCELS': Parcels} + return_type = {'CIFTI_INDEX_TYPE_SCALARS': ScalarAxis, + 'CIFTI_INDEX_TYPE_LABELS': LabelAxis, + 'CIFTI_INDEX_TYPE_SERIES': SeriesAxis, + 'CIFTI_INDEX_TYPE_BRAIN_MODELS': BrainModelAxis, + 'CIFTI_INDEX_TYPE_PARCELS': ParcelsAxis} return return_type[mim.indices_map_to_data_type].from_index_mapping(mim) @@ -218,7 +218,7 @@ def __getitem__(self, item): pass -class BrainModel(Axis): +class BrainModelAxis(Axis): """ Each row/column in the CIFTI-2 vector/matrix represents a single vertex or voxel @@ -228,15 +228,15 @@ class BrainModel(Axis): def __init__(self, name, voxel=None, vertex=None, affine=None, volume_shape=None, nvertices=None): """ - New BrainModel axes can be constructed by passing on the greyordinate brain-structure + New BrainModelAxis axes can be constructed by passing on the greyordinate brain-structure names and voxel/vertex indices to the constructor or by one of the factory methods: - - :py:meth:`~BrainModel.from_mask`: creates surface or volumetric BrainModel axis + - :py:meth:`~BrainModelAxis.from_mask`: creates surface or volumetric BrainModelAxis axis from respectively 1D or 3D masks - - :py:meth:`~BrainModel.from_surface`: creates a surface BrainModel axis + - :py:meth:`~BrainModelAxis.from_surface`: creates a surface BrainModelAxis axis - The resulting BrainModel axes can be concatenated by adding them together. + The resulting BrainModelAxis axes can be concatenated by adding them together. Parameters ---------- @@ -291,7 +291,7 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, else: if affine is None or volume_shape is None: raise ValueError("Affine and volume shape should be defined " - "for BrainModel containing voxels") + "for BrainModelAxis containing voxels") self.affine = affine self.volume_shape = volume_shape @@ -303,18 +303,18 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, for check_name in ('name', 'voxel', 'vertex'): shape = (self.size, 3) if check_name == 'voxel' else (self.size, ) if getattr(self, check_name).shape != shape: - raise ValueError("Input {} has incorrect shape ({}) for BrainModel axis".format( + raise ValueError("Input {} has incorrect shape ({}) for BrainModelAxis axis".format( check_name, getattr(self, check_name).shape)) @classmethod def from_mask(cls, mask, name='other', affine=None): """ - Creates a new BrainModel axis describing the provided mask + Creates a new BrainModelAxis axis describing the provided mask Parameters ---------- mask : np.ndarray - all non-zero voxels will be included in the BrainModel axis + all non-zero voxels will be included in the BrainModelAxis axis should be (Nx, Ny, Nz) array for volume mask or (Nvertex, ) array for surface mask name : str Name of the brain structure (e.g. 'CortexRight', 'thalamus_left' or 'brain_stem') @@ -324,7 +324,7 @@ def from_mask(cls, mask, name='other', affine=None): Returns ------- - BrainModel which covers the provided mask + BrainModelAxis which covers the provided mask """ if affine is None: affine = np.eye(4) @@ -344,7 +344,7 @@ def from_mask(cls, mask, name='other', affine=None): @classmethod def from_surface(cls, vertices, nvertex, name='Other'): """ - Creates a new BrainModel axis describing the vertices on a surface + Creates a new BrainModelAxis axis describing the vertices on a surface Parameters ---------- @@ -357,7 +357,7 @@ def from_surface(cls, vertices, nvertex, name='Other'): Returns ------- - BrainModel which covers (part of) the surface + BrainModelAxis which covers (part of) the surface """ cifti_name = cls.to_cifti_brain_structure_name(name) return cls(cifti_name, vertex=vertices, @@ -374,7 +374,7 @@ def from_index_mapping(cls, mim): Returns ------- - BrainModel + BrainModelAxis """ nbm = sum(bm.index_count for bm in mim.brain_models) voxel = np.full((nbm, 3), fill_value=-1, dtype=int) @@ -588,7 +588,7 @@ def __len__(self): return self.name.size def __eq__(self, other): - if not isinstance(other, BrainModel) or len(self) != len(other): + if not isinstance(other, BrainModelAxis) or len(self) != len(other): return False if xor(self.affine is None, other.affine is None): return False @@ -608,14 +608,14 @@ def __add__(self, other): Parameters ---------- - other : BrainModel + other : BrainModelAxis brain model to be appended to the current one Returns ------- - BrainModel + BrainModelAxis """ - if not isinstance(other, BrainModel): + if not isinstance(other, BrainModelAxis): return NotImplemented if self.affine is None: affine, shape = other.affine, other.volume_shape @@ -656,12 +656,12 @@ def __getitem__(self, item): - vertex index if it is a surface element, otherwise array with 3 voxel indices - structure.BrainStructure object describing the brain structure the element was taken from - Otherwise returns a new BrainModel + Otherwise returns a new BrainModelAxis """ if isinstance(item, integer_types): return self.get_element(item) if isinstance(item, string_types): - raise IndexError("Can not index an Axis with a string (except for Parcels)") + raise IndexError("Can not index an Axis with a string (except for ParcelsAxis)") return self.__class__(self.name[item], self.voxel[item], self.vertex[item], self.affine, self.volume_shape, self.nvertices) @@ -688,7 +688,7 @@ def get_element(self, index): return element_type, struct[index], self.name[index] -class Parcels(Axis): +class ParcelsAxis(Axis): """ Each row/column in the CIFTI-2 vector/matrix represents a parcel of voxels/vertices @@ -700,8 +700,9 @@ class Parcels(Axis): def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvertices=None): """ - Use of this constructor is not recommended. New Parcels axes can be constructed more easily - from a sequence of BrainModel axes using :py:meth:`~Parcels.from_brain_models` + Use of this constructor is not recommended. New ParcelsAxis axes can be constructed more + easily from a sequence of BrainModelAxis axes using + :py:meth:`~ParcelsAxis.from_brain_models` Parameters ---------- @@ -738,7 +739,7 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert if nvertices is None: self.nvertices = {} else: - self.nvertices = {BrainModel.to_cifti_brain_structure_name(name): number + self.nvertices = {BrainModelAxis.to_cifti_brain_structure_name(name): number for name, number in nvertices.items()} for check_name in ('name', 'voxels', 'vertices'): @@ -749,16 +750,16 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert @classmethod def from_brain_models(cls, named_brain_models): """ - Creates a Parcel axis from a list of BrainModel axes with names + Creates a Parcel axis from a list of BrainModelAxis axes with names Parameters ---------- - named_brain_models : iterable of 2-element tuples of string and BrainModel + named_brain_models : iterable of 2-element tuples of string and BrainModelAxis list of (parcel name, brain model representation) pairs defining each parcel Returns ------- - Parcels + ParcelsAxis """ nparcels = len(named_brain_models) affine = None @@ -789,7 +790,7 @@ def from_brain_models(cls, named_brain_models): nvertices[name] = bm.nvertices[name] vertices[name] = bm_part.vertex all_vertices[idx_parcel] = vertices - return Parcels(all_names, all_voxels, all_vertices, affine, volume_shape, nvertices) + return ParcelsAxis(all_names, all_voxels, all_vertices, affine, volume_shape, nvertices) @classmethod def from_index_mapping(cls, mim): @@ -802,7 +803,7 @@ def from_index_mapping(cls, mim): Returns ------- - Parcels + ParcelsAxis """ nparcels = len(list(mim.parcels)) all_names = [] @@ -928,14 +929,14 @@ def __add__(self, other): Parameters ---------- - other : Parcels + other : ParcelsAxis parcel to be appended to the current one Returns ------- Parcel """ - if not isinstance(other, Parcels): + if not isinstance(other, ParcelsAxis): return NotImplemented if self.affine is None: affine, shape = other.affine, other.volume_shape @@ -943,12 +944,12 @@ def __add__(self, other): affine, shape = self.affine, self.volume_shape if other.affine is not None and (not np.allclose(other.affine, affine) or other.volume_shape != shape): - raise ValueError("Trying to concatenate two Parcels defined " + raise ValueError("Trying to concatenate two ParcelsAxis defined " "in a different brain volume") nvertices = dict(self.nvertices) for name, value in other.nvertices.items(): if name in nvertices.keys() and nvertices[name] != value: - raise ValueError("Trying to concatenate two Parcels with inconsistent " + raise ValueError("Trying to concatenate two ParcelsAxis with inconsistent " "number of vertices for %s" % name) nvertices[name] = value @@ -998,7 +999,7 @@ def get_element(self, index): return self.name[index], self.voxels[index], self.vertices[index] -class Scalar(Axis): +class ScalarAxis(Axis): """ Along this axis of the CIFTI-2 vector/matrix each row/column has been given a unique name and optionally metadata @@ -1021,7 +1022,7 @@ def __init__(self, name, meta=None): for check_name in ('name', 'meta'): if getattr(self, check_name).shape != (self.size, ): - raise ValueError("Input {} has incorrect shape ({}) for Scalar axis".format( + raise ValueError("Input {} has incorrect shape ({}) for ScalarAxis axis".format( check_name, getattr(self, check_name).shape)) @classmethod @@ -1035,7 +1036,7 @@ def from_index_mapping(cls, mim): Returns ------- - Scalar + ScalarAxis """ names = [nm.map_name for nm in mim.named_maps] meta = [{} if nm.metadata is None else dict(nm.metadata) for nm in mim.named_maps] @@ -1070,14 +1071,14 @@ def __eq__(self, other): Parameters ---------- - other : Scalar + other : ScalarAxis scalar axis to be compared Returns ------- bool : False if type, length or content do not match """ - if not isinstance(other, Scalar) or self.size != other.size: + if not isinstance(other, ScalarAxis) or self.size != other.size: return False return np.array_equal(self.name, other.name) and np.array_equal(self.meta, other.meta) @@ -1087,16 +1088,16 @@ def __add__(self, other): Parameters ---------- - other : Scalar + other : ScalarAxis scalar axis to be appended to the current one Returns ------- - Scalar + ScalarAxis """ - if not isinstance(other, Scalar): + if not isinstance(other, ScalarAxis): return NotImplemented - return Scalar( + return ScalarAxis( np.append(self.name, other.name), np.append(self.meta, other.meta), ) @@ -1124,7 +1125,7 @@ def get_element(self, index): return self.name[index], self.meta[index] -class Label(Axis): +class LabelAxis(Axis): """ Defines CIFTI-2 axis for label array. @@ -1155,7 +1156,7 @@ def __init__(self, name, label, meta=None): for check_name in ('name', 'meta', 'label'): if getattr(self, check_name).shape != (self.size, ): - raise ValueError("Input {} has incorrect shape ({}) for Label axis".format( + raise ValueError("Input {} has incorrect shape ({}) for LabelAxis axis".format( check_name, getattr(self, check_name).shape)) @classmethod @@ -1169,12 +1170,12 @@ def from_index_mapping(cls, mim): Returns ------- - Label + LabelAxis """ tables = [{key: (value.label, value.rgba) for key, value in nm.label_table.items()} for nm in mim.named_maps] - rest = Scalar.from_index_mapping(mim) - return Label(rest.name, tables, rest.meta) + rest = ScalarAxis.from_index_mapping(mim) + return LabelAxis(rest.name, tables, rest.meta) def to_mapping(self, dim): """ @@ -1210,14 +1211,14 @@ def __eq__(self, other): Parameters ---------- - other : Label + other : LabelAxis label axis to be compared Returns ------- bool : False if type, length or content do not match """ - if not isinstance(other, Label) or self.size != other.size: + if not isinstance(other, LabelAxis) or self.size != other.size: return False return ( np.array_equal(self.name, other.name) and @@ -1231,16 +1232,16 @@ def __add__(self, other): Parameters ---------- - other : Label + other : LabelAxis label axis to be appended to the current one Returns ------- - Label + LabelAxis """ - if not isinstance(other, Label): + if not isinstance(other, LabelAxis): return NotImplemented - return Label( + return LabelAxis( np.append(self.name, other.name), np.append(self.label, other.label), np.append(self.meta, other.meta), @@ -1270,7 +1271,7 @@ def get_element(self, index): return self.name[index], self.label[index], self.meta[index] -class Series(Axis): +class SeriesAxis(Axis): """ Along this axis of the CIFTI-2 vector/matrix the rows/columns increase monotonously in time @@ -1280,7 +1281,7 @@ class Series(Axis): def __init__(self, start, step, size, unit="SECOND"): """ - Creates a new Series axis + Creates a new SeriesAxis axis Parameters ---------- @@ -1305,7 +1306,7 @@ def time(self): @classmethod def from_index_mapping(cls, mim): """ - Creates a new Series axis based on a CIFTI-2 dataset + Creates a new SeriesAxis axis based on a CIFTI-2 dataset Parameters ---------- @@ -1313,7 +1314,7 @@ def from_index_mapping(cls, mim): Returns ------- - Series + SeriesAxis """ start = mim.series_start * 10 ** mim.series_exponent step = mim.series_step * 10 ** mim.series_exponent @@ -1321,7 +1322,7 @@ def from_index_mapping(cls, mim): def to_mapping(self, dim): """ - Converts the Series to a MatrixIndicesMap for storage in CIFTI-2 format + Converts the SeriesAxis to a MatrixIndicesMap for storage in CIFTI-2 format Parameters ---------- @@ -1349,7 +1350,7 @@ def unit(self): @unit.setter def unit(self, value): if value.upper() not in ("SECOND", "HERTZ", "METER", "RADIAN"): - raise ValueError("Series unit should be one of " + + raise ValueError("SeriesAxis unit should be one of " + "('second', 'hertz', 'meter', or 'radian'") self._unit = value.upper() @@ -1361,7 +1362,7 @@ def __eq__(self, other): True if start, step, size, and unit are the same. """ return ( - isinstance(other, Series) and + isinstance(other, SeriesAxis) and self.start == other.start and self.step == other.step and self.size == other.size and @@ -1370,30 +1371,30 @@ def __eq__(self, other): def __add__(self, other): """ - Concatenates two Series + Concatenates two SeriesAxis Parameters ---------- - other : Series - Time Series to append at the end of the current time Series. - Note that the starting time of the other time Series is ignored. + other : SeriesAxis + Time SeriesAxis to append at the end of the current time SeriesAxis. + Note that the starting time of the other time SeriesAxis is ignored. Returns ------- - Series - New time Series with the concatenation of the two + SeriesAxis + New time SeriesAxis with the concatenation of the two Raises ------ ValueError - raised if the repetition time of the two time Series is different + raised if the repetition time of the two time SeriesAxis is different """ - if isinstance(other, Series): + if isinstance(other, SeriesAxis): if other.step != self.step: - raise ValueError('Can only concatenate Series with the same step size') + raise ValueError('Can only concatenate SeriesAxis with the same step size') if other.unit != self.unit: - raise ValueError('Can only concatenate Series with the same unit') - return Series(self.start, self.step, self.size + other.size, self.unit) + raise ValueError('Can only concatenate SeriesAxis with the same unit') + return SeriesAxis(self.start, self.step, self.size + other.size, self.unit) return NotImplemented def __getitem__(self, item): @@ -1412,11 +1413,11 @@ def __getitem__(self, item): nelements = (idx_end - idx_start) // step if nelements < 0: nelements = 0 - return Series(idx_start * self.step + self.start, self.step * step, - nelements, self.unit) + return SeriesAxis(idx_start * self.step + self.start, self.step * step, + nelements, self.unit) elif isinstance(item, integer_types): return self.get_element(item) - raise IndexError('Series can only be indexed with integers or slices ' + raise IndexError('SeriesAxis can only be indexed with integers or slices ' 'without breaking the regular structure') def get_element(self, index): @@ -1436,6 +1437,6 @@ def get_element(self, index): if index < 0: index = self.size + index if index >= self.size or index < 0: - raise IndexError("index %i is out of range for Series with size %i" % + raise IndexError("index %i is out of range for SeriesAxis with size %i" % (original_index, self.size)) return self.start + self.step * index diff --git a/nibabel/cifti2/tests/test_axes.py b/nibabel/cifti2/tests/test_axes.py index cd7682408d..56457187a2 100644 --- a/nibabel/cifti2/tests/test_axes.py +++ b/nibabel/cifti2/tests/test_axes.py @@ -12,26 +12,26 @@ def get_brain_models(): """ - Generates a set of practice BrainModel axes + Generates a set of practice BrainModelAxis axes Yields ------ - BrainModel axis + BrainModelAxis axis """ mask = np.zeros(vol_shape) mask[0, 1, 2] = 1 mask[0, 4, 2] = True mask[0, 4, 0] = True - yield axes.BrainModel.from_mask(mask, 'ThalamusRight', rand_affine) + yield axes.BrainModelAxis.from_mask(mask, 'ThalamusRight', rand_affine) mask[0, 0, 0] = True - yield axes.BrainModel.from_mask(mask, affine=rand_affine) + yield axes.BrainModelAxis.from_mask(mask, affine=rand_affine) - yield axes.BrainModel.from_surface([0, 5, 10], 15, 'CortexLeft') - yield axes.BrainModel.from_surface([0, 5, 10, 13], 15) + yield axes.BrainModelAxis.from_surface([0, 5, 10], 15, 'CortexLeft') + yield axes.BrainModelAxis.from_surface([0, 5, 10, 13], 15) surface_mask = np.zeros(15, dtype='bool') surface_mask[[2, 9, 14]] = True - yield axes.BrainModel.from_mask(surface_mask, name='CortexRight') + yield axes.BrainModelAxis.from_mask(surface_mask, name='CortexRight') def get_parcels(): @@ -43,43 +43,43 @@ def get_parcels(): Parcel axis """ bml = list(get_brain_models()) - return axes.Parcels.from_brain_models([('mixed', bml[0] + bml[2]), ('volume', bml[1]), ('surface', bml[3])]) + return axes.ParcelsAxis.from_brain_models([('mixed', bml[0] + bml[2]), ('volume', bml[1]), ('surface', bml[3])]) def get_scalar(): """ - Generates a practice Scalar axis with names ('one', 'two', 'three') + Generates a practice ScalarAxis axis with names ('one', 'two', 'three') Returns ------- - Scalar axis + ScalarAxis axis """ - return axes.Scalar(['one', 'two', 'three']) + return axes.ScalarAxis(['one', 'two', 'three']) def get_label(): """ - Generates a practice Label axis with names ('one', 'two', 'three') and two labels + Generates a practice LabelAxis axis with names ('one', 'two', 'three') and two labels Returns ------- - Label axis + LabelAxis axis """ - return axes.Label(['one', 'two', 'three'], use_label) + return axes.LabelAxis(['one', 'two', 'three'], use_label) def get_series(): """ - Generates a set of 4 practice Series axes with different starting times/lengths/time steps and units + Generates a set of 4 practice SeriesAxis axes with different starting times/lengths/time steps and units Yields ------ - Series axis + SeriesAxis axis """ - yield axes.Series(3, 10, 4) - yield axes.Series(8, 10, 3) - yield axes.Series(3, 2, 4) - yield axes.Series(5, 10, 5, "HERTZ") + yield axes.SeriesAxis(3, 10, 4) + yield axes.SeriesAxis(8, 10, 3) + yield axes.SeriesAxis(3, 2, 4) + yield axes.SeriesAxis(5, 10, 5, "HERTZ") def get_axes(): @@ -101,7 +101,7 @@ def get_axes(): def test_brain_models(): """ - Tests the introspection and creation of CIFTI-2 BrainModel axes + Tests the introspection and creation of CIFTI-2 BrainModelAxis axes """ bml = list(get_brain_models()) assert len(bml[0]) == 3 @@ -109,7 +109,7 @@ def test_brain_models(): assert (bml[0].voxel == [[0, 1, 2], [0, 4, 0], [0, 4, 2]]).all() assert bml[0][1][0] == 'CIFTI_MODEL_TYPE_VOXELS' assert (bml[0][1][1] == [0, 4, 0]).all() - assert bml[0][1][2] == axes.BrainModel.to_cifti_brain_structure_name('thalamus_right') + assert bml[0][1][2] == axes.BrainModelAxis.to_cifti_brain_structure_name('thalamus_right') assert len(bml[1]) == 4 assert (bml[1].vertex == -1).all() assert (bml[1].voxel == [[0, 0, 0], [0, 1, 2], [0, 4, 0], [0, 4, 2]]).all() @@ -131,7 +131,7 @@ def test_brain_models(): structures = list(bm.iter_structures()) assert len(structures) == 1 name = structures[0][0] - assert name == axes.BrainModel.to_cifti_brain_structure_name(label) + assert name == axes.BrainModelAxis.to_cifti_brain_structure_name(label) if is_surface: assert bm.nvertices[name] == 15 else: @@ -172,51 +172,51 @@ def test_brain_models(): bmt['thalamus_left'] # Test the constructor - bm_vox = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + bm_vox = axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) assert np.all(bm_vox.name == ['CIFTI_STRUCTURE_THALAMUS_LEFT'] * 5) assert np.array_equal(bm_vox.vertex, np.full(5, -1)) assert np.array_equal(bm_vox.voxel, np.full((5, 3), 1)) with assert_raises(ValueError): # no volume shape - axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4)) + axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4)) with assert_raises(ValueError): # no affine - axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), volume_shape=(2, 3, 4)) + axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), volume_shape=(2, 3, 4)) with assert_raises(ValueError): # incorrect name - axes.BrainModel('random_name', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + axes.BrainModelAxis('random_name', voxel=np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) with assert_raises(ValueError): # negative voxel indices - axes.BrainModel('thalamus_left', voxel=-np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + axes.BrainModelAxis('thalamus_left', voxel=-np.ones((5, 3), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) with assert_raises(ValueError): # no voxels or vertices - axes.BrainModel('thalamus_left', affine=np.eye(4), volume_shape=(2, 3, 4)) + axes.BrainModelAxis('thalamus_left', affine=np.eye(4), volume_shape=(2, 3, 4)) with assert_raises(ValueError): # incorrect voxel shape - axes.BrainModel('thalamus_left', voxel=np.ones((5, 2), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) + axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 2), dtype=int), affine=np.eye(4), volume_shape=(2, 3, 4)) - bm_vertex = axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + bm_vertex = axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) assert np.array_equal(bm_vertex.name, ['CIFTI_STRUCTURE_CORTEX_LEFT'] * 5) assert np.array_equal(bm_vertex.vertex, np.full(5, 1)) assert np.array_equal(bm_vertex.voxel, np.full((5, 3), -1)) with assert_raises(ValueError): - axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int)) + axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int)) with assert_raises(ValueError): - axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_right': 20}) + axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_right': 20}) with assert_raises(ValueError): - axes.BrainModel('cortex_left', vertex=-np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + axes.BrainModelAxis('cortex_left', vertex=-np.ones(5, dtype=int), nvertices={'cortex_left': 20}) # test from_mask errors with assert_raises(ValueError): # affine should be 4x4 matrix - axes.BrainModel.from_mask(np.arange(5) > 2, affine=np.ones(5)) + axes.BrainModelAxis.from_mask(np.arange(5) > 2, affine=np.ones(5)) with assert_raises(ValueError): # only 1D or 3D masks accepted - axes.BrainModel.from_mask(np.ones((5, 3))) + axes.BrainModelAxis.from_mask(np.ones((5, 3))) - # tests error in adding together or combining as Parcels - bm_vox = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), - affine=np.eye(4), volume_shape=(2, 3, 4)) + # tests error in adding together or combining as ParcelsAxis + bm_vox = axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4), volume_shape=(2, 3, 4)) bm_vox + bm_vox assert (bm_vertex + bm_vox)[:bm_vertex.size] == bm_vertex assert (bm_vox + bm_vertex)[:bm_vox.size] == bm_vox @@ -225,33 +225,33 @@ def test_brain_models(): assert np.all(bm_added.affine == bm_vox.affine) assert bm_added.volume_shape == bm_vox.volume_shape - axes.Parcels.from_brain_models([('a', bm_vox), ('b', bm_vox)]) + axes.ParcelsAxis.from_brain_models([('a', bm_vox), ('b', bm_vox)]) with assert_raises(Exception): bm_vox + get_label() - bm_other_shape = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), - affine=np.eye(4), volume_shape=(4, 3, 4)) + bm_other_shape = axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4), volume_shape=(4, 3, 4)) with assert_raises(ValueError): bm_vox + bm_other_shape with assert_raises(ValueError): - axes.Parcels.from_brain_models([('a', bm_vox), ('b', bm_other_shape)]) - bm_other_affine = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), - affine=np.eye(4) * 2, volume_shape=(2, 3, 4)) + axes.ParcelsAxis.from_brain_models([('a', bm_vox), ('b', bm_other_shape)]) + bm_other_affine = axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4) * 2, volume_shape=(2, 3, 4)) with assert_raises(ValueError): bm_vox + bm_other_affine with assert_raises(ValueError): - axes.Parcels.from_brain_models([('a', bm_vox), ('b', bm_other_affine)]) + axes.ParcelsAxis.from_brain_models([('a', bm_vox), ('b', bm_other_affine)]) - bm_vertex = axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) - bm_other_number = axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 30}) + bm_vertex = axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + bm_other_number = axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 30}) with assert_raises(ValueError): bm_vertex + bm_other_number with assert_raises(ValueError): - axes.Parcels.from_brain_models([('a', bm_vertex), ('b', bm_other_number)]) + axes.ParcelsAxis.from_brain_models([('a', bm_vertex), ('b', bm_other_number)]) # test equalities - bm_vox = axes.BrainModel('thalamus_left', voxel=np.ones((5, 3), dtype=int), - affine=np.eye(4), volume_shape=(2, 3, 4)) + bm_vox = axes.BrainModelAxis('thalamus_left', voxel=np.ones((5, 3), dtype=int), + affine=np.eye(4), volume_shape=(2, 3, 4)) bm_other = deepcopy(bm_vox) assert bm_vox == bm_other bm_other.voxel[1, 0] = 0 @@ -259,7 +259,7 @@ def test_brain_models(): bm_other = deepcopy(bm_vox) bm_other.vertex[1] = 10 - assert bm_vox == bm_other, 'vertices are ignored in volumetric BrainModel' + assert bm_vox == bm_other, 'vertices are ignored in volumetric BrainModelAxis' bm_other = deepcopy(bm_vox) bm_other.name[1] = 'BRAIN_STRUCTURE_OTHER' @@ -278,11 +278,11 @@ def test_brain_models(): bm_other.volume_shape = (10, 3, 4) assert bm_vox != bm_other - bm_vertex = axes.BrainModel('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) + bm_vertex = axes.BrainModelAxis('cortex_left', vertex=np.ones(5, dtype=int), nvertices={'cortex_left': 20}) bm_other = deepcopy(bm_vertex) assert bm_vertex == bm_other bm_other.voxel[1, 0] = 0 - assert bm_vertex == bm_other, 'voxels are ignored in surface BrainModel' + assert bm_vertex == bm_other, 'voxels are ignored in surface BrainModelAxis' bm_other = deepcopy(bm_vertex) bm_other.vertex[1] = 10 @@ -309,7 +309,7 @@ def test_parcels(): Test the introspection and creation of CIFTI-2 Parcel axes """ prc = get_parcels() - assert isinstance(prc, axes.Parcels) + assert isinstance(prc, axes.ParcelsAxis) assert prc[0] == ('mixed', ) + prc['mixed'] assert prc['mixed'][0].shape == (3, 3) assert len(prc['mixed'][1]) == 1 @@ -426,7 +426,7 @@ def test_parcels(): assert prc != prc_other # test direct initialisation - axes.Parcels( + axes.ParcelsAxis( voxels=[np.ones((3, 2), dtype=int)], vertices=[{}], name=['single_voxel'], @@ -435,7 +435,7 @@ def test_parcels(): ) with assert_raises(ValueError): - axes.Parcels( + axes.ParcelsAxis( voxels=[np.ones((3, 2), dtype=int)], vertices=[{}], name=[['single_voxel']], # wrong shape name array @@ -446,11 +446,11 @@ def test_parcels(): def test_scalar(): """ - Test the introspection and creation of CIFTI-2 Scalar axes + Test the introspection and creation of CIFTI-2 ScalarAxis axes """ sc = get_scalar() assert len(sc) == 3 - assert isinstance(sc, axes.Scalar) + assert isinstance(sc, axes.ScalarAxis) assert (sc.name == ['one', 'two', 'three']).all() assert (sc.meta == [{}] * 3).all() assert sc[1] == ('two', {}) @@ -483,22 +483,22 @@ def test_scalar(): assert sc == sc_other # test constructor - assert axes.Scalar(['scalar_name'], [{}]) == axes.Scalar(['scalar_name']) + assert axes.ScalarAxis(['scalar_name'], [{}]) == axes.ScalarAxis(['scalar_name']) with assert_raises(ValueError): - axes.Scalar([['scalar_name']]) # wrong shape + axes.ScalarAxis([['scalar_name']]) # wrong shape with assert_raises(ValueError): - axes.Scalar(['scalar_name'], [{}, {}]) # wrong size + axes.ScalarAxis(['scalar_name'], [{}, {}]) # wrong size def test_label(): """ - Test the introspection and creation of CIFTI-2 Scalar axes + Test the introspection and creation of CIFTI-2 ScalarAxis axes """ lab = get_label() assert len(lab) == 3 - assert isinstance(lab, axes.Label) + assert isinstance(lab, axes.LabelAxis) assert (lab.name == ['one', 'two', 'three']).all() assert (lab.meta == [{}] * 3).all() assert (lab.label == [use_label] * 3).all() @@ -538,18 +538,18 @@ def test_label(): assert lab == other_lab # test constructor - assert axes.Label(['scalar_name'], [{}], [{}]) == axes.Label(['scalar_name'], [{}]) + assert axes.LabelAxis(['scalar_name'], [{}], [{}]) == axes.LabelAxis(['scalar_name'], [{}]) with assert_raises(ValueError): - axes.Label([['scalar_name']], [{}]) # wrong shape + axes.LabelAxis([['scalar_name']], [{}]) # wrong shape with assert_raises(ValueError): - axes.Label(['scalar_name'], [{}, {}]) # wrong size + axes.LabelAxis(['scalar_name'], [{}, {}]) # wrong size def test_series(): """ - Test the introspection and creation of CIFTI-2 Series axes + Test the introspection and creation of CIFTI-2 SeriesAxis axes """ sr = list(get_series()) assert sr[0].unit == 'SECOND' @@ -635,7 +635,7 @@ def test_common_interface(): concatenated = axis1 + axis2 assert axis1 != concatenated assert axis1 == concatenated[:axis1.size] - if isinstance(axis1, axes.Series): + if isinstance(axis1, axes.SeriesAxis): assert axis2 != concatenated[axis1.size:] else: assert axis2 == concatenated[axis1.size:] diff --git a/nibabel/cifti2/tests/test_cifti2io_axes.py b/nibabel/cifti2/tests/test_cifti2io_axes.py index e1025710ff..4089395b78 100644 --- a/nibabel/cifti2/tests/test_cifti2io_axes.py +++ b/nibabel/cifti2/tests/test_cifti2io_axes.py @@ -22,9 +22,9 @@ def check_hcp_grayordinates(brain_model): - """Checks that a BrainModel matches the expected 32k HCP grayordinates + """Checks that a BrainModelAxis matches the expected 32k HCP grayordinates """ - assert isinstance(brain_model, cifti2_axes.BrainModel) + assert isinstance(brain_model, cifti2_axes.BrainModelAxis) structures = list(brain_model.iter_structures()) assert len(structures) == len(hcp_labels) idx_start = 0 @@ -40,7 +40,7 @@ def check_hcp_grayordinates(brain_model): assert (bm.vertex == -1).all() assert (bm.affine == hcp_affine).all() assert bm.volume_shape == (91, 109, 91) - assert name == cifti2_axes.BrainModel.to_cifti_brain_structure_name(label) + assert name == cifti2_axes.BrainModelAxis.to_cifti_brain_structure_name(label) assert len(bm) == nel assert (bm.name == brain_model.name[idx_start:idx_start + nel]).all() assert (bm.voxel == brain_model.voxel[idx_start:idx_start + nel]).all() @@ -60,9 +60,9 @@ def check_hcp_grayordinates(brain_model): def check_Conte69(brain_model): - """Checks that the BrainModel matches the expected Conte69 surface coordinates + """Checks that the BrainModelAxis matches the expected Conte69 surface coordinates """ - assert isinstance(brain_model, cifti2_axes.BrainModel) + assert isinstance(brain_model, cifti2_axes.BrainModelAxis) structures = list(brain_model.iter_structures()) assert len(structures) == 2 assert structures[0][0] == 'CIFTI_STRUCTURE_CORTEX_LEFT' @@ -106,7 +106,7 @@ def test_read_ones(): arr = img.get_data() axes = [img.header.get_axis(dim) for dim in range(2)] assert (arr == 1).all() - assert isinstance(axes[0], cifti2_axes.Scalar) + assert isinstance(axes[0], cifti2_axes.ScalarAxis) assert len(axes[0]) == 1 assert axes[0].name[0] == 'ones' assert axes[0].meta[0] == {} @@ -120,7 +120,7 @@ def test_read_conte69_dscalar(): img = nib.load(os.path.join(test_directory, 'Conte69.MyelinAndCorrThickness.32k_fs_LR.dscalar.nii')) arr = img.get_data() axes = [img.header.get_axis(dim) for dim in range(2)] - assert isinstance(axes[0], cifti2_axes.Scalar) + assert isinstance(axes[0], cifti2_axes.ScalarAxis) assert len(axes[0]) == 2 assert axes[0].name[0] == 'MyelinMap_BC_decurv' assert axes[0].name[1] == 'corrThickness' @@ -134,7 +134,7 @@ def test_read_conte69_dtseries(): img = nib.load(os.path.join(test_directory, 'Conte69.MyelinAndCorrThickness.32k_fs_LR.dtseries.nii')) arr = img.get_data() axes = [img.header.get_axis(dim) for dim in range(2)] - assert isinstance(axes[0], cifti2_axes.Series) + assert isinstance(axes[0], cifti2_axes.SeriesAxis) assert len(axes[0]) == 2 assert axes[0].start == 0 assert axes[0].step == 1 @@ -149,7 +149,7 @@ def test_read_conte69_dlabel(): img = nib.load(os.path.join(test_directory, 'Conte69.parcellations_VGD11b.32k_fs_LR.dlabel.nii')) arr = img.get_data() axes = [img.header.get_axis(dim) for dim in range(2)] - assert isinstance(axes[0], cifti2_axes.Label) + assert isinstance(axes[0], cifti2_axes.LabelAxis) assert len(axes[0]) == 3 assert (axes[0].name == ['Composite Parcellation-lh (FRB08_OFP03_retinotopic)', 'Brodmann lh (from colin.R via pals_R-to-fs_LR)', 'MEDIAL WALL lh (fs_LR)']).all() @@ -164,7 +164,7 @@ def test_read_conte69_ptseries(): img = nib.load(os.path.join(test_directory, 'Conte69.MyelinAndCorrThickness.32k_fs_LR.ptseries.nii')) arr = img.get_data() axes = [img.header.get_axis(dim) for dim in range(2)] - assert isinstance(axes[0], cifti2_axes.Series) + assert isinstance(axes[0], cifti2_axes.SeriesAxis) assert len(axes[0]) == 2 assert axes[0].start == 0 assert axes[0].step == 1 diff --git a/nibabel/cifti2/tests/test_name.py b/nibabel/cifti2/tests/test_name.py index b656f88875..6b53d46523 100644 --- a/nibabel/cifti2/tests/test_name.py +++ b/nibabel/cifti2/tests/test_name.py @@ -12,7 +12,7 @@ def test_name_conversion(): """ Tests the automatic name conversion to a format recognized by CIFTI-2 """ - func = cifti2_axes.BrainModel.to_cifti_brain_structure_name + func = cifti2_axes.BrainModelAxis.to_cifti_brain_structure_name for base_name, input_names in equivalents: assert base_name == func(base_name) for name in input_names: From cffb8c0fb764ac6b94d6582c5e87bf295c65b296 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Wed, 27 Mar 2019 11:02:18 +0000 Subject: [PATCH 44/57] DOC: test creation of `bm_thal` and `bm_cortex` --- nibabel/cifti2/cifti2_axes.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 90d3c6f4c4..eb8a628658 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -50,10 +50,14 @@ We can create brain models covering the left cortex and left thalamus using: >>> from nibabel import cifti2 ->>> bm_cortex = cifti2.BrainModelAxis.from_mask(cortex_mask, -... brain_structure='cortex_left') # doctest: +SKIP ->>> bm_thal = cifti2.BrainModelAxis.from_mask(thalamus_mask, affine=affine, -... brain_structure='thalamus_left') # doctest: +SKIP +>>> import numpy as np +>>> bm_cortex = cifti2.BrainModelAxis.from_mask([True, False, True, True], +... name='cortex_left') +>>> bm_thal = cifti2.BrainModelAxis.from_mask(np.ones((2, 2, 2)), affine=np.eye(4), +... name='thalamus_left') + +In this very simple case ``bm_cortex`` describes a left cortical surface skipping the second +out of four vertices. ``bm_thal`` contains all voxels in a 2x2x2 volume. Brain structure names automatically get converted to valid CIFTI-2 indentifiers using :meth:`BrainModelAxis.to_cifti_brain_structure_name`. @@ -332,6 +336,8 @@ def from_mask(cls, mask, name='other', affine=None): affine = np.asanyarray(affine) if affine.shape != (4, 4): raise ValueError("Affine transformation should be a 4x4 array or None, not %r" % affine) + + mask = np.asanyarray(mask) if mask.ndim == 1: return cls.from_surface(np.where(mask != 0)[0], mask.size, name=name) elif mask.ndim == 3: From 9b2276db70d5550324836e68550c527c2da31bc6 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Wed, 27 Mar 2019 11:03:46 +0000 Subject: [PATCH 45/57] DOC: got rid of most of the :class:`Axis` in tutorial --- nibabel/cifti2/cifti2_axes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index eb8a628658..aa9acb48a9 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -10,22 +10,22 @@ * :class:`LabelAxis`: each row/column has a unique name and label table (with optional meta-data) * :class:`SeriesAxis`: each row/column is a timepoint, which increases monotonically -All of these classes are derived from the :class:`Axis` class. +All of these classes are derived from the Axis class. After loading a CIFTI-2 file a tuple of axes describing the rows and columns can be obtained from the :meth:`.cifti2.Cifti2Header.get_axis` method on the header object (e.g. ``nibabel.load().header.get_axis()``). Inversely, a new -:class:`.cifti2.Cifti2Header` object can be created from existing :class:`Axis` objects +:class:`.cifti2.Cifti2Header` object can be created from existing Axis objects using the :meth:`.cifti2.Cifti2Header.from_axes` factory method. -CIFTI-2 :class:`Axis` objects of the same type can be concatenated using the '+'-operator. +CIFTI-2 Axis objects of the same type can be concatenated using the '+'-operator. Numpy indexing also works on axes (except for SeriesAxis objects, which have to remain monotonically increasing or decreasing). Creating new CIFTI-2 axes ----------------------- -New :class:`Axis` objects can be constructed by providing a description for what is contained -in each row/column of the described tensor. For each :class:`Axis` sub-class this descriptor is: +New Axis objects can be constructed by providing a description for what is contained +in each row/column of the described tensor. For each Axis sub-class this descriptor is: * :class:`BrainModelAxis`: a CIFTI-2 structure name and a voxel or vertex index * :class:`ParcelsAxis`: a name and a sequence of voxel and vertex indices From 110334bdaa8e4211e46c4819a8b48e722f85e1c5 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Wed, 27 Mar 2019 11:05:06 +0000 Subject: [PATCH 46/57] Update nibabel/cifti2/cifti2.py Co-Authored-By: MichielCottaar --- nibabel/cifti2/cifti2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 7ca5584bb1..66ac855cf0 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1276,7 +1276,7 @@ def get_axis(self, index): Returns ------- - axis : cifti2_axes.Axis + axis : :class:`.cifti2_axes.Axis` ''' from . import cifti2_axes return cifti2_axes.from_index_mapping(self.matrix.get_index_map(index)) From 063047fa9806a76fbdb1ec8a7347d4e849ef39c6 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Wed, 27 Mar 2019 11:08:20 +0000 Subject: [PATCH 47/57] Apply suggestions from code review Co-Authored-By: MichielCottaar --- nibabel/cifti2/cifti2.py | 4 ++-- nibabel/cifti2/cifti2_axes.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nibabel/cifti2/cifti2.py b/nibabel/cifti2/cifti2.py index 66ac855cf0..8a4f12e767 100644 --- a/nibabel/cifti2/cifti2.py +++ b/nibabel/cifti2/cifti2.py @@ -1288,7 +1288,7 @@ def from_axes(cls, axes): Parameters ---------- - axes : Tuple[cifti2_axes.Axis] + axes : tuple of :class`.cifti2_axes.Axis` sequence of Cifti2 axes describing each row/column of the matrix to be stored Returns @@ -1328,7 +1328,7 @@ def __init__(self, Object containing image data. It should be some object that returns an array from ``np.asanyarray``. It should have a ``shape`` attribute or property. - header : Cifti2Header instance or Sequence[cifti2_axes.Axis] + header : Cifti2Header instance or sequence of :class:`cifti2_axes.Axis` Header with data for / from XML part of CIFTI-2 format. Alternatively a sequence of cifti2_axes.Axis objects can be provided describing each dimension of the array. diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index aa9acb48a9..e9f56ae38f 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -124,11 +124,11 @@ def from_index_mapping(mim): Parameters ---------- - mim : cifti2.Cifti2MatrixIndicesMap + mim : :class:`.cifti2.Cifti2MatrixIndicesMap` Returns ------- - subtype of Axis + subclass of :class:`Axis` """ return_type = {'CIFTI_INDEX_TYPE_SCALARS': ScalarAxis, 'CIFTI_INDEX_TYPE_LABELS': LabelAxis, @@ -149,7 +149,7 @@ def to_header(axes): Returns ------- - cifti2.Cifti2Header + :class:`.cifti2.Cifti2Header` """ axes = tuple(axes) mims_all = [] @@ -777,7 +777,7 @@ def from_brain_models(cls, named_brain_models): for idx_parcel, (parcel_name, bm) in enumerate(named_brain_models): all_names.append(parcel_name) - voxels = bm.voxel[~bm.surface_mask] + voxels = bm.voxel[bm.volume_mask] if voxels.shape[0] != 0: if affine is None: affine = bm.affine From cefb8c6cf7c65c1a8fa077fec4821df6698070d8 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Wed, 27 Mar 2019 11:10:57 +0000 Subject: [PATCH 48/57] RF: replace ~surface_mask with volume_mask --- nibabel/cifti2/cifti2_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index e9f56ae38f..df7269ce95 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -604,7 +604,7 @@ def __eq__(self, other): self.volume_shape == other.volume_shape) and self.nvertices == other.nvertices and np.array_equal(self.name, other.name) and - np.array_equal(self.voxel[~self.surface_mask], other.voxel[~other.surface_mask]) and + np.array_equal(self.voxel[self.volume_mask], other.voxel[self.volume_mask]) and np.array_equal(self.vertex[self.surface_mask], other.vertex[other.surface_mask]) ) From 0f0e1f746d9c5e2caaebd3e9c00c464e9a0b31af Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Wed, 27 Mar 2019 11:17:22 +0000 Subject: [PATCH 49/57] DOC: added list of concrete classes to Axis object --- nibabel/cifti2/cifti2_axes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index df7269ce95..00999dfe66 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -172,6 +172,14 @@ class Axis(object): Abstract class for any object describing the rows or columns of a CIFTI-2 vector/matrix Mainly used for type checking. + + Base class for the following concrete CIFTI-2 axes: + + * :class:`BrainModelAxis`: each row/column is a voxel or vertex + * :class:`ParcelsAxis`: each row/column is a group of voxels and/or vertices + * :class:`ScalarAxis`: each row/column has a unique name (with optional meta-data) + * :class:`LabelAxis`: each row/column has a unique name and label table (with optional meta-data) + * :class:`SeriesAxis`: each row/column is a timepoint, which increases monotonically """ @property From 0270ad9f8e2bd0e469f5fea6b914fcdca80e5eaa Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Wed, 27 Mar 2019 11:31:17 +0000 Subject: [PATCH 50/57] BF: add name to return, so that link works in html --- nibabel/cifti2/cifti2_axes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 00999dfe66..77cf01427f 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -128,7 +128,7 @@ def from_index_mapping(mim): Returns ------- - subclass of :class:`Axis` + axis : subclass of :class:`Axis` """ return_type = {'CIFTI_INDEX_TYPE_SCALARS': ScalarAxis, 'CIFTI_INDEX_TYPE_LABELS': LabelAxis, @@ -149,7 +149,7 @@ def to_header(axes): Returns ------- - :class:`.cifti2.Cifti2Header` + header : :class:`.cifti2.Cifti2Header` """ axes = tuple(axes) mims_all = [] From ed764182d78c94824849b2b38bd874f1c83624f0 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Wed, 27 Mar 2019 11:32:17 +0000 Subject: [PATCH 51/57] RF: fix line length --- nibabel/cifti2/cifti2_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 77cf01427f..ddcc59ad4f 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -178,7 +178,7 @@ class Axis(object): * :class:`BrainModelAxis`: each row/column is a voxel or vertex * :class:`ParcelsAxis`: each row/column is a group of voxels and/or vertices * :class:`ScalarAxis`: each row/column has a unique name (with optional meta-data) - * :class:`LabelAxis`: each row/column has a unique name and label table (with optional meta-data) + * :class:`LabelAxis`: each row/column has a unique name and label table with optional meta-data * :class:`SeriesAxis`: each row/column is a timepoint, which increases monotonically """ From d825a3c30cac875617c4e9cfa1f779b8ed7ca934 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Wed, 27 Mar 2019 11:33:00 +0000 Subject: [PATCH 52/57] DOC: make format in list of axes more consistent --- nibabel/cifti2/cifti2_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index ddcc59ad4f..e42aa42a1b 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -177,7 +177,7 @@ class Axis(object): * :class:`BrainModelAxis`: each row/column is a voxel or vertex * :class:`ParcelsAxis`: each row/column is a group of voxels and/or vertices - * :class:`ScalarAxis`: each row/column has a unique name (with optional meta-data) + * :class:`ScalarAxis`: each row/column has a unique name with optional meta-data * :class:`LabelAxis`: each row/column has a unique name and label table with optional meta-data * :class:`SeriesAxis`: each row/column is a timepoint, which increases monotonically """ From 1bc459e2c42405088e07e7514e47bcd629599517 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 27 Mar 2019 13:02:00 -0400 Subject: [PATCH 53/57] DOCTEST: Drop doctest SKIP directives --- nibabel/cifti2/cifti2_axes.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index e42aa42a1b..2cd92ba8a0 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -67,7 +67,7 @@ These can be concatenated in a single brain model covering the left cortex and thalamus by simply adding them together ->>> bm_full = bm_cortex + bm_thal # doctest: +SKIP +>>> bm_full = bm_cortex + bm_thal Brain models covering the full HCP grayordinate space can be constructed by adding all the volumetric and surface brain models together like this (or by reading one from an already @@ -75,12 +75,12 @@ Getting a specific brain region from the full brain model is as simple as: ->>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_CORTEX_LEFT'] == bm_cortex # doctest: +SKIP ->>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_THALAMUS_LEFT'] == bm_thal # doctest: +SKIP +>>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_CORTEX_LEFT'] == bm_cortex +>>> assert bm_full[bm_full.name == 'CIFTI_STRUCTURE_THALAMUS_LEFT'] == bm_thal You can also iterate over all brain structures in a brain model: ->>> for name, slc, bm in bm_full.iter_structures(): ... # doctest: +SKIP +>>> for name, slc, bm in bm_full.iter_structures(): ... In this case there will be two iterations, namely: ('CIFTI_STRUCTURE_CORTEX_LEFT', slice(0, ), bm_cortex) @@ -93,23 +93,23 @@ ... ('surface_parcel', bm_cortex[:100]), # contains first 100 cortical vertices ... ('volume_parcel', bm_thal), # contains thalamus ... ('combined_parcel', bm_full[[1, 8, 10, 120, 127]) # contains selected voxels/vertices -... ]) # doctest: +SKIP +... ]) Time series are represented by their starting time (typically 0), step size (i.e. sampling time or TR), and number of elements: ->>> series = cifti2.SeriesAxis(start=0, step=100, size=5000) # doctest: +SKIP +>>> series = cifti2.SeriesAxis(start=0, step=100, size=5000) So a header for fMRI data with a TR of 100 ms covering the left cortex and thalamus with 5000 timepoints could be created with ->>> cifti2.Cifti2Header.from_axes((series, bm_cortex + bm_thal)) # doctest: +SKIP +>>> cifti2.Cifti2Header.from_axes((series, bm_cortex + bm_thal)) Similarly the curvature and cortical thickness on the left cortex could be stored using a header like: >>> cifti2.Cifti2Header.from_axes((cifti.ScalarAxis(['curvature', 'thickness'], -... bm_cortex)) # doctest: +SKIP +... bm_cortex)) """ import numpy as np from . import cifti2 From 04a4b45623caabf9617f4de2743d4ee67d249b85 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 27 Mar 2019 13:18:20 -0400 Subject: [PATCH 54/57] FIX: Use other.volume_mask to index other.voxel --- nibabel/cifti2/cifti2_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 2cd92ba8a0..fe27c16035 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -612,7 +612,7 @@ def __eq__(self, other): self.volume_shape == other.volume_shape) and self.nvertices == other.nvertices and np.array_equal(self.name, other.name) and - np.array_equal(self.voxel[self.volume_mask], other.voxel[self.volume_mask]) and + np.array_equal(self.voxel[self.volume_mask], other.voxel[other.volume_mask]) and np.array_equal(self.vertex[self.surface_mask], other.vertex[other.surface_mask]) ) From 4aa360996a36a2bcba312be86ccb034a18b44400 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 27 Mar 2019 13:31:29 -0400 Subject: [PATCH 55/57] DOC: Update docstrings with a few more links and array_like --- nibabel/cifti2/cifti2_axes.py | 69 ++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index fe27c16035..6b4757347e 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -252,20 +252,20 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, Parameters ---------- - name : str or np.ndarray + name : array_like brain structure name or (N, ) string array with the brain structure names - voxel : np.ndarray + voxel : array_like, optional (N, 3) array with the voxel indices (can be omitted for CIFTI-2 files only covering the surface) - vertex : np.ndarray + vertex : array_like, optional (N, ) array with the vertex indices (can be omitted for volumetric CIFTI-2 files) - affine : np.ndarray + affine : array_like, optional (4, 4) array mapping voxel indices to mm space (not needed for CIFTI-2 files only covering the surface) - volume_shape : tuple of three integers + volume_shape : tuple of three integers, optional shape of the volume in which the voxels were defined (not needed for CIFTI-2 files only covering the surface) - nvertices : dict from string to integer + nvertices : dict from string to integer, optional maps names of surface elements to integers (not needed for volumetric CIFTI-2 files) """ if voxel is None: @@ -304,7 +304,7 @@ def __init__(self, name, voxel=None, vertex=None, affine=None, if affine is None or volume_shape is None: raise ValueError("Affine and volume shape should be defined " "for BrainModelAxis containing voxels") - self.affine = affine + self.affine = np.asanyarray(affine) self.volume_shape = volume_shape if np.any(self.vertex[surface_mask] < 0): @@ -325,12 +325,12 @@ def from_mask(cls, mask, name='other', affine=None): Parameters ---------- - mask : np.ndarray + mask : array_like all non-zero voxels will be included in the BrainModelAxis axis should be (Nx, Ny, Nz) array for volume mask or (Nvertex, ) array for surface mask - name : str + name : str, optional Name of the brain structure (e.g. 'CortexRight', 'thalamus_left' or 'brain_stem') - affine : np.ndarray + affine : array_like, optional (4, 4) array with the voxel to mm transformation (defaults to identity matrix) Argument will be ignored for surface masks @@ -362,7 +362,7 @@ def from_surface(cls, vertices, nvertex, name='Other'): Parameters ---------- - vertices : np.ndarray + vertices : array_like indices of the vertices on the surface nvertex : int total number of vertices on the surface @@ -384,7 +384,7 @@ def from_index_mapping(cls, mim): Parameters ---------- - mim : cifti2.Cifti2MatrixIndicesMap + mim : :class:`.cifti2.Cifti2MatrixIndicesMap` Returns ------- @@ -422,7 +422,7 @@ def to_mapping(self, dim): Returns ------- - cifti2.Cifti2MatrixIndicesMap + :class:`.cifti2.Cifti2MatrixIndicesMap` """ mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_BRAIN_MODELS') for name, to_slice, bm in self.iter_structures(): @@ -720,22 +720,22 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert Parameters ---------- - name : np.ndarray + name : array_like (N, ) string array with the parcel names - voxels : np.ndarray + voxels : array_like (N, ) object array each containing a sequence of voxels. For each parcel the voxels are represented by a (M, 3) index array - vertices : np.ndarray + vertices : array_like (N, ) object array each containing a sequence of vertices. For each parcel the vertices are represented by a mapping from brain structure name to (M, ) index array - affine : np.ndarray + affine : array_like, optional (4, 4) array mapping voxel indices to mm space (not needed for CIFTI-2 files only covering the surface) - volume_shape : tuple of three integers + volume_shape : tuple of three integers, optional shape of the volume in which the voxels were defined (not needed for CIFTI-2 files only covering the surface) - nvertices : dict[String -> int] + nvertices : dict from string to integer, optional maps names of surface elements to integers (not needed for volumetric CIFTI-2 files) """ self.name = np.asanyarray(name, dtype='U') @@ -748,7 +748,7 @@ def __init__(self, name, voxels, vertices, affine=None, volume_shape=None, nvert voxels[idx] = as_array[idx] self.voxels = np.asanyarray(voxels, dtype='object') self.vertices = np.asanyarray(vertices, dtype='object') - self.affine = affine + self.affine = np.asanyarray(affine) if affine is not None else None self.volume_shape = volume_shape if nvertices is None: self.nvertices = {} @@ -813,7 +813,7 @@ def from_index_mapping(cls, mim): Parameters ---------- - mim : cifti2.Cifti2MatrixIndicesMap + mim : :class:`cifti2.Cifti2MatrixIndicesMap` Returns ------- @@ -859,7 +859,7 @@ def to_mapping(self, dim): Returns ------- - cifti2.Cifti2MatrixIndicesMap + :class:`cifti2.Cifti2MatrixIndicesMap` """ mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_PARCELS') if self.affine is not None: @@ -1008,7 +1008,8 @@ def get_element(self, index): tuple with 3 elements - unicode name of the parcel - (M, 3) int array with voxel indices - - Dict[String -> (K, ) int array] with vertex indices for a specific surface brain structure + - dict from string to (K, ) int array with vertex indices + for a specific surface brain structure """ return self.name[index], self.voxels[index], self.vertices[index] @@ -1023,9 +1024,9 @@ def __init__(self, name, meta=None): """ Parameters ---------- - name : np.ndarray of string + name : array_like (N, ) string array with the parcel names - meta : np.ndarray of dict + meta : array_like (N, ) object array with a dictionary of metadata for each row/column. Defaults to empty dictionary """ @@ -1046,7 +1047,7 @@ def from_index_mapping(cls, mim): Parameters ---------- - mim : cifti2.Cifti2MatrixIndicesMap + mim : :class:`.cifti2.Cifti2MatrixIndicesMap` Returns ------- @@ -1067,7 +1068,7 @@ def to_mapping(self, dim): Returns ------- - cifti2.Cifti2MatrixIndicesMap + :class:`.cifti2.Cifti2MatrixIndicesMap` """ mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_SCALARS') for name, meta in zip(self.name, self.meta): @@ -1151,13 +1152,13 @@ def __init__(self, name, label, meta=None): """ Parameters ---------- - name : np.ndarray + name : array_like (N, ) string array with the parcel names - label : np.ndarray + label : array_like single dictionary or (N, ) object array with dictionaries mapping from integers to (name, (R, G, B, A)), where name is a string and R, G, B, and A are floats between 0 and 1 giving the colour and alpha (i.e., transparency) - meta : np.ndarray + meta : array_like, optional (N, ) object array with a dictionary of metadata for each row/column """ self.name = np.asanyarray(name, dtype='U') @@ -1180,7 +1181,7 @@ def from_index_mapping(cls, mim): Parameters ---------- - mim : cifti2.Cifti2MatrixIndicesMap + mim : :class:`.cifti2.Cifti2MatrixIndicesMap` Returns ------- @@ -1202,7 +1203,7 @@ def to_mapping(self, dim): Returns ------- - cifti2.Cifti2MatrixIndicesMap + :class:`.cifti2.Cifti2MatrixIndicesMap` """ mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_LABELS') for name, label, meta in zip(self.name, self.label, self.meta): @@ -1324,7 +1325,7 @@ def from_index_mapping(cls, mim): Parameters ---------- - mim : cifti2.Cifti2MatrixIndicesMap + mim : :class:`.cifti2.Cifti2MatrixIndicesMap` Returns ------- @@ -1345,7 +1346,7 @@ def to_mapping(self, dim): Returns ------- - cifti2.Cifti2MatrixIndicesMap + :class:`cifti2.Cifti2MatrixIndicesMap` """ mim = cifti2.Cifti2MatrixIndicesMap([dim], 'CIFTI_INDEX_TYPE_SERIES') mim.series_exponent = 0 From 36c162d1f02043de251044d01007ea8780c39f02 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Thu, 28 Mar 2019 11:48:36 +0000 Subject: [PATCH 56/57] BF: doctest fixes for tutorial --- nibabel/cifti2/cifti2_axes.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 6b4757347e..3b0e9519f4 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -80,7 +80,12 @@ You can also iterate over all brain structures in a brain model: ->>> for name, slc, bm in bm_full.iter_structures(): ... +>>> for idx, (name, slc, bm) in enumerate(bm_full.iter_structures()): +... print(name, slc) +... assert bm == bm_full[slc] +... assert bm == bm_cortex if idx == 0 else bm_thal +CIFTI_STRUCTURE_CORTEX_LEFT slice(0, 3, None) +CIFTI_STRUCTURE_THALAMUS_LEFT slice(3, None, None) In this case there will be two iterations, namely: ('CIFTI_STRUCTURE_CORTEX_LEFT', slice(0, ), bm_cortex) @@ -90,9 +95,9 @@ ParcelsAxis can be constructed from selections of these brain models: >>> parcel = cifti2.ParcelsAxis.from_brain_models([ -... ('surface_parcel', bm_cortex[:100]), # contains first 100 cortical vertices +... ('surface_parcel', bm_cortex[:2]), # contains first 2 cortical vertices ... ('volume_parcel', bm_thal), # contains thalamus -... ('combined_parcel', bm_full[[1, 8, 10, 120, 127]) # contains selected voxels/vertices +... ('combined_parcel', bm_full[[1, 8, 10]]), # contains selected voxels/vertices ... ]) Time series are represented by their starting time (typically 0), step size @@ -103,13 +108,15 @@ So a header for fMRI data with a TR of 100 ms covering the left cortex and thalamus with 5000 timepoints could be created with ->>> cifti2.Cifti2Header.from_axes((series, bm_cortex + bm_thal)) +>>> type(cifti2.Cifti2Header.from_axes((series, bm_cortex + bm_thal))) + Similarly the curvature and cortical thickness on the left cortex could be stored using a header like: ->>> cifti2.Cifti2Header.from_axes((cifti.ScalarAxis(['curvature', 'thickness'], -... bm_cortex)) +>>> type(cifti2.Cifti2Header.from_axes((cifti2.ScalarAxis(['curvature', 'thickness']), +... bm_cortex))) + """ import numpy as np from . import cifti2 From 0927424e6582750c5fb1556cdd87ebff4884b556 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar Date: Fri, 29 Mar 2019 10:19:08 +0000 Subject: [PATCH 57/57] BF: fixed doctest for python 2.7 --- nibabel/cifti2/cifti2_axes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nibabel/cifti2/cifti2_axes.py b/nibabel/cifti2/cifti2_axes.py index 3b0e9519f4..30decec3d1 100644 --- a/nibabel/cifti2/cifti2_axes.py +++ b/nibabel/cifti2/cifti2_axes.py @@ -81,11 +81,11 @@ You can also iterate over all brain structures in a brain model: >>> for idx, (name, slc, bm) in enumerate(bm_full.iter_structures()): -... print(name, slc) +... print((str(name), slc)) ... assert bm == bm_full[slc] ... assert bm == bm_cortex if idx == 0 else bm_thal -CIFTI_STRUCTURE_CORTEX_LEFT slice(0, 3, None) -CIFTI_STRUCTURE_THALAMUS_LEFT slice(3, None, None) +('CIFTI_STRUCTURE_CORTEX_LEFT', slice(0, 3, None)) +('CIFTI_STRUCTURE_THALAMUS_LEFT', slice(3, None, None)) In this case there will be two iterations, namely: ('CIFTI_STRUCTURE_CORTEX_LEFT', slice(0, ), bm_cortex)