diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fbfcf66ec..5f7c2bcd8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,17 +17,17 @@ repos: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.15 + rev: v0.16 hooks: - id: validate-pyproject - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 24.1.1 hooks: - id: black exclude: ^(docs/|examples/|tools/) args: [--check] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.0 + rev: v0.1.15 hooks: - id: ruff types_or: [python, jupyter] diff --git a/docs/api/data.rst b/docs/api/data.rst index 842ce47af..0ddfd5c7e 100644 --- a/docs/api/data.rst +++ b/docs/api/data.rst @@ -4,6 +4,7 @@ Parcellations .. automodule:: junifer.data.parcellations :members: + Coordinates =========== @@ -16,3 +17,10 @@ Masks .. automodule:: junifer.data.masks :members: + + +Template Spaces +=============== + +.. automodule:: junifer.data.template_spaces + :members: diff --git a/docs/changes/newsfragments/297.feature b/docs/changes/newsfragments/297.feature new file mode 100644 index 000000000..4a23bd147 --- /dev/null +++ b/docs/changes/newsfragments/297.feature @@ -0,0 +1 @@ +Introduce :func:`.get_xfm` to fetch transformation files for moving between template spaces by `Synchon Mandal`_ diff --git a/junifer/api/cli.py b/junifer/api/cli.py index b69754c28..71759aa76 100644 --- a/junifer/api/cli.py +++ b/junifer/api/cli.py @@ -141,6 +141,7 @@ def _validate_verbose( ------- str or int The validated value. + """ if isinstance(value, int): return value diff --git a/junifer/api/functions.py b/junifer/api/functions.py index 80e3e48d6..ec00a93ee 100644 --- a/junifer/api/functions.py +++ b/junifer/api/functions.py @@ -575,6 +575,7 @@ def _queue_slurm( (default None). config : dict The configuration to be used for queueing the job. + """ pass # logger.debug("Creating SLURM job") diff --git a/junifer/api/tests/test_api_utils.py b/junifer/api/tests/test_api_utils.py index de5aad532..bef0c19e9 100644 --- a/junifer/api/tests/test_api_utils.py +++ b/junifer/api/tests/test_api_utils.py @@ -32,7 +32,7 @@ def test_get_python_information() -> None: def test_get_dependency_information_short() -> None: """Test short version of _get_dependency_information().""" dependency_information = _get_dependency_information(long_=False) - assert list(dependency_information.keys()) == [ + dependency_list = [ "click", "numpy", "scipy", @@ -42,14 +42,20 @@ def test_get_dependency_information_short() -> None: "nilearn", "sqlalchemy", "ruamel.yaml", + "httpx", ] + if int(pl.python_version_tuple()[1]) < 10: + dependency_list.append("importlib_metadata") + assert frozenset(dependency_information.keys()) == frozenset( + dependency_list + ) def test_get_dependency_information_long() -> None: """Test long version of _get_dependency_information().""" dependency_information = _get_dependency_information(long_=True) dependency_information_keys = list(dependency_information.keys()) - for key in [ + dependency_list = [ "click", "numpy", "scipy", @@ -59,7 +65,9 @@ def test_get_dependency_information_long() -> None: "nilearn", "sqlalchemy", "ruamel.yaml", - ]: + "httpx", + ] + for key in dependency_list: assert key in dependency_information_keys diff --git a/junifer/data/__init__.py b/junifer/data/__init__.py index e879179bc..263fbedc1 100644 --- a/junifer/data/__init__.py +++ b/junifer/data/__init__.py @@ -25,4 +25,6 @@ from .masks import ( get_mask, ) +from .template_spaces import get_xfm + from . import utils diff --git a/junifer/data/template_spaces.py b/junifer/data/template_spaces.py new file mode 100644 index 000000000..4f10bb5ad --- /dev/null +++ b/junifer/data/template_spaces.py @@ -0,0 +1,91 @@ +"""Provide functions for template spaces.""" + +# Authors: Synchon Mandal +# License: AGPL + +from pathlib import Path +from typing import Union + +import httpx + +from ..utils import logger, raise_error + + +def get_xfm( + src: str, dst: str, xfms_dir: Union[str, Path, None] = None +) -> Path: # pragma: no cover + """Fetch warp files to convert from ``src`` to ``dst``. + + Parameters + ---------- + src : str + The template space to transform from. + dst : str + The template space to transform to. + xfms_dir : str or pathlib.Path, optional + Path where the retrieved transformation files are stored. + The default location is "$HOME/junifer/data/xfms" (default None). + + Returns + ------- + pathlib.Path + The path to the transformation file. + + Raises + ------ + RuntimeError + If there is a problem fetching files. + + """ + if xfms_dir is None: + xfms_dir = Path().home() / "junifer" / "data" / "xfms" + logger.debug(f"Creating xfm directory at: {xfms_dir.resolve()}") + # Create default junifer data directory if not present + xfms_dir.mkdir(exist_ok=True, parents=True) + # Convert str to Path + elif not isinstance(xfms_dir, Path): + xfms_dir = Path(xfms_dir) + + # Set local file prefix + xfm_file_prefix = f"{src}_to_{dst}" + # Set local file dir + xfm_file_dir = xfms_dir / xfm_file_prefix + # Create local directory if not present + xfm_file_dir.mkdir(exist_ok=True, parents=True) + # Set file name with extension + xfm_file = f"{src}_to_{dst}_Composite.h5" + # Set local file path + xfm_file_path = xfm_file_dir / xfm_file + # Check if the file exists + if xfm_file_path.exists(): + logger.info( + f"Found existing xfm file for {src} to {dst} at " + f"{xfm_file_path.resolve()}" + ) + return xfm_file_path + + # Set URL + url = ( + "https://gin.g-node.org/juaml/human-template-xfms/raw/main/xfms/" + f"{xfm_file_prefix}/{xfm_file}" + ) + # Create the file before proceeding + xfm_file_path.touch() + + logger.info(f"Downloading xfm file for {src} to {dst} from {url}") + # Steam response + with httpx.stream("GET", url) as resp: + try: + resp.raise_for_status() + except httpx.HTTPError as exc: + raise_error( + f"Error response {exc.response.status_code} while " + f"requesting {exc.request.url!r}", + klass=RuntimeError, + ) + else: + with open(xfm_file_path, "ab") as f: + for chunk in resp.iter_bytes(): + f.write(chunk) + + return xfm_file_path diff --git a/junifer/data/tests/test_data_utils.py b/junifer/data/tests/test_data_utils.py index 614250b84..37b5a23ed 100644 --- a/junifer/data/tests/test_data_utils.py +++ b/junifer/data/tests/test_data_utils.py @@ -36,6 +36,7 @@ def test_closest_resolution( The valid resolutions. expected: float The expected result. + """ assert closest_resolution(resolution, valid_resolutions) == expected assert ( diff --git a/junifer/data/tests/test_masks.py b/junifer/data/tests/test_masks.py index c908eba66..4bc23f2da 100644 --- a/junifer/data/tests/test_masks.py +++ b/junifer/data/tests/test_masks.py @@ -343,6 +343,7 @@ def test_nilearn_compute_masks( Parameters to pass to the function. resample : bool Whether to resample the mask to the target data. + """ reader = DefaultDataReader() with SPMAuditoryTestingDataGrabber() as dg: @@ -423,6 +424,7 @@ def test_get_mask_multiple( Masks to get, junifer style. params : dict Parameters to pass to the intersect_masks function. + """ reader = DefaultDataReader() with SPMAuditoryTestingDataGrabber() as dg: diff --git a/junifer/data/tests/test_template_spaces.py b/junifer/data/tests/test_template_spaces.py new file mode 100644 index 000000000..d61e36696 --- /dev/null +++ b/junifer/data/tests/test_template_spaces.py @@ -0,0 +1,30 @@ +"""Provide tests for template spaces.""" + +# Authors: Synchon Mandal +# License: AGPL + +import socket +from pathlib import Path + +import pytest + +from junifer.data import get_xfm + + +@pytest.mark.skipif( + socket.gethostname() != "juseless", + reason="only for juseless", +) +def test_get_xfm(tmp_path: Path) -> None: + """Test warp file fetching. + + Parameters + ---------- + tmp_path : pathlib.Path + The path to the test directory. + + """ + xfm_path = get_xfm( + src="MNI152NLin6Asym", dst="MNI152NLin2009cAsym", xfms_dir=tmp_path + ) + assert isinstance(xfm_path, Path) diff --git a/junifer/data/utils.py b/junifer/data/utils.py index 97fb524d6..99306b3c1 100644 --- a/junifer/data/utils.py +++ b/junifer/data/utils.py @@ -1,4 +1,5 @@ """Provide utilities for data module.""" + from typing import List, Optional, Union import numpy as np @@ -24,6 +25,7 @@ def closest_resolution( ------- float or int The closest valid resolution. + """ # Convert list of int to numpy.ndarray if not isinstance(valid_resolution, np.ndarray): diff --git a/junifer/external/nilearn/junifer_nifti_spheres_masker.py b/junifer/external/nilearn/junifer_nifti_spheres_masker.py index 1163d65af..8f00448f9 100644 --- a/junifer/external/nilearn/junifer_nifti_spheres_masker.py +++ b/junifer/external/nilearn/junifer_nifti_spheres_masker.py @@ -98,6 +98,7 @@ def _apply_mask_and_get_affinity( A : scipy.sparse.lil_matrix Contains the boolean indices for each sphere. shape: (number of seeds, number of voxels) + """ seeds = list(seeds) @@ -204,6 +205,7 @@ def _iter_signals_from_spheres( mask_img : Niimg-like object, optional See :ref:`extracting_data`. Mask to apply to regions before extracting signals. + """ X, A = _apply_mask_and_get_affinity( seeds, niimg, radius, allow_overlap, mask_img=mask_img diff --git a/junifer/markers/falff/falff_base.py b/junifer/markers/falff/falff_base.py index d22dd7f72..f57411474 100644 --- a/junifer/markers/falff/falff_base.py +++ b/junifer/markers/falff/falff_base.py @@ -184,6 +184,7 @@ class ALFFBase(BaseMarker): other data kind that needs to be used in the computation. For example, the functional connectivity markers can make use of the confounds if available (default None). + """ raise_error( "_postprocess must be implemented", klass=NotImplementedError diff --git a/junifer/markers/falff/falff_parcels.py b/junifer/markers/falff/falff_parcels.py index 1c8a73a7a..25e8fcfc8 100644 --- a/junifer/markers/falff/falff_parcels.py +++ b/junifer/markers/falff/falff_parcels.py @@ -59,6 +59,7 @@ class ALFFParcels(ALFFBase): ALFF/fALFF are computed using a bandpass butterworth filter. See :func:`scipy.signal.butter` and :func:`scipy.signal.filtfilt` for more details. + """ def __init__( diff --git a/junifer/markers/falff/falff_spheres.py b/junifer/markers/falff/falff_spheres.py index 51132ef03..9768eadff 100644 --- a/junifer/markers/falff/falff_spheres.py +++ b/junifer/markers/falff/falff_spheres.py @@ -66,6 +66,7 @@ class ALFFSpheres(ALFFBase): ALFF/fALFF are computed using a bandpass butterworth filter. See :func:`scipy.signal.butter` and :func:`scipy.signal.filtfilt` for more details. + """ def __init__( diff --git a/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py b/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py index 95045109d..c56319180 100644 --- a/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +++ b/junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py @@ -37,6 +37,7 @@ class CrossParcellationFC(BaseMarker): name : str, optional The name of the marker. If None, will use the class name (default None). + """ _DEPENDENCIES: ClassVar[Set[str]] = {"nilearn"} diff --git a/junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py b/junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py index 5873a2b0f..ba3ea9070 100644 --- a/junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +++ b/junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py @@ -97,6 +97,7 @@ class EdgeCentricFCParcels(FunctionalConnectivityBase): * ``data`` : the actual computed values as a numpy.ndarray * ``col_names`` : the column labels for the computed values as list + """ parcel_aggregation = ParcelAggregation( parcellation=self.parcellation, diff --git a/junifer/markers/functional_connectivity/functional_connectivity_base.py b/junifer/markers/functional_connectivity/functional_connectivity_base.py index 3924e2db5..743adc976 100644 --- a/junifer/markers/functional_connectivity/functional_connectivity_base.py +++ b/junifer/markers/functional_connectivity/functional_connectivity_base.py @@ -84,6 +84,7 @@ class FunctionalConnectivityBase(BaseMarker): ------- list of str The list of data types that can be used as input for this marker. + """ return ["BOLD"] diff --git a/junifer/markers/reho/reho_estimator.py b/junifer/markers/reho/reho_estimator.py index d3ddd420b..9ebfdc2e9 100644 --- a/junifer/markers/reho/reho_estimator.py +++ b/junifer/markers/reho/reho_estimator.py @@ -500,9 +500,7 @@ def _kendall_w_reho( numerator = (12 * np.sum(np.square(np.sum(timeseries_ranks, axis=0)))) - ( 3 * m**2 * n * (n + 1) ** 2 ) - denominator = (m**2 * n * (n**2 - 1)) - ( - m * np.sum(tied_rank_corrections) - ) + denominator = (m**2 * n * (n**2 - 1)) - (m * np.sum(tied_rank_corrections)) if denominator == 0: kcc = 1.0 diff --git a/junifer/markers/temporal_snr/temporal_snr_base.py b/junifer/markers/temporal_snr/temporal_snr_base.py index 3972c97cb..64e1953a5 100644 --- a/junifer/markers/temporal_snr/temporal_snr_base.py +++ b/junifer/markers/temporal_snr/temporal_snr_base.py @@ -65,6 +65,7 @@ class TemporalSNRBase(BaseMarker): ------- list of str The list of data types that can be used as input for this marker. + """ return ["BOLD"] diff --git a/junifer/pipeline/tests/test_update_meta_mixin.py b/junifer/pipeline/tests/test_update_meta_mixin.py index 63a31703f..2160f84ca 100644 --- a/junifer/pipeline/tests/test_update_meta_mixin.py +++ b/junifer/pipeline/tests/test_update_meta_mixin.py @@ -38,6 +38,7 @@ def test_UpdateMetaMixin( The dependencies of the pipeline step. expected : set The expected dependencies. + """ class TestUpdateMetaMixin(UpdateMetaMixin): diff --git a/junifer/storage/base.py b/junifer/storage/base.py index 948bfc51f..3977fb57b 100644 --- a/junifer/storage/base.py +++ b/junifer/storage/base.py @@ -177,6 +177,7 @@ class BaseFeatureStorage(ABC): The element as a dictionary. meta : dict The metadata as a dictionary. + """ raise_error( msg="Concrete classes need to implement store_metadata().", diff --git a/junifer/storage/hdf5.py b/junifer/storage/hdf5.py index 2fdb38db2..3dea107fb 100644 --- a/junifer/storage/hdf5.py +++ b/junifer/storage/hdf5.py @@ -57,6 +57,7 @@ def _create_chunk( ------ ValueError If `kind` is not one of ['vector', 'matrix', 'timeseries']. + """ if kind in ["vector", "matrix"]: features_data = np.concatenate(chunk_data, axis=-1) @@ -678,9 +679,13 @@ class HDF5FeatureStorage(BaseFeatureStorage): elif isinstance(data, list): if self.force_float32: data = [ - x.astype(dtype=np.dtype("float32"), casting="same_kind") - if x.dtype == np.dtype("float64") - else x + ( + x.astype( + dtype=np.dtype("float32"), casting="same_kind" + ) + if x.dtype == np.dtype("float64") + else x + ) for x in data ] # Handle cases for existing and new entry diff --git a/junifer/storage/sqlite.py b/junifer/storage/sqlite.py index 101492420..d43fbb3fc 100644 --- a/junifer/storage/sqlite.py +++ b/junifer/storage/sqlite.py @@ -366,6 +366,7 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage): The element as a dictionary. meta : dict The metadata as a dictionary. + """ # Get sqlalchemy engine engine = self.get_engine(element=element) diff --git a/junifer/storage/tests/test_hdf5.py b/junifer/storage/tests/test_hdf5.py index ade6dfc9c..7e0973de7 100644 --- a/junifer/storage/tests/test_hdf5.py +++ b/junifer/storage/tests/test_hdf5.py @@ -838,6 +838,7 @@ def _create_data_to_store(n_elements: int, kind: str) -> Tuple[str, Dict]: The meta md5. dict The data to store. + """ all_data = [] t_md5 = None diff --git a/junifer/storage/utils.py b/junifer/storage/utils.py index 629b64f20..f386d0033 100644 --- a/junifer/storage/utils.py +++ b/junifer/storage/utils.py @@ -131,6 +131,7 @@ def element_to_prefix(element: Dict) -> str: ------- str The element converted to prefix. + """ logger.debug(f"Converting element {element} to prefix.") prefix = "element" diff --git a/junifer/testing/utils.py b/junifer/testing/utils.py index 48aad4d97..3a5b25a0c 100644 --- a/junifer/testing/utils.py +++ b/junifer/testing/utils.py @@ -18,6 +18,7 @@ def get_testing_data(fname: str) -> Path: ------- pathlib.Path The absolute path to the file. + """ t_path = Path(__file__).parent / "data" / fname if not t_path.exists(): diff --git a/pyproject.toml b/pyproject.toml index 213bc37f7..0897cdfb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,11 +41,12 @@ dependencies = [ "datalad>=0.15.4,<0.20", "pandas>=1.4.0,<2.2", "nibabel>=3.2.0,<5.11", - "nilearn>=0.9.0,<=0.11.0", + "nilearn>=0.9.0,<=0.10.2", "sqlalchemy>=1.4.27,<=2.1.0", "ruamel.yaml>=0.17,<0.18", "importlib_metadata; python_version<'3.10'", "h5py>=3.8.0,<3.10", + "httpx[http2]==0.26.0", ] dynamic = ["version"] @@ -185,6 +186,7 @@ known-third-party =[ "nilearn", "sqlalchemy", "yaml", + "httpx", "bct", "neurokit2", "pytest",