Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MAINT] Refactor fixtures #3905

Merged
merged 16 commits into from
Aug 24, 2023
Merged
173 changes: 153 additions & 20 deletions nilearn/conftest.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Remi-Gau why is it that for mni affine the fixture returns the AFFINE_MNI array itself whereas with the eye affine the fixture returns a function that returns AFFINE_EYE array? Same question for shape_3d_default vs shape_4d_default

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General answer.

I went for

# global variable
X = "some value"

# function
def _x()
  return X

# fixture
@pytest.mark.fixture()
def x():
  return _x()

When some other fixtures needed to access the value of X and change it.

So to avoid having that fixture change the global (AKA "best way to make things break") I added a function as a go between.

I have not done that systematically because I have not had the need for it for all of those constants. But I think that it would make sense to have the same pattern for all, to avoid this confusion.

Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,103 @@ def close_all():
plt.close("all") # takes < 1 us so just always do it


MNI_AFFINE = np.array(
[
[2.0, 0.0, 0.0, -98.0],
[0.0, 2.0, 0.0, -134.0],
[0.0, 0.0, 2.0, -72.0],
[0.0, 0.0, 0.0, 1.0],
]
)
# ------------------------ AFFINES ------------------------#


def _mni_3d_img(affine=MNI_AFFINE):
def _affine_mni():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does anything prevent from always using this even inside conftest? What is the benefit of still having the global?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I was thinking that the global was progressively becoming useless.

will try to simplify.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually removed all constants.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But isn't that a breaking change ? I'd rather have deprecation cycles...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking through all the files changed, all those constants only appear in tests so I don't think deprecation is necessary there.

are we actually storing anything in conftest.py that is not just for testing? if so we should probably move it somewhere else

"""Return an affine corresponding to 2mm isotropic MNI template.

Mostly used for set up in other fixtures in other testing modules.
"""
return np.array(
[
[2.0, 0.0, 0.0, -98.0],
[0.0, 2.0, 0.0, -134.0],
[0.0, 0.0, 2.0, -72.0],
[0.0, 0.0, 0.0, 1.0],
]
)


@pytest.fixture()
def affine_mni():
"""Return an affine corresponding to 2mm isotropic MNI template."""
return _affine_mni()


def _affine_eye():
"""Return an identity matrix affine.

Mostly used for set up in other fixtures in other testing modules.
"""
return np.eye(4)


@pytest.fixture()
def affine_eye():
"""Return an identity matrix affine."""
return _affine_eye()


# ------------------------ SHAPES ------------------------#


def _shape_3d_default():
"""Return default shape for a 3D image.

Mostly used for set up in other fixtures in other testing modules.
"""
return (10, 10, 10)


def _shape_4d_default():
"""Return default shape for a 4D image.

Mostly used for set up in other fixtures in other testing modules.
"""
return (10, 10, 10, 10)


@pytest.fixture()
def shape_3d_default():
"""Return default shape for a 3D image."""
return _shape_3d_default()


@pytest.fixture()
def shape_4d_default():
"""Return default shape for a 4D image."""
return _shape_4d_default()


def _img_zeros(shape, affine):
return Nifti1Image(np.zeros(shape), affine)


def _img_ones(shape, affine):
return Nifti1Image(np.ones(shape), affine)


# ------------------------ 3D IMAGES ------------------------#


def _img_3d_rand(affine=_affine_eye()):
"""Return random 3D Nifti1Image in MNI space.

Mostly used for set up in other fixtures in other testing modules.
"""
rng = np.random.RandomState(42)
data = rng.rand(*_shape_3d_default())
return Nifti1Image(data, affine)


@pytest.fixture()
def img_3d_rand_eye():
"""Return random 3D Nifti1Image in MNI space."""
return _img_3d_rand()


def _img_3d_mni(affine=_affine_mni()):
data_positive = np.zeros((7, 7, 3))
rng = np.random.RandomState(42)
data_rng = rng.rand(7, 7, 3)
Expand All @@ -98,28 +184,75 @@ def _mni_3d_img(affine=MNI_AFFINE):


@pytest.fixture()
def mni_affine():
"""Return an affine corresponding to 2mm isotropic MNI template."""
return MNI_AFFINE
def img_3d_mni():
"""Return a default random 3D Nifti1Image in MNI space."""
return _img_3d_mni()


@pytest.fixture()
def mni_3d_img():
"""Fixture for a random 3D image in MNI space."""
return _mni_3d_img()
def _img_3d_zeros(shape=_shape_3d_default(), affine=_affine_eye()):
"""Return a default zeros filled 3D Nifti1Image (identity affine).

Mostly used for set up in other fixtures in other testing modules.
"""
return _img_zeros(shape, affine)


@pytest.fixture
def img_3d_zeros_eye():
"""Return a zeros-filled 3D Nifti1Image (identity affine)."""
return _img_3d_zeros()


def _img_3d_ones(shape=_shape_3d_default(), affine=_affine_eye()):
"""Return a ones-filled 3D Nifti1Image (identity affine).

Mostly used for set up in other fixtures in other testing modules.
"""
return _img_ones(shape, affine)


# ------------------------ 4D IMAGES ------------------------#


def _img_4d_zeros(shape=_shape_4d_default(), affine=_affine_eye()):
"""Return a default zeros filled 4D Nifti1Image (identity affine).

Mostly used for set up in other fixtures in other testing modules.
"""
return _img_zeros(shape, affine)


@pytest.fixture
def img_4d_zeros_eye():
"""Return a default zeros filled 4D Nifti1Image (identity affine)."""
return _img_4d_zeros()


@pytest.fixture
def img_4d_ones_eye():
"""Return a default ones filled 4D Nifti1Image (identity affine)."""
return _img_ones(_shape_4d_default(), _affine_eye())


@pytest.fixture
def img_4D_rand_eye():
"""Return a default random filled 4D Nifti1Image (identity affine)."""
rng = np.random.RandomState(42)
data = rng.rand(*_shape_4d_default())
return Nifti1Image(data, _affine_eye())


@pytest.fixture()
def testdata_4d_for_plotting():
"""Random 4D images for testing figures for multivolume data."""
rng = np.random.RandomState(42)
img_4d = Nifti1Image(rng.uniform(size=(7, 7, 3, 10)), MNI_AFFINE)
img_4d_long = Nifti1Image(rng.uniform(size=(7, 7, 3, 1777)), MNI_AFFINE)
img_mask = Nifti1Image(np.ones((7, 7, 3), dtype="uint8"), MNI_AFFINE)
img_4d = Nifti1Image(rng.uniform(size=(7, 7, 3, 10)), _affine_mni())
img_4d_long = Nifti1Image(rng.uniform(size=(7, 7, 3, 1777)), _affine_mni())
img_mask = Nifti1Image(np.ones((7, 7, 3), dtype="uint8"), _affine_mni())
atlas = np.ones((7, 7, 3), dtype="int32")
atlas[2:5, :, :] = 2
atlas[5:8, :, :] = 3
img_atlas = Nifti1Image(atlas, MNI_AFFINE)
img_atlas = Nifti1Image(atlas, _affine_mni())
atlas_labels = {
"gm": 1,
"wm": 2,
Expand Down
23 changes: 10 additions & 13 deletions nilearn/decomposition/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
from numpy.testing import assert_array_almost_equal
from scipy import linalg

from nilearn.conftest import _affine_eye, _img_3d_ones
from nilearn.decomposition._base import _fast_svd, _mask_and_reduce
from nilearn.maskers import MultiNiftiMasker

AFFINE_EYE = np.eye(4)

SHAPE = (6, 8, 10)


@pytest.fixture
def data_for_mask_and_reduce():
Remi-Gau marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -25,15 +22,14 @@ def data_for_mask_and_reduce():
# Add activation
this_img[2:4, 2:4, 2:4, :] += 10

imgs.append(Nifti1Image(this_img, AFFINE_EYE))
imgs.append(Nifti1Image(this_img, _affine_eye()))

return imgs


@pytest.fixture
def masker():
mask_img = Nifti1Image(np.ones(SHAPE, dtype=np.int8), AFFINE_EYE)
return MultiNiftiMasker(mask_img=mask_img).fit()
return MultiNiftiMasker(mask_img=_img_3d_ones()).fit()


# We need to use n_features > 500 to trigger the randomized_svd
Expand Down Expand Up @@ -79,6 +75,7 @@ def test_mask_reducer_multiple_image(
n_components,
reduction_ratio,
expected_shape_0,
shape_3d_default,
):
"""Mask and reduce 4D images with several values of input arguments."""
data = _mask_and_reduce(
Expand All @@ -88,21 +85,21 @@ def test_mask_reducer_multiple_image(
reduction_ratio=reduction_ratio,
)

expected_shape = (expected_shape_0, 6 * 8 * 10)
expected_shape = (expected_shape_0, np.prod(shape_3d_default))

assert data.shape == expected_shape


def test_mask_reducer_single_image_same_with_multiple_jobs(
data_for_mask_and_reduce, masker
data_for_mask_and_reduce, masker, shape_3d_default
):
"""Mask and reduce a 3D image and check results is the same \
when split over several CPUs."""
data_single = _mask_and_reduce(
masker, data_for_mask_and_reduce[0], n_components=3
)

assert data_single.shape == (3, 6 * 8 * 10)
assert data_single.shape == (3, np.prod(shape_3d_default))

# Test n_jobs > 1
data = _mask_and_reduce(
Expand All @@ -113,19 +110,19 @@ def test_mask_reducer_single_image_same_with_multiple_jobs(
random_state=0,
)

assert data.shape == (3, 6 * 8 * 10)
assert data.shape == (3, np.prod(shape_3d_default))
assert_array_almost_equal(data_single, data)


def test_mask_reducer_reduced_data_is_orthogonal(
data_for_mask_and_reduce, masker
data_for_mask_and_reduce, masker, shape_3d_default
):
"""Test that the reduced data is orthogonal."""
data = _mask_and_reduce(
masker, data_for_mask_and_reduce[0], n_components=3, random_state=0
)

assert data.shape == (3, 6 * 8 * 10)
assert data.shape == (3, np.prod(shape_3d_default))

cov = data.dot(data.T)
cov_diag = np.zeros((3, 3))
Expand Down
9 changes: 4 additions & 5 deletions nilearn/decomposition/tests/test_canica.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,20 @@
from numpy.testing import assert_array_almost_equal

from nilearn._utils.testing import write_tmp_imgs
from nilearn.conftest import _affine_eye
from nilearn.decomposition.canica import CanICA
from nilearn.decomposition.tests.test_multi_pca import _tmp_dir
from nilearn.image import get_data, iter_img
from nilearn.maskers import MultiNiftiMasker

AFFINE_EYE = np.eye(4)

SHAPE = (30, 30, 5)

N_SUBJECTS = 2


def _make_data_from_components(
components,
affine=AFFINE_EYE,
affine=_affine_eye(),
shape=SHAPE,
rng=None,
n_subjects=N_SUBJECTS,
Expand Down Expand Up @@ -87,7 +86,7 @@ def _make_canica_test_data(rng=None, n_subjects=N_SUBJECTS, noisy=True):

# Create a "multi-subject" dataset
data = _make_data_from_components(
components, AFFINE_EYE, SHAPE, rng=rng, n_subjects=n_subjects
components, _affine_eye(), SHAPE, rng=rng, n_subjects=n_subjects
)

return data, components, rng
Expand All @@ -102,7 +101,7 @@ def mask_img():
mask[:, -5:] = 0
mask[..., -2:] = 0
mask[..., :2] = 0
return Nifti1Image(mask, AFFINE_EYE)
return Nifti1Image(mask, _affine_eye())


@pytest.fixture(scope="module")
Expand Down
5 changes: 2 additions & 3 deletions nilearn/decomposition/tests/test_dict_learning.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
from nibabel import Nifti1Image

from nilearn._utils.testing import write_tmp_imgs
from nilearn.conftest import _affine_eye
from nilearn.decomposition.dict_learning import DictLearning
from nilearn.decomposition.tests.test_canica import _make_canica_test_data
from nilearn.decomposition.tests.test_multi_pca import _tmp_dir
from nilearn.image import get_data, iter_img
from nilearn.maskers import NiftiMasker

AFFINE_EYE = np.eye(4)

SHAPE = (30, 30, 5)


Expand All @@ -23,7 +22,7 @@ def mask_img():
mask[:, -5:] = 0
mask[..., -2:] = 0
mask[..., :2] = 0
return Nifti1Image(mask, AFFINE_EYE)
return Nifti1Image(mask, _affine_eye())


@pytest.fixture(scope="module")
Expand Down
7 changes: 3 additions & 4 deletions nilearn/decomposition/tests/test_multi_pca.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
from numpy.testing import assert_almost_equal

from nilearn._utils.testing import write_tmp_imgs
from nilearn.conftest import _affine_eye
from nilearn.decomposition._multi_pca import _MultiPCA
from nilearn.maskers import MultiNiftiMasker, NiftiMasker

AFFINE_EYE = np.eye(4)

SHAPE = (6, 8, 10)


Expand All @@ -30,7 +29,7 @@ def img_4D():
def _make_multi_pca_test_data(with_activation=True):
"""Create a multi-subject dataset with or without activation."""
shape = (6, 8, 10, 5)
affine = AFFINE_EYE
affine = _affine_eye()
rng = np.random.RandomState(0)
n_sub = 4

Expand All @@ -48,7 +47,7 @@ def _make_multi_pca_test_data(with_activation=True):

@pytest.fixture(scope="module")
def mask_img():
return Nifti1Image(np.ones(SHAPE, dtype=np.int8), AFFINE_EYE)
return Nifti1Image(np.ones(SHAPE, dtype=np.int8), _affine_eye())


@pytest.fixture(scope="module")
Expand Down