Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions sampo/scheduler/genetic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ def __init__(self,
# for optimization on one criteria set False
is_multiobjective: bool = False,
# for experiments with classic RCPSP formulation (initialize population with LFT)
only_lft_initialization: bool = False):
only_lft_initialization: bool = False,
fitness_raw_path: str | None = None,
fitness_stats_path: str | None = None):
super().__init__(scheduler_type=scheduler_type,
resource_optimizer=resource_optimizer,
work_estimator=work_estimator)
Expand All @@ -75,6 +77,8 @@ def __init__(self,
self._is_multiobjective = is_multiobjective
self._weights = weights
self._only_lft_initialization = only_lft_initialization
self.fitness_raw_path = fitness_raw_path
self.fitness_stats_path = fitness_stats_path

self._time_border = None
self._max_plateau_steps = None
Expand Down Expand Up @@ -248,7 +252,9 @@ def upgrade_pop(self,
self._optimize_resources,
deadline,
self._only_lft_initialization,
self._is_multiobjective)
self._is_multiobjective,
self.fitness_raw_path,
self.fitness_stats_path)
return new_pop

def schedule_with_cache(self,
Expand Down Expand Up @@ -303,7 +309,9 @@ def schedule_with_cache(self,
self._optimize_resources,
deadline,
self._only_lft_initialization,
self._is_multiobjective)
self._is_multiobjective,
self.fitness_raw_path,
self.fitness_stats_path)
schedules = [
(Schedule.from_scheduled_works(scheduled_works.values(), wg), schedule_start_time, timeline, order_nodes)
for scheduled_works, schedule_start_time, timeline, order_nodes in schedules]
Expand Down
24 changes: 17 additions & 7 deletions sampo/scheduler/genetic/schedule_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from sampo.scheduler.genetic.converter import convert_schedule_to_chromosome, ScheduleGenerationScheme
from sampo.scheduler.genetic.operators import init_toolbox, ChromosomeType, FitnessFunction, TimeFitness
from sampo.scheduler.genetic.utils import prepare_optimized_data_structures
from sampo.scheduler.genetic.utils import FitnessHistory
from sampo.scheduler.timeline.base import Timeline
from sampo.schemas.contractor import Contractor
from sampo.schemas.graph import GraphNode, WorkGraph
Expand Down Expand Up @@ -112,14 +113,17 @@ def build_schedules(wg: WorkGraph,
optimize_resources: bool = False,
deadline: Time | None = None,
only_lft_initialization: bool = False,
is_multiobjective: bool = False) \
is_multiobjective: bool = False,
fitness_raw_path: str | None = None,
fitness_stats_path: str | None = None) \
-> list[tuple[ScheduleWorkDict, Time, Timeline, list[GraphNode]]]:
return build_schedules_with_cache(wg, contractors, population_size, generation_number,
mutpb_order, mutpb_res, mutpb_zones, init_schedules,
rand, spec, weights, pop, landscape, fitness_object,
fitness_weights, work_estimator, sgs_type, assigned_parent_time,
timeline, time_border, max_plateau_steps, optimize_resources,
deadline, only_lft_initialization, is_multiobjective)[0]
deadline, only_lft_initialization, is_multiobjective,
fitness_raw_path, fitness_stats_path)[0]


def build_schedules_with_cache(wg: WorkGraph,
Expand All @@ -146,7 +150,9 @@ def build_schedules_with_cache(wg: WorkGraph,
optimize_resources: bool = False,
deadline: Time | None = None,
only_lft_initialization: bool = False,
is_multiobjective: bool = False) \
is_multiobjective: bool = False,
fitness_raw_path: str | None = None,
fitness_stats_path: str | None = None) \
-> tuple[list[tuple[ScheduleWorkDict, Time, Timeline, list[GraphNode]]], list[ChromosomeType]]:
"""
Genetic algorithm.
Expand Down Expand Up @@ -193,6 +199,7 @@ def build_schedules_with_cache(wg: WorkGraph,

evaluation_start = time.time()

fitness_history = FitnessHistory()
hof = tools.ParetoFront(similar=compare_individuals)

# map to each individual fitness function
Expand All @@ -203,7 +210,7 @@ def build_schedules_with_cache(wg: WorkGraph,
for ind, fit in zip(pop, fitness):
ind.fitness.values = fit

hof.update(pop)
hof.update(pop); fitness_history.update_history(pop, note="first generation")
best_fitness = hof[0].fitness.values

SAMPO.logger.info(f'First population evaluation took {evaluation_time * 1000} ms')
Expand Down Expand Up @@ -235,7 +242,7 @@ def build_schedules_with_cache(wg: WorkGraph,
# renewing population
pop += offspring
pop = toolbox.select(pop)
hof.update(pop)
hof.update(pop); fitness_history.update_history(pop, note="genetic update")

prev_best_fitness = best_fitness
best_fitness = hof[0].fitness.values
Expand Down Expand Up @@ -282,7 +289,7 @@ def build_schedules_with_cache(wg: WorkGraph,

evaluation_time += time.time() - evaluation_start

hof.update(pop)
hof.update(pop); fitness_history.update_history(pop, note="first deadline population")

if best_fitness[0] <= deadline:
# Optimizing resources
Expand Down Expand Up @@ -326,7 +333,7 @@ def build_schedules_with_cache(wg: WorkGraph,
# renewing population
pop += offspring
pop = toolbox.select(pop)
hof.update(pop)
hof.update(pop); fitness_history.update_history(pop, note="genetic deadline update")

prev_best_fitness = best_fitness
best_fitness = hof[0].fitness.values
Expand All @@ -338,6 +345,9 @@ def build_schedules_with_cache(wg: WorkGraph,
SAMPO.logger.info(f'Generations processing took {(time.time() - start) * 1000} ms')
SAMPO.logger.info(f'Full genetic processing took {(time.time() - global_start) * 1000} ms')
SAMPO.logger.info(f'Evaluation time: {evaluation_time * 1000}')
# save fitness history
fitness_history.write_fitness_raw(path=fitness_raw_path)
fitness_history.write_fitness_stats(path=fitness_stats_path)

best_chromosomes = [chromosome for chromosome in hof]

Expand Down
111 changes: 111 additions & 0 deletions sampo/scheduler/genetic/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,114 @@ def create_toolbox_using_cached_chromosomes(wg: WorkGraph,
sgs_type,
only_lft_initialization,
is_multiobjective)


class FitnessHistory:
"""
Class to track fitness and other stats for genetic algorithm
* fintess function is assumed to stay the same during evolution
* if you want to change the function, feel free to create another object
"""

def __init__(self):
# [generation1, generation2, ...]
self.fitness_history = []
# optional, comments about how this generation was created
self.notes = []

def update_history(self, population: list, note: str = ""):
fitness_values = [i.fitness.values for i in population]
self.fitness_history.append(fitness_values)
self.notes.append(note)

def get_agg_functions_for_fitness(self):
means, medians, stds = [], [], []
for fitness_values in self.fitness_history:
means.append(np.mean(fitness_values, axis=0))
medians.append(np.median(fitness_values, axis=0))
stds.append(np.std(fitness_values, axis=0))
return np.array(means), np.array(medians), np.array(stds)

def get_uniqueness_scores(self):
"""
Calculate uniqueness of fitness values in population:
how many genomes with the same fitness are in the population
higher score = more fitness values are unique
"""
uniqueness_scores = [
len(set(fitness_values)) / len(fitness_values)
for fitness_values in self.fitness_history
]

# round values for readability
uniqueness_scores = [round(score, 4) for score in uniqueness_scores]
return uniqueness_scores

def get_generation_shifts(self):
"""
Calculate how much the population has changed compared to previous generation
"""
# for keeping len(generation_shifts) == len(dataframe)
generation_shifts = [0]
n_generations = len(self.fitness_history)
for i in range(1, n_generations):
old_fitness = self.fitness_history[i-1]
new_fitness = self.fitness_history[i]

n_fresh_fitness = 0
for f in new_fitness:
if f not in old_fitness:
n_fresh_fitness += 1
ratio = n_fresh_fitness / len(new_fitness)
generation_shifts.append(ratio)

# round values for readability
generation_shifts = [round(score, 4) for score in generation_shifts]
return generation_shifts

def create_fitness_raw_df(self) -> pd.DataFrame:
df = pd.DataFrame(self.fitness_history)
df["iteration"] = list(range(len(self.fitness_history)))
df["notes"] = self.notes
return df

def create_fitness_info_df(self) -> pd.DataFrame:
iteration = list(range(len(self.fitness_history)))
means, medians, stds = self.get_agg_functions_for_fitness()
uniqueness_scores = self.get_uniqueness_scores()
generation_shifts = self.get_generation_shifts()

df = pd.DataFrame({
"iteration": iteration,
"notes": self.notes,
"uniqueness_scores": uniqueness_scores,
"generation_shifts": generation_shifts,
})

n_fitness_objectives = means.shape[1]
for i in range(n_fitness_objectives):
df[f"mean_{i}"] = means[:, i]
df[f"median_{i}"] = medians[:, i]
df[f"std_{i}"] = stds[:, i]
return df

def write_fitness_raw(self, path=None):
"""Save current fitness raw values to CSV file"""
if not path:
return
try:
df = self.create_fitness_raw_df()
df.to_csv(path, index=False)
except Exception as e:
print(f"Error occured when trying to write fitness history (raw): {e}")

def write_fitness_stats(self, path=None):
"""Save current fitness stats to CSV file"""
if not path:
return
try:
df = self.create_fitness_info_df()
df.to_csv(path, index=False)
except Exception as e:
print(f"Error occured when trying to write fitness history (stats): {e}")

Loading