docs: def to_junction_tree(self):
          """
          Creates a junction tree (or clique tree) for a given markov model.
  
          For a given markov model (H) a junction tree (G) is a graph
          1. where each node in G corresponds to a maximal clique in H
          2. each sepset in G separates the variables strictly on one side of the
          edge to other.
  
          Examples
          --------
          >>> from pgmpy.models import MarkovModel
          >>> from pgmpy.factors.discrete import DiscreteFactor
          >>> mm = MarkovModel()
          >>> mm.add_nodes_from(['x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7'])
          >>> mm.add_edges_from([('x1', 'x3'), ('x1', 'x4'), ('x2', 'x4'),
          ...                    ('x2', 'x5'), ('x3', 'x6'), ('x4', 'x6'),
          ...                    ('x4', 'x7'), ('x5', 'x7')])
          >>> phi = [DiscreteFactor(edge, [2, 2], np.random.rand(4)) for edge in mm.edges()]
          >>> mm.add_factors(*phi)
          >>> junction_tree = mm.to_junction_tree()
          """
          from pgmpy.models import JunctionTree
  
          # Check whether the model is valid or not
          self.check_model()
  
          # Triangulate the graph to make it chordal
          triangulated_graph = self.triangulate()
  
          # Find maximal cliques in the chordal graph
          cliques = list(map(tuple, nx.find_cliques(triangulated_graph)))
  
          # If there is only 1 clique, then the junction tree formed is just a
          # clique tree with that single clique as the node
          if len(cliques) == 1:
              clique_trees = JunctionTree()
              clique_trees.add_node(cliques[0])
  
          # Else if the number of cliques is more than 1 then create a complete
          # graph with all the cliques as nodes and weight of the edges being
          # the length of sepset between two cliques
          elif len(cliques) >= 2:
              complete_graph = UndirectedGraph()
              edges = list(itertools.combinations(cliques, 2))
              weights = list(map(lambda x: len(set(x[0]).intersection(set(x[1]))),
                             edges))
              for edge, weight in zip(edges, weights):
                  complete_graph.add_edge(*edge, weight=-weight)
  
              # Create clique trees by minimum (or maximum) spanning tree method
              clique_trees = JunctionTree(nx.minimum_spanning_tree(complete_graph).edges())
  
          # Check whether the factors are defined for all the random variables or not
          all_vars = itertools.chain(*[factor.scope() for factor in self.factors])
          if set(all_vars) != set(self.nodes()):
              ValueError('DiscreteFactor for all the random variables not specified')
  
          # Dictionary stating whether the factor is used to create clique
          # potential or not
          # If false, then it is not used to create any clique potential
          is_used = {factor: False for factor in self.factors}
  
          for node in clique_trees.nodes():
              clique_factors = []
              for factor in self.factors:
                  # If the factor is not used in creating any clique potential as
                  # well as has any variable of the given clique in its scope,
                  # then use it in creating clique potential
                  if not is_used[factor] and set(factor.scope()).issubset(node):
                      clique_factors.append(factor)
                      is_used[factor] = True
  
              # To compute clique potential, initially set it as unity factor
              var_card = [self.get_cardinality()[x] for x in node]
              clique_potential = DiscreteFactor(node, var_card, np.ones(np.product(var_card)))
              # multiply it with the factors associated with the variables present
              # in the clique (or node)
              # Checking if there's clique_factors, to handle the case when clique_factors
              # is empty, otherwise factor_product with throw an error [ref #889]
              if clique_factors:
                  clique_potential *= factor_product(*clique_factors)
              clique_trees.add_factors(clique_potential)
  
          if not all(is_used.values()):
              raise ValueError('All the factors were not used to create Junction Tree.'
                               'Extra factors are defined.')
  
          return clique_trees