Repository: nilearn/nilearn

Issue:
Refactor utils to create confounds from utils/data_gen and interfaces/fmriprep
noticing that there is a bit of code duplication (at least in terms of goals) between some of the utils used in testing to generate dummy fmriprep confounds and some of the code in _utils.data_gen to generate dummy fmriprep datasets.

Nothing big but will probably end up opening a PR to refactor.

_Originally posted by @Remi-Gau in https://github.com/nilearn/nilearn/issues/3819#issuecomment-1625115096_
            


Discussion:

Resolving patch:
diff --git a/nilearn/_utils/data_gen.py b/nilearn/_utils/data_gen.py
--- a/nilearn/_utils/data_gen.py
+++ b/nilearn/_utils/data_gen.py
@@ -1257,7 +1257,10 @@ def _create_bids_filename(
             value = fields["entities"][key]
             if value not in (None, ""):
                 filename += f"{key}-{value}_"
-    filename += f"{fields['suffix']}.{fields['extension']}"
+    if "suffix" in fields:
+        filename += f"{fields['suffix']}"
+    if "extension" in fields:
+        filename += f".{fields['extension']}"
 
     return filename
 


Test patch:
diff --git a/nilearn/interfaces/fmriprep/tests/test_load_confounds.py b/nilearn/interfaces/fmriprep/tests/test_load_confounds.py
--- a/nilearn/interfaces/fmriprep/tests/test_load_confounds.py
+++ b/nilearn/interfaces/fmriprep/tests/test_load_confounds.py
@@ -18,7 +18,7 @@
 )
 from nilearn.interfaces.fmriprep.tests.utils import (
     create_tmp_filepath,
-    get_leagal_confound,
+    get_legal_confound,
 )
 from nilearn.maskers import NiftiMasker
 
@@ -391,7 +391,8 @@ def test_not_found_exception(tmp_path):
     # Aggressive ICA-AROMA strategy requires
     # default nifti
     aroma_nii, _ = create_tmp_filepath(
-        tmp_path, image_type="ica_aroma", suffix="aroma"
+        tmp_path, image_type="ica_aroma", bids_fields={"entities":
+                                                       {"sub": "icaAroma"}}
     )
     with pytest.raises(ValueError) as exc_info:
         load_confounds(
@@ -418,7 +419,7 @@ def test_non_steady_state(tmp_path):
     """Warn when 'non_steady_state' is in strategy."""
     # supplying 'non_steady_state' in strategy is not necessary
     # check warning is correctly raised
-    img, conf = create_tmp_filepath(
+    img, _ = create_tmp_filepath(
         tmp_path, copy_confounds=True
     )
     warning_message = (r"Non-steady state")
@@ -452,14 +453,13 @@ def test_load_non_nifti(tmp_path):
 def test_invalid_filetype(tmp_path):
     """Invalid file types/associated files for load method."""
     bad_nii, bad_conf = create_tmp_filepath(tmp_path,
-                                            suffix="sub-test01_task-test",
                                             copy_confounds=True,
                                             old_derivative_suffix=False)
-    conf, _ = load_confounds(bad_nii)
+    _, _ = load_confounds(bad_nii)
 
     # more than one legal filename for confounds
     add_conf = "sub-test01_task-test_desc-confounds_regressors.tsv"
-    leagal_confounds, _ = get_leagal_confound()
+    leagal_confounds, _ = get_legal_confound()
     leagal_confounds.to_csv(tmp_path / add_conf, sep="\t", index=False)
     with pytest.raises(ValueError) as info:
         load_confounds(bad_nii)
@@ -474,7 +474,7 @@ def test_invalid_filetype(tmp_path):
     assert "The confound file contains no header." in str(error_log.value)
 
     # invalid fmriprep version: old camel case header (<1.2)
-    leagal_confounds, _ = get_leagal_confound()
+    leagal_confounds, _ = get_legal_confound()
     camel_confounds = leagal_confounds.copy()
     camel_confounds.columns = [
         _to_camel_case(col_name) for col_name in leagal_confounds.columns
@@ -545,7 +545,7 @@ def test_sample_mask(tmp_path):
     assert reg.shape[0] - len(mask) == 1
 
     # When no non-steady state volumes are present
-    conf_data, _ = get_leagal_confound(non_steady_state=False)
+    conf_data, _ = get_legal_confound(non_steady_state=False)
     conf_data.to_csv(regular_conf, sep="\t", index=False)  # save to tmp
     reg, mask = load_confounds(regular_nii, strategy=("motion", ))
     assert mask is None
@@ -576,7 +576,10 @@ def test_inputs(tmp_path, image_type):
     for i in range(2):  # gifti edge case
         nii, _ = create_tmp_filepath(
             tmp_path,
-            suffix=f"sub-test{i+1}_ses-test_task-testimg_run-01",
+            bids_fields={"entities": {"sub": f"test{i+1}",
+                                      "ses": "test",
+                                      "task": "testimg",
+                                      "run": "01"}},
             image_type=image_type,
             copy_confounds=True,
             copy_json=True,
diff --git a/nilearn/interfaces/fmriprep/tests/test_load_confounds_utils.py b/nilearn/interfaces/fmriprep/tests/test_load_confounds_utils.py
--- a/nilearn/interfaces/fmriprep/tests/test_load_confounds_utils.py
+++ b/nilearn/interfaces/fmriprep/tests/test_load_confounds_utils.py
@@ -42,7 +42,10 @@ def test_get_file_name(tmp_path, flag, image_type):
         suffix = "_desc-confounds_timeseries"
 
     if image_type == "part":
-        kwargs = {"suffix": "sub-test01_task-test_part-mag_run-01"}
+        kwargs = {"bids_fields": {"entities": {"sub": "test01",
+                                               "task": "test",
+                                               "part": "mag",
+                                               "run": "01"}}}
     else:
         kwargs = {}
 
diff --git a/nilearn/interfaces/fmriprep/tests/utils.py b/nilearn/interfaces/fmriprep/tests/utils.py
--- a/nilearn/interfaces/fmriprep/tests/utils.py
+++ b/nilearn/interfaces/fmriprep/tests/utils.py
@@ -4,24 +4,44 @@
 
 import pandas as pd
 
+from nilearn._utils.data_gen import _create_bids_filename
+from nilearn.interfaces.bids._utils import _bids_entities
 from nilearn.interfaces.fmriprep import load_confounds_utils
 
 img_file_patterns = {
     "ica_aroma":
-        "_space-MNI152NLin2009cAsym_desc-smoothAROMAnonaggr_bold.nii.gz",
-    "regular": "_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz",
-    "res": "_space-MNI152NLin2009cAsym_res-2_desc-preproc_bold.nii.gz",
-    "native": "_desc-preproc_bold.nii.gz",
-    "cifti": "_space-fsLR_den-91k_bold.dtseries.nii",
-    "den": "_space-fsLR_den-32k_desc-preproc_bold.nii.gz",
-    "part": (
-        "_part-mag_space-MNI152NLin2009cAsym_"
-        "desc-preproc_bold.nii.gz"
-    ),
+        {"entities": {"space": "MNI152NLin2009cAsym",
+                      "desc": "smoothAROMAnonaggr"}},
+    "regular":
+        {"entities": {"space": "MNI152NLin2009cAsym",
+                      "desc": "preproc"}},
+    "res":
+        {"entities": {"space": "MNI152NLin2009cAsym",
+                      "res": "2",
+                      "desc": "preproc"}},
+    "native":
+        {"entities": {"desc": "preproc"}},
+    "cifti":
+        {"entities": {"desc": "preproc",
+                      "space": "fsLR",
+                      "den": "91k"},
+         "extension": "dtseries.nii"},
+    "den":
+        {"entities": {"space": "fsLR",
+                      "den": "32k",
+                      "desc": "preproc"}},
+    "part":
+        {"entities": {"part": "mag",
+                      "space": "MNI152NLin2009cAsym",
+                      "desc": "preproc",
+                      }},
     "gifti": (
-        "_space-fsaverage5_hemi-L_bold.func.gii",
-        "_space-fsaverage5_hemi-R_bold.func.gii",
-    ),
+        {"entities": {"hemi": "L",
+                      "space": "fsaverage5"},
+         "extension": "func.gii"},
+        {"entities": {"hemi": "R",
+                      "space": "fsaverage5"},
+         "extension": "func.gii"})
 }
 
 
@@ -51,53 +71,95 @@ def get_testdata_path(non_steady_state=True):
 def create_tmp_filepath(
     base_path,
     image_type="regular",
-    suffix="sub-test01_task-test",
+    bids_fields=None,
     copy_confounds=False,
     copy_json=False,
     old_derivative_suffix=False
 ):
+    entities_to_include = [
+        *_bids_entities()["raw"],
+        *_bids_entities()["derivatives"]
+    ]
+    if bids_fields is None:
+        bids_fields = {"entities": {"sub": "test01",
+                                    "task": "test"}}
+
     """Create test files in temporary directory."""
     derivative = "regressors" if old_derivative_suffix else "timeseries"
 
     # confound files
-    confounds_root = f"_desc-confounds_{derivative}.tsv"
-    tmp_conf = base_path / (suffix + confounds_root)
+    bids_fields["entities"]["desc"] = "confounds"
+    bids_fields["suffix"] = derivative
+    bids_fields["extension"] = "tsv"
+    confounds_filename = _create_bids_filename(
+        fields=bids_fields,
+        entities_to_include=entities_to_include
+    )
+    tmp_conf = base_path / confounds_filename
 
     if copy_confounds:
-        conf, meta = get_leagal_confound()
+        conf, meta = get_legal_confound()
         conf.to_csv(tmp_conf, sep="\t", index=False)
     else:
         tmp_conf.touch()
 
     if copy_json:
-        meta_root = f"_desc-confounds_{derivative}.json"
-        tmp_meta = base_path / (suffix + meta_root)
-        conf, meta = get_leagal_confound()
+        bids_fields["extension"] = "json"
+        confounds_sidecar = _create_bids_filename(
+            fields=bids_fields,
+            entities_to_include=entities_to_include
+        )
+        tmp_meta = base_path / confounds_sidecar
+        conf, meta = get_legal_confound()
         with open(tmp_meta, "w") as file:
             json.dump(meta, file, indent=2)
 
     # image data
     # convert path object to string as nibabel do strings
-    img_root = img_file_patterns[image_type]
-    if type(img_root) is str:
-        tmp_img = suffix + img_root
+    img_file_patterns_type = img_file_patterns[image_type]
+    if type(img_file_patterns_type) is dict:
+        bids_fields = update_bids_fields(bids_fields, img_file_patterns_type)
+        tmp_img = _create_bids_filename(
+            fields=bids_fields,
+            entities_to_include=entities_to_include
+        )
         tmp_img = base_path / tmp_img
         tmp_img.touch()
         tmp_img = str(tmp_img)
     else:
         tmp_img = []
-        for root in img_root:
-            tmp_gii = suffix + root
+        for root in img_file_patterns_type:
+            bids_fields = update_bids_fields(bids_fields, root)
+            tmp_gii = _create_bids_filename(
+                fields=bids_fields,
+                entities_to_include=entities_to_include
+            )
             tmp_gii = base_path / tmp_gii
             tmp_gii.touch()
             tmp_img.append(str(tmp_gii))
     return tmp_img, tmp_conf
 
 
-def get_leagal_confound(non_steady_state=True):
+def get_legal_confound(non_steady_state=True):
     """Load the valid confound files for manipulation."""
     conf, meta = get_testdata_path(non_steady_state=non_steady_state)
     conf = pd.read_csv(conf, delimiter="\t", encoding="utf-8")
     with open(meta) as file:
         meta = json.load(file)
     return conf, meta
+
+
+def update_bids_fields(bids_fields, img_file_patterns_type):
+    """Update the bids_fields dictionary with the img_file_patterns_type."""
+    if "extension" not in img_file_patterns_type:
+        bids_fields["extension"] = "nii.gz"
+    if "suffix" not in img_file_patterns_type:
+        bids_fields["suffix"] = "bold"
+    for key in img_file_patterns_type:
+        if key == "entities":
+            for entity in img_file_patterns_type["entities"]:
+                bids_fields["entities"][entity] = \
+                    img_file_patterns_type["entities"][entity]
+        else:
+            bids_fields[key] = img_file_patterns_type[key]
+    return bids_fields