docs: def get_clique_tree(nodes, edges):
      """Given a set of int nodes i and edges (i,j), returns an nx.Graph object G
      which is a clique tree, where:
          - G.node[i]['members'] contains the set of original nodes in the ith
              maximal clique
          - G[i][j]['members'] contains the set of original nodes in the seperator
              set between maximal cliques i and j
  
      Note: This method is currently only implemented for chordal graphs; TODO:
      add a step to triangulate non-chordal graphs.
      """
      # Form the original graph G1
      G1 = nx.Graph()
      G1.add_nodes_from(nodes)
      G1.add_edges_from(edges)
  
      # Check if graph is chordal
      # TODO: Add step to triangulate graph if not
      if not nx.is_chordal(G1):
          raise NotImplementedError("Graph triangulation not implemented.")
  
      # Create maximal clique graph G2
      # Each node is a maximal clique C_i
      # Let w = |C_i \cap C_j|; C_i, C_j have an edge with weight w if w > 0
      G2 = nx.Graph()
      for i, c in enumerate(nx.chordal_graph_cliques(G1)):
          G2.add_node(i, members=c)
      for i in G2.nodes:
          for j in G2.nodes:
              S = G2.node[i]["members"].intersection(G2.node[j]["members"])
              w = len(S)
              if w > 0:
                  G2.add_edge(i, j, weight=w, members=S)
  
      # Return a minimum spanning tree of G2
      return nx.minimum_spanning_tree(G2)