import multiprocessing


def sanitize_cpus(num_cpus, num_tasks, fallback_cpus):
    """
    Bounds num_cpus such that it is:
    - no less than 1
    - no greater than the lesser of:
      - num_tasks
      - the number of logical CPUs (or fallback_cpus if that could
        not be determined)

    Parameters
    ----------
    num_cpus : int
        the value to be sanitized
    num_tasks : int
        the number of runs
    fallback_cpus : int
        the upper bound to use when the number of logical CPUs cannot
        be determined

    Returns
    -------
    (int, str)
        a tuple of:
        - the sanitized value
        - a description of the boundary violated (may be None)
    """
    if num_cpus < 1:
        return (1, 'minimum allowable value')
    else:
        try:
            maximum = multiprocessing.cpu_count()
            source = 'number of logical CPUs'
        except:
            maximum = 16
            source = 'maximum allowed value'

        if num_tasks < maximum:
            maximum = num_tasks
            source = 'number of runs'

        if num_cpus > maximum:
            return (maximum, source)

    return (num_cpus, None)