From 01f08df50bf5b79321c01a103c51d757536d8977 Mon Sep 17 00:00:00 2001 From: maximdu Date: Fri, 5 Dec 2025 14:38:27 +0300 Subject: [PATCH 01/15] Create fitness_history.py --- sampo/scheduler/utils/fitness_history.py | 111 +++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 sampo/scheduler/utils/fitness_history.py diff --git a/sampo/scheduler/utils/fitness_history.py b/sampo/scheduler/utils/fitness_history.py new file mode 100644 index 00000000..53137306 --- /dev/null +++ b/sampo/scheduler/utils/fitness_history.py @@ -0,0 +1,111 @@ +import numpy as np # for calculating aggregate functions +import pandas as pd # for saving data to csv + +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}") From 302b2009a4fbebd7771cac54169fb163a8a880e3 Mon Sep 17 00:00:00 2001 From: maximdu Date: Fri, 5 Dec 2025 14:39:56 +0300 Subject: [PATCH 02/15] Added path parameters for fitness history --- sampo/scheduler/genetic/base.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/sampo/scheduler/genetic/base.py b/sampo/scheduler/genetic/base.py index 4712f733..dbd43a40 100644 --- a/sampo/scheduler/genetic/base.py +++ b/sampo/scheduler/genetic/base.py @@ -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) @@ -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 @@ -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, @@ -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] From 0e6f94429bb52581f6647ede01a505e6bed825b7 Mon Sep 17 00:00:00 2001 From: maximdu Date: Fri, 5 Dec 2025 14:48:14 +0300 Subject: [PATCH 03/15] Added saving history with FitnessHistory --- sampo/scheduler/genetic/schedule_builder.py | 24 +++++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/sampo/scheduler/genetic/schedule_builder.py b/sampo/scheduler/genetic/schedule_builder.py index e1eb839c..99462aef 100644 --- a/sampo/scheduler/genetic/schedule_builder.py +++ b/sampo/scheduler/genetic/schedule_builder.py @@ -17,6 +17,7 @@ from sampo.schemas.schedule_spec import ScheduleSpec from sampo.schemas.time import Time from sampo.schemas.time_estimator import WorkTimeEstimator, DefaultWorkEstimator +from sampo.scheduler.utils.fitness_history import FitnessHistory def create_toolbox(wg: WorkGraph, @@ -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, @@ -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. @@ -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 @@ -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') @@ -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 @@ -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 @@ -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 @@ -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] From a0b2125f6d0177c0e802a28099022d1e8cbae060 Mon Sep 17 00:00:00 2001 From: maximdu Date: Mon, 8 Dec 2025 17:17:28 +0300 Subject: [PATCH 04/15] Update fitness_history.py --- sampo/scheduler/utils/fitness_history.py | 178 ++++++++++------------- 1 file changed, 77 insertions(+), 101 deletions(-) diff --git a/sampo/scheduler/utils/fitness_history.py b/sampo/scheduler/utils/fitness_history.py index 53137306..55819433 100644 --- a/sampo/scheduler/utils/fitness_history.py +++ b/sampo/scheduler/utils/fitness_history.py @@ -1,111 +1,87 @@ -import numpy as np # for calculating aggregate functions -import pandas as pd # for saving data to csv +import json +from typing import Any, Iterable +from itertools import pairwise + 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 - """ + """Class to track fitness values during evolution""" 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 - ] + self.history: list[dict[str, Any]] = [] + + def update_history(self, population: Iterable = [], pareto_front: Iterable = [], comment: str = ""): + self.history.append({ + "population_fitness": [i.fitness.values for i in population], - # 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, + # while you could get pareto-front from fitness later, + # it is easier to save fitness values from hall-of-fame + "pareto_front_fitness": [i.fitness.values for i in pareto_front], + + # comments about how current generation was created + # or if there are any changes that need attention + "comment": comment }) - 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 + def save_fitness_history(self, path: str): + """Save current fitness history to JSON file""" try: - df = self.create_fitness_raw_df() - df.to_csv(path, index=False) + with open(path, "w") as json_file: + json.dump(self.history, json_file) except Exception as e: - print(f"Error occured when trying to write fitness history (raw): {e}") + print(f"Error while saving history to {path}: {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}") + +class FitnessHistorySummary: + + def __init__(self, path: str): + with open(path, "r") as json_file: + history = json.load(json_file) + + self.population_fitness_history = [generation["population_fitness"] for generation in history] + self.pareto_front_fitness_history = [generation["pareto_front_fitness"] for generation in history] + self.comments = [generation["comment"] for generation in history] + + def agg_population_fitness(self, agg_function): + # List is used in case population size or number of objectives changes (np.array needs the same shape) + return [ + agg_function(population_fitness, axis=0) + for population_fitness in self.population_fitness_history + ] + + def agg_pareto_front_fitness(self, agg_function): + # List is used in case population size or number of objectives changes (np.array needs the same shape) + return [ + agg_function(pareto_front_fitness, axis=0) + for pareto_front_fitness in self.pareto_front_fitness_history + ] + + def uniqueness_scores(self): + """Calculate uniqueness of fitness values in population""" + return [ + len(set(map(tuple, population_fitness))) / len(population_fitness) + for population_fitness in self.population_fitness_history + ] + + def pareto_front_ratios(self): + """Calculate what ratio of the population is in pareto front""" + return [ + len(pareto_front_fitness) / len(population_fitness) + for population_fitness, pareto_front_fitness in zip( + self.population_fitness_history, + self.pareto_front_fitness_history + ) + ] + + def population_shifts(self): + """Calculate how much the population has changed compared to previous generation""" + return [ + sum(1 for i in new_fitness if i not in old_fitness) / len(new_fitness) + for old_fitness, new_fitness in pairwise(self.population_fitness_history) + ] + + def pareto_front_shifts(self): + """Calculate how much the pareto front has changed compared to previous generation""" + return [ + sum(1 for i in new_fitness if i not in old_fitness) / len(new_fitness) + for old_fitness, new_fitness in pairwise(self.pareto_front_fitness_history) + ] From bcbd162272a02fb2d7343669d80fe29d025682c0 Mon Sep 17 00:00:00 2001 From: maximdu Date: Mon, 8 Dec 2025 17:18:06 +0300 Subject: [PATCH 05/15] Update base.py --- sampo/scheduler/genetic/base.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/sampo/scheduler/genetic/base.py b/sampo/scheduler/genetic/base.py index dbd43a40..d031527b 100644 --- a/sampo/scheduler/genetic/base.py +++ b/sampo/scheduler/genetic/base.py @@ -57,8 +57,8 @@ def __init__(self, is_multiobjective: bool = False, # for experiments with classic RCPSP formulation (initialize population with LFT) only_lft_initialization: bool = False, - fitness_raw_path: str | None = None, - fitness_stats_path: str | None = None): + # where to save history of fitness values + fitness_history_save_path: str | None = None): super().__init__(scheduler_type=scheduler_type, resource_optimizer=resource_optimizer, work_estimator=work_estimator) @@ -77,8 +77,7 @@ 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.fitness_history_save_path = fitness_history_save_path self._time_border = None self._max_plateau_steps = None @@ -253,8 +252,7 @@ def upgrade_pop(self, deadline, self._only_lft_initialization, self._is_multiobjective, - self.fitness_raw_path, - self.fitness_stats_path) + self.fitness_history_save_path) return new_pop def schedule_with_cache(self, @@ -310,8 +308,7 @@ def schedule_with_cache(self, deadline, self._only_lft_initialization, self._is_multiobjective, - self.fitness_raw_path, - self.fitness_stats_path) + self.fitness_history_save_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] From 9a45cacc04034efac221e28ea6446f27129b698e Mon Sep 17 00:00:00 2001 From: maximdu Date: Mon, 8 Dec 2025 17:18:52 +0300 Subject: [PATCH 06/15] Update schedule_builder.py --- sampo/scheduler/genetic/schedule_builder.py | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/sampo/scheduler/genetic/schedule_builder.py b/sampo/scheduler/genetic/schedule_builder.py index 99462aef..0c3a0b80 100644 --- a/sampo/scheduler/genetic/schedule_builder.py +++ b/sampo/scheduler/genetic/schedule_builder.py @@ -114,8 +114,7 @@ def build_schedules(wg: WorkGraph, deadline: Time | None = None, only_lft_initialization: bool = False, is_multiobjective: bool = False, - fitness_raw_path: str | None = None, - fitness_stats_path: str | None = None) \ + fitness_history_save_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, @@ -123,7 +122,7 @@ def build_schedules(wg: WorkGraph, fitness_weights, work_estimator, sgs_type, assigned_parent_time, timeline, time_border, max_plateau_steps, optimize_resources, deadline, only_lft_initialization, is_multiobjective, - fitness_raw_path, fitness_stats_path)[0] + fitness_history_save_path)[0] def build_schedules_with_cache(wg: WorkGraph, @@ -151,8 +150,7 @@ def build_schedules_with_cache(wg: WorkGraph, deadline: Time | None = None, only_lft_initialization: bool = False, is_multiobjective: bool = False, - fitness_raw_path: str | None = None, - fitness_stats_path: str | None = None) \ + fitness_history_save_path: str | None = None) \ -> tuple[list[tuple[ScheduleWorkDict, Time, Timeline, list[GraphNode]]], list[ChromosomeType]]: """ Genetic algorithm. @@ -210,7 +208,8 @@ def build_schedules_with_cache(wg: WorkGraph, for ind, fit in zip(pop, fitness): ind.fitness.values = fit - hof.update(pop); fitness_history.update_history(pop, note="first generation") + hof.update(pop) + fitness_history.update_history(pop, hof, comment="first generation") best_fitness = hof[0].fitness.values SAMPO.logger.info(f'First population evaluation took {evaluation_time * 1000} ms') @@ -242,7 +241,8 @@ def build_schedules_with_cache(wg: WorkGraph, # renewing population pop += offspring pop = toolbox.select(pop) - hof.update(pop); fitness_history.update_history(pop, note="genetic update") + hof.update(pop) + fitness_history.update_history(pop, hof, comment="genetic update") prev_best_fitness = best_fitness best_fitness = hof[0].fitness.values @@ -289,7 +289,8 @@ def build_schedules_with_cache(wg: WorkGraph, evaluation_time += time.time() - evaluation_start - hof.update(pop); fitness_history.update_history(pop, note="first deadline population") + hof.update(pop) + fitness_history.update_history(pop, hof, comment="first deadline population") if best_fitness[0] <= deadline: # Optimizing resources @@ -333,7 +334,8 @@ def build_schedules_with_cache(wg: WorkGraph, # renewing population pop += offspring pop = toolbox.select(pop) - hof.update(pop); fitness_history.update_history(pop, note="genetic deadline update") + hof.update(pop) + fitness_history.update_history(pop, hof, comment="genetic deadline update") prev_best_fitness = best_fitness best_fitness = hof[0].fitness.values @@ -346,8 +348,8 @@ def build_schedules_with_cache(wg: WorkGraph, 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) + if fitness_history_save_path: + fitness_history.save_fitness_history(path=fitness_history_save_path) best_chromosomes = [chromosome for chromosome in hof] From e48c672da6fd8a62dff4b59d6705ccaa3cfb5195 Mon Sep 17 00:00:00 2001 From: maximdu Date: Mon, 8 Dec 2025 17:20:36 +0300 Subject: [PATCH 07/15] Add files via upload --- examples/fitness_history.ipynb | 235 +++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 examples/fitness_history.ipynb diff --git a/examples/fitness_history.ipynb b/examples/fitness_history.ipynb new file mode 100644 index 00000000..f8770262 --- /dev/null +++ b/examples/fitness_history.ipynb @@ -0,0 +1,235 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "6ad37843-2d46-41a7-8637-f7786eb7f72c", + "metadata": {}, + "outputs": [], + "source": [ + "from sampo.generator.base import SimpleSynthetic\n", + "from sampo.generator.environment.contractor_by_wg import get_contractor_by_wg\n", + "\n", + "from sampo.scheduler.genetic.base import GeneticScheduler\n", + "from sampo.scheduler.genetic.operators import TimeAndResourcesFitness\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "import plotly.express as px" + ] + }, + { + "cell_type": "markdown", + "id": "505ef085-4abb-48f5-9c22-cfc8211b8da0", + "metadata": {}, + "source": [ + "## Set parameters and generate synthetic graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af3dc563-fe56-4fea-96ef-30fb0f7552a2", + "metadata": {}, + "outputs": [], + "source": [ + "graph_size = 250\n", + "seed = 123\n", + "\n", + "size_of_population = 25\n", + "number_of_generation = 10\n", + "\n", + "mutate_order = 0.02\n", + "mutate_resources = 0.02\n", + "\n", + "fitness_constructor = TimeAndResourcesFitness()\n", + "fitness_weights = (-1, -1)\n", + "is_multiobjective = True\n", + "optimize_resources = True\n", + "\n", + "fitness_history_save_path = \"./history_test.json\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b8c8da3-f19f-4c74-ac90-db27ef775be0", + "metadata": {}, + "outputs": [], + "source": [ + "ss = SimpleSynthetic(seed)\n", + "wg = ss.work_graph(bottom_border=graph_size)\n", + "contractors = [get_contractor_by_wg(wg)]\n", + "print(wg.vertex_count)" + ] + }, + { + "cell_type": "markdown", + "id": "d2add0c6-3b37-4a17-8af2-96340635b277", + "metadata": {}, + "source": [ + "## Use the genetic algorithm" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "00a68d26-7d3b-4eb6-8395-247c0962c86d", + "metadata": {}, + "outputs": [], + "source": [ + "genetic_algorithm = GeneticScheduler(\n", + " number_of_generation=number_of_generation,\n", + " size_of_population=size_of_population,\n", + " \n", + " mutate_order=mutate_order,\n", + " mutate_resources=mutate_resources,\n", + " \n", + " fitness_constructor=fitness_constructor,\n", + " fitness_weights=fitness_weights,\n", + " is_multiobjective=is_multiobjective,\n", + " optimize_resources=optimize_resources,\n", + " \n", + " seed=seed,\n", + " fitness_history_save_path=fitness_history_save_path\n", + ")\n", + "genetic_result = genetic_algorithm.schedule(wg, contractors)" + ] + }, + { + "cell_type": "markdown", + "id": "0a2019d6-dd49-4150-9c25-2cac2560063c", + "metadata": {}, + "source": [ + "## Get summary of the evolution" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73095ca6-9433-4270-abf5-f8beae4cb90d", + "metadata": {}, + "outputs": [], + "source": [ + "from sampo.scheduler.utils.fitness_history import FitnessHistorySummary" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6aefd90-e8ee-4ccb-9451-0ff5ba850792", + "metadata": {}, + "outputs": [], + "source": [ + "summary = FitnessHistorySummary(fitness_history_save_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dadc10b3-9171-4b81-96cd-7faace7bd868", + "metadata": {}, + "outputs": [], + "source": [ + "means = np.array(summary.agg_population_fitness(np.mean))\n", + "fig = px.line(x=means[:, 0], y=means[:, 1], markers=True, template=\"plotly_white\")\n", + "fig.update_layout(height=500, width=500)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea8885bf-def2-47a5-97e5-4cd176b7f4ee", + "metadata": {}, + "outputs": [], + "source": [ + "medians = np.array(summary.agg_pareto_front_fitness(np.median))\n", + "fig = px.line(x=medians[:, 0], y=medians[:, 1], markers=True, template=\"plotly_white\")\n", + "fig.update_layout(height=500, width=500)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a36746bb-867d-4f56-a691-ff373ed8e29c", + "metadata": {}, + "outputs": [], + "source": [ + "population_shifts = summary.population_shifts()\n", + "fig = px.line(y=population_shifts, markers=True, template=\"plotly_white\")\n", + "fig.update_layout(height=500, width=1000)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbf65b45-248a-417d-9274-2773312e3e2a", + "metadata": {}, + "outputs": [], + "source": [ + "pareto_shifts = summary.pareto_front_shifts()\n", + "fig = px.line(y=pareto_shifts, markers=True, template=\"plotly_white\")\n", + "fig.update_layout(height=500, width=1000)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "626ca043-54be-449d-a682-aef6abbad455", + "metadata": {}, + "outputs": [], + "source": [ + "pareto_ratios = summary.pareto_front_ratios()\n", + "fig = px.line(y=pareto_ratios, markers=True, template=\"plotly_white\")\n", + "fig.update_layout(height=500, width=1000)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88d5aeb0-0af3-4002-9be4-6e090c6c0fd2", + "metadata": {}, + "outputs": [], + "source": [ + "uniqueness = summary.uniqueness_scores()\n", + "fig = px.line(y=uniqueness, markers=True, template=\"plotly_white\")\n", + "fig.update_layout(height=500, width=1000)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64abcfe7-203b-4cab-b015-7fbd0dbe66d3", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 9d258dd76575f28a8931c364ee162e7d2726e23b Mon Sep 17 00:00:00 2001 From: maximdu Date: Wed, 10 Dec 2025 19:42:47 +0300 Subject: [PATCH 08/15] Added saving pareto-front From 36166e900af74abbffb6fb10b7121b867bc704e6 Mon Sep 17 00:00:00 2001 From: maximdu Date: Wed, 10 Dec 2025 19:43:57 +0300 Subject: [PATCH 09/15] Added stats for pareto-front --- sampo/scheduler/utils/fitness_history.py | 58 ++++++++++-------------- 1 file changed, 24 insertions(+), 34 deletions(-) diff --git a/sampo/scheduler/utils/fitness_history.py b/sampo/scheduler/utils/fitness_history.py index 55819433..b6ed6180 100644 --- a/sampo/scheduler/utils/fitness_history.py +++ b/sampo/scheduler/utils/fitness_history.py @@ -1,6 +1,7 @@ import json from typing import Any, Iterable from itertools import pairwise +import numpy as np class FitnessHistory: @@ -24,6 +25,7 @@ def update_history(self, population: Iterable = [], pareto_front: Iterable = [], def save_fitness_history(self, path: str): """Save current fitness history to JSON file""" + # JSON format is a bit more flexible try: with open(path, "w") as json_file: json.dump(self.history, json_file) @@ -37,51 +39,39 @@ def __init__(self, path: str): with open(path, "r") as json_file: history = json.load(json_file) - self.population_fitness_history = [generation["population_fitness"] for generation in history] - self.pareto_front_fitness_history = [generation["pareto_front_fitness"] for generation in history] + self.population_history = [generation["population_fitness"] for generation in history] + self.pareto_front_history = [generation["pareto_front_fitness"] for generation in history] self.comments = [generation["comment"] for generation in history] - def agg_population_fitness(self, agg_function): - # List is used in case population size or number of objectives changes (np.array needs the same shape) + def get_mean_fitness(self, only_pareto: bool = False): + """Average fitness in population for each objective and generation""" + data = self.pareto_front_history if only_pareto else self.population_history return [ - agg_function(population_fitness, axis=0) - for population_fitness in self.population_fitness_history + np.mean(fitness_values, axis=0) + for fitness_values in data ] - def agg_pareto_front_fitness(self, agg_function): - # List is used in case population size or number of objectives changes (np.array needs the same shape) - return [ - agg_function(pareto_front_fitness, axis=0) - for pareto_front_fitness in self.pareto_front_fitness_history - ] - - def uniqueness_scores(self): - """Calculate uniqueness of fitness values in population""" - return [ - len(set(map(tuple, population_fitness))) / len(population_fitness) - for population_fitness in self.population_fitness_history - ] - - def pareto_front_ratios(self): - """Calculate what ratio of the population is in pareto front""" + def get_pareto_to_population_ratios(self): + """What part of the population is in pareto front""" + data = zip(self.population_history, self.pareto_front_history) return [ len(pareto_front_fitness) / len(population_fitness) - for population_fitness, pareto_front_fitness in zip( - self.population_fitness_history, - self.pareto_front_fitness_history - ) + for population_fitness, pareto_front_fitness in data ] - def population_shifts(self): - """Calculate how much the population has changed compared to previous generation""" + def get_generation_shifts(self, only_pareto: bool = False): + """How much the population has changed compared to previous generation""" + data = self.pareto_front_history if only_pareto else self.population_history + data = pairwise(data) return [ - sum(1 for i in new_fitness if i not in old_fitness) / len(new_fitness) - for old_fitness, new_fitness in pairwise(self.population_fitness_history) + sum(1 for value in new_fitness if value not in old_fitness) / len(new_fitness) + for old_fitness, new_fitness in data ] - def pareto_front_shifts(self): - """Calculate how much the pareto front has changed compared to previous generation""" + def get_uniqueness_scores(self, only_pareto: bool = False): + """Uniqueness of fitness values in population""" + data = self.pareto_front_history if only_pareto else self.population_history return [ - sum(1 for i in new_fitness if i not in old_fitness) / len(new_fitness) - for old_fitness, new_fitness in pairwise(self.pareto_front_fitness_history) + len(set(map(tuple, fitness_values))) / len(fitness_values) + for fitness_values in data ] From 061aed65154cd47f36555ab8bccdf54b48e09401 Mon Sep 17 00:00:00 2001 From: maximdu Date: Wed, 10 Dec 2025 19:44:44 +0300 Subject: [PATCH 10/15] Updated example to work with pareto-front stats --- examples/fitness_history.ipynb | 55 +++++++++++++--------------------- 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/examples/fitness_history.ipynb b/examples/fitness_history.ipynb index f8770262..d38626cc 100644 --- a/examples/fitness_history.ipynb +++ b/examples/fitness_history.ipynb @@ -9,7 +9,6 @@ "source": [ "from sampo.generator.base import SimpleSynthetic\n", "from sampo.generator.environment.contractor_by_wg import get_contractor_by_wg\n", - "\n", "from sampo.scheduler.genetic.base import GeneticScheduler\n", "from sampo.scheduler.genetic.operators import TimeAndResourcesFitness\n", "\n", @@ -36,8 +35,8 @@ "graph_size = 250\n", "seed = 123\n", "\n", - "size_of_population = 25\n", - "number_of_generation = 10\n", + "size_of_population = 100\n", + "number_of_generation = 20\n", "\n", "mutate_order = 0.02\n", "mutate_resources = 0.02\n", @@ -59,8 +58,7 @@ "source": [ "ss = SimpleSynthetic(seed)\n", "wg = ss.work_graph(bottom_border=graph_size)\n", - "contractors = [get_contractor_by_wg(wg)]\n", - "print(wg.vertex_count)" + "contractors = [get_contractor_by_wg(wg)]" ] }, { @@ -68,7 +66,7 @@ "id": "d2add0c6-3b37-4a17-8af2-96340635b277", "metadata": {}, "source": [ - "## Use the genetic algorithm" + "## Use the genetic algorithm and save history" ] }, { @@ -131,8 +129,8 @@ "metadata": {}, "outputs": [], "source": [ - "means = np.array(summary.agg_population_fitness(np.mean))\n", - "fig = px.line(x=means[:, 0], y=means[:, 1], markers=True, template=\"plotly_white\")\n", + "population_means = np.array(summary.get_mean_fitness())\n", + "fig = px.line(x=population_means[:, 0], y=population_means[:, 1], markers=True, template=\"plotly_white\")\n", "fig.update_layout(height=500, width=500)\n", "fig.show()" ] @@ -144,8 +142,8 @@ "metadata": {}, "outputs": [], "source": [ - "medians = np.array(summary.agg_pareto_front_fitness(np.median))\n", - "fig = px.line(x=medians[:, 0], y=medians[:, 1], markers=True, template=\"plotly_white\")\n", + "pareto_front_means = np.array(summary.get_mean_fitness(only_pareto=True))\n", + "fig = px.line(x=pareto_front_means[:, 0], y=pareto_front_means[:, 1], markers=True, template=\"plotly_white\")\n", "fig.update_layout(height=500, width=500)\n", "fig.show()" ] @@ -153,25 +151,12 @@ { "cell_type": "code", "execution_count": null, - "id": "a36746bb-867d-4f56-a691-ff373ed8e29c", - "metadata": {}, - "outputs": [], - "source": [ - "population_shifts = summary.population_shifts()\n", - "fig = px.line(y=population_shifts, markers=True, template=\"plotly_white\")\n", - "fig.update_layout(height=500, width=1000)\n", - "fig.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bbf65b45-248a-417d-9274-2773312e3e2a", + "id": "626ca043-54be-449d-a682-aef6abbad455", "metadata": {}, "outputs": [], "source": [ - "pareto_shifts = summary.pareto_front_shifts()\n", - "fig = px.line(y=pareto_shifts, markers=True, template=\"plotly_white\")\n", + "pareto_ratios = summary.get_pareto_to_population_ratios()\n", + "fig = px.line(y=pareto_ratios, markers=True, template=\"plotly_white\")\n", "fig.update_layout(height=500, width=1000)\n", "fig.show()" ] @@ -179,13 +164,14 @@ { "cell_type": "code", "execution_count": null, - "id": "626ca043-54be-449d-a682-aef6abbad455", + "id": "a36746bb-867d-4f56-a691-ff373ed8e29c", "metadata": {}, "outputs": [], "source": [ - "pareto_ratios = summary.pareto_front_ratios()\n", - "fig = px.line(y=pareto_ratios, markers=True, template=\"plotly_white\")\n", - "fig.update_layout(height=500, width=1000)\n", + "population_shifts = summary.get_generation_shifts()\n", + "pareto_front_shifts = summary.get_generation_shifts(only_pareto=True)\n", + "fig = px.line(y=[population_shifts, pareto_front_shifts], markers=True, template=\"plotly_white\")\n", + "fig.update_layout(height=500, width=1000, showlegend=False)\n", "fig.show()" ] }, @@ -196,16 +182,17 @@ "metadata": {}, "outputs": [], "source": [ - "uniqueness = summary.uniqueness_scores()\n", - "fig = px.line(y=uniqueness, markers=True, template=\"plotly_white\")\n", - "fig.update_layout(height=500, width=1000)\n", + "population_uniqueness = summary.get_uniqueness_scores()\n", + "pareto_uniqueness = summary.get_uniqueness_scores(only_pareto=True)\n", + "fig = px.line(y=[population_uniqueness, pareto_uniqueness], markers=True, template=\"plotly_white\")\n", + "fig.update_layout(height=500, width=1000, showlegend=False)\n", "fig.show()" ] }, { "cell_type": "code", "execution_count": null, - "id": "64abcfe7-203b-4cab-b015-7fbd0dbe66d3", + "id": "3564e9ca-a9fd-43df-9a80-cbd2c74426e4", "metadata": {}, "outputs": [], "source": [] From 1cdb82d8cb89cbec017812ff76f87d978f15e3a0 Mon Sep 17 00:00:00 2001 From: maximdu Date: Fri, 12 Dec 2025 14:11:58 +0300 Subject: [PATCH 11/15] minor changes --- sampo/scheduler/utils/fitness_history.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sampo/scheduler/utils/fitness_history.py b/sampo/scheduler/utils/fitness_history.py index b6ed6180..59ff75c9 100644 --- a/sampo/scheduler/utils/fitness_history.py +++ b/sampo/scheduler/utils/fitness_history.py @@ -5,12 +5,12 @@ class FitnessHistory: - """Class to track fitness values during evolution""" + """Recording fitness values during evolution""" def __init__(self): self.history: list[dict[str, Any]] = [] - def update_history(self, population: Iterable = [], pareto_front: Iterable = [], comment: str = ""): + def update(self, population: Iterable = [], pareto_front: Iterable = [], comment: str = ""): self.history.append({ "population_fitness": [i.fitness.values for i in population], @@ -23,7 +23,7 @@ def update_history(self, population: Iterable = [], pareto_front: Iterable = [], "comment": comment }) - def save_fitness_history(self, path: str): + def save_to_json(self, path: str): """Save current fitness history to JSON file""" # JSON format is a bit more flexible try: @@ -34,6 +34,7 @@ def save_fitness_history(self, path: str): class FitnessHistorySummary: + """Functions for creating summary of evolution""" def __init__(self, path: str): with open(path, "r") as json_file: @@ -43,8 +44,9 @@ def __init__(self, path: str): self.pareto_front_history = [generation["pareto_front_fitness"] for generation in history] self.comments = [generation["comment"] for generation in history] - def get_mean_fitness(self, only_pareto: bool = False): - """Average fitness in population for each objective and generation""" + def get_fitness_means(self, only_pareto: bool = False): + """Average fitness in population for each generation and objective""" + # mean might be better than median, outliers matter data = self.pareto_front_history if only_pareto else self.population_history return [ np.mean(fitness_values, axis=0) From 6254c1951e3020b2ee79318d14ccbe51be83cc4b Mon Sep 17 00:00:00 2001 From: maximdu Date: Fri, 12 Dec 2025 14:14:19 +0300 Subject: [PATCH 12/15] minor changes --- sampo/scheduler/genetic/base.py | 9 +++++---- sampo/scheduler/genetic/schedule_builder.py | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/sampo/scheduler/genetic/base.py b/sampo/scheduler/genetic/base.py index d031527b..050d2a63 100644 --- a/sampo/scheduler/genetic/base.py +++ b/sampo/scheduler/genetic/base.py @@ -58,7 +58,8 @@ def __init__(self, # for experiments with classic RCPSP formulation (initialize population with LFT) only_lft_initialization: bool = False, # where to save history of fitness values - fitness_history_save_path: str | None = None): + # saving instead of returning to avoid breaking modules + save_history_to: str | None = None): super().__init__(scheduler_type=scheduler_type, resource_optimizer=resource_optimizer, work_estimator=work_estimator) @@ -77,7 +78,7 @@ def __init__(self, self._is_multiobjective = is_multiobjective self._weights = weights self._only_lft_initialization = only_lft_initialization - self.fitness_history_save_path = fitness_history_save_path + self.save_history_to = save_history_to self._time_border = None self._max_plateau_steps = None @@ -252,7 +253,7 @@ def upgrade_pop(self, deadline, self._only_lft_initialization, self._is_multiobjective, - self.fitness_history_save_path) + self.save_history_to) return new_pop def schedule_with_cache(self, @@ -308,7 +309,7 @@ def schedule_with_cache(self, deadline, self._only_lft_initialization, self._is_multiobjective, - self.fitness_history_save_path) + self.save_history_to) 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] diff --git a/sampo/scheduler/genetic/schedule_builder.py b/sampo/scheduler/genetic/schedule_builder.py index 0c3a0b80..a596523e 100644 --- a/sampo/scheduler/genetic/schedule_builder.py +++ b/sampo/scheduler/genetic/schedule_builder.py @@ -114,7 +114,7 @@ def build_schedules(wg: WorkGraph, deadline: Time | None = None, only_lft_initialization: bool = False, is_multiobjective: bool = False, - fitness_history_save_path: str | None = None) \ + save_history_to: 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, @@ -122,7 +122,7 @@ def build_schedules(wg: WorkGraph, fitness_weights, work_estimator, sgs_type, assigned_parent_time, timeline, time_border, max_plateau_steps, optimize_resources, deadline, only_lft_initialization, is_multiobjective, - fitness_history_save_path)[0] + save_history_to)[0] def build_schedules_with_cache(wg: WorkGraph, @@ -150,7 +150,7 @@ def build_schedules_with_cache(wg: WorkGraph, deadline: Time | None = None, only_lft_initialization: bool = False, is_multiobjective: bool = False, - fitness_history_save_path: str | None = None) \ + save_history_to: str | None = None) \ -> tuple[list[tuple[ScheduleWorkDict, Time, Timeline, list[GraphNode]]], list[ChromosomeType]]: """ Genetic algorithm. @@ -197,8 +197,8 @@ def build_schedules_with_cache(wg: WorkGraph, evaluation_start = time.time() - fitness_history = FitnessHistory() hof = tools.ParetoFront(similar=compare_individuals) + fitness_history = FitnessHistory() # map to each individual fitness function fitness = SAMPO.backend.compute_chromosomes(fitness_f, pop) @@ -209,7 +209,7 @@ def build_schedules_with_cache(wg: WorkGraph, ind.fitness.values = fit hof.update(pop) - fitness_history.update_history(pop, hof, comment="first generation") + fitness_history.update(pop, hof, comment="first generation") best_fitness = hof[0].fitness.values SAMPO.logger.info(f'First population evaluation took {evaluation_time * 1000} ms') @@ -242,7 +242,7 @@ def build_schedules_with_cache(wg: WorkGraph, pop += offspring pop = toolbox.select(pop) hof.update(pop) - fitness_history.update_history(pop, hof, comment="genetic update") + fitness_history.update(pop, hof, comment="genetic update") prev_best_fitness = best_fitness best_fitness = hof[0].fitness.values @@ -290,7 +290,7 @@ def build_schedules_with_cache(wg: WorkGraph, evaluation_time += time.time() - evaluation_start hof.update(pop) - fitness_history.update_history(pop, hof, comment="first deadline population") + fitness_history.update(pop, hof, comment="first deadline population") if best_fitness[0] <= deadline: # Optimizing resources @@ -335,7 +335,7 @@ def build_schedules_with_cache(wg: WorkGraph, pop += offspring pop = toolbox.select(pop) hof.update(pop) - fitness_history.update_history(pop, hof, comment="genetic deadline update") + fitness_history.update(pop, hof, comment="genetic deadline update") prev_best_fitness = best_fitness best_fitness = hof[0].fitness.values @@ -348,8 +348,8 @@ def build_schedules_with_cache(wg: WorkGraph, 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 - if fitness_history_save_path: - fitness_history.save_fitness_history(path=fitness_history_save_path) + if save_history_to: + fitness_history.save_to_json(path=save_history_to) best_chromosomes = [chromosome for chromosome in hof] From 36f9d4af6f3f0140580436d29e49783acab8ab6d Mon Sep 17 00:00:00 2001 From: maximdu Date: Fri, 12 Dec 2025 14:14:50 +0300 Subject: [PATCH 13/15] minor changes --- examples/fitness_history.ipynb | 58 +++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/examples/fitness_history.ipynb b/examples/fitness_history.ipynb index d38626cc..ae3d3fef 100644 --- a/examples/fitness_history.ipynb +++ b/examples/fitness_history.ipynb @@ -10,11 +10,7 @@ "from sampo.generator.base import SimpleSynthetic\n", "from sampo.generator.environment.contractor_by_wg import get_contractor_by_wg\n", "from sampo.scheduler.genetic.base import GeneticScheduler\n", - "from sampo.scheduler.genetic.operators import TimeAndResourcesFitness\n", - "\n", - "import numpy as np\n", - "import pandas as pd\n", - "import plotly.express as px" + "from sampo.scheduler.genetic.operators import TimeAndResourcesFitness" ] }, { @@ -46,7 +42,7 @@ "is_multiobjective = True\n", "optimize_resources = True\n", "\n", - "fitness_history_save_path = \"./history_test.json\"" + "save_history_to = \"./history_test.json\"" ] }, { @@ -58,7 +54,8 @@ "source": [ "ss = SimpleSynthetic(seed)\n", "wg = ss.work_graph(bottom_border=graph_size)\n", - "contractors = [get_contractor_by_wg(wg)]" + "contractors = [get_contractor_by_wg(wg)]\n", + "print(f\"Generated graph with size: {wg.vertex_count}\")" ] }, { @@ -89,7 +86,7 @@ " optimize_resources=optimize_resources,\n", " \n", " seed=seed,\n", - " fitness_history_save_path=fitness_history_save_path\n", + " save_history_to=save_history_to\n", ")\n", "genetic_result = genetic_algorithm.schedule(wg, contractors)" ] @@ -109,7 +106,14 @@ "metadata": {}, "outputs": [], "source": [ - "from sampo.scheduler.utils.fitness_history import FitnessHistorySummary" + "from sampo.scheduler.utils.fitness_history import FitnessHistorySummary\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "import plotly.express as px\n", + "import plotly.io as pio\n", + "pio.templates.default = \"plotly_dark\"" ] }, { @@ -119,7 +123,8 @@ "metadata": {}, "outputs": [], "source": [ - "summary = FitnessHistorySummary(fitness_history_save_path)" + "# Load history from file\n", + "summary = FitnessHistorySummary(save_history_to)" ] }, { @@ -129,9 +134,10 @@ "metadata": {}, "outputs": [], "source": [ - "population_means = np.array(summary.get_mean_fitness())\n", - "fig = px.line(x=population_means[:, 0], y=population_means[:, 1], markers=True, template=\"plotly_white\")\n", - "fig.update_layout(height=500, width=500)\n", + "population_means = np.array(summary.get_fitness_means())\n", + "\n", + "fig = px.line(x=population_means[:, 0], y=population_means[:, 1], markers=True)\n", + "fig.update_layout(height=700, width=700)\n", "fig.show()" ] }, @@ -142,9 +148,10 @@ "metadata": {}, "outputs": [], "source": [ - "pareto_front_means = np.array(summary.get_mean_fitness(only_pareto=True))\n", - "fig = px.line(x=pareto_front_means[:, 0], y=pareto_front_means[:, 1], markers=True, template=\"plotly_white\")\n", - "fig.update_layout(height=500, width=500)\n", + "pareto_front_means = np.array(summary.get_fitness_means(only_pareto=True))\n", + "\n", + "fig = px.line(x=pareto_front_means[:, 0], y=pareto_front_means[:, 1], markers=True)\n", + "fig.update_layout(height=700, width=700)\n", "fig.show()" ] }, @@ -156,7 +163,8 @@ "outputs": [], "source": [ "pareto_ratios = summary.get_pareto_to_population_ratios()\n", - "fig = px.line(y=pareto_ratios, markers=True, template=\"plotly_white\")\n", + "\n", + "fig = px.line(y=pareto_ratios, markers=True)\n", "fig.update_layout(height=500, width=1000)\n", "fig.show()" ] @@ -170,7 +178,9 @@ "source": [ "population_shifts = summary.get_generation_shifts()\n", "pareto_front_shifts = summary.get_generation_shifts(only_pareto=True)\n", - "fig = px.line(y=[population_shifts, pareto_front_shifts], markers=True, template=\"plotly_white\")\n", + "\n", + "fig = px.line(y=[population_shifts, pareto_front_shifts], markers=True)\n", + "fig.data[1].line.color = \"white\"\n", "fig.update_layout(height=500, width=1000, showlegend=False)\n", "fig.show()" ] @@ -184,7 +194,9 @@ "source": [ "population_uniqueness = summary.get_uniqueness_scores()\n", "pareto_uniqueness = summary.get_uniqueness_scores(only_pareto=True)\n", - "fig = px.line(y=[population_uniqueness, pareto_uniqueness], markers=True, template=\"plotly_white\")\n", + "\n", + "fig = px.line(y=[population_uniqueness, pareto_uniqueness], markers=True)\n", + "fig.data[1].line.color = \"white\"\n", "fig.update_layout(height=500, width=1000, showlegend=False)\n", "fig.show()" ] @@ -196,6 +208,14 @@ "metadata": {}, "outputs": [], "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9bf8b8c-9ffd-4e0d-ae76-cdc4dbbf86c0", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { From 88864b77df671c75f5150fec757584005d353e1a Mon Sep 17 00:00:00 2001 From: maximdu Date: Tue, 16 Dec 2025 11:22:40 +0300 Subject: [PATCH 14/15] Added comments and tracking offsprings --- sampo/scheduler/utils/fitness_history.py | 69 ++++++++++++++++-------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/sampo/scheduler/utils/fitness_history.py b/sampo/scheduler/utils/fitness_history.py index 59ff75c9..3b7d803c 100644 --- a/sampo/scheduler/utils/fitness_history.py +++ b/sampo/scheduler/utils/fitness_history.py @@ -1,7 +1,8 @@ import json -from typing import Any, Iterable -from itertools import pairwise import numpy as np +from itertools import pairwise +from typing import Any, Iterable +from sampo.base import SAMPO class FitnessHistory: @@ -10,27 +11,35 @@ class FitnessHistory: def __init__(self): self.history: list[dict[str, Any]] = [] - def update(self, population: Iterable = [], pareto_front: Iterable = [], comment: str = ""): + def update(self, + population: Iterable = [], + # while you could get pareto-front from fitness later, + # it is easier to save fitness values from hall-of-fame + pareto_front: Iterable = [], + # currently, history is updated only after genetic selection, + # so offsprings were generated for this population, not from it + offsprings: Iterable = [], + # comments about how current generation was created + # or if there are any changes that need attention + comment: str = "") -> None: + self.history.append({ "population_fitness": [i.fitness.values for i in population], - - # while you could get pareto-front from fitness later, - # it is easier to save fitness values from hall-of-fame "pareto_front_fitness": [i.fitness.values for i in pareto_front], - - # comments about how current generation was created - # or if there are any changes that need attention + "offsprings_fitness": [i.fitness.values for i in offsprings], "comment": comment }) + # JSON format is more flexible + # try: except: is just in case def save_to_json(self, path: str): """Save current fitness history to JSON file""" - # JSON format is a bit more flexible try: with open(path, "w") as json_file: json.dump(self.history, json_file) + SAMPO.logger.info(f"Saved history to: {path}") except Exception as e: - print(f"Error while saving history to {path}: {e}") + SAMPO.logger.info(f"Error while saving history to {path}: {e}") class FitnessHistorySummary: @@ -42,25 +51,27 @@ def __init__(self, path: str): self.population_history = [generation["population_fitness"] for generation in history] self.pareto_front_history = [generation["pareto_front_fitness"] for generation in history] + self.offsprings_history = [generation["offsprings_fitness"] for generation in history] self.comments = [generation["comment"] for generation in history] - def get_fitness_means(self, only_pareto: bool = False): - """Average fitness in population for each generation and objective""" - # mean might be better than median, outliers matter - data = self.pareto_front_history if only_pareto else self.population_history + # is the mean fitness of population improving? + # mean might be better than median, outliers matter + def get_fitness_means(self, only_pareto: bool = False, only_offsprings: bool = False): + """Mean fitness in population for each generation and objective""" + if only_pareto: + data = self.pareto_front_history + elif only_offsprings: + data = self.offsprings_history + else: + data = self.population_history + return [ np.mean(fitness_values, axis=0) for fitness_values in data ] - def get_pareto_to_population_ratios(self): - """What part of the population is in pareto front""" - data = zip(self.population_history, self.pareto_front_history) - return [ - len(pareto_front_fitness) / len(population_fitness) - for population_fitness, pareto_front_fitness in data - ] - + # be careful if selection method is not truncation selection + # higher shifts != better algorithm, but generally higher is better def get_generation_shifts(self, only_pareto: bool = False): """How much the population has changed compared to previous generation""" data = self.pareto_front_history if only_pareto else self.population_history @@ -70,6 +81,18 @@ def get_generation_shifts(self, only_pareto: bool = False): for old_fitness, new_fitness in data ] + # how "stretched" are the fitness values near the pareto-front + # higher ratio generally means more diverse fitness values + def get_pareto_to_population_ratios(self): + """What part of the population is in pareto front""" + data = zip(self.population_history, self.pareto_front_history) + return [ + len(pareto_front_fitness) / len(population_fitness) + for population_fitness, pareto_front_fitness in data + ] + + # it might be better to remove phenotype duplicates: + # https://www.ac.tuwien.ac.at/files/pub/raidl-99c.pdf def get_uniqueness_scores(self, only_pareto: bool = False): """Uniqueness of fitness values in population""" data = self.pareto_front_history if only_pareto else self.population_history From 6da780e4da28a4db79417602e75288eb6f2e704e Mon Sep 17 00:00:00 2001 From: maximdu Date: Tue, 16 Dec 2025 11:25:47 +0300 Subject: [PATCH 15/15] Added tracking offsprings --- sampo/scheduler/genetic/schedule_builder.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sampo/scheduler/genetic/schedule_builder.py b/sampo/scheduler/genetic/schedule_builder.py index a596523e..6b5ef6db 100644 --- a/sampo/scheduler/genetic/schedule_builder.py +++ b/sampo/scheduler/genetic/schedule_builder.py @@ -209,7 +209,7 @@ def build_schedules_with_cache(wg: WorkGraph, ind.fitness.values = fit hof.update(pop) - fitness_history.update(pop, hof, comment="first generation") + fitness_history.update(pop, hof, pop, comment="first generation") best_fitness = hof[0].fitness.values SAMPO.logger.info(f'First population evaluation took {evaluation_time * 1000} ms') @@ -242,7 +242,7 @@ def build_schedules_with_cache(wg: WorkGraph, pop += offspring pop = toolbox.select(pop) hof.update(pop) - fitness_history.update(pop, hof, comment="genetic update") + fitness_history.update(pop, hof, offspring, comment="genetic update") prev_best_fitness = best_fitness best_fitness = hof[0].fitness.values @@ -290,7 +290,7 @@ def build_schedules_with_cache(wg: WorkGraph, evaluation_time += time.time() - evaluation_start hof.update(pop) - fitness_history.update(pop, hof, comment="first deadline population") + fitness_history.update(pop, hof, pop, comment="first deadline population") if best_fitness[0] <= deadline: # Optimizing resources @@ -335,7 +335,7 @@ def build_schedules_with_cache(wg: WorkGraph, pop += offspring pop = toolbox.select(pop) hof.update(pop) - fitness_history.update(pop, hof, comment="genetic deadline update") + fitness_history.update(pop, hof, offspring, comment="genetic deadline update") prev_best_fitness = best_fitness best_fitness = hof[0].fitness.values