def execute_algorithm_with_params(params):

    """Execute the algorithm specified in the first of the passed parameters with the rest of parameters, and return the solution, value and elapsed time"""

    # unpack the algorithm and its parameters
    algorithm, algorithm_name, problem, show_solution_plot, solution_plot_save_path, calculate_times, calculate_value_evolution = params

    start_time = time.time()
    value_evolution = None
    times_dict = None
    if calculate_value_evolution:
        if algorithm == evolutionary.solve_problem:
            if calculate_times:
                solution, times_dict, value_evolution = algorithm(problem, calculate_times=calculate_times, return_population_fitness_per_generation=calculate_value_evolution)
            else:
                solution, value_evolution = algorithm(problem, calculate_times=calculate_times, return_population_fitness_per_generation=calculate_value_evolution)
        else:
            if calculate_times:
                solution, times_dict, value_evolution = algorithm(problem, calculate_times=calculate_times, return_value_evolution=calculate_value_evolution)
            else:
                solution, value_evolution = algorithm(problem, calculate_times=calculate_times, return_value_evolution=calculate_value_evolution)
    elif calculate_times:
        solution, times_dict = algorithm(problem, calculate_times=calculate_times)
    else:
        solution = algorithm(problem)
    elapsed_time = get_time_since(start_time)

    if solution and (show_solution_plot or solution_plot_save_path):
        solution.visualize(show_plot=show_solution_plot, save_path=solution_plot_save_path)

    return solution, solution.value, value_evolution, elapsed_time, times_dict
def solve_problem(
        problem,
        greedy_score_function=get_weighted_sum_of_item_value_and_profitability_ratio,
        value_weight=VALUE_WEIGHT,
        area_weight=AREA_WEIGHT,
        max_iter_num=MAX_ITER_NUM,
        max_iter_num_without_changes=MAX_ITER_NUM_WITHOUT_CHANGES,
        repetition_num=REPETITION_NUM,
        item_index_to_place_first=-1,
        item_specialization_iter_proportion=0.,
        calculate_times=False,
        return_value_evolution=False):
    """Find and return a solution to the passed problem, using a greedy strategy"""

    # determine the bounds of the container
    min_x, min_y, max_x, max_y = get_bounds(problem.container.shape)

    max_item_specialization_iter_num = item_specialization_iter_proportion * max_iter_num

    start_time = 0
    sort_time = 0
    item_discarding_time = 0
    item_selection_time = 0
    addition_time = 0
    value_evolution_time = 0

    if calculate_times:
        start_time = time.time()

    if return_value_evolution:
        value_evolution = list()
    else:
        value_evolution = None

    if calculate_times:
        value_evolution_time += get_time_since(start_time)

    if calculate_times:
        start_time = time.time()

    # sort items (with greedy score calculated) by weight, to speed up their discarding (when they would cause the capacity to be exceeded)
    original_items_by_weight = [
        (index_item_tuple[0],
         greedy_score_function(index_item_tuple[1], value_weight,
                               area_weight), index_item_tuple[1])
        for index_item_tuple in sorted(
            list(problem.items.items()),
            key=lambda index_item_tuple: index_item_tuple[1].weight)
    ]

    if calculate_times:
        sort_time += get_time_since(start_time)

    if calculate_times:
        start_time = time.time()

    # discard the items that would make the capacity of the container to be exceeded
    original_items_by_weight = original_items_by_weight[:get_index_after_weight_limit(
        original_items_by_weight, problem.container.max_weight)]

    if calculate_times:
        item_discarding_time += get_time_since(start_time)

    best_solution = None

    # if the algorithm is iterated, it is repeated and the best solution is kept in the end
    for _ in range(repetition_num):

        # if the algorithm is iterated, use a copy of the initial sorted items, to start fresh next time
        if repetition_num > 1:
            items_by_weight = copy.deepcopy(original_items_by_weight)
        else:
            items_by_weight = original_items_by_weight

        # create an initial solution with no item placed in the container
        solution = Solution(problem)

        # placements can only be possible with capacity and valid items
        if problem.container.max_weight and items_by_weight:

            iter_count_without_changes = 0

            # try to add items to the container, for a maximum number of iterations
            for i in range(max_iter_num):

                if calculate_times:
                    start_time = time.time()

                # if needed, select a specific item to try to place (only for a maximum number of attempts)
                if item_index_to_place_first >= 0 and i < max_item_specialization_iter_num:
                    item_index = item_index_to_place_first
                    list_index = -1

                # perform a random choice of the next item to try to place, weighting each item with their profitability ratio, that acts as an stochastic selection probability
                else:
                    list_index = select_item(items_by_weight)
                    item_index = items_by_weight[list_index][0]

                if calculate_times:
                    item_selection_time += get_time_since(start_time)

                if calculate_times:
                    start_time = time.time()

                # try to add the item in a random position and with a random rotation; if it is valid, remove the item from the pending list
                if solution.add_item(item_index, (random.uniform(
                        min_x, max_x), random.uniform(min_y, max_y)),
                                     random.uniform(0, 360)):

                    # the item to place first is assumed to have been placed, if there was any
                    item_index_to_place_first = -1

                    if calculate_times:
                        addition_time += get_time_since(start_time)

                    # find the weight that can still be added
                    remaining_weight = problem.container.max_weight - solution.weight

                    # stop early if the capacity has been exactly reached
                    if not remaining_weight:
                        break

                    # remove the placed item from the list of pending items
                    if list_index >= 0:
                        items_by_weight.pop(list_index)

                    # if focusing on an item to place first, find its associated entry in the list to remove it
                    else:
                        for list_i in range(len(items_by_weight)):
                            if items_by_weight[list_i][0] == item_index:
                                items_by_weight.pop(list_i)
                                break

                    if calculate_times:
                        start_time = time.time()

                    # discard the items that would make the capacity of the container to be exceeded
                    items_by_weight = items_by_weight[:
                                                      get_index_after_weight_limit(
                                                          items_by_weight,
                                                          remaining_weight)]

                    if calculate_times:
                        item_discarding_time += get_time_since(start_time)

                    # stop early if it is not possible to place more items, because all have been placed or all the items outside would cause the capacity to be exceeded
                    if not items_by_weight:
                        break

                    # reset the potential convergence counter, since an item has been added
                    iter_count_without_changes = 0

                else:

                    if calculate_times:
                        addition_time += get_time_since(start_time)

                    # register the fact of being unable to place an item this iteration
                    iter_count_without_changes += 1

                    # stop early if there have been too many iterations without changes (unless a specific item is tried to be placed first)
                    if iter_count_without_changes >= max_iter_num_without_changes and item_index_to_place_first < 0:
                        break

                if return_value_evolution:

                    if calculate_times:
                        start_time = time.time()

                    value_evolution.append(solution.value)

                    if calculate_times:
                        value_evolution_time += get_time_since(start_time)

        # if the algorithm uses multiple iterations, adopt the current solution as the best one if it is the one with highest value up to now
        if not best_solution or solution.value > best_solution.value:
            best_solution = solution

    # encapsulate all times informatively in a dictionary
    if calculate_times:
        approx_total_time = sort_time + item_selection_time + item_discarding_time + addition_time + value_evolution_time
        time_dict = {
            "Weight-sort and profit ratio calculation":
            (sort_time, sort_time / approx_total_time),
            "Stochastic item selection":
            (item_selection_time, item_selection_time / approx_total_time),
            "Item discarding": (item_discarding_time,
                                item_discarding_time / approx_total_time),
            "Addition and geometric validation":
            (addition_time, addition_time / approx_total_time),
            "Keeping value of each iteration":
            (value_evolution_time, value_evolution_time / approx_total_time)
        }
        if return_value_evolution:
            return best_solution, time_dict, value_evolution
        return best_solution, time_dict

    if return_value_evolution:
        return best_solution, value_evolution

    return best_solution
def solve_problem(problem,
                  max_iter_num=MAX_ITER_NUM,
                  max_iter_num_without_adding=MAX_ITER_NUM_WITHOUT_ADDITIONS,
                  iter_num_to_revert_removal=ITER_NUM_TO_REVERT_REMOVAL,
                  remove_prob=ITEM_REMOVAL_PROBABILITY,
                  consec_remove_prob=CONSECUTIVE_ITEM_REMOVAL_PROBABILITY,
                  ignore_removed_item_prob=IGNORE_REMOVED_ITEM_PROBABILITY,
                  modify_prob=PLACEMENT_MODIFICATION_PROBABILITY,
                  calculate_times=False,
                  return_value_evolution=False):
    """Find and return a solution to the passed problem, using an reversible strategy"""

    # create an initial solution with no item placed in the container
    solution = Solution(problem)

    # determine the bounds of the container
    min_x, min_y, max_x, max_y = get_bounds(problem.container.shape)

    start_time = 0
    sort_time = 0
    item_discarding_time = 0
    item_selection_time = 0
    addition_time = 0
    removal_time = 0
    modification_time = 0
    value_evolution_time = 0

    if calculate_times:
        start_time = time.time()

    if return_value_evolution:
        value_evolution = list()
    else:
        value_evolution = None

    if calculate_times:
        value_evolution_time += get_time_since(start_time)

    if calculate_times:
        start_time = time.time()

    # sort items by weight, to speed up their discarding (when they would cause the capacity to be exceeded)
    items_by_weight = sorted(
        list(problem.items.items()),
        key=lambda index_item_tuple: index_item_tuple[1].weight)

    if calculate_times:
        sort_time += get_time_since(start_time)

    iter_count_since_addition = 0
    iter_count_since_removal = 0
    solution_before_removal = None

    if calculate_times:
        start_time = time.time()

    # discard the items that would make the capacity of the container to be exceeded
    items_by_weight = items_by_weight[:get_index_after_weight_limit(
        items_by_weight, problem.container.max_weight)]

    ignored_item_index = -1

    if calculate_times:
        item_discarding_time += get_time_since(start_time)

    # placements can only be possible with capacity and valid items
    if problem.container.max_weight and items_by_weight:

        # try to add items to the container, for a maximum number of iterations
        for i in range(max_iter_num):

            if calculate_times:
                start_time = time.time()

            # perform a random choice of the next item to try to place
            list_index, item_index = select_item(items_by_weight)

            if calculate_times:
                item_selection_time += get_time_since(start_time)

            if calculate_times:
                start_time = time.time()

            # try to add the item in a random position and with a random rotation; if it is valid, remove the item from the pending list
            if solution.add_item(
                    item_index,
                (random.uniform(min_x, max_x), random.uniform(min_y, max_y)),
                    random.uniform(0, 360)):

                if calculate_times:
                    addition_time += get_time_since(start_time)

                # find the weight that can still be added
                remaining_weight = problem.container.max_weight - solution.weight

                # stop early if the capacity has been exactly reached
                if not remaining_weight:
                    break

                # remove the placed item from the list of pending items
                items_by_weight.pop(list_index)

                if calculate_times:
                    start_time = time.time()

                # discard the items that would make the capacity of the container to be exceeded
                items_by_weight = items_by_weight[:get_index_after_weight_limit(
                    items_by_weight, remaining_weight)]

                if calculate_times:
                    item_discarding_time += get_time_since(start_time)

                # stop early if it is not possible to place more items, because all have been placed or all the items outside would cause the capacity to be exceeded
                if not items_by_weight:
                    break

                # reset the potential convergence counter, since an item has been added
                iter_count_since_addition = 0

            else:

                if calculate_times:
                    addition_time += get_time_since(start_time)

                # register the fact of being unable to place an item this iteration
                iter_count_since_addition += 1

                # stop early if there have been too many iterations without changes
                if iter_count_since_addition == max_iter_num_without_adding:
                    break

            if calculate_times:
                start_time = time.time()

            # if there are items in the container, try to remove an item with a certain probability (different if there was a recent removal)
            if solution.weight > 0 and random.uniform(
                    0., 1.) < (consec_remove_prob
                               if solution_before_removal else remove_prob):

                # if there is no solution prior to a removal with pending re-examination
                if not solution_before_removal:

                    # save the current solution before removing, just in case in needs to be restored later
                    solution_before_removal = copy.deepcopy(solution)

                    # reset the counter of iterations since removal, to avoid reverting earlier than needed
                    iter_count_since_removal = 0

                # get the index of the removed item, which is randomly chosen
                removed_index = solution.remove_random_item()

                # with a certain probability, only if not ignoring any item yet, ignore placing again the removed item until the operation gets reverted or permanently accepted
                if ignored_item_index < 0 and items_by_weight and random.uniform(
                        0., 1.) < ignore_removed_item_prob:
                    ignored_item_index = removed_index

                # otherwise, add the removed item to the weight-sorted list of pending-to-add items
                else:
                    items_by_weight.insert(
                        get_index_after_weight_limit(
                            items_by_weight,
                            problem.items[removed_index].weight),
                        (removed_index, problem.items[removed_index]))

            # if there is a recent removal to be confirmed or discarded after some time
            if solution_before_removal:

                # re-examine a removal after a certain number of iterations
                if iter_count_since_removal == iter_num_to_revert_removal:

                    # if the value in the container has improved since removal, accept the operation in a definitive way
                    if solution.value > solution_before_removal.value:

                        # if an item had been ignored, make it available for placement again
                        if ignored_item_index >= 0:
                            items_by_weight.insert(
                                get_index_after_weight_limit(
                                    items_by_weight,
                                    problem.items[ignored_item_index].weight),
                                (ignored_item_index,
                                 problem.items[ignored_item_index]))

                    # otherwise, revert the solution to the pre-removal state
                    else:
                        solution = solution_before_removal

                        # after reverting a removal, have some margin to try to add items
                        iter_count_since_addition = 0

                    # reset removal data
                    solution_before_removal = None
                    iter_count_since_removal = 0
                    ignored_item_index = -1

                # the check will be done after more iterations
                else:
                    iter_count_since_removal += 1

            if calculate_times:
                removal_time += get_time_since(start_time)

            if calculate_times:
                start_time = time.time()

            # if there are still items in the container (maybe there was a removal), modify existing placements with a certain probability
            if solution.weight > 0 and random.uniform(0., 1.) < modify_prob:

                # perform a random choice of the item to try to affect
                _, item_index = select_item(items_by_weight)

                # move to a random position of the container with a probability of 50%
                if random.uniform(0., 1.) < 0.5:
                    solution.move_item_to(item_index, (random.uniform(
                        min_x, max_x), random.uniform(min_y, max_y)))

                # otherwise, perform a random rotation
                else:
                    solution.rotate_item_to(item_index, random.uniform(0, 360))

            if calculate_times:
                modification_time += get_time_since(start_time)

            if return_value_evolution:

                if calculate_times:
                    start_time = time.time()

                value_evolution.append(solution.value)

                if calculate_times:
                    value_evolution_time += get_time_since(start_time)

        # in the end, revert the last unconfirmed removal if it did not improve the container's value
        if solution_before_removal and solution.value < solution_before_removal.value:
            solution = solution_before_removal

            if return_value_evolution:

                if calculate_times:
                    start_time = time.time()

                value_evolution[-1] = solution.value

                if calculate_times:
                    value_evolution_time += get_time_since(start_time)

    # encapsulate all times informatively in a dictionary
    if calculate_times:
        approx_total_time = sort_time + item_selection_time + item_discarding_time + addition_time + removal_time + modification_time + value_evolution_time
        time_dict = {
            "Weight-sort": (sort_time, sort_time / approx_total_time),
            "Stochastic item selection":
            (item_selection_time, item_selection_time / approx_total_time),
            "Item discarding": (item_discarding_time,
                                item_discarding_time / approx_total_time),
            "Addition (with geometric validation)":
            (addition_time, addition_time / approx_total_time),
            "Removal and reverting-removal":
            (removal_time, removal_time / approx_total_time),
            "Placement modification (with geometric validation)":
            (modification_time, modification_time / approx_total_time),
            "Keeping value of each iteration":
            (value_evolution_time, value_evolution_time / approx_total_time)
        }
        if return_value_evolution:
            return solution, time_dict, value_evolution
        return solution, time_dict

    if return_value_evolution:
        return solution, value_evolution

    return solution
def perform_experiments(problem_type, output_dir, load_experiments):

    """Perform a set of experiments for the problem with the passed index, and producing output in the specified directory (when applicable)"""

    experiment_file_path = output_dir + "experiments.pickle"

    # data structure where to store all the problems, and the experimental results for each algorithm: solutions, final values and times
    experiment_dict = dict()

    # if experiments should be loaded (not repeated), do it if possible
    if load_experiments:
        with open(experiment_file_path, "rb") as _file:
            experiment_dict = pickle.load(_file)

    # perform experiments if pre-existing results were not loaded
    if not experiment_dict:

        # given a problem type, create a set of problem instances that are solved (optimally) with manual placements (using actions available for all the algorithms)
        problems, problem_names, manual_solutions = create_problems(problem_type)

        if problems and problem_names and manual_solutions:

            # parameters for the experimentation; note: calculating internal times and value evolution can increase the overall time of algorithms (in a slight, almost neglectible way)
            execution_num = 10  # 1
            process_num = 10  # 1
            calculate_internal_times = True
            calculate_value_evolution = True

            start_time = time.time()

            # solve each problem with each algorithm
            for i, (problem, problem_name, solution) in enumerate(zip(problems, problem_names, manual_solutions)):

                experiment_dict[problem_name] = {"problem": problem, "manual_solution": solution, "algorithms": dict()}

                # solve the problem with different algorithms, executing each one multiple times to gain statistical significance
                for (algorithm_name, algorithm) in [("Greedy", greedy.solve_problem), ("Reversible", reversible.solve_problem), ("Evolutionary", evolutionary.solve_problem)]:

                    solutions, values, value_evolutions, times, time_divisions = execute_algorithm(algorithm=algorithm, algorithm_name=algorithm_name, problem=problem, execution_num=execution_num, process_num=process_num, calculate_times=calculate_internal_times, calculate_fitness_stats=calculate_value_evolution)
                    experiment_dict[problem_name]["algorithms"][algorithm_name] = {"solutions": solutions, "values": values, "value_evolutions": value_evolutions, "times": times, "time_divisions": time_divisions}

            # show the total time spent doing experiments (note that significant overhead can be introduced beyond calculation time if plots are shown or saved to files; for strict time measurements, plotting should be avoided altogether)
            print("Total experimental calculation time: {} s".format(round(get_time_since(start_time) / 1000.), 3))

    if experiment_dict:

        # experiment-saving parameter
        save_experiments = True

        # if possible, save the experiments to a binary file
        if not load_experiments and save_experiments:
            with open(experiment_file_path, "wb") as file:
                pickle.dump(experiment_dict, file)

        # visualization/saving parameters
        show_problem_stats = False
        save_problem_stats = False  # True
        show_manual_solution_plots = False
        save_manual_solution_plots = False  # True
        show_algorithm_solution_plots = False
        save_algorithm_solution_plots = False  # True
        show_value_evolution_plots = False
        save_value_evolution_plots = False  # True
        show_time_division_plots = False
        save_time_division_plots = False  # True
        show_algorithm_comparison = False
        save_algorithm_comparison = False  # True
        show_aggregated_result_tables = True
        save_aggregated_result_tables = False  # True

        # show/save the results of the experiments
        visualize_and_save_experiments(experiment_dict, output_dir, can_plots_show_value_and_weight=problem_type == KNAPSACK_PACKING_PROBLEM_TYPE, show_problem_stats=show_problem_stats, save_problem_stats=save_problem_stats, show_manual_solution_plots=show_manual_solution_plots, save_manual_solution_plots=save_manual_solution_plots, show_algorithm_solution_plots=show_algorithm_solution_plots, save_algorithm_solution_plots=save_algorithm_solution_plots, show_value_evolution_plots=show_value_evolution_plots, save_value_evolution_plots=save_value_evolution_plots, show_time_division_plots=show_time_division_plots, save_time_division_plots=save_time_division_plots, show_algorithm_comparison=show_algorithm_comparison, save_algorithm_comparison=save_algorithm_comparison, show_aggregated_result_tables=show_aggregated_result_tables, save_aggregated_result_tables=save_aggregated_result_tables)

    else:
        print("The experiments cannot be performed (there are no problems available).")
def create_knapsack_packing_problems_with_manual_solutions(can_print=False):

    """Create a set of Knapsack-Packing problem instances that are solved (optimally) with manual placements (using actions available for all the algorithms); both the problems and solutions are returned"""

    problems, solutions = list(), list()

    start_time = time.time()

    # Problem 1

    max_weight = 120.
    container_shape = Circle((3.3, 3.3), 3.3)
    container = Container(max_weight, container_shape)
    items = [Item(Polygon([(0, 0), (0, 4.5), (4.5, 4.5), (4.5, 0)]), 40., 50.),
             Item(Circle((0, 0), 0.45), 20., 5.),
             Item(Circle((0, 0), 0.45), 20., 10.),
             Item(Circle((0, 0), 0.45), 20., 15.),
             Item(Circle((0, 0), 0.45), 20., 20.)]
    problem = Problem(container, items)
    problems.append(problem)

    solution = Solution(problem)
    solutions.append(solution)

    print_if_allowed(solution.add_item(0, (3.3, 3.3), 0.), can_print)
    print_if_allowed(solution.add_item(1, (3.3, 6.05), 0.), can_print)
    print_if_allowed(solution.add_item(2, (3.3, 0.55), 0.), can_print)
    print_if_allowed(solution.add_item(3, (6.05, 3.3), 0.), can_print)
    print_if_allowed(solution.add_item(4, (0.55, 3.3), 0.), can_print)

    # Problem 2

    max_weight = 100.
    container_shape = Point(5, 5).buffer(5, 4)
    container = Container(max_weight, container_shape)
    items = [Item(MultiPolygon([(Point(5, 5).buffer(4.7, 4).exterior.coords,
                                 [tuple(Point(5, 5).buffer(4, 4).exterior.coords)])]), 10., 25.),
             Item(MultiPolygon([(Point(5, 5).buffer(3.7, 4).exterior.coords,
                                 [tuple(Point(5, 5).buffer(3, 4).exterior.coords)])]), 10., 15.),
             Item(MultiPolygon([(Point(5, 5).buffer(2.7, 4).exterior.coords,
                                 [tuple(Point(5, 5).buffer(2, 4).exterior.coords)])]), 10., 20.),
             Item(MultiPolygon([(Point(5, 5).buffer(1.7, 4).exterior.coords,
                                 [tuple(Point(5, 5).buffer(1, 4).exterior.coords)])]), 20., 20.),
             Item(Circle((0., 0.), 0.7), 20., 10)]
    problem = Problem(container, items)
    problems.append(problem)

    solution = Solution(problem)
    solutions.append(solution)

    print_if_allowed(solution.add_item(0, (5., 5.), 0.), can_print)
    print_if_allowed(solution.add_item(1, (5., 5.), 0.), can_print)
    print_if_allowed(solution.add_item(2, (5., 5.), 0.), can_print)
    print_if_allowed(solution.add_item(3, (5., 5.), 0.), can_print)
    print_if_allowed(solution.add_item(4, (5., 5.), 0.), can_print)

    # Problem 3

    max_weight = 32.
    container_shape = Polygon([(0, 0), (0, 10), (10, 10), (10, 0)])
    container = Container(max_weight, container_shape)
    items = [Item(Polygon([(0, 0), (0, 6.), (6., 0)]), 10., 20.),
             Item(Polygon([(0, 0), (0, 6.), (6., 0)]), 10., 10.),
             Item(Ellipse((0, 0), 1.5, 0.3), 10., 5.),
             Item(Ellipse((0, 0), 3, 0.3), 5., 5.),
             Item(Ellipse((0, 0), 1.5, 0.3), 5., 5.),
             Item(Ellipse((0, 0), 3, 0.3), 10., 5.)]
    problem = Problem(container, items)
    problems.append(problem)

    solution = Solution(problem)
    solutions.append(solution)

    print_if_allowed(solution.add_item(0, (4.99, 5), 0.), can_print)
    print_if_allowed(solution.add_item(1, (5.01, 5), 180.), can_print)
    print_if_allowed(solution.add_item(3, (5., 1.65), 0), can_print)
    print_if_allowed(solution.add_item(4, (5., 8.35), 0), can_print)

    # Problem 4

    max_weight = 50.
    container_shape = Ellipse((3., 2.), 3., 2.)
    container = Container(max_weight, container_shape)
    items = [Item(Ellipse((0., 0.), 0.7, 0.5), 5., 7),
             Item(Ellipse((0., 0.), 0.3, 0.1), 7., 2),
             Item(Ellipse((0., 0.), 0.2, 0.4), 8., 4),
             Item(Ellipse((0., 0.), 0.5, 0.3), 3., 5),
             Item(Circle((0., 0.), 0.4), 4., 5),
             Item(Circle((0., 0.), 0.25), 3., 2),
             Item(Circle((0., 0.), 0.2), 9., 5),
             Item(Circle((0., 0.), 0.1), 4., 3.),
             Item(Circle((0., 0.), 0.7), 9., 3.)]
    problem = Problem(container, items)
    problems.append(problem)

    solution = Solution(problem)
    solutions.append(solution)

    print_if_allowed(solution.add_item(0, (3., 1.94), 0.), can_print)
    print_if_allowed(solution.add_item(2, (3., 3.24), 90.), can_print)
    print_if_allowed(solution.add_item(3, (3., 2.74), 0.), can_print)
    print_if_allowed(solution.add_item(4, (2.25, 3.5), 0.), can_print)
    print_if_allowed(solution.add_item(5, (3., 3.71), 0.), can_print)
    print_if_allowed(solution.add_item(6, (3.46, 3.75), 0.), can_print)
    print_if_allowed(solution.add_item(7, (3.44, 3.43), 0.), can_print)
    print_if_allowed(solution.add_item(8, (3., 0.72), 0.), can_print)

    # Problem 5

    max_weight = 100.
    container_shape = MultiPolygon([(((0, 0), (0.5, 3), (0, 5), (5, 4.5), (5, 0)),
                                     [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1)),
                                      ((0.3, 0.3), (0.3, 1.2), (1.6, 2.9), (0.75, 0.4)),
                                      ((3.1, 1.5), (3.5, 4.5), (4.9, 4.4), (4.8, 1.2))])])
    container = Container(max_weight, container_shape)
    items = [Item(Polygon([(0, 0), (1, 1), (1, 0)]), 15., 32.),
             Item(Polygon([(1, 2), (1.5, 3), (4, 5), (1, 4)]), 30., 100.),
             Item(MultiPolygon([(((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)),
                                 [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1)),
                                  ((0.3, 0.3), (0.3, 0.6), (0.6, 0.6), (0.6, 0.4))])]), 12., 30.),
             Item(Polygon([(0.1, 0.1), (0.1, 0.2), (0.2, 0.2)]), 10., 10.),
             Item(MultiPolygon([(((0., 0.), (0., 1.4), (2., 1.3), (2., 0.)),
                                 [((0.1, 0.1), (0.1, 0.15), (0.15, 0.15), (0.15, 0.1)),
                                  ((0.2, 0.2), (0.2, 1.2), (1.8, 1.1), (1.8, 0.2))
                                  ])]), 1., 5.),
             Item(Circle((0., 0.), 0.4), 1., 14.),
             Item(Circle((0., 0.), 0.1), 2., 12.),
             Item(Ellipse((0., 0.), 0.5, 0.2), 3., 12.),
             Item(Polygon([(0., 0.), (0., 0.3), (0.3, 0.3)]), 1., 10.),
             Item(Ellipse((0., 0.), 0.8, 0.3), 10., 12.),
             Item(Ellipse((0., 0.), 0.1, 0.05), 1., 2.),
             # random items
             # Item(shape_functions.create_random_polygon(0, 0, 0.8, 0.8, 10), 1., 5.),
             # Item(shape_functions.create_random_triangle_in_rectangle_corner(0, 0, 0.8, 0.8), 1., 5.),
             # Item(shape_functions.create_random_quadrilateral_in_rectangle_corners(0, 0, 0.8, 0.8), 1., 5.),
             # out-items
             Item(Circle((0., 0.), 0.2), 50., 1.),
             ]
    problem = Problem(container, items)
    problems.append(problem)

    solution = Solution(problem)
    solutions.append(solution)

    # solution.visualize()

    # print(solution.add_item(0, (1.2, 0.5), 150.))
    print_if_allowed(solution.add_item(0, (1.2, 0.5), 0.), can_print)
    print_if_allowed(solution.add_item(1, (2., 3.), 0.), can_print)
    print_if_allowed(solution.add_item(2, (2.5, 2.5), 0.), can_print)
    print_if_allowed(solution.move_item_in_direction(2, (1, -1), evolutionary.MUTATION_MODIFY_MOVE_UNTIL_INTERSECTION_POINT_NUM, evolutionary.MUTATION_MODIFY_MOVE_UNTIL_INTERSECTION_MIN_DIST_PROPORTION, 9999), can_print)
    print_if_allowed(solution.add_item(3, (2.5, 2.4), 0.), can_print)
    print_if_allowed(solution.add_item(4, (3., 0.7), 0.), can_print)
    print_if_allowed(solution.add_item(5, (3.03, 0.73), 0.), can_print)
    print_if_allowed(solution.add_item(6, (3.45, 1.02), 0.), can_print)
    print_if_allowed(solution.add_item(7, (3., 3.82), 45.), can_print)
    print_if_allowed(solution.add_item(8, (2.4, 0.7), 0.), can_print)
    print_if_allowed(solution.move_item(0, (0.29, 0)), can_print)
    # print_if_allowed(solution.move_item_to(0, (1.49, 2.5)), can_print)
    print_if_allowed(solution.rotate_item_to(0, 180.), can_print)
    print_if_allowed(solution.rotate_item(0, 90.), can_print)
    # print_if_allowed(solution.remove_item(0), can_print)
    # print_if_allowed(solution.rotate_item(4, 20), can_print)
    # print_if_allowed(solution.move_item(7, (1, 0)), can_print)
    # print_if_allowed(solution.rotate_item(7, -45), can_print)
    # print_if_allowed(solution.move_item(5, (-0.4, 0)), can_print)
    print_if_allowed(solution.add_item(9, (1.2, 4.07), 15.), can_print)
    print_if_allowed(solution.add_item(10, (3.6, 0.45), 30.), can_print)
    # print_if_allowed(solution.add_item(11, (4.5, 0.5), 0.), can_print)

    # Problem 6

    max_weight = 150.
    container_shape = MultiPolygon([(((0., 0.), (5., 0.), (5., 5.), (0., 5.)),
                                     [((0.7, 0.7), (1.5, 0.7), (1.5, 1.5), (0.7, 1.5)),
                                      ((2.4, 0.3), (4.3, 0.3), (4.3, 4.3), (2.4, 4.3)),
                                      ((0.7, 2.7), (1.5, 2.7), (1.5, 3.5), (0.7, 3.5))])])
    container = Container(max_weight, container_shape)
    items = [Item(Polygon([(0., 0.), (1.6, 0.), (1.4, 0.2), (1.7, 1.)]), 6., 13.),
             Item(Polygon([(0., 0.), (1.6, 3.), (2.8, 2.9), (1.5, 2.7), (1.9, 1.6)]), 11., 12.),
             Item(Polygon([(0., 0.), (1.8, 1.5), (0., 2.8)]), 15., 25.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.2), (0., 0.2)]), 14., 10.),
             Item(Polygon([(0., 0.), (2.5, 0.), (1.5, 0.2), (0., 0.2)]), 10., 12.),
             Item(Polygon([(0., 0.), (1.6, 0.), (0.8, 0.45), (0.6, 0.7), (0., 0.45)]), 17., 8.),
             Item(Polygon([(0., 0.), (1.5, 0.), (0.8, 0.15), (0., 0.1)]), 13., 12.),
             Item(Polygon([(0., 0.), (1.5, 0.), (0.8, 0.15), (0., 0.1)]), 15., 7.),
             Item(Ellipse((0., 0.), 0.5, 0.3), 15., 8.),
             Item(Ellipse((0., 0.), 0.2, 0.8), 14., 21.),
             Item(Circle((0., 0.), 0.2), 18., 18.),
             Item(Circle((0., 0.), 0.6), 11., 12.),
             Item(Circle((0., 0.), 0.35), 12., 9.)]
    problem = Problem(container, items)
    problems.append(problem)

    solution = Solution(problem)
    solutions.append(solution)

    print_if_allowed(solution.add_item(0, (0.9, 2.02), 0.), can_print)
    print_if_allowed(solution.add_item(3, (0.78, 0.12), 0.), can_print)
    print_if_allowed(solution.add_item(4, (2.8, 0.12), 0.), can_print)
    print_if_allowed(solution.add_item(5, (0.8, 3.85), 0.), can_print)
    print_if_allowed(solution.add_item(6, (0.78, 0.3), 0.), can_print)
    print_if_allowed(solution.add_item(7, (2.3, 2.57), 90.), can_print)
    print_if_allowed(solution.add_item(8, (0.3, 2.98), 90.), can_print)
    print_if_allowed(solution.add_item(9, (2.17, 1.05), 0.), can_print)
    print_if_allowed(solution.add_item(10, (1.8, 0.45), 0.), can_print)
    print_if_allowed(solution.add_item(11, (1.77, 4.38), 0.), can_print)
    print_if_allowed(solution.add_item(12, (0.35, 4.63), 0.), can_print)

    # Problem 7

    max_weight = 122.
    container_shape = Polygon([(3.5, 0.6), (0.5, 0.9), (3.7, 5.5), (1.7, 4.), (0., 6.5), (0.2, 8.6), (0.8, 9.8), (1.7, 8.9), (2, 9.1), (4.4, 9.3), (4.2, 6.7), (4.9, 7.5), (6.5, 8.4), (6.6, 7.9), (7.4, 8.2), (8.7, 5.5), (9.3, 4.8), (6.3, 0.2), (5., 3.5), (5, 0.7), (3.5, 0.6)])
    container = Container(max_weight, container_shape)
    items = [Item(Polygon([(0, 3), (0, 2.), (4., 0)]), 5., 6.),
             Item(Polygon([(0, 0), (1., 2.), (2.5, 2), (1, 1.2)]), 10., 7.),
             Item(Polygon([(0, 1), (1, 2.), (3., 0)]), 9., 4.),
             Item(Polygon([(0, 0.5), (1, 1.), (3, 1), (2., 0)]), 19., 14.),
             Item(Polygon([(0, 0.6), (2, 1), (2., 1.5), (1.2, 1.5)]), 19., 15.),
             Item(Polygon([(0, 0), (0, 2.), (0.5, 2), (0.5, 0.5), (2.5, 0.5), (2.5, 0)]), 7., 15.),
             Item(MultiPolygon([(((0.0, 0.0), (0.0, 1.8), (1.0, 2.7), (2.3, 0.0)),
                                 [((0.2, 0.2), (0.2, 1.4), (0.7, 2.1), (1.8, 0.5))])]), 12., 6.),
             Item(MultiPolygon([(((0.0, 0.0), (1.0, 1.8), (2.0, 2.5), (2.6, 0.7)),
                                 [((0.2, 0.2), (1.2, 1.4), (2.1, 1.7))])]), 7., 13.),
             Item(Ellipse((0, 0), 0.5, 0.2), 4., 9.),
             Item(Ellipse((0, 0), 0.2, 1.5), 21., 14.),
             Item(Ellipse((0, 0), 2.5, 3.5), 16., 30.),
             Item(Circle((0, 0), 0.4), 7., 12.),
             Item(Circle((0, 0), 0.3), 10., 3.),
             Item(Circle((0, 0), 1.), 1., 3.)]
    problem = Problem(container, items)
    problems.append(problem)

    solution = Solution(problem)
    solutions.append(solution)

    print_if_allowed(solution.add_item(0, (5.73, 3.02), 318.), can_print)
    print_if_allowed(solution.add_item(1, (6.3, 4.1), 40.), can_print)
    print_if_allowed(solution.add_item(2, (4.58, 2.5), 315.), can_print)
    print_if_allowed(solution.add_item(3, (1.3, 5.4), 320.), can_print)
    print_if_allowed(solution.add_item(4, (1.4, 1.7), 20.), can_print)
    print_if_allowed(solution.add_item(5, (2.9, 7.9), 180.), can_print)
    print_if_allowed(solution.add_item(6, (8.2, 4), 300.), can_print)
    print_if_allowed(solution.add_item(7, (2.5, 7.4), 340.), can_print)
    print_if_allowed(solution.add_item(8, (7.3, 4.), 320.), can_print)
    print_if_allowed(solution.add_item(9, (2.9, 3.9), 330.), can_print)
    print_if_allowed(solution.add_item(11, (7.8, 4.4), 0.), can_print)
    print_if_allowed(solution.add_item(13, (6.2, 6.8), 0.), can_print)

    # Problem 8

    max_weight = 100.
    container_shape = Polygon([(0., 0.), (0., 5.), (2.5, 3.4), (5., 5.), (5., 0), (2.5, 1.6)])
    container = Container(max_weight, container_shape)
    items = [Item(Polygon([(0., 0.), (0., 3.), (0.25, 3.), (0.25, 0.25), (2., 2.5), (3.75, 0.25), (3.75, 3.), (4., 3.), (4., 0.), (3.75, 0.), (2., 2.), (0.25, 0.)]), 100., 100.),
             Item(Polygon([(0., 0.), (1.6, 1.), (1.8, 1.9), (0.9, 1.6)]), 11., 12.),
             Item(Polygon([(0., 0.), (1.8, 2.5), (0., 1.8)]), 15., 5.),
             Item(Polygon([(0., 0.), (0.5, 0.), (1.2, 0.4), (0., 0.5)]), 4., 10.),
             Item(Polygon([(0., 0.), (2.5, 0.), (1.5, 0.2), (0., 0.5)]), 1., 2.),
             Item(Polygon([(0., 0.), (0.7, 0.25), (1.6, 1.5), (0.6, 0.7), (0., 0.45)]), 17., 8.),
             Item(Polygon([(0., 0.), (0.8, 0.5), (1.5, 1.2), (0., 0.5)]), 13., 11.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.2, 0.6), (0., 0.3)]), 15., 7.),
             Item(Ellipse((0., 0.), 0.6, 0.4), 15., 8.),
             Item(Ellipse((0., 0.), 2., 0.5), 15., 8.),
             Item(Ellipse((0., 0.), 0.5, 0.3), 24., 6.),
             Item(Ellipse((0., 0.), 0.4, 0.1), 4., 3.),
             Item(Circle((0., 0.), 0.6), 11., 2.),
             Item(Circle((0., 0.), 0.35), 12., 4.),
             Item(Circle((0., 0.), 0.2), 18., 8.)]
    problem = Problem(container, items)
    problems.append(problem)

    solution = Solution(problem)
    solutions.append(solution)

    print_if_allowed(solution.add_item(0, (2.5, 2.02), 0.), can_print)

    # Problem 9

    max_weight = 200.
    container_shape = Point(5, 5).buffer(5, 3)
    container = Container(max_weight, container_shape)
    items = [Item(MultiPolygon([(Point(5, 5).buffer(4.7, 2).exterior.coords,
                                 [((9., 5.), (5., 1.), (1., 5.), (5., 9.))])]), 120., 110.),
             Item(Polygon([(0., 0.), (0., 5.), (5., 5.), (5., 0.)]), 50., 80.),
             Item(Polygon([(1., 4.2), (1.5, 2.), (4., 0)]), 15., 14.),
             Item(Polygon([(0, 0), (1., 2.), (2.5, 2), (1, 1.2)]), 11., 11.),
             Item(Polygon([(0, 1), (1, 2.), (3., 0)]), 11., 4.),
             Item(Polygon([(0, 0.5), (1, 1.), (3, 1), (2., 0)]), 19., 14.),
             Item(Polygon([(0, 0.4), (1.8, .8), (1.5, 1.3), (1.2, 3.3)]), 17., 15.),
             Item(Polygon([(0, 0), (0, 2.), (0.9, 2), (0.9, 0.5), (1.5, 0.5), (1.5, 0)]), 70., 15.),
             Item(Ellipse((0, 0), 0.8, 1.2), 14., 13.),
             Item(Ellipse((0, 0), 1.2, 1.5), 12., 6.),
             Item(Ellipse((0, 0), 2.5, 1.7), 16., 10.),
             Item(Circle((0, 0), 0.7), 17., 11.),
             Item(Circle((0, 0), 0.8), 13., 10.),
             Item(Circle((0, 0), 1.), 4., 4.),
             Item(Circle((0, 0), 2.), 22., 8.)]
    problem = Problem(container, items)
    problems.append(problem)

    solution = Solution(problem)
    solutions.append(solution)

    print_if_allowed(solution.add_item(0, (5., 5.), 0.), can_print)
    print_if_allowed(solution.add_item(1, (5., 5.), 45.), can_print)

    # Problem 10

    max_weight = 150.
    container_shape = Polygon([(2., 5.), (3., 5), (3., 3.), (5., 3.), (5., 2.), (3., 2.), (3., 0.), (2., 0.), (2., 2.), (0., 2.), (0., 3.), (2., 3.)])
    container = Container(max_weight, container_shape)
    items = [Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95)]), 10., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95)]), 10., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95)]), 10., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95)]), 10., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95)]), 10., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95)]), 10., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95)]), 10., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95)]), 10., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95)]), 10., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95)]), 10., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95), (0., 0.95)]), 20., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95), (0., 0.95)]), 20., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95), (0., 0.95)]), 20., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95), (0., 0.95)]), 20., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95), (0., 0.95)]), 20., 10.),
             Item(Polygon([(0., 0.), (1.5, 0.), (1.5, 0.95), (0., 0.95)]), 20., 10.),
             Item(Polygon([(0., 0.), (0.8, 0.), (0.8, 0.45), (0., 0.45)]), 20., 30.),
             Item(Polygon([(0., 0.), (0.8, 0.), (0.8, 0.45), (0., 0.45)]), 20., 30.),
             Item(Polygon([(0., 0.), (0.8, 0.), (0.8, 0.1), (0., 0.1)]), 5., 25.),
             Item(Polygon([(0., 0.), (0.8, 0.), (0.8, 0.1), (0., 0.1)]), 5., 25.)]
    problem = Problem(container, items)
    problems.append(problem)

    solution = Solution(problem)
    solutions.append(solution)

    print_if_allowed(solution.add_item(0, (4.23, 2.48), 0.), can_print)
    print_if_allowed(solution.add_item(1, (4.23, 2.52), 180.), can_print)
    print_if_allowed(solution.add_item(2, (0.77, 2.48), 0.), can_print)
    print_if_allowed(solution.add_item(3, (0.77, 2.52), 180.), can_print)
    print_if_allowed(solution.add_item(4, (2.48, 0.76), 270.), can_print)
    print_if_allowed(solution.add_item(5, (2.52, 0.76), 90.), can_print)
    print_if_allowed(solution.add_item(6, (2.48, 4.24), 270.), can_print)
    print_if_allowed(solution.add_item(7, (2.52, 4.24), 90.), can_print)
    print_if_allowed(solution.add_item(8, (2.5, 2.48), 0.), can_print)
    print_if_allowed(solution.add_item(9, (2.5, 2.52), 180.), can_print)
    print_if_allowed(solution.add_item(16, (2.5, 3.25), 0.), can_print)
    print_if_allowed(solution.add_item(17, (2.5, 1.75), 0.), can_print)
    print_if_allowed(solution.add_item(18, (1.64, 2.5), 90.), can_print)
    print_if_allowed(solution.add_item(19, (3.36, 2.5), 90.), can_print)

    # show elapsed time
    elapsed_time = get_time_since(start_time)
    print_if_allowed("Manual elapsed time: {} ms".format(round(elapsed_time, 3)), can_print)

    return problems, [str(i + 1) for i in range(len(problems))], solutions