def is_product(n, den_tuple):
    r"""
    INPUT:

    - ``n`` - number of variables

    - ``den_tuple`` - tuple of pairs ``(vector, power)``

    TESTS::

        sage: from surface_dynamics.misc.generalized_multiple_zeta_values import is_product
        sage: is_product(3, [((1,0,0),2), ((0,1,0),5), ((1,1,0),1), ((0,0,1),3)])
        [(2, (((0, 1), 5), ((1, 0), 2), ((1, 1), 1))), (1, (((1), 3),))]
        sage: is_product(3, [((1,0,0),2), ((0,1,0),3), ((1,0,1),1), ((0,0,1),5)])
        [(2, (((0, 1), 5), ((1, 0), 2), ((1, 1), 1))), (1, (((1), 3),))]
        sage: is_product(3, [((1,1,1),3)]) is None
        True
    """
    D = DisjointSet(n)
    assert all(len(v) == n for v, p in den_tuple), (n, den_tuple)

    # 1. product structure
    for v, _ in den_tuple:
        i0 = 0
        while not v[i0]:
            i0 += 1
        i = i0 + 1
        while i < n:
            if v[i]:
                D.union(i0, i)
            i += 1
        if D.number_of_subsets() == 1:
            # no way to split variables
            return

    # split variables
    Rdict = D.root_to_elements_dict()
    keys = sorted(Rdict.keys())
    key_indices = {k: i for i, k in enumerate(keys)}
    values = [Rdict[k] for k in keys]
    values_indices = [{v: i for i, v in enumerate(v)} for v in values]
    n_list = [len(J) for J in values]
    F = [FreeModule(ZZ, nn) for nn in n_list]
    new_terms = [[] for _ in range(len(Rdict))]
    for v, p in den_tuple:
        i0 = 0
        while not v[i0]:
            i0 += 1
        i0 = D.find(i0)
        assert all(D.find(i) == i0 for i in range(n)
                   if v[i]), (i0, [D.find(i) for i in range(n) if v[i]])
        k = key_indices[i0]
        vv = F[k]()
        for i in range(n):
            if v[i]:
                vv[values_indices[k][i]] = v[i]
        vv.set_immutable()
        new_terms[k].append((vv, p))

    return list(zip(n_list, [tuple(sorted(terms)) for terms in new_terms]))
Exemplo n.º 2
0
    def decomposition(self):
        """
        Find the decomposition of the matroid as a direct sum of indecomposable matroids.
        Return a partition of the groundset.
        Uses the algorithm of [PP19, Section 7].
        """
        B = self.basis()
        new_groundset = list(B) + list(self.groundset().difference(B))

        # construct matrix with permuted columns
        columns = [vector(self._A[:,self._groundset_to_index[e]]) for e in new_groundset]
        A = matrix(ZZ, columns).transpose().echelon_form()

        uf = DisjointSet(self.groundset())

        for i in xrange(A.nrows()):
            for j in xrange(i+1, A.ncols()):
                if A[i,j] != 0:
                    uf.union(new_groundset[i], new_groundset[j])

        return SetPartition(uf)
Exemplo n.º 3
0
def is_partial_cube(G, certificate=False):
    r"""
    Test whether the given graph is a partial cube.

    A partial cube is a graph that can be isometrically embedded into a
    hypercube, i.e., its vertices can be labelled with (0,1)-vectors of some
    fixed length such that the distance between any two vertices in the graph
    equals the Hamming distance of their labels.

    Originally written by D. Eppstein for the PADS library
    (http://www.ics.uci.edu/~eppstein/PADS/), see also
    [Epp2008]_.  The algorithm runs in `O(n^2)` time, where `n`
    is the number of vertices. See the documentation of
    :mod:`~sage.graphs.partial_cube` for an overview of the algorithm.

    INPUT:

    - ``certificate`` -- boolean (default: ``False``); this function returns
      ``True`` or ``False`` according to the graph, when ``certificate =
      False``. When ``certificate = True`` and the graph is a partial cube, the
      function returns ``(True, mapping)``, where ``mapping`` is an isometric
      mapping of the vertices of the graph to the vertices of a hypercube
      ((0, 1)-strings of a fixed length). When ``certificate = True`` and the
      graph is not a partial cube, ``(False, None)`` is returned.

    EXAMPLES:

    The Petersen graph is not a partial cube::

        sage: g = graphs.PetersenGraph()
        sage: g.is_partial_cube()
        False

    All prisms are partial cubes::

        sage: g = graphs.CycleGraph(10).cartesian_product(graphs.CompleteGraph(2))
        sage: g.is_partial_cube()
        True

    TESTS:

    The returned mapping is an isometric embedding into a hypercube::

        sage: g = graphs.DesarguesGraph()
        sage: _, m = g.is_partial_cube(certificate=True)
        sage: m # random
        {0: '00000',
         1: '00001',
         2: '00011',
         3: '01011',
         4: '11011',
         5: '11111',
         6: '11110',
         7: '11100',
         8: '10100',
         9: '00100',
         10: '01000',
         11: '10001',
         12: '00111',
         13: '01010',
         14: '11001',
         15: '10111',
         16: '01110',
         17: '11000',
         18: '10101',
         19: '00110'}
        sage: all(all(g.distance(u, v) == len([i for i in range(len(m[u])) if m[u][i] != m[v][i]]) for v in m) for u in m)
        True

    A graph without vertices is trivially a partial cube::

        sage: Graph().is_partial_cube(certificate=True)
        (True, {})

    """
    G._scream_if_not_simple()

    if not G.order():
        if certificate:
            return (True, {})
        else:
            return True

    if certificate:
        fail = (False, None)
    else:
        fail = False

    if not G.is_connected():
        return fail
    n = G.order()

    # Initial sanity check: are there few enough edges?
    # Needed so that we don't try to use union-find on a dense
    # graph and incur superquadratic runtimes.
    if 1 << (2 * G.size() // n) > n:
        return fail

    # Check for bipartiteness.
    # This ensures also that each contraction will be bipartite.
    if not G.is_bipartite():
        return fail

    # Set up data structures for algorithm:
    # - contracted: contracted graph at current stage of algorithm
    # - unionfind: union find data structure representing known edge equivalences
    # - available: limit on number of remaining available labels
    from sage.graphs.digraph import DiGraph
    from sage.graphs.graph import Graph
    from sage.sets.disjoint_set import DisjointSet
    contracted = DiGraph({v: {w: (v, w) for w in G[v]} for v in G})
    unionfind = DisjointSet(contracted.edges(labels=False))
    available = n - 1

    # Main contraction loop in place of the original algorithm's recursion
    while contracted.order() > 1:
        # Find max degree vertex in contracted, and update label limit
        deg, root = max([(contracted.out_degree(v), v) for v in contracted],
                        key=lambda x: x[0])
        if deg > available:
            return fail
        available -= deg

        # Set up bitvectors on vertices
        bitvec = {v: 0 for v in contracted}
        neighbors = {}
        for i, neighbor in enumerate(contracted[root]):
            bitvec[neighbor] = 1 << i
            neighbors[1 << i] = neighbor

        # Breadth first search to propagate bitvectors to the rest of the graph
        for level in breadth_first_level_search(contracted, root):
            for v in level:
                for w in level[v]:
                    bitvec[w] |= bitvec[v]

        # Make graph of labeled edges and union them together
        labeled = Graph([contracted.vertices(), []])
        for v, w in contracted.edge_iterator(labels=False):
            diff = bitvec[v] ^ bitvec[w]
            if not diff or not bitvec[w] & ~bitvec[v]:
                continue  # zero edge or wrong direction
            if diff not in neighbors:
                return fail
            neighbor = neighbors[diff]
            unionfind.union(contracted.edge_label(v, w),
                            contracted.edge_label(root, neighbor))
            unionfind.union(contracted.edge_label(w, v),
                            contracted.edge_label(neighbor, root))
            labeled.add_edge(v, w)

        # Map vertices to components of labeled-edge graph
        component = {}
        for i, SCC in enumerate(labeled.connected_components()):
            for v in SCC:
                component[v] = i

        # generate new compressed subgraph
        newgraph = DiGraph()
        for v, w, t in contracted.edge_iterator():
            if bitvec[v] == bitvec[w]:
                vi = component[v]
                wi = component[w]
                if vi == wi:
                    return fail
                if newgraph.has_edge(vi, wi):
                    unionfind.union(newgraph.edge_label(vi, wi), t)
                else:
                    newgraph.add_edge(vi, wi, t)
        contracted = newgraph

    # Make a digraph with edges labeled by the equivalence classes in unionfind
    g = DiGraph({v: {w: unionfind.find((v, w)) for w in G[v]} for v in G})

    # Associates to a vertex the token that acts on it, an check that
    # no two edges on a single vertex have the same label
    action = {}
    for v in g:
        action[v] = set(t for _, _, t in g.edge_iterator(v))
        if len(action[v]) != g.out_degree(v):
            return fail

    # Associate every token to its reverse
    reverse = {}
    for v, w, t in g.edge_iterator():
        rt = g.edge_label(w, v)
        reverse[t] = rt
        reverse[rt] = t

    current = initialState = next(g.vertex_iterator())

    # A token T is said to be 'active' for a vertex u if it takes u
    # one step closer to the source in terms of distance. The 'source'
    # is initially 'initialState'. See the module's documentation for
    # more explanations.

    # Find list of tokens that lead to the initial state
    activeTokens = set()
    for level in breadth_first_level_search(g, initialState):
        for v in level:
            for w in level[v]:
                activeTokens.add(g.edge_label(w, v))
    for t in activeTokens:
        if reverse[t] in activeTokens:
            return fail
    activeTokens = list(activeTokens)

    # Rest of data structure: point from states to list and list to states
    state_to_active_token = {v: -1 for v in g}
    token_to_states = [[] for i in activeTokens
                       ]  # (i.e. vertices on which each token acts)

    def scan(v):
        """Find the next token that is effective for v."""
        a = next(
            i for i in range(state_to_active_token[v] + 1, len(activeTokens))
            if activeTokens[i] is not None and activeTokens[i] in action[v])
        state_to_active_token[v] = a
        token_to_states[a].append(v)

    # Initialize isometric embedding into a hypercube
    if certificate:
        dim = 0
        tokmap = {}
        for t in reverse:
            if t not in tokmap:
                tokmap[t] = tokmap[reverse[t]] = 1 << dim
                dim += 1
        embed = {initialState: 0}

    # Set initial active states
    for v in g:
        if v != current:
            try:
                scan(v)
            except StopIteration:
                return fail

    # Traverse the graph, maintaining active tokens
    for prev, current, fwd in depth_first_traversal(g, initialState):
        if not fwd:
            prev, current = current, prev
        elif certificate:
            embed[current] = embed[prev] ^ tokmap[g.edge_label(prev, current)]

        # Add token to end of list, point to it from old state
        activeTokens.append(g.edge_label(prev, current))
        state_to_active_token[prev] = len(activeTokens) - 1
        token_to_states.append([prev])

        # Inactivate reverse token, find new token for its states
        #
        # (the 'active' token of 'current' is necessarily the label of
        #  (current, previous))
        activeTokens[state_to_active_token[current]] = None
        for v in token_to_states[state_to_active_token[current]]:
            if v != current:
                try:
                    scan(v)
                except StopIteration:
                    return fail

    # All checks passed, return the result
    if certificate:
        format = "{0:0%db}" % dim
        return (True, {v: format.format(l) for v, l in embed.items()})
    else:
        return True
Exemplo n.º 4
0
    def poset_of_layers(self):
        """
        Compute the poset of layers of the associated toric arrangement, using Lenz's algorithm [Len17a].
        """
        # TODO: implement for Q != 0
        if self._Q.ncols() > 0:
            raise NotImplementedError


        A = self._A.transpose()
        E = range(A.nrows())

        data = {}

        # compute Smith normal forms of all submatrices
        for S in powerset(E):
            D, U, V = A[S,:].smith_form()   # D == U*A[S,:]*V
            diagonal = [D[i,i] if i < D.ncols() else 0 for i in xrange(len(S))]
            data[tuple(S)] = (diagonal, U)

        # generate al possible elements of the poset of layers
        elements = {tuple(S): list(vector(ZZ, x) for x in itertools.product(*(range(max(data[tuple(S)][0][i], 1)) for i in xrange(len(S))))) for S in powerset(E)}

        for l in elements.itervalues():
            for v in l:
                v.set_immutable()

        possible_layers = list((S, x) for (S, l) in elements.iteritems() for x in l)
        uf = DisjointSet(possible_layers)

        cover_relations = []

        for (S, l) in elements.iteritems():
            diagonal_S, U_S = data[S]
            rk_S = A[S,:].rank()

            for s in S:
                i = S.index(s)  # index where the element s appears in S
                T = tuple(t for t in S if t != s)

                diagonal_T, U_T = data[T]
                rk_T = A[T,:].rank()

                for x in l:
                    h = (S, x)

                    y = U_S**(-1) * x
                    z = U_T * vector(ZZ, y[:i].list() + y[i+1:].list())
                    w = vector(ZZ, (a % diagonal_T[j] if diagonal_T[j] > 0 else 0 for j, a in enumerate(z)))
                    w.set_immutable()

                    ph = (T, w)

                    if rk_S == rk_T:
                        uf.union(h, ph)

                    else:
                        cover_relations.append((ph, h))

        # find representatives for layers (we keep the representative (S,x) with maximal S)
        root_to_representative_dict = {}
        for root, subset in uf.root_to_elements_dict().iteritems():
            S, x = max(subset, key=lambda (S, x): len(S))
            S_labeled = tuple(self._E[i] for i in S)
            root_to_representative_dict[root] = (S_labeled, x)

        # get layers and cover relations
        layers = root_to_representative_dict.values()
        cover_relations = set(
            (root_to_representative_dict[uf.find(a)], root_to_representative_dict[uf.find(b)])
            for (a,b) in cover_relations)

        return Poset(data=(layers, cover_relations), cover_relations=True)
Exemplo n.º 5
0
    def is_equivalent(self, other, morphism=None):
        """
        Check if the two ToricArithmeticMatroids are equivalent,
        i.e. the defining representations are equivalent (see [PP19, Section 2]).
        If morphism is None, assume that the groundsets coincide.
        """
        if not isinstance(other, ToricArithmeticMatroid):
            raise TypeError("can only test for equivalence between toric arithmetic matroids.")

        if self._Q.ncols() > 0 or other._Q.ncols() > 0:
            # TODO
            raise NotImplementedError

        if morphism is None:
            assert self.groundset() == other.groundset()
            morphism = {e: e for e in self.groundset()}

        E = self._E

        # take matrices in Hermite normal form, removing zero rows
        # (copy is needed to make matrices mutable)
        M = copy.copy(self._A.echelon_form(include_zero_rows=False))
        N = copy.copy(other._A[:, [other._groundset_to_index[morphism[e]] for e in self._E]].echelon_form(include_zero_rows=False))

        # choose a basis
        B = self.basis()
        if not other.is_basis(frozenset(morphism[e] for e in B)):
            return False

        # find bipartite graph
        edges = []
        for x in B:
            for y in E:
                C = B.difference([x]).union([y])
                if y not in B and self.is_basis(C):
                    if not other.is_basis(frozenset(morphism[e] for e in C)):
                        return False

                    edges.append((x,y))

        spanning_forest = nx.Graph()

        # find spanning forest
        uf = DisjointSet(E)
        for (x,y) in edges:
            if uf.find(x) != uf.find(y):
                spanning_forest.add_edge(x,y)
                uf.union(x,y)

        B_indices = list(sorted(self._groundset_to_index[e] for e in B))

        M1 = M[:, B_indices].inverse() * M
        N1 = N[:, B_indices].inverse() * N

        def change_signs(A, A1):
            for (i,j) in nx.edge_dfs(spanning_forest):
                (x,y) = (i,j) if i in B else (j,i)
                if A1[x,y] < 0:
                    if j in B:
                        # change sign of row j and column j
                        A1[j,:] *= -1
                        A1[:,j] *= -1

                        A[j,:] *= -1
                        A[:,j] *= -1

                        assert A1 == A[:, B_indices].inverse() * A

                    else:
                        # change sign of column j
                        A1[:,j] *= -1

                        A[:,j] *= -1

        change_signs(M, M1)
        change_signs(N, N1)

        return M.echelon_form() == N.echelon_form()
Exemplo n.º 6
0
    def _representation_surjective(self, ordered_groundset=None, check_bases=False):
        """
        Find a representation (if it exists) for a surjective matroid (m(emptyset)=m(E)=1).
        If check_bases==True, find a representation of a matroid (E,rk,m')
        such that m'(B)=m(B) for every basis B.
        Return None if no representation exists.
        """
        assert self.full_multiplicity() == 1

        r = self.full_rank()
        n = len(self.groundset())

        if ordered_groundset is not None:
            # use the groundset in the given order
            E = ordered_groundset
            assert frozenset(E) == self.groundset()
            assert len(E) == len(self.groundset())
        else:
            # sort the groundset
            E = list(sorted(self.groundset()))

        B = self.basis()
        # print "Basis:", B

        # find bipartite graph
        edges = [(x,y) for x in B for y in E if y not in B and self.is_basis(B.difference([x]).union([y]))]

        spanning_forest = nx.Graph()

        # find spanning forest
        uf = DisjointSet(E)
        for (x,y) in edges:
            if uf.find(x) != uf.find(y):
                spanning_forest.add_edge(x,y)
                uf.union(x,y)

        # print "Graph:", edges
        # print "Spanning forest:", spanning_forest.edges()

        # fix an order of B
        B_ordered = list(sorted(B))

        # compute entries of matrix A
        def entry(i,j):
            x = B_ordered[i]
            y = E[j]

            if y in B:
                return self.multiplicity(B) if x == y else 0

            elif (x,y) in edges:
                return self.multiplicity(B.difference([x]).union([y]))

            else:
                return 0

        A = matrix(ZZ, r, n, entry)
        # print A

        B_to_index = {B_ordered[i]: i for i in xrange(r)}
        E_to_index = {E[j]: j for j in xrange(n)}


        graph = spanning_forest
        while graph.number_of_edges() < len(edges):
            # find all paths in the graph
            paths = dict(nx.all_pairs_dijkstra_path(graph))
            for (x,y) in sorted(edges, key=lambda (x,y): len(paths[x][y])):
                if len(paths[x][y]) == 2:
                    # (x,y) is in the graph
                    assert (x,y) in graph.edges()
                    continue

                i = B_to_index[x]
                j = E_to_index[y]

                rows = [B_to_index[z] for z in paths[x][y][::2]]
                columns = [E_to_index[z] for z in paths[x][y][1::2]]

                # print x, y
                # print "rows:", rows
                # print "columns:", columns

                new_tuple = [z for z in B_ordered + paths[x][y] if z not in B or z not in paths[x][y]]
                # print "new_tuple:", new_tuple
                expected_mult = self.multiplicity(new_tuple) * self.multiplicity(B)**(len(rows)-1) if self.rank(new_tuple) == r else 0
                if abs(A[rows,columns].determinant()) != expected_mult:
                    # change sign
                    # print "change sign!"
                    # print A[rows,columns].determinant()
                    A[i,j] = -A[i,j]

                    if abs(A[rows,columns].determinant()) != expected_mult:
                        # print A
                        # print A[rows,columns].determinant(), expected_mult
                        return None

                graph.add_edge(x,y)
                break

        D, U, V = A.smith_form()
        res = V.inverse()[:r,:]
        res = matrix(ZZ, res)

        # print >> sys.stderr, "Candidate representation:"
        # print >> sys.stderr, res

        # check if this is indeed a representation
        if not self.check_representation(res, ordered_groundset=ordered_groundset, check_bases=check_bases):
            return None

        return res
Exemplo n.º 7
0
def is_partial_cube(G, certificate=False):
    r"""
    Test whether the given graph is a partial cube.

    A partial cube is a graph that can be isometrically embedded into a
    hypercube, i.e., its vertices can be labelled with (0,1)-vectors of some
    fixed length such that the distance between any two vertices in the graph
    equals the Hamming distance of their labels.

    Originally written by D. Eppstein for the PADS library
    (http://www.ics.uci.edu/~eppstein/PADS/), see also
    [Eppstein2008]_.  The algorithm runs in `O(n^2)` time, where `n`
    is the number of vertices. See the documentation of
    :mod:`~sage.graphs.partial_cube` for an overview of the algorithm.

    INPUT:

    - ``certificate`` (boolean; ``False``) -- The function returns ``True``
      or ``False`` according to the graph, when ``certificate = False``. When
      ``certificate = True`` and the graph is a partial cube, the function
      returns ``(True, mapping)``, where ``mapping`` is an isometric mapping of
      the vertices of the graph to the vertices of a hypercube ((0, 1)-strings
      of a fixed length). When ``certificate = True`` and the graph is not a
      partial cube, ``(False, None)`` is returned.

    EXAMPLES:

    The Petersen graph is not a partial cube::

        sage: g = graphs.PetersenGraph()
        sage: g.is_partial_cube()
        False

    All prisms are partial cubes::

        sage: g = graphs.CycleGraph(10).cartesian_product(graphs.CompleteGraph(2))
        sage: g.is_partial_cube()
        True

    TESTS:

    The returned mapping is an isometric embedding into a hypercube::

        sage: g = graphs.DesarguesGraph()
        sage: _, m = g.is_partial_cube(certificate = True)
        sage: m # random
        {0: '00000',
         1: '00001',
         2: '00011',
         3: '01011',
         4: '11011',
         5: '11111',
         6: '11110',
         7: '11100',
         8: '10100',
         9: '00100',
         10: '01000',
         11: '10001',
         12: '00111',
         13: '01010',
         14: '11001',
         15: '10111',
         16: '01110',
         17: '11000',
         18: '10101',
         19: '00110'}
        sage: all(all(g.distance(u, v) == len([i for i in range(len(m[u])) if m[u][i] != m[v][i]]) for v in m) for u in m)
        True

    A graph without vertices is trivially a partial cube::

        sage: Graph().is_partial_cube(certificate = True)
        (True, {})

    """
    G._scream_if_not_simple()

    if G.order() == 0:
        if certificate:
            return (True, {})
        else:
            return True

    if certificate:
        fail = (False, None)
    else:
        fail = False

    if not G.is_connected():
        return fail
    n = G.order()

    # Initial sanity check: are there few enough edges?
    # Needed so that we don't try to use union-find on a dense
    # graph and incur superquadratic runtimes.
    if 1 << (2*G.size()//n) > n:
        return fail

    # Check for bipartiteness.
    # This ensures also that each contraction will be bipartite.
    if not G.is_bipartite():
        return fail

    # Set up data structures for algorithm:
    # - contracted: contracted graph at current stage of algorithm
    # - unionfind: union find data structure representing known edge equivalences
    # - available: limit on number of remaining available labels
    from sage.graphs.digraph import DiGraph
    from sage.graphs.graph import Graph
    from sage.sets.disjoint_set import DisjointSet
    contracted = DiGraph({v: {w: (v, w) for w in G[v]} for v in G})
    unionfind = DisjointSet(contracted.edges(labels = False))
    available = n-1

    # Main contraction loop in place of the original algorithm's recursion
    while contracted.order() > 1:
        # Find max degree vertex in contracted, and update label limit
        deg, root = max((contracted.out_degree(v), v) for v in contracted)
        if deg > available:
            return fail
        available -= deg

        # Set up bitvectors on vertices
        bitvec = {v:0 for v in contracted}
        neighbors = {}
        for i, neighbor in enumerate(contracted[root]):
            bitvec[neighbor] = 1 << i
            neighbors[1 << i] = neighbor

        # Breadth first search to propagate bitvectors to the rest of the graph
        for level in breadth_first_level_search(contracted, root):
            for v in level:
                for w in level[v]:
                    bitvec[w] |= bitvec[v]

        # Make graph of labeled edges and union them together
        labeled = Graph([contracted.vertices(), []])
        for v, w in contracted.edge_iterator(labels = False):
            diff = bitvec[v]^bitvec[w]
            if not diff or bitvec[w] &~ bitvec[v] == 0:
                continue    # zero edge or wrong direction
            if diff not in neighbors:
                return fail
            neighbor = neighbors[diff]
            unionfind.union(contracted.edge_label(v, w),
                            contracted.edge_label(root, neighbor))
            unionfind.union(contracted.edge_label(w, v),
                            contracted.edge_label(neighbor, root))
            labeled.add_edge(v, w)

        # Map vertices to components of labeled-edge graph
        component = {}
        for i, SCC in enumerate(labeled.connected_components()):
            for v in SCC:
                component[v] = i

        # generate new compressed subgraph
        newgraph = DiGraph()
        for v, w, t in contracted.edge_iterator():
            if bitvec[v] == bitvec[w]:
                vi = component[v]
                wi = component[w]
                if vi == wi:
                    return fail
                if newgraph.has_edge(vi, wi):
                    unionfind.union(newgraph.edge_label(vi, wi), t)
                else:
                    newgraph.add_edge(vi, wi, t)
        contracted = newgraph

    # Make a digraph with edges labeled by the equivalence classes in unionfind
    g = DiGraph({v: {w: unionfind.find((v, w)) for w in G[v]} for v in G})

    # Associates to a vertex the token that acts on it, an check that
    # no two edges on a single vertex have the same label
    action = {}
    for v in g:
        action[v] = set(t for _, _, t in g.edge_iterator(v))
        if len(action[v]) != g.out_degree(v):
            return fail

    # Associate every token to its reverse
    reverse = {}
    for v, w, t in g.edge_iterator():
        rt = g.edge_label(w, v)
        reverse[t] = rt
        reverse[rt] = t

    current = initialState = next(g.vertex_iterator())

    # A token T is said to be 'active' for a vertex u if it takes u
    # one step closer to the source in terms of distance. The 'source'
    # is initially 'initialState'. See the module's documentation for
    # more explanations.

    # Find list of tokens that lead to the initial state
    activeTokens = set()
    for level in breadth_first_level_search(g, initialState):
        for v in level:
            for w in level[v]:
                activeTokens.add(g.edge_label(w, v))
    for t in activeTokens:
        if reverse[t] in activeTokens:
            return fail
    activeTokens = list(activeTokens)

    # Rest of data structure: point from states to list and list to states
    state_to_active_token = {v: -1 for v in g}
    token_to_states = [[] for i in activeTokens] # (i.e. vertices on which each token acts)

    def scan(v):
        """Find the next token that is effective for v."""
        a = next(i for i in range(state_to_active_token[v]+1, len(activeTokens))
                 if activeTokens[i] is not None
                    and activeTokens[i] in action[v])
        state_to_active_token[v] = a
        token_to_states[a].append(v)

    # Initialize isometric embedding into a hypercube
    if certificate:
        dim = 0
        tokmap = {}
        for t in reverse:
            if t not in tokmap:
                tokmap[t] = tokmap[reverse[t]] = 1 << dim
                dim += 1
        embed = {initialState: 0}

    # Set initial active states
    for v in g:
        if v != current:
            try:
                scan(v)
            except StopIteration:
                return fail

    # Traverse the graph, maintaining active tokens
    for prev, current, fwd in depth_first_traversal(g, initialState):
        if not fwd:
            prev, current = current, prev
        elif certificate:
            embed[current] = embed[prev] ^ tokmap[g.edge_label(prev, current)]

        # Add token to end of list, point to it from old state
        activeTokens.append(g.edge_label(prev, current))
        state_to_active_token[prev] = len(activeTokens) - 1
        token_to_states.append([prev])

        # Inactivate reverse token, find new token for its states
        #
        # (the 'active' token of 'current' is necessarily the label of
        #  (current, previous))
        activeTokens[state_to_active_token[current]] = None
        for v in token_to_states[state_to_active_token[current]]:
            if v != current:
                try:
                    scan(v)
                except StopIteration:
                    return fail

    # All checks passed, return the result
    if certificate:
        format = "{0:0%db}" % dim
        return (True, {v: format.format(l) for v, l in embed.items()})
    else:
        return True
Exemplo n.º 8
0
def RandomBlockGraph(m, k, kmax=None, incidence_structure=False):
    r"""
    Return a Random Block Graph.

    A block graph is a connected graph in which every biconnected component
    (block) is a clique.

    .. SEEALSO::

        - :wikipedia:`Block_graph` for more details on these graphs
        - :meth:`~sage.graphs.graph.Graph.is_block_graph` -- test if a graph is a block graph
        - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices`
        - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cuts_tree`
        - :meth:`~sage.combinat.designs.incidence_structures.IncidenceStructure` 

    INPUT:

    - ``m`` -- integer; number of blocks (at least one).

    - ``k`` -- integer; minimum number of vertices of a block (at least two).

    - ``kmax`` -- integer (default: ``None``) By default, each block has `k`
      vertices. When the parameter `kmax` is specified (with `kmax \geq k`), the
      number of vertices of each block is randomly chosen between `k` and
      `kmax`.

    - ``incidence_structure`` -- boolean (default: ``False``) when set to
      ``True``, the incidence structure of the graphs is returned instead of the
      graph itself, that is the list of the lists of vertices in each
      block. This is useful for the creation of some hypergraphs.

    OUTPUT:

    A Graph when ``incidence_structure==False`` (default), and otherwise an
    incidence structure.

    EXAMPLES:

    A block graph with a single block is a clique::

        sage: B = graphs.RandomBlockGraph(1, 4)
        sage: B.is_clique()
        True

    A block graph with blocks of order 2 is a tree::

        sage: B = graphs.RandomBlockGraph(10, 2)
        sage: B.is_tree()
        True

    Every biconnected component of a block graph is a clique::

        sage: B = graphs.RandomBlockGraph(5, 3, kmax=6)
        sage: blocks,cuts = B.blocks_and_cut_vertices()
        sage: all(B.is_clique(block) for block in blocks)
        True

    A block graph with blocks of order `k` has `m*(k-1)+1` vertices::

        sage: m, k = 6, 4
        sage: B = graphs.RandomBlockGraph(m, k)
        sage: B.order() == m*(k-1)+1
        True

    Test recognition methods::

        sage: B = graphs.RandomBlockGraph(6, 2, kmax=6)
        sage: B.is_block_graph()
        True
        sage: B in graph_classes.Block
        True

    Asking for the incidence structure::

        sage: m, k = 6, 4
        sage: IS = graphs.RandomBlockGraph(m, k, incidence_structure=True)
        sage: from sage.combinat.designs.incidence_structures import IncidenceStructure
        sage: IncidenceStructure(IS)
        Incidence structure with 19 points and 6 blocks
        sage: m*(k-1)+1
        19

    TESTS:

    A block graph has at least one block, so `m\geq 1`::

        sage: B = graphs.RandomBlockGraph(0, 1)
        Traceback (most recent call last):
        ...
        ValueError: the number `m` of blocks must be >= 1

    A block has at least 2 vertices, so `k\geq 2`::

        sage: B = graphs.RandomBlockGraph(1, 1)
        Traceback (most recent call last):
        ...
        ValueError: the minimum number `k` of vertices in a block must be >= 2

    The maximum size of a block is at least its minimum size, so `k\leq kmax`::

        sage: B = graphs.RandomBlockGraph(1, 3, kmax=2)
        Traceback (most recent call last):
        ...
        ValueError: the maximum number `kmax` of vertices in a block must be >= `k`
    """
    import itertools
    from sage.misc.prandom import choice
    from sage.sets.disjoint_set import DisjointSet

    if m < 1:
        raise ValueError("the number `m` of blocks must be >= 1")
    if k < 2:
        raise ValueError(
            "the minimum number `k` of vertices in a block must be >= 2")
    if kmax is None:
        kmax = k
    elif kmax < k:
        raise ValueError(
            "the maximum number `kmax` of vertices in a block must be >= `k`")

    if m == 1:
        # A block graph with a single block is a clique
        IS = [list(range(randint(k, kmax)))]

    elif kmax == 2:
        # A block graph with blocks of order 2 is a tree
        IS = [list(e) for e in RandomTree(m + 1).edges(labels=False)]

    else:
        # We start with a random tree of order m
        T = RandomTree(m)

        # We create a block of order in range [k,kmax] per vertex of the tree
        B = {u: [(u, i) for i in range(randint(k, kmax))] for u in T}

        # For each edge of the tree, we choose 1 vertex in each of the
        # corresponding blocks and we merge them. We use a disjoint set data
        # structure to keep a unique identifier per merged vertices
        DS = DisjointSet([i for u in B for i in B[u]])
        for u, v in T.edges(labels=0):
            DS.union(choice(B[u]), choice(B[v]))

        # We relabel vertices in the range [0, m*(k-1)] and build the incidence
        # structure
        new_label = {
            root: i
            for i, root in enumerate(DS.root_to_elements_dict())
        }
        IS = [[new_label[DS.find(v)] for v in B[u]] for u in B]

    if incidence_structure:
        return IS

    # We finally build the block graph
    if k == kmax:
        BG = Graph(
            name="Random Block Graph with {} blocks of order {}".format(m, k))
    else:
        BG = Graph(
            name="Random Block Graph with {} blocks of order {} to {}".format(
                m, k, kmax))
    for block in IS:
        BG.add_clique(block)
    return BG
Exemplo n.º 9
0
def RandomBlockGraph(m, k, kmax=None, incidence_structure=False):
    r"""
    Return a Random Block Graph.

    A block graph is a connected graph in which every biconnected component
    (block) is a clique.

    .. SEEALSO::

        - :wikipedia:`Block_graph` for more details on these graphs
        - :meth:`~sage.graphs.graph.Graph.is_block_graph` -- test if a graph is a block graph
        - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices`
        - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cuts_tree`
        - :meth:`~sage.combinat.designs.incidence_structures.IncidenceStructure` 

    INPUT:

    - ``m`` -- integer; number of blocks (at least one).

    - ``k`` -- integer; minimum number of vertices of a block (at least two).

    - ``kmax`` -- integer (default: ``None``) By default, each block has `k`
      vertices. When the parameter `kmax` is specified (with `kmax \geq k`), the
      number of vertices of each block is randomly chosen between `k` and
      `kmax`.

    - ``incidence_structure`` -- boolean (default: ``False``) when set to
      ``True``, the incidence structure of the graphs is returned instead of the
      graph itself, that is the list of the lists of vertices in each
      block. This is useful for the creation of some hypergraphs.

    OUTPUT:

    A Graph when ``incidence_structure==False`` (default), and otherwise an
    incidence structure.

    EXAMPLES:

    A block graph with a single block is a clique::

        sage: B = graphs.RandomBlockGraph(1, 4)
        sage: B.is_clique()
        True

    A block graph with blocks of order 2 is a tree::

        sage: B = graphs.RandomBlockGraph(10, 2)
        sage: B.is_tree()
        True

    Every biconnected component of a block graph is a clique::

        sage: B = graphs.RandomBlockGraph(5, 3, kmax=6)
        sage: blocks,cuts = B.blocks_and_cut_vertices()
        sage: all(B.is_clique(block) for block in blocks)
        True

    A block graph with blocks of order `k` has `m*(k-1)+1` vertices::

        sage: m, k = 6, 4
        sage: B = graphs.RandomBlockGraph(m, k)
        sage: B.order() == m*(k-1)+1
        True

    Test recognition methods::

        sage: B = graphs.RandomBlockGraph(6, 2, kmax=6)
        sage: B.is_block_graph()
        True
        sage: B in graph_classes.Block
        True

    Asking for the incidence structure::

        sage: m, k = 6, 4
        sage: IS = graphs.RandomBlockGraph(m, k, incidence_structure=True)
        sage: from sage.combinat.designs.incidence_structures import IncidenceStructure
        sage: IncidenceStructure(IS)
        Incidence structure with 19 points and 6 blocks
        sage: m*(k-1)+1
        19

    TESTS:

    A block graph has at least one block, so `m\geq 1`::

        sage: B = graphs.RandomBlockGraph(0, 1)
        Traceback (most recent call last):
        ...
        ValueError: the number `m` of blocks must be >= 1

    A block has at least 2 vertices, so `k\geq 2`::

        sage: B = graphs.RandomBlockGraph(1, 1)
        Traceback (most recent call last):
        ...
        ValueError: the minimum number `k` of vertices in a block must be >= 2

    The maximum size of a block is at least its minimum size, so `k\leq kmax`::

        sage: B = graphs.RandomBlockGraph(1, 3, kmax=2)
        Traceback (most recent call last):
        ...
        ValueError: the maximum number `kmax` of vertices in a block must be >= `k`
    """
    import itertools
    from sage.misc.prandom import choice
    from sage.sets.disjoint_set import DisjointSet

    if m < 1:
        raise ValueError("the number `m` of blocks must be >= 1")
    if k < 2:
        raise ValueError("the minimum number `k` of vertices in a block must be >= 2")
    if kmax is None:
        kmax = k
    elif kmax < k:
        raise ValueError("the maximum number `kmax` of vertices in a block must be >= `k`")

    if m == 1:
        # A block graph with a single block is a clique
        IS = [ list(range(randint(k, kmax))) ]
        
    elif kmax == 2:
        # A block graph with blocks of order 2 is a tree
        IS = [ list(e) for e in RandomTree(m+1).edges(labels=False) ]

    else:
        # We start with a random tree of order m
        T = RandomTree(m)

        # We create a block of order in range [k,kmax] per vertex of the tree
        B = {u:[(u,i) for i in range(randint(k, kmax))] for u in T}

        # For each edge of the tree, we choose 1 vertex in each of the
        # corresponding blocks and we merge them. We use a disjoint set data
        # structure to keep a unique identifier per merged vertices
        DS = DisjointSet([i for u in B for i in B[u]])
        for u,v in T.edges(labels=0):
            DS.union(choice(B[u]), choice(B[v]))

        # We relabel vertices in the range [0, m*(k-1)] and build the incidence
        # structure
        new_label = {root:i for i,root in enumerate(DS.root_to_elements_dict())}
        IS = [ [new_label[DS.find(v)] for v in B[u]] for u in B ]

    if incidence_structure:
        return IS
    
    # We finally build the block graph
    if k == kmax:
        BG = Graph(name = "Random Block Graph with {} blocks of order {}".format(m, k))
    else:
        BG = Graph(name = "Random Block Graph with {} blocks of order {} to {}".format(m, k, kmax))
    for block in IS:
        BG.add_clique( block )
    return BG