Skip to content

Commit

Permalink
Merge pull request #585 from nipy/rel/2.2.1
Browse files Browse the repository at this point in the history
2.2.1 bug fix release
  • Loading branch information
effigies authored Nov 23, 2017
2 parents 303a370 + 9b1ead0 commit 6fb7538
Show file tree
Hide file tree
Showing 18 changed files with 150 additions and 71 deletions.
1 change: 1 addition & 0 deletions .mailmap
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ Satrajit Ghosh <[email protected]> Satrajit Ghosh <[email protected]>
Jasper J.F. van den Bosch <[email protected]> Jasper <[email protected]>
Gregory R. Lee <[email protected]> Gregory R. Lee <[email protected]>
Demian Wassermann <[email protected]> Demian Wassermann <[email protected]>
Paul McCarthy <[email protected]> Paul McCarthy <[email protected]>
19 changes: 19 additions & 0 deletions Changelog
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@ Gerhard (SG) and Eric Larson (EL).

References like "pr/298" refer to github pull request numbers.

2.2.1 (Wednesday 22 November 2017)
==================================

Bug fixes
---------

* Set L/R labels in orthoview correctly (pr/564) (CM)
* Defer use of ufunc / memmap test - allows "freezing" (pr/572) (MB, reviewed
by Satra Ghosh)
* Fix doctest failures with pre-release numpy (pr/582) (MB, reviewed by CM)

Maintenance
-----------

* Update documentation around NIfTI qform/sform codes (pr/576) (Paul McCarthy,
reviewed by MB, CM) + (pr/580) (Bennet Fauber, reviewed by Paul McCarthy)
* Skip precision test on macOS, newer numpy (pr/583) (MB, reviewed by CM)
* Simplify AppVeyor script, removing conda (pr/584) (MB, reviewed by CM)

2.2 (Friday 13 October 2017)
============================

Expand Down
58 changes: 16 additions & 42 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,60 +1,34 @@
# vim ft=yaml
# CI on Windows via appveyor
# This file was based on Olivier Grisel's python-appveyor-demo

environment:

matrix:
- PYTHON: "C:\\Python27-conda32"
PYTHON_VERSION: "2.7"
PYTHON_ARCH: "32"

- PYTHON: "C:\\Python34-conda32"
PYTHON_VERSION: "3.4"
PYTHON_ARCH: "32"

- PYTHON: "C:\\Python34-conda64"
PYTHON_VERSION: "3.4"
PYTHON_ARCH: "64"

- PYTHON: "C:\\Python35-conda64"
PYTHON_VERSION: "3.5"
PYTHON_ARCH: "64"

- PYTHON: "C:\\Python35-conda32"
PYTHON_VERSION: "3.5"
PYTHON_ARCH: "32"
- PYTHON: C:\Python27
- PYTHON: C:\Python27-x64
- PYTHON: C:\Python34
- PYTHON: C:\Python34-x64
- PYTHON: C:\Python35
- PYTHON: C:\Python35-x64
- PYTHON: C:\Python36
- PYTHON: C:\Python36-x64

install:
# Install miniconda Python
- "powershell ./tools/install_python.ps1"

# Prepend newly installed Python to the PATH of this build (this cannot be
# done from inside the powershell script as it would require to restart
# the parent CMD process).
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"

# Set up a conda environment:
- conda config --set always_yes yes
- conda update -q conda
- conda info -a
- conda create -q -n test-environment python=%PYTHON_VERSION%
- activate test-environment

# Check that we have the expected version and architecture for Python
- "python --version"
- "python -c \"import struct; print(struct.calcsize('P') * 8)\""
- SET PATH=%PYTHON%;%PYTHON%\Scripts;%PATH%

# Install the dependencies of the project.
- "conda install --yes --quiet numpy scipy matplotlib nose h5py mock"
- "pip install pydicom"
- "python setup.py install"
- "SET NIBABEL_DATA_DIR=%CD%\\nibabel-data"
- pip install numpy scipy matplotlib nose h5py mock
- pip install pydicom
- pip install .
- SET NIBABEL_DATA_DIR=%CD%\nibabel-data

build: false # Not a C# project, build stuff at the test step instead.

test_script:
# Change into an innocuous directory and find tests from installation
- "mkdir for_testing"
- "cd for_testing"
- "nosetests --with-doctest nibabel"
- mkdir for_testing
- cd for_testing
- nosetests --with-doctest nibabel
1 change: 1 addition & 0 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ contributed code and discussion (in rough order of appearance):
* Venky Reddy
* Mark Hymers
* Jasper J.F. van den Bosch
* Bennet Fauber

License reprise
===============
Expand Down
59 changes: 58 additions & 1 deletion doc/source/nifti_images.rst
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ You can get the affine and the code using the ``coded=True`` argument to
[ 0. , 0.32, 2.17, -7.25],
[ 0. , 0. , 0. , 1. ]]), array(1, dtype=int16))

You can set the sform with with the ``get_sform()`` method of the header and
You can set the sform with the ``set_sform()`` method of the header and
the image.

>>> n1_header.set_sform(np.diag([2, 3, 4, 1]))
Expand Down Expand Up @@ -314,6 +314,63 @@ The algorithm is defined in the ``get_best_affine()`` method. It is:
#. If ``qform_code`` != 0 ('unknown') use the qform affine; else
#. Use the fall-back affine.

.. _default-sform-qform-codes:

Default sform and qform codes
=============================

If you create a new image, e.g.:

>>> data = np.random.random((20, 20, 20))
>>> xform = np.eye(4) * 2
>>> img = nib.nifti1.Nifti1Image(data, xform)

The sform and qform codes will be initialised to 2 (aligned) and 0 (unknown)
respectively:

>>> img.get_sform(coded=True) # doctest: +NORMALIZE_WHITESPACE
(array([[ 2., 0., 0., 0.],
[ 0., 2., 0., 0.],
[ 0., 0., 2., 0.],
[ 0., 0., 0., 1.]]), array(2, dtype=int16))
>>> img.get_qform(coded=True)
(None, 0)

This is based on the assumption that the affine you specify for a newly
created image will align the image to some known coordinate system. According
to the `NIfTI specification <nifti1>`_, the qform is intended to encode a
transformation into scanner coordinates - for a programmatically created
image, we have no way of knowing what the scanner coordinate system is;
furthermore, the qform cannot be used to store an arbitrary affine transform,
as it is unable to encode shears. So the provided affine will be stored in the
sform, and the qform will be left uninitialised.

If you create a new image and specify an existing header, e.g.:

>>> example_ni1 = os.path.join(data_path, 'example4d.nii.gz')
>>> n1_img = nib.load(example_ni1)
>>> new_header = header=n1_img.header.copy()
>>> new_data = np.random.random(n1_img.shape[:3])
>>> new_img = nib.nifti1.Nifti1Image(data, None, header=new_header)

then the newly created image will inherit the same sform and qform codes that
are in the provided header. However, if you create a new image with both an
affine and a header specified, e.g.:

>>> xform = np.eye(4)
>>> new_img = nib.nifti1.Nifti1Image(data, xform, header=new_header)

then the sform and qform codes will *only* be preserved if the provided affine
is the same as the affine in the provided header. If the affines do not match,
the sform and qform codes will be set to their default values of 2 and 0
respectively. This is done on the basis that, if you are changing the affine,
you are likely to be changing the space to which the affine is pointing. So
the original sform and qform codes can no longer be assumed to be valid.

If you wish to set the sform and qform affines and/or codes to some other
value, you can always set them after creation using the ``set_sform`` and
``set_qform`` methods, as described above.

************
Data scaling
************
Expand Down
2 changes: 1 addition & 1 deletion nibabel/benchmarks/bench_arrayproxy_slicing.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def testfunc():
data[:, 1] = [r[4] for r in results]
try:
data[:, 2] = [r[3] / r[4] for r in results]
except:
except ZeroDivisionError:
data[:, 2] = np.nan
data[:, 3] = [r[5] - r[6] for r in results]

Expand Down
12 changes: 6 additions & 6 deletions nibabel/ecat.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,13 +468,13 @@ def get_series_framenumbers(mlist):
mlist_nframes = len(frames_order)
trueframenumbers = np.arange(nframes - mlist_nframes, nframes)
frame_dict = {}
try:
for frame_stored, (true_order, _) in frames_order.items():
# frame as stored in file -> true number in series
for frame_stored, (true_order, _) in frames_order.items():
# frame as stored in file -> true number in series
try:
frame_dict[frame_stored] = trueframenumbers[true_order] + 1
return frame_dict
except:
raise IOError('Error in header or mlist order unknown')
except IndexError:
raise IOError('Error in header or mlist order unknown')
return frame_dict


def read_subheaders(fileobj, mlist, endianness):
Expand Down
4 changes: 2 additions & 2 deletions nibabel/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
_version_major = 2
_version_minor = 2
_version_micro = 1
_version_extra = 'dev'
# _version_extra = ''
# _version_extra = 'dev'
_version_extra = ''

# Format expected by setup.py and doc/source/conf.py: string of form "X.Y.Z"
__version__ = "%s.%s.%s%s" % (_version_major,
Expand Down
2 changes: 1 addition & 1 deletion nibabel/nicom/dwiparams.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
'''
import numpy as np
import numpy.linalg as npl
from ..testing import setup_test # flake8: noqa F401
from ..testing import setup_test as setup_module # flake8: noqa F401


def B2q(B, tol=None):
Expand Down
15 changes: 14 additions & 1 deletion nibabel/nifti1.py
Original file line number Diff line number Diff line change
Expand Up @@ -1764,7 +1764,20 @@ def __init__(self, dataobj, affine, header=None,
if header is None and affine is not None:
self._affine2header()
# Copy docstring
__init__.doc = analyze.AnalyzeImage.__init__.__doc__
__init__.__doc__ = analyze.AnalyzeImage.__init__.__doc__ + '''
Notes
-----
If both a `header` and an `affine` are specified, and the `affine` does
not match the affine that is in the `header`, the `affine` will be used,
but the ``sform_code`` and ``qform_code`` fields in the header will be
re-initialised to their default values. This is performed on the basis
that, if you are changing the affine, you are likely to be changing the
space to which the affine is pointing. The :meth:`set_sform` and
:meth:`set_qform` methods can be used to update the codes after an image
has been created - see those methods, and the :ref:`manual
<default-sform-qform-codes>` for more details. '''


def update_header(self):
''' Harmonize header with image data and affine
Expand Down
4 changes: 2 additions & 2 deletions nibabel/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
data_path = abspath(pjoin(dirname(__file__), '..', 'tests', 'data'))


from .np_features import VIRAL_MEMMAP
from .np_features import memmap_after_ufunc

def assert_dt_equal(a, b):
""" Assert two numpy dtype specifiers are equal
Expand Down Expand Up @@ -218,4 +218,4 @@ def setup_test():
"""
from distutils.version import LooseVersion
if LooseVersion(np.__version__) >= LooseVersion('1.14'):
np.set_printoptions(sign='legacy')
np.set_printoptions(legacy="1.13")
16 changes: 10 additions & 6 deletions nibabel/testing/np_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
import numpy as np


def _memmap_after_ufunc():
def memmap_after_ufunc():
""" Return True if ufuncs on memmap arrays always return memmap arrays
This should be True for numpy < 1.12, False otherwise.
Memoize after first call. We do this to avoid having to call this when
importing nibabel.testing, because we cannot depend on the source file
being present - see gh-571.
"""
if memmap_after_ufunc.result is not None:
return memmap_after_ufunc.result
with open(__file__, 'rb') as fobj:
mm_arr = np.memmap(fobj, mode='r', shape=(10,), dtype=np.uint8)
mm_preserved = isinstance(mm_arr + 1, np.memmap)
return mm_preserved

memmap_after_ufunc.result = isinstance(mm_arr + 1, np.memmap)
return memmap_after_ufunc.result

# True if ufunc on memmap always returns a memmap
VIRAL_MEMMAP = _memmap_after_ufunc()
memmap_after_ufunc.result = None
8 changes: 5 additions & 3 deletions nibabel/tests/test_arrayproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from numpy.testing import assert_array_equal, assert_array_almost_equal
from nose.tools import (assert_true, assert_false, assert_equal,
assert_not_equal, assert_raises)
from nibabel.testing import VIRAL_MEMMAP
from nibabel.testing import memmap_after_ufunc

from .test_fileslice import slicer_samples
from .test_openers import patch_indexed_gzip
Expand Down Expand Up @@ -298,6 +298,8 @@ def check_mmap(hdr, offset, proxy_class,
# Whether scaled array memory backed by memory map (regardless of what
# numpy says).
scaled_really_mmap = unscaled_really_mmap and not has_scaling
# Whether ufunc on memmap return memmap
viral_memmap = memmap_after_ufunc()
with InTemporaryDirectory():
with open(fname, 'wb') as fobj:
fobj.write(b' ' * offset)
Expand All @@ -324,9 +326,9 @@ def check_mmap(hdr, offset, proxy_class,
assert_false(back_is_mmap)
else:
assert_equal(unscaled_is_mmap,
VIRAL_MEMMAP or unscaled_really_mmap)
viral_memmap or unscaled_really_mmap)
assert_equal(back_is_mmap,
VIRAL_MEMMAP or scaled_really_mmap)
viral_memmap or scaled_really_mmap)
if scaled_really_mmap:
assert_equal(back_data.mode, expected_mode)
del prox, back_data
Expand Down
9 changes: 8 additions & 1 deletion nibabel/tests/test_floating.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

PY2 = sys.version_info[0] < 3

from distutils.version import LooseVersion

import numpy as np

from ..casting import (floor_exact, ceil_exact, as_int, FloatingError,
Expand Down Expand Up @@ -103,7 +105,12 @@ def test_check_nmant_nexp():
ti = type_info(t)
if ti['nmant'] != 106: # This check does not work for PPC double pair
assert_true(_check_nmant(t, ti['nmant']))
assert_true(_check_maxexp(t, ti['maxexp']))
# Test fails for longdouble after blacklisting of OSX powl as of numpy
# 1.12 - see https://github.com/numpy/numpy/issues/8307
if (t != np.longdouble or
sys.platform != 'darwin' or
LooseVersion(np.__version__) < LooseVersion('1.12')):
assert_true(_check_maxexp(t, ti['maxexp']))


def test_as_int():
Expand Down
2 changes: 1 addition & 1 deletion nibabel/tests/test_image_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,9 @@ def validate_shape(self, imaker, params):

def validate_shape_deprecated(self, imaker, params):
# Check deprecated get_shape API
img = imaker()
with clear_and_catch_warnings() as w:
warnings.simplefilter('always', DeprecationWarning)
img = imaker()
assert_equal(img.get_shape(), params['shape'])
assert_equal(len(w), 1)

Expand Down
5 changes: 3 additions & 2 deletions nibabel/tests/test_spatialimages.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from .test_helpers import bytesio_round_trip
from ..testing import (clear_and_catch_warnings, suppress_warnings,
VIRAL_MEMMAP)
memmap_after_ufunc)
from ..tmpdirs import InTemporaryDirectory
from .. import load as top_load

Expand Down Expand Up @@ -464,6 +464,7 @@ def get_disk_image(self):
def test_load_mmap(self):
# Test memory mapping when loading images
img_klass = self.image_class
viral_memmap = memmap_after_ufunc()
with InTemporaryDirectory():
img, fname, has_scaling = self.get_disk_image()
file_map = img.file_map.copy()
Expand All @@ -485,7 +486,7 @@ def test_load_mmap(self):
# numpies returned a memmap object, even though the array
# has no mmap memory backing. See:
# https://github.com/numpy/numpy/pull/7406
if has_scaling and not VIRAL_MEMMAP:
if has_scaling and not viral_memmap:
expected_mode = None
kwargs = {}
if mmap is not None:
Expand Down
Loading

0 comments on commit 6fb7538

Please sign in to comment.