From 857036149e515212213ccc9dc09427f0c1affbb7 Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Fri, 27 Oct 2023 12:15:58 +0200 Subject: [PATCH 01/21] first draft of batches and multiprocessing --- example/run_mopso.py | 2 +- optimizer/mopso.py | 119 +++++++++++++++++++++++++++++++++++-------- setup.py | 2 +- 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/example/run_mopso.py b/example/run_mopso.py index bacdb7f..01dbf74 100644 --- a/example/run_mopso.py +++ b/example/run_mopso.py @@ -35,4 +35,4 @@ def objective_function_2(x): plt.xlabel("Objective 1") plt.ylabel("Objective 2") plt.title("Pareto Front") - plt.savefig("tmp/pareto") \ No newline at end of file + plt.savefig("tmp/pareto") diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 9584c8c..41dda6c 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -21,6 +21,8 @@ import random import numpy as np from optimizer import Optimizer, FileManager +from concurrent.futures import ProcessPoolExecutor +import warnings class Particle: @@ -195,7 +197,7 @@ class MOPSO(Optimizer): def __init__(self, objective, - lower_bounds, upper_bounds, num_particles=50, + lower_bounds, upper_bounds, num_particles=50, num_batch = 1, inertia_weight=0.5, cognitive_coefficient=1, social_coefficient=1, incremental_pareto=False, initial_particles_position='spread'): self.objective = objective @@ -206,6 +208,7 @@ def __init__(self, except FileNotFoundError as e: print("Checkpoint not found. Fallback to standard construction.") self.num_particles = num_particles + self.num_batch = num_batch self.num_params = len(lower_bounds) self.lower_bounds = lower_bounds self.upper_bounds = upper_bounds @@ -231,6 +234,33 @@ def __init__(self, else: raise ValueError( f"MOPSO: initial_particles_position must be one of {VALID_INITIAL_PARTICLES_POSITIONS}") + if (num_batch == 1): + self.particles_batch.append(self.particles) + self.batch_size = len(self.particles) + else: + # Calculate the approximate batch size + self.batch_size = len(self.particles) // self.num_batch + + # Check if the division leaves some elements unallocated + remaining_elements = len(input_list) % self.num_batch + + if remaining_elements > 0: + # Warn the user and suggest adjusting the number of particles or batches + warning_message = ( + f"The specified number of batches ({self.num_batch}) does not evenly divide the number of particles ({len(input_list)}). " + f"Consider incrementing the number of particles by a multiple of {remaining_elements} or decreasing the number of batches." + ) + warnings.warn(warning_message) + + # Use list comprehension to create batches + self.particles_batch = [self.particles[i:i + self.batch_size] + for i in range(0, len(self.particles), self.batch_size)] + + # If the division leaves some elements unallocated, add them to the last batch + if remaining_elements > 0: + last_batch = self.particles_batch.pop() + last_batch.extend(input_list[len(batches) * self.batch_size:]) + self.particles_batch.append(last_batch) self.iteration = 0 self.incremental_pareto = incremental_pareto @@ -256,6 +286,7 @@ def save_attributes(self): 'upper_bounds': self.upper_bounds, 'num_particles': self.num_particles, 'num_params': self.num_params, + 'num_batch': slef.num_batch, 'inertia_weight': self.inertia_weight, 'cognitive_coefficient': self.cognitive_coefficient, 'social_coefficient': self.social_coefficient, @@ -283,11 +314,11 @@ def save_state(self): particle.velocity, particle.best_position, np.ravel(particle.best_fitness)]) - for particle in self.particles], + for particle in batch for batch in self.particles_batch], 'checkpoint/individual_states.csv') FileManager.save_csv([np.concatenate([particle.position, np.ravel(particle.fitness)]) - for particle in self.pareto_front], + for particle in batch for batch in self.particles_batch], 'checkpoint/pareto_front.csv') def load_checkpoint(self): @@ -310,6 +341,7 @@ def load_checkpoint(self): self.upper_bounds = pso_attributes['upper_bounds'] self.num_particles = pso_attributes['num_particles'] self.num_params = pso_attributes['num_params'] + self.num_batch = pso_attributes['num_batch'] self.inertia_weight = pso_attributes['inertia_weight'] self.cognitive_coefficient = pso_attributes['cognitive_coefficient'] self.social_coefficient = pso_attributes['social_coefficient'] @@ -350,7 +382,37 @@ def load_checkpoint(self): best_fitness=None) self.pareto_front.append(particle) - def optimize(self, num_iterations=100, max_iter_no_improv=None): + # restor batches + self.particles_batch = [] + if (num_batch == 1): + self.particles_batch.append(self.particles) + self.batch_size = len(self.particles) + else: + # Calculate the approximate batch size + self.batch_size = len(self.particles) // self.num_batch + + # Check if the division leaves some elements unallocated + remaining_elements = len(input_list) % self.num_batch + + if remaining_elements > 0: + # Warn the user and suggest adjusting the number of particles or batches + warning_message = ( + f"The specified number of batches ({self.num_batch}) does not evenly divide the number of particles ({len(input_list)}). " + f"Consider incrementing the number of particles by a multiple of {remaining_elements} or decreasing the number of batches." + ) + warnings.warn(warning_message) + + # Use list comprehension to create batches + self.particles_batch = [self.particles[i:i + self.batch_size] + for i in range(0, len(self.particles), self.batch_size)] + + # If the division leaves some elements unallocated, add them to the last batch + if remaining_elements > 0: + last_batch = self.particles_batch.pop() + last_batch.extend(input_list[len(batches) * self.batch_size:]) + self.particles_batch.append(last_batch) + + def optimize(self, history_dir=None, checkpoint_dir=None): """ Perform the MOPSO optimization process and return the Pareto front of non-dominated solutions. If `history_dir` is specified, the position and fitness of all particles @@ -364,24 +426,36 @@ def optimize(self, num_iterations=100, max_iter_no_improv=None): Returns: list: List of Particle objects representing the Pareto front of non-dominated solutions. """ - for _ in range(num_iterations): - optimization_output = self.objective.evaluate( - [particle.position for particle in self.particles]) - [particle.set_fitness(optimization_output[:, p_id]) - for p_id, particle in enumerate(self.particles)] + + for _ in range(self.num_iterations): + if self.optimization_mode == 'global': + optimization_output = [objective_function([particle.position for particle in batch]) + for objective_function in self.objective_functions + for batch in self.particles_batch] + with ProcessPoolExecutor(max_workers=self.num_batch) as executor: + futures = [executor.submit(self.process_batch, batch, optimization_output) + for batch, optimization_output in zip(self.particlesBatches, optimization_output)] + + new_batches = [] + for future in futures: + batch = future.result() + new_batches.append(batch) + self.particles_batch = new_batches FileManager.save_csv([np.concatenate([particle.position, np.ravel( - particle.fitness)]) for particle in self.particles], + particle.fitness)]) for batch in self.particles_batch for particle in batches], 'history/iteration' + str(self.iteration) + '.csv') self.update_pareto_front() - for particle in self.particles: - particle.update_velocity(self.pareto_front, - self.inertia_weight, - self.cognitive_coefficient, - self.social_coefficient) - particle.update_position(self.lower_bounds, self.upper_bounds) + for batch in self.particles_batch: + for particle in batch: + particle.update_velocity(self.pareto_front, + self.inertia_weight, + self.cognitive_coefficient, + self.social_coefficient) + particle.update_position( + self.lower_bounds, self.upper_bounds) self.iteration += 1 @@ -394,10 +468,11 @@ def update_pareto_front(self): """ Update the Pareto front of non-dominated solutions across all iterations. """ - particles = self.particles - if self.incremental_pareto: - particles = particles + self.pareto_front + all_particles = [ + particle for batch in self.particles_batch for particle in batch] + if self.incremental_pareto: + particles = all_particles + self.pareto_front self.pareto_front = [copy.deepcopy(particle) for particle in particles if not particle.is_dominated(particles)] @@ -413,10 +488,12 @@ def get_current_pareto_front(self): Returns: list: List of Particle objects representing the Pareto front. """ + all_particles = [ + particle for batch in self.particles_batch for particle in batch] pareto_front = [] - for particle in self.particles: + for particle in all_particles: dominated = False - for other_particle in self.particles: + for other_particle in all_particles: if np.all(particle.fitness >= other_particle.fitness) and \ np.any(particle.fitness > other_particle.fitness): dominated = True diff --git a/setup.py b/setup.py index 38b35fa..31100d5 100644 --- a/setup.py +++ b/setup.py @@ -5,4 +5,4 @@ version='0.0.1', packages=find_packages(include=['optimizer', 'optimizer.*']), install_requires=['scikit-learn','numpy','matplotlib','pandas'] -) \ No newline at end of file +) From 364da0f8cf48071057207cfbed91d8348de08ab7 Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Fri, 27 Oct 2023 12:19:42 +0200 Subject: [PATCH 02/21] fix indentation and syntax --- optimizer/mopso.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 41dda6c..167922b 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -314,11 +314,11 @@ def save_state(self): particle.velocity, particle.best_position, np.ravel(particle.best_fitness)]) - for particle in batch for batch in self.particles_batch], + for batch in self.particles_batch for particle in batch], 'checkpoint/individual_states.csv') FileManager.save_csv([np.concatenate([particle.position, np.ravel(particle.fitness)]) - for particle in batch for batch in self.particles_batch], + for batch in self.particles_batch for particle in batch], 'checkpoint/pareto_front.csv') def load_checkpoint(self): @@ -429,7 +429,7 @@ def optimize(self, history_dir=None, checkpoint_dir=None): for _ in range(self.num_iterations): if self.optimization_mode == 'global': - optimization_output = [objective_function([particle.position for particle in batch]) + optimization_output = [objective_function([particle.position for particle in batch]) for objective_function in self.objective_functions for batch in self.particles_batch] with ProcessPoolExecutor(max_workers=self.num_batch) as executor: From 3f4fbf3453ea7a8852e0ac743dbd348833bff872 Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Fri, 27 Oct 2023 15:32:40 +0200 Subject: [PATCH 03/21] working - multiprocess and batch splitting --- optimizer/mopso.py | 64 +++++++++++++++++++++++++++++------------ optimizer/termcolors.py | 11 +++++++ 2 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 optimizer/termcolors.py diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 167922b..f02ce28 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -23,6 +23,7 @@ from optimizer import Optimizer, FileManager from concurrent.futures import ProcessPoolExecutor import warnings +from .termcolors import bcolors class Particle: @@ -242,13 +243,12 @@ def __init__(self, self.batch_size = len(self.particles) // self.num_batch # Check if the division leaves some elements unallocated - remaining_elements = len(input_list) % self.num_batch + remaining_elements = len(self.particles) % self.num_batch if remaining_elements > 0: # Warn the user and suggest adjusting the number of particles or batches warning_message = ( - f"The specified number of batches ({self.num_batch}) does not evenly divide the number of particles ({len(input_list)}). " - f"Consider incrementing the number of particles by a multiple of {remaining_elements} or decreasing the number of batches." + f"{bcolors.WARNING}The specified number of batches ({self.num_batch}) does not evenly divide the number of particles ({len(self.particles)}).{bcolors.ENDC} " ) warnings.warn(warning_message) @@ -259,7 +259,7 @@ def __init__(self, # If the division leaves some elements unallocated, add them to the last batch if remaining_elements > 0: last_batch = self.particles_batch.pop() - last_batch.extend(input_list[len(batches) * self.batch_size:]) + last_batch.extend(self.particles[len(self.particles_batch) * self.batch_size:]) self.particles_batch.append(last_batch) self.iteration = 0 @@ -286,7 +286,7 @@ def save_attributes(self): 'upper_bounds': self.upper_bounds, 'num_particles': self.num_particles, 'num_params': self.num_params, - 'num_batch': slef.num_batch, + 'num_batch': self.num_batch, 'inertia_weight': self.inertia_weight, 'cognitive_coefficient': self.cognitive_coefficient, 'social_coefficient': self.social_coefficient, @@ -392,13 +392,12 @@ def load_checkpoint(self): self.batch_size = len(self.particles) // self.num_batch # Check if the division leaves some elements unallocated - remaining_elements = len(input_list) % self.num_batch + remaining_elements = len(self.particles) % self.num_batch if remaining_elements > 0: # Warn the user and suggest adjusting the number of particles or batches warning_message = ( - f"The specified number of batches ({self.num_batch}) does not evenly divide the number of particles ({len(input_list)}). " - f"Consider incrementing the number of particles by a multiple of {remaining_elements} or decreasing the number of batches." + f"{bcolors.WARNING}The specified number of batches ({self.num_batch}) does not evenly divide the number of particles ({len(self.particles)}).{bcolors.END} " ) warnings.warn(warning_message) @@ -409,10 +408,23 @@ def load_checkpoint(self): # If the division leaves some elements unallocated, add them to the last batch if remaining_elements > 0: last_batch = self.particles_batch.pop() - last_batch.extend(input_list[len(batches) * self.batch_size:]) + last_batch.extend(input_list[len(self.particles_batch) * self.batch_size:]) self.particles_batch.append(last_batch) - def optimize(self, history_dir=None, checkpoint_dir=None): + def process_batch(self, worker_id, batch, objective_functions): + # Launch a program for this batch using objective_function + print(f"Worker ID {worker_id}") + for objective_function in objective_functions: + optimization_output = objective_function([particle.position for particle in batch], worker_id) + for p_id, output in enumerate(optimization_output): + particle = batch[p_id] + if self.optimization_mode == 'individual': + particle.evaluate_fitness(self.objective_functions) + if self.optimization_mode == 'global': + particle.set_fitness(output) + return batch + + def optimize(self, num_iterations = 100, max_iter_no_improv = None): """ Perform the MOPSO optimization process and return the Pareto front of non-dominated solutions. If `history_dir` is specified, the position and fitness of all particles @@ -427,14 +439,13 @@ def optimize(self, history_dir=None, checkpoint_dir=None): list: List of Particle objects representing the Pareto front of non-dominated solutions. """ - for _ in range(self.num_iterations): - if self.optimization_mode == 'global': - optimization_output = [objective_function([particle.position for particle in batch]) - for objective_function in self.objective_functions - for batch in self.particles_batch] + for it in range(self.num_iterations): + print(f"Start itration {it}") with ProcessPoolExecutor(max_workers=self.num_batch) as executor: - futures = [executor.submit(self.process_batch, batch, optimization_output) - for batch, optimization_output in zip(self.particlesBatches, optimization_output)] + futures = [executor.submit( + self.process_batch, worker_id, batch, self.objective_functions) for worker_id, batch in enumerate(self.particles_batch)] + # futures = [executor.submit(self.process_batch, batch, optimization_output) + # for batch, optimization_output in zip(self.particles_batch, optimization_output)] new_batches = [] for future in futures: @@ -464,6 +475,21 @@ def optimize(self, history_dir=None, checkpoint_dir=None): return self.pareto_front + +# def process_batch(self, batch, optimization_output): +# print("Starting processing Batch") +# for p_id, particle in enumerate(batch): +# if self.optimization_mode == 'individual': +# particle.evaluate_fitness(self.objective_functions) +# if self.optimization_mode == 'global': +# print(optimization_output, len(optimization_output)) +# for output in optimization_output: +# print(output, len(output)) +# particle.set_fitness([output[p_id] +# for output in optimization_output]) +# return batch + + def update_pareto_front(self): """ Update the Pareto front of non-dominated solutions across all iterations. @@ -491,9 +517,9 @@ def get_current_pareto_front(self): all_particles = [ particle for batch in self.particles_batch for particle in batch] pareto_front = [] - for particle in all_particles: + for particle in all_particles: dominated = False - for other_particle in all_particles: + for other_particle in all_particles: if np.all(particle.fitness >= other_particle.fitness) and \ np.any(particle.fitness > other_particle.fitness): dominated = True diff --git a/optimizer/termcolors.py b/optimizer/termcolors.py new file mode 100644 index 0000000..2522604 --- /dev/null +++ b/optimizer/termcolors.py @@ -0,0 +1,11 @@ +class bcolors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + From a54dc9cc5c3d5979e2df3fbadffc4a4733ca91fa Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Fri, 27 Oct 2023 15:40:40 +0200 Subject: [PATCH 04/21] clean-up --- optimizer/mopso.py | 21 ++------------------- setup.py | 1 + 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index f02ce28..abab276 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -243,7 +243,7 @@ def __init__(self, self.batch_size = len(self.particles) // self.num_batch # Check if the division leaves some elements unallocated - remaining_elements = len(self.particles) % self.num_batch + remaining_elements = len(self.particles) % self.batch_size if remaining_elements > 0: # Warn the user and suggest adjusting the number of particles or batches @@ -392,7 +392,7 @@ def load_checkpoint(self): self.batch_size = len(self.particles) // self.num_batch # Check if the division leaves some elements unallocated - remaining_elements = len(self.particles) % self.num_batch + remaining_elements = len(self.particles) % self.batch_size if remaining_elements > 0: # Warn the user and suggest adjusting the number of particles or batches @@ -444,8 +444,6 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): with ProcessPoolExecutor(max_workers=self.num_batch) as executor: futures = [executor.submit( self.process_batch, worker_id, batch, self.objective_functions) for worker_id, batch in enumerate(self.particles_batch)] - # futures = [executor.submit(self.process_batch, batch, optimization_output) - # for batch, optimization_output in zip(self.particles_batch, optimization_output)] new_batches = [] for future in futures: @@ -475,21 +473,6 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): return self.pareto_front - -# def process_batch(self, batch, optimization_output): -# print("Starting processing Batch") -# for p_id, particle in enumerate(batch): -# if self.optimization_mode == 'individual': -# particle.evaluate_fitness(self.objective_functions) -# if self.optimization_mode == 'global': -# print(optimization_output, len(optimization_output)) -# for output in optimization_output: -# print(output, len(output)) -# particle.set_fitness([output[p_id] -# for output in optimization_output]) -# return batch - - def update_pareto_front(self): """ Update the Pareto front of non-dominated solutions across all iterations. diff --git a/setup.py b/setup.py index 31100d5..75c56cf 100644 --- a/setup.py +++ b/setup.py @@ -6,3 +6,4 @@ packages=find_packages(include=['optimizer', 'optimizer.*']), install_requires=['scikit-learn','numpy','matplotlib','pandas'] ) + From 4c051a1444d3b3af899785f56a30d2fdd9f89df1 Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Thu, 2 Nov 2023 12:10:08 +0100 Subject: [PATCH 05/21] buf fix: update particle in batch --- optimizer/mopso.py | 1 + 1 file changed, 1 insertion(+) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index abab276..e062520 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -422,6 +422,7 @@ def process_batch(self, worker_id, batch, objective_functions): particle.evaluate_fitness(self.objective_functions) if self.optimization_mode == 'global': particle.set_fitness(output) + batch[p_id] = particle return batch def optimize(self, num_iterations = 100, max_iter_no_improv = None): From 8c0b1ee7eda76fdb47ff140ee2ba7c139c385ba3 Mon Sep 17 00:00:00 2001 From: Simone Rossi Tisbeni Date: Fri, 3 Nov 2023 09:07:09 +0100 Subject: [PATCH 06/21] Save to file (#7) * First implementation of FileManager * Fix tests for File Manager * Fix examples * Changed module name to snake_case * Format and lint * Format and lint * Changed FileManager to static class * Delegate all IO to FileManager * Update example to previous commit --- optimizer/mopso.py | 1 + 1 file changed, 1 insertion(+) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index e062520..1b9f33c 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -24,6 +24,7 @@ from concurrent.futures import ProcessPoolExecutor import warnings from .termcolors import bcolors +from optimizer import Optimizer, FileManager class Particle: From bba7358645a7f8be1c5f520cf814b2322113f2f1 Mon Sep 17 00:00:00 2001 From: Simone Rossi Tisbeni Date: Thu, 2 Nov 2023 23:28:10 +0100 Subject: [PATCH 07/21] Set incremental pareto as an option --- optimizer/mopso.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 1b9f33c..690ec57 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -469,7 +469,7 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): self.lower_bounds, self.upper_bounds) self.iteration += 1 - + self.particles = [particle for particle in batch for batch in self.particles_batch] self.save_attributes() self.save_state() From c93af389471605ea39d1d47ba8b017352f2dc008 Mon Sep 17 00:00:00 2001 From: Simone Rossi Tisbeni Date: Thu, 2 Nov 2023 20:36:30 +0100 Subject: [PATCH 08/21] First implementation of split --- optimizer/objective.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/optimizer/objective.py b/optimizer/objective.py index 10afc6a..3625b5a 100644 --- a/optimizer/objective.py +++ b/optimizer/objective.py @@ -8,20 +8,19 @@ def __init__(self, objective_functions, num_objectives = None) -> None: else: self.num_objectives = num_objectives pass - + def evaluate(self, items): return np.array([objective_function([item for item in items]) for objective_function in self.objective_functions]) - def type(self): return self.__class__.__name__ class ElementWiseObjective(Objective): def __init__(self, objective_functions, num_objectives=None) -> None: super().__init__(objective_functions, num_objectives) - + def evaluate(self, items): return np.array([[obj_func(item) for item in items] for obj_func in self.objective_functions]) - + class BatchObjective(Objective): def __init__(self, objective_functions, num_objectives=None) -> None: - super().__init__(objective_functions, num_objectives) \ No newline at end of file + super().__init__(objective_functions, num_objectives) From 94326f3d52c90ffd6971a3cf3bec2d38ee77ab13 Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Fri, 27 Oct 2023 12:15:58 +0200 Subject: [PATCH 09/21] first draft of batches and multiprocessing --- optimizer/mopso.py | 58 +++++++++++++--------------------------------- 1 file changed, 16 insertions(+), 42 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 690ec57..b140c8e 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -24,7 +24,6 @@ from concurrent.futures import ProcessPoolExecutor import warnings from .termcolors import bcolors -from optimizer import Optimizer, FileManager class Particle: @@ -249,7 +248,7 @@ def __init__(self, if remaining_elements > 0: # Warn the user and suggest adjusting the number of particles or batches warning_message = ( - f"{bcolors.WARNING}The specified number of batches ({self.num_batch}) does not evenly divide the number of particles ({len(self.particles)}).{bcolors.ENDC} " + f"{bcolors.WARNING}The specified number of batches ({self.num_batch}) does not evenly divide the number of particles ({len(self.particles)}).{bcolors.ENDC}" ) warnings.warn(warning_message) @@ -383,42 +382,14 @@ def load_checkpoint(self): best_fitness=None) self.pareto_front.append(particle) - # restor batches - self.particles_batch = [] - if (num_batch == 1): - self.particles_batch.append(self.particles) - self.batch_size = len(self.particles) - else: - # Calculate the approximate batch size - self.batch_size = len(self.particles) // self.num_batch - - # Check if the division leaves some elements unallocated - remaining_elements = len(self.particles) % self.batch_size - - if remaining_elements > 0: - # Warn the user and suggest adjusting the number of particles or batches - warning_message = ( - f"{bcolors.WARNING}The specified number of batches ({self.num_batch}) does not evenly divide the number of particles ({len(self.particles)}).{bcolors.END} " - ) - warnings.warn(warning_message) - - # Use list comprehension to create batches - self.particles_batch = [self.particles[i:i + self.batch_size] - for i in range(0, len(self.particles), self.batch_size)] - - # If the division leaves some elements unallocated, add them to the last batch - if remaining_elements > 0: - last_batch = self.particles_batch.pop() - last_batch.extend(input_list[len(self.particles_batch) * self.batch_size:]) - self.particles_batch.append(last_batch) - - def process_batch(self, worker_id, batch, objective_functions): + def process_batch(self, worker_id, batch): # Launch a program for this batch using objective_function print(f"Worker ID {worker_id}") - for objective_function in objective_functions: - optimization_output = objective_function([particle.position for particle in batch], worker_id) - for p_id, output in enumerate(optimization_output): - particle = batch[p_id] + params = [particle.position for particle in batch] + + optimization_output = self.objective.evaluate(params, worker_id ) + for p_id, output in enumerate(optimization_output[0]): + particle = batch[p_id] if self.optimization_mode == 'individual': particle.evaluate_fitness(self.objective_functions) if self.optimization_mode == 'global': @@ -440,18 +411,21 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): Returns: list: List of Particle objects representing the Pareto front of non-dominated solutions. """ - - for it in range(self.num_iterations): - print(f"Start itration {it}") + for _ in range(self.num_iterations): with ProcessPoolExecutor(max_workers=self.num_batch) as executor: - futures = [executor.submit( - self.process_batch, worker_id, batch, self.objective_functions) for worker_id, batch in enumerate(self.particles_batch)] + futures = [executor.submit(self.process_batch, worker_id, batch) + for worker_id, batch in enumerate(self.particles_batch)] new_batches = [] for future in futures: batch = future.result() new_batches.append(batch) self.particles_batch = new_batches + save_particles = np.array([]) + for batch in self.particles_batch: + for particle in batch: + l = np.concatenate([particle.position, np.ravel(particle.fitness)]) + save_particles = np.append(save_particles, l) FileManager.save_csv([np.concatenate([particle.position, np.ravel( particle.fitness)]) for batch in self.particles_batch for particle in batches], @@ -469,7 +443,7 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): self.lower_bounds, self.upper_bounds) self.iteration += 1 - self.particles = [particle for particle in batch for batch in self.particles_batch] + self.save_attributes() self.save_state() From 8dc49e91c1f7c841d045f7503e6d8f6b8c66813b Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Mon, 6 Nov 2023 11:31:53 +0100 Subject: [PATCH 10/21] add worker id to Objective --- optimizer/objective.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/optimizer/objective.py b/optimizer/objective.py index 3625b5a..9250d7c 100644 --- a/optimizer/objective.py +++ b/optimizer/objective.py @@ -9,8 +9,9 @@ def __init__(self, objective_functions, num_objectives = None) -> None: self.num_objectives = num_objectives pass - def evaluate(self, items): - return np.array([objective_function([item for item in items]) for objective_function in self.objective_functions]) + def evaluate(self, items, worker_id=0): + return np.array([objective_function([item for item in items], worker_id) for objective_function in self.objective_functions]) + def type(self): return self.__class__.__name__ @@ -18,8 +19,8 @@ class ElementWiseObjective(Objective): def __init__(self, objective_functions, num_objectives=None) -> None: super().__init__(objective_functions, num_objectives) - def evaluate(self, items): - return np.array([[obj_func(item) for item in items] for obj_func in self.objective_functions]) + def evaluate(self, items, worker_id=0): + return np.array([[obj_func(item, worker_id) for item in items] for obj_func in self.objective_functions]) class BatchObjective(Objective): def __init__(self, objective_functions, num_objectives=None) -> None: From 09c4e91d71eb799b0543061855b316f7b1bcffd0 Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Mon, 6 Nov 2023 15:51:11 +0100 Subject: [PATCH 11/21] fix save state --- optimizer/mopso.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index b140c8e..6a9e08b 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -316,7 +316,7 @@ def save_state(self): np.ravel(particle.best_fitness)]) for batch in self.particles_batch for particle in batch], 'checkpoint/individual_states.csv') - + print(f"Saving Pareto Front {len(self.pareto_front)}") FileManager.save_csv([np.concatenate([particle.position, np.ravel(particle.fitness)]) for batch in self.particles_batch for particle in batch], 'checkpoint/pareto_front.csv') @@ -421,16 +421,16 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): batch = future.result() new_batches.append(batch) self.particles_batch = new_batches - save_particles = np.array([]) + save_particles = [] for batch in self.particles_batch: for particle in batch: l = np.concatenate([particle.position, np.ravel(particle.fitness)]) - save_particles = np.append(save_particles, l) - - FileManager.save_csv([np.concatenate([particle.position, np.ravel( - particle.fitness)]) for batch in self.particles_batch for particle in batches], + print(l) + save_particles.append(l) + FileManager.save_csv(save_particles, 'history/iteration' + str(self.iteration) + '.csv') + self.update_pareto_front() for batch in self.particles_batch: @@ -457,9 +457,13 @@ def update_pareto_front(self): particle for batch in self.particles_batch for particle in batch] if self.incremental_pareto: + all_particles = [ + particle for batch in self.particles_batch for particle in batch] + particles = all_particles + self.pareto_front self.pareto_front = [copy.deepcopy(particle) for particle in particles if not particle.is_dominated(particles)] + print(len(self.pareto_front)) crowding_distances = self.calculate_crowding_distance( self.pareto_front) From 3ddceaa00be93f07daef3f717552f5169dd2e64c Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Fri, 27 Oct 2023 12:15:58 +0200 Subject: [PATCH 12/21] first draft of batches and multiprocessing --- optimizer/mopso.py | 3 +++ setup.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 6a9e08b..f0d1642 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -25,6 +25,9 @@ import warnings from .termcolors import bcolors +from concurrent.futures import ProcessPoolExecutor +import warnings + class Particle: """ diff --git a/setup.py b/setup.py index 75c56cf..31100d5 100644 --- a/setup.py +++ b/setup.py @@ -6,4 +6,3 @@ packages=find_packages(include=['optimizer', 'optimizer.*']), install_requires=['scikit-learn','numpy','matplotlib','pandas'] ) - From 0cdd70bcbda73d5d482d8ab412ea810c09a24718 Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Fri, 27 Oct 2023 15:32:40 +0200 Subject: [PATCH 13/21] working - multiprocess and batch splitting --- optimizer/mopso.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index f0d1642..20684f1 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -25,9 +25,6 @@ import warnings from .termcolors import bcolors -from concurrent.futures import ProcessPoolExecutor -import warnings - class Particle: """ @@ -414,7 +411,7 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): Returns: list: List of Particle objects representing the Pareto front of non-dominated solutions. """ - for _ in range(self.num_iterations): + for _ in range(num_iterations): with ProcessPoolExecutor(max_workers=self.num_batch) as executor: futures = [executor.submit(self.process_batch, worker_id, batch) for worker_id, batch in enumerate(self.particles_batch)] @@ -452,6 +449,21 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): return self.pareto_front + +# def process_batch(self, batch, optimization_output): +# print("Starting processing Batch") +# for p_id, particle in enumerate(batch): +# if self.optimization_mode == 'individual': +# particle.evaluate_fitness(self.objective_functions) +# if self.optimization_mode == 'global': +# print(optimization_output, len(optimization_output)) +# for output in optimization_output: +# print(output, len(output)) +# particle.set_fitness([output[p_id] +# for output in optimization_output]) +# return batch + + def update_pareto_front(self): """ Update the Pareto front of non-dominated solutions across all iterations. From 615d9500d3d7035feab005500efc1a0865f0916a Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Fri, 27 Oct 2023 15:40:40 +0200 Subject: [PATCH 14/21] clean-up --- optimizer/mopso.py | 16 ---------------- setup.py | 1 + 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 20684f1..2a595ca 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -386,7 +386,6 @@ def process_batch(self, worker_id, batch): # Launch a program for this batch using objective_function print(f"Worker ID {worker_id}") params = [particle.position for particle in batch] - optimization_output = self.objective.evaluate(params, worker_id ) for p_id, output in enumerate(optimization_output[0]): particle = batch[p_id] @@ -449,21 +448,6 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): return self.pareto_front - -# def process_batch(self, batch, optimization_output): -# print("Starting processing Batch") -# for p_id, particle in enumerate(batch): -# if self.optimization_mode == 'individual': -# particle.evaluate_fitness(self.objective_functions) -# if self.optimization_mode == 'global': -# print(optimization_output, len(optimization_output)) -# for output in optimization_output: -# print(output, len(output)) -# particle.set_fitness([output[p_id] -# for output in optimization_output]) -# return batch - - def update_pareto_front(self): """ Update the Pareto front of non-dominated solutions across all iterations. diff --git a/setup.py b/setup.py index 31100d5..75c56cf 100644 --- a/setup.py +++ b/setup.py @@ -6,3 +6,4 @@ packages=find_packages(include=['optimizer', 'optimizer.*']), install_requires=['scikit-learn','numpy','matplotlib','pandas'] ) + From 3cfd458b87334625dc1f86009208b581d42a647e Mon Sep 17 00:00:00 2001 From: Simone Rossi Tisbeni Date: Fri, 3 Nov 2023 09:07:09 +0100 Subject: [PATCH 15/21] Save to file (#7) * First implementation of FileManager * Fix tests for File Manager * Fix examples * Changed module name to snake_case * Format and lint * Format and lint * Changed FileManager to static class * Delegate all IO to FileManager * Update example to previous commit --- optimizer/mopso.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 2a595ca..dea4150 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -24,6 +24,7 @@ from concurrent.futures import ProcessPoolExecutor import warnings from .termcolors import bcolors +from optimizer import Optimizer, FileManager class Particle: @@ -208,6 +209,11 @@ def __init__(self, return except FileNotFoundError as e: print("Checkpoint not found. Fallback to standard construction.") + + if num_objectives is None: + self.num_objectives = len(self.objective_functions) + else: + self.num_objectives = num_objectives self.num_particles = num_particles self.num_batch = num_batch self.num_params = len(lower_bounds) @@ -330,10 +336,8 @@ def load_checkpoint(self): num_additional_iterations: Number of additional iterations to run. """ # load saved data - pso_attributes = FileManager.load_json( - 'checkpoint/pso_attributes.json') - individual_states = FileManager.load_csv( - 'checkpoint/individual_states.csv') + pso_attributes = FileManager.load_json('checkpoint/pso_attributes.json') + individual_states = FileManager.load_csv('checkpoint/individual_states.csv') pareto_front = FileManager.load_csv('checkpoint/pareto_front.csv') # restore pso attributes From 5bc8ae5ec54c16774b85574ff13f329a1c2bbd2e Mon Sep 17 00:00:00 2001 From: Simone Rossi Tisbeni Date: Thu, 2 Nov 2023 23:21:50 +0100 Subject: [PATCH 16/21] Remove diversity coefficient. --- optimizer/mopso.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index dea4150..020add2 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -336,8 +336,10 @@ def load_checkpoint(self): num_additional_iterations: Number of additional iterations to run. """ # load saved data - pso_attributes = FileManager.load_json('checkpoint/pso_attributes.json') - individual_states = FileManager.load_csv('checkpoint/individual_states.csv') + pso_attributes = FileManager.load_json( + 'checkpoint/pso_attributes.json') + individual_states = FileManager.load_csv( + 'checkpoint/individual_states.csv') pareto_front = FileManager.load_csv('checkpoint/pareto_front.csv') # restore pso attributes From a400dbf2e543024f2cf035cb7a0c664d6112c1f4 Mon Sep 17 00:00:00 2001 From: Simone Rossi Tisbeni Date: Thu, 2 Nov 2023 23:28:10 +0100 Subject: [PATCH 17/21] Set incremental pareto as an option --- optimizer/mopso.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 020add2..51c45c2 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -448,7 +448,7 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): self.lower_bounds, self.upper_bounds) self.iteration += 1 - + self.particles = [particle for particle in batch for batch in self.particles_batch] self.save_attributes() self.save_state() From aaf5c3d5ba77fa971c1dd9556dde85520f210951 Mon Sep 17 00:00:00 2001 From: Simone Rossi Tisbeni Date: Thu, 2 Nov 2023 20:36:30 +0100 Subject: [PATCH 18/21] First implementation of split --- optimizer/mopso.py | 5 ----- optimizer/objective.py | 5 +---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 51c45c2..21f375c 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -209,11 +209,6 @@ def __init__(self, return except FileNotFoundError as e: print("Checkpoint not found. Fallback to standard construction.") - - if num_objectives is None: - self.num_objectives = len(self.objective_functions) - else: - self.num_objectives = num_objectives self.num_particles = num_particles self.num_batch = num_batch self.num_params = len(lower_bounds) diff --git a/optimizer/objective.py b/optimizer/objective.py index 9250d7c..25a9d2b 100644 --- a/optimizer/objective.py +++ b/optimizer/objective.py @@ -1,8 +1,5 @@ -import numpy as np - class Objective(): - def __init__(self, objective_functions, num_objectives = None) -> None: - self.objective_functions = objective_functions + def __init__(self, function_list, num_objectives = None) -> None: if num_objectives is None: self.num_objectives = len(self.objective_functions) else: From c54ecc7c7986c1b7f307ce681f61406979fe8535 Mon Sep 17 00:00:00 2001 From: Simone Rossi Tisbeni Date: Thu, 2 Nov 2023 23:09:13 +0100 Subject: [PATCH 19/21] Add evaluation function to objectives --- optimizer/objective.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/optimizer/objective.py b/optimizer/objective.py index 25a9d2b..9250d7c 100644 --- a/optimizer/objective.py +++ b/optimizer/objective.py @@ -1,5 +1,8 @@ +import numpy as np + class Objective(): - def __init__(self, function_list, num_objectives = None) -> None: + def __init__(self, objective_functions, num_objectives = None) -> None: + self.objective_functions = objective_functions if num_objectives is None: self.num_objectives = len(self.objective_functions) else: From b25cc944aa8de3ce85620cd911b947a4b8f69b45 Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Fri, 27 Oct 2023 12:15:58 +0200 Subject: [PATCH 20/21] first draft of batches and multiprocessing --- optimizer/mopso.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 21f375c..2a595ca 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -24,7 +24,6 @@ from concurrent.futures import ProcessPoolExecutor import warnings from .termcolors import bcolors -from optimizer import Optimizer, FileManager class Particle: @@ -443,7 +442,7 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): self.lower_bounds, self.upper_bounds) self.iteration += 1 - self.particles = [particle for particle in batch for batch in self.particles_batch] + self.save_attributes() self.save_state() From dbfd03a8123c6f08637011d12cfaf22386ec2d13 Mon Sep 17 00:00:00 2001 From: Wahid Redjeb Date: Tue, 7 Nov 2023 14:02:44 +0100 Subject: [PATCH 21/21] fix process batch and restore batches --- optimizer/mopso.py | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/optimizer/mopso.py b/optimizer/mopso.py index 2a595ca..69fdd4c 100644 --- a/optimizer/mopso.py +++ b/optimizer/mopso.py @@ -368,6 +368,35 @@ def load_checkpoint(self): individual_states[i][3*self.num_params:], dtype=float) ) self.particles.append(particle) + + #restore batches + self.particles_batch = [] + if (self.num_batch == 1): + self.particles_batch.append(self.particles) + self.batch_size = len(self.particles) + else: + # Calculate the approximate batch size + self.batch_size = len(self.particles) // self.num_batch + + # Check if the division leaves some elements unallocated + remaining_elements = len(self.particles) % self.batch_size + + if remaining_elements > 0: + # Warn the user and suggest adjusting the number of particles or batches + warning_message = ( + f"{bcolors.WARNING}The specified number of batches ({self.num_batch}) does not evenly divide the number of particles ({len(self.particles)}).{bcolors.ENDC}" + ) + warnings.warn(warning_message) + + # Use list comprehension to create batches + self.particles_batch = [self.particles[i:i + self.batch_size] + for i in range(0, len(self.particles), self.batch_size)] + + # If the division leaves some elements unallocated, add them to the last batch + if remaining_elements > 0: + last_batch = self.particles_batch.pop() + last_batch.extend(self.particles[len(self.particles_batch) * self.batch_size:]) + self.particles_batch.append(last_batch) # restore pareto front self.pareto_front = [] @@ -381,18 +410,15 @@ def load_checkpoint(self): best_position=None, best_fitness=None) self.pareto_front.append(particle) + def process_batch(self, worker_id, batch): # Launch a program for this batch using objective_function print(f"Worker ID {worker_id}") params = [particle.position for particle in batch] optimization_output = self.objective.evaluate(params, worker_id ) - for p_id, output in enumerate(optimization_output[0]): - particle = batch[p_id] - if self.optimization_mode == 'individual': - particle.evaluate_fitness(self.objective_functions) - if self.optimization_mode == 'global': - particle.set_fitness(output) + for p_id, particle in enumerate(batch): + particle.set_fitness(optimization_output[:,p_id]) batch[p_id] = particle return batch @@ -413,7 +439,7 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): for _ in range(num_iterations): with ProcessPoolExecutor(max_workers=self.num_batch) as executor: futures = [executor.submit(self.process_batch, worker_id, batch) - for worker_id, batch in enumerate(self.particles_batch)] + for worker_id, batch in enumerate(self.particles_batch )] new_batches = [] for future in futures: @@ -421,15 +447,16 @@ def optimize(self, num_iterations = 100, max_iter_no_improv = None): new_batches.append(batch) self.particles_batch = new_batches save_particles = [] + updated_particles = [] for batch in self.particles_batch: for particle in batch: l = np.concatenate([particle.position, np.ravel(particle.fitness)]) - print(l) save_particles.append(l) + updated_particles.append(particle) FileManager.save_csv(save_particles, 'history/iteration' + str(self.iteration) + '.csv') - + self.particles = updated_particles self.update_pareto_front() for batch in self.particles_batch: