diff --git a/bin/parrec2nii b/bin/parrec2nii index 166bcfdcb2..e49aea3181 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -138,7 +138,9 @@ def proc_file(infile, opts): # load the PAR header and data scaling = None if opts.scaling == 'off' else opts.scaling infile = fname_ext_ul_case(infile) - pr_img = pr.load(infile, opts.permit_truncated, scaling) + pr_img = pr.load(infile, + permit_truncated=opts.permit_truncated, + scaling=scaling) pr_hdr = pr_img.header raw_data = pr_img.dataobj.get_unscaled() affine = pr_hdr.get_affine(origin=opts.origin) diff --git a/nibabel/keywordonly.py b/nibabel/keywordonly.py new file mode 100644 index 0000000000..53bfcdd2d6 --- /dev/null +++ b/nibabel/keywordonly.py @@ -0,0 +1,27 @@ +""" Decorator for labeling keyword arguments as keyword only +""" + +from functools import wraps + +def kw_only_func(n): + """ Return function decorator enforcing maximum of `n` positional arguments + """ + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + if len(args) > n: + raise TypeError( + '{0} takes at most {1} positional argument{2}'.format( + func.__name__, n, 's' if n > 1 else '')) + return func(*args, **kwargs) + return wrapper + return decorator + + +def kw_only_meth(n): + """ Return method decorator enforcing maximum of `n` positional arguments + + The method has at least one positional argument ``self`` or ``cls``; allow + for that. + """ + return kw_only_func(n+1) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 92032006bf..08f93527ea 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -82,6 +82,7 @@ from copy import deepcopy import re +from .keywordonly import kw_only_meth from .spatialimages import SpatialImage, Header from .eulerangles import euler2mat from .volumeutils import Recoder, array_from_file, BinOpener @@ -462,6 +463,18 @@ def _data_from_rec(rec_fileobj, in_shape, dtype, slice_indices, out_shape, class PARRECArrayProxy(object): def __init__(self, file_like, header, scaling): + """ Initialize PARREC array proxy + + Parameters + ---------- + file_like : file-like object + Filename or object implementing ``read, seek, tell`` + header : PARRECHeader instance + Implementing ``get_data_shape, get_data_dtype``, + ``get_sorted_slice_indices``, ``get_data_scaling`` + scaling : {'fp', 'dv'} + Type of scaling to use - see header ``get_data_scaling`` method. + """ self.file_like = file_like # Copies of values needed to read array self._shape = header.get_data_shape() @@ -512,6 +525,7 @@ def __init__(self, info, image_defs, permit_truncated=False): """ self.general_info = info.copy() self.image_defs = image_defs.copy() + self.permit_truncated = permit_truncated _truncation_checks(info, image_defs, permit_truncated) # charge with basic properties to be able to use base class # functionality @@ -538,7 +552,8 @@ def from_fileobj(klass, fileobj, permit_truncated=False): def copy(self): return PARRECHeader(deepcopy(self.general_info), - self.image_defs.copy()) + self.image_defs.copy(), + self.permit_truncated) def as_analyze_map(self): """Convert PAR parameters to NIFTI1 format""" @@ -886,7 +901,8 @@ class PARRECImage(SpatialImage): ImageArrayProxy = PARRECArrayProxy @classmethod - def from_file_map(klass, file_map, permit_truncated, scaling): + @kw_only_meth(1) + def from_file_map(klass, file_map, permit_truncated=False, scaling='dv'): pt = permit_truncated with file_map['header'].get_prepare_fileobj('rt') as hdr_fobj: hdr = klass.header_class.from_fileobj(hdr_fobj, @@ -896,8 +912,15 @@ def from_file_map(klass, file_map, permit_truncated, scaling): return klass(data, hdr.get_affine(), header=hdr, extra=None, file_map=file_map) + @classmethod + @kw_only_meth(1) + def from_filename(klass, filename, permit_truncated=False, scaling='dv'): + file_map = klass.filespec_to_file_map(filename) + return klass.from_file_map(file_map, + permit_truncated=permit_truncated, + scaling=scaling) + + load = from_filename + -def load(filename, permit_truncated=False, scaling='dv'): - file_map = PARRECImage.filespec_to_file_map(filename) - return PARRECImage.from_file_map(file_map, permit_truncated, scaling) -load.__doc__ = PARRECImage.load.__doc__ +load = PARRECImage.load diff --git a/nibabel/tests/data/README.rst b/nibabel/tests/data/README.rst new file mode 100644 index 0000000000..c33499578a --- /dev/null +++ b/nibabel/tests/data/README.rst @@ -0,0 +1,8 @@ +################## +Nibabel data files +################## + +``phantom_truncated.REC`` is a copy of ``phantom_EPI_asc_CLEAR_2_1.REC``. + +``phantom_truncated.PAR`` is a slightly edited copy of +``phantom_EPI_asc_CLEAR_2_1.PAR``. diff --git a/nibabel/tests/data/phantom_truncated.PAR b/nibabel/tests/data/phantom_truncated.PAR new file mode 100644 index 0000000000..3013c81972 --- /dev/null +++ b/nibabel/tests/data/phantom_truncated.PAR @@ -0,0 +1,129 @@ +# === DATA DESCRIPTION FILE ====================================================== +# +# CAUTION - Investigational device. +# Limited by Federal Law to investigational use. +# +# Dataset name: E:\\Export\phantom_EPI_asc_CLEAR_2_1 +# +# CLINICAL TRYOUT Research image export tool V4.2 +# +# === GENERAL INFORMATION ======================================================== +# +. Patient name : phantom +. Examination name : Konvertertest +. Protocol name : EPI_asc CLEAR +. Examination date/time : 2014.02.14 / 09:00:57 +. Series Type : Image MRSERIES +. Acquisition nr : 2 +. Reconstruction nr : 1 +. Scan Duration [sec] : 14 +. Max. number of cardiac phases : 1 +. Max. number of echoes : 1 +. Max. number of slices/locations : 9 +. Max. number of dynamics : 4 +. Max. number of mixes : 1 +. Patient position : Head First Supine +. Preparation direction : Anterior-Posterior +. Technique : FEEPI +. Scan resolution (x, y) : 64 39 +. Scan mode : MS +. Repetition time [ms] : 2000.000 +. FOV (ap,fh,rl) [mm] : 240.000 70.000 240.000 +. Water Fat shift [pixels] : 11.050 +. Angulation midslice(ap,fh,rl)[degr]: -13.265 0.000 0.000 +. Off Centre midslice(ap,fh,rl) [mm] : 2.508 30.339 -16.032 +. Flow compensation <0=no 1=yes> ? : 0 +. Presaturation <0=no 1=yes> ? : 0 +. Phase encoding velocity [cm/sec] : 0.000000 0.000000 0.000000 +. MTC <0=no 1=yes> ? : 0 +. SPIR <0=no 1=yes> ? : 1 +. EPI factor <0,1=no EPI> : 39 +. Dynamic scan <0=no 1=yes> ? : 1 +. Diffusion <0=no 1=yes> ? : 0 +. Diffusion echo time [ms] : 0.0000 +. Max. number of diffusion values : 1 +. Max. number of gradient orients : 1 +. Number of label types <0=no ASL> : 0 +# +# === PIXEL VALUES ============================================================= +# PV = pixel value in REC file, FP = floating point value, DV = displayed value on console +# RS = rescale slope, RI = rescale intercept, SS = scale slope +# DV = PV * RS + RI FP = DV / (RS * SS) +# +# === IMAGE INFORMATION DEFINITION ============================================= +# The rest of this file contains ONE line per image, this line contains the following information: +# +# slice number (integer) +# echo number (integer) +# dynamic scan number (integer) +# cardiac phase number (integer) +# image_type_mr (integer) +# scanning sequence (integer) +# index in REC file (in images) (integer) +# image pixel size (in bits) (integer) +# scan percentage (integer) +# recon resolution (x y) (2*integer) +# rescale intercept (float) +# rescale slope (float) +# scale slope (float) +# window center (integer) +# window width (integer) +# image angulation (ap,fh,rl in degrees ) (3*float) +# image offcentre (ap,fh,rl in mm ) (3*float) +# slice thickness (in mm ) (float) +# slice gap (in mm ) (float) +# image_display_orientation (integer) +# slice orientation ( TRA/SAG/COR ) (integer) +# fmri_status_indication (integer) +# image_type_ed_es (end diast/end syst) (integer) +# pixel spacing (x,y) (in mm) (2*float) +# echo_time (float) +# dyn_scan_begin_time (float) +# trigger_time (float) +# diffusion_b_factor (float) +# number of averages (integer) +# image_flip_angle (in degrees) (float) +# cardiac frequency (bpm) (integer) +# minimum RR-interval (in ms) (integer) +# maximum RR-interval (in ms) (integer) +# TURBO factor <0=no turbo> (integer) +# Inversion delay (in ms) (float) +# diffusion b value number (imagekey!) (integer) +# gradient orientation number (imagekey!) (integer) +# contrast type (string) +# diffusion anisotropy type (string) +# diffusion (ap, fh, rl) (3*float) +# label type (ASL) (imagekey!) (integer) +# +# === IMAGE INFORMATION ========================================================== +# sl ec dyn ph ty idx pix scan% rec size (re)scale window angulation offcentre thick gap info spacing echo dtime ttime diff avg flip freq RR-int turbo delay b grad cont anis diffusion L.ty + + 1 1 1 1 0 2 0 16 62 64 64 0.00000 1.29035 4.28404e-003 1070 1860 -13.26 -0.00 -0.00 2.51 -0.81 -8.69 6.000 2.000 0 1 0 2 3.750 3.750 30.00 0.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 2 1 1 1 0 2 1 16 62 64 64 0.00000 1.29035 4.28404e-003 1122 1951 -13.26 -0.00 -0.00 2.51 6.98 -10.53 6.000 2.000 0 1 0 2 3.750 3.750 30.00 0.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 3 1 1 1 0 2 2 16 62 64 64 0.00000 1.29035 4.28404e-003 1137 1977 -13.26 -0.00 -0.00 2.51 14.77 -12.36 6.000 2.000 0 1 0 2 3.750 3.750 30.00 0.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 4 1 1 1 0 2 3 16 62 64 64 0.00000 1.29035 4.28404e-003 1217 2116 -13.26 -0.00 -0.00 2.51 22.55 -14.20 6.000 2.000 0 1 0 2 3.750 3.750 30.00 0.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 5 1 1 1 0 2 4 16 62 64 64 0.00000 1.29035 4.28404e-003 1216 2113 -13.26 -0.00 -0.00 2.51 30.34 -16.03 6.000 2.000 0 1 0 2 3.750 3.750 30.00 0.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 6 1 1 1 0 2 5 16 62 64 64 0.00000 1.29035 4.28404e-003 1141 1983 -13.26 -0.00 -0.00 2.51 38.13 -17.87 6.000 2.000 0 1 0 2 3.750 3.750 30.00 0.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 7 1 1 1 0 2 6 16 62 64 64 0.00000 1.29035 4.28404e-003 1119 1945 -13.26 -0.00 -0.00 2.51 45.91 -19.70 6.000 2.000 0 1 0 2 3.750 3.750 30.00 0.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 8 1 1 1 0 2 7 16 62 64 64 0.00000 1.29035 4.28404e-003 1097 1907 -13.26 -0.00 -0.00 2.51 53.70 -21.54 6.000 2.000 0 1 0 2 3.750 3.750 30.00 0.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 9 1 1 1 0 2 8 16 62 64 64 0.00000 1.29035 4.28404e-003 1146 1991 -13.26 -0.00 -0.00 2.51 61.49 -23.37 6.000 2.000 0 1 0 2 3.750 3.750 30.00 0.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 1 1 2 1 0 2 9 16 62 64 64 0.00000 1.29035 4.28404e-003 1071 1863 -13.26 -0.00 -0.00 2.51 -0.81 -8.69 6.000 2.000 0 1 0 2 3.750 3.750 30.00 2.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 2 1 2 1 0 2 10 16 62 64 64 0.00000 1.29035 4.28404e-003 1123 1953 -13.26 -0.00 -0.00 2.51 6.98 -10.53 6.000 2.000 0 1 0 2 3.750 3.750 30.00 2.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 3 1 2 1 0 2 11 16 62 64 64 0.00000 1.29035 4.28404e-003 1135 1973 -13.26 -0.00 -0.00 2.51 14.77 -12.36 6.000 2.000 0 1 0 2 3.750 3.750 30.00 2.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 4 1 2 1 0 2 12 16 62 64 64 0.00000 1.29035 4.28404e-003 1209 2101 -13.26 -0.00 -0.00 2.51 22.55 -14.20 6.000 2.000 0 1 0 2 3.750 3.750 30.00 2.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 5 1 2 1 0 2 13 16 62 64 64 0.00000 1.29035 4.28404e-003 1215 2113 -13.26 -0.00 -0.00 2.51 30.34 -16.03 6.000 2.000 0 1 0 2 3.750 3.750 30.00 2.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 6 1 2 1 0 2 14 16 62 64 64 0.00000 1.29035 4.28404e-003 1145 1990 -13.26 -0.00 -0.00 2.51 38.13 -17.87 6.000 2.000 0 1 0 2 3.750 3.750 30.00 2.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 7 1 2 1 0 2 15 16 62 64 64 0.00000 1.29035 4.28404e-003 1119 1945 -13.26 -0.00 -0.00 2.51 45.91 -19.70 6.000 2.000 0 1 0 2 3.750 3.750 30.00 2.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 8 1 2 1 0 2 16 16 62 64 64 0.00000 1.29035 4.28404e-003 1093 1899 -13.26 -0.00 -0.00 2.51 53.70 -21.54 6.000 2.000 0 1 0 2 3.750 3.750 30.00 2.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 9 1 2 1 0 2 17 16 62 64 64 0.00000 1.29035 4.28404e-003 1150 1999 -13.26 -0.00 -0.00 2.51 61.49 -23.37 6.000 2.000 0 1 0 2 3.750 3.750 30.00 2.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 1 1 3 1 0 2 18 16 62 64 64 0.00000 1.29035 4.28404e-003 1070 1860 -13.26 -0.00 -0.00 2.51 -0.81 -8.69 6.000 2.000 0 1 0 2 3.750 3.750 30.00 4.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 2 1 3 1 0 2 19 16 62 64 64 0.00000 1.29035 4.28404e-003 1125 1955 -13.26 -0.00 -0.00 2.51 6.98 -10.53 6.000 2.000 0 1 0 2 3.750 3.750 30.00 4.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 3 1 3 1 0 2 20 16 62 64 64 0.00000 1.29035 4.28404e-003 1135 1973 -13.26 -0.00 -0.00 2.51 14.77 -12.36 6.000 2.000 0 1 0 2 3.750 3.750 30.00 4.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 4 1 3 1 0 2 21 16 62 64 64 0.00000 1.29035 4.28404e-003 1211 2105 -13.26 -0.00 -0.00 2.51 22.55 -14.20 6.000 2.000 0 1 0 2 3.750 3.750 30.00 4.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 5 1 3 1 0 2 22 16 62 64 64 0.00000 1.29035 4.28404e-003 1218 2118 -13.26 -0.00 -0.00 2.51 30.34 -16.03 6.000 2.000 0 1 0 2 3.750 3.750 30.00 4.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 6 1 3 1 0 2 23 16 62 64 64 0.00000 1.29035 4.28404e-003 1143 1987 -13.26 -0.00 -0.00 2.51 38.13 -17.87 6.000 2.000 0 1 0 2 3.750 3.750 30.00 4.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 7 1 3 1 0 2 24 16 62 64 64 0.00000 1.29035 4.28404e-003 1120 1947 -13.26 -0.00 -0.00 2.51 45.91 -19.70 6.000 2.000 0 1 0 2 3.750 3.750 30.00 4.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 8 1 3 1 0 2 25 16 62 64 64 0.00000 1.29035 4.28404e-003 1093 1901 -13.26 -0.00 -0.00 2.51 53.70 -21.54 6.000 2.000 0 1 0 2 3.750 3.750 30.00 4.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + 9 1 3 1 0 2 26 16 62 64 64 0.00000 1.29035 4.28404e-003 1151 2001 -13.26 -0.00 -0.00 2.51 61.49 -23.37 6.000 2.000 0 1 0 2 3.750 3.750 30.00 4.00 0.00 0.00 0 90.00 0 0 0 39 0.0 1 1 8 0 0.000 0.000 0.000 1 + +# === END OF DATA DESCRIPTION FILE =============================================== diff --git a/nibabel/tests/data/phantom_truncated.REC b/nibabel/tests/data/phantom_truncated.REC new file mode 100644 index 0000000000..958095f2a8 Binary files /dev/null and b/nibabel/tests/data/phantom_truncated.REC differ diff --git a/nibabel/tests/test_keywordonly.py b/nibabel/tests/test_keywordonly.py new file mode 100644 index 0000000000..bdf662fdc0 --- /dev/null +++ b/nibabel/tests/test_keywordonly.py @@ -0,0 +1,39 @@ +""" Test kw_only decorators """ + +from ..keywordonly import kw_only_func, kw_only_meth + +from nose.tools import assert_true, assert_false, assert_equal, assert_raises + + +def test_kw_only_func(): + # Test decorator + def func(an_arg): + "My docstring" + return an_arg + assert_equal(func(1), 1) + assert_raises(TypeError, func, 1, 2) + dec_func = kw_only_func(1)(func) + assert_equal(dec_func(1), 1) + assert_raises(TypeError, dec_func, 1, 2) + assert_raises(TypeError, dec_func, 1, akeyarg=3) + assert_equal(dec_func.__doc__, 'My docstring') + @kw_only_func(1) + def kw_func(an_arg, a_kwarg='thing'): + "Another docstring" + return an_arg, a_kwarg + assert_equal(kw_func(1), (1, 'thing')) + assert_raises(TypeError, kw_func, 1, 2) + assert_equal(kw_func(1, a_kwarg=2), (1, 2)) + assert_raises(TypeError, kw_func, 1, akeyarg=3) + assert_equal(kw_func.__doc__, 'Another docstring') + class C(object): + @kw_only_meth(1) + def kw_meth(self, an_arg, a_kwarg='thing'): + "Method docstring" + return an_arg, a_kwarg + c = C() + assert_equal(c.kw_meth(1), (1, 'thing')) + assert_raises(TypeError, c.kw_meth, 1, 2) + assert_equal(c.kw_meth(1, a_kwarg=2), (1, 2)) + assert_raises(TypeError, c.kw_meth, 1, akeyarg=3) + assert_equal(c.kw_meth.__doc__, 'Method docstring') diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index 1ec0374177..8fb7bbfdee 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -10,8 +10,9 @@ from .. import parrec from ..parrec import (parse_PAR_header, PARRECHeader, PARRECError, vol_numbers, - vol_is_full) + vol_is_full, PARRECImage, PARRECArrayProxy) from ..openers import Opener +from ..fileholders import FileHolder from numpy.testing import (assert_almost_equal, assert_array_equal) @@ -19,7 +20,7 @@ from nose.tools import (assert_true, assert_false, assert_raises, assert_equal, assert_not_equal) -from ..testing import catch_warn_reset +from ..testing import catch_warn_reset, suppress_warnings DATA_PATH = pjoin(dirname(__file__), 'data') @@ -27,6 +28,9 @@ EG_REC = pjoin(DATA_PATH, 'phantom_EPI_asc_CLEAR_2_1.REC') with Opener(EG_PAR, 'rt') as _fobj: HDR_INFO, HDR_DEFS = parse_PAR_header(_fobj) +# Fake truncated +TRUNC_PAR = pjoin(DATA_PATH, 'phantom_truncated.PAR') +TRUNC_REC = pjoin(DATA_PATH, 'phantom_truncated.REC') # Affine as we determined it mid-2014 AN_OLD_AFFINE = np.array( [[-3.64994708, 0., 1.83564171, 123.66276611], @@ -243,6 +247,16 @@ def gen_par_fobj(): yield par, fobj +def test_truncated_load(): + # Test loading of truncated header + with open(TRUNC_PAR, 'rt') as fobj: + gen_info, slice_info = parse_PAR_header(fobj) + assert_raises(PARRECError, PARRECHeader, gen_info, slice_info) + with catch_warn_reset(record=True) as wlist: + hdr = PARRECHeader(gen_info, slice_info, True) + assert_equal(len(wlist), 1) + + def test_vol_calculations(): # Test vol_is_full on sample data for par, fobj in gen_par_fobj(): @@ -253,8 +267,10 @@ def test_vol_calculations(): assert_array_equal(vol_is_full(slice_nos, max_slice), True) if par.endswith('NA.PAR'): continue # Cannot parse this one + # Load truncated without warnings + with suppress_warnings(): + hdr = PARRECHeader(gen_info, slice_info, True) # Fourth dimension shows same number of volumes as vol_numbers - hdr = PARRECHeader(gen_info, slice_info) shape = hdr.get_data_shape() d4 = 1 if len(shape) == 3 else shape[3] assert_equal(max(vol_numbers(slice_nos)), d4 - 1) @@ -282,7 +298,8 @@ def test_null_diffusion_params(): if basename(par) in ('DTI.PAR', 'NA.PAR'): continue gen_info, slice_info = parse_PAR_header(fobj) - hdr = PARRECHeader(gen_info, slice_info) + with suppress_warnings(): + hdr = PARRECHeader(gen_info, slice_info, True) assert_equal(hdr.get_bvals_bvecs(), (None, None)) assert_equal(hdr.get_q_vectors(), None) @@ -367,3 +384,75 @@ def test_copy_on_init(): hdr.image_defs['image pixel size'] = 8 assert_array_equal(hdr.image_defs['image pixel size'], 8) assert_array_equal(HDR_DEFS['image pixel size'], 16) + + +def assert_arr_dict_equal(dict1, dict2): + assert_equal(set(dict1), set(dict2)) + for key, value1 in dict1.items(): + value2 = dict2[key] + assert_array_equal(value1, value2) + + +def test_header_copy(): + # Test header copying + hdr = PARRECHeader(HDR_INFO, HDR_DEFS) + hdr2 = hdr.copy() + + def assert_copy_ok(hdr1, hdr2): + assert_false(hdr1 is hdr2) + assert_equal(hdr1.permit_truncated, hdr2.permit_truncated) + assert_false(hdr1.general_info is hdr2.general_info) + assert_arr_dict_equal(hdr1.general_info, hdr2.general_info) + assert_false(hdr1.image_defs is hdr2.image_defs) + assert_array_equal(hdr1.image_defs, hdr2.image_defs) + + assert_copy_ok(hdr, hdr2) + assert_false(hdr.permit_truncated) + assert_false(hdr2.permit_truncated) + with open(TRUNC_PAR, 'rt') as fobj: + assert_raises(PARRECError, PARRECHeader.from_fileobj, fobj) + with open(TRUNC_PAR, 'rt') as fobj: + trunc_hdr = PARRECHeader.from_fileobj(fobj, True) + assert_true(trunc_hdr.permit_truncated) + trunc_hdr2 = trunc_hdr.copy() + assert_copy_ok(trunc_hdr, trunc_hdr2) + + +def test_image_creation(): + # Test parts of image API in parrec image creation + hdr = PARRECHeader(HDR_INFO, HDR_DEFS) + arr_prox_dv = np.array(PARRECArrayProxy(EG_REC, hdr, 'dv')) + arr_prox_fp = np.array(PARRECArrayProxy(EG_REC, hdr, 'fp')) + good_map = dict(image = FileHolder(EG_REC), + header = FileHolder(EG_PAR)) + trunc_map = dict(image = FileHolder(TRUNC_REC), + header = FileHolder(TRUNC_PAR)) + for func, good_param, trunc_param in ( + (PARRECImage.from_filename, EG_PAR, TRUNC_PAR), + (PARRECImage.load, EG_PAR, TRUNC_PAR), + (parrec.load, EG_PAR, TRUNC_PAR), + (PARRECImage.from_file_map, good_map, trunc_map)): + img = func(good_param) + assert_array_equal(img.dataobj, arr_prox_dv) + # permit_truncated is keyword only + assert_raises(TypeError, func, good_param, False) + img = func(good_param, permit_truncated=False) + assert_array_equal(img.dataobj, arr_prox_dv) + # scaling is keyword only + assert_raises(TypeError, func, good_param, False, 'dv') + img = func(good_param, permit_truncated=False, scaling='dv') + assert_array_equal(img.dataobj, arr_prox_dv) + img = func(good_param, scaling='dv') + assert_array_equal(img.dataobj, arr_prox_dv) + # Can use fp scaling + img = func(good_param, scaling='fp') + assert_array_equal(img.dataobj, arr_prox_fp) + # Truncated raises error without permit_truncated=True + assert_raises(PARRECError, func, trunc_param) + assert_raises(PARRECError, func, trunc_param, permit_truncated=False) + img = func(trunc_param, permit_truncated=True) + assert_array_equal(img.dataobj, arr_prox_dv) + img = func(trunc_param, permit_truncated=True, scaling='dv') + assert_array_equal(img.dataobj, arr_prox_dv) + img = func(trunc_param, permit_truncated=True, scaling='fp') + assert_array_equal(img.dataobj, arr_prox_fp)