"""
Test Figure.grdimage
"""
import numpy as np
import pytest
import xarray as xr

from .. import Figure
from ..datasets import load_earth_relief
from ..exceptions import GMTInvalidInput
from ..helpers.testing import check_figures_equal

@pytest.fixture(scope="module", name="grid")
def fixture_grid():
    "Load the grid data from the sample earth_relief file"
    return load_earth_relief(registration="gridline")

@pytest.fixture(scope="module", name="xrgrid")
def fixture_xrgrid():
    """
    Create a sample xarray.DataArray grid for testing
    """
    longitude = np.arange(0, 360, 1)
    latitude = np.arange(-89, 90, 1)
    x = np.sin(np.deg2rad(longitude))
    y = np.linspace(start=0, stop=1, num=179)
    data = y[:, np.newaxis] * x

    return xr.DataArray(
        data,
        coords=[
            ("latitude", latitude, {"units": "degrees_north"}),
            ("longitude", longitude, {"units": "degrees_east"}),
        ],
        attrs={"actual_range": [-1, 1]},
    )

@pytest.mark.mpl_image_compare
def test_grdimage(grid):
    "Plot an image using an xarray grid"
    fig = Figure()
    fig.grdimage(grid, cmap="earth", projection="W0/6i")
    return fig

@pytest.mark.mpl_image_compare
def test_grdimage_slice(grid):
    "Plot an image using an xarray grid that has been sliced"
    grid_ = grid.sel(lat=slice(-30, 30))
    fig = Figure()
    fig.grdimage(grid_, cmap="earth", projection="M6i")
    return fig

@pytest.mark.mpl_image_compare
def test_grdimage_file():
    "Plot an image using file input"
    fig = Figure()
    fig.grdimage(
        "@earth_relief_01d_g",
        cmap="ocean",
        region=[-180, 180, -70, 70],
        projection="W0/10i",
        shading=True,
    )
    return fig

@pytest.mark.xfail(reason="Upstream bug in GMT 6.1.1")
@check_figures_equal()
def test_grdimage_xarray_shading(grid, fig_ref, fig_test):
    """
    Test that shading works well for xarray.
    See https://github.com/GenericMappingTools/pygmt/issues/364
    """
    fig_ref, fig_test = Figure(), Figure()
    kwargs = dict(
        region=[-180, 180, -90, 90],
        frame=True,
        projection="Cyl_stere/6i",
        cmap="geo",
        shading=True,
    )

    fig_ref.grdimage("@earth_relief_01d_g", **kwargs)
    fig_test.grdimage(grid, **kwargs)
    return fig_ref, fig_test

def test_grdimage_fails():
    "Should fail for unrecognized input"
    fig = Figure()
    with pytest.raises(GMTInvalidInput):
        fig.grdimage(np.arange(20).reshape((4, 5)))

@pytest.mark.mpl_image_compare
def test_grdimage_over_dateline(xrgrid):
    """
    Ensure no gaps are plotted over the 180 degree international dateline.
    Specifically checking that `xrgrid.gmt.gtype = 1` sets `GMT_GRID_IS_GEO`,
    and that `xrgrid.gmt.registration = 0` sets `GMT_GRID_NODE_REG`. Note that
    there would be a gap over the dateline if a pixel registered grid is used.
    See also https://github.com/GenericMappingTools/pygmt/issues/375.
    """
    fig = Figure()
    assert xrgrid.gmt.registration == 0  # gridline registration
    xrgrid.gmt.gtype = 1  # geographic coordinate system
    fig.grdimage(grid=xrgrid, region="g", projection="A0/0/1c", V="i")
    return fig

@check_figures_equal()
@pytest.mark.parametrize("lon0", [0, 123, 180])
@pytest.mark.parametrize("proj_type", ["H", "W"])
def test_grdimage_central_meridians(grid, proj_type, lon0):
    """
    Test that plotting a grid with different central meridians (lon0) using
    Hammer (H) and Mollweide (W) projection systems work.
    """
    fig_ref, fig_test = Figure(), Figure()
    fig_ref.grdimage(
        "@earth_relief_01d_g", projection=f"{proj_type}{lon0}/15c", cmap="geo"
    )
    fig_test.grdimage(grid, projection=f"{proj_type}{lon0}/15c", cmap="geo")
    return fig_ref, fig_test

# Cylindrical Equidistant (Q) projections plotted with xarray and NetCDF grids
# are still slightly different with an RMS error of 25, see issue at
# https://github.com/GenericMappingTools/pygmt/issues/390
# TO-DO remove tol=1.5 and pytest.mark.xfail once bug is solved in upstream GMT
@check_figures_equal(tol=1.5)
@pytest.mark.parametrize("lat0", [0, 30])
@pytest.mark.parametrize("lon0", [0, 123, 180])
@pytest.mark.parametrize("proj_type", [pytest.param("Q", marks=pytest.mark.xfail), "S"])
def test_grdimage_central_meridians_and_standard_parallels(grid, proj_type, lon0, lat0):
    """
    Test that plotting a grid with different central meridians (lon0) and
    standard_parallels (lat0) using Cylindrical Equidistant (Q) and General
    Stereographic (S) projection systems work.
    """
    fig_ref, fig_test = Figure(), Figure()
    fig_ref.grdimage(
        "@earth_relief_01d_g", projection=f"{proj_type}{lon0}/{lat0}/15c", cmap="geo"
    )
    fig_test.grdimage(grid, projection=f"{proj_type}{lon0}/{lat0}/15c", cmap="geo")
    return fig_ref, fig_test


#     ______     _____ _           ________
#    / ____/___ / ___/(_)___ ___  /  _/ __ |
#   / /   / __ \\__ \/ / __ `__ \ / // / / /
#  / /___/ /_/ /__/ / / / / / / // // /_/ /
#  \____/\____/____/_/_/ /_/ /_/___/\____/
#  Kratos CoSimulationApplication
#
#  License:         BSD License, see license.txt
#
#  Main authors:    Philipp Bucher (https://github.com/philbucher)
#

# tests for the python exposure of CoSimIO::Vector
# (which is a small python wrapper fro std::vector<double>)

# python imports
import unittest
from abc import ABCMeta, abstractmethod

try:
    import numpy as np
    numpy_available = True
except:
    numpy_available = False

import CoSimIO

class CoSimIO_Vector:
    class BaseTests(unittest.TestCase, metaclass=ABCMeta):
        maxDiff = None # to display all the diff
        @abstractmethod
        def _CreateVector(self, *args): pass

        def test_basics(self):
            vec = self._CreateVector()
            self.assertEqual(vec.size(), 0)
            self.assertEqual(len(vec), 0)

            vec.append(15)
            self.assertEqual(vec.size(), 1)
            self.assertEqual(len(vec), 1)
            self.assertAlmostEqual(vec[0], 15)

            vec.append(-3)
            self.assertEqual(vec.size(), 2)
            self.assertEqual(len(vec), 2)
            self.assertAlmostEqual(vec[0], 15)
            self.assertAlmostEqual(vec[1], -3)

        def test_resize(self):
            vec = self._CreateVector()
            self.assertEqual(vec.size(), 0)

            vec.resize(10)
            self.assertEqual(vec.size(), 10)

            vec.resize(5)
            self.assertEqual(vec.size(), 5)

            vec.resize(23)
            self.assertEqual(vec.size(), 23)

        def test_iterators(self):
            vec = self._CreateVector()
            for i in range(10):
                vec.append(i)

            self.assertEqual(vec.size(), 10)

            counter = 0
            for i, v in enumerate(vec):
                self.assertAlmostEqual(i,v)
                counter += 1
            self.assertEqual(counter, 10)

            # check for after resize to smaller and larger
            counter = 0
            vec.resize(5)
            for i, v in enumerate(vec):
                self.assertAlmostEqual(i,v)
                counter += 1
            self.assertEqual(counter, 5)

            counter = 0
            vec.resize(23)
            for i, v in enumerate(vec):
                counter += 1

            self.assertEqual(counter, 23)

        def test_copy_constructor(self):
            vec = self._CreateVector()
            for i in range(10):
                vec.append(i)

            vec_copy = self._CreateVector(vec)

            self.assertEqual(vec.size(), vec_copy.size())
            for v, v_c in zip(vec, vec_copy):
                self.assertAlmostEqual(v, v_c)

            # make sure that it is a deep and not a shallow copy
            vec[0] = 123456
            self.assertAlmostEqual(vec_copy[0], 0) # copy is unchanged

            vec.resize(5)
            self.assertEqual(vec_copy.size(), 10)

        def test_construction_from_list(self):
            init_list = [1, 2, 3, -4, 17]

            vec = self._CreateVector(init_list)
            self.assertEqual(vec.size(), len(init_list))

            for l, v in zip(init_list, vec):
                self.assertAlmostEqual(l,v)

class CoSimIO_DoubleVector(CoSimIO_Vector.BaseTests):
    def _CreateVector(self, *args):
        return CoSimIO.DoubleVector(*args)

    def test_print(self):
        vec = self._CreateVector([1.2, 2.5, 3.3, -4.1, 17])

        self.assertMultiLineEqual(str(vec), "[1.2, 2.5, 3.3, -4.1, 17]")

    def test_doubles(self):
        # general tests are done with ints, hence explicitly checking with doubles
        vec = self._CreateVector()
        for i in range(10):
            vec.append(i*1.5)

        self.assertEqual(vec.size(), 10)

        for i in range(10):
            self.assertAlmostEqual(vec[i], i*1.5)

    @unittest.skipUnless(numpy_available, "this test requries numpy")
    def test_construction_from_numpy_array(self):
        arr = np.array([1.1,2.2,-3.2,4,5,6.78,7,8.78,9], dtype=np.double)

        vec = self._CreateVector(arr)
        self.assertEqual(vec.size(), arr.size)

        for a, v in zip(arr, vec):
            self.assertAlmostEqual(a,v)

class CoSimIO_IntVector(CoSimIO_Vector.BaseTests):
    def _CreateVector(self, *args):
        return CoSimIO.IntVector(*args)

    def test_print(self):
        vec = self._CreateVector([1, 2, 3, -4, 17])

        self.assertMultiLineEqual(str(vec), "[1, 2, 3, -4, 17]")

    @unittest.skipUnless(numpy_available, "this test requries numpy")
    def test_construction_from_numpy_array(self):
        arr = np.array([10,2,-3,4,5,6,7,8,9], dtype=np.intc)

        vec = self._CreateVector(arr)
        self.assertEqual(vec.size(), arr.size)

        for a, v in zip(arr, vec):
            self.assertEqual(a,v)

if __name__ == '__main__':
    unittest.main()


# sage.doctest: optional - sage.graphs
r"""
Base class for polyhedra, part 4

Define methods relying on :mod:`sage.graphs`.
"""

# ****************************************************************************
#       Copyright (C) 2008-2012 Marshall Hampton <hamptonio@gmail.com>
#       Copyright (C) 2011-2015 Volker Braun <vbraun.name@gmail.com>
#       Copyright (C) 2012-2018 Frederic Chapoton
#       Copyright (C) 2013      Andrey Novoseltsev
#       Copyright (C) 2014-2017 Moritz Firsching
#       Copyright (C) 2014-2019 Thierry Monteil
#       Copyright (C) 2015      Nathann Cohen
#       Copyright (C) 2015-2017 Jeroen Demeyer
#       Copyright (C) 2015-2017 Vincent Delecroix
#       Copyright (C) 2015-2018 Dima Pasechnik
#       Copyright (C) 2015-2020 Jean-Philippe Labbe <labbe at math.huji.ac.il>
#       Copyright (C) 2015-2021 Matthias Koeppe
#       Copyright (C) 2016-2019 Daniel Krenn
#       Copyright (C) 2017      Marcelo Forets
#       Copyright (C) 2017-2018 Mark Bell
#       Copyright (C) 2019      Julian Ritter
#       Copyright (C) 2019-2020 Laith Rastanawi
#       Copyright (C) 2019-2020 Sophia Elia
#       Copyright (C) 2019-2021 Jonathan Kliem <jonathan.kliem@fu-berlin.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#                  https://www.gnu.org/licenses/
# ****************************************************************************

from sage.misc.cachefunc import cached_method
from .base3 import Polyhedron_base3

class Polyhedron_base4(Polyhedron_base3):
    """
    Methods relying on :mod:`sage.graphs`.

    See :class:`sage.geometry.polyhedron.base.Polyhedron_base`.

    TESTS::

        sage: from sage.geometry.polyhedron.base4 import Polyhedron_base4
        sage: P = polytopes.cube()
        sage: Polyhedron_base4.vertex_facet_graph.f(P)
        Digraph on 14 vertices
        sage: Polyhedron_base4.vertex_graph(P)
        Graph on 8 vertices
        sage: Polyhedron_base4.face_lattice(P)
        Finite lattice containing 28 elements
        sage: Polyhedron_base4.flag_f_vector(P, 0, 2)
        24
        sage: Polyhedron_base4.is_self_dual(P)
        False
        sage: Q = polytopes.cube(intervals='zero_one')
        sage: P == Q
        False
        sage: Polyhedron_base4.is_combinatorially_isomorphic(P, Q)
        True
    """

    @cached_method
    def vertex_facet_graph(self, labels=True):
        r"""
        Return the vertex-facet graph.

        This function constructs a directed bipartite graph.
        The nodes of the graph correspond to the vertices of the polyhedron
        and the facets of the polyhedron. There is an directed edge
        from a vertex to a face if and only if the vertex is incident to the face.

        INPUT:

        - ``labels`` -- boolean (default: ``True``); decide how the nodes
          of the graph are labelled. Either with the original vertices/facets
          of the Polyhedron or with integers.

        OUTPUT:

        - a bipartite DiGraph. If ``labels`` is ``True``, then the nodes
          of the graph will actually be the vertices and facets of ``self``,
          otherwise they will be integers.

        .. SEEALSO::

            :meth:`combinatorial_automorphism_group`,
            :meth:`is_combinatorially_isomorphic`.

        EXAMPLES::

            sage: P = polytopes.cube()
            sage: G = P.vertex_facet_graph(); G
            Digraph on 14 vertices
            sage: G.vertices(key = lambda v: str(v))
            [A vertex at (-1, -1, -1),
             A vertex at (-1, -1, 1),
             A vertex at (-1, 1, -1),
             A vertex at (-1, 1, 1),
             A vertex at (1, -1, -1),
             A vertex at (1, -1, 1),
             A vertex at (1, 1, -1),
             A vertex at (1, 1, 1),
             An inequality (-1, 0, 0) x + 1 >= 0,
             An inequality (0, -1, 0) x + 1 >= 0,
             An inequality (0, 0, -1) x + 1 >= 0,
             An inequality (0, 0, 1) x + 1 >= 0,
             An inequality (0, 1, 0) x + 1 >= 0,
             An inequality (1, 0, 0) x + 1 >= 0]
            sage: G.automorphism_group().is_isomorphic(P.hasse_diagram().automorphism_group())
            True
            sage: O = polytopes.octahedron(); O
            A 3-dimensional polyhedron in ZZ^3 defined as the convex hull of 6 vertices
            sage: O.vertex_facet_graph()
            Digraph on 14 vertices
            sage: H = O.vertex_facet_graph()
            sage: G.is_isomorphic(H)
            False
            sage: G2 = copy(G)
            sage: G2.reverse_edges(G2.edges())
            sage: G2.is_isomorphic(H)
            True

        TESTS:

        Check that :trac:`28828` is fixed::

            sage: G._immutable
            True

        Check that :trac:`29188` is fixed::

            sage: P = polytopes.cube()
            sage: P.vertex_facet_graph().is_isomorphic(P.vertex_facet_graph(False))
            True
        """
        return self.combinatorial_polyhedron().vertex_facet_graph(names=labels)

    def vertex_graph(self, **kwds):
        """
        Return a graph in which the vertices correspond to vertices
        of the polyhedron, and edges to edges.

        INPUT:

        - ``names`` -- boolean (default: ``True``); if ``False``,
          then the nodes of the graph are labeld by the
          indices of the Vrepresentation

        - ``algorithm`` -- string (optional);
          specify whether the face generator starts with facets or vertices:
          * ``'primal'`` -- start with the facets
          * ``'dual'`` -- start with the vertices
          * ``None`` -- choose automatically

        ..NOTE::

            The graph of a polyhedron with lines has no vertices,
            as the polyhedron has no vertices (`0`-faces).

            The method :meth:`Polyhedron_base:vertices` returns
            the defining points in this case.

        EXAMPLES::

            sage: g3 = polytopes.hypercube(3).vertex_graph(); g3
            Graph on 8 vertices
            sage: g3.automorphism_group().cardinality()
            48
            sage: s4 = polytopes.simplex(4).vertex_graph(); s4
            Graph on 5 vertices
            sage: s4.is_eulerian()
            True

        The graph of an unbounded polyhedron
        is the graph of the bounded complex::

            sage: open_triangle = Polyhedron(vertices=[[1,0], [0,1]],
            ....:                            rays    =[[1,1]])
            sage: open_triangle.vertex_graph()
            Graph on 2 vertices

        The graph of a polyhedron with lines has no vertices::

            sage: line = Polyhedron(lines=[[0,1]])
            sage: line.vertex_graph()
            Graph on 0 vertices

        TESTS:

        Check for a line segment (:trac:`30545`)::

            sage: polytopes.simplex(1).graph().edges()
            [(A vertex at (0, 1), A vertex at (1, 0), None)]
        """
        return self.combinatorial_polyhedron().vertex_graph(**kwds)

    graph = vertex_graph

    def vertex_digraph(self, f, increasing=True):
        r"""
        Return the directed graph of the polyhedron according to a linear form.

        The underlying undirected graph is the graph of vertices and edges.

        INPUT:

        - ``f`` -- a linear form. The linear form can be provided as:

            - a vector space morphism with one-dimensional codomain, (see
              :meth:`sage.modules.vector_space_morphism.linear_transformation`
              and
              :class:`sage.modules.vector_space_morphism.VectorSpaceMorphism`)
            - a vector ; in this case the linear form is obtained by duality
              using the dot product: ``f(v) = v.dot_product(f)``.

        - ``increasing`` -- boolean (default ``True``) whether to orient
          edges in the increasing or decreasing direction.

        By default, an edge is oriented from `v` to `w` if
        `f(v) \leq f(w)`.

        If `f(v)=f(w)`, then two opposite edges are created.

        EXAMPLES::

            sage: penta = Polyhedron([[0,0],[1,0],[0,1],[1,2],[3,2]])
            sage: G = penta.vertex_digraph(vector([1,1])); G
            Digraph on 5 vertices
            sage: G.sinks()
            [A vertex at (3, 2)]

            sage: A = matrix(ZZ, [[1], [-1]])
            sage: f = linear_transformation(A)
            sage: G = penta.vertex_digraph(f) ; G
            Digraph on 5 vertices
            sage: G.is_directed_acyclic()
            False

        .. SEEALSO::

            :meth:`vertex_graph`
        """
        from sage.modules.vector_space_morphism import VectorSpaceMorphism
        if isinstance(f, VectorSpaceMorphism):
            if f.codomain().dimension() == 1:
                orientation_check = lambda v: f(v) >= 0
            else:
                raise TypeError('the linear map f must have '
                                'one-dimensional codomain')
        else:
            try:
                if f.is_vector():
                    orientation_check = lambda v: v.dot_product(f) >= 0
                else:
                    raise TypeError('f must be a linear map or a vector')
            except AttributeError:
                raise TypeError('f must be a linear map or a vector')
        if not increasing:
            f = -f
        from sage.graphs.digraph import DiGraph
        dg = DiGraph()
        for j in range(self.n_vertices()):
            vj = self.Vrepresentation(j)
            for vi in vj.neighbors():
                if orientation_check(vj.vector() - vi.vector()):
                    dg.add_edge(vi, vj)
        return dg

    def face_lattice(self):
        """
        Return the face-lattice poset.

        OUTPUT:

        A :class:`~sage.combinat.posets.posets.FinitePoset`. Elements
        are given as
        :class:`~sage.geometry.polyhedron.face.PolyhedronFace`.

        In the case of a full-dimensional polytope, the faces are
        pairs (vertices, inequalities) of the spanning vertices and
        corresponding saturated inequalities. In general, a face is
        defined by a pair (V-rep. objects, H-rep. objects). The
        V-representation objects span the face, and the corresponding
        H-representation objects are those inequalities and equations
        that are saturated on the face.

        The bottom-most element of the face lattice is the "empty
        face". It contains no V-representation object. All
        H-representation objects are incident.

        The top-most element is the "full face". It is spanned by all
        V-representation objects. The incident H-representation
        objects are all equations and no inequalities.

        In the case of a full-dimensional polytope, the "empty face"
        and the "full face" are the empty set (no vertices, all
        inequalities) and the full polytope (all vertices, no
        inequalities), respectively.

        ALGORITHM:

        See :mod:`sage.geometry.polyhedron.combinatorial_polyhedron.face_iterator`.

        .. NOTE::

            The face lattice is not cached, as long as this creates a memory leak, see :trac:`28982`.

        EXAMPLES::

            sage: square = polytopes.hypercube(2)
            sage: fl = square.face_lattice();fl
            Finite lattice containing 10 elements
            sage: list(f.ambient_V_indices() for f in fl)
            [(), (0,), (1,), (0, 1), (2,), (1, 2), (3,), (0, 3), (2, 3), (0, 1, 2, 3)]
            sage: poset_element = fl[5]
            sage: a_face = poset_element
            sage: a_face
            A 1-dimensional face of a Polyhedron in ZZ^2 defined as the convex hull of 2 vertices
            sage: a_face.ambient_V_indices()
            (1, 2)
            sage: set(a_face.ambient_Vrepresentation()) == \
            ....: set([square.Vrepresentation(1), square.Vrepresentation(2)])
            True
            sage: a_face.ambient_Vrepresentation()
            (A vertex at (1, 1), A vertex at (-1, 1))
            sage: a_face.ambient_Hrepresentation()
            (An inequality (0, -1) x + 1 >= 0,)

        A more complicated example::

            sage: c5_10 = Polyhedron(vertices = [[i,i^2,i^3,i^4,i^5] for i in range(1,11)])
            sage: c5_10_fl = c5_10.face_lattice()
            sage: [len(x) for x in c5_10_fl.level_sets()]
            [1, 10, 45, 100, 105, 42, 1]

        Note that if the polyhedron contains lines then there is a
        dimension gap between the empty face and the first non-empty
        face in the face lattice::

            sage: line = Polyhedron(vertices=[(0,)], lines=[(1,)])
            sage: [ fl.dim() for fl in line.face_lattice() ]
            [-1, 1]

        TESTS::

            sage: c5_20 = Polyhedron(vertices = [[i,i^2,i^3,i^4,i^5]
            ....:     for i in range(1,21)])
            sage: c5_20_fl = c5_20.face_lattice() # long time
            sage: [len(x) for x in c5_20_fl.level_sets()] # long time
            [1, 20, 190, 580, 680, 272, 1]
            sage: polytopes.hypercube(2).face_lattice().plot()  # optional - sage.plot
            Graphics object consisting of 27 graphics primitives
            sage: level_sets = polytopes.cross_polytope(2).face_lattice().level_sets()
            sage: level_sets[0][0].ambient_V_indices(), level_sets[-1][0].ambient_V_indices()
            ((), (0, 1, 2, 3))

        Various degenerate polyhedra::

            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(vertices=[[0,0,0],[1,0,0],[0,1,0]]).face_lattice().level_sets()]
            [[()], [(0,), (1,), (2,)], [(0, 1), (0, 2), (1, 2)], [(0, 1, 2)]]
            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(vertices=[(1,0,0),(0,1,0)], rays=[(0,0,1)]).face_lattice().level_sets()]
            [[()], [(1,), (2,)], [(0, 1), (0, 2), (1, 2)], [(0, 1, 2)]]
            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(rays=[(1,0,0),(0,1,0)], vertices=[(0,0,1)]).face_lattice().level_sets()]
            [[()], [(0,)], [(0, 1), (0, 2)], [(0, 1, 2)]]
            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(rays=[(1,0),(0,1)], vertices=[(0,0)]).face_lattice().level_sets()]
            [[()], [(0,)], [(0, 1), (0, 2)], [(0, 1, 2)]]
            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(vertices=[(1,),(0,)]).face_lattice().level_sets()]
            [[()], [(0,), (1,)], [(0, 1)]]
            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(vertices=[(1,0,0),(0,1,0)], lines=[(0,0,1)]).face_lattice().level_sets()]
            [[()], [(0, 1), (0, 2)], [(0, 1, 2)]]
            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(lines=[(1,0,0)], vertices=[(0,0,1)]).face_lattice().level_sets()]
            [[()], [(0, 1)]]
            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(lines=[(1,0),(0,1)], vertices=[(0,0)]).face_lattice().level_sets()]
            [[()], [(0, 1, 2)]]
            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(lines=[(1,0)], rays=[(0,1)], vertices=[(0,0)]).face_lattice().level_sets()]
            [[()], [(0, 1)], [(0, 1, 2)]]
            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(vertices=[(0,)], lines=[(1,)]).face_lattice().level_sets()]
            [[()], [(0, 1)]]
            sage: [[ls.ambient_V_indices() for ls in lss] for lss in Polyhedron(lines=[(1,0)], vertices=[(0,0)]).face_lattice().level_sets()]
            [[()], [(0, 1)]]

        """
        from sage.combinat.posets.lattices import FiniteLatticePoset
        return FiniteLatticePoset(self.hasse_diagram())

    @cached_method
    def hasse_diagram(self):
        r"""
        Return the Hasse diagram of the face lattice of ``self``.

        This is the Hasse diagram of the poset of the faces of ``self``.

        OUTPUT: a directed graph

        EXAMPLES::

            sage: P = polytopes.regular_polygon(4).pyramid()                    # optional - sage.rings.number_field
            sage: D = P.hasse_diagram(); D                                      # optional - sage.rings.number_field
            Digraph on 20 vertices
            sage: D.degree_polynomial()                                         # optional - sage.rings.number_field
            x^5 + x^4*y + x*y^4 + y^5 + 4*x^3*y + 8*x^2*y^2 + 4*x*y^3

        Faces of an mutable polyhedron are not hashable. Hence those are not suitable as
        vertices of the hasse diagram. Use the combinatorial polyhedron instead::

            sage: P = polytopes.regular_polygon(4).pyramid()                    # optional - sage.rings.number_field
            sage: parent = P.parent()                                           # optional - sage.rings.number_field
            sage: parent = parent.change_ring(QQ, backend='ppl')                # optional - sage.rings.number_field
            sage: Q = parent._element_constructor_(P, mutable=True)             # optional - sage.rings.number_field
            sage: Q.hasse_diagram()                                             # optional - sage.rings.number_field
            Traceback (most recent call last):
            ...
            TypeError: mutable polyhedra are unhashable
            sage: C = Q.combinatorial_polyhedron()                              # optional - sage.rings.number_field
            sage: D = C.hasse_diagram()                                         # optional - sage.rings.number_field
            sage: set(D.vertices()) == set(range(20))                           # optional - sage.rings.number_field
            True
            sage: def index_to_combinatorial_face(n):
            ....:     return C.face_by_face_lattice_index(n)
            sage: D.relabel(index_to_combinatorial_face, inplace=True)          # optional - sage.rings.number_field
            sage: D.vertices()                                                  # optional - sage.rings.number_field
            [A -1-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 0-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 0-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 0-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 0-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 0-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 1-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 1-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 1-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 1-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 1-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 1-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 1-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 1-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 2-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 2-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 2-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 2-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 2-dimensional face of a 3-dimensional combinatorial polyhedron,
             A 3-dimensional face of a 3-dimensional combinatorial polyhedron]
            sage: D.degree_polynomial()                                         # optional - sage.rings.number_field
            x^5 + x^4*y + x*y^4 + y^5 + 4*x^3*y + 8*x^2*y^2 + 4*x*y^3
        """

        from sage.geometry.polyhedron.face import combinatorial_face_to_polyhedral_face
        C = self.combinatorial_polyhedron()
        D = C.hasse_diagram()

        def index_to_polyhedron_face(n):
            return combinatorial_face_to_polyhedral_face(
                    self, C.face_by_face_lattice_index(n))

        return D.relabel(index_to_polyhedron_face, inplace=False, immutable=True)

    def flag_f_vector(self, *args):
        r"""
        Return the flag f-vector.

        For each `-1 < i_0 < \dots < i_n < d` the flag f-vector
        counts the number of flags `F_0 \subset \dots \subset F_n`
        with `F_j` of dimension `i_j` for each `0 \leq j \leq n`,
        where `d` is the dimension of the polyhedron.

        INPUT:

        - ``args`` -- integers (optional); specify an entry of the
          flag-f-vector; must be an increasing sequence of integers

        OUTPUT:

        - a dictionary, if no arguments were given

        - an Integer, if arguments were given

        EXAMPLES:

        Obtain the entire flag-f-vector::

            sage: P = polytopes.twenty_four_cell()
            sage: P.flag_f_vector()
                {(-1,): 1,
                 (0,): 24,
                 (0, 1): 192,
                 (0, 1, 2): 576,
                 (0, 1, 2, 3): 1152,
                 (0, 1, 3): 576,
                 (0, 2): 288,
                 (0, 2, 3): 576,
                 (0, 3): 144,
                 (1,): 96,
                 (1, 2): 288,
                 (1, 2, 3): 576,
                 (1, 3): 288,
                 (2,): 96,
                 (2, 3): 192,
                 (3,): 24,
                 (4,): 1}

        Specify an entry::

            sage: P.flag_f_vector(0,3)
            144
            sage: P.flag_f_vector(2)
            96

        Leading ``-1`` and trailing entry of dimension are allowed::

            sage: P.flag_f_vector(-1,0,3)
            144
            sage: P.flag_f_vector(-1,0,3,4)
            144

        One can get the number of trivial faces::

            sage: P.flag_f_vector(-1)
            1
            sage: P.flag_f_vector(4)
            1

        Polyhedra with lines, have ``0`` entries accordingly::

            sage: P = (Polyhedron(lines=[[1]]) * polytopes.cross_polytope(3))
            sage: P.flag_f_vector()
            {(-1,): 1,
             (0, 1): 0,
             (0, 1, 2): 0,
             (0, 1, 3): 0,
             (0, 2): 0,
             (0, 2, 3): 0,
             (0, 3): 0,
             (0,): 0,
             (1, 2): 24,
             (1, 2, 3): 48,
             (1, 3): 24,
             (1,): 6,
             (2, 3): 24,
             (2,): 12,
             (3,): 8,
             4: 1}

        If the arguments are not stricly increasing or out of range, a key error is raised::

            sage: P.flag_f_vector(-1,0,3,6)
            Traceback (most recent call last):
            ...
            KeyError: (0, 3, 6)
            sage: P.flag_f_vector(-1,3,0)
            Traceback (most recent call last):
            ...
            KeyError: (3, 0)
        """
        flag = self._flag_f_vector()
        if len(args) == 0:
            return flag
        elif len(args) == 1:
            return flag[(args[0],)]
        else:
            dim = self.dimension()
            if args[0] == -1:
                args = args[1:]
            if args[-1] == dim:
                args = args[:-1]
            return flag[tuple(args)]

    @cached_method(do_pickle=True)
    def _flag_f_vector(self):
        r"""
        Return the flag-f-vector.

        See :meth:`flag_f_vector`.

        TESTS::

            sage: polytopes.hypercube(4)._flag_f_vector()
            {(-1,): 1,
            (0,): 16,
            (0, 1): 64,
            (0, 1, 2): 192,
            (0, 1, 2, 3): 384,
            (0, 1, 3): 192,
            (0, 2): 96,
            (0, 2, 3): 192,
            (0, 3): 64,
            (1,): 32,
            (1, 2): 96,
            (1, 2, 3): 192,
            (1, 3): 96,
            (2,): 24,
            (2, 3): 48,
            (3,): 8,
            (4,): 1}
        """
        return self.combinatorial_polyhedron()._flag_f_vector()

    @cached_method
    def combinatorial_automorphism_group(self, vertex_graph_only=False):
        """
        Computes the combinatorial automorphism group.

        If ``vertex_graph_only`` is ``True``,  the automorphism group
        of the vertex-edge graph of the polyhedron is returned. Otherwise
        the automorphism group of the vertex-facet graph, which is
        isomorphic to the automorphism group of the face lattice is returned.

        INPUT:

        - ``vertex_graph_only`` -- boolean (default: ``False``); whether
          to return the automorphism group of the vertex edges graph or
          of the lattice

        OUTPUT:

        A
        :class:`PermutationGroup<sage.groups.perm_gps.permgroup.PermutationGroup_generic_with_category'>`
        that is isomorphic to the combinatorial automorphism group is
        returned.

        - if ``vertex_graph_only`` is ``True``:
          The automorphism group of the vertex-edge graph of the polyhedron

        - if ``vertex_graph_only`` is ``False`` (default):
          The automorphism group of the vertex-facet graph of the polyhedron,
          see :meth:`vertex_facet_graph`. This group is isomorphic to the
          automorphism group of the face lattice of the polyhedron.

        NOTE:

            Depending on ``vertex_graph_only``, this method returns groups
            that are not necessarily isomorphic, see the examples below.

        .. SEEALSO::

            :meth:`is_combinatorially_isomorphic`,
            :meth:`graph`,
            :meth:`vertex_facet_graph`.

        EXAMPLES::

            sage: quadrangle = Polyhedron(vertices=[(0,0),(1,0),(0,1),(2,3)])
            sage: quadrangle.combinatorial_automorphism_group().is_isomorphic(groups.permutation.Dihedral(4))
            True
            sage: quadrangle.restricted_automorphism_group()
            Permutation Group with generators [()]

        Permutations of the vertex graph only exchange vertices with vertices::

            sage: P = Polyhedron(vertices=[(1,0), (1,1)], rays=[(1,0)])
            sage: P.combinatorial_automorphism_group(vertex_graph_only=True)
            Permutation Group with generators [(A vertex at (1,0),A vertex at (1,1))]

        This shows an example of two polytopes whose vertex-edge graphs are isomorphic,
        but their face_lattices are not isomorphic::

            sage: Q=Polyhedron([[-123984206864/2768850730773, -101701330976/922950243591, -64154618668/2768850730773, -2748446474675/2768850730773],
            ....: [-11083969050/98314591817, -4717557075/98314591817, -32618537490/98314591817, -91960210208/98314591817],
            ....: [-9690950/554883199, -73651220/554883199, 1823050/554883199, -549885101/554883199], [-5174928/72012097, 5436288/72012097, -37977984/72012097, 60721345/72012097],
            ....: [-19184/902877, 26136/300959, -21472/902877, 899005/902877], [53511524/1167061933, 88410344/1167061933, 621795064/1167061933, 982203941/1167061933],
            ....: [4674489456/83665171433, -4026061312/83665171433, 28596876672/83665171433, -78383796375/83665171433], [857794884940/98972360190089, -10910202223200/98972360190089, 2974263671400/98972360190089, -98320463346111/98972360190089]])
            sage: C = polytopes.cyclic_polytope(4,8)
            sage: C.is_combinatorially_isomorphic(Q)
            False
            sage: C.combinatorial_automorphism_group(vertex_graph_only=True).is_isomorphic(Q.combinatorial_automorphism_group(vertex_graph_only=True))
            True
            sage: C.combinatorial_automorphism_group(vertex_graph_only=False).is_isomorphic(Q.combinatorial_automorphism_group(vertex_graph_only=False))
            False

        The automorphism group of the face lattice is isomorphic to the combinatorial automorphism group::

            sage: CG = C.hasse_diagram().automorphism_group()
            sage: C.combinatorial_automorphism_group().is_isomorphic(CG)
            True
            sage: QG = Q.hasse_diagram().automorphism_group()
            sage: Q.combinatorial_automorphism_group().is_isomorphic(QG)
            True

        """
        if vertex_graph_only:
            G = self.graph()
        else:
            G = self.vertex_facet_graph()
        return G.automorphism_group(edge_labels=True)

    @cached_method
    def restricted_automorphism_group(self, output="abstract"):
        r"""
        Return the restricted automorphism group.

        First, let the linear automorphism group be the subgroup of
        the affine group `AGL(d,\RR) = GL(d,\RR) \ltimes \RR^d`
        preserving the `d`-dimensional polyhedron. The affine group
        acts in the usual way `\vec{x}\mapsto A\vec{x}+b` on the
        ambient space.

        The restricted automorphism group is the subgroup of the linear
        automorphism group generated by permutations of the generators
        of the same type. That is, vertices can only be permuted with
        vertices, ray generators with ray generators, and line
        generators with line generators.

        For example, take the first quadrant

        .. MATH::

            Q = \Big\{ (x,y) \Big| x\geq 0,\; y\geq0 \Big\}
            \subset \QQ^2

        Then the linear automorphism group is

        .. MATH::

            \mathrm{Aut}(Q) =
            \left\{
            \begin{pmatrix}
            a & 0 \\ 0 & b
            \end{pmatrix}
            ,~
            \begin{pmatrix}
            0 & c \\ d & 0
            \end{pmatrix}
            :~
            a, b, c, d \in \QQ_{>0}
            \right\}
            \subset
            GL(2,\QQ)
            \subset
            E(d)

        Note that there are no translations that map the quadrant `Q`
        to itself, so the linear automorphism group is contained in
        the general linear group (the subgroup of transformations
        preserving the origin). The restricted automorphism group is

        .. MATH::

            \mathrm{Aut}(Q) =
            \left\{
            \begin{pmatrix}
            1 & 0 \\ 0 & 1
            \end{pmatrix}
            ,~
            \begin{pmatrix}
            0 & 1 \\ 1 & 0
            \end{pmatrix}
            \right\}
            \simeq \ZZ_2

        INPUT:

        - ``output`` -- how the group should be represented:

          - ``"abstract"`` (default) -- return an abstract permutation
            group without further meaning.

          - ``"permutation"`` -- return a permutation group on the
            indices of the polyhedron generators. For example, the
            permutation ``(0,1)`` would correspond to swapping
            ``self.Vrepresentation(0)`` and ``self.Vrepresentation(1)``.

          - ``"matrix"`` -- return a matrix group representing affine
            transformations. When acting on affine vectors, you should
            append a `1` to every vector. If the polyhedron is not full
            dimensional, the returned matrices act as the identity on
            the orthogonal complement of the affine space spanned by
            the polyhedron.

          - ``"matrixlist"`` -- like ``matrix``, but return the list of
            elements of the matrix group. Useful for fields without a
            good implementation of matrix groups or to avoid the
            overhead of creating the group.

        OUTPUT:

        - For ``output="abstract"`` and ``output="permutation"``:
          a :class:`PermutationGroup<sage.groups.perm_gps.permgroup.PermutationGroup_generic>`.

        - For ``output="matrix"``: a :class:`MatrixGroup`.

        - For ``output="matrixlist"``: a list of matrices.

        REFERENCES:

        - [BSS2009]_

        EXAMPLES:

        A cross-polytope example::

            sage: P = polytopes.cross_polytope(3)
            sage: P.restricted_automorphism_group() == PermutationGroup([[(3,4)], [(2,3),(4,5)],[(2,5)],[(1,2),(5,6)],[(1,6)]])
            True
            sage: P.restricted_automorphism_group(output="permutation") == PermutationGroup([[(2,3)],[(1,2),(3,4)],[(1,4)],[(0,1),(4,5)],[(0,5)]])
            True
            sage: mgens = [[[1,0,0,0],[0,1,0,0],[0,0,-1,0],[0,0,0,1]], [[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]], [[0,1,0,0],[1,0,0,0],[0,0,1,0],[0,0,0,1]]]

        We test groups for equality in a fool-proof way; they can have different generators, etc::

            sage: poly_g = P.restricted_automorphism_group(output="matrix")
            sage: matrix_g = MatrixGroup([matrix(QQ,t) for t in mgens])
            sage: all(t.matrix() in poly_g for t in matrix_g.gens())
            True
            sage: all(t.matrix() in matrix_g for t in poly_g.gens())
            True

        24-cell example::

            sage: P24 = polytopes.twenty_four_cell()
            sage: AutP24 = P24.restricted_automorphism_group()
            sage: PermutationGroup([
            ....:     '(1,20,2,24,5,23)(3,18,10,19,4,14)(6,21,11,22,7,15)(8,12,16,17,13,9)',
            ....:     '(1,21,8,24,4,17)(2,11,6,15,9,13)(3,20)(5,22)(10,16,12,23,14,19)'
            ....: ]).is_isomorphic(AutP24)
            True
            sage: AutP24.order()
            1152

        Here is the quadrant example mentioned in the beginning::

            sage: P = Polyhedron(rays=[(1,0),(0,1)])
            sage: P.Vrepresentation()
            (A vertex at (0, 0), A ray in the direction (0, 1), A ray in the direction (1, 0))
            sage: P.restricted_automorphism_group(output="permutation")
            Permutation Group with generators [(1,2)]

        Also, the polyhedron need not be full-dimensional::

            sage: P = Polyhedron(vertices=[(1,2,3,4,5),(7,8,9,10,11)])
            sage: P.restricted_automorphism_group()
            Permutation Group with generators [(1,2)]
            sage: G = P.restricted_automorphism_group(output="matrixlist")
            sage: G
            (
            [1 0 0 0 0 0]  [ -87/55  -82/55    -2/5   38/55   98/55   12/11]
            [0 1 0 0 0 0]  [-142/55  -27/55    -2/5   38/55   98/55   12/11]
            [0 0 1 0 0 0]  [-142/55  -82/55     3/5   38/55   98/55   12/11]
            [0 0 0 1 0 0]  [-142/55  -82/55    -2/5   93/55   98/55   12/11]
            [0 0 0 0 1 0]  [-142/55  -82/55    -2/5   38/55  153/55   12/11]
            [0 0 0 0 0 1], [      0       0       0       0       0       1]
            )
            sage: g = AffineGroup(5, QQ)(G[1])
            sage: g
                  [ -87/55  -82/55    -2/5   38/55   98/55]     [12/11]
                  [-142/55  -27/55    -2/5   38/55   98/55]     [12/11]
            x |-> [-142/55  -82/55     3/5   38/55   98/55] x + [12/11]
                  [-142/55  -82/55    -2/5   93/55   98/55]     [12/11]
                  [-142/55  -82/55    -2/5   38/55  153/55]     [12/11]
            sage: g^2
                  [1 0 0 0 0]     [0]
                  [0 1 0 0 0]     [0]
            x |-> [0 0 1 0 0] x + [0]
                  [0 0 0 1 0]     [0]
                  [0 0 0 0 1]     [0]
            sage: g(list(P.vertices()[0]))
            (7, 8, 9, 10, 11)
            sage: g(list(P.vertices()[1]))
            (1, 2, 3, 4, 5)

        Affine transformations do not change the restricted automorphism
        group. For example, any non-degenerate triangle has the
        dihedral group with 6 elements, `D_6`, as its automorphism
        group::

            sage: initial_points = [vector([1,0]), vector([0,1]), vector([-2,-1])]
            sage: points = initial_points
            sage: Polyhedron(vertices=points).restricted_automorphism_group()
            Permutation Group with generators [(2,3), (1,2)]
            sage: points = [pt - initial_points[0] for pt in initial_points]
            sage: Polyhedron(vertices=points).restricted_automorphism_group()
            Permutation Group with generators [(2,3), (1,2)]
            sage: points = [pt - initial_points[1] for pt in initial_points]
            sage: Polyhedron(vertices=points).restricted_automorphism_group()
            Permutation Group with generators [(2,3), (1,2)]
            sage: points = [pt - 2*initial_points[1] for pt in initial_points]
            sage: Polyhedron(vertices=points).restricted_automorphism_group()
            Permutation Group with generators [(2,3), (1,2)]

        The ``output="matrixlist"`` can be used over fields without a
        complete implementation of matrix groups::

            sage: P = polytopes.dodecahedron(); P
            A 3-dimensional polyhedron in (Number Field in sqrt5 with defining polynomial x^2 - 5 with sqrt5 = 2.236067977499790?)^3 defined as the convex hull of 20 vertices
            sage: G = P.restricted_automorphism_group(output="matrixlist")
            sage: len(G)
            120

        Floating-point computations are supported with a simple fuzzy
        zero implementation::

            sage: P = Polyhedron(vertices=[(1/3,0,0,1),(0,1/4,0,1),(0,0,1/5,1)], base_ring=RDF)
            sage: P.restricted_automorphism_group()
            Permutation Group with generators [(2,3), (1,2)]
            sage: len(P.restricted_automorphism_group(output="matrixlist"))
            6

        TESTS::

            sage: P = Polyhedron(vertices=[(1,0), (1,1)], rays=[(1,0)])
            sage: P.restricted_automorphism_group(output="permutation")
            Permutation Group with generators [(1,2)]
            sage: P.restricted_automorphism_group(output="matrix")
            Matrix group over Rational Field with 1 generators (
            [ 1  0  0]
            [ 0 -1  1]
            [ 0  0  1]
            )
            sage: P.restricted_automorphism_group(output="foobar")
            Traceback (most recent call last):
            ...
            ValueError: unknown output 'foobar', valid values are ('abstract', 'permutation', 'matrix', 'matrixlist')

        Check that :trac:`28828` is fixed::

            sage: P.restricted_automorphism_group(output="matrixlist")[0].is_immutable()
            True
        """
        # The algorithm works as follows:
        #
        # Let V be the matrix where every column is a homogeneous
        # coordinate of a V-representation object (vertex, ray, line).
        # Let us assume that V has full rank, that the polyhedron is
        # full dimensional.
        #
        # Let Q = V Vt and C = Vt Q^-1 V. The rows and columns of C
        # can be thought of as being indexed by the V-rep objects of the
        # polytope.
        #
        # It turns out that we can identify the restricted automorphism
        # group with the automorphism group of the edge-colored graph
        # on the V-rep objects with colors determined by the symmetric
        # matrix C.
        #
        # An automorphism of this graph is equivalent to a permutation
        # matrix P such that C = Pt C P. If we now define
        # A = V P Vt Q^-1, then one can check that V P = A V.
        # In other words: permuting the generators is the same as
        # applying the affine transformation A on the generators.
        #
        # If the given polyhedron is not fully-dimensional,
        # then Q will be not invertible. In this case, we use a
        # pseudoinverse Q+ instead of Q^-1. The formula for A acting on
        # the space spanned by V then simplifies to A = V P V+ where V+
        # denotes the pseudoinverse of V, which also equals V+ = Vt Q+.
        #
        # If we are asked to return the (group of) transformation
        # matrices to the user, we also require that those
        # transformations act as the identity on the orthogonal
        # complement of the space spanned by V. This complement is the
        # space spanned by the columns of W = 1 - V V+. One can check
        # that B = (V P V+) + W is the correct matrix: it acts the same
        # as A on V and it satisfies B W = W.

        outputs = ("abstract", "permutation", "matrix", "matrixlist")
        if output not in outputs:
            raise ValueError("unknown output {!r}, valid values are {}".format(output, outputs))

        # For backwards compatibility, we treat "abstract" as
        # "permutation", but where we add 1 to the indices of the
        # permutations.
        index0 = 0
        if output == "abstract":
            index0 = 1
            output = "permutation"

        if self.base_ring().is_exact():
            def rational_approximation(c):
                return c
        else:
            c_list = []

            def rational_approximation(c):
                # Implementation detail: Return unique integer if two
                # c-values are the same up to machine precision. But
                # you can think of it as a uniquely-chosen rational
                # approximation.
                for i, x in enumerate(c_list):
                    if self._is_zero(x - c):
                        return i
                c_list.append(c)
                return len(c_list) - 1

        if self.is_compact():
            def edge_label(i, j, c_ij):
                return c_ij
        else:
            # In the non-compact case, we also label the edges by the
            # type of the V-representation object. This ensures that
            # vertices, rays, and lines are only permuted amongst
            # themselves.
            def edge_label(i, j, c_ij):
                return (self.Vrepresentation(i).type(), c_ij, self.Vrepresentation(j).type())

        # Homogeneous coordinates for the V-representation objects.
        # Mathematically, V is a matrix. For efficiency however, we
        # represent it as a list of column vectors.
        V = [v.homogeneous_vector() for v in self.Vrepresentation()]

        # Pseudoinverse of V Vt
        Qplus = sum(v.column() * v.row() for v in V).pseudoinverse()

        # Construct the graph.
        from sage.graphs.graph import Graph
        G = Graph()
        for i in range(len(V)):
            for j in range(i+1, len(V)):
                c_ij = rational_approximation(V[i] * Qplus * V[j])
                G.add_edge(index0+i, index0+j, edge_label(i, j, c_ij))

        permgroup = G.automorphism_group(edge_labels=True)
        if output == "permutation":
            return permgroup
        elif output == "matrix":
            permgroup = permgroup.gens()

        # Compute V+ = Vt Q+ as list of row vectors
        from sage.matrix.constructor import matrix
        Vplus = list(matrix(V) * Qplus)  # matrix(V) is Vt

        # Compute W = 1 - V V+
        W = 1 - sum(V[i].column() * Vplus[i].row() for i in range(len(V)))

        # Convert the permutation group to a matrix group.
        # If P is a permutation, then we return the matrix
        # B = (V P V+) + W.
        #
        # If output == "matrix", we loop over the generators of the group.
        # Otherwise, we loop over all elements.
        matrices = []
        for perm in permgroup:
            A = sum(V[perm(i)].column() * Vplus[i].row() for i in range(len(V)))
            matrices.append(A + W)

        for mat in matrices:
            mat.set_immutable()

        if output == "matrixlist":
            return tuple(matrices)
        else:
            from sage.groups.matrix_gps.finitely_generated import MatrixGroup
            return MatrixGroup(matrices)

    def is_combinatorially_isomorphic(self, other, algorithm='bipartite_graph'):
        r"""
        Return whether the polyhedron is combinatorially isomorphic to another polyhedron.

        We only consider bounded polyhedra. By definition, they are
        combinatorially isomorphic if their face lattices are isomorphic.

        INPUT:

        - ``other`` -- a polyhedron object
        - ``algorithm`` (default = ``bipartite_graph``) -- the algorithm to use.
          The other possible value is ``face_lattice``.

        OUTPUT:

        - ``True`` if the two polyhedra are combinatorially isomorphic
        - ``False`` otherwise

        .. SEEALSO::

            :meth:`combinatorial_automorphism_group`,
            :meth:`vertex_facet_graph`.

        REFERENCES:

        For the equivalence of the two algorithms see [KK1995]_, p. 877-878

        EXAMPLES:

        The square is combinatorially isomorphic to the 2-dimensional cube::

            sage: polytopes.hypercube(2).is_combinatorially_isomorphic(polytopes.regular_polygon(4))
            True

        All the faces of the 3-dimensional permutahedron are either
        combinatorially isomorphic to a square or a hexagon::

            sage: H = polytopes.regular_polygon(6)                              # optional - sage.rings.number_field
            sage: S = polytopes.hypercube(2)
            sage: P = polytopes.permutahedron(4)
            sage: all(F.as_polyhedron().is_combinatorially_isomorphic(S)        # optional - sage.rings.number_field
            ....:       or F.as_polyhedron().is_combinatorially_isomorphic(H)
            ....:     for F in P.faces(2))
            True

        Checking that a regular simplex intersected with its reflection
        through the origin is combinatorially isomorphic to the intersection
        of a cube with a hyperplane perpendicular to its long diagonal::

            sage: def simplex_intersection(k):
            ....:   S1 = Polyhedron([vector(v)-vector(polytopes.simplex(k).center()) for v in polytopes.simplex(k).vertices_list()])
            ....:   S2 = Polyhedron([-vector(v) for v in S1.vertices_list()])
            ....:   return S1.intersection(S2)
            sage: def cube_intersection(k):
            ....:    C = polytopes.hypercube(k+1)
            ....:    H = Polyhedron(eqns=[[0]+[1 for i in range(k+1)]])
            ....:    return C.intersection(H)
            sage: [simplex_intersection(k).is_combinatorially_isomorphic(cube_intersection(k)) for k in range(2,5)]
            [True, True, True]
            sage: simplex_intersection(2).is_combinatorially_isomorphic(polytopes.regular_polygon(6))   # optional - sage.rings.number_field
            True
            sage: simplex_intersection(3).is_combinatorially_isomorphic(polytopes.octahedron())
            True

        Two polytopes with the same `f`-vector, but different combinatorial types::

            sage: P = Polyhedron([[-605520/1525633, -605520/1525633, -1261500/1525633, -52200/1525633, 11833/1525633],\
             [-720/1769, -600/1769, 1500/1769, 0, -31/1769], [-216/749, 240/749, -240/749, -432/749, 461/749], \
             [-50/181, 50/181, 60/181, -100/181, -119/181], [-32/51, -16/51, -4/51, 12/17, 1/17],\
             [1, 0, 0, 0, 0], [16/129, 128/129, 0, 0, 1/129], [64/267, -128/267, 24/89, -128/267, 57/89],\
             [1200/3953, -1200/3953, -1440/3953, -360/3953, -3247/3953], [1512/5597, 1512/5597, 588/5597, 4704/5597, 2069/5597]])
            sage: C = polytopes.cyclic_polytope(5,10)
            sage: C.f_vector() == P.f_vector(); C.f_vector()
            True
            (1, 10, 45, 100, 105, 42, 1)
            sage: C.is_combinatorially_isomorphic(P)
            False

            sage: S = polytopes.simplex(3)
            sage: S = S.face_truncation(S.faces(0)[3])
            sage: S = S.face_truncation(S.faces(0)[4])
            sage: S = S.face_truncation(S.faces(0)[5])
            sage: T = polytopes.simplex(3)
            sage: T = T.face_truncation(T.faces(0)[3])
            sage: T = T.face_truncation(T.faces(0)[4])
            sage: T = T.face_truncation(T.faces(0)[4])
            sage: T.is_combinatorially_isomorphic(S)
            False
            sage: T.f_vector(), S.f_vector()
            ((1, 10, 15, 7, 1), (1, 10, 15, 7, 1))

            sage: C = polytopes.hypercube(5)
            sage: C.is_combinatorially_isomorphic(C)
            True
            sage: C.is_combinatorially_isomorphic(C, algorithm='magic')
            Traceback (most recent call last):
            ...
            AssertionError: `algorithm` must be 'bipartite graph' or 'face_lattice'

            sage: G = Graph()
            sage: C.is_combinatorially_isomorphic(G)
            Traceback (most recent call last):
            ...
            AssertionError: input `other` must be a polyhedron

            sage: H = Polyhedron(eqns=[[0,1,1,1,1]]); H
            A 3-dimensional polyhedron in QQ^4 defined as the convex hull of 1 vertex and 3 lines
            sage: C.is_combinatorially_isomorphic(H)
            Traceback (most recent call last):
            ...
            AssertionError: polyhedron `other` must be bounded

        """
        assert isinstance(other, Polyhedron_base4), "input `other` must be a polyhedron"
        assert self.is_compact(), "polyhedron `self` must be bounded"
        assert other.is_compact(), "polyhedron `other` must be bounded"
        assert algorithm in ['bipartite_graph', 'face_lattice'], "`algorithm` must be 'bipartite graph' or 'face_lattice'"

        # For speed, we check if the polyhedra have the same number of facets and vertices.
        # This is faster than building the bipartite graphs first and
        # then check that they won't be isomorphic.
        if self.n_vertices() != other.n_vertices() or self.n_facets() != other.n_facets():
            return False

        if algorithm == 'bipartite_graph':
            G_self = self.vertex_facet_graph(False)
            G_other = other.vertex_facet_graph(False)

            return G_self.is_isomorphic(G_other)
        else:
            return self.face_lattice().is_isomorphic(other.face_lattice())

    def _test_is_combinatorially_isomorphic(self, tester=None, **options):
        """
        Run tests on the method :meth:`.is_combinatorially_isomorphic`.

        TESTS::

            sage: polytopes.cross_polytope(3)._test_is_combinatorially_isomorphic()
        """
        if tester is None:
            tester = self._tester(**options)

        if not self.is_compact():
            with tester.assertRaises(AssertionError):
                self.is_combinatorially_isomorphic(self)
            return

        if self.n_vertices() > 200 or self.n_facets() > 200:
            # Avoid very long doctests.
            return

        try:
            import sage.graphs.graph
        except ImportError:
            return

        from sage.rings.integer_ring import ZZ
        tester.assertTrue(self.is_combinatorially_isomorphic(ZZ(4)*self))
        if self.n_vertices():
            tester.assertTrue(self.is_combinatorially_isomorphic(self + self.center()))

        if self.n_vertices() < 20 and self.n_facets() < 20 and self.is_immutable():
            tester.assertTrue(self.is_combinatorially_isomorphic(ZZ(4)*self, algorithm='face_lattice'))
            if self.n_vertices():
                tester.assertTrue(self.is_combinatorially_isomorphic(self + self.center(), algorithm='face_lattice'))

    def is_self_dual(self):
        r"""
        Return whether the polytope is self-dual.

        A polytope is self-dual if its face lattice is isomorphic to the face
        lattice of its dual polytope.

        EXAMPLES::

            sage: polytopes.simplex().is_self_dual()
            True
            sage: polytopes.twenty_four_cell().is_self_dual()
            True
            sage: polytopes.cube().is_self_dual()
            False
            sage: polytopes.hypersimplex(5,2).is_self_dual()
            False
            sage: P = Polyhedron(vertices=[[1/2, 1/3]], rays=[[1, 1]]).is_self_dual()
            Traceback (most recent call last):
            ...
            ValueError: polyhedron has to be compact

        """
        if not self.is_compact():
            raise ValueError("polyhedron has to be compact")

        n = self.n_vertices()
        m = self.n_facets()
        if n != m:
            return False

        G1 = self.vertex_facet_graph()
        G2 = G1.reverse()
        return G1.is_isomorphic(G2)


# -*- coding: utf-8 -*-

# Author: Daniel Yang <daniel.yj.yang@gmail.com>
#
# License: BSD 3 clause

# Some references:
# https://huggingface.co/docs/datasets/
# https://scikit-learn.org/stable/datasets/index.html#general-dataset-api

import io
#from zipfile import ZipFile
#import urllib.request
import csv

import pkgutil

import os
import numpy as np

def public_dataset(name=None):
    """
    name can be one of the following:
        - iris
        - SMS_spam
        - Social_Network_Ads
        - bank_note_authentication
        - marketing
        - Hitters
        - Gender
        - boston
        - Fashion_MNIST
        - nltk_data_path
        - scikit_learn_data_path

    Disclaimer:
        - The datasets are shared with the sole intention to provide the convenience of accessing publicly available datasets and reproducing/comparing results.
        - They are shared under a good-faith understanding that they are widely viewed and accepted as public-domain datasets.
        - If there is any misunderstanding, please contact the author.
        - The author does not own any of these datasets.
        - The readme in respective folder (or related Internet link) should be followed for citation/license requirements.
    """

    if name == "iris":
        import pandas as pd
        from sklearn import datasets
        iris = datasets.load_iris()
        dataset = pd.DataFrame(data = iris.data, columns = iris.feature_names)
        dataset['target'] = iris.target
        # iris.target_names  # y_classes = ['setosa', 'versicolor', 'virginica']
        print(f"Fisher's Iris is a publicly available dataset that consists of {len(iris.data)} samples from three species of Iris ('setosa', 'versicolor', 'virginica'), while four features were measured from each sample: the length and the width of the sepals and petals, in centimeters.\n")
        return dataset

    #print(public_dataset.__doc__)
    if name == "SMS_spam":
        import pandas as pd
        # https://archive.ics.uci.edu/ml/datasets/sms+spam+collection (UCI Machine Learning Repository)
        df = pd.read_csv(io.BytesIO(pkgutil.get_data(__name__, "public/UCI_Machine_Learning_Repository/SMS_Spam_Collection/SMSSpamCollection.tsv")), sep='\t', quoting=csv.QUOTE_NONE, names=("label", "message"))
        n_spam = df['label'].value_counts()['spam']
        n_ham = df['label'].value_counts()['ham']
        print(f"SMS_spam is a publicly available dataset that has a total of {len(df)} messages = {n_ham} ham (legitimate) and {n_spam} spam.\n")
        return df
        #url = urllib.request.urlopen("https://archive.ics.uci.edu/ml/machine-learning-databases/00228/smsspamcollection.zip")
        #df = pd.read_csv(ZipFile(io.BytesIO(url.read())).open('SMSSpamCollection'), sep='\t', quoting=csv.QUOTE_NONE, names=("label", "message"))

    if name == "Social_Network_Ads":
        import pandas as pd
        df = pd.read_csv(io.BytesIO(pkgutil.get_data(__name__, "public/Social_Network_Ads/Social_Network_Ads.csv")), encoding='utf8', sep=",")
        print("Social Network Ads is a publicly available dataset that can be used to determine what audience a car company should target in its ads in order to sell a SUV on a social network website.\n")
        return df
        #url = urllib.request.urlopen("https://github.com/daniel-yj-yang/machlearn/raw/master/machlearn/datasets/public/Social_Network_Ads/Social_Network_Ads.csv")
        #df = pd.read_csv(io.BytesIO(url.read()), encoding='utf8', sep=",")

    if name == "bank_note_authentication":
        # http://archive.ics.uci.edu/ml/datasets/banknote+authentication (UCI Machine Learning Repository)
        import pandas as pd
        df = pd.read_csv(io.BytesIO(pkgutil.get_data(__name__, "public/UCI_Machine_Learning_Repository/bank_note_authentication/data_banknote_authentication.txt")), header=None, encoding='utf8', sep=",")
        # 'variance of Wavelet Transformed image', 'skewness of Wavelet Transformed image', 'curtosis of Wavelet Transformed image', 'entropy of image'
        df.columns = ['variance', 'skewness', 'curtosis', 'entropy', 'class']
        print(f"The dataset of bank note authentication is a publicly available dataset, where data were extracted from {len(df)} images (400x400 pixels, resolution of about 660 dpi) taken from {df['class'].value_counts()[0]} genuine and {df['class'].value_counts()[1]} forged banknote-like specimens. Wavelet Transform tool were used to extract features from images.\n")
        return df

    if name == "marketing":
        # https://cran.r-project.org/web/packages/datarium/datarium.pdf (GPL-2 License)
        import pandas as pd
        df = pd.read_csv(io.BytesIO(pkgutil.get_data(__name__, "public/R_datarium/marketing/marketing.csv")), header=0, encoding='utf8', sep=",")
        print(f"The dataset of marketing is a publicly available dataset from R-datarium, containing the impact of three advertising medias (youtube, facebook and newspaper) on sales. Data are the advertising budget in thousands of dollars along with the sales. The advertising experiment has been repeated 200 times.\n")
        return df

    if name == "Hitters":
        # https://cran.r-project.org/web/packages/ISLR/index.html (GPL-2 License)
        import pandas as pd
        df = pd.read_csv(io.BytesIO(pkgutil.get_data(__name__, "public/R_ISLR/Hitters/Hitters.csv")), header=0, encoding='utf8', sep=",")
        print(f"The dataset of Hitters is a publicly available dataset from R-ISLR, containing Major League Baseball Data from the 1986 and 1987 seasons.\n")
        return df

    if name == "Gender":
        # https://github.com/johnmyleswhite/ML_for_Hackers/blob/master/02-Exploration/data/01_heights_weights_genders.csv (FreeBSD License)
        import pandas as pd
        df = pd.read_csv(io.BytesIO(pkgutil.get_data(__name__, "public/ML_for_Hackers/01_heights_weights_genders.csv")), header=0, encoding='utf8', sep=",")
        print(f"The dataset of Gender is a publicly available dataset, containing 10000 heights and weights from 5000 males and 5000 females.\n")
        return df

    if name == "boston":
        import pandas as pd
        from sklearn.datasets import load_boston
        boston = load_boston()
        X = pd.DataFrame(data=boston.data, columns=boston.feature_names)
        y = pd.DataFrame(data=boston.target, columns=['MEDV'])
        df = pd.concat([X, y], axis=1)
        print("The dataset of boston is a publicly available dataset, including 506 cases of housing price in the area of Boston, Mass.")
        print("Here are the 13 X features:")
        print("1. CRIM - per capita crime rate by town")
        print("2. ZN - proportion of residential land zoned for lots over 25,000 sq.ft.")
        print("3. INDUS - proportion of non-retail business acres per town.")
        print("4. CHAS - Charles River dummy variable(1 if tract bounds river; 0 otherwise)")
        print("5. NOX - nitric oxides concentration (parts per 10 million)")
        print("6. RM - average number of rooms per dwelling")
        print("7. AGE - proportion of owner-occupied units built prior to 1940")
        print("8. DIS - weighted distances to five Boston employment centres")
        print("9. RAD - index of accessibility to radial highways")
        print("10. TAX - full-value property-tax rate per $10,000")
        print("11. PTRATIO - pupil-teacher ratio by town")
        print("12. B - 1000(Bk - 0.63)^2 where Bk is the proportion of blacks by town")
        print("13. LSTAT - % lower status of the population")
        print("")
        print("Here is the y target variable:")
        print("14. MEDV - Median value of owner-occupied homes in $1000's\n")
        return df

    if name == "Fashion_MNIST":
        # this part of the code is modeled after https://github.com/zalandoresearch/fashion-mnist/blob/master/utils/mnist_reader.py
        import gzip
        path = os.path.dirname(__file__) + "/public/Fashion_MNIST"
        images_train_filepath = os.path.join(path, 'train-images-idx3-ubyte.gz')
        labels_train_filepath = os.path.join(path, 'train-labels-idx1-ubyte.gz')
        images_test_filepath  = os.path.join(path,  't10k-images-idx3-ubyte.gz')
        labels_test_filepath  = os.path.join(path,  't10k-labels-idx1-ubyte.gz')
        with gzip.open(labels_train_filepath, 'rb') as lbpath:
            labels_train = np.frombuffer(lbpath.read(),  dtype=np.uint8, offset=8)
        with gzip.open(images_train_filepath, 'rb') as imgpath:
            images_train = np.frombuffer(imgpath.read(), dtype=np.uint8, offset=16).reshape(len(labels_train), 784)
        with gzip.open(labels_test_filepath,  'rb') as lbpath:
            labels_test  = np.frombuffer(lbpath.read(),  dtype=np.uint8, offset=8)
        with gzip.open(images_test_filepath,  'rb') as imgpath:
            images_test  = np.frombuffer(imgpath.read(), dtype=np.uint8, offset=16).reshape(len(labels_test), 784)
        return images_train, labels_train, images_test, labels_test

    if name == 'nltk_data_path':
        return os.path.dirname(__file__) + "/public/nltk_data"

    if name == 'scikit_learn_data_path':
        return os.path.dirname(__file__) + "/public/scikit_learn_data"

    raise TypeError('recognizable dataset name is not provided')


"""
Copyright 2020 The OneFlow Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import oneflow
from oneflow.framework.docstr.utils import add_docstr

add_docstr(
    oneflow.tile,
    """
    tile(input, dims) -> Tensor

    The interface is consistent with PyTorch.
    The documentation is referenced from:
    https://pytorch.org/docs/1.10/generated/torch.tile.html.

    Constructs a tensor by repeating the elements of ``input``.  The ``dims`` argument specifies the number
    of repetitions in each dimension.

    If ``dims`` specifies fewer dimensions than ``input`` has, then ones are prepended to ``dims`` until
    all dimensions are specified.  For example, if ``input`` has shape (8, 6, 4, 2) and ``dims`` is (2, 2),
    then ``dims`` is treated as (1, 1, 2, 2).

    Analogously, if ``input`` has fewer dimensions than ``dims`` specifies, then ``input`` is treated as
    if it were unsqueezed at dimension zero until it has as many dimensions as ``dims`` specifies.
    For example, if ``input`` has shape (4, 2) and ``dims`` is (3, 3, 2, 2), then ``input`` is treated as
    if it had the shape (1, 1, 4, 2).

    .. note::
        This function is similar to NumPy’s tile function.

    Args:
        input (oneflow.Tensor): the tensor whose elements to repeat.
        dims (tuple): the number of repetitions per dimension.

    For example:

    .. code-block:: python

        >>> import oneflow as flow
        >>> import numpy as np

        >>> np_arr = np.random.randn(5, 3, 6, 9).astype(np.float32)
        >>> input = flow.Tensor(np_arr)
        >>> out = input.tile(2,1,2,1)
        >>> out.shape
        oneflow.Size([10, 3, 12, 9])
        >>> x = np.random.randn(5, 2, 1)
        >>> input = flow.Tensor(x)
        >>> out = input.tile(3,4)
        >>> out.shape
        oneflow.Size([5, 6, 4])
    """,
)


from caffe2.python import core, workspace
from hypothesis import given, settings

import caffe2.python.hypothesis_test_util as hu
import caffe2.python.serialized_test.serialized_test_util as serial
import hypothesis.strategies as st
import numpy as np
import itertools as it

class TestReduceOps(serial.SerializedTestCase):
    def run_reduce_op_test_impl(
            self, op_name, X, axes, keepdims, ref_func, gc, dc, allow_broadcast_fastpath):
        extra_args = dict(allow_broadcast_fastpath=True) if allow_broadcast_fastpath else {}
        if axes is None:
            op = core.CreateOperator(
                op_name,
                ["X"],
                ["Y"],
                keepdims=keepdims,
                **extra_args,
            )
        else:
            op = core.CreateOperator(
                op_name,
                ["X"],
                ["Y"],
                axes=axes,
                keepdims=keepdims,
                **extra_args,
            )

        def ref(X):
            return [ref_func(
                X, axis=None if axes is None else tuple(axes),
                keepdims=keepdims)]

        with self.set_disable_serialized_check(allow_broadcast_fastpath):
            self.assertReferenceChecks(gc, op, [X], ref)
        self.assertDeviceChecks(dc, op, [X], [0])
        self.assertGradientChecks(gc, op, [X], 0, [0])

    def run_reduce_op_test(
            self, op_name, X, keepdims, num_axes, ref_func, gc, dc, allow_broadcast_fastpath=False):
        self.run_reduce_op_test_impl(
            op_name, X, None, keepdims, ref_func, gc, dc, allow_broadcast_fastpath)

        num_dims = len(X.shape)
        if num_dims < num_axes:
            self.run_reduce_op_test_impl(
                op_name, X, range(num_dims), keepdims, ref_func, gc, dc, allow_broadcast_fastpath)
        else:
            for axes in it.combinations(range(num_dims), num_axes):
                self.run_reduce_op_test_impl(
                    op_name, X, axes, keepdims, ref_func, gc, dc, allow_broadcast_fastpath)

    @serial.given(
        X=hu.tensor(max_dim=3, dtype=np.float32),
        keepdims=st.booleans(),
        allow_broadcast_fastpath=st.booleans(),
        num_axes=st.integers(1, 3), **hu.gcs)
    def test_reduce_min(self, X, keepdims, allow_broadcast_fastpath, num_axes, gc, dc):
        X_dims = X.shape
        X_size = X.size
        X = np.arange(X_size, dtype=np.float32)
        np.random.shuffle(X)
        X = X.reshape(X_dims)
        self.run_reduce_op_test(
            "ReduceMin", X, keepdims, num_axes, np.min, gc, dc,
            allow_broadcast_fastpath=allow_broadcast_fastpath)

    @serial.given(
        X=hu.tensor(max_dim=3, dtype=np.float32),
        keepdims=st.booleans(),
        allow_broadcast_fastpath=st.booleans(),
        num_axes=st.integers(1, 3), **hu.gcs)
    def test_reduce_max(self, X, keepdims, allow_broadcast_fastpath, num_axes, gc, dc):
        X_dims = X.shape
        X_size = X.size
        X = np.arange(X_size, dtype=np.float32)
        np.random.shuffle(X)
        X = X.reshape(X_dims)
        self.run_reduce_op_test(
            "ReduceMax", X, keepdims, num_axes, np.max, gc, dc,
            allow_broadcast_fastpath=allow_broadcast_fastpath)

    @given(n=st.integers(0, 5), m=st.integers(0, 5), k=st.integers(0, 5),
           t=st.integers(0, 5), keepdims=st.booleans(),
           allow_broadcast_fastpath=st.booleans(),
           num_axes=st.integers(1, 3), **hu.gcs)
    @settings(deadline=10000)
    def test_reduce_sum(self, n, m, k, t, keepdims, allow_broadcast_fastpath, num_axes, gc, dc):
        X = np.random.randn(n, m, k, t).astype(np.float32)
        self.run_reduce_op_test(
            "ReduceSum", X, keepdims, num_axes, np.sum, gc, dc,
            allow_broadcast_fastpath=allow_broadcast_fastpath)

    @serial.given(X=hu.tensor(dtype=np.float32), keepdims=st.booleans(),
                  allow_broadcast_fastpath=st.booleans(),
                  num_axes=st.integers(1, 4), **hu.gcs)
    def test_reduce_mean(self, X, keepdims, allow_broadcast_fastpath, num_axes, gc, dc):
        self.run_reduce_op_test(
            "ReduceMean", X, keepdims, num_axes, np.mean, gc, dc,
            allow_broadcast_fastpath=allow_broadcast_fastpath)

    @given(n=st.integers(1, 3), m=st.integers(1, 3), k=st.integers(1, 3),
           keepdims=st.booleans(), allow_broadcast_fastpath=st.booleans(),
           num_axes=st.integers(1, 3), **hu.gcs_cpu_only)
    @settings(deadline=10000)
    def test_reduce_l1(self, n, m, k, keepdims, allow_broadcast_fastpath, num_axes, gc, dc):
        X = np.arange(n * m * k, dtype=np.float32) - 0.5
        np.random.shuffle(X)
        X = X.reshape((m, n, k))
        self.run_reduce_op_test(
            "ReduceL1", X, keepdims, num_axes, getNorm(1), gc, dc,
            allow_broadcast_fastpath=allow_broadcast_fastpath)

    @serial.given(n=st.integers(1, 5), m=st.integers(1, 5), k=st.integers(1, 5),
                  keepdims=st.booleans(), allow_broadcast_fastpath=st.booleans(),
                  num_axes=st.integers(1, 3), **hu.gcs_cpu_only)
    def test_reduce_l2(self, n, m, k, keepdims, allow_broadcast_fastpath, num_axes, gc, dc):
        X = np.random.randn(n, m, k).astype(np.float32)
        self.run_reduce_op_test(
            "ReduceL2", X, keepdims, num_axes, getNorm(2), gc, dc,
            allow_broadcast_fastpath=allow_broadcast_fastpath)

def getNorm(p):
    if p == 1:
        def norm(X, axis, keepdims):
            return np.sum(np.abs(X), axis=axis, keepdims=keepdims)
    elif p == 2:
        def norm(X, axis, keepdims):
            return np.sqrt(np.sum(np.power(X, 2), axis=axis, keepdims=keepdims))
    else:
        raise RuntimeError("Only L1 and L2 norms supported")
    return norm

class TestReduceFrontReductions(serial.SerializedTestCase):
    def grad_variant_input_test(self, grad_op_name, X, ref, num_reduce_dim):
        workspace.ResetWorkspace()

        Y = np.array(ref(X)[0]).astype(np.float32)
        dY = np.array(np.random.rand(*Y.shape)).astype(np.float32)
        shape = np.array(X.shape).astype(np.int64)

        workspace.FeedBlob("X", X)
        workspace.FeedBlob("dY", dY)
        workspace.FeedBlob("shape", shape)

        grad_op = core.CreateOperator(
            grad_op_name, ["dY", "X"], ["dX"], num_reduce_dim=num_reduce_dim)

        grad_op1 = core.CreateOperator(
            grad_op_name, ["dY", "shape"], ["dX1"],
            num_reduce_dim=num_reduce_dim)

        workspace.RunOperatorOnce(grad_op)
        workspace.RunOperatorOnce(grad_op1)

        dX = workspace.FetchBlob("dX")
        dX1 = workspace.FetchBlob("dX1")
        np.testing.assert_array_equal(dX, dX1)

    def max_op_test(
            self, op_name, num_reduce_dim, gc, dc, in_data, in_names, ref_max):

        op = core.CreateOperator(
            op_name,
            in_names,
            ["outputs"],
            num_reduce_dim=num_reduce_dim
        )

        self.assertReferenceChecks(
            device_option=gc,
            op=op,
            inputs=in_data,
            reference=ref_max,
        )

        # Skip gradient check because it is too unreliable with max.
        # Just check CPU and CUDA have same results
        Y = np.array(ref_max(*in_data)[0]).astype(np.float32)
        dY = np.array(np.random.rand(*Y.shape)).astype(np.float32)
        if len(in_data) == 2:
            grad_in_names = ["dY", in_names[0], "Y", in_names[1]]
            grad_in_data = [dY, in_data[0], Y, in_data[1]]
        else:
            grad_in_names = ["dY", in_names[0], "Y"]
            grad_in_data = [dY, in_data[0], Y]

        grad_op = core.CreateOperator(
            op_name + "Gradient",
            grad_in_names,
            ["dX"],
            num_reduce_dim=num_reduce_dim
        )
        self.assertDeviceChecks(dc, grad_op, grad_in_data, [0])

    def reduce_op_test(self, op_name, op_ref, in_data, in_names,
                       num_reduce_dims, device):
        op = core.CreateOperator(
            op_name,
            in_names,
            ["outputs"],
            num_reduce_dim=num_reduce_dims
        )

        self.assertReferenceChecks(
            device_option=device,
            op=op,
            inputs=in_data,
            reference=op_ref
        )

        self.assertGradientChecks(
            device, op, in_data, 0, [0], stepsize=1e-2, threshold=1e-2)

    @given(num_reduce_dim=st.integers(0, 4), **hu.gcs)
    @settings(deadline=10000)
    def test_reduce_front_sum(self, num_reduce_dim, gc, dc):
        X = np.random.rand(7, 4, 3, 5).astype(np.float32)

        def ref_sum(X):
            return [np.sum(X, axis=(tuple(range(num_reduce_dim))))]

        self.reduce_op_test(
            "ReduceFrontSum", ref_sum, [X], ["input"], num_reduce_dim, gc)
        self.grad_variant_input_test(
            "ReduceFrontSumGradient", X, ref_sum, num_reduce_dim)

    @given(num_reduce_dim=st.integers(0, 4), seed=st.integers(0, 4), **hu.gcs)
    def test_reduce_front_sum_empty_batch(self, num_reduce_dim, seed, gc, dc):
        np.random.seed(seed)
        X = np.random.rand(0, 4, 3, 5).astype(np.float32)

        def ref_sum(X):
            return [np.sum(X, axis=(tuple(range(num_reduce_dim))))]

        self.reduce_op_test(
            "ReduceFrontSum", ref_sum, [X], ["input"], num_reduce_dim, gc)
        self.grad_variant_input_test(
            "ReduceFrontSumGradient", X, ref_sum, num_reduce_dim)

        # test the second iteration
        not_empty_X = np.random.rand(2, 4, 3, 5).astype(np.float32)
        net = core.Net('test')
        with core.DeviceScope(gc):
            net.ReduceFrontSum(
                ['X'], ['output'],
                num_reduce_dim=num_reduce_dim
            )
            workspace.CreateNet(net)

            workspace.FeedBlob('X', not_empty_X)
            workspace.RunNet(workspace.GetNetName(net))
            output = workspace.FetchBlob('output')
            np.testing.assert_allclose(
                output, ref_sum(not_empty_X)[0], atol=1e-3)

            workspace.FeedBlob('X', X)
            workspace.RunNet(workspace.GetNetName(net))
            output = workspace.FetchBlob('output')
            np.testing.assert_allclose(output, ref_sum(X)[0], atol=1e-3)

    @given(**hu.gcs)
    @settings(deadline=None)
    def test_reduce_front_sum_with_length(self, dc, gc):
        num_reduce_dim = 1
        X = np.random.rand(2, 3, 4, 5).astype(np.float32)
        batch_size = int(np.prod([2, 3, 4, 5][num_reduce_dim:]))
        d = 120 // batch_size
        lengths = np.random.randint(1, d, size=batch_size).astype(np.int32)

        def ref_sum(X, lengths):
            Y = X.reshape(d, lengths.size)
            rv = np.zeros((lengths.size, 1)).astype(np.float32)
            for ii in range(lengths.size):
                rv[ii] = np.sum(Y[:lengths[ii], ii])
            return [rv.reshape((2, 3, 4, 5)[num_reduce_dim:])]

        self.reduce_op_test(
            "ReduceFrontSum", ref_sum, [X, lengths], ["input", "lengths"],
            num_reduce_dim, gc)

    @given(num_reduce_dim=st.integers(0, 4), **hu.gcs)
    @settings(deadline=10000)
    def test_reduce_front_mean(self, num_reduce_dim, gc, dc):
        X = np.random.rand(6, 7, 8, 2).astype(np.float32)

        def ref_mean(X):
            return [np.mean(X, axis=(tuple(range(num_reduce_dim))))]

        self.reduce_op_test(
            "ReduceFrontMean", ref_mean, [X], ["input"], num_reduce_dim, gc)
        self.grad_variant_input_test(
            "ReduceFrontMeanGradient", X, ref_mean, num_reduce_dim)

    @given(**hu.gcs)
    @settings(deadline=10000)
    def test_reduce_front_mean_with_length(self, dc, gc):
        num_reduce_dim = 1
        X = np.random.rand(2, 3, 4, 5).astype(np.float32)
        batch_size = int(np.prod([2, 3, 4, 5][num_reduce_dim:]))
        d = 120 // batch_size
        lengths = np.random.randint(1, d, size=batch_size).astype(np.int32)

        def ref_mean(X, lengths):
            Y = X.reshape(d, lengths.size)
            rv = np.zeros((lengths.size, 1)).astype(np.float32)
            for ii in range(lengths.size):
                rv[ii] = np.mean(Y[:lengths[ii], ii])
            return [rv.reshape((2, 3, 4, 5)[num_reduce_dim:])]

        self.reduce_op_test(
            "ReduceFrontMean", ref_mean, [X, lengths], ["input", "lengths"],
            num_reduce_dim, gc)

    @serial.given(num_reduce_dim=st.integers(0, 4), **hu.gcs)
    def test_reduce_front_max(self, num_reduce_dim, gc, dc):
        X = np.random.rand(6, 7, 8, 2).astype(np.float32)

        def ref_frontmax(X):
            return [np.max(X, axis=(tuple(range(num_reduce_dim))))]

        self.max_op_test(
            "ReduceFrontMax", num_reduce_dim, gc, dc, [X], ["X"], ref_frontmax)

    @given(**hu.gcs)
    def test_reduce_front_max_with_length(self, dc, gc):
        num_reduce_dim = 1
        X = np.random.rand(2, 3, 4, 5).astype(np.float32)
        batch_size = int(np.prod([2, 3, 4, 5][num_reduce_dim:]))
        d = 120 // batch_size
        lengths = np.random.randint(1, d, size=batch_size).astype(np.int32)

        def ref_max(X, lengths):
            Y = X.reshape(d, lengths.size)
            rv = np.zeros((lengths.size, 1)).astype(np.float32)
            for ii in range(lengths.size):
                rv[ii] = np.max(Y[:lengths[ii], ii])
            return [rv.reshape((2, 3, 4, 5)[num_reduce_dim:])]

        self.max_op_test(
            "ReduceFrontMax", num_reduce_dim, gc, dc, [X, lengths],
            ["X", "lengths"], ref_max)

    @serial.given(num_reduce_dim=st.integers(0, 4), **hu.gcs)
    def test_reduce_back_max(self, num_reduce_dim, gc, dc):
        X = np.random.rand(6, 7, 8, 2).astype(np.float32)

        def ref_backmax(X):
            return [np.max(X, axis=(0, 1, 2, 3)[4 - num_reduce_dim:])]

        self.max_op_test(
            "ReduceBackMax", num_reduce_dim, gc, dc, [X], ["X"], ref_backmax)

    @given(**hu.gcs)
    def test_reduce_back_max_with_length(self, gc, dc):
        num_reduce_dim = 1
        X = np.random.rand(2, 3, 4, 5).astype(np.float32)
        batch_size = int(np.prod([2, 3, 4, 5][:4 - num_reduce_dim]))
        d = 120 // batch_size
        lengths = np.random.randint(1, d, size=batch_size).astype(np.int32)

        def ref_max(X, lengths):
            Y = X.reshape(lengths.size, d)
            rv = np.zeros((lengths.size, 1)).astype(np.float32)
            for ii in range(lengths.size):
                rv[ii] = np.max(Y[ii, :lengths[ii]])
            return [rv.reshape((2, 3, 4, 5)[:4 - num_reduce_dim])]

        self.max_op_test(
            "ReduceBackMax", num_reduce_dim, gc, dc, [X, lengths],
            ["X", "lengths"], ref_max)

    @given(**hu.gcs)
    @settings(deadline=10000)
    def test_reduce_back_sum(self, dc, gc):
        num_reduce_dim = 1
        X = np.random.rand(6, 7, 8, 2).astype(np.float32)

        def ref_sum(X):
            return [np.sum(X, axis=(0, 1, 2, 3)[4 - num_reduce_dim:])]

        self.reduce_op_test(
            "ReduceBackSum", ref_sum, [X], ["input"], num_reduce_dim, gc)
        self.grad_variant_input_test(
            "ReduceBackSumGradient", X, ref_sum, num_reduce_dim)

    @given(**hu.gcs)
    @settings(deadline=10000)
    def test_reduce_back_sum_with_length(self, dc, gc):
        num_reduce_dim = 1
        X = np.random.rand(2, 3, 4, 5).astype(np.float32)
        batch_size = int(np.prod([2, 3, 4, 5][:4 - num_reduce_dim]))
        d = 120 // batch_size
        lengths = np.random.randint(1, d, size=batch_size).astype(np.int32)

        def ref_sum(X, lengths):
            Y = X.reshape(lengths.size, d)
            rv = np.zeros((lengths.size, 1)).astype(np.float32)
            for ii in range(lengths.size):
                rv[ii] = np.sum(Y[ii, :lengths[ii]])
            return [rv.reshape((2, 3, 4, 5)[:4 - num_reduce_dim])]

        self.reduce_op_test(
            "ReduceBackSum", ref_sum, [X, lengths], ["input", "lengths"],
            num_reduce_dim, gc)

    @given(num_reduce_dim=st.integers(0, 4), **hu.gcs)
    @settings(deadline=10000)
    def test_reduce_back_mean(self, num_reduce_dim, dc, gc):
        X = np.random.rand(6, 7, 8, 2).astype(np.float32)

        def ref_mean(X):
            return [np.mean(X, axis=(0, 1, 2, 3)[4 - num_reduce_dim:])]

        self.reduce_op_test(
            "ReduceBackMean", ref_mean, [X], ["input"], num_reduce_dim, gc)
        self.grad_variant_input_test(
            "ReduceBackMeanGradient", X, ref_mean, num_reduce_dim)

    @given(**hu.gcs)
    @settings(deadline=None)
    def test_reduce_back_mean_with_length(self, dc, gc):
        num_reduce_dim = 1
        X = np.random.rand(2, 3, 4, 5).astype(np.float32)
        batch_size = int(np.prod([2, 3, 4, 5][:4 - num_reduce_dim]))
        d = 120 // batch_size
        lengths = np.random.randint(1, d, size=batch_size).astype(np.int32)

        def ref_mean(X, lengths):
            Y = X.reshape(lengths.size, d)
            rv = np.zeros((lengths.size, 1)).astype(np.float32)
            for ii in range(lengths.size):
                rv[ii] = np.mean(Y[ii, :lengths[ii]])
            return [rv.reshape((2, 3, 4, 5)[:4 - num_reduce_dim])]

        self.reduce_op_test(
            "ReduceBackMean", ref_mean, [X, lengths], ["input", "lengths"],
            num_reduce_dim, gc)