def get_max_nudge(self, individuals, number_of_generations):
        """
        Find the maximum synergistic nudge.

        Parameters:
        ----------
        number_of_generations: an integer
        individuals: list of IndividualNudge objects
 
        Returns:
        -------
        A numpy array which represents the maximum individual nudge
        for the start distribution.

        """
        population = individuals
        scores = []
        for timestep in range(number_of_generations):
            population = self.evolve(population, timestep)
            scores.append(population[0].score)
            if TEST:
                print("time step {} best score {} worst score {}".format(
                    timestep, population[0].score, population[-1].score))

        return ea.sort_individuals(population)[0]
def find_synergistic_nudge_with_max_impact(input_dist, cond_output, nudge_size,
                                           evolutionary_parameters):
    """
    find the synergistic nudge with the maximum impact
    
    Parameters:
    ----------
    input_dist: nd-array, representing the joint input distribution
    cond_output: nd-array, representing the output distribution conditioned on the input
    nudge_size: positive float
    evolutionary_parameters: dict with the following keys
        number_of_generations: integer 
        population_size: integer
        number_of_children: 
            integer, if generational larger than or equal to population size  
        generational: Boolean, whether to replace the old generation 
        parent_selection_mode: "rank_exponential" or None (for random selection)
        start_mutation_size: positive float, the mutation size at the start
        change_mutation_size: positive float, 
            the upper bound on the range the change is selected from uniformly
        mutations_per_update_step: integer

    Returns: A SynergisticNudge object
    
    """
    #create initial population
    synergistic_nudges = []
    for _ in range(evolutionary_parameters["population_size"]):
        new_synergistic_nudge = SynergisticNudge.create_nudge(
            input_dist,
            cond_output,
            nudge_size,
            evolutionary_parameters["mutations_per_update_step"],
            evolutionary_parameters["start_mutation_size"],
            evolutionary_parameters["change_mutation_size"],
            timestamp=0)
        synergistic_nudges.append(new_synergistic_nudge)
    for synergistic_nudge in synergistic_nudges:
        synergistic_nudge.evaluate()

    initial_impact = ea.sort_individuals(synergistic_nudges)[0].score

    #evolve the population
    find_max_synergistic_nudge = FindMaximumSynergisticNudge(
        evolutionary_parameters["generational"],
        evolutionary_parameters["number_of_children"],
        evolutionary_parameters["parent_selection_mode"])
    max_synergistic_nudge = find_max_synergistic_nudge.get_max_nudge(
        synergistic_nudges, evolutionary_parameters["number_of_generations"])
    if TEST:
        print("synergistic nudge: intial impact {}, max impact {}".format(
            initial_impact, max_synergistic_nudge.score))
    return max_synergistic_nudge
def find_maximum_local_nudge(input_dist,
                             cond_output,
                             nudge_size,
                             evolution_params,
                             verbose=False):
    """
    find the synergistic nudge with the maximum impact
    
    Parameters:
    ----------
    input_dist: nd-array, representing the joint input distribution
    cond_output: nd-array, representing the output distribution conditioned on the input
    nudge_size: positive float
    evolutionary_parameters: dict with the following keys
        number_of_generations: integer
        population_size: integer
        number_of_children:
            integer, if generational larger than or equal to population size
        generational: Boolean, whether to replace the old generation 
        parent_selection_mode: "rank_exponential" or None (for random selection)
        start_mutation_size: positive float, the mutation size at the start
        change_mutation_size: positive float, 
            the maximum percentage the mutation size is changed
        mutation_size_weights: positive float
        change_mutation_size_weights: positive float
            The maximum percentage the mutation_size for the weights is
            changed.

    Returns: A LocalNudge object
    
    """
    #create initial population
    nudged_vars_to_states = {
        nudged_var: number_of_states
        for nudged_var, number_of_states in enumerate(input_dist.shape)
    }
    local_nudges = []
    for _ in range(evolution_params["population_size"]):
        new_local_nudge = LocalNudge.create_local_nudge(
            nudged_vars_to_states,
            nudge_size,
            evolution_params["mutation_size_weights"],
            evolution_params["start_mutation_size"],
            evolution_params["change_mutation_size"],
            evolution_params["change_mutation_size_weights"],
            timestamp=0)
        local_nudges.append(new_local_nudge)

    for local_nudge in local_nudges:
        local_nudge.evaluate(input_dist, cond_output)

    initial_impact = ea.sort_individuals(local_nudges)[0].score

    #evolve the population
    find_max_local_nudge = FindMaximumLocalNudge(
        input_dist, cond_output, nudge_size, evolution_params["generational"],
        evolution_params["number_of_children"],
        evolution_params["parent_selection_mode"])
    for timestep in range(evolution_params["number_of_generations"]):
        local_nudges = find_max_local_nudge.evolve(local_nudges, timestep)
        best_local_nudge = ea.sort_individuals(local_nudges)[0]
        if verbose:
            print("best score {}, worst score {}".format(
                best_local_nudge.score, local_nudges[-1].score))

    if TEST:
        print("local nudge: intial impact {}, max impact {}".format(
            initial_impact, best_local_nudge.score))

    return ea.sort_individuals(local_nudges)[0]
    #individual nudge optimization
    number_of_generations = 50
    population_size = 10
    number_of_children = 10
    generational = False
    mutation_size = nudge_size / 4
    parent_selection_mode = "rank_exponential"
    #parent_selection_mode = None

    individuals = create_individual_nudges(population_size, number_of_states,
                                           nudge_size, "random")
    for individual in individuals:
        individual.evaluate(input_dist, cond_output)
    print("initial impact {}".format(
        ea.sort_individuals(individuals)[0].score))
    find_max_nudge = FindMaximumIndividualNudge(input_dist, cond_output,
                                                nudge_size, generational,
                                                number_of_children,
                                                parent_selection_mode)
    # max_individual = find_max_nudge.get_max_nudge(
    #     individuals, number_of_generations, mutation_size
    # )
    # print("the found max impact for an individual nudge {}".format(
    #     max_individual.score
    # ))

    print("the found max impact for a local nudge {}".format(
        max_local_nudge.score))

    max_impact = maximum_nudges.find_max_impact_individual_nudge_exactly(
 def get_best_individual(self):
     return ea.sort_individuals(self.individuals)[0]
def get_cond_output_with_max_distance(input_shape,
                                      number_of_output_states,
                                      goal_distance,
                                      evolutionary_parameters,
                                      input_dists,
                                      number_of_input_distributions=None):
    """ 
    Create a conditional output distribution which gives as different 
    as possible marginal for the different input distributions

    Parameters:
    ----------
    input_shape: a list, the number of states of the inut variables
    number_of_states_output: integer
    input_distributions: a list of nd-arrays or None
        Every array representing a probability distribution
    number_of_input_distributions: integer
        The number of input distributions to generate using a Dirichlet
        distribution with all parameters equal to 1.
    evolutionary_parameters: dict with the keys
        number_of_generations: integer 
        population_size: integer
        number_of_children: integer, 
            if generational larger than or equal to population size  
        generational: Boolean, whether to replace the old generation 
        mutation_size: positive float
        change_mutation_size: positive float
        parent_selection_mode: "rank_exponential" or None (for random selection)
       
    """
    if input_dists is None:
        number_of_input_states = reduce(lambda x, y: x * y, input_shape)
        input_dists = [
            np.random.dirichlet([1] * number_of_input_states)
            for _ in range(number_of_input_distributions)
        ]
        input_dists = [np.reshape(dist, input_shape) for dist in input_dists]

    #create initial population
    conditional_outputs = create_conditonal_distributions(
        evolutionary_parameters["population_size"], number_of_output_states,
        len(input_shape))
    mutation_size = evolutionary_parameters["mutation_size"]
    change_mutation_size = evolutionary_parameters["change_mutation_size"]
    conditional_outputs = [
        ConditionalOutput(cond_output, mutation_size, change_mutation_size)
        for cond_output in conditional_outputs
    ]
    #for dist in conditional_outputs:
    #    found_sum = np.sum(dist.cond_output)
    #    expected_sum = reduce(lambda x,y: x*y, dist.cond_output.shape[:-1])
    #    if abs(found_sum-expected_sum) > 10**(-7):
    #        raise ValueError()

    for conditional_output in conditional_outputs:
        conditional_output.evaluate(goal_distance, input_dists)

    initial_distance = ea.sort_individuals(conditional_outputs)[-1].score

    #evolve the population
    find_conditional_output = FindConditionalOutput(
        conditional_outputs, goal_distance,
        evolutionary_parameters["number_of_generations"],
        evolutionary_parameters["number_of_children"],
        evolutionary_parameters["parent_selection_mode"])
    find_conditional_output.evolve(evolutionary_parameters["generational"],
                                   input_dists)

    final_distance = find_conditional_output.get_best_individual()
    print("initial distance {}, distance after evolution {}".format(
        initial_distance, final_distance.score))
    return find_conditional_output.individuals[0]