def get_duals(drive_model: Model, x_sample_count: int, y_sample_count: int,
              horizontal_shifting: float):
    """
    Get duals of a given drive model, self-creating debugger
    :param drive_model: the driving model
    :param x_sample_count: count of samples in x direction
    :param y_sample_count: count of samples in y direction
    :param horizontal_shifting: shifting in x direction to keep the drive away from input
    :return: None
    """
    debugger, _, plotter = initialize((drive_model, ), None, ['duals'])
    drive_contour = shape_factory.get_shape_contour(drive_model, True, None,
                                                    drive_model.smooth)
    logging.debug('drive model loaded')

    # get the bounding
    drive_polygon = Polygon(drive_contour)
    min_x, min_y, max_x, max_y = drive_polygon.bounds
    drive_windows = [(min_x, max_x, min_y, max_y)]
    drive_windows = split_window(drive_windows[0], x_sample_count,
                                 y_sample_count)
    centers = [center_of_window(window) for window in drive_windows]

    # start finding the dual
    for index, center in enumerate(centers):
        if not point_in_contour(drive_contour, *center):
            logging.info(f'Point #{index}{center} not in contour')
            continue

        drive_polar = toExteriorPolarCoord(Point(*center), drive_contour, 1024)
        driven_polar, center_distance, phi = compute_dual_gear(drive_polar)
        drive_new_contour = toCartesianCoordAsNp(drive_polar,
                                                 horizontal_shifting, 0)
        driven_contour = toCartesianCoordAsNp(
            driven_polar, horizontal_shifting + center_distance, 0)
        driven_contour = np.array(
            rotate(driven_contour, phi[0],
                   (horizontal_shifting + center_distance, 0)))

        # move things back to center
        drive_new_contour += np.array((center[0], center[1]))
        driven_contour += np.array((center[0], center[1]))

        plotter.draw_contours(
            debugger.file_path(f'{index}.png'),
            [('input_drive', drive_contour), ('math_drive', drive_new_contour),
             ('math_driven', driven_contour)],
            [(horizontal_shifting + center[0], center[1]),
             (horizontal_shifting + center_distance + center[0], center[1])])
Example #2
0
def plot_polar_shape(ax, title, polar_contour, center, sample_num):
    cartesian_contour = toCartesianCoordAsNp(polar_contour, center[0],
                                             center[1])
    ax.set_title(title)
    ax.fill(cartesian_contour[:, 0], cartesian_contour[:, 1], "g", alpha=0.3)
    for p in cartesian_contour[1:-1:int(len(cartesian_contour) / 32)]:
        l = Line2D([center[0], p[0]], [center[1], p[1]], linewidth=1)
        ax.add_line(l)
    ax.scatter(center[0], center[1], s=10, c='b')
    ax.axis('equal')
Example #3
0
def math_cut(drive_model: Model,
             cart_drive: np.ndarray,
             reporter: Reporter,
             plotter: Optional[Plotter],
             animation=False,
             center_point: Optional[Tuple[float, float]] = None):
    center = center_point or drive_model.center_point
    polar_math_drive = toExteriorPolarCoord(Point(center[0], center[1]),
                                            cart_drive, drive_model.sample_num)
    polar_math_driven, center_distance, phi = compute_dual_gear(
        polar_math_drive, k=drive_model.k)

    if animation:
        plot_sampled_function((polar_math_drive, polar_math_driven), (phi, ),
                              reporter.get_math_debug_dir_name(),
                              100,
                              0.001, [(0, 0), (center_distance, 0)], (8, 8),
                              ((-0.5, 1.5), (-1.1, 1.1)),
                              plotter=plotter)

    # save figures
    plotter.draw_contours(
        reporter.file_path('math_drive.png'),
        [('math_drive', toCartesianCoordAsNp(polar_math_drive, 0, 0))], None)
    plotter.draw_contours(
        reporter.file_path('math_driven.png'),
        [('math_driven', toCartesianCoordAsNp(polar_math_driven, 0, 0))], None)
    plotter.draw_contours(
        reporter.file_path('math_results.png'),
        [('math_drive', toCartesianCoordAsNp(polar_math_drive, 0, 0)),
         ('math_driven',
          np.array(
              rotate(
                  list(
                      toCartesianCoordAsNp(polar_math_driven, center_distance,
                                           0)), phi[0],
                  (center_distance, 0))))], [(0, 0), (center_distance, 0)])

    logging.info('math rotate complete')
    logging.info(f'Center Distance = {center_distance}')

    return center_distance, phi, polar_math_drive, polar_math_driven
def optimize_center(cart_input_drive,
                    cart_input_driven,
                    debugger,
                    opt_config,
                    plotter,
                    k=1):
    debug_suite = ReportingSuite(debugger, plotter,
                                 plt.figure(figsize=(16, 9)))
    results = sampling_optimization(
        cart_input_drive,
        cart_input_driven,
        opt_config['sampling_count'],
        opt_config['keep_count'],
        opt_config['resampling_accuracy'],
        opt_config['max_sample_depth'],
        debug_suite,
        opt_config['torque_weight'],
        k=k,
        mismatch_penalty=opt_config['mismatch_penalty'])
    results.sort(key=lambda total_score, *_: total_score)
    best_result = results[0]
    logging.info(f'Best result with score {best_result[0]}')
    score, polar_drive = best_result
    polar_driven, center_distance, phi = compute_dual_gear(polar_drive, k)
    drive_contour = toCartesianCoordAsNp(polar_drive, 0, 0)
    driven_contour = toCartesianCoordAsNp(polar_driven, center_distance, 0)
    driven_contour = np.array(
        rotate(driven_contour, phi[0], (center_distance, 0)))
    plotter.draw_contours(debugger.file_path('optimize_result.png'),
                          [('carve_drive', drive_contour),
                           ('carve_driven', driven_contour)],
                          [(0, 0), (center_distance, 0)])
    save_contour(debugger.file_path('optimized_drive.dat'), drive_contour)
    save_contour(debugger.file_path('optimized_driven.dat'), driven_contour)
    return (0, 0), center_distance, toCartesianCoordAsNp(polar_drive, 0,
                                                         0), score
def shape_average(polygon_a: Iterable[float], polygon_b: Iterable[float],
                  area_a: float, area_b: float) -> np.ndarray:
    """
    get the averages of two shapes with respect to polar coordinates from the centroid
    :param polygon_a: polygon a in polar coordinates
    :param polygon_b: polygon b in polar coordinates, same length as polygon a
    :param area_a: area of polygon a, used for normalization
    :param area_b: area of polygon b, used for normalization
    :return: the average contour, not necessarily uniformly sampled
    """
    if hasattr(polygon_a, '__len__') and hasattr(polygon_b, '__len__'):
        # noinspection PyTypeChecker
        assert len(polygon_a) == len(polygon_b)
    # sqrt_a, sqrt_b = [math.sqrt(area_a) for area in (area_a, area_b)]
    offset = align(polygon_a, polygon_b)
    # return toCartesianCoordAsNp([(ra / sqrt_a + rb / sqrt_b) / 2 for ra, rb in zip(polygon_a, polygon_b)], 0, 0)
    return toCartesianCoordAsNp([
        (ra + rb) / 2
        for ra, rb in zip(polygon_a, polygon_b[offset:] + polygon_b[:offset])
    ], 0, 0)
def sample_result(drive_contour: np.ndarray, drive_polygon: Polygon,
                  sample_window: Tuple[float, float, float, float], k: int) \
        -> Union[Tuple[float, float, float, np.ndarray], None]:
    """
    sample the center of the sample window and get the driven gear
    :param drive_contour: the drive gear contour
    :param drive_polygon: the driving polygon
    :param sample_window: the window in which to take sample (minx, maxx, miny, maxy)
    :param k: the drive/driven ratio
    :return: center_x, center_y, center_distance, the driven gear | None if not possible
    """
    min_x, max_x, min_y, max_y = sample_window
    center_x, center_y = (min_x + max_x) / 2, (min_y + max_y) / 2
    if not drive_polygon.contains(Point(center_x, center_y)):
        return None
    polar_coordinates = toExteriorPolarCoord(Point(center_x,
                                                   center_y), drive_contour,
                                             drive_contour.shape[0])
    driven, center_distance, phi = compute_dual_gear(polar_coordinates, k)
    driven_contour = toCartesianCoordAsNp(driven, 0, 0)
    return center_x, center_y, center_distance, driven_contour
def sampling_optimization(drive_contour: np.ndarray, driven_contour: np.ndarray, k: int, sampling_count: (int, int),
                          keep_count: int, resampling_accuracy: int, comparing_accuracy: int, debugger: Reporter,
                          max_sample_depth: int = 5, max_iteration: int = 1, smoothing: Tuple[int, int] = (0, 0),
                          visualization: Union[Dict, None] = None, draw_tar_functions: bool = False) \
        -> List[Tuple[float, float, float, float, float, np.ndarray, np.ndarray]]:
    """
    perform sampling optimization for drive contour and driven contour
    :param drive_contour: the driving gear's contour
    :param driven_contour: the driven gear's contour
    :param k: drive/driven ratio
    :param sampling_count: the number of samples in each dimension
    :param keep_count: the count of samples kept
    :param resampling_accuracy: count of points in the sampling procedure
    :param comparing_accuracy: count of samples during comparison
    :param debugger: the debugger for storing data
    :param max_sample_depth: maximum depth for the sampling optimization to use
    :param max_iteration: maximum time for drive/driven to swap and iterate
    :param smoothing: smoothing level to be taken by uniform re-sampling
    :param visualization: None for no figure, otherwise for visualization configuration
    :param draw_tar_functions: True for drawing tar functions in debug windows (affect performance)
    :return: final total score, score, center_x, center_y, center_distance, drive contour and driven contour
    """
    drive_contour = counterclockwise_orientation(drive_contour)
    driven_contour = counterclockwise_orientation(driven_contour)
    drive_polygon = Polygon(drive_contour)
    driven_polygon = Polygon(driven_contour)
    drive_polar = toExteriorPolarCoord(drive_polygon.centroid, drive_contour,
                                       resampling_accuracy)
    driven_polar = toExteriorPolarCoord(driven_polygon.centroid,
                                        driven_contour, resampling_accuracy)
    drive_smoothing, driven_smoothing = smoothing
    drive_contour = getUniformContourSampledShape(drive_contour,
                                                  resampling_accuracy,
                                                  drive_smoothing > 0)
    driven_contour = getUniformContourSampledShape(driven_contour,
                                                   resampling_accuracy,
                                                   driven_smoothing > 0)
    visualize_config = {
        'fig_size': (16, 9),
    }
    subplots = None
    if visualization is not None:
        visualize_config.update(visualization)
        plt.ion()
        fig, subplots = plt.subplots(3, 2)
        fig.set_size_inches(*visualize_config['fig_size'])
        update_polygon_subplots(drive_contour, driven_contour, subplots[0])

    debugging_root_directory = debugger.get_root_debug_dir_name()
    results = []
    # following two variables change during iteration
    drive = drive_contour
    driven = driven_contour
    for iteration_count in range(max_iteration):
        debug_directory = os.path.join(debugging_root_directory,
                                       f'iteration_{iteration_count}')
        os.makedirs(debug_directory, exist_ok=True)
        drive = counterclockwise_orientation(drive)
        new_res = sample_drive_gear(
            drive, driven_contour, k, sampling_count, keep_count,
            comparing_accuracy, max_sample_depth, debug_directory,
            subplots[1] if subplots is not None else None)
        results += [(None, score, *center, center_distance, drive, driven)
                    for score, *center, center_distance, driven in new_res]
        for index, result in enumerate(results):
            total_score, score, *center, center_distance, this_drive, driven = result
            if subplots is not None:
                update_polygon_subplots(
                    drive_contour, driven_contour,
                    subplots[0])  # so that the two subplots can iterate
                update_polygon_subplots(this_drive, driven, subplots[1])
                subplots[1][0].scatter(center[0], center[1], 3)
                subplots[1][0].text(0, 0, str(center))
                subplots[1][1].text(0, 0, str(score))
                subplots[1][1].scatter(0, 0, 3)
                if draw_tar_functions:
                    tars = [
                        triangle_area_representation(contour,
                                                     comparing_accuracy)
                        for contour in (this_drive, driven)
                    ]
                    for subplot, tar in zip(subplots[2], tars):
                        tar = tar[:, 0]
                        subplot.clear()
                        subplot.plot(range(len(tar)), tar, color='blue')
                if total_score is None:
                    total_score = score + shape_difference_rating(
                        this_drive,
                        drive_contour,
                        comparing_accuracy,
                        distance_function=trivial_distance)
                    results[index] = (total_score, *result[1:])
                score_str = "%.8f" % total_score
                plt.savefig(
                    os.path.join(debug_directory,
                                 f'final_result_{index}_{score_str}.png'))
                save_contour(
                    os.path.join(debug_directory,
                                 f'final_result_{index}_drive.dat'),
                    this_drive)
                save_contour(
                    os.path.join(debug_directory,
                                 f'final_result_{index}_driven.dat'), driven)
        *_, drive, driven = results[-1]  # get the last result
        drive_contour, driven_contour = driven_contour, drive_contour
        drive_polygon, driven_polygon = driven_polygon, drive_polygon
        drive_polar, driven_polar = driven_polar, drive_polar
        drive, driven = driven, drive
        drive_smoothing, driven_smoothing = driven_smoothing, drive_smoothing
        # drive_poly = Polygon(drive)
        # drive = shape_average(drive_polar, toExteriorPolarCoord(Polygon(drive).centroid, drive, resampling_accuracy),
        #                       drive_polygon.area, drive_poly.area)
        drive = phi_average.shape_average(
            drive_polar,
            toExteriorPolarCoord(
                Polygon(drive).centroid, drive, resampling_accuracy))
        drive = toCartesianCoordAsNp(drive, 0, 0)
        drive = getUniformContourSampledShape(drive, resampling_accuracy,
                                              drive_smoothing > 0)
        for subplot in subplots[2]:
            subplot.clear()
    return results
Example #8
0
def generate_std_shapes(type: str, n: int, center_point):
    if type not in std_shapes:
        print(f"Type Error! No {type} found!")
    else:
        return toCartesianCoordAsNp(std_shapes[type](n), center_point[0], center_point[1])
def sampling_optimization(drive_contour: np.ndarray, driven_contour: np.ndarray, sampling_count: Tuple[int, int],
                          keep_count: int, sampling_accuracy: int, iteration_count: int,
                          debugging_suite: ReportingSuite, torque_weight: float = 0.0, k: int = 1,
                          mismatch_penalty=0.5) -> List[Tuple[float, Polar_T]]:
    logger.info(f'Initiating Sampling Optimization with torque_weight = {torque_weight},'
                f' mismatch_penalty = {mismatch_penalty}')
    logger.info(f'k={k}')
    drive_polygon = Polygon(drive_contour)
    driven_polygon = Polygon(driven_contour)
    min_x, min_y, max_x, max_y = drive_polygon.bounds
    window_pairs = (min_x, max_x, min_y, max_y)
    min_x, min_y, max_x, max_y = driven_polygon.bounds
    window_pairs = [(window_pairs, (min_x, max_x, min_y, max_y))]
    x_sample, y_sample = sampling_count

    # start iteration
    results = []  # dummy
    for iteration in range(iteration_count):
        path = debugging_suite.debugger.file_path('iteration_' + str(iteration))
        os.makedirs(path, exist_ok=True)
        # if k == 1:
        window_pairs = list(itertools.chain.from_iterable([
            itertools.product(split_window(drive_window, x_sample, y_sample),
                              split_window(driven_window, x_sample, y_sample))
            for drive_window, driven_window in window_pairs
        ]))
        # else:
        #     # do not allow the driven gear with k to move center
        #     window_pairs = list(itertools.chain.from_iterable([
        #         itertools.product(split_window(drive_window, x_sample, y_sample),
        #                           [driven_window])
        #         for drive_window, driven_window in window_pairs
        #     ]))
        results = sample_in_windows(drive_contour, driven_contour, window_pairs, keep_count,
                                    debugging_suite.sub_suite(os.path.join(path, 'result_')),
                                    sampling_accuracy=sampling_accuracy, torque_weight=torque_weight, k=k,
                                    mismatch_penalty=mismatch_penalty)
        window_pairs = [(drive_window, driven_window) for _, drive_window, driven_window, *__ in results]
        if debugging_suite.plotter is not None:
            for index, final_result in enumerate(results):
                score, *_, reconstructed_drive, max_phi, m_penalty = final_result
                driven, center_distance, phi = compute_dual_gear(reconstructed_drive, k)
                final_drive = toCartesianCoordAsNp(reconstructed_drive, 0, 0)
                final_driven = np.array(
                    psf_rotate(toCartesianCoordAsNp(driven, center_distance, 0), phi[0], (center_distance, 0)))
                debugging_suite.plotter.draw_contours(
                    os.path.join(path, f'final_result_{index}_{"%.6f" % (score,)}.png'),
                    [('carve_drive', final_drive),
                     ('carve_driven', final_driven)],
                    [(0, 0), (center_distance, 0)])
                save_contour(os.path.join(path, f'final_result_{index}_drive.dat'), final_drive)
                save_contour(os.path.join(path, f'final_result_{index}_driven.dat'), final_driven)
                d_drive = differentiate_function(pre_process(phi))
                save_information(os.path.join(path, f'final_result_{index}.txt'), (0, 0), (center_distance, 0), score,
                                 max_dphi_drive=max(d_drive),
                                 actual_distance=score - torque_weight * max_phi,
                                 mismatch_penalty=m_penalty)

    results = results[:keep_count]
    results.sort(key=lambda dist, *_: dist)
    results = [(score, reconstructed_drive)
               for score, drive_window, driven_window, reconstructed_drive, max_phi, m_penalty in results]
    return results
def sample_in_windows(drive_contour: np.ndarray, driven_contour: np.ndarray,
                      window_pairs: List[Tuple[Window_T, Window_T]], keep_count: int,
                      debugging_suite: ReportingSuite, k: int = 1, center_determine_function=center_of_window,
                      sampling_accuracy=1024, torque_weight=0.0, mismatch_penalty: float = 0.5) \
        -> List[Tuple[float, Window_T, Window_T, Polar_T, float, float]]:
    """
    find the best sample windows
    :param drive_contour: the drive contour
    :param driven_contour: the driven contour
    :param window_pairs: pair of windows
    :param keep_count: count of the windows to be kept
    :param debugging_suite: the debugging suite
    :param k: the k of the opposing gear
    :param center_determine_function: function to determine from window to center
    :param sampling_accuracy: number of samples when converting to polar contour
    :param torque_weight: weight of torque term
    :param mismatch_penalty: penalty for the extended d_driven not matching start and end (only for k>1)
    :return: list of (score, drive_window, driven_window, reconstructed_drive, max_phi, mismatch_penalty)
    """
    results = []
    path_prefix = debugging_suite.path_prefix  # store in a directory
    if debugging_suite.figure is not None:
        debugging_suite.figure.clear()  # clear the figure
        plt.figure(debugging_suite.figure.number)
        subplots = debugging_suite.figure.subplots(2, 2)
        update_polygon_subplots(drive_contour, driven_contour, subplots[0])
    else:
        subplots = None
    for index, (drive_window, driven_window) in enumerate(window_pairs):
        center_drive = center_determine_function(drive_window)
        center_driven = center_determine_function(driven_window)
        if not (Polygon(drive_contour).contains(Point(*center_drive)) and Polygon(driven_contour).contains(
                Point(*center_driven))):
            # not good windows
            continue
        # polar, dist, phi = compute_dual_gear(
        #     toExteriorPolarCoord(Point(*center_driven), driven_contour, sampling_accuracy), 1)
        # polar, dist, phi = compute_dual_gear(polar, 1)
        # d_phi = differentiate_function(pre_process(phi))
        # fig, splts = plt.subplots(2, 2, figsize=(9, 9))
        # splts[0][1].plot(*driven_contour.transpose())
        # splts[0][1].axis('equal')
        # splts[1][0].plot(np.linspace(0, 2 * math.pi, len(phi), endpoint=False), phi)
        # splts[1][1].plot(np.linspace(0, 2 * math.pi, len(d_phi), endpoint=False), d_phi, color='red')
        # plt.show() #checkpoint passed
        distance, d_drive, d_driven, dist_drive, dist_driven = \
            contour_distance(drive_contour, center_drive, driven_contour, center_driven, sampling_accuracy, k)
        # splts[1][1].plot(np.linspace(0, 2 * math.pi, len(d_driven), endpoint=False), d_driven, color='blue')
        # plt.show()
        reconstructed_drive = rebuild_polar(0.9, align_and_average(d_drive, d_driven, k=k))
        list_reconstructed_drive = list(reconstructed_drive)
        max_phi = max(differentiate_function(pre_process(compute_dual_gear(list_reconstructed_drive)[-1])))
        final_score = distance + torque_weight * max_phi
        m_penalty = None
        if k != 1:
            m_penalty = mismatch_penalty * abs(d_driven[0] - d_driven[-1])
            logger.info(f'{index} gear: mismatching start = {d_driven[0]}, end = {d_driven[-1]}, penalized {m_penalty}')
            final_score += m_penalty
        logging.info(f'{index} gear: plain score={distance}, max of phi\'={max_phi}')
        results.append((final_score, drive_window, driven_window, list_reconstructed_drive, max_phi, m_penalty))
        if subplots is not None:
            update_polygon_subplots(drive_contour, driven_contour, subplots[0])  # clear sample regions
            reconstructed_driven, plt_center_dist, plt_phi = compute_dual_gear(list_reconstructed_drive, k)
            reconstructed_drive_contour = toCartesianCoordAsNp(reconstructed_drive, 0, 0)
            reconstructed_driven_contour = toCartesianCoordAsNp(reconstructed_driven, 0, 0)
            update_polygon_subplots(reconstructed_drive_contour, reconstructed_driven_contour, subplots[1])
            min_x, max_x, min_y, max_y = drive_window
            sample_region = Rectangle((min_x, min_y), max_x - min_x, max_y - min_y, color='red', fill=False)
            subplots[0][0].add_patch(sample_region)
            min_x, max_x, min_y, max_y = driven_window
            sample_region = Rectangle((min_x, min_y), max_x - min_x, max_y - min_y, color='red', fill=False)
            subplots[0][1].add_patch(sample_region)
            subplots[0][0].scatter(*center_drive, 5)
            subplots[0][1].scatter(*center_driven, 5)
            subplots[1][0].scatter(0, 0, 5)
            subplots[1][1].scatter(0, 0, 5)
            plt.savefig(path_prefix + f'{index}.png')
            save_contour(path_prefix + f'{index}_drive.dat', reconstructed_drive_contour)
            save_contour(path_prefix + f'{index}_driven.dat', reconstructed_driven_contour)
            save_information(path_prefix + f'{index}.txt', center_drive, center_driven, final_score,
                             max_dphi_drive=max_phi, actual_distance=distance, mismatch_penalty=m_penalty)

            # get information about thee phi' functions
            original_figure = plt.gcf()
            figure, new_subplots = plt.subplots(2, 2, figsize=(16, 16))
            new_subplots[0][0].plot(np.linspace(0, 2 * math.pi, len(d_drive), endpoint=False), d_drive)
            new_subplots[0][0].axis([0, 2 * math.pi, 0, 2 * math.pi])
            new_subplots[0][1].plot(np.linspace(0, 2 * math.pi, len(d_driven), endpoint=False), d_driven)
            new_subplots[0][1].axis([0, 2 * math.pi, 0, 2 * math.pi])
            new_subplots[1][0].plot(np.linspace(0, 2 * math.pi, len(d_drive), endpoint=False),
                                    align_and_average(d_drive, d_driven, k=k))
            new_subplots[1][0].axis([0, 2 * math.pi, 0, 2 * math.pi])
            debugging_suite.plotter.draw_contours(
                path_prefix + f'drive_contour_{index}.png',
                [('carve_drive', drive_contour)],
                [center_drive])
            debugging_suite.plotter.draw_contours(
                path_prefix + f'driven_contour_{index}.png',
                [('carve_driven', driven_contour)],
                [center_driven])
            final_driven = np.array(
                psf_rotate(toCartesianCoordAsNp(reconstructed_driven, plt_center_dist, 0), plt_phi[0],
                           (plt_center_dist, 0)))
            debugging_suite.plotter.draw_contours(
                path_prefix + f'reconstructed_contour_{index}.png',
                [('carve_drive', reconstructed_drive_contour),
                 ('carve_driven', final_driven)],
                [(0, 0), (plt_center_dist, 0)])
            if k != 1:
                # then offset shall be 0
                new_subplots[1][1].plot(np.linspace(0, 2 * math.pi, len(d_drive), endpoint=False),
                                        extend_part(d_driven, 0, int(len(d_driven) / k), len(d_drive)))
                new_subplots[1][1].axis('equal')
            plt.axis('equal')
            plt.savefig(path_prefix + f'{index}_functions.png')
            plt.close()
            plt.figure(original_figure.number)
    results.sort(key=lambda dist, *_: dist)
    return results[:keep_count]