def total_node_hours(pt_df, cpu_factor=8, cores_per_node=68,
                     memory_per_node=96, memory_min=10):
    """
    Estimate the total number of node hours to do an image processing
    run.

    Parameters
    ----------
    pt_df : pandas.DataFrame
        DataFrame containing the number of instances, total cpu time,
        and maximum and average memory used per pipetask.  This is
        the output of `tabulate_pipetask_resources`.
    cpu_factor : float [8]
        Slow down factor to apply to the pipetask cpu times.  The
        cpu times were derived from Cori-Haswell runs and the default
        value of 8 is the empirically observed slow-down for running
        the same cpu-bound job on a Cori-KNL node.
    cores_per_node : int [68]
        Number of cores per node.  68 is the Cori-KNL value.
    memory_per_node : int [96]
        Memory per node in GB.  96 is the Cori-KNL value.
    memory_min : int [10]
        Memory in GB to reserve per node as a safety factor.  10GB is
        a conservative number for these jobs.

    Returns
    -------
    tuple(float, float): The first entry, `node_hours`, is computed
    using the maximum memory estimate per process to determine the
    number of cores per node for a given pipe task ; the second entry,
    `node_hours_opt`, is an optimistic estimate using the average
    memory per process.
    """
    available_memory = memory_per_node - memory_min
    node_hours = 0
    node_hours_opt = 0
    for _, row in pt_df.iterrows():
        ncores = min(cores_per_node, int(available_memory/row['max_GB']))
        ncores_avg = min(cores_per_node, int(available_memory/row['avg_GB']))
        node_hours += row['cpu_hours']*cpu_factor/ncores
        node_hours_opt += row['cpu_hours']*cpu_factor/ncores_avg
    return node_hours, node_hours_opt