def graph_symmetric_difference(graph1,
                               graph2,
                               edge_diff=False,
                               return_copy=False):
    """
    Return a new graph with the symmetric difference in nodes and edges of two
    graphs.

    The symmetric difference represents the nodes and edges connecting them
    that are present in `graph1` but not in `graph2` and vice versa.
    It is thus the opposite of the `graph_intersection`.
    The difference is node driven resulting in edges being removed when the
    nodes on either end are removed. Use the `edge_diff` argument to switch
    to an edge driven difference calculation,

    If the graphs share a common origin graph and `return_copy` is False
    the returned intersection graph will be a view on the origin, else it is
    a new graph.

    :param graph1:      first graph
    :type graph1:       :graphit:Graph
    :param graph2:      second graph
    :type graph2:       :graphit:Graph
    :param edge_diff:   switch from node to edge driven difference calculation
    :type edge_diff:    :py:bool
    :param return_copy: force return a new grpah as deep copy based on graph1
    :type return_copy:  :py:bool

    :return:            symmetric difference graph
    :rtype:             :graphit:Graph
    :raises:            AttributeError, if arguments no instance of Graph class
    """

    # Validate if all arguments are Graphs
    check_graphbase_instance(graph1, graph2)

    # Compute node or edge symmetric difference.
    if edge_diff:
        symdiff_edges = graph1.edges.symmetric_difference(graph2.edges)
    else:
        symdiff_nodes = graph1.nodes.symmetric_difference(graph2.nodes)

    if share_common_origin(graph1, graph2) and not return_copy:
        if edge_diff:
            return graph1.origin.getedges(symdiff_edges)
        return graph1.origin.getnodes(symdiff_nodes)
    else:
        # Get node or edge difference for both and join them in a new graph
        if edge_diff:
            result = graph1.getedges(symdiff_edges.difference(
                graph2.edges)).copy(deep=True, copy_view=False)
            graph_join(result,
                       graph2.getedges(symdiff_edges.difference(graph1.edges)))
        else:
            result = graph1.getnodes(symdiff_nodes.difference(
                graph2.nodes)).copy(deep=True, copy_view=False)
            graph_join(result,
                       graph2.getnodes(symdiff_nodes.difference(graph1.nodes)))

        return result
def graph_difference(graph1, graph2, edge_diff=False, return_copy=False):
    """
    Return a graph with the difference in nodes and edges of `graph1` with
    respect to `graph2`.

    The difference represents the nodes and edges connecting them that are
    present in `graph1` but not in `graph2`. Difference is based on ID.
    The difference is node driven resulting in edges being removed when the
    nodes on either end are removed. Use the `edge_diff` argument to switch
    to an edge driven difference calculation,

    If the graphs share a common origin graph and `return_copy` is False
    the returned intersection graph will be a view on the origin, else it is
    a new graph.

    :param graph1:      first graph
    :type graph1:       :graphit:Graph
    :param graph2:      second graph
    :type graph2:       :graphit:Graph
    :param edge_diff:   switch from node to edge driven difference calculation
    :type edge_diff:    :py:bool
    :param return_copy: force return a new graph as deep copy based on graph1
    :type return_copy:  :py:bool

    :return:            difference graph
    :rtype:             :graphit:Graph
    :raises:            AttributeError, if arguments no instance of Graph class
    """

    # Validate if all arguments are Graphs
    check_graphbase_instance(graph1, graph2)

    # Compute edge or node difference.
    if edge_diff:
        difference_edges = graph1.edges.difference(graph2.edges)
    else:
        difference_nodes = graph1.nodes.difference(graph2.nodes)

    if share_common_origin(graph1, graph2) and not return_copy:
        if edge_diff:
            return graph1.origin.getedges(difference_edges)
        return graph1.origin.getnodes(difference_nodes)
    else:
        if edge_diff:
            result = graph1.getedges(difference_edges)
        else:
            result = graph1.getnodes(difference_nodes)
        return result.copy(deep=True, copy_view=False)
def graph_intersection(graph1, graph2, return_copy=False):
    """
    Return a graph with the intersection in nodes and edges between the two
    input graphs

    The intersection represents the nodes and edges connecting them present
    in both graphs. Node and edge similarity is based on ID.

    If the graphs share a common origin graph and `return_copy` is False
    the returned intersection graph will be a view on the origin, else it is
    a new graph.

    :param graph1:      first graph
    :type graph1:       :graphit:Graph
    :param graph2:      second graph
    :type graph2:       :graphit:Graph
    :param return_copy: force return a new graph as deep copy based on graph1
    :type return_copy:  :py:bool

    :return:            intersection graph
    :rtype:             :graphit:Graph
    :raises:            AttributeError, if arguments no instance of Graph class
    """

    # Validate if all arguments are Graphs
    check_graphbase_instance(graph1, graph2)

    # Compute node/edge intersection
    intersect_nodes = graph1.nodes.intersection(graph2.nodes)
    intersect_edges = graph1.edges.intersection(graph2.edges)

    if share_common_origin(graph1, graph2) and not return_copy:

        # First get node intersection then edges
        result = graph1.origin.getnodes(intersect_nodes)
        result.edges.set_view(intersect_edges)
        return result
    else:
        result = graph1.getnodes(intersect_nodes)
        result.edges.set_view(intersect_edges)
        return result.copy(deep=True, copy_view=False)
def graph_union(*args, **kwargs):
    """
    Return a graph with the union of the input graphs

    The union represents the combined unique nodes and edges present in
    the graphs. Similarity is based on ID.

    Nodes and edges are added without updating the respective attributes.
    The latter can be done by calling the `update` function afterwards.

    If the graphs share a common origin graph and `return_copy` is False
    the returned intersection graph will be a view on the origin, else it is
    a new graph.
    In both cases, the first graph in the argument list is used as basis
    for the returned union graph.

    :param args:        graphs to return the union for
    :type args:         :py:list
    :param return_copy: force return a new graph as deep copy based on graph1
    :type return_copy:  :py:bool

    :return:            union graph
    :rtype:             :graphit:Graph
    :raises:            AttributeError, if arguments no instance of Graph class
    """

    if not len(args) > 1:
        raise AttributeError('At least two input Graphs required')

    # Validate if all arguments are Graphs
    check_graphbase_instance(*args)

    all_share_common_origin = all(
        [share_common_origin(args[0], n) for n in args[1:]])
    if all_share_common_origin and not kwargs.get('return_copy', False):

        nids = []
        for graph in args:
            nids.extend([n for n in graph.nodes if n not in nids])

        eids = []
        for graph in args:
            eids.extend([e for e in graph.edges if e not in eids])

        result = args[0].origin.getnodes(nids)
        result.edges.set_view(eids)
        return result
    else:

        # make a deep copy of the first graph
        result = args[0].copy(deep=True, copy_view=False)

        # we need control over the node ID to add
        # temporary turn off auto_nid if needed
        auto_nid = result.data.auto_nid
        result.data.auto_nid = False

        for graph in args[1:]:
            for node, attrib in graph.nodes.items():
                if node not in result.nodes:
                    result.add_node(node, **attrib)

            for edge, attrib in graph.edges.items():
                if edge not in result.edges:
                    result.add_edge(*edge, **attrib)

        # Restore auto_nid
        result.data.auto_nid = auto_nid

        return result