Repository: nilearn/nilearn

Issue:
Improve Issue Templates
As discussed in last meeting:

TODO:

- [x] Fix labels in current issue templates which are not added automatically
- [ ] Improve existing templates
- [x] Add new templates? (Documentation Improvement, Question, others...?)

Comments:

Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 89db404bd..f62478c2d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -6,21 +6,20 @@ labels: 'Bug'
 assignees: ''
 
 ---
+<!--Provide a brief description of the bug.-->
 
-<!--
 
-Include this information:
--------------------------
-What version of Nilearn are you using?
-What were you trying to do?
-What did you expect will happen?
-What actually happened?
+<!--Please fill in the following information, to the best of your ability.-->
+Nilearn version:
 
-List the steps you performed that revealed the bug to you.
-Include any code samples. Enclose them in triple back-ticks (```)
-Like this:
+### Expected behavior
+
+
+### Actual behavior
+
+
+### Steps and code to reproduce bug
+
+```python
 
 ```
-<code>
-``` 
--->
diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md
index 5e8bd7e6f..97c126845 100644
--- a/.github/ISSUE_TEMPLATE/documentation.md
+++ b/.github/ISSUE_TEMPLATE/documentation.md
@@ -5,8 +5,7 @@ title: ''
 labels: Documentation
 assignees: ''
 ---
-
-
-#### Proposed documentation enhancement
 <!--Describe your proposed enhancement in detail.-->
 
+<!--List any pages that would be impacted by the enhancement.-->
+### Affected pages
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index bd5fbf55f..3e98ff321 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -6,12 +6,15 @@ labels: 'Enhancement'
 assignees: ''
 
 ---
+<!--Provide a brief description what you would like changes and why.-->
 
-<!-- 
-Include the following:
-------------------------
-What would you like changed/added and why?
-What would be the benefit? Does the change make something easier to use?
-Clarifies something?
-If it is a new feature, what is the benefit? 
--->
+
+<!--Please fill in the following information, to the best of your ability.-->
+### Benefits to the change
+
+
+### Pseudocode for the new behavior, if applicable
+
+```python
+
+```
diff --git a/doc/whats_new.rst b/doc/whats_new.rst
index 18cc7ae5f..08a50a88c 100644
--- a/doc/whats_new.rst
+++ b/doc/whats_new.rst
@@ -18,6 +18,9 @@ NEW
 Fixes
 -----
 
+- Fix altered, non-zero baseline in design matrices where multiple events in the same condition
+  end at the same time (https://github.com/nilearn/nilearn/issues/2674).
+
 - Fix testing issues on ARM machine.
 
 Enhancements
diff --git a/nilearn/surface/__init__.py b/nilearn/surface/__init__.py
index d367acbd4..4dfdc2ea0 100644
--- a/nilearn/surface/__init__.py
+++ b/nilearn/surface/__init__.py
@@ -3,7 +3,10 @@ Functions for surface manipulation.
 """
 
 from .surface import (vol_to_surf, load_surf_data,
-                      load_surf_mesh, check_mesh_and_data, Mesh)
+                      load_surf_mesh, load_surface,
+                      check_mesh_and_data, check_surface,
+                      Mesh, Surface)
 
 __all__ = ['vol_to_surf', 'load_surf_data', 'load_surf_mesh',
-           'check_mesh_and_data', 'Mesh']
+           'load_surface', 'check_surface', 'check_mesh_and_data',
+           'Mesh', 'Surface']
diff --git a/nilearn/surface/surface.py b/nilearn/surface/surface.py
index 9f7cda88e..f799c0dce 100644
--- a/nilearn/surface/surface.py
+++ b/nilearn/surface/surface.py
@@ -33,6 +33,8 @@ from nilearn.image import get_data
 # Create a namedtuple object for meshes
 Mesh = namedtuple("mesh", ["coordinates", "faces"])
 
+# Create a namedtuple object for surfaces
+Surface = namedtuple("surface", ["mesh", "data"])
 
 def _uniform_ball_cloud(n_points=20, dim=3, n_monte_carlo=50000):
     """Get points uniformly spaced in the unit ball."""
@@ -895,6 +897,55 @@ def load_surf_mesh(surf_mesh):
     return mesh
 
 
+def load_surface(surface):
+    """Loads a surface.
+
+    Parameters
+    ----------
+    surface : Surface-like (see description)
+        The surface to be loaded.
+        A surface can be:
+            - a nilearn.surface.Surface
+            - a sequence (mesh, data) where:
+                - mesh can be:
+                    - a nilearn.surface.Mesh
+                    - a path to .gii or .gii.gz etc.
+                    - a sequence of two numpy arrays,
+                    the first containing vertex coordinates
+                    and the second containing triangles.
+                - data can be:
+                    - a path to .gii or .gii.gz etc.
+                    - a numpy array with shape (n_vertices,)
+                    or (n_time_points, n_vertices)
+
+    Returns
+    --------
+    surface : Surface
+        With the fields "mesh" (Mesh object) and "data" (numpy.ndarray).
+
+    """
+    # Handle the case where we received a Surface
+    # object with mesh and data attributes
+    if hasattr(surface, "mesh") and hasattr(surface, "data"):
+        mesh = load_surf_mesh(surface.mesh)
+        data = load_surf_data(surface.data)
+    # Handle the case where we received a sequence
+    # (mesh, data)
+    elif isinstance(surface, (list, tuple, np.ndarray)):
+        if len(surface) == 2:
+            mesh = load_surf_mesh(surface[0])
+            data = load_surf_data(surface[1])
+        else:
+            raise ValueError("`load_surface` accepts iterables "
+                             "of length 2 to define a surface. "
+                             "You provided a {} of length {}.".format(
+                                 type(surface), len(surface)))
+    else:
+        raise ValueError("Wrong parameter `surface` in `load_surface`. "
+                         "Please refer to the documentation for more information.")
+    return Surface(mesh, data)
+
+
 def _check_mesh(mesh):
     """Check that mesh data is either a str, or a dict with sufficient
     entries.
@@ -918,7 +969,32 @@ def _check_mesh(mesh):
 
 
 def check_mesh_and_data(mesh, data):
-    """Load surface mesh and data, check that they have compatible shapes."""
+    """Load surface mesh and data, check that they have compatible shapes.
+
+    Parameters
+    ----------
+    mesh : str or numpy.ndarray or Mesh
+        Either a file containing surface mesh geometry (valid formats
+        are .gii .gii.gz or Freesurfer specific files such as .orig, .pial,
+        .sphere, .white, .inflated) or two Numpy arrays organized in a list,
+        tuple or a namedtuple with the fields "coordinates" and "faces", or a
+        Mesh object with "coordinates" and "faces" attributes.
+
+    data : str or numpy.ndarray
+        Either a file containing surface data (valid format are .gii,
+        .gii.gz, .mgz, .nii, .nii.gz, or Freesurfer specific files such as
+        .thickness, .curv, .sulc, .annot, .label), lists of 1D data files are
+        returned as 2D arrays, or a Numpy array containing surface data.
+
+    Returns
+    -------
+    mesh : Mesh
+        Checked mesh.
+
+    data : numpy.ndarray
+        Checked data.
+
+    """
     mesh = load_surf_mesh(mesh)
     data = load_surf_data(data)
     # Check that mesh coordinates has a number of nodes
@@ -936,3 +1012,38 @@ def check_mesh_and_data(mesh, data):
             "Maximum face index is {} while coordinates array has length {}.".format(
                 mesh.faces.max(), len(mesh.coordinates)))
     return mesh, data
+
+
+def check_surface(surface):
+    """Load a surface as a Surface object.
+    This function will make sure that the surfaces's
+    mesh and data have compatible shapes.
+
+    Parameters
+    ----------
+    surface : Surface-like (see description)
+        The surface to be loaded.
+        A surface can be:
+            - a nilearn.surface.Surface
+            - a sequence (mesh, data) where:
+                - mesh can be:
+                    - a nilearn.surface.Mesh
+                    - a path to .gii or .gii.gz etc.
+                    - a sequence of two numpy arrays,
+                    the first containing vertex coordinates
+                    and the second containing triangles.
+                - data can be:
+                    - a path to .gii or .gii.gz etc.
+                    - a numpy array with shape (n_vertices,)
+                    or (n_time_points, n_vertices)
+
+    Returns
+    -------
+    surface : Surface
+        Checked surface object.
+
+    """
+    surface = load_surface(surface)
+    mesh, data = check_mesh_and_data(surface.mesh,
+                                     surface.data)
+    return Surface(mesh, data)


Test patch:
diff --git a/nilearn/datasets/tests/test_neurovault.py b/nilearn/datasets/tests/test_neurovault.py
index 74cccbbe3..53c907a8e 100644
--- a/nilearn/datasets/tests/test_neurovault.py
+++ b/nilearn/datasets/tests/test_neurovault.py
@@ -431,7 +431,7 @@ def test_result_filter():
     filter_0 = neurovault.ResultFilter(query_terms={'a': 0},
                                        callable_filter=lambda d: len(d) < 5,
                                        b=1)
-    assert np.unicode(filter_0) == u'ResultFilter'
+    assert str(filter_0) == 'ResultFilter'
     assert filter_0['a'] == 0
     assert filter_0({'a': 0, 'b': 1, 'c': 2})
     assert not filter_0({'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4})
diff --git a/nilearn/decomposition/tests/test_canica.py b/nilearn/decomposition/tests/test_canica.py
index ce2b58fa6..07c5e59a2 100644
--- a/nilearn/decomposition/tests/test_canica.py
+++ b/nilearn/decomposition/tests/test_canica.py
@@ -158,8 +158,13 @@ def test_percentile_range():
     canica = CanICA(n_components=edge_case, threshold=float(edge_case))
     with warnings.catch_warnings(record=True) as warning:
         canica.fit(data)
-        assert len(warning) == 1  # ensure single warning
-        assert "critical threshold" in str(warning[-1].message)
+        # Filter out deprecation warnings
+        not_deprecation_warning = [not issubclass(w.category,
+                                                  DeprecationWarning)
+                                        for w in warning]
+        assert sum(not_deprecation_warning) == 1  # ensure single warning
+        idx_critical_warning = not_deprecation_warning.index(True)
+        assert "critical threshold" in str(warning[idx_critical_warning].message)
 
 
 def test_masker_attributes_with_fit():
diff --git a/nilearn/input_data/tests/test_nifti_maps_masker.py b/nilearn/input_data/tests/test_nifti_maps_masker.py
index 34dd02765..cb142c9f8 100644
--- a/nilearn/input_data/tests/test_nifti_maps_masker.py
+++ b/nilearn/input_data/tests/test_nifti_maps_masker.py
@@ -132,7 +132,8 @@ def test_nifti_maps_masker_with_nans():
 
     # nans
     maps_data = get_data(maps_img)
-    mask_data = get_data(mask_img)
+    mask_data = np.array(get_data(mask_img),
+                         dtype=np.float64)
 
     maps_data[:, 9, 9] = np.nan
     maps_data[:, 5, 5] = np.inf
diff --git a/nilearn/surface/tests/test_surface.py b/nilearn/surface/tests/test_surface.py
index 1c5879bc2..8d0c15d7f 100644
--- a/nilearn/surface/tests/test_surface.py
+++ b/nilearn/surface/tests/test_surface.py
@@ -4,6 +4,7 @@ import os
 import tempfile
 import warnings
 
+from collections import namedtuple
 from distutils.version import LooseVersion
 
 import nibabel as nb
@@ -20,7 +21,7 @@ from nilearn import datasets
 from nilearn import image
 from nilearn.image import resampling
 from nilearn.image.tests.test_resampling import rotation
-from nilearn.surface import Mesh
+from nilearn.surface import Mesh, Surface
 from nilearn.surface import surface
 from nilearn.surface import load_surf_data, load_surf_mesh, vol_to_surf
 from nilearn.surface.surface import (_gifti_img_to_mesh,
@@ -47,6 +48,23 @@ class MeshLikeObject(object):
     def faces(self):
         return self._faces
 
+class SurfaceLikeObject(object):
+    """Class with attributes mesh and
+    data to be used for testing purposes.
+    """
+    def __init__(self, mesh, data):
+        self._mesh = mesh
+        self._data = data
+    @classmethod
+    def fromarrays(cls, coordinates, faces, data):
+        return cls(MeshLikeObject(coordinates, faces), data)
+    @property
+    def mesh(self):
+        return self._mesh
+    @property
+    def data(self):
+        return self._data
+
 
 def test_load_surf_data_array():
     # test loading and squeezing data from numpy array
@@ -182,6 +200,39 @@ def test_load_surf_mesh():
     assert_array_equal(mesh_like.faces, loaded_mesh.faces)
 
 
+def test_load_surface():
+    coords, faces = generate_surf()
+    mesh = Mesh(coords, faces)
+    data = mesh[0][:,0]
+    surf = Surface(mesh, data)
+    surf_like_obj = SurfaceLikeObject(mesh, data)
+    # Load the surface from:
+    #   - Surface-like objects having the right attributes
+    #   - a list of length 2 (mesh, data)
+    for loadings in [surf,
+                     surf_like_obj,
+                     [mesh, data]]:
+        s = surface.load_surface(loadings)
+        assert_array_equal(s.data, data)
+        assert_array_equal(s.data, surf.data)
+        assert_array_equal(s.mesh.coordinates, coords)
+        assert_array_equal(s.mesh.coordinates, surf.mesh.coordinates)
+        assert_array_equal(s.mesh.faces, surf.mesh.faces)
+    # Giving an iterable of length other than 2 will raise an error
+    # Length 3
+    with pytest.raises(ValueError,
+                       match="`load_surface` accepts iterables of length 2"):
+        s = surface.load_surface([coords, faces, data])
+    # Length 1
+    with pytest.raises(ValueError,
+                       match="`load_surface` accepts iterables of length 2"):
+        s = surface.load_surface([coords])
+    # Giving other objects will raise an error
+    with pytest.raises(ValueError,
+                       match="Wrong parameter `surface` in `load_surface`"):
+        s = surface.load_surface("foo")
+
+
 def test_load_surf_mesh_list():
     # test if correct list is returned
     mesh = generate_surf()
@@ -661,9 +712,41 @@ def test_check_mesh_and_data():
     wrong_faces = rng.randint(coords.shape[0] + 1, size=(30, 3))
     wrong_mesh = Mesh(coords, wrong_faces)
     # Check that check_mesh_and_data raises an error with the resulting wrong mesh
-    with pytest.raises(ValueError, match="Mismatch between the indices of faces and the number of nodes."):
+    with pytest.raises(ValueError,
+                       match="Mismatch between the indices of faces and the number of nodes."):
         surface.check_mesh_and_data(wrong_mesh, data)
     # Alter the data and check that an error is raised
     data = mesh[0][::2, 0]
-    with pytest.raises(ValueError, match="Mismatch between number of nodes in mesh"):
+    with pytest.raises(ValueError,
+                       match="Mismatch between number of nodes in mesh"):
         surface.check_mesh_and_data(mesh, data)
+
+
+def test_check_surface():
+    coords, faces = generate_surf()
+    mesh = Mesh(coords, faces)
+    data = mesh[0][:,0]
+    surf = Surface(mesh, data)
+    s = surface.check_surface(surf)
+    assert_array_equal(s.data, data)
+    assert_array_equal(s.data, surf.data)
+    assert_array_equal(s.mesh.coordinates, coords)
+    assert_array_equal(s.mesh.coordinates, mesh.coordinates)
+    assert_array_equal(s.mesh.faces, faces)
+    assert_array_equal(s.mesh.faces, mesh.faces)
+    # Generate faces such that max index is larger than
+    # the length of coordinates array.
+    rng = np.random.RandomState(42)
+    wrong_faces = rng.randint(coords.shape[0] + 1, size=(30, 3))
+    wrong_mesh = Mesh(coords, wrong_faces)
+    wrong_surface = Surface(wrong_mesh, data)
+    # Check that check_mesh_and_data raises an error with the resulting wrong mesh
+    with pytest.raises(ValueError,
+                       match="Mismatch between the indices of faces and the number of nodes."):
+        surface.check_surface(wrong_surface)
+    # Alter the data and check that an error is raised
+    wrong_data = mesh[0][::2, 0]
+    wrong_surface = Surface(mesh, wrong_data)
+    with pytest.raises(ValueError,
+                       match="Mismatch between number of nodes in mesh"):
+        surface.check_surface(wrong_surface)