Skip to content

Commit

Permalink
Add Conjunction Analysis Workflow (#841)
Browse files Browse the repository at this point in the history
* Add Conjunction Analysis Workflow

* Solve lint issue

* Numba needs NumPy 1.22 or greater

* Improve coverage

* Fix ALE workflow deprecation warning to 0.2.0
  • Loading branch information
JulioAPeraza authored Oct 26, 2023
1 parent d0b4c0f commit 2a674a0
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 10 deletions.
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
54 changes: 53 additions & 1 deletion nimare/tests/test_workflows.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Test nimare.workflows."""
import os.path as op

import nibabel as nib
import numpy as np
import pytest

import nimare
Expand All @@ -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):
Expand Down Expand Up @@ -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])
2 changes: 2 additions & 0 deletions nimare/workflows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from .cbma import CBMAWorkflow, PairwiseCBMAWorkflow
from .ibma import IBMAWorkflow
from .macm import macm_workflow
from .misc import conjunction_analysis

__all__ = [
"ale_sleuth_workflow",
"CBMAWorkflow",
"PairwiseCBMAWorkflow",
"IBMAWorkflow",
"macm_workflow",
"conjunction_analysis",
]
2 changes: 1 addition & 1 deletion nimare/workflows/ale.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)

Expand Down
64 changes: 64 additions & 0 deletions nimare/workflows/misc.py
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 2a674a0

Please sign in to comment.