Пример #1
0
def swap(clique: list, graph: nx.Graph, node_select: str = "uniform") -> list:
    """If possible, generates a new clique by swapping a node in the input clique with a node
    outside the clique.

    Proceeds by calculating the set :math:`C_1` of nodes in the rest of the graph that are
    connected to all but one of the nodes in the clique. If this set is not empty, this function
    randomly picks a node and swaps it with the corresponding node in the clique that is not
    connected to it. The set :math:`C_1` and corresponding nodes in the clique are provided by the
    :func:`~strawberryfields.gbs.graph.utils.c_1` function.

    Whenever there are multiple nodes within :math:`C_1`, one must choose which node to add to
    the growing clique. This function allows a method of choosing nodes to be set with the
    ``node_select`` argument, with node selection based on uniform randomness and node degree
    supported. Degree-based node selection involves picking the node with the greatest degree,
    with ties settled by uniform random choice.

    **Example usage:**

    >>> graph = nx.wheel_graph(5)
    >>> graph.remove_edge(0, 4)
    >>> clique = [0, 1, 2]
    >>> swap(clique, graph)
    [0, 2, 3]

    Args:
        clique (list[int]): a subgraph specified by a list of nodes; the subgraph must be a clique
        graph (nx.Graph): the input graph
        node_select (str): method of selecting nodes from :math:`C_0` during growth. Can be either
            ``"uniform"`` for uniform random selection or ``"degree"`` for degree-based selection.
            Defaults to ``"uniform"``.

       Returns:
           list[int]: a new clique subgraph of equal size as the input
       """

    if not utils.is_subgraph(clique, graph):
        raise ValueError("Input is not a valid subgraph")

    if not utils.is_clique(graph.subgraph(clique)):
        raise ValueError("Input subgraph is not a clique")

    clique = set(clique)
    c_1 = utils.c_1(clique, graph)

    if c_1:
        if node_select == "uniform":
            swap_index = np.random.choice(len(c_1))
            swap_nodes = c_1[swap_index]
        elif node_select == "degree":
            degrees = np.array([graph.degree(n[1]) for n in c_1])
            to_swap_index = np.random.choice(
                np.where(degrees == degrees.max())[0])
            swap_nodes = c_1[to_swap_index]
        else:
            raise ValueError("Node selection method not recognized")

        clique.remove(swap_nodes[0])
        clique.add(swap_nodes[1])

    return sorted(clique)
Пример #2
0
    def test_invalid_subgraphs(self, graph, dim):
        """Test if function returns ``False`` when fed invalid subgraphs. Note that graph nodes are
        numbered as [0, 1, 4, 9, ...] (i.e., squares of the usual list)."""
        graph = nx.relabel_nodes(graph, lambda x: x**2)
        subgraphs = itertools.combinations(range(dim), int(dim / 2))

        assert not all([utils.is_subgraph(list(s), graph) for s in subgraphs])
Пример #3
0
def greedy_degree(subgraphs: Iterable,
                  graph: nx.Graph,
                  target: int,
                  resize_options: Optional[dict] = None) -> list:
    """Method to greedily resize subgraphs based upon vertex degree.

    This function uses a greedy approach to iteratively add/remove nodes to an input subgraph.
    Suppose the input subgraph has :math:`M` nodes. When shrinking, the algorithm considers
    all the :math:`M` nodes and removes the one with the lowest degree (number of incident
    edges). It then repeats the process for the resultant :math:`M-1`-node subgraph,
    and continues until the desired subgraph size has been reached.

    When growing, the algorithm considers all :math:`N-M` nodes that are within the
    :math:`N`-node graph but not within the :math:`M` node subgraph, and then picks the node with
    the highest degree to create an :math:`M+1`-node subgraph. It then considers all
    :math:`N-M-1` nodes in the complement of the :math:`M+1`-node subgraph and adds the node with
    the highest degree, continuing until the desired subgraph size has been reached.

    ``resize_options`` is not currently in use in :func:`greedy_degree`.

    Args:
        subgraphs (iterable[list[int]]): an iterable over subgraphs, where each subgraph is given
            by a list of nodes
        graph (nx.Graph): the input graph
        target (int): the target number of nodes in the subgraph (must be
            larger than 1)
        resize_options (dict[str, Any]): dictionary specifying options used during resizing;
            defaults to :const:`RESIZE_DEFAULTS`

    Returns:
        list[list[int]]: a list of resized subgraphs, where each subgraph is given by a list of
        nodes
    """
    # Note that ``resize_options`` will become non-trivial once randomness is added to methods
    resize_options = {**RESIZE_DEFAULTS, **(resize_options or {})}

    nodes = graph.nodes

    resized = []

    for s in subgraphs:
        s = set(s)

        if not utils.is_subgraph(s, graph):
            raise ValueError("Input is not a valid subgraph")

        while len(s) != target:
            if len(s) < target:
                n_candidates = graph.degree(set(nodes) - s)
                n_opt = max(n_candidates, key=lambda x: x[1])[0]
                s |= {n_opt}
            else:
                n_candidates = graph.degree(s)
                n_opt = min(n_candidates, key=lambda x: x[1])[0]
                s -= {n_opt}

        resized.append(sorted(s))

    return resized
Пример #4
0
def grow(clique: list, graph: nx.Graph, node_select: str = "uniform") -> list:
    """Iteratively adds new nodes to the input clique to generate a larger clique.

    Each iteration involves calculating the set :math:`C_0` (provided by the function
    :func:`~strawberryfields.gbs.graph.utils.c_0`) with respect to the current clique. This set
    represents the nodes in the rest of the graph that are connected to all of the nodes in the
    current clique. Therefore, adding any of the nodes in :math:`C_0` will create a larger clique.
    This function proceeds by repeatedly evaluating :math:`C_0` and selecting and adding a node
    from this set to add to the current clique. Growth is continued until :math:`C_0` becomes empty.

    Whenever there are multiple nodes within :math:`C_0`, one must choose which node to add to
    the growing clique. This function allows a method of choosing nodes to be set with the
    ``node_select`` argument, with node selection based on uniform randomness and node degree
    supported. Degree-based node selection involves picking the node with the greatest degree,
    with ties settled by uniform random choice.

    **Example usage:**

    >>> graph = nx.complete_graph(10)
    >>> clique = [0, 1, 2, 3, 4]
    >>> grow(clique, graph)
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    Args:
        clique (list[int]): a subgraph specified by a list of nodes; the subgraph must be a clique
        graph (nx.Graph): the input graph
        node_select (str): method of selecting nodes from :math:`C_0` during growth. Can be either
            ``"uniform"`` for uniform random selection or ``"degree"`` for degree-based selection.
            Defaults to ``"uniform"``.

    Returns:
        list[int]: a new clique subgraph of equal or larger size than the input
    """

    if not utils.is_subgraph(clique, graph):
        raise ValueError("Input is not a valid subgraph")

    if not utils.is_clique(graph.subgraph(clique)):
        raise ValueError("Input subgraph is not a clique")

    clique = set(clique)
    c_0 = utils.c_0(clique, graph)

    while c_0:
        if node_select == "uniform":
            clique.add(np.random.choice(c_0))
        elif node_select == "degree":
            degrees = np.array([graph.degree(n) for n in c_0])
            to_add_index = np.random.choice(
                np.where(degrees == degrees.max())[0])
            to_add = c_0[to_add_index]
            clique.add(to_add)
        else:
            raise ValueError("Node selection method not recognized")

        c_0 = utils.c_0(clique, graph)

    return sorted(clique)
Пример #5
0
def greedy_density(subgraphs: Iterable,
                   graph: nx.Graph,
                   target: int,
                   resize_options: Optional[dict] = None) -> list:
    """Method to greedily resize subgraphs based upon density.

    This function uses a greedy approach to iteratively add/remove nodes to an input subgraph.
    Suppose the input subgraph has :math:`M` nodes. When shrinking, the algorithm considers
    all :math:`M-1` node subgraphs of the input subgraph, and picks the one with the greatest
    density. It then considers all :math:`M-2` node subgraphs of the chosen :math:`M-1` node
    subgraph, and continues until the desired subgraph size has been reached.

    When growing, the algorithm considers all :math:`M+1` node subgraphs of the input graph given by
    combining the input :math:`M` node subgraph with another node from the remainder of the input
    graph, and picks the one with the greatest density. It then considers all :math:`M+2` node
    subgraphs given by combining the chosen :math:`M+1` node subgraph with nodes remaining from
    the overall graph, and continues until the desired subgraph size has been reached.

    ``resize_options`` is not currently in use in :func:`greedy_density`.

    Args:
        subgraphs (iterable[list[int]]): an iterable over subgraphs, where each subgraph is given
            by a list of nodes
        graph (nx.Graph): the input graph
        target (int): the target number of nodes in the subgraph (must be larger than 1)
        resize_options (dict[str, Any]): dictionary specifying options used during resizing;
            defaults to :const:`RESIZE_DEFAULTS`

    Returns:
        list[list[int]]: a list of resized subgraphs, where each subgraph is given by a list of
        nodes
    """
    # Note that ``resize_options`` will become non-trivial once randomness is added to methods
    resize_options = {**RESIZE_DEFAULTS, **(resize_options or {})}

    nodes = graph.nodes

    resized = []

    for s in subgraphs:
        s = set(s)

        if not utils.is_subgraph(s, graph):
            raise ValueError("Input is not a valid subgraph")

        while len(s) != target:
            if len(s) < target:
                s_candidates = [s | {n} for n in set(nodes) - s]
            else:
                s_candidates = list(itertools.combinations(s, len(s) - 1))

            _d, s = max(
                (nx.density(graph.subgraph(n)), n) for n in s_candidates)

        resized.append(sorted(s))

    return resized
Пример #6
0
def shrink(subgraph: list, graph: nx.Graph) -> list:
    """Shrinks an input subgraph until it forms a clique.

    Proceeds by removing nodes in the input subgraph one at a time until the result is a clique
    that satisfies :func:`~strawberryfields.gbs.graph.utils.is_clique`. Upon each iteration,
    this function selects the node with lowest degree relative to the subgraph and removes it.

    **Example usage:**

    >>> graph = nx.barbell_graph(4, 0)
    >>> subgraph = [0, 1, 2, 3, 4, 5]
    >>> shrink(subgraph, graph)
    [0, 1, 2, 3]

    Args:
        subgraph (list[int]): a subgraph specified by a list of nodes
        graph (nx.Graph): the input graph

    Returns:
        list[int]: a clique of size smaller than or equal to the input subgraph
    """

    if not utils.is_subgraph(subgraph, graph):
        raise ValueError("Input is not a valid subgraph")

    subgraph = graph.subgraph(
        subgraph).copy()  # A copy is required to be able to modify the
    # structure of the subgraph (https://networkx.github.io/documentation/stable/reference/classes/generated/networkx.Graph.subgraph.html)

    while not utils.is_clique(subgraph):
        degrees = list(subgraph.degree())
        np.random.shuffle(
            degrees
        )  # used to make sure selection of node with lowest degree is not
        # deterministic in case of a tie (https://docs.python.org/3/library/functions.html#min)

        to_remove = min(degrees, key=lambda x: x[1])
        subgraph.remove_node(to_remove[0])

    return sorted(subgraph.nodes())
Пример #7
0
 def test_invalid_type(self, graph):
     """Test if function raises a ``TypeError`` when fed an invalid subgraph type (i.e.,
     not iterable)."""
     with pytest.raises(TypeError,
                        match="subgraph and graph.nodes must be iterable"):
         utils.is_subgraph(None, graph)