Commit 6ffd9c76 authored by William Clarke's avatar William Clarke
Browse files

Merge branch 'master' into 'master'

v1.1.4

See merge request !21
parents 7f92d9d5 63f916b3
Pipeline #10066 passed with stages
in 3 minutes and 58 seconds
This document contains the FSL-MRS release history in reverse chronological order.
1.1.4 (Tuesday 3rd August 2021)
-------------------------------
- Fixed bug in calculation of molality concentration. Tissue mole fractions had been swapped for tissue volume fractions. Molar concentrations unaffected.
- Fixed bug in mrs_tools split
- Fixed bug in alignment of multi-dimensional data.
- Fixed bug in fsl_mrsi: data without a water reference now works.
- fsl_mrsi now outputs fitting nuisance parameters: phases, and shifts & linewidths for each metabolite group.
- Add NIfTI-MRS reshape command
- Add basis_tools remove_peak option to run HLSVD, typical usage for removing TMS peak.
- Added an add_water_peak method to MRS class.
- Updated fit_FSLModel defaults to match fsl_mrs command line defaults.
1.1.3 (Tuesday 29th June 2021)
------------------------------
- Added mrs_tools script. Replaces mrs_vis and mrs_info. Adds split/merge/reorder functionality.
......
......@@ -22,6 +22,8 @@ Referencing to water is carried out by comparing the integrated water resonance
The integrated areas are shown in the final plot of the html report if a reference dataset is provided.
**Please note that molality concentrations were calculated incorrectly in versions prior to 1.1.4.**
References
----------
......
......@@ -9,6 +9,8 @@
import warnings
from copy import deepcopy
from fsl_mrs.utils import misc
from fsl_mrs.utils.constants import GYRO_MAG_RATIO, PPM_SHIFT, PPM_RANGE
from fsl_mrs.core.basis import Basis
......@@ -86,7 +88,7 @@ class MRS(object):
if isinstance(basis, np.ndarray):
self.basis = Basis(basis, names, basis_hdr)
elif isinstance(basis, Basis):
self.basis = basis
self.basis = deepcopy(basis)
else:
raise TypeError('Basis must be a numpy array (+ names & headers) or a fsl_mrs.core.Basis object.')
else:
......@@ -654,6 +656,25 @@ class MRS(object):
return len(ppmlist)
def add_water_peak(self, gamma=0.0, sigma=0.0, ppm=4.65, amp=1.0, name='H2O'):
"""Add a peak at 4.65 ppm to capture (residual) water.
:param gamma: Lorentzian broadening, defaults to 0
:type gamma: float, optional
:param sigma: Gaussian broadening, defaults to 0
:type sigma: float, optional
:param ppm: Peak position, defaults to 4.65
:type ppm: float, optional
:param amp: Peak amplitude, defaults to 1.0
:type amp: float, optional
:param name: Basis name, defaults to 'H2O'
:type name: str, optional
:return: Number of basis spectra added (1).
:rtype: int
"""
self._basis.add_peak(ppm, amp, name, gamma, sigma, conj=self.conj_Basis)
return 1
# Plotting functions
def plot(self, ppmlim=(0.2, 4.2)):
"""Plot the spectrum in the mrs object
......
......@@ -8,6 +8,8 @@
# Copyright (C) 2020 University of Oxford
# SHBASECOPYRIGHT
from copy import deepcopy
import numpy as np
import matplotlib.pyplot as plt
......@@ -25,7 +27,7 @@ class MRSI(object):
# process H2O
if H2O is None:
H2O = np.full(FID.shape, None)
H2O = np.full(FID.shape[:3], None)
elif H2O.shape != FID.shape:
raise ValueError('H2O must be None or numpy array '
'of the same shape as FID.')
......@@ -52,7 +54,7 @@ class MRSI(object):
if isinstance(basis, np.ndarray):
self._basis = Basis(basis, names, basis_hdr)
elif isinstance(basis, Basis):
self._basis = basis
self._basis = deepcopy(basis)
else:
raise TypeError('Basis must be a numpy array (+ names & headers) or a fsl_mrs.core.Basis object.')
else:
......@@ -206,9 +208,12 @@ class MRSI(object):
as a single MRS object.
'''
FID = misc.volume_to_list(self.data, self.mask)
H2O = misc.volume_to_list(self.H2O, self.mask)
FID = sum(FID) / len(FID)
H2O = sum(H2O) / len(H2O)
if not np.array_equal(self.H2O, np.full(self.data.shape[:3], None)):
H2O = misc.volume_to_list(self.H2O, self.mask)
H2O = sum(H2O) / len(H2O)
else:
H2O = None
mrs_out = MRS(FID=FID,
header=self.header,
......
......@@ -19,6 +19,24 @@ import fsl_mrs.core as core
from fsl_mrs.utils.misc import checkCFUnits
dimension_tags = {"DIM_COIL": "For storage of data from each individual receiver coil element.",
"DIM_DYN": "For storage of each individual acquisition transient."
" E.g. for post-acquisition B0 drift correction.",
"DIM_INDIRECT_0": "The indirect detection dimension - necessary for 2D"
" (and greater) MRS acquisitions.",
"DIM_INDIRECT_1": "The indirect detection dimension - necessary for 2D"
" (and greater) MRS acquisitions.",
"DIM_INDIRECT_2": "The indirect detection dimension - necessary for 2D"
" (and greater) MRS acquisitions.",
"DIM_PHASE_CYCLE": "Used for the time-proportional phase incrementation method.",
"DIM_EDIT": "Used for edited MRS techniques such as MEGA or HERMES.",
"DIM_MEAS": "Used to indicate multiple repeats of the full sequence"
" contained within the same original data file.",
"DIM_USER_0": "User defined dimension.",
"DIM_USER_1": "User defined dimension.",
"DIM_USER_2": "User defined dimension."}
def gen_new_nifti_mrs(data, dwelltime, spec_freq, nucleus='1H', affine=None, dim_tags=[None, None, None]):
'''Generate a NIFTI_MRS object from a np array and header info.
......@@ -107,20 +125,6 @@ class NIFTI_MRS(Image):
except KeyError:
raise NotNIFTI_MRS('NIFTI-MRS header extension must have nucleus and spectrometerFrequency keys.')
# Extract key parameters from the header extension
self._set_dim_tags()
def _set_dim_tags(self):
self.dim_tags = [None, None, None]
std_tags = ['DIM_COIL', 'DIM_DYN', 'DIM_INDIRECT_0']
for idx in range(3):
curr_dim = idx + 5
curr_tag = f'dim_{curr_dim}'
if curr_tag in self.hdr_ext:
self.dim_tags[idx] = self.hdr_ext[curr_tag]
elif curr_dim < self.ndim:
self.dim_tags[idx] = std_tags[idx]
def __getitem__(self, sliceobj):
'''Apply conjugation at use. This swaps from the
NIFTI-MRS and Levvit inspired right-handed reference frame
......@@ -179,7 +183,6 @@ class NIFTI_MRS(Image):
extension = Nifti1Extension(44, json_s.encode('UTF-8'))
self.header.extensions.clear()
self.header.extensions.append(extension)
self._set_dim_tags()
@property
def filename(self):
......@@ -190,6 +193,24 @@ class NIFTI_MRS(Image):
else:
return ''
@property
def dim_tags(self):
"""Return the three higher dimension tags"""
return self._read_dim_tags()
def _read_dim_tags(self):
"""Read dim tags from current header extension"""
dim_tags = [None, None, None]
std_tags = ['DIM_COIL', 'DIM_DYN', 'DIM_INDIRECT_0']
for idx in range(3):
curr_dim = idx + 5
curr_tag = f'dim_{curr_dim}'
if curr_tag in self.hdr_ext:
dim_tags[idx] = self.hdr_ext[curr_tag]
elif curr_dim < self.ndim:
dim_tags[idx] = std_tags[idx]
return dim_tags
def dim_position(self, dim_tag):
'''Return position of dim if it exists.'''
if dim_tag in self.dim_tags:
......@@ -237,6 +258,24 @@ class NIFTI_MRS(Image):
current_hdr_ext.pop(key, None)
self.hdr_ext = current_hdr_ext
def set_dim_tag(self, dim, tag):
"""Set or update the 'dim_N' fields
Tag must be one of the standard-defined tags (e.g. DIM_DYN)
:param dim: The existing dim tag or python dimension index (i.e. N-1)
:type dim: str or int
:param tag: New tag
:type tag: str
"""
if tag not in dimension_tags.keys():
raise ValueError(f'Tag must be one of: {", ".join(list(dimension_tags.keys()))}.')
dim = self._dim_tag_to_index(dim)
current_hdr_ext = self.hdr_ext
current_hdr_ext[f'dim_{dim + 1}'] = tag
self.hdr_ext = current_hdr_ext
def set_dim_info(self, dim, info_str):
"""Set or update the 'dim_N_info' field
......@@ -309,8 +348,6 @@ class NIFTI_MRS(Image):
hdr_ext[f'dim_{dd}_info'] = hdr_ext[f'dim_{dd + 1}_info']
new_obj.hdr_ext = hdr_ext
new_obj._set_dim_tags()
return new_obj
else:
return NIFTI_MRS(self.data, header=self.header)
......
......@@ -135,6 +135,24 @@ def main():
help='Specify metabolite to conjugate')
conjparser.set_defaults(func=conj)
# Remove peak
rempeakparser = sp.add_parser(
'remove_peak',
help='Edit a basis set by removing a peak (e.g. TMS reference).')
rempeakparser.add_argument('file', type=Path,
help='Basis file')
rempeakparser.add_argument('output', type=Path,
help='Output location, can overwrite.')
mutual = rempeakparser.add_mutually_exclusive_group(required=True)
mutual.add_argument('--metabolite', type=str, default=None,
help='Specify metabolite to edit')
mutual.add_argument('--all', action="store_true",
help='Edit all basis spectra.')
rempeakparser.add_argument('--ppmlim', default=(-.2, .2), type=float,
nargs=2, metavar=('LOW', 'HIGH'),
help='Peak removal ppm range (default=(-.2, .2))')
rempeakparser.set_defaults(func=rem_peak)
# Parse command-line arguments
args = p.parse_args()
......@@ -262,5 +280,16 @@ def conj(args):
).save(args.output, overwrite=True)
def rem_peak(args):
from fsl_mrs.utils import basis_tools
from fsl_mrs.utils.mrs_io import read_basis
basis_tools.remove_peak(
read_basis(args.file),
args.ppmlim,
name=args.metabolite,
all=args.all
).save(args.output, overwrite=True)
if __name__ == '__main__':
main()
......@@ -254,7 +254,7 @@ def main():
if args.hlsvd:
if args.verbose:
print('... Residual water removal ...')
hlsvdlimits = [-0.35, 0.35]
hlsvdlimits = [-0.25, 0.25]
supp_data = nifti_mrs_proc.remove_peaks(supp_data, hlsvdlimits, limit_units='ppm', report=report_dir)
if args.leftshift:
......
......@@ -315,11 +315,13 @@ def main():
# Generate the folders
concs_folder = os.path.join(args.output, 'concs')
uncer_folder = os.path.join(args.output, 'uncertainties')
nuisance_folder = os.path.join(args.output, 'nuisance')
qc_folder = os.path.join(args.output, 'qc')
fit_folder = os.path.join(args.output, 'fit')
os.mkdir(concs_folder)
os.mkdir(uncer_folder)
os.mkdir(nuisance_folder)
os.mkdir(qc_folder)
os.mkdir(fit_folder)
......@@ -367,6 +369,65 @@ def main():
cleanup=True,
dtype=float))
# Fitting nuisance parameters
# Phases - p0, p1
p0_list = [res[0].getPhaseParams()[0] for res in results]
file_p0 = os.path.join(nuisance_folder, 'p0.nii.gz')
save_img_output(file_p0,
mrsi.list_to_matched_array(
p0_list,
indicies=indicies,
cleanup=False,
dtype=float))
p1_list = [res[0].getPhaseParams()[1] for res in results]
file_p1 = os.path.join(nuisance_folder, 'p1.nii.gz')
save_img_output(file_p1,
mrsi.list_to_matched_array(
p1_list,
indicies=indicies,
cleanup=False,
dtype=float))
# Grouped - shifts, widths (gamma, sigma, combined)
for group in range(results[0][0].g):
shiftn_list = [res[0].getShiftParams()[group] for res in results]
file_sn = os.path.join(nuisance_folder, f'shift_group{group}.nii.gz')
save_img_output(file_sn,
mrsi.list_to_matched_array(
shiftn_list,
indicies=indicies,
cleanup=False,
dtype=float))
comb_n_list = [res[0].getLineShapeParams()[0][group] for res in results]
file_comb = os.path.join(nuisance_folder, f'combined_lw_group{group}.nii.gz')
save_img_output(file_comb,
mrsi.list_to_matched_array(
comb_n_list,
indicies=indicies,
cleanup=False,
dtype=float))
gamma_n_list = [res[0].getLineShapeParams()[1][group] for res in results]
file_gam = os.path.join(nuisance_folder, f'gamma_group{group}.nii.gz')
save_img_output(file_gam,
mrsi.list_to_matched_array(
gamma_n_list,
indicies=indicies,
cleanup=False,
dtype=float))
if results[0][0].model == 'voigt':
sigma_n_list = [res[0].getLineShapeParams()[2][group] for res in results]
file_sig = os.path.join(nuisance_folder, f'sigma_group{group}.nii.gz')
save_img_output(file_sig,
mrsi.list_to_matched_array(
sigma_n_list,
indicies=indicies,
cleanup=False,
dtype=float))
# qc - SNR & FWHM
for metab in results[0][0].original_metabs:
metab_fwhm_list = [res[0].getQCParams(metab=metab)[1]
......@@ -436,11 +497,11 @@ def runvoxel(mrs_in, args, Fitargs, echotime, repetition_time):
# Internal and Water quantification if requested
if (mrs.H2O is None) or (echotime is None) or (repetition_time is None):
if echotime is None:
if mrs.H2O is not None and echotime is None:
warnings.warn('H2O file provided but could not determine TE:'
' no absolute quantification will be performed.',
UserWarning)
if repetition_time is None:
if mrs.H2O is not None and repetition_time is None:
warnings.warn('H2O file provided but could not determine TR:'
' no absolute quantification will be performed.',
UserWarning)
......
......@@ -78,7 +78,7 @@ def main():
group.add_argument('--indices', type=int, nargs='+',
help='List of indices to extract into second file.'
'All indices are zero-indexed.')
group.add_argument('--index', type=int, nargs='+',
group.add_argument('--index', type=int,
help='Index to split at (split after index, zero-indexed).')
splitparser.add_argument('--output',
required=True, type=Path, default=Path('.'),
......@@ -256,7 +256,7 @@ def split(args):
split_name = args.file.with_suffix('').with_suffix('').name
# 2. Merge the files
if args.index:
if args.index is not None:
split_1, split_2 = nmrs_tools.split(to_split, args.dim, args.index)
elif args.indices:
split_1, split_2 = nmrs_tools.split(to_split, args.dim, args.indices)
......
......@@ -148,22 +148,29 @@ def test_basis_manipulations(synth_data):
assert mrs.numBasis == 6
assert mrs.names == ['ppm_2', 'MM09', 'MM12', 'MM14', 'MM17', 'MM21']
mrs.add_water_peak(gamma=10, sigma=10, name='myh2o')
assert mrs.basis.shape == (2048, 7)
assert mrs.numBasis == 7
assert mrs.names == ['ppm_2', 'MM09', 'MM12', 'MM14', 'MM17', 'MM21', 'myh2o']
mrs.ignore = ['MM09']
assert mrs.basis.shape == (2048, 5)
assert mrs.numBasis == 5
assert mrs.basis.shape == (2048, 6)
assert mrs.numBasis == 6
mrs.ignore = []
# This suprising result is because mrs.keep is still populated.
assert mrs.basis.shape == (2048, 1)
assert mrs.numBasis == 1
mrs.keep = []
assert mrs.basis.shape == (2048, 7)
assert mrs.numBasis == 7
assert mrs.basis.shape == (2048, 8)
assert mrs.numBasis == 8
mrs.keep = None
mrs.ignore = None
assert mrs.basis.shape == (2048, 7)
assert mrs.numBasis == 7
assert mrs.basis.shape == (2048, 8)
assert mrs.numBasis == 8
def test_nucleus_identification():
......
......@@ -177,6 +177,25 @@ def test_add_remove_field():
assert 'RepetitionTime' not in nmrs.hdr_ext
def test_set_dim_tag():
nmrs = NIFTI_MRS(data['unprocessed'])
with pytest.raises(ValueError) as exc_info:
nmrs.set_dim_tag('DIM_DYN', 'DIM_FOO')
assert exc_info.type is ValueError
assert exc_info.value.args[0] == \
'Tag must be one of: DIM_COIL, DIM_DYN, DIM_INDIRECT_0, DIM_INDIRECT_1, DIM_INDIRECT_2,'\
' DIM_PHASE_CYCLE, DIM_EDIT, DIM_MEAS, DIM_USER_0, DIM_USER_1, DIM_USER_2.'
nmrs.set_dim_tag('DIM_DYN', 'DIM_USER_0')
assert nmrs.hdr_ext['dim_6'] == 'DIM_USER_0'
assert nmrs.dim_tags == ['DIM_COIL', 'DIM_USER_0', None]
nmrs.set_dim_tag(4, 'DIM_USER_1')
assert nmrs.hdr_ext['dim_5'] == 'DIM_USER_1'
assert nmrs.dim_tags == ['DIM_USER_1', 'DIM_USER_0', None]
def test_set_dim_info():
nmrs = NIFTI_MRS(data['unprocessed'])
nmrs.set_dim_info('DIM_DYN', 'my info')
......
......@@ -41,9 +41,48 @@ def test_fsl_mrsi(tmp_path):
assert (tmp_path / 'fit_out/qc').exists()
assert (tmp_path / 'fit_out/uncertainties').exists()
assert (tmp_path / 'fit_out/concs').exists()
assert (tmp_path / 'fit_out/nuisance').exists()
assert (tmp_path / 'fit_out/concs/raw/NAA.nii.gz').exists()
assert (tmp_path / 'fit_out/concs/molality/NAA.nii.gz').exists()
assert (tmp_path / 'fit_out/uncertainties/NAA_sd.nii.gz').exists()
assert (tmp_path / 'fit_out/qc/NAA_snr.nii.gz').exists()
assert (tmp_path / 'fit_out/fit/fit.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/p0.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/p1.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/shift_group0.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/combined_lw_group0.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/gamma_group0.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/sigma_group0.nii.gz').exists()
def test_fsl_mrsi_noh2o(tmp_path):
subprocess.check_call(['fsl_mrsi',
'--data', data['metab'],
'--basis', data['basis'],
'--output', str(tmp_path / 'fit_out'),
'--add_MM',
'--mask', data['mask'],
'--overwrite',
'--combine', 'Cr', 'PCr'])
assert (tmp_path / 'fit_out/fit').exists()
assert (tmp_path / 'fit_out/qc').exists()
assert (tmp_path / 'fit_out/uncertainties').exists()
assert (tmp_path / 'fit_out/concs').exists()
assert (tmp_path / 'fit_out/nuisance').exists()
assert (tmp_path / 'fit_out/concs/raw/NAA.nii.gz').exists()
assert (tmp_path / 'fit_out/concs/internal/NAA.nii.gz').exists()
assert (tmp_path / 'fit_out/uncertainties/NAA_sd.nii.gz').exists()
assert (tmp_path / 'fit_out/qc/NAA_snr.nii.gz').exists()
assert (tmp_path / 'fit_out/fit/fit.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/p0.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/p1.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/shift_group0.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/combined_lw_group0.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/gamma_group0.nii.gz').exists()
assert (tmp_path / 'fit_out/nuisance/sigma_group0.nii.gz').exists()
......@@ -7,6 +7,7 @@ Copyright Will Clarke, University of Oxford, 2021'''
# Imports
import subprocess
from pathlib import Path
import nibabel as nib
# Files
testsPath = Path(__file__).parent
......@@ -99,6 +100,10 @@ def test_split(tmp_path):
assert (tmp_path / 'split_file_1.nii.gz').exists()
assert (tmp_path / 'split_file_2.nii.gz').exists()
f1 = nib.load(tmp_path / 'split_file_1.nii.gz')
f2 = nib.load(tmp_path / 'split_file_2.nii.gz')
assert f1.shape[5] == 32
assert f2.shape[5] == 32
subprocess.check_call(['mrs_tools', 'split',
'--dim', 'DIM_DYN',
......@@ -108,15 +113,24 @@ def test_split(tmp_path):
assert (tmp_path / 'metab_raw_1.nii.gz').exists()
assert (tmp_path / 'metab_raw_2.nii.gz').exists()
f1 = nib.load(tmp_path / 'metab_raw_1.nii.gz')
f2 = nib.load(tmp_path / 'metab_raw_2.nii.gz')
assert f1.shape[5] == 32
assert f2.shape[5] == 32
subprocess.check_call(['mrs_tools', 'split',
'--dim', 'DIM_DYN',
'--indices', '31', '34', '40',
'--filename', 'indicies_select',
'--output', str(tmp_path),
'--file', str(test_data_split)])
assert (tmp_path / 'metab_raw_1.nii.gz').exists()
assert (tmp_path / 'metab_raw_2.nii.gz').exists()
assert (tmp_path / 'indicies_select_1.nii.gz').exists()
assert (tmp_path / 'indicies_select_2.nii.gz').exists()
f1 = nib.load(tmp_path / 'indicies_select_1.nii.gz')
f2 = nib.load(tmp_path / 'indicies_select_2.nii.gz')
assert f1.shape[5] == 61
assert f2.shape[5] == 3
# Test reorder option
......
......@@ -8,6 +8,7 @@ from pathlib import Path
from fsl_mrs.utils.preproc import nifti_mrs_proc as nproc
from fsl_mrs.utils.mrs_io import read_FID
from fsl_mrs.utils.nifti_mrs_tools import split
from fsl_mrs import __version__
......@@ -58,11 +59,19 @@ def test_average():
def test_align():
nmrs_obj = read_FID(metab)
with_coils, _ = split(nmrs_obj, 'DIM_COIL', 3)
aligned1 = nproc.align(with_coils, 'DIM_DYN', ppmlim=(1.0, 4.0), niter=1, apodize=5)
assert aligned1.hdr_ext['ProcessingApplied'][0]['Method'] == 'Frequency and phase correction'
assert aligned1.hdr_ext['ProcessingApplied'][0]['Details']\
== 'fsl_mrs.utils.preproc.nifti_mrs_proc.align, dim=DIM_DYN, '\
'target=None, ppmlim=(1.0, 4.0), niter=1, apodize=5.'
combined = nproc.coilcombine(nmrs_obj)
aligned = nproc.align(combined, 'DIM_DYN', ppmlim=(1.0, 4.0), niter=1, apodize=5)
aligned2 = nproc.align(combined, 'DIM_DYN', ppmlim=(1.0, 4.0), niter=1, apodize=5)
assert aligned.hdr_ext['ProcessingApplied'][1]['Method'] == 'Frequency and phase correction'
assert aligned.hdr_ext['ProcessingApplied'][1]['Details']\
assert aligned2.hdr_ext['ProcessingApplied'][1]['Method'] == 'Frequency and phase correction'
assert aligned2.hdr_ext['ProcessingApplied'][1]['Details']\
== 'fsl_mrs.utils.preproc.nifti_mrs_proc.align, dim=DIM_DYN, '\
'target=None, ppmlim=(1.0, 4.0), niter=1, apodize=5.'
......
from pathlib import Path
import pytest
from fsl_mrs.utils import mrs_io
from fsl_mrs.utils import nifti_mrs_tools as nmrs_tools
testsPath = Path(__file__).parent
test_data = testsPath / 'testdata' / 'fsl_mrs_preproc' / 'metab_raw.nii.gz'
def test_reshape():
# Data is (1, 1, 1, 4096, 32, 64) ['DIM_COIL', 'DIM_DYN', None]
nmrs = mrs_io.read_FID(test_data)
new_shape = (16, 2, 64)
with pytest.raises(TypeError) as exc_info:
reshaped = nmrs_tools.reshape(nmrs, new_shape, d6='DIM_USER_0')