def best_elements_order_hybrid2(relations,
                                elements=None,
                                filter_order=None,
                                nb_tested_solution=10000):
    present_elements, present_element_groups, properties, property_groups, element_2_property_2_relation, property_2_element_2_relation = relations_2_model(
        relations)
    if not elements: elements = present_elements
    elements = set(elements)
    for e in elements:
        if not e in element_2_property_2_relation:
            element_2_property_2_relation[e] = {
            }  # Element with no relation are not present
    for p in properties:
        if not p in property_2_element_2_relation:
            property_2_element_2_relation[p] = {
            }  # Property with no relation are not present

    def get_number_of_relation(e0):
        return len([
            prop for prop in element_2_property_2_relation[e0]
            if len(property_2_element_2_relation[prop]) > 1
        ])

    candidate_first_elements, best_score = bests(elements,
                                                 get_number_of_relation)

    properties_with_more_than_one_element = [
        property for property in properties
        if len(property_2_element_2_relation[property]) > 1
    ]

    insertion_score_cache = {}

    def insertion_score(element, position, order, order_set):
        if position is "beginning": neighbor = order[0]
        else: neighbor = order[-1]
        score = 0

        for x in element_2_property_2_relation[element]:
            score += 2 * (x in element_2_property_2_relation[neighbor])

        for y in properties_with_more_than_one_element:
            if ((not y in element_2_property_2_relation[element]) and
                    order_set.isdisjoint(property_2_element_2_relation[y])):
                score += 1

        insertion_score_cache[element, position, order] = score
        return score

    def fly():
        if len(candidate_first_elements) == 1:
            first_element = candidate_first_elements[0]
        else:
            first_element = random.choice(candidate_first_elements)
        order = (first_element, )

        remnants = list(elements)
        remnants.remove(first_element)

        order_set = set(order)

        while remnants:
            best_score = -999
            best_insertions = []
            for e in remnants:
                score = insertion_score(e, "beginning", order, order_set)
                if score == best_score:
                    best_insertions.append((e, "beginning"))
                elif score > best_score:
                    best_insertions = [(e, "beginning")]

                score = insertion_score(e, "end", order, order_set)
                if score == best_score: best_insertions.append((e, "end"))
                elif score > best_score: best_insertions = [(e, "end")]

            if len(best_insertions) == 1:
                best_element, best_position = best_insertions[0]
            else:
                best_element, best_position = random.choice(best_insertions)

            remnants.remove(best_element)
            order_set.add(best_element)
            if best_position == "beginning": order = (best_element, ) + order
            else: order = order + (best_element, )

        return list(order)

    def cost(order):
        nb_hole = 0
        nb_prop_with_hole = 0
        total_hole_length = 0
        #sum_of_hatch_pos  = 0
        for property in properties:
            start = None
            end = None
            in_hole = False
            for i, element in enumerate(order):
                if element in property_2_element_2_relation[property]:
                    if start is None: start = i
                    end = i
                    in_hole = False
                    #if property_2_element_2_relation[property][element].hatch: sum_of_hatch_pos += i
                else:
                    if (not start is None) and (not in_hole):
                        in_hole = True
                        nb_hole += 1

            if not end is None:
                # After end, it is not a hole!
                if end != i: nb_hole -= 1

                length = end - start + 1

                if length > len(property_2_element_2_relation[property]):
                    total_hole_length += length - len(
                        property_2_element_2_relation[property])
                    nb_prop_with_hole += 1

        #groups_preserved = 0 # Favor orders maintaining groups
        #for i in range(len(order) - 1):
        #  if order[i].group == order[i + 1].group: groups_preserved += 1

        #order_keys_preserved = 0 # Favor orders maintaining alphabetical order
        #for i in range(len(order) - 1):
        #  if order[i].order_key <= order[i + 1].order_key: order_keys_preserved += 1

        #return (-nb_prop_with_hole, -nb_hole * 3 + -total_hole_length, sum_of_hatch_pos, groups_preserved, order_keys_preserved)

        #return (nb_prop_with_hole, nb_hole * 3 + total_hole_length, nb_hole)
        #return (total_hole_length, nb_hole)
        #return (nb_hole * 3 + total_hole_length, nb_hole, total_hole_length)
        #return (nb_hole, total_hole_length)
        #return (nb_hole, nb_prop_with_hole, total_hole_length)
        return total_hole_length, nb_hole, nb_prop_with_hole
        #return (nb_prop_with_hole, nb_hole, total_hole_length)

    import metaheuristic_optimizer.artificial_feeding_birds as optim_module
    algo = optim_module.OrderingAlgorithm(list(elements), cost, nb=20)
    #algo.fly = fly
    algo.run(nb_tested_solution=nb_tested_solution)

    lowest_cost = algo.get_lowest_cost()
    if isinstance(lowest_cost, tuple): lowest_cost = list(lowest_cost)

    print("Tested %s element orders..." % algo.nb_cost_computed,
          file=sys.stderr)
    print("Lowest cost: %s" % lowest_cost, file=sys.stderr)

    order = algo.get_best_position()

    # Reorder alphabetically consecutive sublist or identical elements (due to optimization, only one order is tested)
    order = list(order)
    i = 0
    while True:
        if i >= len(order): break
        nb_identical_element = 1
        while True:
            if i + nb_identical_element >= len(order): break
            if order[i].group != order[i + nb_identical_element].group: break
            if frozenset(element_2_property_2_relation[order[i]]) != frozenset(
                    element_2_property_2_relation[order[
                        i + nb_identical_element]]):
                break
            nb_identical_element += 1

        if nb_identical_element > 1:
            order[i:i + nb_identical_element] = sorted(
                order[i:i + nb_identical_element], key=lambda e: e.order_key)

        i += nb_identical_element

    return order
def best_elements_order_optim(relations,
                              elements=None,
                              filter_order=None,
                              nb_tested_solution=10000,
                              optim_module=None,
                              bench=False):
    if optim_module is None:
        import metaheuristic_optimizer.artificial_feeding_birds as optim_module

    present_elements, present_element_groups, properties, property_groups, element_2_property_2_relation, property_2_element_2_relation = relations_2_model(
        relations)
    if not elements: elements = present_elements
    elements = set(elements)
    for e in elements:
        if not e in element_2_property_2_relation:
            element_2_property_2_relation[e] = {
            }  # Element with no relation are not present
    for p in properties:
        if not p in property_2_element_2_relation:
            property_2_element_2_relation[p] = {
            }  # Property with no relation are not present

    def score_order(*order):
        nb_hole = 0
        nb_prop_with_hole = 0
        total_hole_length = 0
        hatch_diff = 0
        for property in properties:
            if isinstance(property.weight, str):
                prop_weight = 1
            else:
                prop_weight = property.weight or 1

            start = None
            end = None
            in_hole = False
            in_hatch = None
            for i, element in enumerate(order):
                if element in property_2_element_2_relation[property]:
                    if start is None: start = i
                    end = i
                    in_hole = False
                    relation = property_2_element_2_relation[property][element]
                    new_in_hatch = relation.hatch, relation.color
                    if new_in_hatch != in_hatch:
                        hatch_diff += 1  # property.weight or 1
                        in_hatch = new_in_hatch
                else:
                    if (not start is None) and (not in_hole):
                        in_hole = True
                        nb_hole += prop_weight * element.weight
                        in_hatch = None

            if not end is None:
                if end != i:
                    nb_hole -= prop_weight  # After end, it is not a hole!

                length = end - start + 1

                if length > len(property_2_element_2_relation[property]):
                    total_hole_length += (length - len(
                        property_2_element_2_relation[property])) * prop_weight
                    nb_prop_with_hole += prop_weight

        # Favor orders maintaining groups
        #groups_preserved = 0
        #for i in range(len(order) - 1):
        #  if order[i].group == order[i + 1].group: groups_preserved += 1

        # Favor orders maintaining alphabetical order
        #order_keys_preserved = 0
        #for i in range(len(order) - 1):
        #  if order[i].order_key <= order[i + 1].order_key: order_keys_preserved += 1

        #return (nb_prop_with_hole, nb_hole * 3 + -total_hole_length, -sum_of_hatch_pos, -groups_preserved, -order_keys_preserved)
        #return (nb_prop_with_hole * 5 + nb_hole * 3 + total_hole_length)
        #return nb_hole, nb_prop_with_hole, total_hole_length
        #return nb_prop_with_hole, total_hole_length, nb_hole
        #return nb_prop_with_hole + nb_hole + total_hole_length
        #return nb_prop_with_hole, nb_hole, total_hole_length, hatch_diff
        return nb_hole, total_hole_length, hatch_diff
        #return nb_hole

    def cost(ranks):
        pairs = sorted(zip(ranks, elements), key=lambda a: a[0])
        order = [element for (rank, element) in pairs]
        return score_order(*order)

    #import optim.artificial_bee_colony as optim_module
    #import optim.artificial_feeding_birds as optim_module
    #algo  = optim_module.NumericAlgorithm(cost, nb_dimension = len(elements))

    #algo.run(nb_tested_solution = nb_tested_solution)
    #ranks = algo.get_best_position()
    #pairs = sorted(zip(ranks, elements), key = lambda a: a[0])
    #order = [element for (rank, element) in pairs]

    #import optim.ant_colony_optimization as optim_module
    #algo  = optim_module.OrderingAlgorithm(list(elements), lambda a: score_order(*a),
    #                                       nb_ant = 8,
    #                                       evaporation_rate =  0.01,
    #                                       pheromon_default =  0.1,
    #                                       pheromon_min     =  0.1,
    #                                       pheromon_max     = 10.0,
    #)

    #import optim.genetic_algorithm as optim_module

    algo = optim_module.OrderingAlgorithm(list(elements),
                                          lambda a: score_order(*a))

    #x = algo.multiple_run(250, nb_tested_solution = 10000)
    #print(x, file = sys.stderr)

    algo.run(nb_tested_solution=nb_tested_solution)

    # ACO-pants
    # import pants
    # max_shared = max(
    #   len(set(element_2_property_2_relation[e1]) & set(element_2_property_2_relation[e2]))
    #   for e1 in elements
    #   for e2 in elements
    #   if not(e1 is e2)
    # )
    # def lfunc(e1, e2):
    #   return (max_shared - len(set(element_2_property_2_relation[e1]) & set(element_2_property_2_relation[e2]))) * 2.0
    # def cfunc(indexes):
    #   return score_order(*[world.data(i) for i in indexes])
    # world    = pants.World(list(elements), lfunc, cfunc)
    # solver   = pants.Solver(limit = 2000, ant_count = 5)
    # solution = solver.solve(world)
    # print(list(elements))
    # print(solution.visited)
    # print(cfunc(solution.visited))
    # return solution.distance

    if bench: return algo.get_lowest_cost()

    order = algo.get_best_position()

    # Optimize alphabetical order
    def get_hatches(property_2_relation):
        return [(property_2_relation[p].hatch, property_2_relation[p].color)
                for p in properties if p in property_2_relation]

    i = 0
    while True:
        if i >= len(order): break
        current_memberships = frozenset(
            element_2_property_2_relation[order[i]])
        current_hatches = get_hatches(element_2_property_2_relation[order[i]])
        nb_identical_element = 1
        while True:
            if i + nb_identical_element >= len(order): break
            new_element = order[i + nb_identical_element]
            if order[i].group != new_element.group: break
            new_memberships = frozenset(
                element_2_property_2_relation[new_element])
            if current_memberships != new_memberships: break
            new_hatches = get_hatches(
                element_2_property_2_relation[new_element])
            if current_hatches != new_hatches:
                break
            nb_identical_element += 1

        if nb_identical_element > 1:
            order[i:i + nb_identical_element] = sorted(
                order[i:i + nb_identical_element], key=lambda e: e.order_key)

        i += nb_identical_element

    return order