docs: def _calibrate_junction_tree(self, operation):
          """
          Generalized calibration of junction tree or clique using belief propagation. This method can be used for both
          calibrating as well as max-calibrating.
          Uses Lauritzen-Spiegelhalter algorithm or belief-update message passing.
  
          Parameters
          ----------
          operation: str ('marginalize' | 'maximize')
              The operation to do for passing messages between nodes.
  
          Reference
          ---------
          Algorithm 10.3 Calibration using belief propagation in clique tree
          Probabilistic Graphical Models: Principles and Techniques
          Daphne Koller and Nir Friedman.
          """
          # Initialize clique beliefs as well as sepset beliefs
          self.clique_beliefs = {clique: self.junction_tree.get_factors(clique)
                                 for clique in self.junction_tree.nodes()}
          self.sepset_beliefs = {frozenset(edge): None for edge in self.junction_tree.edges()}
  
          for clique in self.junction_tree.nodes():
              if not self._is_converged(operation=operation):
                  neighbors = self.junction_tree.neighbors(clique)
                  # update root's belief using nieighbor clique's beliefs
                  # upward pass
                  for neighbor_clique in neighbors:
                      self._update_beliefs(neighbor_clique, clique, operation=operation)
                  bfs_edges = nx.algorithms.breadth_first_search.bfs_edges(self.junction_tree, clique)
                  # update the beliefs of all the nodes starting from the root to leaves using root's belief
                  # downward pass
                  for edge in bfs_edges:
                      self._update_beliefs(edge[0], edge[1], operation=operation)
              else:
                  break