Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conda-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dependencies:
- ruamel.yaml>=0.17,<0.19
- h5py>=3.10
- tqdm>=4.66.1,<4.67.0
- templateflow>=23.0.0
- templateflow>=23.0.0,<25.0.0
- lapy>=1.0.0,<2.0.0
- lazy_loader==0.4
- importlib_metadata
Expand Down
34 changes: 34 additions & 0 deletions docs/builtin.rst
Original file line number Diff line number Diff line change
Expand Up @@ -743,5 +743,39 @@ Available
Planned
~~~~~~~


Maps
----

..
Provide a list of the maps that are implemented or planned.

Version added: The junifer version in which the maps was added.

Available
~~~~~~~~~

.. list-table::
:widths: auto
:header-rows: 1

* - Name
- Options
- Keys
- Template Spaces
- Version Added
- Publication
* - Smith
- ``components``, ``dimension``
- | ``Smith_rsn_10``, ``Smith_rsn_20``, ``Smith_rsn_70``,
| ``Smith_bm_10``, ``Smith_bm_20``, ``Smith_bm_70``
- ``MNI152NLin6Asym``
- 0.0.7
- | S.M. Smith, P.T. Fox, K.L. Miller et al.
| Correspondence of the brain's functional architecture during activation and rest
| Proc. Natl. Acad. Sci. U.S.A. 106 (31) 13040-13045 (2009)
| https://doi.org/10.1073/pnas.0905267106


..
helpful site for creating tables: https://rest-sphinx-memo.readthedocs.io/en/latest/ReST.html#tables
1 change: 1 addition & 0 deletions docs/changes/newsfragments/458.change
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:func:`.get_data`, :func:`.load_data`, :func:`.list_data`, :func:`.register_data` and :func:`.deregister_data` now support ``"maps"`` as a valid argument for ``kind`` by `Synchon Mandal`_
1 change: 1 addition & 0 deletions docs/changes/newsfragments/458.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introduce :class:`.MapsRegistry` to centralise probabilistic atlas (maps) data management by `Synchon Mandal`_
1 change: 1 addition & 0 deletions docs/changes/newsfragments/458.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Constrain ``templateflow`` version to ``>=23.0.0,<25.0.0`` by `Synchon Mandal`_
1 change: 1 addition & 0 deletions docs/extending/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ DataGrabbers, Preprocessors, Markers, etc., following the *junifer* way.
parcellations
coordinates
masks
maps
plugins
data_registries
data_types
Expand Down
140 changes: 140 additions & 0 deletions docs/extending/maps.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
.. include:: ../links.inc

.. _adding_maps:

Adding Maps
===========

Maps in ``junifer`` are basically probabilistic atlases. They are differentiated
from parcellations for ease of computation and to handle edge cases in a sane manner.
Before you start adding your own maps, check whether ``junifer`` has
the map(s) :ref:`in-built already <builtin>`. Perhaps, what is available
there will suffice to achieve your goals. However, of course ``junifer`` will not
have every map(s) available that you may want to use, and if so, it will
be nice to be able to add it yourself using a format that ``junifer`` understands.

Similarly, you may even be interested in creating your own custom maps
and then adding them to ``junifer``, so you can use ``junifer`` to obtain
different Markers to assess and validate your own maps. So, how can you do
this?

Since both of these use-cases are quite common, and not being able to use your
favourite map(s) is of course quite a buzzkill, ``junifer`` actually
provides the easy-to-use :func:`.register_data` function to do just that.
Let's try to understand the API reference and then use this function to register our
own map(s).

From the API reference, we can see that it has 3 positional arguments:

* ``kind``
* ``name``
* ``space``

as well as one optional keyword argument: ``overwrite``.
As the ``kind`` needs to be ``"maps"``, we can check
``MapsRegistry.register`` for keyword arguments to be passed:

* ``maps_path``
* ``maps_labels``

The ``name`` of the map(s) is up to you and will be the name that
``junifer`` will use to refer to this particular maps. You can think of
this as being similar to a key in a Python dictionary, i.e. a key that is used to
obtain and operate on the actual maps data. This ``name`` must always
be a string. For example, we could call our map(s)
``"my_custom_maps"`` (Note, that in a real-world use case this is
likely not a good name, and you should try to choose a meaningful name that
conveys as much relevant information about your maps as necessary).

The ``maps_path`` must be a ``str`` or ``Path`` object indicating a
path to a valid 4D NIfTI image, which contains floating-point labels indicating the
individual regions-of-interest (ROIs) of your map(s).

We also want to make sure that we can associate each label with
a human readable name (i.e. the name for each ROI). This serves naming the
features that maps-based markers produce in an unambiguous way, such
that a user can easily identify which ROIs were used to produce a specific
feature (multiple ROIs, because some features consist of information from two
or more ROIs, as for example in functional connectivity). Therefore, we provide
junifer with a list of strings, that contains the names for each ROI. In this
list, the label at the i-th position indicates the i-th index in the 4th dimension
of the NIfTI image.

Lastly, we specify the ``space`` that the map(s) is in, for example,
``"MNI152NLin6Asym"`` or ``"native"`` (scanner-native space).

Step 1: Prepare code to register a maps
---------------------------------------

Now we know everything that we need to know to make sure ``junifer`` can use our
own map(s) to compute any maps-based Marker. A simple example could
look like this:

.. code-block:: python

from pathlib import Path

import numpy as np
from junifer.data import register_data


# these are of course just example paths, replace it with your own:
path_to_maps = (
Path.cwd() / "my_custom_maps.nii.gz"
)
path_to_labels = (
Path.cwd() / "my_custom_maps_labels.txt"
)

my_labels = list(np.loadtxt(path_to_labels, dtype=str))

register_data(
kind="maps",
name="my_custom_maps",
maps_path=path_to_maps,
maps_labels=my_labels,
space="MNI152NLin6Asym",
)

We can run this code and it seems to work, however, how can we actually
include the custom map(s) in a ``junifer`` pipeline using a
:ref:`code-less YAML configuration <codeless>`?

Step 2: Add maps registration to the YAML file
----------------------------------------------

In order to use the maps in a ``junifer`` pipeline configured by a YAML
file, we can save the above code in a Python file, say
``registering_my_maps.py``. We can then simply add this file using the
``with`` keyword provided by ``junifer``:

.. code-block:: yaml

with:
- registering_my_maps.py

Afterwards continue configuring the rest of the pipeline in this YAML file, and
you will be able to use this maps using the name you gave the
maps when registering it. For example, we can add a
``MapsAggregation`` Marker to demonstrate how this can be done:

.. code-block:: yaml

markers:
- name: CustomMaps_mean
kind: MapsAggregation
maps: my_custom_maps
method: mean

Now, you can simply use this YAML file to run your pipeline.

.. important::

It's important to keep in mind that if the paths given in
``registering_my_maps.py`` are relative paths, they will be interpreted
by ``junifer`` as relative to the jobs directory (i.e. where ``junifer`` will
create submit files, logs directory and so on). For simplicity, you may just
want to use absolute paths to avoid confusion, yet using relative paths is
likely a better way to make your pipeline directory / repository more portable
and therefore more reproducible for others. Really, once you understand how
these paths are interpreted by ``junifer``, it is quite easy.
2 changes: 2 additions & 0 deletions junifer/data/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ __all__ = [
"CoordinatesRegistry",
"DataDispatcher",
"ParcellationRegistry",
"MapsRegistry",
"MaskRegistry",
"get_data",
"list_data",
Expand All @@ -17,6 +18,7 @@ __all__ = [
from .pipeline_data_registry_base import BasePipelineDataRegistry
from .coordinates import CoordinatesRegistry
from .parcellations import ParcellationRegistry
from .maps import MapsRegistry
from .masks import MaskRegistry

from ._dispatch import (
Expand Down
18 changes: 11 additions & 7 deletions junifer/data/_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from ..utils import raise_error
from .coordinates import CoordinatesRegistry
from .maps import MapsRegistry
from .masks import MaskRegistry
from .parcellations import ParcellationRegistry
from .pipeline_data_registry_base import BasePipelineDataRegistry
Expand Down Expand Up @@ -52,6 +53,7 @@ def __new__(cls):
{
"coordinates": CoordinatesRegistry,
"parcellation": ParcellationRegistry,
"maps": MapsRegistry,
"mask": MaskRegistry,
}
)
Expand Down Expand Up @@ -118,14 +120,14 @@ def get_data(
extra_input: Optional[dict[str, Any]] = None,
) -> Union[
tuple[ArrayLike, list[str]], # coordinates
tuple["Nifti1Image", list[str]], # parcellation
tuple["Nifti1Image", list[str]], # parcellation / maps
"Nifti1Image", # mask
]:
"""Get tailored ``kind`` for ``target_data``.

Parameters
----------
kind : {"coordinates", "parcellation", "mask"}
kind : {"coordinates", "parcellation", "mask", "maps"}
Kind of data to fetch and apply.
names : str or dict or list of str / dict
The registered name(s) of the data.
Expand Down Expand Up @@ -166,7 +168,7 @@ def list_data(kind: str) -> list[str]:

Parameters
----------
kind : {"coordinates", "parcellation", "mask"}
kind : {"coordinates", "parcellation", "mask", "maps"}
Kind of data registry to list.

Returns
Expand Down Expand Up @@ -195,7 +197,9 @@ def load_data(
**kwargs,
) -> Union[
tuple[ArrayLike, list[str], str], # coordinates
tuple[Optional["Nifti1Image"], list[str], Path, str], # parcellation
tuple[
Optional["Nifti1Image"], list[str], Path, str
], # parcellation / maps
tuple[
Optional[Union["Nifti1Image", Callable]], Optional[Path], str
], # mask
Expand All @@ -204,7 +208,7 @@ def load_data(

Parameters
----------
kind : {"coordinates", "parcellation", "mask"}
kind : {"coordinates", "parcellation", "mask", "maps"}
Kind of data to load.
name : str
The registered name of the data.
Expand Down Expand Up @@ -244,7 +248,7 @@ def register_data(

Parameters
----------
kind : {"coordinates", "parcellation", "mask"}
kind : {"coordinates", "parcellation", "mask", "maps"}
Kind of data to register.
name : str
The name to register.
Expand Down Expand Up @@ -277,7 +281,7 @@ def deregister_data(kind: str, name: str) -> None:

Parameters
----------
kind : {"coordinates", "parcellation", "mask"}
kind : {"coordinates", "parcellation", "mask", "maps"}
Kind of data to register.
name : str
The name to de-register.
Expand Down
9 changes: 9 additions & 0 deletions junifer/data/maps/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Maps."""

# Authors: Synchon Mandal <s.mandal@fz-juelich.de>
# License: AGPL

import lazy_loader as lazy


__getattr__, __dir__, __all__ = lazy.attach_stub(__name__, __file__)
5 changes: 5 additions & 0 deletions junifer/data/maps/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__all__ = [
"MapsRegistry",
]

from ._maps import MapsRegistry
Loading