Skip to content

Commit bb8c6fa

Browse files
authored
Merge pull request #647 from yarikoptic/bf-sliceorder
BF: deterministic order of slice_time deduction, warning if multiple match
2 parents 2a127cc + d756751 commit bb8c6fa

File tree

5 files changed

+155
-32
lines changed

5 files changed

+155
-32
lines changed

COPYING

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -121,36 +121,40 @@ Sphinx 0.6 doesn't work properly.
121121
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
122122
DAMAGE.
123123

124-
Ordereddict
124+
OrderedSet
125125
-----------
126126

127-
In ``nibabel/externals/ordereddict.py``
127+
In ``nibabel/externals/oset.py``
128128

129-
Copied from: https://pypi.python.org/packages/source/o/ordereddict/ordereddict-1.1.tar.gz#md5=a0ed854ee442051b249bfad0f638bbec
129+
Copied from: https://files.pythonhosted.org/packages/d6/b1/a49498c699a3fda5d635cc1fa222ffc686ea3b5d04b84a3166c4cab0c57b/oset-0.1.3.tar.gz
130130

131131
::
132132

133-
Copyright (c) 2009 Raymond Hettinger
134-
135-
Permission is hereby granted, free of charge, to any person
136-
obtaining a copy of this software and associated documentation files
137-
(the "Software"), to deal in the Software without restriction,
138-
including without limitation the rights to use, copy, modify, merge,
139-
publish, distribute, sublicense, and/or sell copies of the Software,
140-
and to permit persons to whom the Software is furnished to do so,
141-
subject to the following conditions:
142-
143-
The above copyright notice and this permission notice shall be
144-
included in all copies or substantial portions of the Software.
145-
146-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
147-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
148-
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
149-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
150-
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
151-
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
152-
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
153-
OTHER DEALINGS IN THE SOFTWARE.
133+
Copyright (c) 2009, Raymond Hettinger, and others All rights reserved.
134+
135+
Package structured based on the one developed to odict Copyright (c) 2010, BlueDynamics Alliance, Austria
136+
137+
- Redistributions of source code must retain the above copyright notice, this
138+
list of conditions and the following disclaimer.
139+
140+
- Redistributions in binary form must reproduce the above copyright notice, this
141+
list of conditions and the following disclaimer in the documentation and/or
142+
other materials provided with the distribution.
143+
144+
- Neither the name of the BlueDynamics Alliance nor the names of its
145+
contributors may be used to endorse or promote products derived from this
146+
software without specific prior written permission.
147+
148+
THIS SOFTWARE IS PROVIDED BY BlueDynamics Alliance AS IS AND ANY EXPRESS OR
149+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
150+
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
151+
SHALL BlueDynamics Alliance BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
152+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
153+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
154+
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
155+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
156+
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
157+
OF SUCH DAMAGE.
154158

155159
mni_icbm152_t1_tal_nlin_asym_09a
156160
--------------------------------

nibabel/externals/oset.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
4+
#
5+
# See COPYING file distributed along with the NiBabel package for the
6+
# copyright and license terms.
7+
#
8+
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
9+
"""OrderedSet implementation
10+
11+
Borrowed from https://pypi.org/project/oset/
12+
Copyright (c) 2009, Raymond Hettinger, and others All rights reserved.
13+
License: BSD-3
14+
"""
15+
16+
from __future__ import absolute_import
17+
18+
from collections import MutableSet
19+
20+
KEY, PREV, NEXT = range(3)
21+
22+
23+
class OrderedSet(MutableSet):
24+
25+
def __init__(self, iterable=None):
26+
self.end = end = []
27+
end += [None, end, end] # sentinel node for doubly linked list
28+
self.map = {} # key --> [key, prev, next]
29+
if iterable is not None:
30+
self |= iterable
31+
32+
def __len__(self):
33+
return len(self.map)
34+
35+
def __contains__(self, key):
36+
return key in self.map
37+
38+
def __getitem__(self, key):
39+
return list(self)[key]
40+
41+
def add(self, key):
42+
if key not in self.map:
43+
end = self.end
44+
curr = end[PREV]
45+
curr[NEXT] = end[PREV] = self.map[key] = [key, curr, end]
46+
47+
def discard(self, key):
48+
if key in self.map:
49+
key, prev, next = self.map.pop(key)
50+
prev[NEXT] = next
51+
next[PREV] = prev
52+
53+
def __iter__(self):
54+
end = self.end
55+
curr = end[NEXT]
56+
while curr is not end:
57+
yield curr[KEY]
58+
curr = curr[NEXT]
59+
60+
def __reversed__(self):
61+
end = self.end
62+
curr = end[PREV]
63+
while curr is not end:
64+
yield curr[KEY]
65+
curr = curr[PREV]
66+
67+
def pop(self, last=True):
68+
if not self:
69+
raise KeyError('set is empty')
70+
key = next(reversed(self)) if last else next(iter(self))
71+
self.discard(key)
72+
return key
73+
74+
def __repr__(self):
75+
if not self:
76+
return '%s()' % (self.__class__.__name__,)
77+
return '%s(%r)' % (self.__class__.__name__, list(self))
78+
79+
def __eq__(self, other):
80+
if isinstance(other, OrderedSet):
81+
return len(self) == len(other) and list(self) == list(other)
82+
return set(self) == set(other)
83+
84+
def __del__(self):
85+
self.clear() # remove circular references

nibabel/nifti1.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,14 +1573,23 @@ def set_slice_times(self, slice_times):
15731573
so_recoder = self._field_recoders['slice_code']
15741574
labels = so_recoder.value_set('label')
15751575
labels.remove('unknown')
1576+
1577+
matching_labels = []
15761578
for label in labels:
15771579
if np.all(st_order == self._slice_time_order(
15781580
label,
15791581
n_timed)):
1580-
break
1581-
else:
1582+
matching_labels.append(label)
1583+
1584+
if not matching_labels:
15821585
raise HeaderDataError('slice ordering of %s fits '
15831586
'with no known scheme' % st_order)
1587+
if len(matching_labels) > 1:
1588+
warnings.warn(
1589+
'Multiple slice orders satisfy: %s. Choosing the first one'
1590+
% ', '.join(matching_labels)
1591+
)
1592+
label = matching_labels[0]
15841593
# Set values into header
15851594
hdr['slice_start'] = slice_start
15861595
hdr['slice_end'] = slice_end

nibabel/tests/test_nifti1.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@
3838
from nose.tools import (assert_true, assert_false, assert_equal,
3939
assert_raises)
4040

41-
from ..testing import data_path, suppress_warnings, runif_extra_has
41+
from ..testing import (
42+
clear_and_catch_warnings,
43+
data_path,
44+
runif_extra_has,
45+
suppress_warnings,
46+
)
4247

4348
from . import test_analyze as tana
4449
from . import test_spm99analyze as tspm
@@ -558,6 +563,22 @@ def test_slice_times(self):
558563
assert_equal(hdr['slice_end'], 5)
559564
assert_array_almost_equal(hdr['slice_duration'], 0.1)
560565

566+
# Ambiguous case
567+
hdr2 = self.header_class()
568+
hdr2.set_dim_info(slice=2)
569+
hdr2.set_slice_duration(0.1)
570+
hdr2.set_data_shape((1, 1, 2))
571+
with clear_and_catch_warnings() as w:
572+
warnings.simplefilter("always")
573+
hdr2.set_slice_times([0.1, 0])
574+
assert len(w) == 1
575+
# but always must be choosing sequential one first
576+
assert_equal(hdr2.get_value_label('slice_code'), 'sequential decreasing')
577+
# and the other direction
578+
hdr2.set_slice_times([0, 0.1])
579+
assert_equal(hdr2.get_value_label('slice_code'), 'sequential increasing')
580+
581+
561582
def test_intents(self):
562583
ehdr = self.header_class()
563584
ehdr.set_intent('t test', (10,), name='some score')

nibabel/volumeutils.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import warnings
1414
import gzip
1515
import bz2
16+
from collections import OrderedDict
1617
from os.path import exists, splitext
1718
from operator import mul
1819
from functools import reduce
@@ -22,6 +23,7 @@
2223
from .casting import (shared_range, type_info, OK_FLOATS)
2324
from .openers import Opener
2425
from .deprecated import deprecate_with_version
26+
from .externals.oset import OrderedSet
2527

2628
sys_is_le = sys.byteorder == 'little'
2729
native_code = sys_is_le and '<' or '>'
@@ -78,7 +80,7 @@ class Recoder(object):
7880
2
7981
'''
8082

81-
def __init__(self, codes, fields=('code',), map_maker=dict):
83+
def __init__(self, codes, fields=('code',), map_maker=OrderedDict):
8284
''' Create recoder object
8385
8486
``codes`` give a sequence of code, alias sequences
@@ -97,7 +99,7 @@ def __init__(self, codes, fields=('code',), map_maker=dict):
9799
98100
Parameters
99101
----------
100-
codes : seqence of sequences
102+
codes : sequence of sequences
101103
Each sequence defines values (codes) that are equivalent
102104
fields : {('code',) string sequence}, optional
103105
names by which elements in sequences can be accessed
@@ -133,13 +135,15 @@ def add_codes(self, code_syn_seqs):
133135
134136
Examples
135137
--------
136-
>>> code_syn_seqs = ((1, 'one'), (2, 'two'))
138+
>>> code_syn_seqs = ((2, 'two'), (1, 'one'))
137139
>>> rc = Recoder(code_syn_seqs)
138140
>>> rc.value_set() == set((1,2))
139141
True
140142
>>> rc.add_codes(((3, 'three'), (1, 'first')))
141143
>>> rc.value_set() == set((1,2,3))
142144
True
145+
>>> print(rc.value_set()) # set is actually ordered
146+
OrderedSet([2, 1, 3])
143147
'''
144148
for code_syns in code_syn_seqs:
145149
# Add all the aliases
@@ -186,7 +190,7 @@ def keys(self):
186190
return self.field1.keys()
187191

188192
def value_set(self, name=None):
189-
''' Return set of possible returned values for column
193+
''' Return OrderedSet of possible returned values for column
190194
191195
By default, the column is the first column.
192196
@@ -212,7 +216,7 @@ def value_set(self, name=None):
212216
d = self.field1
213217
else:
214218
d = self.__dict__[name]
215-
return set(d.values())
219+
return OrderedSet(d.values())
216220

217221

218222
# Endian code aliases

0 commit comments

Comments
 (0)