docs: def ungroup_gen(grouped_items, groupxs, fill=None):
      """
      Ungroups items returning a generator.
      Note that this is much slower than the list version and is not gaurenteed
      to have better memory usage.
  
      Args:
          grouped_items (list):
          groupxs (list):
          maxval (int): (default = None)
  
      Returns:
          list: ungrouped_items
  
      SeeAlso:
          vt.invert_apply_grouping
  
      CommandLine:
          python -m utool.util_alg ungroup_unique
  
      Example:
          >>> # ENABLE_DOCTEST
          >>> from utool.util_alg import *  # NOQA
          >>> import utool as ut
          >>> grouped_items = [[1.1, 1.2], [2.1, 2.2], [3.1, 3.2]]
          >>> groupxs = [[1, 2], [5, 6], [9, 3]]
          >>> ungrouped_items1 = list(ungroup_gen(grouped_items, groupxs))
          >>> ungrouped_items2 = ungroup(grouped_items, groupxs)
          >>> assert ungrouped_items1 == ungrouped_items2
          >>> grouped_items = [[1.1, 1.2], [2.1, 2.2], [3.1, 3.2]]
          >>> groupxs = [[0, 2], [1, 5], [4, 3]]
          >>> ungrouped_items1 = list(ungroup_gen(grouped_items, groupxs))
          >>> ungrouped_items2 = ungroup(grouped_items, groupxs)
          >>> assert ungrouped_items1 == ungrouped_items2
  
      Ignore:
          labels = np.random.randint(0, 64, 10000)
          unique_labels, groupxs = ut.group_indices(labels)
          grouped_items = ut.apply_grouping(np.arange(len(labels)), groupxs)
          ungrouped_items1 = list(ungroup_gen(grouped_items, groupxs))
          ungrouped_items2 = ungroup(grouped_items, groupxs)
          assert ungrouped_items2 == ungrouped_items1
          %timeit list(ungroup_gen(grouped_items, groupxs))
          %timeit ungroup(grouped_items, groupxs)
      """
      import utool as ut
      # Determine the number of items if unknown
      #maxpergroup = [max(xs) if len(xs) else 0 for xs in groupxs]
      #maxval = max(maxpergroup) if len(maxpergroup) else 0
  
      minpergroup = [min(xs) if len(xs) else 0 for xs in groupxs]
      minval = min(minpergroup) if len(minpergroup) else 0
  
      flat_groupx = ut.flatten(groupxs)
      sortx = ut.argsort(flat_groupx)
      # Indicates the index being yeilded
      groupx_sorted = ut.take(flat_groupx, sortx)
      flat_items = ut.iflatten(grouped_items)
  
      # Storage for data weiting to be yeilded
      toyeild = {}
      items_yeilded = 0
      # Indicates the index we are curently yeilding
      current_index = 0
  
      # Determine where fills need to happen
      num_fills_before = [minval] + (np.diff(groupx_sorted) - 1).tolist() + [0]
  
      # Check if there are fills before the first item
      fills = num_fills_before[items_yeilded]
      if fills > 0:
          for _ in range(fills):
              yield None
              current_index += 1
      # Yield items as possible
      for yeild_at, item in zip(flat_groupx, flat_items):
          if yeild_at > current_index:
              toyeild[yeild_at] = item
          elif yeild_at == current_index:
              # When we find the next element to yeild
              yield item
              current_index += 1
              items_yeilded += 1
              # Check if there are fills before the next item
              fills = num_fills_before[items_yeilded]
              if fills > 0:
                  for _ in range(fills):
                      yield None
                      current_index += 1
              # Now yield everything that came before this
              while current_index in toyeild:
                  item = toyeild.pop(current_index)
                  yield item
                  current_index += 1
                  items_yeilded += 1
                  # Check if there are fills before the next item
                  fills = num_fills_before[items_yeilded]
                  if fills > 0:
                      for _ in range(fills):
                          yield None
                          current_index += 1