Repository: pyvista/pyvista

Issue:
Multi-version followup
Migrate the default branch on https://github.com/pyvista/pyvista-docs from `gh-pages-multiversion` to `gh-pages`, since that's where our documentation is being deployed to.

I'll commit to moving this during a period of inactivity, probably Friday evening.

If there are any issues, we can always revert back to our working branch. `gh-pages-multiversion` was always meant as a temporary branch to verify that multi-version documentation works.

Discussion:
akaszynski: Wait to merge until I fix the TODO and the `gh-pages` branch please.

Patch:
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 438ec8e8..1b9252cb 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -151,7 +151,7 @@ jobs:
         uses: pyansys/actions/doc-deploy-dev@v4
         with:
           token: ${{ secrets.PYVISTA_BOT_TOKEN }}
-          decompress_artifact: true
+          decompress-artifact: true
           repository: 'pyvista/pyvista-docs'
           doc-artifact-name: docs-build
           cname: ${{ env.DOCUMENTATION_CNAME }}
@@ -161,7 +161,7 @@ jobs:
         uses: pyansys/actions/doc-deploy-stable@v4
         with:
           token: ${{ secrets.PYVISTA_BOT_TOKEN }}
-          decompress_artifact: true
+          decompress-artifact: true
           repository: 'pyvista/pyvista-docs'
           doc-artifact-name: docs-build
           cname: ${{ env.DOCUMENTATION_CNAME }}
diff --git a/doc/Makefile b/doc/Makefile
index 247dbf42..0cd6a01d 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -74,7 +74,7 @@ update-intersphinx:
 
 # nitpicky linkcheck
 linkcheck:
-	@$(SPHINXBUILD) -b linkcheck -D nitpicky=1 -d _build/doctrees . _build/linkcheck
+	@$(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" -D nitpicky=1 -d _build/doctrees . _build/linkcheck
 
 linkcheck-grep:
 	@! grep -h "^.*:.*: \[\(\(local\)\|\(broken\)\)\]" _build/linkcheck/output.txt
diff --git a/doc/source/api/utilities/geometric.rst b/doc/source/api/utilities/geometric.rst
index 567b36ef..63185562 100644
--- a/doc/source/api/utilities/geometric.rst
+++ b/doc/source/api/utilities/geometric.rst
@@ -21,9 +21,9 @@ additional details see :ref:`ref_geometric_example` example.
    Disc
    Dodecahedron
    Icosahedron
+   Icosphere
    Line
    MultipleLines
-   Tube
    Octahedron
    Plane
    PlatonicSolid
@@ -36,4 +36,5 @@ additional details see :ref:`ref_geometric_example` example.
    Tetrahedron
    Text3D
    Triangle
+   Tube
    Wavelet
diff --git a/doc/source/conf.py b/doc/source/conf.py
index bdad71cf..77fc9049 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -68,7 +68,7 @@ extensions = [
     "pyvista.ext.plot_directive",
     "sphinx.ext.autodoc",
     "sphinx.ext.autosummary",
-    'sphinx.ext.linkcode',
+    "sphinx.ext.linkcode",  # This adds the button ``[Source]`` to each Python API site by calling ``linkcode_resolve``
     "sphinx.ext.doctest",
     "sphinx.ext.extlinks",
     "sphinx.ext.intersphinx",
diff --git a/examples/99-advanced/openfoam-cooling.py b/examples/99-advanced/openfoam-cooling.py
index e31eccef..5dff8c4e 100644
--- a/examples/99-advanced/openfoam-cooling.py
+++ b/examples/99-advanced/openfoam-cooling.py
@@ -168,6 +168,5 @@ vol = pl.add_volume(
     scalar_bar_args={'title': 'Temperature'},
 )
 vol.prop.interpolation_type = 'linear'
-pl.enable_anti_aliasing('fxaa')  # also try 'ssaa'
 pl.camera.zoom(2)
 pl.show()
diff --git a/pyvista/core/datasetattributes.py b/pyvista/core/datasetattributes.py
index 50c4a10f..48bb3284 100644
--- a/pyvista/core/datasetattributes.py
+++ b/pyvista/core/datasetattributes.py
@@ -1072,7 +1072,12 @@ class DataSetAttributes(_vtk.VTKObjectWrapper):
 
         """
         if self.GetScalars() is not None:
-            return str(self.GetScalars().GetName())
+            name = self.GetScalars().GetName()
+            if name is None:
+                # Getting the keys has the side effect of naming "unnamed" arrays
+                self.keys()
+                name = self.GetScalars().GetName()
+            return str(name)
         return None
 
     @active_scalars_name.setter
diff --git a/pyvista/core/filters/poly_data.py b/pyvista/core/filters/poly_data.py
index d25aa8b3..6b28c8bf 100644
--- a/pyvista/core/filters/poly_data.py
+++ b/pyvista/core/filters/poly_data.py
@@ -1199,6 +1199,7 @@ class PolyDataFilters(DataSetFilters):
             )
 
         # Subdivide
+        sfilter.SetCheckForTriangles(False)  # we already check for this
         sfilter.SetNumberOfSubdivisions(nsub)
         sfilter.SetInputData(self)
         _update_alg(sfilter, progress_bar, 'Subdividing Mesh')
diff --git a/pyvista/core/filters/rectilinear_grid.py b/pyvista/core/filters/rectilinear_grid.py
index 80a54e5e..97800de2 100644
--- a/pyvista/core/filters/rectilinear_grid.py
+++ b/pyvista/core/filters/rectilinear_grid.py
@@ -17,7 +17,8 @@ class RectilinearGridFilters:
         self,
         tetra_per_cell: int = 5,
         mixed: Union[Sequence[int], bool] = False,
-        pass_cell_ids: bool = False,
+        pass_cell_ids: bool = True,
+        pass_cell_data: bool = True,
         progress_bar: bool = False,
     ):
         """Create a tetrahedral mesh structured grid.
@@ -39,10 +40,17 @@ class RectilinearGridFilters:
             string uses a cell array rather than the active array to determine
             the number of tetrahedra to generate per cell.
 
-        pass_cell_ids : bool, default: False
+        pass_cell_ids : bool, default: True
             Set to ``True`` to make the tetrahedra have scalar data indicating
             which cell they came from in the original
-            :class:`pyvista.RectilinearGrid`.
+            :class:`pyvista.RectilinearGrid`. The name of this array is
+            ``'vtkOriginalCellIds'`` within the ``cell_data``.
+
+        pass_cell_data : bool, default: True
+            Set to ``True`` to make the tetrahedra mesh have the cell data from
+            the original :class:`pyvista.RectilinearGrid`.  This uses
+            ``pass_cell_ids=True`` internally. If ``True``, ``pass_cell_ids``
+            will also be set to ``True``.
 
         progress_bar : bool, default: False
             Display a progress bar to indicate progress.
@@ -80,7 +88,7 @@ class RectilinearGridFilters:
 
         """
         alg = _vtk.vtkRectilinearGridToTetrahedra()
-        alg.SetRememberVoxelId(pass_cell_ids)
+        alg.SetRememberVoxelId(pass_cell_ids or pass_cell_data)
         if mixed is not False:
             if isinstance(mixed, str):
                 self.cell_data.active_scalars_name = mixed
@@ -107,4 +115,17 @@ class RectilinearGridFilters:
 
         alg.SetInputData(self)
         _update_alg(alg, progress_bar, 'Converting to tetrahedra')
-        return _get_output(alg)
+        out = _get_output(alg)
+        if pass_cell_data:
+            # algorithm stores original cell ids in active scalars
+            for name in self.cell_data:  # type: ignore
+                if name != out.cell_data.active_scalars_name:
+                    out[name] = self.cell_data[name][out.cell_data.active_scalars]  # type: ignore
+
+        if alg.GetRememberVoxelId():
+            # original cell_ids are not named and are the active scalars
+            out.cell_data.set_array(
+                out.cell_data.pop(out.cell_data.active_scalars_name), 'vtkOriginalCellIds'
+            )
+
+        return out
diff --git a/pyvista/core/grid.py b/pyvista/core/grid.py
index 6ce23688..5248dd3f 100644
--- a/pyvista/core/grid.py
+++ b/pyvista/core/grid.py
@@ -135,23 +135,30 @@ class RectilinearGrid(_vtk.vtkRectilinearGrid, Grid, RectilinearGridFilters):
                     self.shallow_copy(args[0])
             elif isinstance(args[0], (str, pathlib.Path)):
                 self._from_file(args[0], **kwargs)
-            elif isinstance(args[0], np.ndarray):
-                self._from_arrays(args[0], None, None, check_duplicates)
+            elif isinstance(args[0], (np.ndarray, Sequence)):
+                self._from_arrays(np.asanyarray(args[0]), None, None, check_duplicates)
             else:
                 raise TypeError(f'Type ({type(args[0])}) not understood by `RectilinearGrid`')
 
         elif len(args) == 3 or len(args) == 2:
-            arg0_is_arr = isinstance(args[0], np.ndarray)
-            arg1_is_arr = isinstance(args[1], np.ndarray)
+            arg0_is_arr = isinstance(args[0], (np.ndarray, Sequence))
+            arg1_is_arr = isinstance(args[1], (np.ndarray, Sequence))
             if len(args) == 3:
-                arg2_is_arr = isinstance(args[2], np.ndarray)
+                arg2_is_arr = isinstance(args[2], (np.ndarray, Sequence))
             else:
                 arg2_is_arr = False
 
             if all([arg0_is_arr, arg1_is_arr, arg2_is_arr]):
-                self._from_arrays(args[0], args[1], args[2], check_duplicates)
+                self._from_arrays(
+                    np.asanyarray(args[0]),
+                    np.asanyarray(args[1]),
+                    np.asanyarray(args[2]),
+                    check_duplicates,
+                )
             elif all([arg0_is_arr, arg1_is_arr]):
-                self._from_arrays(args[0], args[1], None, check_duplicates)
+                self._from_arrays(
+                    np.asanyarray(args[0]), np.asanyarray(args[1]), None, check_duplicates
+                )
             else:
                 raise TypeError("Arguments not understood by `RectilinearGrid`.")
 
diff --git a/pyvista/utilities/docs.py b/pyvista/utilities/docs.py
index 0fc21d87..18fcac71 100644
--- a/pyvista/utilities/docs.py
+++ b/pyvista/utilities/docs.py
@@ -23,6 +23,9 @@ def linkcode_resolve(domain, info):
 
     Notes
     -----
+    This function is used by the `sphinx.ext.linkcode` extension to create the "[Source]"
+    button whose link is edited in this function.
+
     This has been adapted to deal with our "verbose" decorator.
 
     Adapted from mne (mne/utils/docs.py), which was adapted from SciPy (doc/source/conf.py).
diff --git a/pyvista/utilities/geometric_objects.py b/pyvista/utilities/geometric_objects.py
index 61c997d1..5ce775f5 100644
--- a/pyvista/utilities/geometric_objects.py
+++ b/pyvista/utilities/geometric_objects.py
@@ -347,6 +347,10 @@ def Sphere(
     pyvista.PolyData
         Sphere mesh.
 
+    See Also
+    --------
+    pyvista.Icosphere
+
     Examples
     --------
     Create a sphere using default parameters.
@@ -1730,3 +1734,76 @@ def Icosahedron(radius=1.0, center=(0.0, 0.0, 0.0)):
 
     """
     return PlatonicSolid(kind='icosahedron', radius=radius, center=center)
+
+
+def Icosphere(radius=1.0, center=(0.0, 0.0, 0.0), nsub=3):
+    """Create an icosphere.
+
+    An icosphere is a `geodesic polyhedron
+    <https://en.wikipedia.org/wiki/Geodesic_polyhedron>`_, which is a
+    convex polyhedron made from triangles.
+
+    Geodesic polyhedra are constructed by subdividing faces of simpler
+    polyhedra, and then projecting the new vertices onto the surface of
+    a sphere. A geodesic polyhedron has straight edges and flat faces
+    that approximate a sphere,
+
+    Parameters
+    ----------
+    radius : float, default: 1.0
+        Radius of the icosphere.
+
+    center : sequence[float], default: (0.0, 0.0, 0.0)
+        Center of the icosphere.
+
+    nsub : int, default: 3
+        This is the number of times each triangle of the original
+        :func:`pyvista.Icosahedron` is subdivided.
+
+    Returns
+    -------
+    pyvista.PolyData
+        Mesh of the icosphere.
+
+    See Also
+    --------
+    pyvista.Sphere
+
+    Examples
+    --------
+    Create the icosphere and plot it with edges.
+
+    >>> import pyvista as pv
+    >>> icosphere = pv.Icosphere()
+    >>> icosphere.plot(show_edges=True)
+
+    Show how this icosphere was created.
+
+    >>> import numpy as np
+    >>> icosahedron = pv.Icosahedron()
+    >>> icosahedron.clear_data()  # remove extra scalars
+    >>> icosahedron_sub = icosahedron.subdivide(nsub=3)
+    >>> pl = pv.Plotter(shape=(1, 3))
+    >>> _ = pl.add_mesh(icosahedron, show_edges=True)
+    >>> pl.subplot(0, 1)
+    >>> _ = pl.add_mesh(icosahedron_sub, show_edges=True)
+    >>> pl.subplot(0, 2)
+    >>> _ = pl.add_mesh(icosphere, show_edges=True)
+    >>> pl.show()
+
+    Show how the triangles are not uniform in area. This is because the
+    ones farther from the edges from the original triangles have farther
+    to travel to the sphere.
+
+    >>> icosphere = pv.Icosphere(nsub=4)
+    >>> icosphere.compute_cell_sizes().plot(scalars='Area')
+
+    """
+    mesh = Icosahedron()
+    mesh.clear_data()
+    mesh = mesh.subdivide(nsub=nsub)
+
+    # scale to desired radius and translate origin
+    dist = np.linalg.norm(mesh.points, axis=1, keepdims=True)  # distance from origin
+    mesh.points = mesh.points * (radius / dist) + center
+    return mesh
diff --git a/requirements_docs.txt b/requirements_docs.txt
index 1d1cf9d7..592786dc 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -30,9 +30,9 @@ sphinx-toolbox==3.4.0
 sphinxcontrib-asciinema==0.3.7
 sphinxcontrib-websupport==1.2.4
 sympy==1.11.1
-trame==2.3.2
+trame==2.4.0
 trame-client==2.7.5
-trame-server==2.10.0
+trame-server==2.11.0
 trame-vtk==2.4.4
 trimesh==3.21.5
 typed-ast==1.5.4


Test patch:
diff --git a/requirements_test.txt b/requirements_test.txt
index 4e81fb90..f5724987 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -3,7 +3,7 @@
 -r requirements.txt
 cmocean<3.1
 colorcet<3.1.0
-hypothesis<6.72.1
+hypothesis<6.74.1
 imageio-ffmpeg<0.5.0
 ipygany<0.6.0
 ipython<9.0.0
@@ -24,8 +24,8 @@ Sphinx<5.4.0
 sphinx-gallery<0.14.0
 sympy<1.12.0
 tqdm<4.66.0
-trame>=2.2.6,<2.4
+trame>=2.2.6,<2.5
 trame-client>=2.5.1,<2.8
-trame-server>=2.8.0,<2.11
+trame-server>=2.8.0,<2.12
 trame-vtk>=2.4.0,<2.5
 trimesh<3.22.0
diff --git a/tests/filters/test_rectilinear_grid.py b/tests/filters/test_rectilinear_grid.py
index 2019cc0a..ab9eb186 100644
--- a/tests/filters/test_rectilinear_grid.py
+++ b/tests/filters/test_rectilinear_grid.py
@@ -45,3 +45,25 @@ def test_to_tetrahedral_mixed(tiny_rectilinear):
 def test_to_tetrahedral_edge_case():
     with pytest.raises(RuntimeError, match='is 1'):
         pv.UniformGrid(dimensions=(1, 2, 2)).to_tetrahedra(tetra_per_cell=12)
+
+
+def test_to_tetrahedral_pass_cell_ids(tiny_rectilinear):
+    tet_grid = tiny_rectilinear.to_tetrahedra(pass_cell_ids=False, pass_cell_data=False)
+    assert not tet_grid.cell_data
+    tet_grid = tiny_rectilinear.to_tetrahedra(pass_cell_ids=True, pass_cell_data=False)
+    assert 'vtkOriginalCellIds' in tet_grid.cell_data
+    assert np.issubdtype(tet_grid.cell_data['vtkOriginalCellIds'].dtype, np.integer)
+
+
+def test_to_tetrahedral_pass_cell_data(tiny_rectilinear):
+    tiny_rectilinear["cell_data"] = np.ones(tiny_rectilinear.n_cells)
+
+    tet_grid = tiny_rectilinear.to_tetrahedra(pass_cell_ids=False, pass_cell_data=False)
+    assert not tet_grid.cell_data
+
+    tet_grid = tiny_rectilinear.to_tetrahedra(pass_cell_ids=False, pass_cell_data=True)
+    assert tet_grid.cell_data
+    assert "cell_data" in tet_grid.cell_data
+
+    # automatically added
+    assert 'vtkOriginalCellIds' in tet_grid.cell_data
diff --git a/tests/test_geometric_objects.py b/tests/test_geometric_objects.py
index 81b1df71..7fb4ee66 100644
--- a/tests/test_geometric_objects.py
+++ b/tests/test_geometric_objects.py
@@ -449,3 +449,15 @@ def test_icosahedron():
 
     doubled_solid = pyvista.Icosahedron(radius=2 * radius, center=center)
     assert np.isclose(doubled_solid.length, 2 * solid.length)
+
+
+def test_icosphere():
+    center = (1.0, 2.0, 3.0)
+    radius = 2.4
+    nsub = 2
+    icosphere = pyvista.Icosphere(radius=radius, center=center, nsub=nsub)
+    assert np.allclose(icosphere.center, center)
+    assert np.allclose(np.linalg.norm(icosphere.points - icosphere.center, axis=1), radius)
+
+    icosahedron = pyvista.Icosahedron()
+    assert icosahedron.n_faces * 4**nsub == icosphere.n_faces
diff --git a/tests/test_grid.py b/tests/test_grid.py
index 1f3645b9..5197f636 100644
--- a/tests/test_grid.py
+++ b/tests/test_grid.py
@@ -735,6 +735,21 @@ def test_create_rectilinear_grid_from_specs():
     assert grid.n_cells == 9 * 3 * 19
     assert grid.n_points == 10 * 4 * 20
     assert grid.bounds == (-10.0, 8.0, -10.0, 5.0, -10.0, 9.0)
+
+    # with Sequence
+    xrng = [0, 1]
+    yrng = [0, 1, 2]
+    zrng = [0, 1, 2, 3]
+    grid = pyvista.RectilinearGrid(xrng)
+    assert grid.n_cells == 1
+    assert grid.n_points == 2
+    grid = pyvista.RectilinearGrid(xrng, yrng)
+    assert grid.n_cells == 2
+    assert grid.n_points == 6
+    grid = pyvista.RectilinearGrid(xrng, yrng, zrng)
+    assert grid.n_cells == 6
+    assert grid.n_points == 24
+
     # 2D example
     cell_spacings = np.array([1.0, 1.0, 2.0, 2.0, 5.0, 10.0])
     x_coordinates = np.cumsum(cell_spacings)