From 422fbddeab78ccc2459ed0eb4591f9654abd3177 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Mon, 4 Jun 2018 11:18:46 -0700 Subject: [PATCH 01/16] Add geomstats module + geodesic distance for Frame Displacement --- nipype/algorithms/confounds.py | 24 +++++++++++++++++++++--- requirements.txt | 27 ++++++++++++++------------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index a660a0fbae..03a0f0894f 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -13,6 +13,9 @@ import nibabel as nb import numpy as np + +from geomstats.invariant_metric import InvariantMetric +from geomstats.special_euclidean_group import SpecialEuclideanGroup from numpy.polynomial import Legendre from scipy import linalg @@ -26,6 +29,10 @@ IFLOGGER = logging.getLogger('interface') +SE3_GROUP = SpecialEuclideanGroup(n=3) +DIM_TRANSLATIONS = SE3_GROUP.translations.dimension +DIM_ROTATIONS = SE3_GROUP.rotations.dimension + class ComputeDVARSInputSpec(BaseInterfaceInputSpec): in_file = File( @@ -307,9 +314,20 @@ def _run_interface(self, runtime): axis=1, arr=mpars, source=self.inputs.parameter_source) - diff = mpars[:-1, :6] - mpars[1:, :6] - diff[:, 3:6] *= self.inputs.radius - fd_res = np.abs(diff).sum(axis=1) + + # TODO(nina): convert to geomstats parameterization: + se3pars = np.hstack( + [mpars[:, DIM_TRANSLATIONS:], mpars[:, :DIM_TRANSLATIONS]]) + + diag_rotations = self.inputs.radius * np.ones(DIM_ROTATIONS) + diag_translations = np.ones(DIM_TRANSLATIONS) + diag = np.concatenate([diag_rotations, diag_translations]) + inner_product = np.diag(diag) + metric = InvariantMetric( + group=SE3_GROUP, + inner_product_mat_at_identity=inner_product, + left_or_right='left') + fd_res = metric.dist(se3pars[:-1], se3pars[1:]) self._results = { 'out_file': op.abspath(self.inputs.out_file), diff --git a/requirements.txt b/requirements.txt index 5ef00ec98b..8703267064 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,20 @@ -numpy>=1.9.0 -scipy>=0.14 +click>=6.6.0 +configparser +funcsigs +future>=0.16.0 +geomstats>=1.7 +mock networkx>=1.9 -traits>=4.6 -python-dateutil>=2.2 nibabel>=2.1.0 -future>=0.16.0 -simplejson>=3.8.0 +numpy>=1.9.0 +python-dateutil>=2.2 +packaging prov==1.5.0 -click>=6.6.0 -funcsigs -configparser +pydotplus +pydot>=1.2.3 pytest>=3.0 pytest-xdist pytest-env -mock -pydotplus -pydot>=1.2.3 -packaging +scipy>=0.14 +simplejson>=3.8.0 +traits>=4.6 From ea9f4dd624ec7b7dcc198f3fe1d8d12d59c1ece8 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Mon, 4 Jun 2018 11:18:46 -0700 Subject: [PATCH 02/16] Add geomstats module + geodesic distance for Frame Displacement --- nipype/algorithms/confounds.py | 24 +++++++++++++++++++++--- requirements.txt | 27 ++++++++++++++------------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index a660a0fbae..03a0f0894f 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -13,6 +13,9 @@ import nibabel as nb import numpy as np + +from geomstats.invariant_metric import InvariantMetric +from geomstats.special_euclidean_group import SpecialEuclideanGroup from numpy.polynomial import Legendre from scipy import linalg @@ -26,6 +29,10 @@ IFLOGGER = logging.getLogger('interface') +SE3_GROUP = SpecialEuclideanGroup(n=3) +DIM_TRANSLATIONS = SE3_GROUP.translations.dimension +DIM_ROTATIONS = SE3_GROUP.rotations.dimension + class ComputeDVARSInputSpec(BaseInterfaceInputSpec): in_file = File( @@ -307,9 +314,20 @@ def _run_interface(self, runtime): axis=1, arr=mpars, source=self.inputs.parameter_source) - diff = mpars[:-1, :6] - mpars[1:, :6] - diff[:, 3:6] *= self.inputs.radius - fd_res = np.abs(diff).sum(axis=1) + + # TODO(nina): convert to geomstats parameterization: + se3pars = np.hstack( + [mpars[:, DIM_TRANSLATIONS:], mpars[:, :DIM_TRANSLATIONS]]) + + diag_rotations = self.inputs.radius * np.ones(DIM_ROTATIONS) + diag_translations = np.ones(DIM_TRANSLATIONS) + diag = np.concatenate([diag_rotations, diag_translations]) + inner_product = np.diag(diag) + metric = InvariantMetric( + group=SE3_GROUP, + inner_product_mat_at_identity=inner_product, + left_or_right='left') + fd_res = metric.dist(se3pars[:-1], se3pars[1:]) self._results = { 'out_file': op.abspath(self.inputs.out_file), diff --git a/requirements.txt b/requirements.txt index 5ef00ec98b..8703267064 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,20 @@ -numpy>=1.9.0 -scipy>=0.14 +click>=6.6.0 +configparser +funcsigs +future>=0.16.0 +geomstats>=1.7 +mock networkx>=1.9 -traits>=4.6 -python-dateutil>=2.2 nibabel>=2.1.0 -future>=0.16.0 -simplejson>=3.8.0 +numpy>=1.9.0 +python-dateutil>=2.2 +packaging prov==1.5.0 -click>=6.6.0 -funcsigs -configparser +pydotplus +pydot>=1.2.3 pytest>=3.0 pytest-xdist pytest-env -mock -pydotplus -pydot>=1.2.3 -packaging +scipy>=0.14 +simplejson>=3.8.0 +traits>=4.6 From 9c65c57345cab8302b5069345a2973131486111b Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Wed, 6 Jun 2018 16:11:55 -0700 Subject: [PATCH 03/16] Add metric as an option to FramewiseDisplacement --- nipype/algorithms/confounds.py | 38 +++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index 03a0f0894f..f27bf5e1f1 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -307,7 +307,7 @@ class FramewiseDisplacement(BaseInterface): 'tags': ['method'] }] - def _run_interface(self, runtime): + def _run_interface(self, runtime, metric='L1'): mpars = np.loadtxt(self.inputs.in_file) # mpars is N_t x 6 mpars = np.apply_along_axis( func1d=normalize_mc_params, @@ -315,19 +315,29 @@ def _run_interface(self, runtime): arr=mpars, source=self.inputs.parameter_source) - # TODO(nina): convert to geomstats parameterization: - se3pars = np.hstack( - [mpars[:, DIM_TRANSLATIONS:], mpars[:, :DIM_TRANSLATIONS]]) - - diag_rotations = self.inputs.radius * np.ones(DIM_ROTATIONS) - diag_translations = np.ones(DIM_TRANSLATIONS) - diag = np.concatenate([diag_rotations, diag_translations]) - inner_product = np.diag(diag) - metric = InvariantMetric( - group=SE3_GROUP, - inner_product_mat_at_identity=inner_product, - left_or_right='left') - fd_res = metric.dist(se3pars[:-1], se3pars[1:]) + if metric == 'L1': + diff = mpars[:-1, :6] - mpars[1:, :6] + diff[:, 3:6] *= self.inputs.radius + fd_res = np.abs(diff).sum(axis=1) + + elif metric == 'riemannian': + # TODO(nina): convert to geomstats parameterization: + se3pars = np.hstack( + [mpars[:, DIM_TRANSLATIONS:], mpars[:, :DIM_TRANSLATIONS]]) + + diag_rotations = self.inputs.radius * np.ones(DIM_ROTATIONS) + diag_translations = np.ones(DIM_TRANSLATIONS) + diag = np.concatenate([diag_rotations, diag_translations]) + inner_product = np.diag(diag) + metric = InvariantMetric( + group=SE3_GROUP, + inner_product_mat_at_identity=inner_product, + left_or_right='left') + fd_res = metric.dist(se3pars[:-1], se3pars[1:]) + + else: + raise ValueError( + 'The parameter \'metric\' should be \'L1\' or \'riemannian\'.') self._results = { 'out_file': op.abspath(self.inputs.out_file), From 30098a551f9e1ac79e352234a1b7ca980fd71dcb Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Mon, 4 Jun 2018 11:18:46 -0700 Subject: [PATCH 04/16] Add geomstats module + geodesic distance for Frame Displacement --- nipype/algorithms/confounds.py | 24 +++++++++++++++++++++--- requirements.txt | 27 ++++++++++++++------------- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index d0f9a5733a..8c79ecedf5 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -13,6 +13,9 @@ import nibabel as nb import numpy as np + +from geomstats.invariant_metric import InvariantMetric +from geomstats.special_euclidean_group import SpecialEuclideanGroup from numpy.polynomial import Legendre from scipy import linalg @@ -26,6 +29,10 @@ IFLOGGER = logging.getLogger('nipype.interface') +SE3_GROUP = SpecialEuclideanGroup(n=3) +DIM_TRANSLATIONS = SE3_GROUP.translations.dimension +DIM_ROTATIONS = SE3_GROUP.rotations.dimension + class ComputeDVARSInputSpec(BaseInterfaceInputSpec): in_file = File( @@ -307,9 +314,20 @@ def _run_interface(self, runtime): axis=1, arr=mpars, source=self.inputs.parameter_source) - diff = mpars[:-1, :6] - mpars[1:, :6] - diff[:, 3:6] *= self.inputs.radius - fd_res = np.abs(diff).sum(axis=1) + + # TODO(nina): convert to geomstats parameterization: + se3pars = np.hstack( + [mpars[:, DIM_TRANSLATIONS:], mpars[:, :DIM_TRANSLATIONS]]) + + diag_rotations = self.inputs.radius * np.ones(DIM_ROTATIONS) + diag_translations = np.ones(DIM_TRANSLATIONS) + diag = np.concatenate([diag_rotations, diag_translations]) + inner_product = np.diag(diag) + metric = InvariantMetric( + group=SE3_GROUP, + inner_product_mat_at_identity=inner_product, + left_or_right='left') + fd_res = metric.dist(se3pars[:-1], se3pars[1:]) self._results = { 'out_file': op.abspath(self.inputs.out_file), diff --git a/requirements.txt b/requirements.txt index 5ef00ec98b..8703267064 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,20 @@ -numpy>=1.9.0 -scipy>=0.14 +click>=6.6.0 +configparser +funcsigs +future>=0.16.0 +geomstats>=1.7 +mock networkx>=1.9 -traits>=4.6 -python-dateutil>=2.2 nibabel>=2.1.0 -future>=0.16.0 -simplejson>=3.8.0 +numpy>=1.9.0 +python-dateutil>=2.2 +packaging prov==1.5.0 -click>=6.6.0 -funcsigs -configparser +pydotplus +pydot>=1.2.3 pytest>=3.0 pytest-xdist pytest-env -mock -pydotplus -pydot>=1.2.3 -packaging +scipy>=0.14 +simplejson>=3.8.0 +traits>=4.6 From b35fc5e813cb7bc36abb4dd31690110533b998b2 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Mon, 4 Jun 2018 11:18:46 -0700 Subject: [PATCH 05/16] Add geomstats module + geodesic distance for Frame Displacement --- nipype/algorithms/confounds.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index 8c79ecedf5..b9be8af554 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -33,6 +33,10 @@ DIM_TRANSLATIONS = SE3_GROUP.translations.dimension DIM_ROTATIONS = SE3_GROUP.rotations.dimension +SE3_GROUP = SpecialEuclideanGroup(n=3) +DIM_TRANSLATIONS = SE3_GROUP.translations.dimension +DIM_ROTATIONS = SE3_GROUP.rotations.dimension + class ComputeDVARSInputSpec(BaseInterfaceInputSpec): in_file = File( From 6199667b6a23b209ccfa37cd84067b824e0eb927 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Wed, 6 Jun 2018 16:11:55 -0700 Subject: [PATCH 06/16] Add metric as an option to FramewiseDisplacement --- nipype/algorithms/confounds.py | 38 +++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index b9be8af554..0d27dda862 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -311,7 +311,7 @@ class FramewiseDisplacement(BaseInterface): 'tags': ['method'] }] - def _run_interface(self, runtime): + def _run_interface(self, runtime, metric='L1'): mpars = np.loadtxt(self.inputs.in_file) # mpars is N_t x 6 mpars = np.apply_along_axis( func1d=normalize_mc_params, @@ -319,19 +319,29 @@ def _run_interface(self, runtime): arr=mpars, source=self.inputs.parameter_source) - # TODO(nina): convert to geomstats parameterization: - se3pars = np.hstack( - [mpars[:, DIM_TRANSLATIONS:], mpars[:, :DIM_TRANSLATIONS]]) - - diag_rotations = self.inputs.radius * np.ones(DIM_ROTATIONS) - diag_translations = np.ones(DIM_TRANSLATIONS) - diag = np.concatenate([diag_rotations, diag_translations]) - inner_product = np.diag(diag) - metric = InvariantMetric( - group=SE3_GROUP, - inner_product_mat_at_identity=inner_product, - left_or_right='left') - fd_res = metric.dist(se3pars[:-1], se3pars[1:]) + if metric == 'L1': + diff = mpars[:-1, :6] - mpars[1:, :6] + diff[:, 3:6] *= self.inputs.radius + fd_res = np.abs(diff).sum(axis=1) + + elif metric == 'riemannian': + # TODO(nina): convert to geomstats parameterization: + se3pars = np.hstack( + [mpars[:, DIM_TRANSLATIONS:], mpars[:, :DIM_TRANSLATIONS]]) + + diag_rotations = self.inputs.radius * np.ones(DIM_ROTATIONS) + diag_translations = np.ones(DIM_TRANSLATIONS) + diag = np.concatenate([diag_rotations, diag_translations]) + inner_product = np.diag(diag) + metric = InvariantMetric( + group=SE3_GROUP, + inner_product_mat_at_identity=inner_product, + left_or_right='left') + fd_res = metric.dist(se3pars[:-1], se3pars[1:]) + + else: + raise ValueError( + 'The parameter \'metric\' should be \'L1\' or \'riemannian\'.') self._results = { 'out_file': op.abspath(self.inputs.out_file), From 8e7f2eebf5cb4733f8ebbee2c5d7cd5165f0d830 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Tue, 14 Aug 2018 08:58:16 -0700 Subject: [PATCH 07/16] Address Chris CR --- nipype/algorithms/confounds.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index 0d27dda862..30b0aa44b2 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -247,6 +247,15 @@ class FramewiseDisplacementInputSpec(BaseInterfaceInputSpec): "NIPY", desc="Source of movement parameters", mandatory=True) + metric = traits.Enum( + 'L1', + 'riemannian', + usedefault=True, + mandatory=True, + desc='Distance metric to apply: ' + 'L1 = Manhattan distance (original definition),' + 'riemannian = Canonical Riemannian distance on the' + 'Special Euclidean group in 3D (geodesic)') radius = traits.Float( 50, usedefault=True, @@ -319,12 +328,12 @@ def _run_interface(self, runtime, metric='L1'): arr=mpars, source=self.inputs.parameter_source) - if metric == 'L1': + if self.inputs.metric == 'L1': diff = mpars[:-1, :6] - mpars[1:, :6] diff[:, 3:6] *= self.inputs.radius fd_res = np.abs(diff).sum(axis=1) - elif metric == 'riemannian': + elif self.inputs.metric == 'riemannian': # TODO(nina): convert to geomstats parameterization: se3pars = np.hstack( [mpars[:, DIM_TRANSLATIONS:], mpars[:, :DIM_TRANSLATIONS]]) From 308e622907e44de3391eac2b637cad875506b509 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Tue, 14 Aug 2018 10:29:08 -0700 Subject: [PATCH 08/16] Reverse requirements --- requirements.txt | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8703267064..1571c22cb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,20 @@ -click>=6.6.0 -configparser -funcsigs -future>=0.16.0 -geomstats>=1.7 -mock -networkx>=1.9 -nibabel>=2.1.0 numpy>=1.9.0 +scipy>=0.14 +networkx>=1.9 +traits>=4.6 python-dateutil>=2.2 -packaging +nibabel>=2.1.0 +future>=0.16.0 +simplejson>=3.8.0 prov==1.5.0 -pydotplus -pydot>=1.2.3 +click>=6.6.0 +funcsigs +configparser pytest>=3.0 pytest-xdist pytest-env -scipy>=0.14 -simplejson>=3.8.0 -traits>=4.6 +mock +pydotplus +pydot>=1.2.3 +packaging +geomstats>=1.10 From 1e7e5505f0836f39cae368c21eadae5408395efa Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Tue, 14 Aug 2018 10:52:42 -0700 Subject: [PATCH 09/16] Add conversion function from Euler angles --- nipype/algorithms/confounds.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index d43608839c..3fda13fd04 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -30,6 +30,7 @@ IFLOGGER = logging.getLogger('nipype.interface') SE3_GROUP = SpecialEuclideanGroup(n=3) +SO3_GROUP = SE3_GROUP.rotations DIM_TRANSLATIONS = SE3_GROUP.translations.dimension DIM_ROTATIONS = SE3_GROUP.rotations.dimension @@ -258,7 +259,7 @@ class FramewiseDisplacementInputSpec(BaseInterfaceInputSpec): mandatory=True, desc='Distance metric to apply: ' 'L1 = Manhattan distance (original definition),' - 'riemannian = Canonical Riemannian distance on the' + 'riemannian = Riemannian distance on the' 'Special Euclidean group in 3D (geodesic)') radius = traits.Float( 50, @@ -338,9 +339,16 @@ def _run_interface(self, runtime, metric='L1'): fd_res = np.abs(diff).sum(axis=1) elif self.inputs.metric == 'riemannian': - # TODO(nina): convert to geomstats parameterization: + # TODO(nina): Check SPM convention for Euler angles + # (Tait-Bryan angles) + so3pars = mpars[:, DIM_TRANSLATIONS:] + so3pars = SO3_GROUP.rotation_vector_from_tait_bryan_angles( + so3pars, + extrinsic_or_intrinsic='intrinsic', + order='xyz') + se3pars = np.hstack( - [mpars[:, DIM_TRANSLATIONS:], mpars[:, :DIM_TRANSLATIONS]]) + [so3pars, mpars[:, :DIM_TRANSLATIONS]]) diag_rotations = self.inputs.radius * np.ones(DIM_ROTATIONS) diag_translations = np.ones(DIM_TRANSLATIONS) From 741f9896e10f1d3b30e9140b571fed992d970c38 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Tue, 14 Aug 2018 11:07:45 -0700 Subject: [PATCH 10/16] Put only use_default=True and add corresponding line in test auto --- nipype/algorithms/confounds.py | 1 - nipype/algorithms/tests/test_auto_FramewiseDisplacement.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index 3fda13fd04..9d9e7c88c5 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -256,7 +256,6 @@ class FramewiseDisplacementInputSpec(BaseInterfaceInputSpec): 'L1', 'riemannian', usedefault=True, - mandatory=True, desc='Distance metric to apply: ' 'L1 = Manhattan distance (original definition),' 'riemannian = Riemannian distance on the' diff --git a/nipype/algorithms/tests/test_auto_FramewiseDisplacement.py b/nipype/algorithms/tests/test_auto_FramewiseDisplacement.py index 685dec61e8..378c8cd969 100644 --- a/nipype/algorithms/tests/test_auto_FramewiseDisplacement.py +++ b/nipype/algorithms/tests/test_auto_FramewiseDisplacement.py @@ -12,6 +12,7 @@ def test_FramewiseDisplacement_inputs(): out_figure=dict(usedefault=True, ), out_file=dict(usedefault=True, ), parameter_source=dict(mandatory=True, ), + metric=dict(usedefault=True, ), radius=dict(usedefault=True, ), save_plot=dict(usedefault=True, ), series_tr=dict(), @@ -21,6 +22,8 @@ def test_FramewiseDisplacement_inputs(): for key, metadata in list(input_map.items()): for metakey, value in list(metadata.items()): assert getattr(inputs.traits()[key], metakey) == value + + def test_FramewiseDisplacement_outputs(): output_map = dict( fd_average=dict(), From 8d1b6f6637c1aa8a6163fdaaa0fb81d167d07286 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Tue, 14 Aug 2018 11:13:01 -0700 Subject: [PATCH 11/16] Add unit test for FrameDisplacement with Riemannian metric --- nipype/algorithms/tests/test_confounds.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nipype/algorithms/tests/test_confounds.py b/nipype/algorithms/tests/test_confounds.py index 2c601374ab..a2807ef7e5 100644 --- a/nipype/algorithms/tests/test_confounds.py +++ b/nipype/algorithms/tests/test_confounds.py @@ -21,6 +21,7 @@ def test_fd(tmpdir): tempdir = tmpdir.strpath ground_truth = np.loadtxt(example_data('fsl_motion_outliers_fd.txt')) + fdisplacement = FramewiseDisplacement( in_file=example_data('fsl_mcflirt_movpar.txt'), out_file=tempdir + '/fd.txt', @@ -36,6 +37,21 @@ def test_fd(tmpdir): ground_truth, np.loadtxt(res.outputs.out_file, skiprows=1), atol=.16) assert np.abs(ground_truth.mean() - res.outputs.fd_average) < 1e-2 + fdisplacement = FramewiseDisplacement( + in_file=example_data('fsl_mcflirt_movpar.txt'), + out_file=tempdir + '/fd.txt', + parameter_source="FSL", + metric='riemannian') + res = fdisplacement.run() + + with open(res.outputs.out_file) as all_lines: + for line in all_lines: + assert 'FramewiseDisplacement' in line + break + + assert np.allclose( + ground_truth, np.loadtxt(res.outputs.out_file, skiprows=1), atol=.16) + assert np.abs(ground_truth.mean() - res.outputs.fd_average) < 1e-2 @pytest.mark.skipif(nonitime, reason="nitime is not installed") def test_dvars(tmpdir): From 8fb79c447009441e5ef41cd801b3434d989c7e39 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Tue, 14 Aug 2018 17:14:20 -0700 Subject: [PATCH 12/16] Add geomstats in Dockerfile dependencies --- docker/generate_dockerfiles.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/generate_dockerfiles.sh b/docker/generate_dockerfiles.sh index bd1fcb637e..99613596cd 100755 --- a/docker/generate_dockerfiles.sh +++ b/docker/generate_dockerfiles.sh @@ -107,7 +107,7 @@ function generate_main_dockerfile() { pip_opts="-e" \ pip_install="/src/nipype[all]" \ --miniconda env_name=neuro \ - pip_install="grabbit==0.1.2" \ + pip_install="geomstats grabbit==0.1.2" \ --run-bash "mkdir -p /src/pybids && curl -sSL --retry 5 https://github.com/INCF/pybids/tarball/0.5.1 | tar -xz -C /src/pybids --strip-components 1 From 58562880275867c34c4f9a9d7e2ff1305de86b32 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Wed, 15 Aug 2018 14:51:05 -0700 Subject: [PATCH 13/16] Address Chris CR --- docker/generate_dockerfiles.sh | 2 +- nipype/algorithms/confounds.py | 34 ++++++++--------------- nipype/algorithms/tests/test_confounds.py | 15 ++++++++++ nipype/info.py | 3 +- requirements.txt | 1 - 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/docker/generate_dockerfiles.sh b/docker/generate_dockerfiles.sh index 99613596cd..bd1fcb637e 100755 --- a/docker/generate_dockerfiles.sh +++ b/docker/generate_dockerfiles.sh @@ -107,7 +107,7 @@ function generate_main_dockerfile() { pip_opts="-e" \ pip_install="/src/nipype[all]" \ --miniconda env_name=neuro \ - pip_install="geomstats grabbit==0.1.2" \ + pip_install="grabbit==0.1.2" \ --run-bash "mkdir -p /src/pybids && curl -sSL --retry 5 https://github.com/INCF/pybids/tarball/0.5.1 | tar -xz -C /src/pybids --strip-components 1 diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index 9d9e7c88c5..cda9c0a594 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -14,8 +14,6 @@ import nibabel as nb import numpy as np -from geomstats.invariant_metric import InvariantMetric -from geomstats.special_euclidean_group import SpecialEuclideanGroup from numpy.polynomial import Legendre from scipy import linalg @@ -29,19 +27,6 @@ IFLOGGER = logging.getLogger('nipype.interface') -SE3_GROUP = SpecialEuclideanGroup(n=3) -SO3_GROUP = SE3_GROUP.rotations -DIM_TRANSLATIONS = SE3_GROUP.translations.dimension -DIM_ROTATIONS = SE3_GROUP.rotations.dimension - -SE3_GROUP = SpecialEuclideanGroup(n=3) -DIM_TRANSLATIONS = SE3_GROUP.translations.dimension -DIM_ROTATIONS = SE3_GROUP.rotations.dimension - -SE3_GROUP = SpecialEuclideanGroup(n=3) -DIM_TRANSLATIONS = SE3_GROUP.translations.dimension -DIM_ROTATIONS = SE3_GROUP.rotations.dimension - class ComputeDVARSInputSpec(BaseInterfaceInputSpec): in_file = File( @@ -256,9 +241,10 @@ class FramewiseDisplacementInputSpec(BaseInterfaceInputSpec): 'L1', 'riemannian', usedefault=True, + mandatory=True, desc='Distance metric to apply: ' - 'L1 = Manhattan distance (original definition),' - 'riemannian = Riemannian distance on the' + 'L1 = Manhattan distance (original definition); ' + 'riemannian = Riemannian distance on the ' 'Special Euclidean group in 3D (geodesic)') radius = traits.Float( 50, @@ -324,7 +310,7 @@ class FramewiseDisplacement(BaseInterface): 'tags': ['method'] }] - def _run_interface(self, runtime, metric='L1'): + def _run_interface(self, runtime): mpars = np.loadtxt(self.inputs.in_file) # mpars is N_t x 6 mpars = np.apply_along_axis( func1d=normalize_mc_params, @@ -338,6 +324,14 @@ def _run_interface(self, runtime, metric='L1'): fd_res = np.abs(diff).sum(axis=1) elif self.inputs.metric == 'riemannian': + from geomstats.invariant_metric import InvariantMetric + from geomstats.special_euclidean_group import SpecialEuclideanGroup + + SE3_GROUP = SpecialEuclideanGroup(n=3) + SO3_GROUP = SE3_GROUP.rotations + DIM_TRANSLATIONS = SE3_GROUP.translations.dimension + DIM_ROTATIONS = SE3_GROUP.rotations.dimension + # TODO(nina): Check SPM convention for Euler angles # (Tait-Bryan angles) so3pars = mpars[:, DIM_TRANSLATIONS:] @@ -359,10 +353,6 @@ def _run_interface(self, runtime, metric='L1'): left_or_right='left') fd_res = metric.dist(se3pars[:-1], se3pars[1:]) - else: - raise ValueError( - 'The parameter \'metric\' should be \'L1\' or \'riemannian\'.') - self._results = { 'out_file': op.abspath(self.inputs.out_file), 'fd_average': float(fd_res.mean()) diff --git a/nipype/algorithms/tests/test_confounds.py b/nipype/algorithms/tests/test_confounds.py index a2807ef7e5..4e3e93f715 100644 --- a/nipype/algorithms/tests/test_confounds.py +++ b/nipype/algorithms/tests/test_confounds.py @@ -17,6 +17,13 @@ except ImportError: pass +nogeomstats = True +try: + import geomstats + nogeomstats = False +except ImportError: + pass + def test_fd(tmpdir): tempdir = tmpdir.strpath @@ -37,6 +44,13 @@ def test_fd(tmpdir): ground_truth, np.loadtxt(res.outputs.out_file, skiprows=1), atol=.16) assert np.abs(ground_truth.mean() - res.outputs.fd_average) < 1e-2 + +@pytest.mark.skipif(nonitime, reason="nitime is not installed") +def test_fd_riemannian(tmpdir): + tempdir = tmpdir.strpath + # TODO(nina): Adapt ground_truth w. SPM Euler angles convention + ground_truth = np.loadtxt(example_data('fsl_motion_outliers_fd.txt')) + fdisplacement = FramewiseDisplacement( in_file=example_data('fsl_mcflirt_movpar.txt'), out_file=tempdir + '/fd.txt', @@ -53,6 +67,7 @@ def test_fd(tmpdir): ground_truth, np.loadtxt(res.outputs.out_file, skiprows=1), atol=.16) assert np.abs(ground_truth.mean() - res.outputs.fd_average) < 1e-2 + @pytest.mark.skipif(nonitime, reason="nitime is not installed") def test_dvars(tmpdir): ground_truth = np.loadtxt(example_data('ds003_sub-01_mc.DVARS')) diff --git a/nipype/info.py b/nipype/info.py index 219fa90a05..0419ef15e3 100644 --- a/nipype/info.py +++ b/nipype/info.py @@ -160,7 +160,8 @@ def get_nipype_gitversion(): 'doc': ['Sphinx>=1.4', 'numpydoc', 'matplotlib', 'pydotplus', 'pydot>=1.2.3'], 'tests': TESTS_REQUIRES, 'specs': ['yapf'], - 'nipy': ['nitime', 'nilearn', 'dipy', 'nipy', 'matplotlib'], + 'nipy': ['nitime', 'nilearn', 'dipy', 'nipy', 'matplotlib', + 'geomstats>=1.1.0; python_version >= "3.5"'], 'profiler': ['psutil>=5.0'], 'duecredit': ['duecredit'], 'xvfbwrapper': ['xvfbwrapper'], diff --git a/requirements.txt b/requirements.txt index 1571c22cb2..5ef00ec98b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,4 +17,3 @@ mock pydotplus pydot>=1.2.3 packaging -geomstats>=1.10 From 0565df4e4d75fa9d5ab44afb5dd28c71b3378ed2 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Tue, 28 Aug 2018 14:06:38 -0700 Subject: [PATCH 14/16] Adapt convention for Euler angles from spm_matrix.m --- nipype/algorithms/confounds.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index cda9c0a594..617eaedfc7 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -332,13 +332,11 @@ def _run_interface(self, runtime): DIM_TRANSLATIONS = SE3_GROUP.translations.dimension DIM_ROTATIONS = SE3_GROUP.rotations.dimension - # TODO(nina): Check SPM convention for Euler angles - # (Tait-Bryan angles) so3pars = mpars[:, DIM_TRANSLATIONS:] so3pars = SO3_GROUP.rotation_vector_from_tait_bryan_angles( so3pars, - extrinsic_or_intrinsic='intrinsic', - order='xyz') + extrinsic_or_intrinsic='extrinsic', + order='zyx') se3pars = np.hstack( [so3pars, mpars[:, :DIM_TRANSLATIONS]]) From 6089d27f9aa8c6df272755b5c9dfa05cb7ef3124 Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Tue, 28 Aug 2018 14:09:49 -0700 Subject: [PATCH 15/16] Address Chris CR --- nipype/algorithms/confounds.py | 1 - nipype/algorithms/tests/test_confounds.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/nipype/algorithms/confounds.py b/nipype/algorithms/confounds.py index 617eaedfc7..483840046b 100644 --- a/nipype/algorithms/confounds.py +++ b/nipype/algorithms/confounds.py @@ -13,7 +13,6 @@ import nibabel as nb import numpy as np - from numpy.polynomial import Legendre from scipy import linalg diff --git a/nipype/algorithms/tests/test_confounds.py b/nipype/algorithms/tests/test_confounds.py index 4e3e93f715..0d072e7d98 100644 --- a/nipype/algorithms/tests/test_confounds.py +++ b/nipype/algorithms/tests/test_confounds.py @@ -45,7 +45,7 @@ def test_fd(tmpdir): assert np.abs(ground_truth.mean() - res.outputs.fd_average) < 1e-2 -@pytest.mark.skipif(nonitime, reason="nitime is not installed") +@pytest.mark.skipif(nogeomstats, reason="geomstats is not installed") def test_fd_riemannian(tmpdir): tempdir = tmpdir.strpath # TODO(nina): Adapt ground_truth w. SPM Euler angles convention From c82b5fbcdbbdbc053f07212dc2a9a24895f28dce Mon Sep 17 00:00:00 2001 From: Nina Miolane Date: Tue, 28 Aug 2018 16:05:00 -0700 Subject: [PATCH 16/16] Run specs for test_auto_FramewiseDisplacement.py --- nipype/algorithms/tests/test_auto_FramewiseDisplacement.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nipype/algorithms/tests/test_auto_FramewiseDisplacement.py b/nipype/algorithms/tests/test_auto_FramewiseDisplacement.py index 378c8cd969..29eeec1f37 100644 --- a/nipype/algorithms/tests/test_auto_FramewiseDisplacement.py +++ b/nipype/algorithms/tests/test_auto_FramewiseDisplacement.py @@ -8,11 +8,14 @@ def test_FramewiseDisplacement_inputs(): figdpi=dict(usedefault=True, ), figsize=dict(usedefault=True, ), in_file=dict(mandatory=True, ), + metric=dict( + mandatory=True, + usedefault=True, + ), normalize=dict(usedefault=True, ), out_figure=dict(usedefault=True, ), out_file=dict(usedefault=True, ), parameter_source=dict(mandatory=True, ), - metric=dict(usedefault=True, ), radius=dict(usedefault=True, ), save_plot=dict(usedefault=True, ), series_tr=dict(), @@ -22,8 +25,6 @@ def test_FramewiseDisplacement_inputs(): for key, metadata in list(input_map.items()): for metakey, value in list(metadata.items()): assert getattr(inputs.traits()[key], metakey) == value - - def test_FramewiseDisplacement_outputs(): output_map = dict( fd_average=dict(),