Repository: nilearn/nilearn

Issue:
`FirstLevelModel._get_voxelwise_model_attribute` only returns first design matrix's attribute
<!--Provide a brief description of the bug.-->
The FirstLevelModel attributes which use `_get_voxelwise_model_attribute()` only return the img for the first design matrix, rather than all of the design matrices' associated imgs.

<!--Please fill in the following information, to the best of your ability.-->
Nilearn version: ~0.7.1 (`master` at c4839dd)

### Expected behavior

Accessing one of the voxelwise attributes which rely on `FirstLevelModel._get_voxelwise_model_attribute()`, such as `FirstLevelModel.residuals`, `FirstLevelModel.predicted`, or `FirstLevelModel.r_square`, should return a list of Nifti1Image objects with the same length as `FirstLevelModel.design_matrices_`.

### Actual behavior

The attributes are lists with only one item.

### The associated code

https://github.com/nilearn/nilearn/blob/c4839ddfe68ddf15775def1fc0ce9ea23544a527/nilearn/glm/first_level/first_level.py#L668-L686

### The solution

Unindenting line 686 should fix it, I think. There should also be at least one test to make sure that the length of the attribute list is the same as the length of `model.design_matrices`.


Discussion:
Thx, good catch !
Currently working on this.

Resolving patch:
diff --git a/nilearn/glm/first_level/first_level.py b/nilearn/glm/first_level/first_level.py
--- a/nilearn/glm/first_level/first_level.py
+++ b/nilearn/glm/first_level/first_level.py
@@ -683,7 +683,7 @@ def _get_voxelwise_model_attribute(self, attribute,
 
             output.append(self.masker_.inverse_transform(voxelwise_attribute))
 
-            return output
+        return output
 
     @auto_attr
     def residuals(self):


Test patch:
diff --git a/nilearn/glm/tests/test_first_level.py b/nilearn/glm/tests/test_first_level.py
--- a/nilearn/glm/tests/test_first_level.py
+++ b/nilearn/glm/tests/test_first_level.py
@@ -286,6 +286,7 @@ def test_compute_contrast_num_contrasts():
     with pytest.warns(UserWarning, match='One contrast given, assuming it for all 3 runs'):
         multi_session_model.compute_contrast([np.eye(rk)[1]])
 
+
 def test_run_glm():
     rng = np.random.RandomState(42)
     n, p, q = 100, 80, 10
@@ -703,6 +704,24 @@ def test_first_level_residuals():
     assert_array_almost_equal(mean_residuals, 0)
 
 
+@pytest.mark.parametrize("shapes", [
+    [(10, 10, 10, 25)],
+    [(10, 10, 10, 25), (10, 10, 10, 100)],
+])
+def test_get_voxelwise_attributes_should_return_as_many_as_design_matrices(shapes):
+    mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(shapes)
+
+    for i in range(len(design_matrices)):
+        design_matrices[i].iloc[:, 0] = 1
+
+    model = FirstLevelModel(mask_img=mask, minimize_memory=False,
+                            noise_model='ols')
+    model.fit(fmri_data, design_matrices=design_matrices)
+
+    # Check that length of outputs is the same as the number of design matrices
+    assert len(model._get_voxelwise_model_attribute("resid", True)) == len(shapes)
+
+
 def test_first_level_predictions_r_square():
     shapes, rk = [(10, 10, 10, 25)], 3
     mask, fmri_data, design_matrices = generate_fake_fmri_data_and_design(shapes, rk)
@@ -726,4 +745,4 @@ def test_first_level_predictions_r_square():
     assert_almost_equal(np.mean(y_predicted - y_measured), 0)
 
     r_square_2d = model.masker_.transform(r_square_3d)
-    assert_array_less(0., r_square_2d)
\ No newline at end of file
+    assert_array_less(0., r_square_2d)