docstr: Label nodes on column level_col based on topological sort depth
  Supports pandas + cudf, using parallelism within each level computation
  Options:
  * allow_cycles: if False and detects a cycle, throw ValueException, else break cycle by picking a lowest-in-degree node
  * warn_cycles: if True and detects a cycle, proceed with a warning
  * remove_self_loops: preprocess by removing self-cycles. Avoids allow_cycles=False, warn_cycles=True messages.
  
  Example:
  
  edges_df = gpd.DataFrame({'s': ['a', 'b', 'c', 'd'],'d': ['b', 'c', 'e', 'e']})
  g = graphistry.edges(edges_df, 's', 'd')
  g2 = g.get_topological_levels()
  g2._nodes.info()  # pd.DataFrame with | 'id' , 'level' |
docstr_tok: ["Label","nodes","on","column","level_col","based","on","topological","sort","depth","Supports","pandas","+","cudf",",","using","parallelism","within","each","level","computation","Options",":","*","allow_cycles",":","if","False","and","detects","a","cycle",",","throw","ValueException",",","else","break","cycle","by","picking","a","lowest-in-degree","node","*","warn_cycles",":","if","True","and","detects","a","cycle",",","proceed","with","a","warning","*","remove_self_loops",":","preprocess","by","removing","self-cycles",".","Avoids","allow_cycles=False",",","warn_cycles=True","messages",".","Example",":","edges_df","=","gpd.DataFrame","(","{","'s","'",":","[","'","a","'",",","'","b","'",",","'","c","'",",","'d","'","]",",","'d","'",":","[","'","b","'",",","'","c","'",",","'","e","'",",","'","e","'","]","}",")","g","=","graphistry.edges","(","edges_df",",","'s","'",",","'d","'",")","g2","=","g.get_topological_levels","(",")","g2._nodes.info","(",")","#","pd.DataFrame","with","|","'id","'",",","'level","'","|"]
code: def get_topological_levels(
      self,
      level_col: str = "level",
      allow_cycles: bool = True,
      warn_cycles: bool = True,
      remove_self_loops: bool = True,
  ) -> Plottable:
      """
      Label nodes on column level_col based on topological sort depth
      Supports pandas + cudf, using parallelism within each level computation
      Options:
      * allow_cycles: if False and detects a cycle, throw ValueException, else break cycle by picking a lowest-in-degree node
      * warn_cycles: if True and detects a cycle, proceed with a warning
      * remove_self_loops: preprocess by removing self-cycles. Avoids allow_cycles=False, warn_cycles=True messages.
  
      Example:
  
      edges_df = gpd.DataFrame({'s': ['a', 'b', 'c', 'd'],'d': ['b', 'c', 'e', 'e']})
      g = graphistry.edges(edges_df, 's', 'd')
      g2 = g.get_topological_levels()
      g2._nodes.info()  # pd.DataFrame with | 'id' , 'level' |
  
      """
  
      g2_base = self.materialize_nodes()
  
      g2 = g2_base
      if (g2._nodes is None) or (len(g2._nodes) == 0):
          return g2
  
      g2 = g2.edges(
          g2._edges.drop_duplicates([g2._source, g2._destination])
      )
      if remove_self_loops:
          non_self_loops = (
              g2._edges[g2._source] != g2._edges[g2._destination]
          )
          g2 = g2.edges(g2._edges[non_self_loops])
  
      nodes_with_levels: List[Any] = []
      while True:
          if len(g2._nodes) == 0:
              break
          g2 = g2.get_degrees()
  
          roots = g2._nodes[g2._nodes["degree_in"] == 0]
          if len(roots) == 0:
              if not allow_cycles:
                  raise ValueError(
                      "Cyclic graph in get_topological_levels(); remove cycles or set allow_cycles=True"
                  )
              # tie break by picking biggest node
              max_degree = g2._nodes["degree"].max()
              roots = g2._nodes[g2._nodes["degree"] == max_degree][:1]
              if warn_cycles:
                  logger.warning(
                      "Cycle on computing level %s",
                      len(nodes_with_levels),
                  )
  
          nodes_with_levels.append(
              (
                  roots[
                      [
                          c
                          for c in roots
                          if c
                          not in ["degree_in", "degree_out", "degree"]
                      ]
                  ].assign(**{level_col: len(nodes_with_levels)})
              )
          )
  
          g2 = g2.drop_nodes(roots[g2._node])
      nodes_df0 = nodes_with_levels[0]
      if len(nodes_with_levels) > 1:
          nodes_df = pd.concat([nodes_df0] + nodes_with_levels[1:])
      else:
          nodes_df = nodes_df0
  
      if self._nodes is None:
          return self.nodes(nodes_df)
      else:
          # use orig cols, esp. in case collisions like degree
          out_df = g2_base._nodes.merge(
              nodes_df[[g2_base._node, level_col]],
              on=g2_base._node,
              how="left",
          )
          return self.nodes(out_df)
  
imports: import logging, numpy, pandas
  from typing import Any, List, Union, TYPE_CHECKING
  from typing_extensions import Literal
  from graphistry.Engine import Engine, EngineAbstract
  from graphistry.Plottable import Plottable
  from graphistry.util import setup_logger
  from .chain import chain
  from .collapse import collapse_by
  from .hop import hop
  from .filter_by_dict import filter_edges_by_dict, filter_nodes_by_dict
label_desc: Use image node_id 8 for calling the ComputeMixin obj's underlying member method code with example usage: obj.get_topological_levels(level_col, allow_cycles, warn_cycles, remove__loops) and returns: g2, self, self