diff --git a/COPYING b/COPYING index 5827950a17..6f03ba5ccd 100644 --- a/COPYING +++ b/COPYING @@ -121,36 +121,40 @@ Sphinx 0.6 doesn't work properly. OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -Ordereddict +OrderedSet ----------- -In ``nibabel/externals/ordereddict.py`` +In ``nibabel/externals/oset.py`` -Copied from: https://pypi.python.org/packages/source/o/ordereddict/ordereddict-1.1.tar.gz#md5=a0ed854ee442051b249bfad0f638bbec +Copied from: https://files.pythonhosted.org/packages/d6/b1/a49498c699a3fda5d635cc1fa222ffc686ea3b5d04b84a3166c4cab0c57b/oset-0.1.3.tar.gz :: - Copyright (c) 2009 Raymond Hettinger - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation files - (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, - publish, distribute, sublicense, and/or sell copies of the Software, - and to permit persons to whom the Software is furnished to do so, - subject to the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. + Copyright (c) 2009, Raymond Hettinger, and others All rights reserved. + + Package structured based on the one developed to odict Copyright (c) 2010, BlueDynamics Alliance, Austria + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + - Neither the name of the BlueDynamics Alliance nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY BlueDynamics Alliance AS IS AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + SHALL BlueDynamics Alliance BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + OF SUCH DAMAGE. mni_icbm152_t1_tal_nlin_asym_09a -------------------------------- diff --git a/nibabel/externals/oset.py b/nibabel/externals/oset.py new file mode 100644 index 0000000000..6bc6ed67a3 --- /dev/null +++ b/nibabel/externals/oset.py @@ -0,0 +1,85 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +"""OrderedSet implementation + +Borrowed from https://pypi.org/project/oset/ +Copyright (c) 2009, Raymond Hettinger, and others All rights reserved. +License: BSD-3 +""" + +from __future__ import absolute_import + +from collections import MutableSet + +KEY, PREV, NEXT = range(3) + + +class OrderedSet(MutableSet): + + def __init__(self, iterable=None): + self.end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.map = {} # key --> [key, prev, next] + if iterable is not None: + self |= iterable + + def __len__(self): + return len(self.map) + + def __contains__(self, key): + return key in self.map + + def __getitem__(self, key): + return list(self)[key] + + def add(self, key): + if key not in self.map: + end = self.end + curr = end[PREV] + curr[NEXT] = end[PREV] = self.map[key] = [key, curr, end] + + def discard(self, key): + if key in self.map: + key, prev, next = self.map.pop(key) + prev[NEXT] = next + next[PREV] = prev + + def __iter__(self): + end = self.end + curr = end[NEXT] + while curr is not end: + yield curr[KEY] + curr = curr[NEXT] + + def __reversed__(self): + end = self.end + curr = end[PREV] + while curr is not end: + yield curr[KEY] + curr = curr[PREV] + + def pop(self, last=True): + if not self: + raise KeyError('set is empty') + key = next(reversed(self)) if last else next(iter(self)) + self.discard(key) + return key + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self)) + + def __eq__(self, other): + if isinstance(other, OrderedSet): + return len(self) == len(other) and list(self) == list(other) + return set(self) == set(other) + + def __del__(self): + self.clear() # remove circular references \ No newline at end of file diff --git a/nibabel/nifti1.py b/nibabel/nifti1.py index 24c1808df5..056d0dbee9 100644 --- a/nibabel/nifti1.py +++ b/nibabel/nifti1.py @@ -1573,14 +1573,23 @@ def set_slice_times(self, slice_times): so_recoder = self._field_recoders['slice_code'] labels = so_recoder.value_set('label') labels.remove('unknown') + + matching_labels = [] for label in labels: if np.all(st_order == self._slice_time_order( label, n_timed)): - break - else: + matching_labels.append(label) + + if not matching_labels: raise HeaderDataError('slice ordering of %s fits ' 'with no known scheme' % st_order) + if len(matching_labels) > 1: + warnings.warn( + 'Multiple slice orders satisfy: %s. Choosing the first one' + % ', '.join(matching_labels) + ) + label = matching_labels[0] # Set values into header hdr['slice_start'] = slice_start hdr['slice_end'] = slice_end diff --git a/nibabel/tests/test_nifti1.py b/nibabel/tests/test_nifti1.py index 1c6fb989b5..78f876ec7d 100644 --- a/nibabel/tests/test_nifti1.py +++ b/nibabel/tests/test_nifti1.py @@ -38,7 +38,12 @@ from nose.tools import (assert_true, assert_false, assert_equal, assert_raises) -from ..testing import data_path, suppress_warnings, runif_extra_has +from ..testing import ( + clear_and_catch_warnings, + data_path, + runif_extra_has, + suppress_warnings, +) from . import test_analyze as tana from . import test_spm99analyze as tspm @@ -558,6 +563,22 @@ def test_slice_times(self): assert_equal(hdr['slice_end'], 5) assert_array_almost_equal(hdr['slice_duration'], 0.1) + # Ambiguous case + hdr2 = self.header_class() + hdr2.set_dim_info(slice=2) + hdr2.set_slice_duration(0.1) + hdr2.set_data_shape((1, 1, 2)) + with clear_and_catch_warnings() as w: + warnings.simplefilter("always") + hdr2.set_slice_times([0.1, 0]) + assert len(w) == 1 + # but always must be choosing sequential one first + assert_equal(hdr2.get_value_label('slice_code'), 'sequential decreasing') + # and the other direction + hdr2.set_slice_times([0, 0.1]) + assert_equal(hdr2.get_value_label('slice_code'), 'sequential increasing') + + def test_intents(self): ehdr = self.header_class() ehdr.set_intent('t test', (10,), name='some score') diff --git a/nibabel/volumeutils.py b/nibabel/volumeutils.py index e442b508d8..b7a510e337 100644 --- a/nibabel/volumeutils.py +++ b/nibabel/volumeutils.py @@ -13,6 +13,7 @@ import warnings import gzip import bz2 +from collections import OrderedDict from os.path import exists, splitext from operator import mul from functools import reduce @@ -22,6 +23,7 @@ from .casting import (shared_range, type_info, OK_FLOATS) from .openers import Opener from .deprecated import deprecate_with_version +from .externals.oset import OrderedSet sys_is_le = sys.byteorder == 'little' native_code = sys_is_le and '<' or '>' @@ -78,7 +80,7 @@ class Recoder(object): 2 ''' - def __init__(self, codes, fields=('code',), map_maker=dict): + def __init__(self, codes, fields=('code',), map_maker=OrderedDict): ''' Create recoder object ``codes`` give a sequence of code, alias sequences @@ -97,7 +99,7 @@ def __init__(self, codes, fields=('code',), map_maker=dict): Parameters ---------- - codes : seqence of sequences + codes : sequence of sequences Each sequence defines values (codes) that are equivalent fields : {('code',) string sequence}, optional names by which elements in sequences can be accessed @@ -133,13 +135,15 @@ def add_codes(self, code_syn_seqs): Examples -------- - >>> code_syn_seqs = ((1, 'one'), (2, 'two')) + >>> code_syn_seqs = ((2, 'two'), (1, 'one')) >>> rc = Recoder(code_syn_seqs) >>> rc.value_set() == set((1,2)) True >>> rc.add_codes(((3, 'three'), (1, 'first'))) >>> rc.value_set() == set((1,2,3)) True + >>> print(rc.value_set()) # set is actually ordered + OrderedSet([2, 1, 3]) ''' for code_syns in code_syn_seqs: # Add all the aliases @@ -186,7 +190,7 @@ def keys(self): return self.field1.keys() def value_set(self, name=None): - ''' Return set of possible returned values for column + ''' Return OrderedSet of possible returned values for column By default, the column is the first column. @@ -212,7 +216,7 @@ def value_set(self, name=None): d = self.field1 else: d = self.__dict__[name] - return set(d.values()) + return OrderedSet(d.values()) # Endian code aliases