diff --git a/docs/api.rst b/docs/api.rst index 00ff683da..774deb0d7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -337,6 +337,7 @@ For more information about fetching data from the internet, see :ref:`fetching t workflows.cbma.CBMAWorkflow workflows.cbma.PairwiseCBMAWorkflow workflows.ibma.IBMAWorkflow + workflows.misc.conjunction_analysis :mod:`nimare.reports`: NiMARE report -------------------------------------------------- diff --git a/examples/02_meta-analyses/08_plot_cbma_subtraction_conjunction.py b/examples/02_meta-analyses/08_plot_cbma_subtraction_conjunction.py index 9591c8204..9bbc9bf2e 100644 --- a/examples/02_meta-analyses/08_plot_cbma_subtraction_conjunction.py +++ b/examples/02_meta-analyses/08_plot_cbma_subtraction_conjunction.py @@ -178,13 +178,9 @@ # can be computed by (a) identifying voxels that were statistically significant # in *both* individual group maps and (b) selecting, for each of these voxels, # the smaller of the two group-specific *z* values :footcite:t:`nichols2005valid`. -# Since this is simple arithmetic on images, conjunction is not implemented as -# a separate method in :code:`NiMARE` but can easily be achieved with -# :func:`nilearn.image.math_img`. -from nilearn.image import math_img +from nimare.workflows.misc import conjunction_analysis -formula = "np.where(img1 * img2 > 0, np.minimum(img1, img2), 0)" -img_conj = math_img(formula, img1=knowledge_img, img2=related_img) +img_conj = conjunction_analysis([knowledge_img, related_img]) plot_stat_map( img_conj, diff --git a/nimare/tests/test_workflows.py b/nimare/tests/test_workflows.py index 4d00afa22..621545c71 100644 --- a/nimare/tests/test_workflows.py +++ b/nimare/tests/test_workflows.py @@ -1,6 +1,8 @@ """Test nimare.workflows.""" import os.path as op +import nibabel as nib +import numpy as np import pytest import nimare @@ -10,7 +12,12 @@ from nimare.meta.cbma import ALE, ALESubtraction, MKDAChi2 from nimare.meta.ibma import Fishers, PermutedOLS, Stouffers from nimare.tests.utils import get_test_data_path -from nimare.workflows import CBMAWorkflow, IBMAWorkflow, PairwiseCBMAWorkflow +from nimare.workflows import ( + CBMAWorkflow, + IBMAWorkflow, + PairwiseCBMAWorkflow, + conjunction_analysis, +) def test_ale_workflow_function_smoke(tmp_path_factory): @@ -246,3 +253,48 @@ def test_ibma_workflow_smoke( filename = f"{tabletype}.tsv" outpath = op.join(tmpdir, filename) assert op.isfile(outpath) + + +def test_conjunction_analysis_smoke(tmp_path_factory): + """Run smoke test for conjunction analysis workflow.""" + # Create two 3D arrays with random values + arr1 = np.random.rand(10, 10, 10) + arr2 = np.random.rand(10, 10, 10) + + # Create two Nifti1Image objects from the arrays + img1 = nib.Nifti1Image(arr1, np.eye(4)) + img2 = nib.Nifti1Image(arr2, np.eye(4)) + + # Perform conjunction analysis on the two images + conj_img = conjunction_analysis([img1, img2]) + + # Check that the output is a Nifti1Image object + assert isinstance(conj_img, nib.Nifti1Image) + + # Check that the output has the same shape as the input images + assert conj_img.shape == img1.shape + + # Check that the output has the correct values + expected_output = np.minimum.reduce([arr1, arr2]) + np.testing.assert_array_equal(conj_img.get_fdata(), expected_output) + + # Test passing in a list of strings + tmpdir = tmp_path_factory.mktemp("test_conjunction_analysis_smoke") + img1_fn = op.join(tmpdir, "image1.nii.gz") + img2_fn = op.join(tmpdir, "image2.nii.gz") + img1.to_filename(img1_fn) + img2.to_filename(img2_fn) + + # Perform conjunction analysis on the two images from nifti files + conj_img_fromstr = conjunction_analysis([img1_fn, img2_fn]) + + # Check that the output has the correct values + np.testing.assert_array_equal(conj_img.get_fdata(), conj_img_fromstr.get_fdata()) + + # Raise error if only one image is provided + with pytest.raises(ValueError): + conjunction_analysis([img1]) + + # Raise error if invalid image type is provided + with pytest.raises(ValueError): + conjunction_analysis([1, 2]) diff --git a/nimare/workflows/__init__.py b/nimare/workflows/__init__.py index af045ad25..e8c12b95e 100644 --- a/nimare/workflows/__init__.py +++ b/nimare/workflows/__init__.py @@ -4,6 +4,7 @@ from .cbma import CBMAWorkflow, PairwiseCBMAWorkflow from .ibma import IBMAWorkflow from .macm import macm_workflow +from .misc import conjunction_analysis __all__ = [ "ale_sleuth_workflow", @@ -11,4 +12,5 @@ "PairwiseCBMAWorkflow", "IBMAWorkflow", "macm_workflow", + "conjunction_analysis", ] diff --git a/nimare/workflows/ale.py b/nimare/workflows/ale.py index 3639e485b..14743eb63 100644 --- a/nimare/workflows/ale.py +++ b/nimare/workflows/ale.py @@ -26,7 +26,7 @@ def ale_sleuth_workflow( ): """Perform ALE meta-analysis from Sleuth text file.""" LGR.warning( - "The ale_sleuth_workflow function is deprecated and will be removed in release 0.1.3. " + "The ale_sleuth_workflow function is deprecated and will be removed after release 0.2.0. " "Use CBMAWorkflow or PairwiseCBMAWorkflow instead." ) diff --git a/nimare/workflows/misc.py b/nimare/workflows/misc.py new file mode 100644 index 000000000..607fda27b --- /dev/null +++ b/nimare/workflows/misc.py @@ -0,0 +1,64 @@ +"""Miscellaneous Workflows.""" +import logging + +import nibabel as nib +from nilearn._utils import check_niimg_3d +from nilearn.image import math_img + +LGR = logging.getLogger(__name__) + + +def conjunction_analysis(imgs): + """Perform a conjunction analysis. + + .. versionadded:: 0.2.0 + + This method is described in :footcite:t:`nichols2005valid`. + + Parameters + ---------- + imgs : :obj:`list` of 3D :obj:`~nibabel.nifti1.Nifti1Image`, or :obj:`list` of :obj:`str` + List of images upon which to perform the conjuction analysis. + If a list of strings is provided, it is assumed to be paths to NIfTI images. + + Returns + ------- + :obj:`~nibabel.nifti1.Nifti1Image` + Conjunction image. + + References + ---------- + .. footbibliography:: + """ + if len(imgs) < 2: + raise ValueError("Conjunction analysis requires more than one image.") + + imgs_dict = {} + mult_formula, min_formula = "", "" + for img_i, img_obj in enumerate(imgs): + if isinstance(img_obj, str): + img = nib.load(img_obj) + elif isinstance(img_obj, nib.Nifti1Image): + img = img_obj + else: + raise ValueError( + f"Invalid image type provided: {type(img_obj)}. Must be a path to a NIfTI image " + "or a NIfTI image object." + ) + + img = check_niimg_3d(img) + + img_label = f"img{img_i}" + imgs_dict[img_label] = img + mult_formula += img_label + min_formula += img_label + + if img_i != len(imgs) - 1: + mult_formula += " * " + min_formula += ", " + + formula = f"np.where({mult_formula} > 0, np.minimum.reduce([{min_formula}]), 0)" + LGR.info("Performing conjunction analysis...") + LGR.info(f"Formula: {formula}") + + return math_img(formula, **imgs_dict) diff --git a/setup.cfg b/setup.cfg index b5c48039c..bdfd80ec5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ install_requires = nibabel>=3.2.0 # I/O of niftis nilearn>=0.10.1 numba>=0.57.0 # used by sparse - numpy>=1.21 + numpy>=1.22 # numba needs NumPy 1.22 or greater pandas>=2.0.0 patsy # for cbmr plotly # nimare.reports @@ -92,7 +92,7 @@ minimum = matplotlib==3.5.2 nibabel==3.2.0 nilearn==0.10.1 - numpy==1.21 + numpy==1.22 pandas==2.0.0 pymare==0.0.4rc2 scikit-learn==1.0.0