From 34f04c6709c550493e01726f7b119d1c249d7023 Mon Sep 17 00:00:00 2001 From: EmmaRenauld Date: Thu, 15 Feb 2024 13:08:55 -0500 Subject: [PATCH 1/5] Add gradient_utils tests --- scilpy/gradients/gen_gradient_sampling.py | 4 +- .../gradients/tests/test_gradients_utils.py | 40 +++++++++++++++++-- scilpy/gradients/utils.py | 39 +++++++++++------- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/scilpy/gradients/gen_gradient_sampling.py b/scilpy/gradients/gen_gradient_sampling.py index 95965d9d9..70e93287a 100644 --- a/scilpy/gradients/gen_gradient_sampling.py +++ b/scilpy/gradients/gen_gradient_sampling.py @@ -13,7 +13,7 @@ import numpy as np from scipy import optimize -from scilpy.gradients.utils import random_uniform_on_sphere +from scilpy.gradients.utils import random_uniform_on_half_sphere def generate_gradient_sampling(nb_samples_per_shell, verbose=1): @@ -125,7 +125,7 @@ def _generate_gradient_sampling_with_weights( nb_point_total = np.sum(nb_points_per_shell) # Initialized with random directions - bvecs = random_uniform_on_sphere(nb_point_total) + bvecs = random_uniform_on_half_sphere(nb_point_total) bvecs = bvecs.reshape(nb_point_total * 3) bvecs = optimize.fmin_slsqp(_multiple_shell_energy, bvecs, diff --git a/scilpy/gradients/tests/test_gradients_utils.py b/scilpy/gradients/tests/test_gradients_utils.py index 560dc73b5..62e6543ef 100644 --- a/scilpy/gradients/tests/test_gradients_utils.py +++ b/scilpy/gradients/tests/test_gradients_utils.py @@ -1,10 +1,44 @@ # -*- coding: utf-8 -*- +import numpy as np + +from scilpy.gradients.bvec_bval_tools import is_normalized_bvecs +from scilpy.gradients.utils import random_uniform_on_half_sphere, \ + get_new_order_philips def test_random_uniform_on_sphere(): - pass + bvecs = random_uniform_on_half_sphere(10) + + # Confirm that they are unit vectors. + assert is_normalized_bvecs(bvecs) + + # Can't check much more than that. Supervising that no co-linear vectors. + # (Each pair of vector should have at least some angle in-between) + # We also tried to check if the closest neighbor to each vector is more or + # less always with the same angle, but it is very variable. + min_expected_angle = 1.0 + smallests = [] + for i in range(10): + angles = np.rad2deg(np.arccos(np.dot(bvecs[i, :], bvecs.T))) + # Smallest, except 0 (with itself). Sometimes this is nan. + smallests.append(np.nanmin(angles[angles > 1e-5])) + assert np.all(np.asarray(smallests) > min_expected_angle) def test_get_new_order_philips(): - # Needs dwi files - philips version - pass + # Using N=4 vectors + philips_table = np.asarray([[1, 1, 1, 1], + [2, 2, 2, 2], + [3, 3, 3, 3], + [4, 4, 4, 4]]) + dwi = np.ones((10, 10, 10, 4)) + bvecs = np.asarray([[3, 3, 3], + [4, 4, 4], + [2, 2, 2], + [1, 1, 1]]) + bvals = np.asarray([3, 4, 2, 1]) + + order = get_new_order_philips(philips_table, dwi, bvals, bvecs) + + assert np.array_equal(bvecs[order, :], philips_table[:, 0:3]) + assert np.array_equal(bvals[order], philips_table[:, 3]) diff --git a/scilpy/gradients/utils.py b/scilpy/gradients/utils.py index 66923434c..29118b0dc 100644 --- a/scilpy/gradients/utils.py +++ b/scilpy/gradients/utils.py @@ -3,10 +3,14 @@ import numpy as np -def random_uniform_on_sphere(nb_vectors): +def random_uniform_on_half_sphere(nb_vectors): """ Creates a set of K pseudo-random unit vectors, following a uniform - distribution on the sphere. + distribution on the half sphere. Reference: Emmanuel Caruyer's code + (https://github.com/ecaruyer). + + This is not intended to create a perfect result. It's usually the + initialization step of a repulsion strategy. Parameters ---------- @@ -23,6 +27,10 @@ def random_uniform_on_sphere(nb_vectors): r = 2 * np.sqrt(np.random.rand(nb_vectors)) theta = 2 * np.arcsin(r / 2) + # See here: https://www.bogotobogo.com/Algorithms/uniform_distribution_sphere.php + # They seem to be using something like this instead: + # theta = np.arccos(2 * np.random.rand(nb_vectors) - 1.0) + bvecs = np.zeros((nb_vectors, 3)) bvecs[:, 0] = np.sin(theta) * np.cos(phi) bvecs[:, 1] = np.sin(theta) * np.sin(phi) @@ -33,14 +41,16 @@ def random_uniform_on_sphere(nb_vectors): def get_new_order_philips(philips_table, dwi, bvals, bvecs): """ - Reorder bval and bvec files based on the philips table. + Find the sorting order that could be applied to the bval and bvec files to + obtain the same order as in the philips table. Parameters ---------- philips_table: nd.array - Philips gradient table - dwis: nibabel - dwis + Philips gradient table, of shape (N, 4), coming from a Philips machine + with SoftwareVersions < 5.6. + dwi: nibabel image + dwi of shape (x, y, z, N). Only used to confirm the dwi's shape. bvals : array, (N,) bvals bvecs : array, (N, 3) @@ -52,9 +62,9 @@ def get_new_order_philips(philips_table, dwi, bvals, bvecs): New index to reorder bvals/bvec """ # Check number of gradients, bvecs, bvals, dwi and oTable - if len(bvecs) != dwi.shape[3] or len(bvals) != len(philips_table): - raise ValueError('bvec/bval/dwi and original table \ - does not contain the same number of gradients') + if not (len(bvecs) == dwi.shape[3] == len(bvals) == len(philips_table)): + raise ValueError('bvec/bval/dwi and original table do not contain ' + 'the same number of gradients.') # Check bvals philips_bval = np.unique(philips_table[:, 3]) @@ -65,10 +75,10 @@ def get_new_order_philips(philips_table, dwi, bvals, bvecs): dwi_shells = np.unique(bvals[bvals > 1]) b0s = np.unique(bvals[bvals < 1]) - if len(philips_dwi_shells) != len(dwi_shells) or\ + if len(philips_dwi_shells) != len(dwi_shells) or \ len(philips_b0s) != len(b0s): - raise ValueError('bvec/bval/dwi and original table\ - does not contain the same shells') + raise ValueError('bvec/bval/dwi and original table do not contain ' + 'the same shells.') new_index = np.zeros(bvals.shape) @@ -77,8 +87,9 @@ def get_new_order_philips(philips_table, dwi, bvals, bvecs): curr_bval_table = np.where(philips_table[:, 3] == nbval)[0] if len(curr_bval) != len(curr_bval_table): - raise ValueError('bval/bvec and orginal table does not contain \ - the same number of gradients for shell {0}'.format(curr_bval)) + raise ValueError('bval/bvec and orginal table do not contain ' + 'the same number of gradients for shell {}.' + .format(curr_bval)) new_index[curr_bval_table] = curr_bval From fc714bcb8a535f44984c12e5556f329de885e521 Mon Sep 17 00:00:00 2001 From: EmmaRenauld Date: Thu, 15 Feb 2024 13:14:45 -0500 Subject: [PATCH 2/5] Fix pep8 --- scilpy/gradients/tests/test_gradients_utils.py | 6 +++--- scilpy/gradients/utils.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scilpy/gradients/tests/test_gradients_utils.py b/scilpy/gradients/tests/test_gradients_utils.py index 62e6543ef..2cea2c275 100644 --- a/scilpy/gradients/tests/test_gradients_utils.py +++ b/scilpy/gradients/tests/test_gradients_utils.py @@ -28,9 +28,9 @@ def test_random_uniform_on_sphere(): def test_get_new_order_philips(): # Using N=4 vectors philips_table = np.asarray([[1, 1, 1, 1], - [2, 2, 2, 2], - [3, 3, 3, 3], - [4, 4, 4, 4]]) + [2, 2, 2, 2], + [3, 3, 3, 3], + [4, 4, 4, 4]]) dwi = np.ones((10, 10, 10, 4)) bvecs = np.asarray([[3, 3, 3], [4, 4, 4], diff --git a/scilpy/gradients/utils.py b/scilpy/gradients/utils.py index 29118b0dc..aaa883e6c 100644 --- a/scilpy/gradients/utils.py +++ b/scilpy/gradients/utils.py @@ -27,7 +27,8 @@ def random_uniform_on_half_sphere(nb_vectors): r = 2 * np.sqrt(np.random.rand(nb_vectors)) theta = 2 * np.arcsin(r / 2) - # See here: https://www.bogotobogo.com/Algorithms/uniform_distribution_sphere.php + # See here: + # https://www.bogotobogo.com/Algorithms/uniform_distribution_sphere.php # They seem to be using something like this instead: # theta = np.arccos(2 * np.random.rand(nb_vectors) - 1.0) From 0572fea2e2b3ccafa983e7f09bee5d960b149ed3 Mon Sep 17 00:00:00 2001 From: EmmaRenauld Date: Mon, 19 Feb 2024 13:07:22 -0500 Subject: [PATCH 3/5] Whole sphere confirmed. Removed half-sphere explanation --- scilpy/gradients/gen_gradient_sampling.py | 4 ++-- scilpy/gradients/tests/test_gradients_utils.py | 4 ++-- scilpy/gradients/utils.py | 10 ++++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/scilpy/gradients/gen_gradient_sampling.py b/scilpy/gradients/gen_gradient_sampling.py index 70e93287a..95965d9d9 100644 --- a/scilpy/gradients/gen_gradient_sampling.py +++ b/scilpy/gradients/gen_gradient_sampling.py @@ -13,7 +13,7 @@ import numpy as np from scipy import optimize -from scilpy.gradients.utils import random_uniform_on_half_sphere +from scilpy.gradients.utils import random_uniform_on_sphere def generate_gradient_sampling(nb_samples_per_shell, verbose=1): @@ -125,7 +125,7 @@ def _generate_gradient_sampling_with_weights( nb_point_total = np.sum(nb_points_per_shell) # Initialized with random directions - bvecs = random_uniform_on_half_sphere(nb_point_total) + bvecs = random_uniform_on_sphere(nb_point_total) bvecs = bvecs.reshape(nb_point_total * 3) bvecs = optimize.fmin_slsqp(_multiple_shell_energy, bvecs, diff --git a/scilpy/gradients/tests/test_gradients_utils.py b/scilpy/gradients/tests/test_gradients_utils.py index 2cea2c275..39c714717 100644 --- a/scilpy/gradients/tests/test_gradients_utils.py +++ b/scilpy/gradients/tests/test_gradients_utils.py @@ -2,12 +2,12 @@ import numpy as np from scilpy.gradients.bvec_bval_tools import is_normalized_bvecs -from scilpy.gradients.utils import random_uniform_on_half_sphere, \ +from scilpy.gradients.utils import random_uniform_on_sphere, \ get_new_order_philips def test_random_uniform_on_sphere(): - bvecs = random_uniform_on_half_sphere(10) + bvecs = random_uniform_on_sphere(10) # Confirm that they are unit vectors. assert is_normalized_bvecs(bvecs) diff --git a/scilpy/gradients/utils.py b/scilpy/gradients/utils.py index aaa883e6c..f59b5a6a7 100644 --- a/scilpy/gradients/utils.py +++ b/scilpy/gradients/utils.py @@ -3,10 +3,10 @@ import numpy as np -def random_uniform_on_half_sphere(nb_vectors): +def random_uniform_on_sphere(nb_vectors): """ Creates a set of K pseudo-random unit vectors, following a uniform - distribution on the half sphere. Reference: Emmanuel Caruyer's code + distribution on the sphere. Reference: Emmanuel Caruyer's code (https://github.com/ecaruyer). This is not intended to create a perfect result. It's usually the @@ -19,9 +19,11 @@ def random_uniform_on_half_sphere(nb_vectors): Returns ------- - bvecs: nd.array - pseudo-random unit vector + bvecs: nd.array of shape (nb_vectors, 3) + Pseudo-random unit vectors """ + # Note. Caruyer's docstring says it's a uniform on the half-sphere, but + # plotted a few results: it is one the whole sphere. phi = 2 * np.pi * np.random.rand(nb_vectors) r = 2 * np.sqrt(np.random.rand(nb_vectors)) From 333ec84e23f7e171b3b5ec619e2999cf5a58e4f6 Mon Sep 17 00:00:00 2001 From: EmmaRenauld Date: Mon, 19 Feb 2024 13:12:58 -0500 Subject: [PATCH 4/5] Rename reorder_philips to reorder_table --- .../gradients/tests/test_gradients_utils.py | 6 +-- scilpy/gradients/utils.py | 42 ++++++++++--------- scripts/scil_dwi_reorder_philips.py | 4 +- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/scilpy/gradients/tests/test_gradients_utils.py b/scilpy/gradients/tests/test_gradients_utils.py index 39c714717..01bffbad4 100644 --- a/scilpy/gradients/tests/test_gradients_utils.py +++ b/scilpy/gradients/tests/test_gradients_utils.py @@ -3,7 +3,7 @@ from scilpy.gradients.bvec_bval_tools import is_normalized_bvecs from scilpy.gradients.utils import random_uniform_on_sphere, \ - get_new_order_philips + get_new_order_table def test_random_uniform_on_sphere(): @@ -25,7 +25,7 @@ def test_random_uniform_on_sphere(): assert np.all(np.asarray(smallests) > min_expected_angle) -def test_get_new_order_philips(): +def test_get_new_order_table(): # Using N=4 vectors philips_table = np.asarray([[1, 1, 1, 1], [2, 2, 2, 2], @@ -38,7 +38,7 @@ def test_get_new_order_philips(): [1, 1, 1]]) bvals = np.asarray([3, 4, 2, 1]) - order = get_new_order_philips(philips_table, dwi, bvals, bvecs) + order = get_new_order_table(philips_table, dwi, bvals, bvecs) assert np.array_equal(bvecs[order, :], philips_table[:, 0:3]) assert np.array_equal(bvals[order], philips_table[:, 3]) diff --git a/scilpy/gradients/utils.py b/scilpy/gradients/utils.py index f59b5a6a7..cb5576aad 100644 --- a/scilpy/gradients/utils.py +++ b/scilpy/gradients/utils.py @@ -42,52 +42,54 @@ def random_uniform_on_sphere(nb_vectors): return bvecs -def get_new_order_philips(philips_table, dwi, bvals, bvecs): +def get_new_order_table(ref_gradients_table, dwi, bvals, bvecs): """ Find the sorting order that could be applied to the bval and bvec files to - obtain the same order as in the philips table. + obtain the same order as in the reference gradient table. + + This is mostly useful to reorder bval and bvec files in the order they were + acquired by the Philips scanner (before version 5.6). Parameters ---------- - philips_table: nd.array - Philips gradient table, of shape (N, 4), coming from a Philips machine - with SoftwareVersions < 5.6. + ref_gradients_table: nd.array + Gradient table, of shape (N, 4). It will use as reference for the + ordering of b-vectors. + Ex: Could be the result of scil_gradients_generate_sampling.py dwi: nibabel image dwi of shape (x, y, z, N). Only used to confirm the dwi's shape. bvals : array, (N,) - bvals + bvals that need to be reordered. bvecs : array, (N, 3) - bvecs + bvecs that need to be reorered. Returns ------- new_index: nd.array New index to reorder bvals/bvec """ - # Check number of gradients, bvecs, bvals, dwi and oTable - if not (len(bvecs) == dwi.shape[3] == len(bvals) == len(philips_table)): - raise ValueError('bvec/bval/dwi and original table do not contain ' + if not (len(bvecs) == dwi.shape[3] == len(bvals) == + len(ref_gradients_table)): + raise ValueError('bvec/bval/dwi and reference table do not contain ' 'the same number of gradients.') - # Check bvals - philips_bval = np.unique(philips_table[:, 3]) - - philips_dwi_shells = philips_bval[philips_bval > 1] - philips_b0s = philips_bval[philips_bval < 1] + ref_bval = np.unique(ref_gradients_table[:, 3]) + ref_dwi_shells = ref_bval[ref_bval > 1] + ref_b0s = ref_bval[ref_bval < 1] dwi_shells = np.unique(bvals[bvals > 1]) b0s = np.unique(bvals[bvals < 1]) - if len(philips_dwi_shells) != len(dwi_shells) or \ - len(philips_b0s) != len(b0s): - raise ValueError('bvec/bval/dwi and original table do not contain ' + if len(ref_dwi_shells) != len(dwi_shells) or \ + len(ref_b0s) != len(b0s): + raise ValueError('bvec/bval/dwi and reference table do not contain ' 'the same shells.') new_index = np.zeros(bvals.shape) - for nbval in philips_bval: + for nbval in ref_bval: curr_bval = np.where(bvals == nbval)[0] - curr_bval_table = np.where(philips_table[:, 3] == nbval)[0] + curr_bval_table = np.where(ref_gradients_table[:, 3] == nbval)[0] if len(curr_bval) != len(curr_bval_table): raise ValueError('bval/bvec and orginal table do not contain ' diff --git a/scripts/scil_dwi_reorder_philips.py b/scripts/scil_dwi_reorder_philips.py index 5ac74d715..8c85a9090 100755 --- a/scripts/scil_dwi_reorder_philips.py +++ b/scripts/scil_dwi_reorder_philips.py @@ -18,7 +18,7 @@ import nibabel as nib import numpy as np -from scilpy.gradients.utils import get_new_order_philips +from scilpy.gradients.utils import get_new_order_table from scilpy.io.utils import (add_overwrite_arg, add_verbose_arg, assert_inputs_exist, @@ -90,7 +90,7 @@ def main(): bvals, bvecs = read_bvals_bvecs(args.in_bval, args.in_bvec) dwi = nib.load(args.in_dwi) - new_index = get_new_order_philips(philips_table, dwi, bvals, bvecs) + new_index = get_new_order_table(philips_table, dwi, bvals, bvecs) bvecs = bvecs[new_index] bvals = bvals[new_index] From 2181190ad97cb47d2d12b3e483123abe40f34db4 Mon Sep 17 00:00:00 2001 From: EmmaRenauld Date: Tue, 20 Feb 2024 08:38:50 -0500 Subject: [PATCH 5/5] Answer Philip's comments --- scilpy/gradients/tests/test_gradients_utils.py | 6 +++--- scilpy/gradients/utils.py | 12 ++++++------ scripts/scil_dwi_reorder_philips.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/scilpy/gradients/tests/test_gradients_utils.py b/scilpy/gradients/tests/test_gradients_utils.py index 01bffbad4..567933d74 100644 --- a/scilpy/gradients/tests/test_gradients_utils.py +++ b/scilpy/gradients/tests/test_gradients_utils.py @@ -3,7 +3,7 @@ from scilpy.gradients.bvec_bval_tools import is_normalized_bvecs from scilpy.gradients.utils import random_uniform_on_sphere, \ - get_new_order_table + get_new_gtab_order def test_random_uniform_on_sphere(): @@ -25,7 +25,7 @@ def test_random_uniform_on_sphere(): assert np.all(np.asarray(smallests) > min_expected_angle) -def test_get_new_order_table(): +def test_get_new_gtab_order(): # Using N=4 vectors philips_table = np.asarray([[1, 1, 1, 1], [2, 2, 2, 2], @@ -38,7 +38,7 @@ def test_get_new_order_table(): [1, 1, 1]]) bvals = np.asarray([3, 4, 2, 1]) - order = get_new_order_table(philips_table, dwi, bvals, bvecs) + order = get_new_gtab_order(philips_table, dwi, bvals, bvecs) assert np.array_equal(bvecs[order, :], philips_table[:, 0:3]) assert np.array_equal(bvals[order], philips_table[:, 3]) diff --git a/scilpy/gradients/utils.py b/scilpy/gradients/utils.py index cb5576aad..f03f051c2 100644 --- a/scilpy/gradients/utils.py +++ b/scilpy/gradients/utils.py @@ -42,7 +42,7 @@ def random_uniform_on_sphere(nb_vectors): return bvecs -def get_new_order_table(ref_gradients_table, dwi, bvals, bvecs): +def get_new_gtab_order(ref_gradient_table, dwi, bvals, bvecs): """ Find the sorting order that could be applied to the bval and bvec files to obtain the same order as in the reference gradient table. @@ -52,7 +52,7 @@ def get_new_order_table(ref_gradients_table, dwi, bvals, bvecs): Parameters ---------- - ref_gradients_table: nd.array + ref_gradient_table: nd.array Gradient table, of shape (N, 4). It will use as reference for the ordering of b-vectors. Ex: Could be the result of scil_gradients_generate_sampling.py @@ -61,7 +61,7 @@ def get_new_order_table(ref_gradients_table, dwi, bvals, bvecs): bvals : array, (N,) bvals that need to be reordered. bvecs : array, (N, 3) - bvecs that need to be reorered. + bvecs that need to be reordered. Returns ------- @@ -69,11 +69,11 @@ def get_new_order_table(ref_gradients_table, dwi, bvals, bvecs): New index to reorder bvals/bvec """ if not (len(bvecs) == dwi.shape[3] == len(bvals) == - len(ref_gradients_table)): + len(ref_gradient_table)): raise ValueError('bvec/bval/dwi and reference table do not contain ' 'the same number of gradients.') - ref_bval = np.unique(ref_gradients_table[:, 3]) + ref_bval = np.unique(ref_gradient_table[:, 3]) ref_dwi_shells = ref_bval[ref_bval > 1] ref_b0s = ref_bval[ref_bval < 1] @@ -89,7 +89,7 @@ def get_new_order_table(ref_gradients_table, dwi, bvals, bvecs): for nbval in ref_bval: curr_bval = np.where(bvals == nbval)[0] - curr_bval_table = np.where(ref_gradients_table[:, 3] == nbval)[0] + curr_bval_table = np.where(ref_gradient_table[:, 3] == nbval)[0] if len(curr_bval) != len(curr_bval_table): raise ValueError('bval/bvec and orginal table do not contain ' diff --git a/scripts/scil_dwi_reorder_philips.py b/scripts/scil_dwi_reorder_philips.py index 8c85a9090..5d23f8dc4 100755 --- a/scripts/scil_dwi_reorder_philips.py +++ b/scripts/scil_dwi_reorder_philips.py @@ -18,7 +18,7 @@ import nibabel as nib import numpy as np -from scilpy.gradients.utils import get_new_order_table +from scilpy.gradients.utils import get_new_gtab_order from scilpy.io.utils import (add_overwrite_arg, add_verbose_arg, assert_inputs_exist, @@ -90,7 +90,7 @@ def main(): bvals, bvecs = read_bvals_bvecs(args.in_bval, args.in_bvec) dwi = nib.load(args.in_dwi) - new_index = get_new_order_table(philips_table, dwi, bvals, bvecs) + new_index = get_new_gtab_order(philips_table, dwi, bvals, bvecs) bvecs = bvecs[new_index] bvals = bvals[new_index]