[ENH]: Introduce get_xfm for fetching transformation files #297

Merged
synchon merged 14 commits from feat/xfm-download into main 2024-02-01 08:32:57 +00:00
29 changed files with 180 additions and 13 deletions

View file

@ -17,17 +17,17 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
args: [--markdown-linebreak-ext=md] args: [--markdown-linebreak-ext=md]
- repo: https://github.com/abravalheri/validate-pyproject - repo: https://github.com/abravalheri/validate-pyproject
rev: v0.15 rev: v0.16
hooks: hooks:
- id: validate-pyproject - id: validate-pyproject
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.9.1 rev: 24.1.1
hooks: hooks:
- id: black - id: black
exclude: ^(docs/|examples/|tools/) exclude: ^(docs/|examples/|tools/)
args: [--check] args: [--check]
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.0 rev: v0.1.15
hooks: hooks:
- id: ruff - id: ruff
types_or: [python, jupyter] types_or: [python, jupyter]

View file

@ -4,6 +4,7 @@ Parcellations
.. automodule:: junifer.data.parcellations .. automodule:: junifer.data.parcellations
:members: :members:
Coordinates Coordinates
=========== ===========
@ -16,3 +17,10 @@ Masks
.. automodule:: junifer.data.masks .. automodule:: junifer.data.masks
:members: :members:
Template Spaces
===============
.. automodule:: junifer.data.template_spaces
:members:

View file

@ -0,0 +1 @@
Introduce :func:`.get_xfm` to fetch transformation files for moving between template spaces by `Synchon Mandal`_

View file

@ -141,6 +141,7 @@ def _validate_verbose(
------- -------
str or int str or int
The validated value. The validated value.
""" """
if isinstance(value, int): if isinstance(value, int):
return value return value

View file

@ -575,6 +575,7 @@ def _queue_slurm(
(default None). (default None).
config : dict config : dict
The configuration to be used for queueing the job. The configuration to be used for queueing the job.
""" """
pass pass
# logger.debug("Creating SLURM job") # logger.debug("Creating SLURM job")

View file

@ -32,7 +32,7 @@ def test_get_python_information() -> None:
def test_get_dependency_information_short() -> None: def test_get_dependency_information_short() -> None:
"""Test short version of _get_dependency_information().""" """Test short version of _get_dependency_information()."""
dependency_information = _get_dependency_information(long_=False) dependency_information = _get_dependency_information(long_=False)
assert list(dependency_information.keys()) == [ dependency_list = [
"click", "click",
"numpy", "numpy",
"scipy", "scipy",
@ -42,14 +42,20 @@ def test_get_dependency_information_short() -> None:
"nilearn", "nilearn",
"sqlalchemy", "sqlalchemy",
"ruamel.yaml", "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: def test_get_dependency_information_long() -> None:
"""Test long version of _get_dependency_information().""" """Test long version of _get_dependency_information()."""
dependency_information = _get_dependency_information(long_=True) dependency_information = _get_dependency_information(long_=True)
dependency_information_keys = list(dependency_information.keys()) dependency_information_keys = list(dependency_information.keys())
for key in [ dependency_list = [
"click", "click",
"numpy", "numpy",
"scipy", "scipy",
@ -59,7 +65,9 @@ def test_get_dependency_information_long() -> None:
"nilearn", "nilearn",
"sqlalchemy", "sqlalchemy",
"ruamel.yaml", "ruamel.yaml",
]: "httpx",
]
for key in dependency_list:
assert key in dependency_information_keys assert key in dependency_information_keys

View file

@ -25,4 +25,6 @@ from .masks import (
get_mask, get_mask,
) )
from .template_spaces import get_xfm
from . import utils from . import utils

View file

@ -0,0 +1,91 @@
"""Provide functions for template spaces."""
fraimondo commented 2024-01-31 10:21:13 +00:00 (Migrated from github.com)

Can we make it the juaml organization in g-node.org?

Can we make it the juaml organization in g-node.org?
synchon commented 2024-01-31 10:30:28 +00:00 (Migrated from github.com)

Yeah we can, since I don't have permission to make a repo in the org, I made it in my account.

Yeah we can, since I don't have permission to make a repo in the org, I made it in my account.
fraimondo commented 2024-01-31 10:45:31 +00:00 (Migrated from github.com)

Now you have !

Now you have !
synchon commented 2024-01-31 10:52:14 +00:00 (Migrated from github.com)

Transferred.

Transferred.
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# 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():
fraimondo commented 2024-01-31 10:25:19 +00:00 (Migrated from github.com)

Is iter_bytes still streaming? or the content is already in memory? If it is still streaming, an exception might occur here an it will not be catch.

Is `iter_bytes` still streaming? or the content is already in memory? If it is still streaming, an exception might occur here an it will not be catch.
synchon commented 2024-01-31 10:34:12 +00:00 (Migrated from github.com)

It's a generator basically thus it's still streaming. I don't find any exceptions being raised there in httpx's source code.

It's a generator basically thus it's still streaming. I don't find any exceptions being raised there in `httpx`'s source code.
fraimondo commented 2024-01-31 10:46:00 +00:00 (Migrated from github.com)

Not from httpx, but mostly from an underlying connection issue if the connection is cut

Not from httpx, but mostly from an underlying connection issue if the connection is cut
synchon commented 2024-01-31 10:47:22 +00:00 (Migrated from github.com)

You will get error from httpcore, which is the underlying HTTP library, like you get from urllib3 when using requests.

You will get error from `httpcore`, which is the underlying HTTP library, like you get from `urllib3` when using `requests`.
fraimondo commented 2024-01-31 10:54:32 +00:00 (Migrated from github.com)

Exactly, I thought of catching any exception and giving a more user-friendly error message of what's going

Exactly, I thought of catching any exception and giving a more user-friendly error message of what's going
synchon commented 2024-01-31 10:57:52 +00:00 (Migrated from github.com)

Ideally it should be dealt by the underlying http lib. You won't get an exception to catch when doing iter_bytes(), having an exception block there would not serve the purpose imo.

Ideally it should be dealt by the underlying http lib. You won't get an exception to catch when doing `iter_bytes()`, having an exception block there would not serve the purpose imo.
f.write(chunk)
return xfm_file_path

View file

@ -36,6 +36,7 @@ def test_closest_resolution(
The valid resolutions. The valid resolutions.
expected: float expected: float
The expected result. The expected result.
""" """
assert closest_resolution(resolution, valid_resolutions) == expected assert closest_resolution(resolution, valid_resolutions) == expected
assert ( assert (

View file

@ -343,6 +343,7 @@ def test_nilearn_compute_masks(
Parameters to pass to the function. Parameters to pass to the function.
resample : bool resample : bool
Whether to resample the mask to the target data. Whether to resample the mask to the target data.
""" """
reader = DefaultDataReader() reader = DefaultDataReader()
with SPMAuditoryTestingDataGrabber() as dg: with SPMAuditoryTestingDataGrabber() as dg:
@ -423,6 +424,7 @@ def test_get_mask_multiple(
Masks to get, junifer style. Masks to get, junifer style.
params : dict params : dict
Parameters to pass to the intersect_masks function. Parameters to pass to the intersect_masks function.
""" """
reader = DefaultDataReader() reader = DefaultDataReader()
with SPMAuditoryTestingDataGrabber() as dg: with SPMAuditoryTestingDataGrabber() as dg:

View file

@ -0,0 +1,30 @@
"""Provide tests for template spaces."""
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL
import socket
from pathlib import Path
import pytest
from junifer.data import get_xfm
@pytest.mark.skipif(
socket.gethostname() != "juseless",
fraimondo commented 2024-01-31 10:25:46 +00:00 (Migrated from github.com)

Why only for juseless?

Why only for juseless?
synchon commented 2024-01-31 10:34:39 +00:00 (Migrated from github.com)

The download takes well around 10 mins from Gin.

The download takes well around 10 mins from Gin.
fraimondo commented 2024-01-31 10:46:51 +00:00 (Migrated from github.com)

damn, we need to create a "slow test" suite that we use it to check only when about to merge or something like that. For the moment we keep it like this.

damn, we need to create a "slow test" suite that we use it to check only when about to merge or something like that. For the moment we keep it like this.
synchon commented 2024-01-31 10:48:49 +00:00 (Migrated from github.com)

Yeah that's a good idea. Well the essential issue is that Gin is not really as powerful as GitHub when you consider infrastructure, so the octet-stream downloads are not cached from what I understand.

Yeah that's a good idea. Well the essential issue is that Gin is not really as powerful as GitHub when you consider infrastructure, so the octet-stream downloads are not cached from what I understand.
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)

View file

@ -1,4 +1,5 @@
"""Provide utilities for data module.""" """Provide utilities for data module."""
from typing import List, Optional, Union from typing import List, Optional, Union
import numpy as np import numpy as np
@ -24,6 +25,7 @@ def closest_resolution(
------- -------
float or int float or int
The closest valid resolution. The closest valid resolution.
""" """
# Convert list of int to numpy.ndarray # Convert list of int to numpy.ndarray
if not isinstance(valid_resolution, np.ndarray): if not isinstance(valid_resolution, np.ndarray):

View file

@ -98,6 +98,7 @@ def _apply_mask_and_get_affinity(
A : scipy.sparse.lil_matrix A : scipy.sparse.lil_matrix
Contains the boolean indices for each sphere. Contains the boolean indices for each sphere.
shape: (number of seeds, number of voxels) shape: (number of seeds, number of voxels)
""" """
seeds = list(seeds) seeds = list(seeds)
@ -204,6 +205,7 @@ def _iter_signals_from_spheres(
mask_img : Niimg-like object, optional mask_img : Niimg-like object, optional
See :ref:`extracting_data`. See :ref:`extracting_data`.
Mask to apply to regions before extracting signals. Mask to apply to regions before extracting signals.
""" """
X, A = _apply_mask_and_get_affinity( X, A = _apply_mask_and_get_affinity(
seeds, niimg, radius, allow_overlap, mask_img=mask_img seeds, niimg, radius, allow_overlap, mask_img=mask_img

View file

@ -184,6 +184,7 @@ class ALFFBase(BaseMarker):
other data kind that needs to be used in the computation. For other data kind that needs to be used in the computation. For
example, the functional connectivity markers can make use of the example, the functional connectivity markers can make use of the
confounds if available (default None). confounds if available (default None).
""" """
raise_error( raise_error(
"_postprocess must be implemented", klass=NotImplementedError "_postprocess must be implemented", klass=NotImplementedError

View file

@ -59,6 +59,7 @@ class ALFFParcels(ALFFBase):
ALFF/fALFF are computed using a bandpass butterworth filter. See ALFF/fALFF are computed using a bandpass butterworth filter. See
:func:`scipy.signal.butter` and :func:`scipy.signal.filtfilt` for more :func:`scipy.signal.butter` and :func:`scipy.signal.filtfilt` for more
details. details.
""" """
def __init__( def __init__(

View file

@ -66,6 +66,7 @@ class ALFFSpheres(ALFFBase):
ALFF/fALFF are computed using a bandpass butterworth filter. See ALFF/fALFF are computed using a bandpass butterworth filter. See
:func:`scipy.signal.butter` and :func:`scipy.signal.filtfilt` for more :func:`scipy.signal.butter` and :func:`scipy.signal.filtfilt` for more
details. details.
""" """
def __init__( def __init__(

View file

@ -37,6 +37,7 @@ class CrossParcellationFC(BaseMarker):
name : str, optional name : str, optional
The name of the marker. If None, will use the class name The name of the marker. If None, will use the class name
(default None). (default None).
""" """
_DEPENDENCIES: ClassVar[Set[str]] = {"nilearn"} _DEPENDENCIES: ClassVar[Set[str]] = {"nilearn"}

View file

@ -97,6 +97,7 @@ class EdgeCentricFCParcels(FunctionalConnectivityBase):
* ``data`` : the actual computed values as a numpy.ndarray * ``data`` : the actual computed values as a numpy.ndarray
* ``col_names`` : the column labels for the computed values as list * ``col_names`` : the column labels for the computed values as list
""" """
parcel_aggregation = ParcelAggregation( parcel_aggregation = ParcelAggregation(
parcellation=self.parcellation, parcellation=self.parcellation,

View file

@ -84,6 +84,7 @@ class FunctionalConnectivityBase(BaseMarker):
------- -------
list of str list of str
The list of data types that can be used as input for this marker. The list of data types that can be used as input for this marker.
""" """
return ["BOLD"] return ["BOLD"]

View file

@ -500,9 +500,7 @@ def _kendall_w_reho(
numerator = (12 * np.sum(np.square(np.sum(timeseries_ranks, axis=0)))) - ( numerator = (12 * np.sum(np.square(np.sum(timeseries_ranks, axis=0)))) - (
3 * m**2 * n * (n + 1) ** 2 3 * m**2 * n * (n + 1) ** 2
) )
denominator = (m**2 * n * (n**2 - 1)) - ( denominator = (m**2 * n * (n**2 - 1)) - (m * np.sum(tied_rank_corrections))
m * np.sum(tied_rank_corrections)
)
if denominator == 0: if denominator == 0:
kcc = 1.0 kcc = 1.0

View file

@ -65,6 +65,7 @@ class TemporalSNRBase(BaseMarker):
------- -------
list of str list of str
The list of data types that can be used as input for this marker. The list of data types that can be used as input for this marker.
""" """
return ["BOLD"] return ["BOLD"]

View file

@ -38,6 +38,7 @@ def test_UpdateMetaMixin(
The dependencies of the pipeline step. The dependencies of the pipeline step.
expected : set expected : set
The expected dependencies. The expected dependencies.
""" """
class TestUpdateMetaMixin(UpdateMetaMixin): class TestUpdateMetaMixin(UpdateMetaMixin):

View file

@ -177,6 +177,7 @@ class BaseFeatureStorage(ABC):
The element as a dictionary. The element as a dictionary.
meta : dict meta : dict
The metadata as a dictionary. The metadata as a dictionary.
""" """
raise_error( raise_error(
msg="Concrete classes need to implement store_metadata().", msg="Concrete classes need to implement store_metadata().",

View file

@ -57,6 +57,7 @@ def _create_chunk(
------ ------
ValueError ValueError
If `kind` is not one of ['vector', 'matrix', 'timeseries']. If `kind` is not one of ['vector', 'matrix', 'timeseries'].
""" """
if kind in ["vector", "matrix"]: if kind in ["vector", "matrix"]:
features_data = np.concatenate(chunk_data, axis=-1) features_data = np.concatenate(chunk_data, axis=-1)
@ -678,9 +679,13 @@ class HDF5FeatureStorage(BaseFeatureStorage):
elif isinstance(data, list): elif isinstance(data, list):
if self.force_float32: if self.force_float32:
data = [ data = [
x.astype(dtype=np.dtype("float32"), casting="same_kind") (
if x.dtype == np.dtype("float64") x.astype(
else x dtype=np.dtype("float32"), casting="same_kind"
)
if x.dtype == np.dtype("float64")
else x
)
for x in data for x in data
] ]
# Handle cases for existing and new entry # Handle cases for existing and new entry

View file

@ -366,6 +366,7 @@ class SQLiteFeatureStorage(PandasBaseFeatureStorage):
The element as a dictionary. The element as a dictionary.
meta : dict meta : dict
The metadata as a dictionary. The metadata as a dictionary.
""" """
# Get sqlalchemy engine # Get sqlalchemy engine
engine = self.get_engine(element=element) engine = self.get_engine(element=element)

View file

@ -838,6 +838,7 @@ def _create_data_to_store(n_elements: int, kind: str) -> Tuple[str, Dict]:
The meta md5. The meta md5.
dict dict
The data to store. The data to store.
""" """
all_data = [] all_data = []
t_md5 = None t_md5 = None

View file

@ -131,6 +131,7 @@ def element_to_prefix(element: Dict) -> str:
------- -------
str str
The element converted to prefix. The element converted to prefix.
""" """
logger.debug(f"Converting element {element} to prefix.") logger.debug(f"Converting element {element} to prefix.")
prefix = "element" prefix = "element"

View file

@ -18,6 +18,7 @@ def get_testing_data(fname: str) -> Path:
------- -------
pathlib.Path pathlib.Path
The absolute path to the file. The absolute path to the file.
""" """
t_path = Path(__file__).parent / "data" / fname t_path = Path(__file__).parent / "data" / fname
if not t_path.exists(): if not t_path.exists():

View file

@ -41,11 +41,12 @@ dependencies = [
"datalad>=0.15.4,<0.20", "datalad>=0.15.4,<0.20",
"pandas>=1.4.0,<2.2", "pandas>=1.4.0,<2.2",
"nibabel>=3.2.0,<5.11", "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", "sqlalchemy>=1.4.27,<=2.1.0",
"ruamel.yaml>=0.17,<0.18", "ruamel.yaml>=0.17,<0.18",
"importlib_metadata; python_version<'3.10'", "importlib_metadata; python_version<'3.10'",
"h5py>=3.8.0,<3.10", "h5py>=3.8.0,<3.10",
"httpx[http2]==0.26.0",
] ]
fraimondo commented 2024-01-31 10:27:01 +00:00 (Migrated from github.com)

Is it that strict the dependency?

Is it that strict the dependency?
synchon commented 2024-01-31 10:35:42 +00:00 (Migrated from github.com)

It's always recommended to keep it like so when dealing with libraries which aren't 1.0 yet as the APIs can change very often.

It's always recommended to keep it like so when dealing with libraries which aren't 1.0 yet as the APIs can change very often.
fraimondo commented 2024-01-31 10:46:59 +00:00 (Migrated from github.com)

perfect

perfect
dynamic = ["version"] dynamic = ["version"]
@ -185,6 +186,7 @@ known-third-party =[
"nilearn", "nilearn",
"sqlalchemy", "sqlalchemy",
"yaml", "yaml",
"httpx",
"bct", "bct",
"neurokit2", "neurokit2",
"pytest", "pytest",