docstr: Calculate required consecutive job layers.
  
  Layers are used to keep resource requirements within given
  constraint. For instance, if the jobs together require 50 threads,
  but only 12 are available, we will use 5 layers. If multiple constraints are
  used, all will be considered and met. Any constraints given as None will be
  treated as infinite.
docstr_tok: ["Calculate","required","consecutive","job","layers",".","Layers","are","used","to","keep","resource","requirements","within","given","constraint",".","For","instance",",","if","the","jobs","together","require","50","threads",",","but","only","12","are","available",",","we","will","use","5","layers",".","If","multiple","constraints","are","used",",","all","will","be","considered","and","met",".","Any","constraints","given","as","None","will","be","treated","as","infinite","."]
code: def _get_layers(cls, resources, constraints, sortby=None):
      """Calculate required consecutive job layers.
  
      Layers are used to keep resource requirements within given
      constraint. For instance, if the jobs together require 50 threads,
      but only 12 are available, we will use 5 layers. If multiple constraints are
      used, all will be considered and met. Any constraints given as None will be
      treated as infinite.
      """
  
      # Calculates the ratio of resource to constraint. E.g, if the resource is 12
      #  cores, and the constraint is 16, it will return 0.75. This is done for
      # every resource type in the group, returning the result in a list
      def _proportion(group):
          return [r / c if c else 0 for r, c in zip(group, constraints)]
  
      # Return the highest _proportion item in the list
      def _highest_proportion(group):
          return max(_proportion(group))
  
      rows = [[]]
  
      # By zipping, we combine the vals into tuples based on job, 1 tuple per
      # job: [ (val1, 1_val1, 2_val1), ...]. In each tuple, the resources
      # will remain in the same order as the original dict, so their identity
      # can be extracted later.
      resource_groups = zip(*resources.values())
  
      # Sort by _proportion highest to lowest
      pre_sorted = sorted(
          resource_groups, key=_highest_proportion, reverse=True
      )
  
      # If a key is provided (e.g. runtime), we sort again by that key from
      # highest to lowest
      for res in sortby or []:
          if res in resources:
              # Find the position of the key in the job tuple
              i = list(resources).index(res)
              pre_sorted = sorted(
                  pre_sorted, key=op.itemgetter(i), reverse=True
              )
  
      for group in pre_sorted:
          appended = False
  
          # Check each row for space, starting with the first.
          for row in rows:
              if not appended and cls._check_constraint(
                  row + [group], constraints
              ):
                  row.append(group)
                  appended = True
  
          # If the final "row" in rows has something, we add a new empty
          # row. That way, we guarantee we have a row with space
          if len(rows[-1]) > 0:
              rows.append([])
  
          # If not appended, that means a rule required more resource
          # than allowed by the constraint. This should only be possible for pipe
          # jobs, which must be run simultaneously.
          if not appended:
              too_high = []
              for i, val in enumerate(_proportion(group)):
                  if val > 1:
                      too_high.append(
                          (
                              list(resources)[i],
                              group[i],
                              list(constraints)[i],
                          )
                      )
  
              error_text = [
                  f"\t{res}: {amount}/{constraint}"
                  for res, amount, constraint in too_high
              ]
              raise WorkflowError("\n".join(error_text))
  
      # Remove final empty row. (The above loop ends each cycle by ensuring
      # there's an empty row)
      rows.pop()
      return rows
  
imports: from collections import UserDict, defaultdict
  import itertools
  import operator
  import re
  import tempfile
  from snakemake.exceptions import ResourceScopesException, WorkflowError
  from snakemake.common.tbdstring import TBDString
label_desc: Use image node_id 7 for calling the GroupResources obj's underlying member method code with example usage: obj._get_layers(cls, resources, constraints, sortby) and returns: rows, unknown, max