[ENH]: Add support for BrainPrint #344

Merged
synchon merged 20 commits from feat/brainprint into main 2024-06-03 09:08:56 +00:00
16 changed files with 741 additions and 11 deletions

3
.gitmodules vendored
View file

@ -1,3 +1,6 @@
[submodule "junifer/external/h5io"] [submodule "junifer/external/h5io"]
path = junifer/external/h5io path = junifer/external/h5io
url = https://github.com/juaml/h5io url = https://github.com/juaml/h5io
[submodule "junifer/external/BrainPrint"]
path = junifer/external/BrainPrint
url = https://github.com/juaml/BrainPrint.git

View file

@ -0,0 +1 @@
Add support for `BrainPrint <https://github.com/Deep-MI/BrainPrint?tab=readme-ov-file>`_ marker by `Synchon Mandal`_

View file

@ -4,3 +4,4 @@ chang
sepulcre sepulcre
arange arange
sinc sinc
whit

View file

@ -45,6 +45,7 @@ def test_get_dependency_information_short() -> None:
"httpx", "httpx",
"tqdm", "tqdm",
"templateflow", "templateflow",
"lapy",
"looseversion", "looseversion",
] ]
@ -74,6 +75,7 @@ def test_get_dependency_information_long() -> None:
"httpx", "httpx",
"tqdm", "tqdm",
"templateflow", "templateflow",
"lapy",
] ]
for key in dependency_list: for key in dependency_list:
assert key in dependency_information_keys assert key in dependency_information_keys

View file

@ -350,7 +350,7 @@ def test_get_mask_errors() -> None:
with pytest.raises(ValueError, match=r"callable params"): with pytest.raises(ValueError, match=r"callable params"):
get_mask(masks={"GM_prob0.2": {"param": 1}}, target_data=vbm_gm) get_mask(masks={"GM_prob0.2": {"param": 1}}, target_data=vbm_gm)
# Pass only parametesr to the intersection function # Pass only parameters to the intersection function
with pytest.raises( with pytest.raises(
ValueError, match=r" At least one mask is required." ValueError, match=r" At least one mask is required."
): ):

View file

@ -6,6 +6,7 @@
# License: AGPL # License: AGPL
import re import re
from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
@ -364,7 +365,7 @@ class PatternDataGrabber(BaseDataGrabber):
# Data type dictionary # Data type dictionary
t_pattern = self.patterns[t_type] t_pattern = self.patterns[t_type]
# Copy data type dictionary in output # Copy data type dictionary in output
out[t_type] = t_pattern.copy() out[t_type] = deepcopy(t_pattern)
# Iterate to check for nested "types" like mask # Iterate to check for nested "types" like mask
for k, v in t_pattern.items(): for k, v in t_pattern.items():
# Resolve pattern for base data type # Resolve pattern for base data type

1
junifer/external/BrainPrint vendored Submodule

@ -0,0 +1 @@
Subproject commit adf2b5b5e3289126e3d66fcbd6cc8575ce5a277c

View file

@ -23,3 +23,4 @@ from .temporal_snr import (
TemporalSNRParcels, TemporalSNRParcels,
TemporalSNRSpheres, TemporalSNRSpheres,
) )
from .brainprint import BrainPrint

View file

@ -0,0 +1,662 @@
"""Provide class for BrainPrint."""
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL
try:
from importlib.metadata import packages_distributions
except ImportError: # pragma: no cover
from importlib_metadata import packages_distributions
import uuid
from copy import deepcopy
from importlib.util import find_spec
from itertools import chain
from pathlib import Path
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Dict,
List,
Optional,
Set,
Union,
)
import numpy as np
import numpy.typing as npt
from ..api.decorators import register_marker
from ..external.BrainPrint.brainprint.brainprint import (
compute_asymmetry,
compute_brainprint,
)
from ..external.BrainPrint.brainprint.surfaces import surf_to_vtk
from ..pipeline import WorkDirManager
from ..pipeline.utils import check_ext_dependencies
from ..utils import logger, raise_error, run_ext_cmd
from .base import BaseMarker
if TYPE_CHECKING:
from junifer.storage import BaseFeatureStorage
@register_marker
class BrainPrint(BaseMarker):
"""Class for BrainPrint.
Parameters
----------
num : positive int, optional
Number of eigenvalues to compute (default 50).
skip_cortex : bool, optional
Whether to skip cortical surface or not (default False).
keep_eigenvectors : bool, optional
Whether to also return eigenvectors or not (default False).
norm : str, optional
Eigenvalues normalization method (default "none").
reweight : bool, optional
Whether to reweight eigenvalues or not (default False).
asymmetry : bool, optional
Whether to calculate asymmetry between lateral structures
(default False).
asymmetry_distance : {"euc"}, optional
Distance measurement to use if ``asymmetry=True``:
* ``"euc"`` : Euclidean
(default "euc").
use_cholmod : bool, optional
If True, attempts to use the Cholesky decomposition for improved
execution speed. Requires the ``scikit-sparse`` library. If it cannot
be found, an error will be thrown. If False, will use slower LU
decomposition (default False).
name : str, optional
The name of the marker. If None, will use the class name (default
None).
"""
_EXT_DEPENDENCIES: ClassVar[List[Dict[str, Union[str, List[str]]]]] = [
{
"name": "freesurfer",
"commands": [
"mri_binarize",
"mri_pretess",
"mri_mc",
"mris_convert",
],
},
]
_DEPENDENCIES: ClassVar[Set[str]] = {"lapy", "numpy"}
def __init__(
self,
num: int = 50,
skip_cortex=False,
keep_eigenvectors: bool = False,
norm: str = "none",
reweight: bool = False,
asymmetry: bool = False,
asymmetry_distance: str = "euc",
use_cholmod: bool = False,
name: Optional[str] = None,
) -> None:
self.num = num
self.skip_cortex = skip_cortex
self.keep_eigenvectors = keep_eigenvectors
self.norm = norm
self.reweight = reweight
self.asymmetry = asymmetry
self.asymmetry_distance = asymmetry_distance
self.use_cholmod = use_cholmod
super().__init__(name=name, on="FreeSurfer")
def get_valid_inputs(self) -> List[str]:
"""Get valid data types for input.
Returns
-------
list of str
The list of data types that can be used as input for this marker.
"""
return ["FreeSurfer"]
# TODO: kept for making this class concrete; should be removed later
def get_output_type(self, input_type: str) -> str:
"""Get output type.
Parameters
----------
input_type : str
The data type input to the marker.
Returns
-------
str
The storage type output by the marker.
"""
return "vector"
# TODO: overridden to allow multiple outputs from single data type; should
# be removed later
def validate(self, input: List[str]) -> List[str]:
"""Validate the the pipeline step.
Parameters
----------
input : list of str
The input to the pipeline step.
Returns
-------
list of str
The output of the pipeline step.
"""
def _check_dependencies(obj) -> None:
"""Check obj._DEPENDENCIES.
Parameters
----------
obj : object
Object to check _DEPENDENCIES of.
Raises
------
ImportError
If the pipeline step object is missing dependencies required
for its working.
"""
# Check if _DEPENDENCIES attribute is found;
# (markers and preprocessors will have them but not datareaders
# as of now)
dependencies_not_found = []
if hasattr(obj, "_DEPENDENCIES"):
# Check if dependencies are importable
for dependency in obj._DEPENDENCIES:
# First perform an easy check
if find_spec(dependency) is None:
# Then check mapped names
if dependency not in list(
chain.from_iterable(
packages_distributions().values()
)
):
dependencies_not_found.append(dependency)
# Raise error if any dependency is not found
if dependencies_not_found:
raise_error(
msg=(
f"{dependencies_not_found} are not installed but are "
f"required for using {obj.__class__.__name__}."
),
klass=ImportError,
)
def _check_ext_dependencies(obj) -> None:
"""Check obj._EXT_DEPENDENCIES.
Parameters
----------
obj : object
Object to check _EXT_DEPENDENCIES of.
"""
# Check if _EXT_DEPENDENCIES attribute is found;
# (some markers and preprocessors might have them)
if hasattr(obj, "_EXT_DEPENDENCIES"):
for dependency in obj._EXT_DEPENDENCIES:
check_ext_dependencies(**dependency)
# Check dependencies
_check_dependencies(self)
# Check external dependencies
# _check_ext_dependencies(self)
# Validate input
_ = self.validate_input(input=input)
# Validate output type
outputs = ["scalar_table", "vector"]
return outputs
def _create_aseg_surface(
self,
aseg_path: Path,
norm_path: Path,
indices: List,
) -> Path:
"""Generate a surface from the aseg and label files.
Parameters
----------
aseg_path : pathlib.Path
The FreeSurfer aseg path.
norm_path : pathlib.Path
The FreeSurfer norm path.
indices : list
List of label indices to include in the surface generation.
Returns
-------
pathlib.Path
Path to the generated surface in VTK format.
"""
tempfile_prefix = f"aseg.{uuid.uuid4()}"
# Set mri_binarize command
mri_binarize_output_path = self._tempdir / f"{tempfile_prefix}.mgz"
mri_binarize_cmd = [
"mri_binarize",
f"--i {aseg_path.resolve()}",
f"--match {''.join(indices)}",
f"--o {mri_binarize_output_path.resolve()}",
]
# Call mri_binarize command
run_ext_cmd(name="mri_binarize", cmd=mri_binarize_cmd)
label_value = "1"
# Fix label (pretess)
# Set mri_pretess command
mri_pretess_cmd = [
"mri_pretess",
f"{mri_binarize_output_path.resolve()}",
f"{label_value}",
f"{norm_path.resolve()}",
f"{mri_binarize_output_path.resolve()}",
]
# Call mri_pretess command
run_ext_cmd(name="mri_pretess", cmd=mri_pretess_cmd)
# Run marching cube to extract surface
# Set mri_mc command
mri_mc_output_path = self._tempdir / f"{tempfile_prefix}.surf"
mri_mc_cmd = [
"mri_mc",
f"{mri_binarize_output_path.resolve()}",
f"{label_value}",
f"{mri_mc_output_path.resolve()}",
]
# Run mri_mc command
run_ext_cmd(name="mri_mc", cmd=mri_mc_cmd)
# Convert to vtk
# Set mris_convert command
surface_path = (
self._element_tempdir / f"aseg.final.{'_'.join(indices)}.vtk"
)
mris_convert_cmd = [
"mris_convert",
f"{mri_mc_output_path.resolve()}",
f"{surface_path.resolve()}",
]
# Run mris_convert command
run_ext_cmd(name="mris_convert", cmd=mris_convert_cmd)
return surface_path
def _create_aseg_surfaces(
self,
aseg_path: Path,
norm_path: Path,
) -> Dict[str, Path]:
"""Create surfaces from FreeSurfer aseg labels.
Parameters
----------
aseg_path : pathlib.Path
The FreeSurfer aseg path.
norm_path : pathlib.Path
The FreeSurfer norm path.
Returns
-------
dict
Dictionary of label names mapped to corresponding surface paths.
"""
# Define aseg labels
# combined and individual aseg labels:
# - Left Striatum: left Caudate + Putamen + Accumbens
# - Right Striatum: right Caudate + Putamen + Accumbens
# - CorpusCallosum: 5 subregions combined
# - Cerebellum: brainstem + (left+right) cerebellum WM and GM
# - Ventricles: (left+right) lat.vent + inf.lat.vent + choroidplexus +
# 3rdVent + CSF
# - Lateral-Ventricle: lat.vent + inf.lat.vent + choroidplexus
# - 3rd-Ventricle: 3rd-Ventricle + CSF
aseg_labels = {
"CorpusCallosum": ["251", "252", "253", "254", "255"],
"Cerebellum": ["7", "8", "16", "46", "47"],
"Ventricles": ["4", "5", "14", "24", "31", "43", "44", "63"],
"3rd-Ventricle": ["14", "24"],
"4th-Ventricle": ["15"],
"Brain-Stem": ["16"],
"Left-Striatum": ["11", "12", "26"],
"Left-Lateral-Ventricle": ["4", "5", "31"],
"Left-Cerebellum-White-Matter": ["7"],
"Left-Cerebellum-Cortex": ["8"],
"Left-Thalamus-Proper": ["10"],
"Left-Caudate": ["11"],
"Left-Putamen": ["12"],
"Left-Pallidum": ["13"],
"Left-Hippocampus": ["17"],
"Left-Amygdala": ["18"],
"Left-Accumbens-area": ["26"],
"Left-VentralDC": ["28"],
"Right-Striatum": ["50", "51", "58"],
"Right-Lateral-Ventricle": ["43", "44", "63"],
"Right-Cerebellum-White-Matter": ["46"],
"Right-Cerebellum-Cortex": ["47"],
"Right-Thalamus-Proper": ["49"],
"Right-Caudate": ["50"],
"Right-Putamen": ["51"],
"Right-Pallidum": ["52"],
"Right-Hippocampus": ["53"],
"Right-Amygdala": ["54"],
"Right-Accumbens-area": ["58"],
"Right-VentralDC": ["60"],
}
return {
label: self._create_aseg_surface(
aseg_path=aseg_path,
norm_path=norm_path,
indices=indices,
)
for label, indices in aseg_labels.items()
}
def _create_cortical_surfaces(
self,
lh_white_path: Path,
rh_white_path: Path,
lh_pial_path: Path,
rh_pial_path: Path,
) -> Dict[str, Path]:
"""Create cortical surfaces from FreeSurfer labels.
Parameters
----------
lh_white_path : pathlib.Path
The FreeSurfer lh.white path.
rh_white_path : pathlib.Path
The FreeSurfer rh.white path.
lh_pial_path : pathlib.Path
The FreeSurfer lh.pial path.
rh_pial_path : pathlib.Path
The FreeSurfer rh.pial path.
Returns
-------
dict
Cortical surface label names with their paths as dictionary.
"""
return {
"lh-white-2d": surf_to_vtk(
lh_white_path.resolve(),
(self._element_tempdir / "lh.white.vtk").resolve(),
),
"rh-white-2d": surf_to_vtk(
rh_white_path.resolve(),
(self._element_tempdir / "rh.white.vtk").resolve(),
),
"lh-pial-2d": surf_to_vtk(
lh_pial_path.resolve(),
(self._element_tempdir / "lh.pial.vtk").resolve(),
),
"rh-pial-2d": surf_to_vtk(
rh_pial_path.resolve(),
(self._element_tempdir / "rh.pial.vtk").resolve(),
),
}
def compute(
self,
input: Dict[str, Any],
extra_input: Optional[Dict] = None,
) -> Dict:
"""Compute.
Parameters
----------
input : dict
The FreeSurfer data as dictionary.
extra_input : dict, optional
The other fields in the pipeline data object (default None).
Returns
-------
dict
The computed result as dictionary. The dictionary has the following
keys:
* ``eigenvalues`` : dict of surface labels (str) and eigenvalues
(``np.ndarray``)
* ``eigenvectors`` : dict of surface labels (str) and eigenvectors
(``np.ndarray``) if ``keep_eigenvectors=True``
else None
* ``distances`` : dict of ``{left_label}_{right_label}`` (str) and
distance (float) if ``asymmetry=True`` else None
References
----------
.. [1] Wachinger, C., Golland, P., Kremen, W. et al. (2015)
BrainPrint: A discriminative characterization of brain
morphology.
NeuroImage, Volume 109, Pages 232-248.
https://doi.org/10.1016/j.neuroimage.2015.01.032.
.. [2] Reuter, M., Wolter, F.E., Peinecke, N. (2006)
Laplace-Beltrami spectra as 'Shape-DNA' of surfaces and solids.
Computer-Aided Design, Volume 38, Issue 4, Pages 342-366.
https://doi.org/10.1016/j.cad.2005.10.011.
"""
logger.debug("Computing BrainPrint")
# Create component-scoped tempdir
self._tempdir = WorkDirManager().get_tempdir(prefix="brainprint")
# Create element-scoped tempdir so that the files are
# available later as nibabel stores file path reference for
# loading on computation
self._element_tempdir = WorkDirManager().get_element_tempdir(
prefix="brainprint"
)
# Generate surfaces
surfaces = self._create_aseg_surfaces(
aseg_path=input["aseg"]["path"],
norm_path=input["norm"]["path"],
)
if not self.skip_cortex:
cortical_surfaces = self._create_cortical_surfaces(
lh_white_path=input["lh_white"]["path"],
rh_white_path=input["rh_white"]["path"],
lh_pial_path=input["lh_pial"]["path"],
rh_pial_path=input["rh_pial"]["path"],
)
surfaces.update(cortical_surfaces)
# Compute brainprint
eigenvalues, _ = compute_brainprint(
surfaces=surfaces,
keep_eigenvectors=self.keep_eigenvectors,
num=self.num,
norm=self.norm,
reweight=self.reweight,
use_cholmod=self.use_cholmod,
)
# Calculate distances (if required)
distances = None
if self.asymmetry:
distances = compute_asymmetry(
eigenvalues=eigenvalues,
distance=self.asymmetry_distance,
skip_cortex=self.skip_cortex,
)
# Delete tempdir
WorkDirManager().delete_tempdir(self._tempdir)
output = {
"eigenvalues": {
"data": self._fix_nan(
[val[2:] for val in eigenvalues.values()]
).T,
"col_names": list(eigenvalues.keys()),
"row_names": [f"ev{i}" for i in range(self.num)],
"row_header_col_name": "eigenvalue",
},
"areas": {
"data": self._fix_nan(
[val[0] for val in eigenvalues.values()]
),
"col_names": list(eigenvalues.keys()),
},
"volumes": {
"data": self._fix_nan(
[val[1] for val in eigenvalues.values()]
),
"col_names": list(eigenvalues.keys()),
},
}
if self.asymmetry:
output["distances"] = {
"data": self._fix_nan(list(distances.values())),
"col_names": list(distances.keys()),
}
return output
def _fix_nan(
self,
input_data: List[Union[float, str, npt.ArrayLike]],
) -> np.ndarray:
"""Convert BrainPrint output with string NaN to ``numpy.nan``.
Parameters
----------
input_data : list of str, float or numpy.ndarray-like
The data to convert.
Returns
-------
np.ndarray
The converted data as ``numpy.ndarray``.
"""
arr = np.asarray(input_data)
arr[arr == "NaN"] = np.nan
return arr.astype(np.float64)
# TODO: overridden to allow storing multiple outputs from single input;
# should be removed later
def store(
self,
type_: str,
feature: str,
out: Dict[str, Any],
storage: "BaseFeatureStorage",
) -> None:
"""Store.
Parameters
----------
type_ : str
The data type to store.
feature : {"eigenvalues", "distances", "areas", "volumes"}
The feature name to store.
out : dict
The computed result as a dictionary to store.
storage : storage-like
The storage class, for example, SQLiteFeatureStorage.
Raises
------
ValueError
If ``feature`` is invalid.
"""
if feature == "eigenvalues":
output_type = "scalar_table"
elif feature in ["distances", "areas", "volumes"]:
output_type = "vector"
else:
raise_error(f"Unknown feature: {feature}")
logger.debug(f"Storing {output_type} in {storage}")
storage.store(kind=output_type, **out)
# TODO: overridden to allow storing multiple outputs from single input;
# should be removed later
def _fit_transform(
self,
input: Dict[str, Dict],
storage: Optional["BaseFeatureStorage"] = None,
) -> Dict:
"""Fit and transform.
Parameters
----------
input : dict
The Junifer Data object.
storage : storage-like, optional
The storage class, for example, SQLiteFeatureStorage.
Returns
-------
dict
The processed output as a dictionary. If `storage` is provided,
empty dictionary is returned.
"""
out = {}
for type_ in self._on:
if type_ in input.keys():
logger.info(f"Computing {type_}")
t_input = input[type_]
extra_input = input.copy()
extra_input.pop(type_)
t_meta = t_input["meta"].copy()
t_meta["type"] = type_
# Returns multiple features
t_out = self.compute(input=t_input, extra_input=extra_input)
if storage is None:
out[type_] = {}
for feature_name, feature_data in t_out.items():
# Make deep copy of the feature data for manipulation
feature_data_copy = deepcopy(feature_data)
# Make deep copy of metadata and add to feature data
feature_data_copy["meta"] = deepcopy(t_meta)
# Update metadata for the feature,
# feature data is not manipulated, only meta
self.update_meta(feature_data_copy, "marker")
# Update marker feature's metadata name
feature_data_copy["meta"]["marker"][
"name"
] += f"_{feature_name}"
if storage is not None:
logger.info(f"Storing in {storage}")
self.store(
type_=type_,
feature=feature_name,
out=feature_data_copy,
storage=storage,
)
else:
logger.info(
"No storage specified, returning dictionary"
)
out[type_][feature_name] = feature_data_copy
return out

View file

@ -72,7 +72,7 @@ class AFNIReHo:
Number of voxels in the neighbourhood, inclusive. Can be: Number of voxels in the neighbourhood, inclusive. Can be:
* 7 : for facewise neighbours only * 7 : for facewise neighbours only
* 19 : for face- and edge-wise nieghbours * 19 : for face- and edge-wise neighbours
* 27 : for face-, edge-, and node-wise neighbors * 27 : for face-, edge-, and node-wise neighbors
(default 27). (default 27).

View file

@ -60,7 +60,7 @@ class JuniferReHo:
Number of voxels in the neighbourhood, inclusive. Can be: Number of voxels in the neighbourhood, inclusive. Can be:
* 7 : for facewise neighbours only * 7 : for facewise neighbours only
* 19 : for face- and edge-wise nieghbours * 19 : for face- and edge-wise neighbours
* 27 : for face-, edge-, and node-wise neighbors * 27 : for face-, edge-, and node-wise neighbors
* 125 : for 5x5 cuboidal volume * 125 : for 5x5 cuboidal volume

View file

@ -37,7 +37,7 @@ class ReHoParcels(ReHoBase):
Number of voxels in the neighbourhood, inclusive. Can be: Number of voxels in the neighbourhood, inclusive. Can be:
- 7 : for facewise neighbours only - 7 : for facewise neighbours only
- 19 : for face- and edge-wise nieghbours - 19 : for face- and edge-wise neighbours
- 27 : for face-, edge-, and node-wise neighbors - 27 : for face-, edge-, and node-wise neighbors
* ``neigh_rad`` : positive float, optional * ``neigh_rad`` : positive float, optional
@ -67,7 +67,7 @@ class ReHoParcels(ReHoBase):
Number of voxels in the neighbourhood, inclusive. Can be: Number of voxels in the neighbourhood, inclusive. Can be:
* 7 : for facewise neighbours only * 7 : for facewise neighbours only
* 19 : for face- and edge-wise nieghbours * 19 : for face- and edge-wise neighbours
* 27 : for face-, edge-, and node-wise neighbors * 27 : for face-, edge-, and node-wise neighbors
* 125 : for 5x5 cuboidal volume * 125 : for 5x5 cuboidal volume

View file

@ -48,7 +48,7 @@ class ReHoSpheres(ReHoBase):
Number of voxels in the neighbourhood, inclusive. Can be: Number of voxels in the neighbourhood, inclusive. Can be:
- 7 : for facewise neighbours only - 7 : for facewise neighbours only
- 19 : for face- and edge-wise nieghbours - 19 : for face- and edge-wise neighbours
- 27 : for face-, edge-, and node-wise neighbors - 27 : for face-, edge-, and node-wise neighbors
* ``neigh_rad`` : positive float, optional * ``neigh_rad`` : positive float, optional
@ -78,7 +78,7 @@ class ReHoSpheres(ReHoBase):
Number of voxels in the neighbourhood, inclusive. Can be: Number of voxels in the neighbourhood, inclusive. Can be:
* 7 : for facewise neighbours only * 7 : for facewise neighbours only
* 19 : for face- and edge-wise nieghbours * 19 : for face- and edge-wise neighbours
* 27 : for face-, edge-, and node-wise neighbors * 27 : for face-, edge-, and node-wise neighbors
* 125 : for 5x5 cuboidal volume * 125 : for 5x5 cuboidal volume

View file

@ -0,0 +1,47 @@
"""Provide tests for BrainPrint."""
# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL
import socket
import pytest
from junifer.datagrabber import DataladAOMICID1000
from junifer.datareader import DefaultDataReader
from junifer.markers import BrainPrint
from junifer.pipeline.utils import _check_freesurfer
def test_get_output_type() -> None:
"""Test BrainPrint get_output_type()."""
marker = BrainPrint()
assert marker.get_output_type("FreeSurfer") == "vector"
def test_validate() -> None:
"""Test BrainPrint validate()."""
marker = BrainPrint()
assert set(marker.validate(["FreeSurfer"])) == {"scalar_table", "vector"}
@pytest.mark.skipif(
_check_freesurfer() is False, reason="requires FreeSurfer to be in PATH"
)
@pytest.mark.skipif(
socket.gethostname() != "juseless",
fraimondo commented 2024-05-28 08:16:18 +00:00 (Migrated from github.com)

Why only for juseless?

Why only for juseless?
synchon commented 2024-06-03 09:08:18 +00:00 (Migrated from github.com)

The data takes quite some time to fetch and the computation is better half of an hour, so would exceed the limitations of the gh-actions standard runners. We can pull it off on gh-actions if we have a small testing dataset providing FreeSurfer derivatives.

The data takes quite some time to fetch and the computation is better half of an hour, so would exceed the limitations of the gh-actions standard runners. We can pull it off on gh-actions if we have a small testing dataset providing FreeSurfer derivatives.
reason="only for juseless",
)
def test_compute() -> None:
"""Test BrainPrint compute()."""
with DataladAOMICID1000(types="FreeSurfer") as dg:
# Fetch element
element = dg["sub-0001"]
# Fetch element data
element_data = DefaultDataReader().fit_transform(element)
# Initialize the marker
marker = BrainPrint()
# Compute the marker
feature_map = marker.fit_transform(element_data)
# Assert the output keys
assert {"eigenvalues", "areas", "volumes"} == set(feature_map.keys())

View file

@ -49,6 +49,7 @@ dependencies = [
"httpx[http2]==0.26.0", "httpx[http2]==0.26.0",
"tqdm==4.66.1", "tqdm==4.66.1",
"templateflow>=23.0.0", "templateflow>=23.0.0",
"lapy>=1.0.0,<2.0.0",
"importlib_metadata; python_version<'3.10'", "importlib_metadata; python_version<'3.10'",
"looseversion==1.3.0; python_version>='3.12'", "looseversion==1.3.0; python_version>='3.12'",
] ]
@ -91,7 +92,12 @@ docs = [
################ ################
[tool.setuptools] [tool.setuptools]
packages = ["junifer", "junifer.external.h5io.h5io"] packages = [
"junifer",
"junifer.external.h5io.h5io",
"junifer.external.BrainPrint.brainprint",
"junifer.external.BrainPrint.brainprint.utils",
]
[tool.setuptools_scm] [tool.setuptools_scm]
version_scheme = "guess-next-dev" version_scheme = "guess-next-dev"
@ -104,11 +110,12 @@ target-version = ["py38", "py39", "py310", "py311", "py312"]
extend-exclude = """ extend-exclude = """
( (
junifer/external/h5io junifer/external/h5io
| junifer/external/BrainPrint
) )
""" """
[tool.codespell] [tool.codespell]
skip = "*/auto_examples/*,*.html,.git/,*.pyc,*/_build/*,*/h5io/*" skip = "*/auto_examples/*,*.html,.git/,*.pyc,*/_build/*,*/h5io/*,*/BrainPrint/*"
count = "" count = ""
quiet-level = 3 quiet-level = 3
ignore-words = "ignore_words.txt" ignore-words = "ignore_words.txt"
@ -152,6 +159,7 @@ select = [
extend-exclude = [ extend-exclude = [
"__init__.py", "__init__.py",
"junifer/external/h5io", "junifer/external/h5io",
"junifer/external/BrainPrint",
"docs", "docs",
"examples", "examples",
"tools", "tools",
@ -197,6 +205,8 @@ known-third-party =[
"templateflow", "templateflow",
"bct", "bct",
"neurokit2", "neurokit2",
"brainprint",
"lapy",
"pytest", "pytest",
] ]
@ -205,7 +215,7 @@ max-complexity = 20
[tool.pytest.ini_options] [tool.pytest.ini_options]
minversion = "7.0" minversion = "7.0"
addopts = "--ignore=junifer/external/h5io -vv" addopts = "--ignore=junifer/external/h5io --ignore=junifer/external/BrainPrint -vv"
[tool.towncrier] [tool.towncrier]
directory = "docs/changes/newsfragments" directory = "docs/changes/newsfragments"

View file

@ -78,6 +78,7 @@ omit =
*/tests/* */tests/*
*/junifer/configs/* */junifer/configs/*
*/junifer/external/h5io/* */junifer/external/h5io/*
*/junifer/external/BrainPrint/*
parallel = false parallel = false
[coverage:report] [coverage:report]