From 5f44d587cbc1b3dc3f271774d6ec844295448ce0 Mon Sep 17 00:00:00 2001 From: simon Date: Wed, 19 Oct 2022 14:36:43 +0200 Subject: [PATCH] Added some type hints, added requirements.txt, improved gitignore --- .gitignore | 4 ++- partition.py | 93 +++++++++++++++++++++++++++++------------------- requirements.txt | 3 ++ 3 files changed, 63 insertions(+), 37 deletions(-) create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index d56c6bb..cfe77a5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ continuous rules tsp multi -symbreg \ No newline at end of file +symbreg +.idea +venv diff --git a/partition.py b/partition.py index 772ae9d..53ad004 100644 --- a/partition.py +++ b/partition.py @@ -1,71 +1,86 @@ import random -import numpy as np +from typing import TypeVar, List, Callable, Tuple import functools - import utils -K = 10 #number of piles -POP_SIZE = 100 # population size -MAX_GEN = 500 # maximum number of generations -CX_PROB = 0.8 # crossover probability -MUT_PROB = 0.2 # mutation probability -MUT_FLIP_PROB = 0.1 # probability of chaninging value during mutation -REPEATS = 10 # number of runs of algorithm (should be at least 10) -OUT_DIR = 'partition' # output directory for logs -EXP_ID = 'default' # the ID of this experiment (used to create log names) +K = 10 # number of piles +POP_SIZE = 100 # population size +MAX_GEN = 500 # maximum number of generations +CX_PROB = 0.8 # crossover probability +MUT_PROB = 0.2 # mutation probability +MUT_FLIP_PROB = 0.1 # probability of chaninging value during mutation +REPEATS = 10 # number of runs of algorithm (should be at least 10) +OUT_DIR = 'partition' # output directory for logs +EXP_ID = 'default' # the ID of this experiment (used to create log names) + +Individual = TypeVar("Individual") +Population = List[Individual] +Operator = Callable[[Population], Population] +FitnessFunction = Callable[[Individual], utils.FitObjPair] + # reads the input set of values of objects -def read_weights(filename): +def read_weights(filename: str) -> List[int]: with open(filename) as f: return list(map(int, f.readlines())) + # computes the bin weights # - bins are the indices of bins into which the object belongs def bin_weights(weights, bins): - bw = [0]*K + bw = [0] * K for w, b in zip(weights, bins): bw[b] += w return bw + # the fitness function -def fitness(ind, weights): +def fitness(ind: Individual, weights: List[int]) -> utils.FitObjPair: bw = bin_weights(weights, ind) - return utils.FitObjPair(fitness=1/(max(bw) - min(bw) + 1), + return utils.FitObjPair(fitness=1 / (max(bw) - min(bw) + 1), objective=max(bw) - min(bw)) + # creates the individual -def create_ind(ind_len): +def create_ind(ind_len: int) -> Individual: return [random.randrange(0, K) for _ in range(ind_len)] + # creates the population using the create individual function -def create_pop(pop_size, create_individual): +def create_pop(pop_size: int, create_individual: Callable[[], Population]) -> Population: return [create_individual() for _ in range(pop_size)] + # the roulette wheel selection -def roulette_wheel_selection(pop, fits, k): +def roulette_wheel_selection(pop: Population, fits: List[float], k) -> Population: return random.choices(pop, fits, k=k) + # implements the one-point crossover of two individuals -def one_pt_cross(p1, p2): +def one_pt_cross(p1: Individual, p2: Individual) -> (Individual, Individual): point = random.randrange(1, len(p1)) o1 = p1[:point] + p2[point:] o2 = p2[:point] + p1[point:] return o1, o2 + # implements the "bit-flip" mutation of one individual -def flip_mutate(p, prob, upper): +def flip_mutate(p: Individual, prob: float, upper: int) -> Individual: return [random.randrange(0, upper) if random.random() < prob else i for i in p] -# applies a list of genetic operators (functions with 1 argument - population) + +# applies a list of genetic operators (functions with 1 argument - population) # to the population -def mate(pop, operators): +def mate(pop: Population, operators: List[Operator]): for o in operators: pop = o(pop) return pop + # applies the cross function (implementing the crossover of two individuals) # to the whole population (with probability cx_prob) -def crossover(pop, cross, cx_prob): +def crossover(pop: Population, cross: Callable[[Individual, Individual], Tuple[Individual, Individual]], + cx_prob: float) -> Population: off = [] for p1, p2 in zip(pop[0::2], pop[1::2]): if random.random() < cx_prob: @@ -76,11 +91,13 @@ def crossover(pop, cross, cx_prob): off.append(o2) return off + # applies the mutate function (implementing the mutation of a single individual) # to the whole population with probability mut_prob) -def mutation(pop, mutate, mut_prob): +def mutation(pop: Population, mutate: Callable[[Individual], Individual], mut_prob: float) -> Population: return [mutate(p) if random.random() < mut_prob else p[:] for p in pop] + # implements the evolutionary algorithm # arguments: # pop_size - the initial population @@ -95,7 +112,9 @@ def mutation(pop, mutate, mut_prob): # map_fn - function to use to map fitness evaluation over the whole # population (default `map`) # log - a utils.Log structure to log the evolution run -def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn=map, log=None): +def evolutionary_algorithm(pop: Population, max_gen: int, fitness: FitnessFunction, + operators: List[Operator], mate_sel: Callable[[Population, List[float], int], Population], *, + map_fn=map, log=None) -> Population: evals = 0 for G in range(max_gen): fits_objs = list(map_fn(fitness, pop)) @@ -111,20 +130,22 @@ def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn return pop + if __name__ == '__main__': # read the weights from input weights = read_weights('inputs/partition-easy.txt') # use `functool.partial` to create fix some arguments of the functions # and create functions with required signatures - cr_ind = functools.partial(create_ind, ind_len=len(weights)) - fit = functools.partial(fitness, weights=weights) - xover = functools.partial(crossover, cross=one_pt_cross, cx_prob=CX_PROB) - mut = functools.partial(mutation, mut_prob=MUT_PROB, - mutate=functools.partial(flip_mutate, prob=MUT_FLIP_PROB, upper=K)) + cr_ind: Callable[[], Individual] = functools.partial(create_ind, ind_len=len(weights)) + fit: FitnessFunction = functools.partial(fitness, weights=weights) + xover: Operator = functools.partial(crossover, cross=one_pt_cross, cx_prob=CX_PROB) + mut: Operator = functools.partial(mutation, mut_prob=MUT_PROB, + mutate=functools.partial(flip_mutate, prob=MUT_FLIP_PROB, upper=K)) # we can use multiprocessing to evaluate fitness in parallel import multiprocessing + pool = multiprocessing.Pool() import matplotlib.pyplot as plt @@ -134,12 +155,12 @@ def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn best_inds = [] for run in range(REPEATS): # initialize the log structure - log = utils.Log(OUT_DIR, EXP_ID, run, - write_immediately=True, print_frequency=5) + log = utils.Log(OUT_DIR, EXP_ID, run, write_immediately=True, print_frequency=5) # create population pop = create_pop(POP_SIZE, cr_ind) # run evolution - notice we use the pool.map as the map_fn - pop = evolutionary_algorithm(pop, MAX_GEN, fit, [xover, mut], roulette_wheel_selection, map_fn=pool.map, log=log) + pop = evolutionary_algorithm(pop, MAX_GEN, fit, [xover, mut], roulette_wheel_selection, map_fn=pool.map, + log=log) # remember the best individual from last generation, save it to file bi = max(pop, key=fit) best_inds.append(bi) @@ -147,7 +168,7 @@ def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn with open(f'{OUT_DIR}/{EXP_ID}_{run}.best', 'w') as f: for w, b in zip(weights, bi): f.write(f'{w} {b}\n') - + # if we used write_immediately = False, we would need to save the # files now # log.write_files() @@ -162,7 +183,7 @@ def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn # read the summary log and plot the experiment evals, lower, mean, upper = utils.get_plot_data(OUT_DIR, EXP_ID) plt.figure(figsize=(12, 8)) - utils.plot_experiment(evals, lower, mean, upper, legend_name = 'Default settings') + utils.plot_experiment(evals, lower, mean, upper, legend_name='Default settings') plt.legend() plt.show() @@ -173,4 +194,4 @@ def evolutionary_algorithm(pop, max_gen, fitness, operators, mate_sel, *, map_fn # rename_dict={'default': 'Default setting'}) # the rename_dict can be used to make reasonable entries in the legend - # experiments that are not in the dict use their id (in this case, the - # legend entries would be 'Default settings' and 'tuned') \ No newline at end of file + # legend entries would be 'Default settings' and 'tuned') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4021870 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy +pandas +matplotlib