def abstract_atom(graph: HyperGraph, loc):
    atom = graph.node[loc]
    # replace ring atoms with nonterminals that just have the connections necessary for the ring
    assert atom.is_terminal, "We only can abstract terminal atoms!"
    new_edges = [
        copy.deepcopy(graph.edges[edge_id]) for edge_id in atom.edge_ids
    ]

    new_graph = HyperGraph()
    new_graph.add_edges(new_edges)

    atom_copy = Node(edge_ids=[x for x in new_graph.edges.keys()],
                     edges=[x for x in new_graph.edges.values()],
                     is_terminal=True,
                     data=atom.data)
    parent = Node(edge_ids=atom_copy.edge_ids, edges=atom_copy.edges)
    new_graph.add_parent_node(parent)
    new_graph.add_node(atom_copy)
    new_graph.validate()

    # replace the original atom with a nonterminal matching the parent of the new graph
    replacement_node = Node(
        edge_ids=atom.edge_ids,
        edges=[graph.edges[edge_id] for edge_id in atom.edge_ids])
    del graph.node[loc]
    graph.add_node(
        replacement_node)  # this makes sure it's appended at the end
    graph.validate()

    return new_graph
def abstract_ring_atom(graph: HyperGraph, loc):
    graph.validate()
    atom = graph.node[loc]
    # replace ring atoms with nonterminals that just have the connections necessary for the ring
    assert atom.is_terminal, "We only can abstract terminal atoms!"
    neighbors = [(edge_id, graph.other_end_of_edge(loc, edge_id))
                 for edge_id in atom.edge_ids]
    # determine the nodes in the 'key structure', which is terminals, parent nonterminal, and nonterminals connecting to more than one of the former
    internal_neighbor_ids = [
        x for x in neighbors if x[1] not in graph.child_ids()  # '
        or len(graph.node[x[1]].edge_ids) > 1
    ]
    child_neighbors = [x for x in neighbors if x not in internal_neighbor_ids]
    if len(internal_neighbor_ids) >= 2 and len(internal_neighbor_ids) < len(
            neighbors):
        # create the edges between the parent placeholder and the atom being abstracted
        new_edges = [
            copy.deepcopy(graph.edges[edge_id[0]])
            for edge_id in internal_neighbor_ids
        ]
        new_graph = HyperGraph()
        new_graph.add_edges(new_edges)
        parent = Node(edge_ids=[x for x in new_graph.edges.keys()],
                      edges=new_edges)
        old_new_edge_map = {
            old[0]: new
            for old, new in zip(internal_neighbor_ids, parent.edge_ids)
        }
        child_edge_ids = [
            old_new_edge_map[edge_id]
            if edge_id in old_new_edge_map else edge_id
            for edge_id in atom.edge_ids
        ]

        # and move over all the edges from the abstracted atom to its children
        for edge_id in child_edge_ids:
            if edge_id not in old_new_edge_map.values():
                new_graph.edges[edge_id] = graph.edges[edge_id]
                del graph.edges[edge_id]
        new_graph.add_parent_node(parent)
        child = Node(
            data=atom.data,
            edge_ids=child_edge_ids,
            edges=[new_graph.edges[edge_id] for edge_id in child_edge_ids],
            is_terminal=True)

        new_graph.add_node(child)

        # new hypergraph takes over all the children
        child_neighbor_nodes = [x[1] for x in child_neighbors]
        # determine which of the children we want to take over, we need the indices to amend the tree later
        child_inds = []
        for ind, child_id in enumerate(graph.child_ids()):
            if child_id in child_neighbor_nodes:
                child_inds.append(ind)

        # and now purge these nodes from the original graph and append them to the child, along with any edges
        for child_id in child_neighbor_nodes:
            new_graph.add_node(graph.node[child_id])
            del graph.node[child_id]
        new_graph.validate()

        # replace the atom with a nonterminal with just the connections for the ring
        replace_edge_ids = [edge_id[0] for edge_id in internal_neighbor_ids]
        replacement_node = Node(
            edge_ids=replace_edge_ids,
            edges=[graph.edges[edge_id] for edge_id in replace_edge_ids])
        del graph.node[loc]
        graph.add_node(replacement_node)
        graph.validate()

        return new_graph, child_inds
    else:
        return None, None