def selection(self, population: Population) -> List[Tuple[Chromosome]]:
        """
        select parents for crossover

        implements basic selection method: 1) create a pool of chromosomes, 2) select the amount needed for mating.
        """
        parents = []
        pairings = math.ceil(population.get_size() / self.get_offsprings_amount())
        self.set_selection_pool([ch for ch in population])
        for _ in range(pairings):
            parents.append(tuple([self.select() for _ in range(self.get_parents_amount())]))
        return parents
    def crossover(self, parents: List[Tuple[Chromosome]], pop_size: int) -> Population:
        """
        get list of parents to mate, return the new population

        NOTE: when population (size - elitism amount) doesn't divide in (offspring amount) then the remainder
         offsprings won't enter the new population.
        """
        new_population = Population()
        for chromosomes in parents:
            # do crossover with probability self.crossover_rate
            if random.random() < self.crossover_rate:
                offsprings = self.pair_chromosomes(chromosomes)
            else:
                offsprings = chromosomes  # [ch.__copy__() for ch in chromosomes]  # todo: maybe redundant
            for ch in offsprings:
                new_population.add_chromosome(ch)
                # if there are too many offsprings then stop adding them to population
                if new_population.get_size() == pop_size:
                    return new_population