Repository: nilearn/nilearn

Issue:
Less restrictive of `load_confounds` file mathcing pattern
<!--Provide a brief description what you would like changes and why.-->
The `load_confounds` function does a lazy search based on the image file name to find other required files from a fMRIPrep derivative. Currently it is restricted to outputs that are mapped to a group template. This raise an issue when people wants to use files in subject's native space. In this case the `space-` entity would not be present. 

<!--Please fill in the following information, to the best of your ability.-->
### Benefits to the change
Load data preprocessed in native space with ease.

### Pseudocode for the new behavior, if applicable
It will not raise errors at this specific line here:
https://github.com/nilearn/nilearn/blob/9c29e3376243b643a72f78b8b98a56d12c2e99bb/nilearn/interfaces/fmriprep/load_confounds_utils.py#L92
No changes needed for the user facing side.


Discussion:
Silly question maybe, but wouldn't it be better to specify a `_space-`argument even in individual space ? I think it would always be a good thing... What you mean is probably that this is not strictly enforced in BIDS and thus many not be done one some dataset ? 
In BIDS convention, `space-` is not strictly required for derivative. This is a direct case they give in the [first common data example](https://bids-specification.readthedocs.io/en/stable/05-derivatives/02-common-data-types.html#examples)
So this is a valid case in BIDS, just it's not commonly used even in fMRIPrep. I only realised this can be an issue if people wish to work in subject space for neuromod data.

[See here for derivatives](https://bids-specification.readthedocs.io/en/stable/05-derivatives/03-imaging.html#preprocessed-coregistered-andor-resampled-volumes)
[More specifically for preprocessed clean data](https://bids-specification.readthedocs.io/en/stable/05-derivatives/02-common-data-types.html#preprocessed-or-cleaned-data)

In the case of mask, atlases and all that, they did state that if the space of the derivative is the same as source, `space-orig` would be the correct entity. 

Resolving patch:
diff --git a/nilearn/interfaces/fmriprep/load_confounds_utils.py b/nilearn/interfaces/fmriprep/load_confounds_utils.py
--- a/nilearn/interfaces/fmriprep/load_confounds_utils.py
+++ b/nilearn/interfaces/fmriprep/load_confounds_utils.py
@@ -10,26 +10,25 @@
 from nilearn._utils.fmriprep_confounds import (
     _flag_single_gifti, _is_camel_case
 )
+from nilearn.interfaces.bids import parse_bids_filename
 
 
 img_file_patterns = {
     "aroma": "_desc-smoothAROMAnonaggr_bold",
-    "nii.gz": "_space-.*_desc-preproc_bold.nii.gz",
-    "dtseries.nii": "_space-.*_bold.dtseries.nii",
-    "func.gii": "_space-.*_hemi-[LR]_bold.func.gii",
+    "nii.gz": "(_space-.*)?_desc-preproc_bold.nii.gz",
+    "dtseries.nii": "(_space-.*)?_bold.dtseries.nii",
+    "func.gii": "(_space-.*)?_hemi-[LR]_bold.func.gii",
 }
 
 img_file_error = {
     "aroma": (
-        "Input must be ~desc-smoothAROMAnonaggr_bold for full ICA-AROMA"
+        "Input must be desc-smoothAROMAnonaggr_bold for full ICA-AROMA"
         " strategy."
-    ),
-    "nii.gz": "Invalid file type for the selected method.",
+    ),    "nii.gz": "Invalid file type for the selected method.",
     "dtseries.nii": "Invalid file type for the selected method.",
     "func.gii": "need fMRIprep output with extension func.gii",
 }
 
-
 def _check_params(confounds_raw, params):
     """Check that specified parameters can be found in the confounds."""
     not_found_params = [
@@ -89,15 +88,28 @@ def _get_file_name(nii_file):
     """Construct the raw confound file name from processed functional data."""
     if isinstance(nii_file, list):  # catch gifti
         nii_file = nii_file[0]
-    suffix = "_space-" + nii_file.split("space-")[1]
+    entities = parse_bids_filename(nii_file)
+    subject_label = f"sub-{entities['sub']}"
+    if "ses" in entities:
+        subject_label = f"{subject_label}_ses-{entities['ses']}"
+    specifiers = f"task-{entities['task']}"
+    if "run" in entities:
+        specifiers = f"{specifiers}_run-{entities['run']}"
+    img_filename = nii_file.split(os.sep)[-1]
     # fmriprep has changed the file suffix between v20.1.1 and v20.2.0 with
     # respect to BEP 012.
     # cf. https://neurostars.org/t/naming-change-confounds-regressors-to-confounds-timeseries/17637 # noqa
     # Check file with new naming scheme exists or replace,
     # for backward compatibility.
     confounds_raw_candidates = [
-        nii_file.replace(suffix, "_desc-confounds_timeseries.tsv"),
-        nii_file.replace(suffix, "_desc-confounds_regressors.tsv"),
+        nii_file.replace(
+            img_filename,
+            f"{subject_label}_{specifiers}_desc-confounds_timeseries.tsv"
+        ),
+        nii_file.replace(
+            img_filename,
+            f"{subject_label}_{specifiers}_desc-confounds_regressors.tsv"
+        ),
     ]
 
     confounds_raw = [


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
@@ -446,12 +446,14 @@ 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, copy_confounds=True,
+    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)
 
     # more than one legal filename for confounds
-    add_conf = "test_desc-confounds_regressors.tsv"
+    add_conf = "sub-test01_task-test_desc-confounds_regressors.tsv"
     leagal_confounds, _ = get_leagal_confound()
     leagal_confounds.to_csv(tmp_path / add_conf, sep="\t", index=False)
     with pytest.raises(ValueError) as info:
@@ -551,7 +553,7 @@ def test_sample_mask(tmp_path):
 
 
 @pytest.mark.parametrize(
-    "image_type", ["regular", "ica_aroma", "gifti", "cifti"]
+    "image_type", ["regular", "native", "ica_aroma", "gifti", "cifti"]
 )
 def test_inputs(tmp_path, image_type):
     """Test multiple images as input."""
@@ -560,7 +562,7 @@ def test_inputs(tmp_path, image_type):
     for i in range(2):  # gifti edge case
         nii, _ = create_tmp_filepath(
             tmp_path,
-            suffix=f"img{i+1}",
+            suffix=f"sub-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/utils.py b/nilearn/interfaces/fmriprep/tests/utils.py
--- a/nilearn/interfaces/fmriprep/tests/utils.py
+++ b/nilearn/interfaces/fmriprep/tests/utils.py
@@ -10,6 +10,8 @@
         "_space-MNI152NLin2009cAsym_desc-smoothAROMAnonaggr_bold.nii.gz",
     "regular":
         "_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz",
+    "native":
+        "_desc-preproc_bold.nii.gz",
     "cifti":
         "_space-fsLR_den-91k_bold.dtseries.nii",
     "gifti": (
@@ -45,7 +47,7 @@ def get_testdata_path(non_steady_state=True):
 def create_tmp_filepath(
     base_path,
     image_type="regular",
-    suffix="test",
+    suffix="sub-test01_task-test",
     copy_confounds=False,
     copy_json=False,
     old_derivative_suffix=False