docs: def get_pgs(self, pool_size, percent_data=DEFAULT_POOL_WEIGHT,
                  device_class=None):
          """Return the number of placement groups to use when creating the pool.
  
          Returns the number of placement groups which should be specified when
          creating the pool. This is based upon the calculation guidelines
          provided by the Ceph Placement Group Calculator (located online at
          http://ceph.com/pgcalc/).
  
          The number of placement groups are calculated using the following:
  
              (Target PGs per OSD) * (OSD #) * (%Data)
              ----------------------------------------
                           (Pool size)
  
          Per the upstream guidelines, the OSD # should really be considered
          based on the number of OSDs which are eligible to be selected by the
          pool. Since the pool creation doesn't specify any of CRUSH set rules,
          the default rule will be dependent upon the type of pool being
          created (replicated or erasure).
  
          This code makes no attempt to determine the number of OSDs which can be
          selected for the specific rule, rather it is left to the user to tune
          in the form of 'expected-osd-count' config option.
  
          :param pool_size: int. pool_size is either the number of replicas for
              replicated pools or the K+M sum for erasure coded pools
          :param percent_data: float. the percentage of data that is expected to
              be contained in the pool for the specific OSD set. Default value
              is to assume 10% of the data is for this pool, which is a
              relatively low % of the data but allows for the pg_num to be
              increased. NOTE: the default is primarily to handle the scenario
              where related charms requiring pools has not been upgraded to
              include an update to indicate their relative usage of the pools.
          :param device_class: str. class of storage to use for basis of pgs
              calculation; ceph supports nvme, ssd and hdd by default based
              on presence of devices of each type in the deployment.
          :return: int.  The number of pgs to use.
          """
  
          # Note: This calculation follows the approach that is provided
          # by the Ceph PG Calculator located at http://ceph.com/pgcalc/.
          validator(value=pool_size, valid_type=int)
  
          # Ensure that percent data is set to something - even with a default
          # it can be set to None, which would wreak havoc below.
          if percent_data is None:
              percent_data = DEFAULT_POOL_WEIGHT
  
          # If the expected-osd-count is specified, then use the max between
          # the expected-osd-count and the actual osd_count
          osd_list = get_osds(self.service, device_class)
          expected = config('expected-osd-count') or 0
  
          if osd_list:
              if device_class:
                  osd_count = len(osd_list)
              else:
                  osd_count = max(expected, len(osd_list))
  
              # Log a message to provide some insight if the calculations claim
              # to be off because someone is setting the expected count and
              # there are more OSDs in reality. Try to make a proper guess
              # based upon the cluster itself.
              if not device_class and expected and osd_count != expected:
                  log("Found more OSDs than provided expected count. "
                      "Using the actual count instead", INFO)
          elif expected:
              # Use the expected-osd-count in older ceph versions to allow for
              # a more accurate pg calculations
              osd_count = expected
          else:
              # NOTE(james-page): Default to 200 for older ceph versions
              # which don't support OSD query from cli
              return LEGACY_PG_COUNT
  
          percent_data /= 100.0
          target_pgs_per_osd = config('pgs-per-osd') or DEFAULT_PGS_PER_OSD_TARGET
          num_pg = (target_pgs_per_osd * osd_count * percent_data) // pool_size
  
          # NOTE: ensure a sane minimum number of PGS otherwise we don't get any
          #       reasonable data distribution in minimal OSD configurations
          if num_pg < DEFAULT_MINIMUM_PGS:
              num_pg = DEFAULT_MINIMUM_PGS
  
          # The CRUSH algorithm has a slight optimization for placement groups
          # with powers of 2 so find the nearest power of 2. If the nearest
          # power of 2 is more than 25% below the original value, the next
          # highest value is used. To do this, find the nearest power of 2 such
          # that 2^n <= num_pg, check to see if its within the 25% tolerance.
          exponent = math.floor(math.log(num_pg, 2))
          nearest = 2 ** exponent
          if (num_pg - nearest) > (num_pg * 0.25):
              # Choose the next highest power of 2 since the nearest is more
              # than 25% below the original value.
              return int(nearest * 2)
          else:
              return int(nearest)