From 9672ea780fc507c90e12b2affac91a61b5c3297a Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 11:35:20 +0200 Subject: [PATCH 001/139] Adjusted requirements.txt. --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8682d46c..8a70f1d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ cycler==0.11.0 -deepdiff==5.8.0 +deepdiff>=5.8.0 kiwisolver==1.4.2 matplotlib==3.2.1 mpi4py==3.0.3 numpy ordered-set==4.1.0 -pyparsing==3.0.7 +pyparsing>=3.0.7 python-dateutil==2.8.2 six==1.16.0 colorlog From 784802f8648b0aeecb87e367ccc67a6e14d3c1f4 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 2 May 2023 11:42:50 +0200 Subject: [PATCH 002/139] Initial commit --- LICENSE | 28 ++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 29 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..039d42f7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2023, Morridin + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index c28c48f7..cdbb3d21 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,4 @@ OpenMPI. + From ecb0830382e8ab6786a4279eb557449f602c3b5c Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 2 May 2023 11:43:38 +0200 Subject: [PATCH 003/139] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cdbb3d21..90e18dd5 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,4 @@ OpenMPI. + From b86d6c12ec7d279eedc69c323cf47f018edea56c Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 2 May 2023 11:45:41 +0200 Subject: [PATCH 004/139] Update README.md Added hyperlink to propulate --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 90e18dd5..19ad1c0b 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,4 @@ OpenMPI. + From b8e3032697025e78fc5a619edb3402d1125e1ccb Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 6 Jun 2023 15:38:25 +0200 Subject: [PATCH 005/139] Added Propulate as submodule. --- .gitmodules | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..6e9be902 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "propulate"] + path = propulate + url = https://github.com/Helmholtz-AI-Energy/propulate/ From 933c3fa1835c9248849346837afa7c7d9b35c2a8 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 6 Jun 2023 15:38:55 +0200 Subject: [PATCH 006/139] Implemented the Particle class. --- ap-pso/__init__.py | 0 ap-pso/particle.py | 103 +++++++++++++++++++++++++++++++++++++++++++++ ap-pso/swarm.py | 2 + 3 files changed, 105 insertions(+) create mode 100644 ap-pso/__init__.py create mode 100644 ap-pso/particle.py create mode 100644 ap-pso/swarm.py diff --git a/ap-pso/__init__.py b/ap-pso/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ap-pso/particle.py b/ap-pso/particle.py new file mode 100644 index 00000000..5d3bd5e4 --- /dev/null +++ b/ap-pso/particle.py @@ -0,0 +1,103 @@ +from typing import Callable + +import numpy as np +from numpy import ndarray + + +class Particle: + """ + This class resembles a single particle within the particle swarm solving tasks. + """ + + def __init__(self, dimension: tuple[int], target_fn: Callable, position: np.ndarray = None, + velocity: np.ndarray = None, seed: int = None): + """ + Constructor of Particle class. + + Parameters + ---------- + dimension : tuple[int] + A tuple containing information on the dimensionality of the search space. + All values that have something to do with the search space, are tested against this value + to ensure usability. Only values > 0 are allowed. + target_fn : callable[np.ndarray -> double] + The function given by this parameter is evaluated in each step of the algorithm + position : optional np.ndarray of shape `dimension` + The initial position of this Particle. Also, the initial value for p_best. + velocity : optional np.ndarray of shape `dimension` + The initial velocity of this Particle. + seed : optional int + The random number generator seed. If set, all values of position or velocity that are not set, will + be filled with random numbers generated via the given seed. + """ + assert all((x > 0 for x in dimension)) + assert all((position is None or position.shape == dimension, velocity is None or velocity.shape == dimension)) + + self._search_space_dim = dimension + self._target_fn = target_fn + + self._rng = None + if seed is not None: + self._rng = np.random.default_rng(seed) + + self._position = np.zeros(self._search_space_dim) + if position is not None: + self._position = position + elif self._rng is not None: + self._position = np.array(self._rng.random(self._search_space_dim)) + + self._velocity = np.zeros(self._search_space_dim) + if velocity is not None: + self._velocity = velocity + elif self._rng is not None: + self._velocity = np.array(self._rng.random(self._search_space_dim)) + + self._p_best = self._g_best = self._position + + def update(self, w_k: float, c_1: float, c_2: float, r_1: float, r_2: float) -> tuple[ndarray, ndarray]: + """ + This method calculates the position and velocity of the particle for the next time step and returns them. + :param w_k: particle inertia - how strong influences the current v the speed for the particle in next round + :param c_1: cognitive factor - how good is the particle's brain + :param c_2: social factor - how strong the particle believes in the swarm + :param r_1: random factor to c_1 + :param r_2: random factor to c_2 + :return: + """ + x: np.ndarray = self._position + self._velocity + v: np.ndarray = w_k * self._velocity + c_1 * r_1 * (self._p_best - self._position) + c_2 * r_2 * ( + self._g_best - self._position) + return x, v + + def eval(self) -> float: + """ + Returns the value of the particles target function on the particle's current position. + """ + return self._target_fn(self._position) + + @property + def position(self) -> np.ndarray: + return self._position + + @position.setter + def position(self, value: np.ndarray) -> None: + assert value.shape == self._search_space_dim + self._position = value + + @property + def velocity(self) -> np.ndarray: + return self._velocity + + @velocity.setter + def velocity(self, value: np.ndarray) -> None: + assert value.shape == self._search_space_dim + self._velocity = value + + @property + def g_best(self) -> np.ndarray: + return self._g_best + + @g_best.setter + def g_best(self, value: np.ndarray) -> None: + assert value.shape == self._search_space_dim + self._g_best = value diff --git a/ap-pso/swarm.py b/ap-pso/swarm.py new file mode 100644 index 00000000..f54c6b28 --- /dev/null +++ b/ap-pso/swarm.py @@ -0,0 +1,2 @@ +class Swarm: + pass From fca0f207e1d6083a84fe9c2013fab97438e0fd21 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 13 Jun 2023 13:23:30 +0200 Subject: [PATCH 007/139] Added some docstring. Moved the Islands equivalent away. --- ap-pso/swarm.py | 2 -- ap_pso/swarm.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) delete mode 100644 ap-pso/swarm.py create mode 100644 ap_pso/swarm.py diff --git a/ap-pso/swarm.py b/ap-pso/swarm.py deleted file mode 100644 index f54c6b28..00000000 --- a/ap-pso/swarm.py +++ /dev/null @@ -1,2 +0,0 @@ -class Swarm: - pass diff --git a/ap_pso/swarm.py b/ap_pso/swarm.py new file mode 100644 index 00000000..ac4cc5dd --- /dev/null +++ b/ap_pso/swarm.py @@ -0,0 +1,16 @@ +""" +This file contains the Swarm class, the technical equivalent to the Islands class of Propulate. +""" + + +class Swarm: + """ + This class resembles together with the propagators the user program interface of this project. + + Also, the Swarm is the instance, in which Particles are grouped together in order to optimize them. + """ + + def __init__(self): + pass + + From e85b72dc405fcd8690ba5798d3788e60882cffca Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 13 Jun 2023 13:24:21 +0200 Subject: [PATCH 008/139] Implemented the Islands equivalent as Optimizer Seemed to me a better-suited name for this object. --- ap_pso/optimizer.py | 214 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 ap_pso/optimizer.py diff --git a/ap_pso/optimizer.py b/ap_pso/optimizer.py new file mode 100644 index 00000000..58d7b891 --- /dev/null +++ b/ap_pso/optimizer.py @@ -0,0 +1,214 @@ +from pathlib import Path +from random import Random +from typing import Callable, Optional + +import numpy as np +from mpi4py import MPI + +from particle import Particle +from propagators import Propagator +from propulate.propulate import Propulator, PolliPropulator +from propulate.propulate.propagators import SelectMin, SelectMax + + +class Optimizer: + """ + This class is the executor of the algorithm and holds the main part of the API. + + To use the algorith, you create an Instance of this class and call its optimize (or evolve, for convenience) + function. Then everything is done by this framework. + """ + + def __init__(self, + loss_fn: Callable, + propagator: Propagator, + rng: Random, + generations: int = 0, + num_swarms: int = 1, + workers_per_swarm: list[int] = None, + migration_topology: np.ndarray = None, + migration_probability: float = 0.0, + emigration_propagator: Propagator = SelectMin, + immigration_propagator: Propagator = SelectMax, + pollination: bool = False, + checkpoint_path: Path = Path('./') + ): + """ + Constructor of Islands() class. + + Parameters + ---------- + loss_fn : callable + loss function to be minimized + propagator : propagators.Propagator + propagator to apply for optimization + generations : int + number of optimization iterations + num_swarms : int + number of separate, equally sized swarms (differences +-1 possible due to load balancing) + workers_per_swarm : list[int] + list with numbers of workers for each swarm (heterogeneous case) + migration_topology : numpy.ndarray + 2D matrix where each entry (i,j) specifies how many individuals are sent by isle i to isle j (int: absolute + number, float: relative fraction of population) + migration_probability : float + probability of migration after each generation + emigration_propagator : propagators.Propagator + emigration propagator, i.e., how to choose individuals for emigration that are sent to destination island. + Should be some kind of selection operator. + immigration_propagator : propagators.Propagator + immigration propagator, i.e., how to choose individuals on target isle to be replaced by immigrants. + Should be some kind of selection operator. + pollination : bool + If True, copies of emigrants are sent, otherwise, emigrants are removed from original isle. + checkpoint_path : pathlib.Path + Path where checkpoints are loaded from and stored. + """ + + if num_swarms < 1: + raise ValueError(f"Invalid number of Swarms: {num_swarms}") + assert migration_topology.shape == (num_swarms, num_swarms) + assert len(workers_per_swarm) == num_swarms + assert all((x > 0 for x in workers_per_swarm)) + assert 0.0 <= migration_probability <= 1.0 + + self.loss_fn = loss_fn + self.propagator = propagator + self.generations = generations + + # Set up MPI stuff + self.mpi_size = MPI.COMM_WORLD.size + self.mpi_rank = MPI.COMM_WORLD.rank + + # FIXME: Print log message/welcome screen! + + if workers_per_swarm is None: + # Homogeneous case + av_wps = self.mpi_size // num_swarms + leftover = self.mpi_size % num_swarms + workers_per_swarm = [av_wps + 1] * leftover + workers_per_swarm += [av_wps] * (num_swarms - leftover) + + # If this doesn't fit, someone did bad things. + given_workers: int = np.sum(workers_per_swarm) + + if given_workers != self.mpi_size: + # Erroneous inhomogeneous case + raise ValueError(f"Given total number of workers ({given_workers}) does not match MPI.COMM_WORLD.size, " + f"which is the number of available workers ({self.mpi_size}).") + # In correct inhomogeneous case, we don't have to do anything, as we got our worker distribution given as + # method parameter. + + # Set up communicators: + intra_color = np.concatenate([idx * np.ones(el, dtype=int) for idx, el in enumerate( + workers_per_swarm)]).reshape(-1) # color because of MPI's parameter naming TODO: Get rid of magic numbers! + + _, u_indices = np.unique(intra_color, return_index=True) + inter_color = np.zeros(self.mpi_size) + # FIXME: Print log message! + inter_color[u_indices] = 1 # TODO: Get rid of this magic number! + + intra_color = intra_color[self.mpi_rank] + inter_color = inter_color[self.mpi_rank] + + self.comm_intra = MPI.COMM_WORLD.Split(intra_color, self.mpi_rank) + self.comm_inter = MPI.COMM_WORLD.Split(inter_color, self.mpi_rank) # TODO: What exactly is this? + + self.swarm_index: Optional[int] = None + if self.comm_intra.rank == 0: # We're root of our intra-communicator + self.swarm_idx = self.comm_inter.rank # And now we set the index of our swarm to our rank in + # inter-communicator + swarm_idx = self.comm_intra.bcast(self.swarm_idx) + + if migration_topology is None: + migration_topology = np.ones((num_swarms, num_swarms), dtype=int) + np.fill_diagonal(migration_topology, 0) + + # FIXME: Print log message! + + self.migration_topology = migration_topology + self.migration_probability = migration_probability / self.comm_intra.size + + # FIXME: Print log messages! + + MPI.COMM_WORLD.barrier() + + self.propulator: Propulator + + if pollination: + self.propulator = Propulator( + self.loss_fn, + self.propagator, + swarm_idx, + self.comm_intra, + self.generations, + checkpoint_path, + self.migration_topology, + self.comm_inter, + self.migration_probability, + emigration_propagator, + u_indices, + workers_per_swarm, + rng + ) + else: + self.propulator = PolliPropulator( + self.loss_fn, + self.propagator, + swarm_idx, + self.comm_intra, + self.generations, + checkpoint_path, + self.migration_topology, + self.comm_inter, + self.migration_probability, + emigration_propagator, + immigration_propagator, + u_indices, + workers_per_swarm, + rng + ) + + # TODO: Outfactor all printing and stuff that is not CLEARLY debug stuff or error messages. See FixMe's + + def optimize(self, top_n: int = 3, out_file="summary.png", logging_interval: int = 10, debug: int = 1) -> \ + Optional[Particle]: + """ + This method runs the PSO algorithm on this swarm. + + Parameters + ---------- + top_n : int + number of best results to report + out_file : str + What's o' ever - what is this for a parameter? + logging_interval : int + Number of generations to generate some logging output + debug : bool + Debug verbosity level + """ + + self.propulator.propulate(logging_interval, debug) + if debug > -1: + best: Particle = self.propulator.summarize(top_n, out_file=out_file, DEBUG=debug) + return best + else: + return None + + def evolve(self, top_n: int = 3, out_file="summary.png", logging_interval: int = 10, debug: int = 1) -> \ + Optional[Particle]: + """ + This is a wrapper for the optimize method in order to ensure compatibility to Propulate. + + Parameters + ---------- + top_n : int + number of best results to report + out_file : str + What's o' ever - what is this for a parameter? + logging_interval : int + Number of generations to generate some logging output + debug : bool + Debug verbosity level + """ + return self.optimize(top_n, out_file, logging_interval, debug) From 797ae1666ba02c87a511bd95db326f751ef5ea3d Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 13 Jun 2023 13:24:57 +0200 Subject: [PATCH 009/139] Updated project/module contents. --- ap-pso/__init__.py | 0 ap_pso/__init__.py | 7 +++++++ 2 files changed, 7 insertions(+) delete mode 100644 ap-pso/__init__.py create mode 100644 ap_pso/__init__.py diff --git a/ap-pso/__init__.py b/ap-pso/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ap_pso/__init__.py b/ap_pso/__init__.py new file mode 100644 index 00000000..57dfb110 --- /dev/null +++ b/ap_pso/__init__.py @@ -0,0 +1,7 @@ +""" +This file is used to steer the visibility of classes within ap-pso project +""" +from optimizer import Optimizer +from utils import get_default_propagator + +__all__ = ["Optimizer", "propagators", "get_default_propagator"] \ No newline at end of file From 6a50c3be831e7980f2dbe51d266e4c7b9040648d Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 13 Jun 2023 13:25:56 +0200 Subject: [PATCH 010/139] Added Propagators to the implementation. Currently they don't do anything but existing. --- ap_pso/propagators/__init__.py | 9 +++++++ ap_pso/propagators/propagator.py | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 ap_pso/propagators/__init__.py create mode 100644 ap_pso/propagators/propagator.py diff --git a/ap_pso/propagators/__init__.py b/ap_pso/propagators/__init__.py new file mode 100644 index 00000000..0234c175 --- /dev/null +++ b/ap_pso/propagators/__init__.py @@ -0,0 +1,9 @@ +""" +This package holds all Propagator subclasses including the Propagator itself. +""" + +__all__ = ["Propagator", "SelectMin", "SelectMax"] + +from propulate.propulate.propagators import SelectMin, SelectMax + +from propagator import Propagator diff --git a/ap_pso/propagators/propagator.py b/ap_pso/propagators/propagator.py new file mode 100644 index 00000000..a43d3f45 --- /dev/null +++ b/ap_pso/propagators/propagator.py @@ -0,0 +1,44 @@ +""" +This file contains the 'abstract base class' for all propagators of this project. +""" +from random import Random + +from particle import Particle + + +class Propagator: + """ + Abstract base class for all propagators, i.e., evolutionary operators, in Propulate. + + Take a collection of individuals and use them to breed a new collection of individuals. + """ + + def __init__(self, parents: int = 0, offspring: int = 0, rng: Random = None): + """ + Constructor of Propagator class. + + Parameters + ---------- + parents : int + number of input individuals (-1 for any) + offspring : int + number of output individuals + rng : random.Random() + random number generator + """ + self.parents = parents + self.rng = rng + self.offspring = offspring + if offspring == 0: + raise ValueError("Propagator has to sire more than 0 offspring.") + + def __call__(self, particles: list[Particle]): + """ + Apply propagator (not implemented!). + + Parameters + ---------- + particles: propulate.population.Individual + individuals the propagator is applied to + """ + raise NotImplementedError() From e70be3c4e401265e2f93a1ebddf686f35b85b043 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 13 Jun 2023 13:26:43 +0200 Subject: [PATCH 011/139] Renamed the source code folder to suit it to Python's demands. --- {ap-pso => ap_pso}/particle.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {ap-pso => ap_pso}/particle.py (100%) diff --git a/ap-pso/particle.py b/ap_pso/particle.py similarity index 100% rename from ap-pso/particle.py rename to ap_pso/particle.py From 57badc37f0e2623f8e5258ba1145d4b2b8fdad74 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 13 Jun 2023 13:27:14 +0200 Subject: [PATCH 012/139] Added a dummy function for get_default_propagator(...) --- ap_pso/utils.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 ap_pso/utils.py diff --git a/ap_pso/utils.py b/ap_pso/utils.py new file mode 100644 index 00000000..6a3f5980 --- /dev/null +++ b/ap_pso/utils.py @@ -0,0 +1,30 @@ +""" +This file contains some random util functions, as, for example, get_default_propagator +""" +from random import Random + + +def get_default_propagator(pop_size: int, limits: dict, mate_prob: float, mut_prob: float, random_prob: float, + sigma_factor: float = 0.05, rng: Random = None): + """ + Returns a generic, but working propagator to use on Swarm objects in order to update the particles. + + + Parameters + ---------- + pop_size : int + number of individuals in breeding population + limits : dict + mate_prob : float + uniform-crossover probability + mut_prob : float + point-mutation probability + random_prob : float + random-initialization probability + sigma_factor : float + scaling factor for obtaining std from search-space boundaries for interval mutation + rng : random.Random() + random number generator + """ + _ = (pop_size, limits, mate_prob, mut_prob, random_prob, sigma_factor, rng) + pass From eeb0fde935dac1d877a55cba51afb39c7abf8e1a Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 27 Jun 2023 11:21:05 +0200 Subject: [PATCH 013/139] Modified .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2da053c5..ade5584f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ *.py[cod] *.so *.cfg -!.isort.cfg +!propulate/.isort.cfg !setup.cfg *.orig *.log @@ -57,3 +57,5 @@ MNIST scripts/*.png voucher_propulate.txt + +.gitignore From 4fb01a8a414780b0aac07bff0adab7b634a303d7 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 27 Jun 2023 13:23:56 +0200 Subject: [PATCH 014/139] Implemented some features of swarm class --- ap_pso/swarm.py | 60 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/ap_pso/swarm.py b/ap_pso/swarm.py index ac4cc5dd..c4e9bf62 100644 --- a/ap_pso/swarm.py +++ b/ap_pso/swarm.py @@ -1,16 +1,68 @@ """ This file contains the Swarm class, the technical equivalent to the Islands class of Propulate. """ +from mpi4py import MPI + +from ap_pso.particle import Particle +from ap_pso.propagators import Propagator +from ap_pso.utils import ExtendedPosition, TELL_TAG class Swarm: """ - This class resembles together with the propagators the user program interface of this project. + The swarm contains the particles. + + It governs, what the particles do, evaluates them and updates them. + + The particles, to be fair don't deserve to be classes at the moment, + as they will lose all their methods quite soon to the swarm. - Also, the Swarm is the instance, in which Particles are grouped together in order to optimize them. + Also, this class handles the internal MPI stuff, especially regarding communication between single workers within + one swarm so that everybody is always up to date. """ - def __init__(self): - pass + def __init__(self, num_workers: int, rank: int, propagator: Propagator, communicator: MPI.Comm): + self.particles: list[Particle] = [] + self.propagator: Propagator = propagator + self.swarm_best: ExtendedPosition = None + self.archive: list[list[Particle]] = [] * num_workers + self.rank: int = rank + self.communicator = communicator + self.size = num_workers + + # Create "randomly" initialised particles! + + # Communicate! + + def update(self): + """ + This function runs an update on the worker's particle. + + The update is performed by the following steps: + - First, a new particle with the stats of the old one plus one movement step update is created. + - Then, the old particle is moved into archive. + - In place of the old particle then the newly created is put. + - At last, the new particle is communicated. + """ + old_p = self.particles[self.rank] + new_p = self.propagator(old_p) + self.archive[self.rank].append(old_p) + self.particles[self.rank] = new_p + + def _communicate_update(self): + for i in range(self.size): + if i != self.rank: + self.communicator.send(self.particles[self.rank], i, TELL_TAG) + + while True: + status = MPI.Status() + if not self.communicator.iprobe(tag=TELL_TAG, status=status): + break + source = status.Get_source() + incoming_p = self.communicator.recv(source=source, tag=TELL_TAG) + self.archive[source].append(self.particles[source]) + self.particles[source] = incoming_p + + From 3187424a088a74d0662d47f967771489d01106f2 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 27 Jun 2023 13:24:30 +0200 Subject: [PATCH 015/139] Fitted Propagator.call to current needs. --- ap_pso/propagators/propagator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ap_pso/propagators/propagator.py b/ap_pso/propagators/propagator.py index a43d3f45..d731d7d4 100644 --- a/ap_pso/propagators/propagator.py +++ b/ap_pso/propagators/propagator.py @@ -32,7 +32,7 @@ def __init__(self, parents: int = 0, offspring: int = 0, rng: Random = None): if offspring == 0: raise ValueError("Propagator has to sire more than 0 offspring.") - def __call__(self, particles: list[Particle]): + def __call__(self, particle: Particle): """ Apply propagator (not implemented!). From 51b7ea7261bf1b2cf560a022dc2555cd02e61729 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 27 Jun 2023 13:25:20 +0200 Subject: [PATCH 016/139] Added some misc stuff. --- ap_pso/utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ap_pso/utils.py b/ap_pso/utils.py index 6a3f5980..c24004bb 100644 --- a/ap_pso/utils.py +++ b/ap_pso/utils.py @@ -28,3 +28,13 @@ def get_default_propagator(pop_size: int, limits: dict, mate_prob: float, mut_pr """ _ = (pop_size, limits, mate_prob, mut_prob, random_prob, sigma_factor, rng) pass + + +class ExtendedPosition: + + def __init__(self, position: np.ndarray, loss: float): + self.position = position + self.loss = loss + + +TELL_TAG = 0 From dbc6c2c6ca511779abdd32ea0dfba93b0fe5cb5c Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 27 Jun 2023 13:37:56 +0200 Subject: [PATCH 017/139] Added comparator function for extended position --- ap_pso/utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ap_pso/utils.py b/ap_pso/utils.py index c24004bb..86253ff8 100644 --- a/ap_pso/utils.py +++ b/ap_pso/utils.py @@ -3,6 +3,8 @@ """ from random import Random +import numpy as np + def get_default_propagator(pop_size: int, limits: dict, mate_prob: float, mut_prob: float, random_prob: float, sigma_factor: float = 0.05, rng: Random = None): @@ -36,5 +38,10 @@ def __init__(self, position: np.ndarray, loss: float): self.position = position self.loss = loss + def __lt__(self, other): + if not isinstance(other, self.__class__): + return NotImplemented + return self.loss < other.loss + TELL_TAG = 0 From 1e7b8559a4597f8c09a7123406fc6fcae2a346dc Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 27 Jun 2023 13:47:56 +0200 Subject: [PATCH 018/139] subtle changes to propagator base class --- ap_pso/propagators/propagator.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/ap_pso/propagators/propagator.py b/ap_pso/propagators/propagator.py index d731d7d4..ca33b9b7 100644 --- a/ap_pso/propagators/propagator.py +++ b/ap_pso/propagators/propagator.py @@ -2,8 +2,11 @@ This file contains the 'abstract base class' for all propagators of this project. """ from random import Random +from typing import Callable -from particle import Particle +import numpy as np + +from ap_pso.particle import Particle class Propagator: @@ -13,24 +16,16 @@ class Propagator: Take a collection of individuals and use them to breed a new collection of individuals. """ - def __init__(self, parents: int = 0, offspring: int = 0, rng: Random = None): + def __init__(self, loss_fn: Callable[[np.ndarray], float]): """ Constructor of Propagator class. Parameters ---------- - parents : int - number of input individuals (-1 for any) - offspring : int - number of output individuals - rng : random.Random() - random number generator + loss_fn: Callable + The function to be optimized by the particles. Should take a numpy array and return a float. """ - self.parents = parents - self.rng = rng - self.offspring = offspring - if offspring == 0: - raise ValueError("Propagator has to sire more than 0 offspring.") + self.loss_fn = loss_fn def __call__(self, particle: Particle): """ @@ -38,7 +33,8 @@ def __call__(self, particle: Particle): Parameters ---------- - particles: propulate.population.Individual - individuals the propagator is applied to + particle: Particle + The particle on which the propagator shall perform a positional update. """ raise NotImplementedError() + From 3e77a89f9320f6315c62d7dd17e2ab620e2f327f Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 27 Jun 2023 14:08:03 +0200 Subject: [PATCH 019/139] Brought Swarm class to something I consider quite a good state. --- ap_pso/particle.py | 103 ++++++--------------------------------------- ap_pso/swarm.py | 56 +++++++++++++++++++----- 2 files changed, 58 insertions(+), 101 deletions(-) diff --git a/ap_pso/particle.py b/ap_pso/particle.py index 5d3bd5e4..c178aa80 100644 --- a/ap_pso/particle.py +++ b/ap_pso/particle.py @@ -3,101 +3,24 @@ import numpy as np from numpy import ndarray +from ap_pso.utils import ExtendedPosition + class Particle: """ This class resembles a single particle within the particle swarm solving tasks. """ - def __init__(self, dimension: tuple[int], target_fn: Callable, position: np.ndarray = None, - velocity: np.ndarray = None, seed: int = None): - """ - Constructor of Particle class. - - Parameters - ---------- - dimension : tuple[int] - A tuple containing information on the dimensionality of the search space. - All values that have something to do with the search space, are tested against this value - to ensure usability. Only values > 0 are allowed. - target_fn : callable[np.ndarray -> double] - The function given by this parameter is evaluated in each step of the algorithm - position : optional np.ndarray of shape `dimension` - The initial position of this Particle. Also, the initial value for p_best. - velocity : optional np.ndarray of shape `dimension` - The initial velocity of this Particle. - seed : optional int - The random number generator seed. If set, all values of position or velocity that are not set, will - be filled with random numbers generated via the given seed. - """ - assert all((x > 0 for x in dimension)) - assert all((position is None or position.shape == dimension, velocity is None or velocity.shape == dimension)) - - self._search_space_dim = dimension - self._target_fn = target_fn - - self._rng = None - if seed is not None: - self._rng = np.random.default_rng(seed) - - self._position = np.zeros(self._search_space_dim) - if position is not None: - self._position = position - elif self._rng is not None: - self._position = np.array(self._rng.random(self._search_space_dim)) - - self._velocity = np.zeros(self._search_space_dim) - if velocity is not None: - self._velocity = velocity - elif self._rng is not None: - self._velocity = np.array(self._rng.random(self._search_space_dim)) - - self._p_best = self._g_best = self._position - - def update(self, w_k: float, c_1: float, c_2: float, r_1: float, r_2: float) -> tuple[ndarray, ndarray]: - """ - This method calculates the position and velocity of the particle for the next time step and returns them. - :param w_k: particle inertia - how strong influences the current v the speed for the particle in next round - :param c_1: cognitive factor - how good is the particle's brain - :param c_2: social factor - how strong the particle believes in the swarm - :param r_1: random factor to c_1 - :param r_2: random factor to c_2 - :return: - """ - x: np.ndarray = self._position + self._velocity - v: np.ndarray = w_k * self._velocity + c_1 * r_1 * (self._p_best - self._position) + c_2 * r_2 * ( - self._g_best - self._position) - return x, v - - def eval(self) -> float: - """ - Returns the value of the particles target function on the particle's current position. - """ - return self._target_fn(self._position) - - @property - def position(self) -> np.ndarray: - return self._position - - @position.setter - def position(self, value: np.ndarray) -> None: - assert value.shape == self._search_space_dim - self._position = value - - @property - def velocity(self) -> np.ndarray: - return self._velocity - - @velocity.setter - def velocity(self, value: np.ndarray) -> None: - assert value.shape == self._search_space_dim - self._velocity = value + def __init__(self, position: np.ndarray, velocity: np.ndarray): + assert position.shape == velocity.shape - @property - def g_best(self) -> np.ndarray: - return self._g_best + self.position = position + self.velocity = velocity + self.loss: float = None + self.p_best: ExtendedPosition = None - @g_best.setter - def g_best(self, value: np.ndarray) -> None: - assert value.shape == self._search_space_dim - self._g_best = value + def update_p_best(self) -> None: + if self.loss is None: + return + if self.p_best is None or self.loss < self.p_best.loss: + self.p_best = ExtendedPosition(self.position, self.loss) diff --git a/ap_pso/swarm.py b/ap_pso/swarm.py index c4e9bf62..933d8163 100644 --- a/ap_pso/swarm.py +++ b/ap_pso/swarm.py @@ -1,6 +1,8 @@ """ This file contains the Swarm class, the technical equivalent to the Islands class of Propulate. """ +from typing import Optional + from mpi4py import MPI from ap_pso.particle import Particle @@ -21,24 +23,36 @@ class Swarm: one swarm so that everybody is always up to date. """ - def __init__(self, num_workers: int, rank: int, propagator: Propagator, communicator: MPI.Comm): - self.particles: list[Particle] = [] + def __init__(self, propagator: Propagator, communicator: MPI.Comm): + """ + Swarm constructor. + + :param propagator: The propagator to be used to update and evaluate the particles within this swarm + :param communicator: The communicator for this swarm's internal communication. + """ self.propagator: Propagator = propagator - self.swarm_best: ExtendedPosition = None - self.archive: list[list[Particle]] = [] * num_workers - self.rank: int = rank self.communicator = communicator - self.size = num_workers + + self.rank: int = communicator.rank + self.size: int = communicator.size + + self.particles: list[Particle] = [] + self.archive: list[list[Particle]] = [] * self.size + + self.swarm_best: Optional[ExtendedPosition] = None # Create "randomly" initialised particles! # Communicate! - def update(self): + def update(self) -> None: """ - This function runs an update on the worker's particle. + This method runs an update on the particles of the swarm. However, only the particle matching to the worker, + on which the swarm object is lying, is updated (usually, multiple similar swarm objects exist at the same time + within one communicator, all resembling the same swarm. This is a side effect of playing with MPI). The update is performed by the following steps: + - First, a new particle with the stats of the old one plus one movement step update is created. - Then, the old particle is moved into archive. - In place of the old particle then the newly created is put. @@ -49,7 +63,12 @@ def update(self): self.archive[self.rank].append(old_p) self.particles[self.rank] = new_p - def _communicate_update(self): + def _communicate_update(self) -> None: + """ + Private method to handle the swarm's internal updating communication. + + At the same time, it performs updates on the swarm-global best value. + """ for i in range(self.size): if i != self.rank: self.communicator.send(self.particles[self.rank], i, TELL_TAG) @@ -59,10 +78,25 @@ def _communicate_update(self): if not self.communicator.iprobe(tag=TELL_TAG, status=status): break source = status.Get_source() - incoming_p = self.communicator.recv(source=source, tag=TELL_TAG) + incoming_p: Particle = self.communicator.recv(source=source, tag=TELL_TAG) self.archive[source].append(self.particles[source]) self.particles[source] = incoming_p + if incoming_p.p_best is not None and (self.swarm_best is None or incoming_p.p_best < self.swarm_best): + self.swarm_best = incoming_p.p_best + def evaluate_particle(self) -> None: + """ + This method calls the swarm's propagator's loss function + on the particles and thus evaluates their fitness. + The method always only considers the particle matching the rank of + the worker within the swarm's internal communicator. - + After evaluation, the method also starts a communication round in + order to update the rest of the workers on the swarm. + """ + p: Particle = self.particles[self.rank] + p.loss = self.propagator.loss_fn(p.position) + p.update_p_best() + self.particles[self.rank] = p + self._communicate_update() From b847ea6001a2143b3f139d5bc2b5693b444fe14d Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 4 Jul 2023 15:20:47 +0200 Subject: [PATCH 020/139] Reset project to be able to integrate the propulate fork --- LICENSE | 28 ---- ap_pso/__init__.py | 7 - ap_pso/optimizer.py | 214 ------------------------------- ap_pso/particle.py | 26 ---- ap_pso/propagators/__init__.py | 9 -- ap_pso/propagators/propagator.py | 40 ------ ap_pso/swarm.py | 102 --------------- ap_pso/utils.py | 47 ------- 8 files changed, 473 deletions(-) delete mode 100644 LICENSE delete mode 100644 ap_pso/__init__.py delete mode 100644 ap_pso/optimizer.py delete mode 100644 ap_pso/particle.py delete mode 100644 ap_pso/propagators/__init__.py delete mode 100644 ap_pso/propagators/propagator.py delete mode 100644 ap_pso/swarm.py delete mode 100644 ap_pso/utils.py diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 039d42f7..00000000 --- a/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2023, Morridin - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ap_pso/__init__.py b/ap_pso/__init__.py deleted file mode 100644 index 57dfb110..00000000 --- a/ap_pso/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -This file is used to steer the visibility of classes within ap-pso project -""" -from optimizer import Optimizer -from utils import get_default_propagator - -__all__ = ["Optimizer", "propagators", "get_default_propagator"] \ No newline at end of file diff --git a/ap_pso/optimizer.py b/ap_pso/optimizer.py deleted file mode 100644 index 58d7b891..00000000 --- a/ap_pso/optimizer.py +++ /dev/null @@ -1,214 +0,0 @@ -from pathlib import Path -from random import Random -from typing import Callable, Optional - -import numpy as np -from mpi4py import MPI - -from particle import Particle -from propagators import Propagator -from propulate.propulate import Propulator, PolliPropulator -from propulate.propulate.propagators import SelectMin, SelectMax - - -class Optimizer: - """ - This class is the executor of the algorithm and holds the main part of the API. - - To use the algorith, you create an Instance of this class and call its optimize (or evolve, for convenience) - function. Then everything is done by this framework. - """ - - def __init__(self, - loss_fn: Callable, - propagator: Propagator, - rng: Random, - generations: int = 0, - num_swarms: int = 1, - workers_per_swarm: list[int] = None, - migration_topology: np.ndarray = None, - migration_probability: float = 0.0, - emigration_propagator: Propagator = SelectMin, - immigration_propagator: Propagator = SelectMax, - pollination: bool = False, - checkpoint_path: Path = Path('./') - ): - """ - Constructor of Islands() class. - - Parameters - ---------- - loss_fn : callable - loss function to be minimized - propagator : propagators.Propagator - propagator to apply for optimization - generations : int - number of optimization iterations - num_swarms : int - number of separate, equally sized swarms (differences +-1 possible due to load balancing) - workers_per_swarm : list[int] - list with numbers of workers for each swarm (heterogeneous case) - migration_topology : numpy.ndarray - 2D matrix where each entry (i,j) specifies how many individuals are sent by isle i to isle j (int: absolute - number, float: relative fraction of population) - migration_probability : float - probability of migration after each generation - emigration_propagator : propagators.Propagator - emigration propagator, i.e., how to choose individuals for emigration that are sent to destination island. - Should be some kind of selection operator. - immigration_propagator : propagators.Propagator - immigration propagator, i.e., how to choose individuals on target isle to be replaced by immigrants. - Should be some kind of selection operator. - pollination : bool - If True, copies of emigrants are sent, otherwise, emigrants are removed from original isle. - checkpoint_path : pathlib.Path - Path where checkpoints are loaded from and stored. - """ - - if num_swarms < 1: - raise ValueError(f"Invalid number of Swarms: {num_swarms}") - assert migration_topology.shape == (num_swarms, num_swarms) - assert len(workers_per_swarm) == num_swarms - assert all((x > 0 for x in workers_per_swarm)) - assert 0.0 <= migration_probability <= 1.0 - - self.loss_fn = loss_fn - self.propagator = propagator - self.generations = generations - - # Set up MPI stuff - self.mpi_size = MPI.COMM_WORLD.size - self.mpi_rank = MPI.COMM_WORLD.rank - - # FIXME: Print log message/welcome screen! - - if workers_per_swarm is None: - # Homogeneous case - av_wps = self.mpi_size // num_swarms - leftover = self.mpi_size % num_swarms - workers_per_swarm = [av_wps + 1] * leftover - workers_per_swarm += [av_wps] * (num_swarms - leftover) - - # If this doesn't fit, someone did bad things. - given_workers: int = np.sum(workers_per_swarm) - - if given_workers != self.mpi_size: - # Erroneous inhomogeneous case - raise ValueError(f"Given total number of workers ({given_workers}) does not match MPI.COMM_WORLD.size, " - f"which is the number of available workers ({self.mpi_size}).") - # In correct inhomogeneous case, we don't have to do anything, as we got our worker distribution given as - # method parameter. - - # Set up communicators: - intra_color = np.concatenate([idx * np.ones(el, dtype=int) for idx, el in enumerate( - workers_per_swarm)]).reshape(-1) # color because of MPI's parameter naming TODO: Get rid of magic numbers! - - _, u_indices = np.unique(intra_color, return_index=True) - inter_color = np.zeros(self.mpi_size) - # FIXME: Print log message! - inter_color[u_indices] = 1 # TODO: Get rid of this magic number! - - intra_color = intra_color[self.mpi_rank] - inter_color = inter_color[self.mpi_rank] - - self.comm_intra = MPI.COMM_WORLD.Split(intra_color, self.mpi_rank) - self.comm_inter = MPI.COMM_WORLD.Split(inter_color, self.mpi_rank) # TODO: What exactly is this? - - self.swarm_index: Optional[int] = None - if self.comm_intra.rank == 0: # We're root of our intra-communicator - self.swarm_idx = self.comm_inter.rank # And now we set the index of our swarm to our rank in - # inter-communicator - swarm_idx = self.comm_intra.bcast(self.swarm_idx) - - if migration_topology is None: - migration_topology = np.ones((num_swarms, num_swarms), dtype=int) - np.fill_diagonal(migration_topology, 0) - - # FIXME: Print log message! - - self.migration_topology = migration_topology - self.migration_probability = migration_probability / self.comm_intra.size - - # FIXME: Print log messages! - - MPI.COMM_WORLD.barrier() - - self.propulator: Propulator - - if pollination: - self.propulator = Propulator( - self.loss_fn, - self.propagator, - swarm_idx, - self.comm_intra, - self.generations, - checkpoint_path, - self.migration_topology, - self.comm_inter, - self.migration_probability, - emigration_propagator, - u_indices, - workers_per_swarm, - rng - ) - else: - self.propulator = PolliPropulator( - self.loss_fn, - self.propagator, - swarm_idx, - self.comm_intra, - self.generations, - checkpoint_path, - self.migration_topology, - self.comm_inter, - self.migration_probability, - emigration_propagator, - immigration_propagator, - u_indices, - workers_per_swarm, - rng - ) - - # TODO: Outfactor all printing and stuff that is not CLEARLY debug stuff or error messages. See FixMe's - - def optimize(self, top_n: int = 3, out_file="summary.png", logging_interval: int = 10, debug: int = 1) -> \ - Optional[Particle]: - """ - This method runs the PSO algorithm on this swarm. - - Parameters - ---------- - top_n : int - number of best results to report - out_file : str - What's o' ever - what is this for a parameter? - logging_interval : int - Number of generations to generate some logging output - debug : bool - Debug verbosity level - """ - - self.propulator.propulate(logging_interval, debug) - if debug > -1: - best: Particle = self.propulator.summarize(top_n, out_file=out_file, DEBUG=debug) - return best - else: - return None - - def evolve(self, top_n: int = 3, out_file="summary.png", logging_interval: int = 10, debug: int = 1) -> \ - Optional[Particle]: - """ - This is a wrapper for the optimize method in order to ensure compatibility to Propulate. - - Parameters - ---------- - top_n : int - number of best results to report - out_file : str - What's o' ever - what is this for a parameter? - logging_interval : int - Number of generations to generate some logging output - debug : bool - Debug verbosity level - """ - return self.optimize(top_n, out_file, logging_interval, debug) diff --git a/ap_pso/particle.py b/ap_pso/particle.py deleted file mode 100644 index c178aa80..00000000 --- a/ap_pso/particle.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Callable - -import numpy as np -from numpy import ndarray - -from ap_pso.utils import ExtendedPosition - - -class Particle: - """ - This class resembles a single particle within the particle swarm solving tasks. - """ - - def __init__(self, position: np.ndarray, velocity: np.ndarray): - assert position.shape == velocity.shape - - self.position = position - self.velocity = velocity - self.loss: float = None - self.p_best: ExtendedPosition = None - - def update_p_best(self) -> None: - if self.loss is None: - return - if self.p_best is None or self.loss < self.p_best.loss: - self.p_best = ExtendedPosition(self.position, self.loss) diff --git a/ap_pso/propagators/__init__.py b/ap_pso/propagators/__init__.py deleted file mode 100644 index 0234c175..00000000 --- a/ap_pso/propagators/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -This package holds all Propagator subclasses including the Propagator itself. -""" - -__all__ = ["Propagator", "SelectMin", "SelectMax"] - -from propulate.propulate.propagators import SelectMin, SelectMax - -from propagator import Propagator diff --git a/ap_pso/propagators/propagator.py b/ap_pso/propagators/propagator.py deleted file mode 100644 index ca33b9b7..00000000 --- a/ap_pso/propagators/propagator.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -This file contains the 'abstract base class' for all propagators of this project. -""" -from random import Random -from typing import Callable - -import numpy as np - -from ap_pso.particle import Particle - - -class Propagator: - """ - Abstract base class for all propagators, i.e., evolutionary operators, in Propulate. - - Take a collection of individuals and use them to breed a new collection of individuals. - """ - - def __init__(self, loss_fn: Callable[[np.ndarray], float]): - """ - Constructor of Propagator class. - - Parameters - ---------- - loss_fn: Callable - The function to be optimized by the particles. Should take a numpy array and return a float. - """ - self.loss_fn = loss_fn - - def __call__(self, particle: Particle): - """ - Apply propagator (not implemented!). - - Parameters - ---------- - particle: Particle - The particle on which the propagator shall perform a positional update. - """ - raise NotImplementedError() - diff --git a/ap_pso/swarm.py b/ap_pso/swarm.py deleted file mode 100644 index 933d8163..00000000 --- a/ap_pso/swarm.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -This file contains the Swarm class, the technical equivalent to the Islands class of Propulate. -""" -from typing import Optional - -from mpi4py import MPI - -from ap_pso.particle import Particle -from ap_pso.propagators import Propagator -from ap_pso.utils import ExtendedPosition, TELL_TAG - - -class Swarm: - """ - The swarm contains the particles. - - It governs, what the particles do, evaluates them and updates them. - - The particles, to be fair don't deserve to be classes at the moment, - as they will lose all their methods quite soon to the swarm. - - Also, this class handles the internal MPI stuff, especially regarding communication between single workers within - one swarm so that everybody is always up to date. - """ - - def __init__(self, propagator: Propagator, communicator: MPI.Comm): - """ - Swarm constructor. - - :param propagator: The propagator to be used to update and evaluate the particles within this swarm - :param communicator: The communicator for this swarm's internal communication. - """ - self.propagator: Propagator = propagator - self.communicator = communicator - - self.rank: int = communicator.rank - self.size: int = communicator.size - - self.particles: list[Particle] = [] - self.archive: list[list[Particle]] = [] * self.size - - self.swarm_best: Optional[ExtendedPosition] = None - - # Create "randomly" initialised particles! - - # Communicate! - - def update(self) -> None: - """ - This method runs an update on the particles of the swarm. However, only the particle matching to the worker, - on which the swarm object is lying, is updated (usually, multiple similar swarm objects exist at the same time - within one communicator, all resembling the same swarm. This is a side effect of playing with MPI). - - The update is performed by the following steps: - - - First, a new particle with the stats of the old one plus one movement step update is created. - - Then, the old particle is moved into archive. - - In place of the old particle then the newly created is put. - - At last, the new particle is communicated. - """ - old_p = self.particles[self.rank] - new_p = self.propagator(old_p) - self.archive[self.rank].append(old_p) - self.particles[self.rank] = new_p - - def _communicate_update(self) -> None: - """ - Private method to handle the swarm's internal updating communication. - - At the same time, it performs updates on the swarm-global best value. - """ - for i in range(self.size): - if i != self.rank: - self.communicator.send(self.particles[self.rank], i, TELL_TAG) - - while True: - status = MPI.Status() - if not self.communicator.iprobe(tag=TELL_TAG, status=status): - break - source = status.Get_source() - incoming_p: Particle = self.communicator.recv(source=source, tag=TELL_TAG) - self.archive[source].append(self.particles[source]) - self.particles[source] = incoming_p - if incoming_p.p_best is not None and (self.swarm_best is None or incoming_p.p_best < self.swarm_best): - self.swarm_best = incoming_p.p_best - - def evaluate_particle(self) -> None: - """ - This method calls the swarm's propagator's loss function - on the particles and thus evaluates their fitness. - - The method always only considers the particle matching the rank of - the worker within the swarm's internal communicator. - - After evaluation, the method also starts a communication round in - order to update the rest of the workers on the swarm. - """ - p: Particle = self.particles[self.rank] - p.loss = self.propagator.loss_fn(p.position) - p.update_p_best() - self.particles[self.rank] = p - self._communicate_update() diff --git a/ap_pso/utils.py b/ap_pso/utils.py deleted file mode 100644 index 86253ff8..00000000 --- a/ap_pso/utils.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -This file contains some random util functions, as, for example, get_default_propagator -""" -from random import Random - -import numpy as np - - -def get_default_propagator(pop_size: int, limits: dict, mate_prob: float, mut_prob: float, random_prob: float, - sigma_factor: float = 0.05, rng: Random = None): - """ - Returns a generic, but working propagator to use on Swarm objects in order to update the particles. - - - Parameters - ---------- - pop_size : int - number of individuals in breeding population - limits : dict - mate_prob : float - uniform-crossover probability - mut_prob : float - point-mutation probability - random_prob : float - random-initialization probability - sigma_factor : float - scaling factor for obtaining std from search-space boundaries for interval mutation - rng : random.Random() - random number generator - """ - _ = (pop_size, limits, mate_prob, mut_prob, random_prob, sigma_factor, rng) - pass - - -class ExtendedPosition: - - def __init__(self, position: np.ndarray, loss: float): - self.position = position - self.loss = loss - - def __lt__(self, other): - if not isinstance(other, self.__class__): - return NotImplemented - return self.loss < other.loss - - -TELL_TAG = 0 From ad8970d36787c49560b54fb0d92b22c7b2ffa3ca Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 4 Jul 2023 16:04:19 +0200 Subject: [PATCH 021/139] Updated requirements to be less restrictive. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 8a70f1d7..e55a0bac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ pyparsing>=3.0.7 python-dateutil==2.8.2 six==1.16.0 colorlog + From ee193f9facd635ad8ed00b12116c886fa7aac688 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 4 Jul 2023 16:05:41 +0200 Subject: [PATCH 022/139] Added example for basic testing. --- .gitignore | 1 + scripts/pso_example.py | 171 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 scripts/pso_example.py diff --git a/.gitignore b/.gitignore index ade5584f..3d45a7ed 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ scripts/*.png voucher_propulate.txt .gitignore +checkpoints/ \ No newline at end of file diff --git a/scripts/pso_example.py b/scripts/pso_example.py new file mode 100644 index 00000000..1dff92f9 --- /dev/null +++ b/scripts/pso_example.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +import random +import sys + +import numpy as np +from mpi4py import MPI + +from propulate import Islands +from propulate.propagators import Compose, InitUniform, SelectMin, SelectMax, Conditional, PSOPropagator + + +############ +# SETTINGS # +############ + +fname = sys.argv[1] # Get function to optimize from command-line. +NUM_GENERATIONS = 10 # Set number of generations. +POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. +num_migrants = 1 + + +# BUKIN N.6 +# continuous, convex, non-separable, non-differentiable, multimodal +# input domain: -15 <= x <= -5, -3 <= y <= 3 +# global minimum 0 at (x, y) = (-10, 1) +def bukin_n6(params): + x = params["x"] + y = params["y"] + return 100 * np.sqrt(np.abs(y - 0.01 * x**2)) + 0.01 * np.abs(x + 10) + + +# EGG CRATE +# continuous, non-convex, separable, differentiable, multimodal +# input domain: -5 <= x, y <= 5 +# global minimum -1 at (x, y) = (0, 0) +def egg_crate(params): + x = params["x"] + y = params["y"] + return x**2 + y**2 + 25 * (np.sin(x) ** 2 + np.sin(y) ** 2) + + +# HIMMELBLAU +# continuous, non-convex, non-separable, differentiable, multimodal +# input domain: -6 <= x, y <= 6 +# global minimum 0 at (x, y) = (3, 2) +def himmelblau(params): + x = params["x"] + y = params["y"] + return (x**2 + y - 11) ** 2 + (x + y**2 - 7) ** 2 + + +# KEANE +# continuous, non-convex, non-separable, differentiable, multimodal +# input domain: -10 <= x, y <= 10 +# global minimum 0.6736675 at (x, y) = (1.3932491, 0) and (x, y) = (0, 1.3932491) +def keane(params): + x = params["x"] + y = params["y"] + return -np.sin(x - y) ** 2 * np.sin(x + y) ** 2 / np.sqrt(x**2 + y**2) + + +# LEON +# continous, non-convex, non-separable, differentiable, non-multimodal, non-random, non-parametric +# input domain: 0 <= x, y <= 10 +# global minimum 0 at (x, y) =(1, 1) +def leon(params): + x = params["x"] + y = params["y"] + return 100 * (y - x**3) ** 2 + (1 - x) ** 2 + + +# RASTRIGIN +# continuous, non-convex, separable, differentiable, multimodal +# input domain: -5.12 <= x, y <= 5.12 +# global minimum -20 at (x, y) = (0, 0) +def rastrigin(params): + x = params["x"] + y = params["y"] + return x**2 - 10 * np.cos(2 * np.pi * x) + y**2 - 10 * np.cos(2 * np.pi * y) + + +# SCHWEFEL 2.20 +# continuous, convex, separable, non-differentiable, non-multimodal +# input domain -100 <= x, y <= 100 +# global minimum 0 at (x, y) = (0, 0) +def schwefel(params): + x = params["x"] + y = params["y"] + return np.abs(x) + np.abs(y) + + +# SPHERE +# continuous, convex, separable, non-differentiable, non-multimodal +# input domain: -5.12 <= x, y <= 5.12 +# global minimum 0 at (x, y) = (0, 0) +def sphere(params): + x = params["x"] + y = params["y"] + return x**2 + y**2 + + +if fname == "bukin": + function = bukin_n6 + limits = { + "x": (-15.0, -5.0), + "y": (-3.0, 3.0), + } +elif fname == "eggcrate": + function = egg_crate + limits = { + "x": (-5.0, 5.0), + "y": (-5.0, 5.0), + } +elif fname == "himmelblau": + function = himmelblau + limits = { + "x": (-6.0, 6.0), + "y": (-6.0, 6.0), + } +elif fname == "keane": + function = keane + limits = { + "x": (-10.0, 10.0), + "y": (-10.0, 10.0), + } +elif fname == "leon": + function = leon + limits = { + "x": (0.0, 10.0), + "y": (0.0, 10.0), + } +elif fname == "rastrigin": + function = rastrigin + limits = { + "x": (-5.12, 5.12), + "y": (-5.12, 5.12), + } +elif fname == "schwefel": + function = schwefel + limits = { + "x": (-100.0, 100.0), + "y": (-100.0, 100.0), + } +elif fname == "sphere": + function = sphere + limits = { + "x": (-5.12, 5.12), + "y": (-5.12, 5.12), + } +else: + sys.exit("ERROR: Function undefined...exiting") + +if __name__ == "__main__": + # migration_topology = num_migrants*np.ones((4, 4), dtype=int) + # np.fill_diagonal(migration_topology, 0) + + rng = random.Random(MPI.COMM_WORLD.rank) + + propagator = Compose( + [ + PSOPropagator(0, 0.7, 0.7, MPI.COMM_WORLD.rank, limits, rng), + InitUniform(limits, parents=1, probability=0.1, rng=rng) + ] + ) + + init = InitUniform(limits, rng=rng) + propagator = Conditional(POP_SIZE, propagator, init) + + islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path='./checkpoints/', + migration_probability=0) + islands.evolve(top_n=1, logging_interval=1, DEBUG=2) From 223ae28f36edba65a173ba043b9eb1718537c949 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 4 Jul 2023 16:10:03 +0200 Subject: [PATCH 023/139] Refactored propagators to ease working with them. --- propulate/propagators/__init__.py | 11 ++++++ propulate/{ => propagators}/propagators.py | 2 +- propulate/propagators/pso_propagator.py | 43 ++++++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 propulate/propagators/__init__.py rename propulate/{ => propagators}/propagators.py (99%) create mode 100644 propulate/propagators/pso_propagator.py diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py new file mode 100644 index 00000000..c4c52bf4 --- /dev/null +++ b/propulate/propagators/__init__.py @@ -0,0 +1,11 @@ +""" +This package holds all Propagator subclasses including the Propagator itself. +""" + +__all__ = ["Propagator", "Stochastic", "Conditional", "Compose", "PointMutation", "RandomPointMutation", + "IntervalMutationNormal", "MateUniform", "MateMultiple", "MateSigmoid", "SelectMin", "SelectMax", + "SelectUniform", "InitUniform", "PSOPropagator"] + +from propulate.propagators.propagators import * +from propulate.propagators.pso_propagator import PSOPropagator + diff --git a/propulate/propagators.py b/propulate/propagators/propagators.py similarity index 99% rename from propulate/propagators.py rename to propulate/propagators/propagators.py index d8dd85ba..6a9fc51d 100644 --- a/propulate/propagators.py +++ b/propulate/propagators/propagators.py @@ -5,7 +5,7 @@ import numpy as np from abc import ABC, abstractmethod -from .population import Individual +from propulate.population import Individual def _check_compatible(out1: int, in2: int) -> bool: diff --git a/propulate/propagators/pso_propagator.py b/propulate/propagators/pso_propagator.py new file mode 100644 index 00000000..474facb4 --- /dev/null +++ b/propulate/propagators/pso_propagator.py @@ -0,0 +1,43 @@ +from random import Random + +from propulate.population import Individual + +from propulate.propagators import Propagator + + +class PSOPropagator(Propagator): + + def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, + limits: dict[str, tuple[float, float]], rng: Random): + """ + + :param w_k: The learning rate ... somehow - currently without effect + :param c_cognitive: constant cognitive factor to scale p_best with + :param c_social: constant social factor to scale g_best with + :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD + :param limits: a dict with str keys and 2-tuples of floats associated to each of them + :param rng: random number generator + """ + super().__init__(parents=-1, offspring=1) + self.c_social = c_social + self.c_cognitive = c_cognitive + self.w_k = w_k + self.rank = rank + self.limits = limits + self.rng = rng + + def __call__(self, particles: list[Individual]) -> Individual: + if len(particles) < self.offspring: + raise ValueError("Not enough Particles") + own_p = [x for x in particles if x.rank == self.rank] + old_p = Individual(generation=-1) + for y in own_p: + if y.generation > old_p.generation: + old_p = y + g_best = sorted(particles, key=lambda p: p.loss)[0] + p_best = sorted(own_p, key=lambda p: p.loss)[0] + new_p = Individual(generation=old_p.generation + 1) + for k in self.limits: + new_p[k] = self.c_cognitive * self.rng.uniform(*self.limits[k]) * (p_best[k] - old_p[k]) \ + + self.c_social * self.rng.uniform(*self.limits[k]) * (g_best[k] - old_p[k]) + return new_p From b286a95cebd22d5b87c08d63d706f2a523046a0b Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 4 Jul 2023 16:55:35 +0200 Subject: [PATCH 024/139] Removed some github actions not working for me. --- .github/workflows/fair-software.yml | 17 ----------------- scripts/pso_example.py | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) delete mode 100644 .github/workflows/fair-software.yml diff --git a/.github/workflows/fair-software.yml b/.github/workflows/fair-software.yml deleted file mode 100644 index 80969ded..00000000 --- a/.github/workflows/fair-software.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: fair-software - -on: - push: - branches: [master] - -jobs: - verify: - name: "fair-software" - runs-on: ubuntu-latest - steps: - - uses: fair-software/howfairis-github-action@0.2.1 - name: Measure compliance with fair-software.eu recommendations - env: - PYCHARM_HOSTED: "Trick colorama into displaying colored output" - with: - MY_REPO_URL: "https://github.com/${{ github.repository }}" diff --git a/scripts/pso_example.py b/scripts/pso_example.py index 1dff92f9..79a88687 100644 --- a/scripts/pso_example.py +++ b/scripts/pso_example.py @@ -6,7 +6,7 @@ from mpi4py import MPI from propulate import Islands -from propulate.propagators import Compose, InitUniform, SelectMin, SelectMax, Conditional, PSOPropagator +from propulate.propagators import Compose, InitUniform, Conditional, PSOPropagator ############ From 8f4ad653a238ae985eceeca70f57f2df4df03c90 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 5 Jul 2023 15:54:44 +0200 Subject: [PATCH 025/139] Re-added the Particle to be able to implement a full version of PSO --- ap_pso/__init__.py | 7 +++++++ ap_pso/particle.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 ap_pso/__init__.py create mode 100644 ap_pso/particle.py diff --git a/ap_pso/__init__.py b/ap_pso/__init__.py new file mode 100644 index 00000000..ff535989 --- /dev/null +++ b/ap_pso/__init__.py @@ -0,0 +1,7 @@ +""" +This file is used to steer the visibility of classes within ap-pso project +""" +__all__ = ["Particle"] + +from ap_pso.particle import Particle + diff --git a/ap_pso/particle.py b/ap_pso/particle.py new file mode 100644 index 00000000..1c8fa80e --- /dev/null +++ b/ap_pso/particle.py @@ -0,0 +1,20 @@ +import numpy as np + +from propulate.population import Individual + + +class Particle(Individual): + """ + Extension of Individual class with additional properties necessary for full PSO. + It also comes along with a numpy array to store positional information in. + As Propulate rather relies on Individuals being dicts and using this property to work with, it is just for future use. + + Please keep in mind, that users of this class are responsible to ensure, that a Particle's position always + matches their dict contents and vice versa. + """ + def __init__(self, position: np.ndarray, velocity: np.ndarray, iteration: int = None, rank: int = None): + super().__init__(generation=iteration, rank=rank) + assert position.shape == velocity.shape + self.velocity = velocity + self.position = position + From 1dd89b3ea8f5a732868f333f5b3a45ccb24249e8 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 5 Jul 2023 15:56:19 +0200 Subject: [PATCH 026/139] Carried out the implementation of PSOInitUniform propagator. Refactored propagators' file layout. --- propulate/propagators/__init__.py | 4 +- propulate/propagators/init_propagators.py | 130 ++++++++++++++++++++++ propulate/propagators/propagators.py | 3 +- 3 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 propulate/propagators/init_propagators.py diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py index c4c52bf4..f870f784 100644 --- a/propulate/propagators/__init__.py +++ b/propulate/propagators/__init__.py @@ -4,8 +4,10 @@ __all__ = ["Propagator", "Stochastic", "Conditional", "Compose", "PointMutation", "RandomPointMutation", "IntervalMutationNormal", "MateUniform", "MateMultiple", "MateSigmoid", "SelectMin", "SelectMax", - "SelectUniform", "InitUniform", "PSOPropagator"] + "SelectUniform", "InitUniform", "PSOPropagator", "PSOInitUniform"] from propulate.propagators.propagators import * from propulate.propagators.pso_propagator import PSOPropagator +from propulate.propagators.init_propagators import InitUniform, PSOInitUniform + diff --git a/propulate/propagators/init_propagators.py b/propulate/propagators/init_propagators.py new file mode 100644 index 00000000..334f9a23 --- /dev/null +++ b/propulate/propagators/init_propagators.py @@ -0,0 +1,130 @@ +from random import Random + +import numpy as np +from particle import Particle + +from propulate.population import Individual +from propulate.propagators import Stochastic + + +# TODO parents should be fixed to one NOTE see utils reason why it is not right now +class InitUniform(Stochastic): + """ + Initialize individuals by uniformly sampling specified limits for each trait. + """ + + def __init__(self, limits, parents=0, probability=1.0, rng=None): + """ + Constructor of InitUniform class. + + In case of parents > 0 and probability < 1., call returns input individual without change. + + Parameters + ---------- + limits : dict + limits of (hyper-)parameters to be optimized + offspring : int + number of offsprings (individuals to be selected) + rng : random.Random() + random number generator + """ + super(InitUniform, self).__init__(parents, 1, probability, rng) + self.limits = limits + + def __call__(self, *inds): + """ + Apply uniform-initialization propagator. + + Parameters + ---------- + inds : list of propulate.population.Individual objects + individuals the propagator is applied to + + Returns + ------- + ind : propulate.population.Individual + list of selected individuals after application of propagator + """ + if (self.rng.random() < self.probability): # Apply only with specified `probability`. + ind = Individual() # Instantiate new individual. + for limit in self.limits: + # Randomly sample from specified limits for each trait. + if (type(self.limits[limit][0]) == int): # If ordinal trait of type integer. + ind[limit] = self.rng.randrange(*self.limits[limit]) + elif (type(self.limits[limit][0]) == float): # If interval trait of type float. + ind[limit] = self.rng.uniform(*self.limits[limit]) + elif (type(self.limits[limit][0]) == str): # If categorical trait of type string. + ind[limit] = self.rng.choice(self.limits[limit]) + else: + raise ValueError( + "Unknown type of limits. Has to be float for interval, int for ordinal, or string for " + "categorical.") + return ind + else: + ind = inds[0] + return ind # Return 1st input individual w/o changes. + + +class PSOInitUniform(Stochastic): + """ + Initialize individuals by uniformly sampling specified limits for each trait. + """ + + def __init__(self, limits: dict[str, tuple[float]], parents=0, probability=1.0, rng: Random = None): + """ + Constructor of PSOInitUniform class. + + In case of parents > 0 and probability < 1., call returns input individual without change. + + Parameters + ---------- + limits : dict + a named list of tuples representing the limits in which the search space resides and where + solutions can be expected to be found. + Limits of (hyper-)parameters to be optimized + parents : int + number of input individuals (-1 for any) + probability : float + the probability with which a completely new individual is created + rng : random.Random() + random number generator + """ + super().__init__(parents, 1, probability, rng) + self.limits = limits + + def __call__(self, *particles: list[Individual]): + """ + Apply uniform-initialization propagator. + + Parameters + ---------- + particles : list of propulate.population.Individual objects + individuals the propagator is applied to + + Returns + ------- + ind : propulate.population.Individual + list of selected individuals after application of propagator + """ + if self.rng.random() < self.probability: # Apply only with specified `probability`. + dim = len(self.limits) + position = np.zeros(shape=dim) + velocity = np.zeros(shape=dim) + + particle = Particle(position, velocity) # Instantiate new particle. + + for index, limit in zip(range(dim), + self.limits): # Since Py 3.7, iterating over dicts is stable, so we can do the following. + if type(self.limits[limit][0]) != float: # Check search space for validity + raise TypeError("PSO only works on continuous search spaces!") + + # Randomly sample from specified limits for each trait. + pos_on_limit = self.rng.uniform(*self.limits[limit]) + particle.position[index] = pos_on_limit + particle[limit] = pos_on_limit + particle.velocity[index] = self.rng.uniform(*self.limits[limit]) + + return particle + else: + particle = particles[0] + return particle # Return 1st input individual w/o changes. diff --git a/propulate/propagators/propagators.py b/propulate/propagators/propagators.py index 6a9fc51d..115b3882 100644 --- a/propulate/propagators/propagators.py +++ b/propulate/propagators/propagators.py @@ -819,7 +819,7 @@ def __call__(self, inds: List[Individual]) -> List[Individual]: raise ValueError( f"Has to have at least {self.offspring} individuals to select {self.offspring} from them." ) - # Return a `self.offspring` length list of unique elements chosen from `inds`. + # Return a `self.offspring` length list of unique elements chosen from `particles`. # Used for random sampling without replacement. return self.rng.sample(inds, self.offspring) @@ -1628,3 +1628,4 @@ def get_evolution_path_co_matrix(self) -> np.ndarray: p_c : evolution path for covariance matrix adaption """ return self.par.p_c + From 6cadb4eae35964f723ccb3c79665787f18f3b633 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 5 Jul 2023 17:40:53 +0200 Subject: [PATCH 027/139] Did a lot of movement and refactorization. --- ap_pso/__init__.py | 7 ++- ap_pso/particle.py | 15 ++++- ap_pso/propagators/__init__.py | 7 +++ ap_pso/propagators/pso_init_uniform.py | 76 ++++++++++++++++++++++++++ ap_pso/propagators/stateless_pso.py | 47 ++++++++++++++++ ap_pso/utils.py | 20 +++++++ 6 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 ap_pso/propagators/__init__.py create mode 100644 ap_pso/propagators/pso_init_uniform.py create mode 100644 ap_pso/propagators/stateless_pso.py create mode 100644 ap_pso/utils.py diff --git a/ap_pso/__init__.py b/ap_pso/__init__.py index ff535989..a1f0a431 100644 --- a/ap_pso/__init__.py +++ b/ap_pso/__init__.py @@ -1,7 +1,8 @@ """ -This file is used to steer the visibility of classes within ap-pso project +This package contains - except for the example and the init propagator everything I added to propulate to be able to +run PSO on it. """ -__all__ = ["Particle"] +__all__ = ["Particle", "propagators", "get_dummy"] from ap_pso.particle import Particle - +from ap_pso.utils import get_dummy diff --git a/ap_pso/particle.py b/ap_pso/particle.py index 1c8fa80e..6c37aaec 100644 --- a/ap_pso/particle.py +++ b/ap_pso/particle.py @@ -1,3 +1,6 @@ +""" +This file contains the Particle class, an extension of Propulate's Individual class. +""" import numpy as np from propulate.population import Individual @@ -12,9 +15,15 @@ class Particle(Individual): Please keep in mind, that users of this class are responsible to ensure, that a Particle's position always matches their dict contents and vice versa. """ - def __init__(self, position: np.ndarray, velocity: np.ndarray, iteration: int = None, rank: int = None): + + def __init__(self, + position: np.ndarray = None, + velocity: np.ndarray = None, + iteration: int = None, + rank: int = None + ): super().__init__(generation=iteration, rank=rank) - assert position.shape == velocity.shape + if position is not None and velocity is not None: + assert position.shape == velocity.shape self.velocity = velocity self.position = position - diff --git a/ap_pso/propagators/__init__.py b/ap_pso/propagators/__init__.py new file mode 100644 index 00000000..d79bba36 --- /dev/null +++ b/ap_pso/propagators/__init__.py @@ -0,0 +1,7 @@ +""" +In this package, I collect all PSO-related propagators. +""" +__all__ = ["PSOInitUniform", "StatelessPSOPropagator"] + +from ap_pso.propagators.stateless_pso import StatelessPSOPropagator +from ap_pso.propagators.pso_init_uniform import PSOInitUniform diff --git a/ap_pso/propagators/pso_init_uniform.py b/ap_pso/propagators/pso_init_uniform.py new file mode 100644 index 00000000..bd2ac2cf --- /dev/null +++ b/ap_pso/propagators/pso_init_uniform.py @@ -0,0 +1,76 @@ +""" +This file contains propagators, that can be used to initialize a population of either Individuals or Particles. +""" +from random import Random + +import numpy as np + +from ap_pso import Particle +from propulate.population import Individual +from propulate.propagators import Stochastic + + +class PSOInitUniform(Stochastic): + """ + Initialize individuals by uniformly sampling specified limits for each trait. + """ + + def __init__(self, limits: dict[str, tuple[float]], parents=0, probability=1.0, rng: Random = None): + """ + Constructor of PSOInitUniform class. + + In case of parents > 0 and probability < 1., call returns input individual without change. + + Parameters + ---------- + limits : dict + a named list of tuples representing the limits in which the search space resides and where + solutions can be expected to be found. + Limits of (hyper-)parameters to be optimized + parents : int + number of input individuals (-1 for any) + probability : float + the probability with which a completely new individual is created + rng : random.Random() + random number generator + """ + super().__init__(parents, 1, probability, rng) + self.limits = limits + + def __call__(self, *particles: Individual) -> Individual: + """ + Apply uniform-initialization propagator. + + Parameters + ---------- + particles : list of propulate.population.Individual objects + individuals the propagator is applied to + + Returns + ------- + ind : propulate.population.Individual + list of selected individuals after application of propagator + """ + if self.rng.random() < self.probability: # Apply only with specified `probability`. + dim = len(self.limits) + position = np.zeros(shape=dim) + velocity = np.zeros(shape=dim) + + particle = Particle(position, velocity) # Instantiate new particle. + + for index, limit in zip(range(dim), self.limits): + # Since Py 3.7, iterating over dicts is stable, so we can do the following. + + if type(self.limits[limit][0]) != float: # Check search space for validity + raise TypeError("PSO only works on continuous search spaces!") + + # Randomly sample from specified limits for each trait. + pos_on_limit = self.rng.uniform(*self.limits[limit]) + particle[limit] = pos_on_limit + particle.position[index] = pos_on_limit + particle.velocity[index] = self.rng.uniform(*self.limits[limit]) + + return particle + else: + particle = particles[0] + return particle # Return 1st input individual w/o changes. diff --git a/ap_pso/propagators/stateless_pso.py b/ap_pso/propagators/stateless_pso.py new file mode 100644 index 00000000..129c3584 --- /dev/null +++ b/ap_pso/propagators/stateless_pso.py @@ -0,0 +1,47 @@ +""" +This file contains the first prototype of a propagator that runs PSO on Propulate. +""" + +from random import Random + +from propulate.population import Individual + +from propulate.propagators import Propagator + + +class StatelessPSOPropagator(Propagator): + + def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, + limits: dict[str, tuple[float, float]], rng: Random): + """ + + :param w_k: The learning rate ... somehow - currently without effect + :param c_cognitive: constant cognitive factor to scale p_best with + :param c_social: constant social factor to scale g_best with + :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD + :param limits: a dict with str keys and 2-tuples of floats associated to each of them + :param rng: random number generator + """ + super().__init__(parents=-1, offspring=1) + self.c_social = c_social + self.c_cognitive = c_cognitive + self.w_k = w_k + self.rank = rank + self.limits = limits + self.rng = rng + + def __call__(self, particles: list[Individual]) -> Individual: + if len(particles) < self.offspring: + raise ValueError("Not enough Particles") + own_p = [x for x in particles if x.rank == self.rank] + old_p = Individual(generation=-1) + for y in own_p: + if y.generation > old_p.generation: + old_p = y + g_best = sorted(particles, key=lambda p: p.loss)[0] + p_best = sorted(own_p, key=lambda p: p.loss)[0] + new_p = Individual(generation=old_p.generation + 1) + for k in self.limits: + new_p[k] = self.c_cognitive * self.rng.uniform(*self.limits[k]) * (p_best[k] - old_p[k]) \ + + self.c_social * self.rng.uniform(*self.limits[k]) * (g_best[k] - old_p[k]) + return new_p diff --git a/ap_pso/utils.py b/ap_pso/utils.py new file mode 100644 index 00000000..96f4f1df --- /dev/null +++ b/ap_pso/utils.py @@ -0,0 +1,20 @@ +""" +This file contains all sort of more or less useful stuff. +""" +from typing import Iterable + +import numpy as np + +from ap_pso import Particle + + +def get_dummy(shape: int | Iterable | tuple[int]) -> Particle: + """ + Returns a dummy particle that is just for age comparisons + + Parameters + ---------- + shape : The dimension(s) of the search space, as used to define numpy arrays. + """ + values = np.zeros(shape=shape) + return Particle(values, values, iteration=-1) From 18babae442dca5776121ba608244a1ee59d9b8a0 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 5 Jul 2023 17:41:21 +0200 Subject: [PATCH 028/139] Implemented the first stateful pso propagator relying on the Particle. --- ap_pso/propagators/basic_pso.py | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 ap_pso/propagators/basic_pso.py diff --git a/ap_pso/propagators/basic_pso.py b/ap_pso/propagators/basic_pso.py new file mode 100644 index 00000000..2a132916 --- /dev/null +++ b/ap_pso/propagators/basic_pso.py @@ -0,0 +1,53 @@ +""" +This file contains the first stateful PSO propagator for Propulate. +""" +from random import Random + +import numpy as np + +from ap_pso import Particle, get_dummy +from propulate.population import Individual +from propulate.propagators import Propagator + + +class BasicPSOPropagator(Propagator): + + def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, + limits: dict[str, tuple[float, float]], rng: Random): + """ + + :param w_k: The learning rate ... somehow + :param c_cognitive: constant cognitive factor to scale p_best with + :param c_social: constant social factor to scale g_best with + :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD + :param limits: a dict with str keys and 2-tuples of floats associated to each of them + :param rng: random number generator + """ + super().__init__(parents=-1, offspring=1) + self.c_social = c_social + self.c_cognitive = c_cognitive + self.w_k = w_k + self.rank = rank + self.limits = limits + self.rng = rng + self.laa = np.array(list(limits.values())).T + + def __call__(self, particles: list[Particle]) -> Particle: + if len(particles) < self.offspring: + raise ValueError("Not enough Particles") + own_p = [x for x in particles if x.rank == self.rank] + old_p = Particle(iteration=-1) + for y in own_p: + if y.generation > old_p.generation: + old_p = y + g_best = sorted(particles, key=lambda p: p.loss)[0] + p_best = sorted(own_p, key=lambda p: p.loss)[0] + new_velocity = self.w_k * old_p.velocity \ + + self.c_cognitive * self.rng.uniform(*self.laa) * (p_best.position - old_p.position) \ + + self.c_social * self.rng.uniform(*self.laa) * (g_best.position - old_p.position) + new_position = old_p.position + new_velocity + + new_p = Particle(new_position, new_velocity, old_p.generation + 1, self.rank) + for i, k in enumerate(self.limits): + new_p[k] += new_p.velocity[i] + return new_p From 25a4a1b223bdae69dc0b7f3d377bfd85feee2511 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 5 Jul 2023 18:15:11 +0200 Subject: [PATCH 029/139] Bug fixing and security measures. --- ap_pso/__init__.py | 4 +- ap_pso/particle.py | 2 +- ap_pso/propagators/__init__.py | 3 +- ap_pso/propagators/basic_pso.py | 8 +- ap_pso/propagators/pso_init_uniform.py | 2 +- ap_pso/scripts/pso_example.py | 59 +++++++++ ap_pso/utils.py | 16 ++- scripts/pso_example.py | 171 ------------------------- 8 files changed, 81 insertions(+), 184 deletions(-) create mode 100644 ap_pso/scripts/pso_example.py delete mode 100644 scripts/pso_example.py diff --git a/ap_pso/__init__.py b/ap_pso/__init__.py index a1f0a431..358d00ff 100644 --- a/ap_pso/__init__.py +++ b/ap_pso/__init__.py @@ -2,7 +2,7 @@ This package contains - except for the example and the init propagator everything I added to propulate to be able to run PSO on it. """ -__all__ = ["Particle", "propagators", "get_dummy"] +__all__ = ["Particle", "propagators", "make_particle"] from ap_pso.particle import Particle -from ap_pso.utils import get_dummy +from ap_pso.utils import make_particle diff --git a/ap_pso/particle.py b/ap_pso/particle.py index 6c37aaec..aa69ef90 100644 --- a/ap_pso/particle.py +++ b/ap_pso/particle.py @@ -19,7 +19,7 @@ class Particle(Individual): def __init__(self, position: np.ndarray = None, velocity: np.ndarray = None, - iteration: int = None, + iteration: int = 0, rank: int = None ): super().__init__(generation=iteration, rank=rank) diff --git a/ap_pso/propagators/__init__.py b/ap_pso/propagators/__init__.py index d79bba36..e413f139 100644 --- a/ap_pso/propagators/__init__.py +++ b/ap_pso/propagators/__init__.py @@ -1,7 +1,8 @@ """ In this package, I collect all PSO-related propagators. """ -__all__ = ["PSOInitUniform", "StatelessPSOPropagator"] +__all__ = ["PSOInitUniform", "StatelessPSOPropagator", "BasicPSOPropagator"] +from ap_pso.propagators.basic_pso import BasicPSOPropagator from ap_pso.propagators.stateless_pso import StatelessPSOPropagator from ap_pso.propagators.pso_init_uniform import PSOInitUniform diff --git a/ap_pso/propagators/basic_pso.py b/ap_pso/propagators/basic_pso.py index 2a132916..72cfe28a 100644 --- a/ap_pso/propagators/basic_pso.py +++ b/ap_pso/propagators/basic_pso.py @@ -5,8 +5,7 @@ import numpy as np -from ap_pso import Particle, get_dummy -from propulate.population import Individual +from ap_pso import Particle, make_particle from propulate.propagators import Propagator @@ -40,6 +39,9 @@ def __call__(self, particles: list[Particle]) -> Particle: for y in own_p: if y.generation > old_p.generation: old_p = y + if not isinstance(old_p, Particle): + old_p = make_particle(old_p) + print(f"R{self.rank}, Iteration#{old_p.generation}: Type Error.") g_best = sorted(particles, key=lambda p: p.loss)[0] p_best = sorted(own_p, key=lambda p: p.loss)[0] new_velocity = self.w_k * old_p.velocity \ @@ -49,5 +51,5 @@ def __call__(self, particles: list[Particle]) -> Particle: new_p = Particle(new_position, new_velocity, old_p.generation + 1, self.rank) for i, k in enumerate(self.limits): - new_p[k] += new_p.velocity[i] + new_p[k] = new_p.velocity[i] return new_p diff --git a/ap_pso/propagators/pso_init_uniform.py b/ap_pso/propagators/pso_init_uniform.py index bd2ac2cf..7f0daf91 100644 --- a/ap_pso/propagators/pso_init_uniform.py +++ b/ap_pso/propagators/pso_init_uniform.py @@ -15,7 +15,7 @@ class PSOInitUniform(Stochastic): Initialize individuals by uniformly sampling specified limits for each trait. """ - def __init__(self, limits: dict[str, tuple[float]], parents=0, probability=1.0, rng: Random = None): + def __init__(self, limits: dict[str, tuple[float, float]], parents=0, probability=1.0, rng: Random = None): """ Constructor of PSOInitUniform class. diff --git a/ap_pso/scripts/pso_example.py b/ap_pso/scripts/pso_example.py new file mode 100644 index 00000000..777f76d2 --- /dev/null +++ b/ap_pso/scripts/pso_example.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import random +import sys + +import numpy as np +from mpi4py import MPI + +from ap_pso.propagators import BasicPSOPropagator, PSOInitUniform +from propulate import Islands +from propulate.propagators import Compose, InitUniform, Conditional + +############ +# SETTINGS # +############ + +fname = sys.argv[1] # Get function to optimize from command-line. +NUM_GENERATIONS = 22 # Set number of generations. +POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. +num_migrants = 1 + + +# SPHERE +# continuous, convex, separable, non-differentiable, non-multimodal +# input domain: -5.12 <= x, y <= 5.12 +# global minimum 0 at (x, y) = (0, 0) +def sphere(params): + x = params["x"] + y = params["y"] + return x ** 2 + y ** 2 + + +if fname == "sphere": + function = sphere + limits = { + "x": (-5.12, 5.12), + "y": (-5.12, 5.12), + } +else: + sys.exit("ERROR: Function undefined...exiting") + +if __name__ == "__main__": + # migration_topology = num_migrants*np.ones((4, 4), dtype=int) + # np.fill_diagonal(migration_topology, 0) + + rng = random.Random(MPI.COMM_WORLD.rank) + + propagator = Compose( + [ + BasicPSOPropagator(0, 0.9, 0.4, MPI.COMM_WORLD.rank, limits, rng), + PSOInitUniform(limits, parents=1, probability=0.1, rng=rng) + ] + ) + + init = PSOInitUniform(limits, rng=rng) + propagator = Conditional(POP_SIZE, propagator, init) + + islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path='./checkpoints/', + migration_probability=0) + islands.evolve(top_n=1, logging_interval=1, DEBUG=2) diff --git a/ap_pso/utils.py b/ap_pso/utils.py index 96f4f1df..6d3a95b1 100644 --- a/ap_pso/utils.py +++ b/ap_pso/utils.py @@ -6,15 +6,21 @@ import numpy as np from ap_pso import Particle +from propulate.population import Individual -def get_dummy(shape: int | Iterable | tuple[int]) -> Particle: +def make_particle(individual: Individual) -> Particle: """ - Returns a dummy particle that is just for age comparisons + Makes particles out of individuals. Parameters ---------- - shape : The dimension(s) of the search space, as used to define numpy arrays. + individual : An Individual that needs to be a particle """ - values = np.zeros(shape=shape) - return Particle(values, values, iteration=-1) + p = Particle(iteration=individual.generation) + p.position = np.zeros(len(individual)) + p.velocity = np.zeros(len(individual)) + for i, k in enumerate(individual): + p.position[i] = individual[k] + p[k] = individual[k] + return p diff --git a/scripts/pso_example.py b/scripts/pso_example.py deleted file mode 100644 index 79a88687..00000000 --- a/scripts/pso_example.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python3 -import random -import sys - -import numpy as np -from mpi4py import MPI - -from propulate import Islands -from propulate.propagators import Compose, InitUniform, Conditional, PSOPropagator - - -############ -# SETTINGS # -############ - -fname = sys.argv[1] # Get function to optimize from command-line. -NUM_GENERATIONS = 10 # Set number of generations. -POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. -num_migrants = 1 - - -# BUKIN N.6 -# continuous, convex, non-separable, non-differentiable, multimodal -# input domain: -15 <= x <= -5, -3 <= y <= 3 -# global minimum 0 at (x, y) = (-10, 1) -def bukin_n6(params): - x = params["x"] - y = params["y"] - return 100 * np.sqrt(np.abs(y - 0.01 * x**2)) + 0.01 * np.abs(x + 10) - - -# EGG CRATE -# continuous, non-convex, separable, differentiable, multimodal -# input domain: -5 <= x, y <= 5 -# global minimum -1 at (x, y) = (0, 0) -def egg_crate(params): - x = params["x"] - y = params["y"] - return x**2 + y**2 + 25 * (np.sin(x) ** 2 + np.sin(y) ** 2) - - -# HIMMELBLAU -# continuous, non-convex, non-separable, differentiable, multimodal -# input domain: -6 <= x, y <= 6 -# global minimum 0 at (x, y) = (3, 2) -def himmelblau(params): - x = params["x"] - y = params["y"] - return (x**2 + y - 11) ** 2 + (x + y**2 - 7) ** 2 - - -# KEANE -# continuous, non-convex, non-separable, differentiable, multimodal -# input domain: -10 <= x, y <= 10 -# global minimum 0.6736675 at (x, y) = (1.3932491, 0) and (x, y) = (0, 1.3932491) -def keane(params): - x = params["x"] - y = params["y"] - return -np.sin(x - y) ** 2 * np.sin(x + y) ** 2 / np.sqrt(x**2 + y**2) - - -# LEON -# continous, non-convex, non-separable, differentiable, non-multimodal, non-random, non-parametric -# input domain: 0 <= x, y <= 10 -# global minimum 0 at (x, y) =(1, 1) -def leon(params): - x = params["x"] - y = params["y"] - return 100 * (y - x**3) ** 2 + (1 - x) ** 2 - - -# RASTRIGIN -# continuous, non-convex, separable, differentiable, multimodal -# input domain: -5.12 <= x, y <= 5.12 -# global minimum -20 at (x, y) = (0, 0) -def rastrigin(params): - x = params["x"] - y = params["y"] - return x**2 - 10 * np.cos(2 * np.pi * x) + y**2 - 10 * np.cos(2 * np.pi * y) - - -# SCHWEFEL 2.20 -# continuous, convex, separable, non-differentiable, non-multimodal -# input domain -100 <= x, y <= 100 -# global minimum 0 at (x, y) = (0, 0) -def schwefel(params): - x = params["x"] - y = params["y"] - return np.abs(x) + np.abs(y) - - -# SPHERE -# continuous, convex, separable, non-differentiable, non-multimodal -# input domain: -5.12 <= x, y <= 5.12 -# global minimum 0 at (x, y) = (0, 0) -def sphere(params): - x = params["x"] - y = params["y"] - return x**2 + y**2 - - -if fname == "bukin": - function = bukin_n6 - limits = { - "x": (-15.0, -5.0), - "y": (-3.0, 3.0), - } -elif fname == "eggcrate": - function = egg_crate - limits = { - "x": (-5.0, 5.0), - "y": (-5.0, 5.0), - } -elif fname == "himmelblau": - function = himmelblau - limits = { - "x": (-6.0, 6.0), - "y": (-6.0, 6.0), - } -elif fname == "keane": - function = keane - limits = { - "x": (-10.0, 10.0), - "y": (-10.0, 10.0), - } -elif fname == "leon": - function = leon - limits = { - "x": (0.0, 10.0), - "y": (0.0, 10.0), - } -elif fname == "rastrigin": - function = rastrigin - limits = { - "x": (-5.12, 5.12), - "y": (-5.12, 5.12), - } -elif fname == "schwefel": - function = schwefel - limits = { - "x": (-100.0, 100.0), - "y": (-100.0, 100.0), - } -elif fname == "sphere": - function = sphere - limits = { - "x": (-5.12, 5.12), - "y": (-5.12, 5.12), - } -else: - sys.exit("ERROR: Function undefined...exiting") - -if __name__ == "__main__": - # migration_topology = num_migrants*np.ones((4, 4), dtype=int) - # np.fill_diagonal(migration_topology, 0) - - rng = random.Random(MPI.COMM_WORLD.rank) - - propagator = Compose( - [ - PSOPropagator(0, 0.7, 0.7, MPI.COMM_WORLD.rank, limits, rng), - InitUniform(limits, parents=1, probability=0.1, rng=rng) - ] - ) - - init = InitUniform(limits, rng=rng) - propagator = Conditional(POP_SIZE, propagator, init) - - islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path='./checkpoints/', - migration_probability=0) - islands.evolve(top_n=1, logging_interval=1, DEBUG=2) From ba1e924bd5497e5cf443045c7f6950de052d078c Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 17 Jul 2023 15:45:40 +0200 Subject: [PATCH 030/139] Updated and enhanced PSO propagators. --- ap_pso/propagators/__init__.py | 5 ++-- ap_pso/propagators/basic_pso.py | 6 ++--- ap_pso/propagators/pso_init_uniform.py | 32 +++++++++++++++----------- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/ap_pso/propagators/__init__.py b/ap_pso/propagators/__init__.py index e413f139..362b1e0e 100644 --- a/ap_pso/propagators/__init__.py +++ b/ap_pso/propagators/__init__.py @@ -1,8 +1,9 @@ """ In this package, I collect all PSO-related propagators. """ -__all__ = ["PSOInitUniform", "StatelessPSOPropagator", "BasicPSOPropagator"] +__all__ = ["PSOInitUniform", "StatelessPSOPropagator", "BasicPSOPropagator", "VelocityClampingPropagator"] from ap_pso.propagators.basic_pso import BasicPSOPropagator -from ap_pso.propagators.stateless_pso import StatelessPSOPropagator from ap_pso.propagators.pso_init_uniform import PSOInitUniform +from ap_pso.propagators.stateless_pso import StatelessPSOPropagator +from ap_pso.propagators.velocity_clamping import VelocityClampingPropagator diff --git a/ap_pso/propagators/basic_pso.py b/ap_pso/propagators/basic_pso.py index 72cfe28a..ca2082bd 100644 --- a/ap_pso/propagators/basic_pso.py +++ b/ap_pso/propagators/basic_pso.py @@ -14,7 +14,7 @@ class BasicPSOPropagator(Propagator): def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, limits: dict[str, tuple[float, float]], rng: Random): """ - + Class constructor. :param w_k: The learning rate ... somehow :param c_cognitive: constant cognitive factor to scale p_best with :param c_social: constant social factor to scale g_best with @@ -42,8 +42,8 @@ def __call__(self, particles: list[Particle]) -> Particle: if not isinstance(old_p, Particle): old_p = make_particle(old_p) print(f"R{self.rank}, Iteration#{old_p.generation}: Type Error.") - g_best = sorted(particles, key=lambda p: p.loss)[0] - p_best = sorted(own_p, key=lambda p: p.loss)[0] + g_best = min(particles, key=lambda p: p.loss) + p_best = min(own_p, key=lambda p: p.loss) new_velocity = self.w_k * old_p.velocity \ + self.c_cognitive * self.rng.uniform(*self.laa) * (p_best.position - old_p.position) \ + self.c_social * self.rng.uniform(*self.laa) * (g_best.position - old_p.position) diff --git a/ap_pso/propagators/pso_init_uniform.py b/ap_pso/propagators/pso_init_uniform.py index 7f0daf91..574df4e8 100644 --- a/ap_pso/propagators/pso_init_uniform.py +++ b/ap_pso/propagators/pso_init_uniform.py @@ -5,7 +5,7 @@ import numpy as np -from ap_pso import Particle +from ap_pso import Particle, make_particle from propulate.population import Individual from propulate.propagators import Stochastic @@ -15,7 +15,8 @@ class PSOInitUniform(Stochastic): Initialize individuals by uniformly sampling specified limits for each trait. """ - def __init__(self, limits: dict[str, tuple[float, float]], parents=0, probability=1.0, rng: Random = None): + def __init__(self, limits: dict[str, tuple[float, float]], parents=0, probability=1.0, rng: Random = None, *, + v_init_limit: float | np.ndarray = 0.1): """ Constructor of PSOInitUniform class. @@ -33,11 +34,17 @@ def __init__(self, limits: dict[str, tuple[float, float]], parents=0, probabilit the probability with which a completely new individual is created rng : random.Random() random number generator + v_init_limit: float | np.ndarray + some multiplicative constant to reduce initial random velocity values. """ super().__init__(parents, 1, probability, rng) self.limits = limits + self.laa = np.array(list(limits.values())).T + if isinstance(v_init_limit, np.ndarray): + assert v_init_limit.shape[-1] == self.laa.shape[-1] + self.v_limits = v_init_limit - def __call__(self, *particles: Individual) -> Individual: + def __call__(self, *particles: Individual) -> Particle: """ Apply uniform-initialization propagator. @@ -52,25 +59,24 @@ def __call__(self, *particles: Individual) -> Individual: list of selected individuals after application of propagator """ if self.rng.random() < self.probability: # Apply only with specified `probability`. - dim = len(self.limits) - position = np.zeros(shape=dim) - velocity = np.zeros(shape=dim) + + position = self.rng.uniform(*self.laa) + velocity = self.rng.uniform(*(self.v_limits * self.laa)) particle = Particle(position, velocity) # Instantiate new particle. - for index, limit in zip(range(dim), self.limits): + for index, limit in enumerate(self.limits): # Since Py 3.7, iterating over dicts is stable, so we can do the following. if type(self.limits[limit][0]) != float: # Check search space for validity raise TypeError("PSO only works on continuous search spaces!") # Randomly sample from specified limits for each trait. - pos_on_limit = self.rng.uniform(*self.limits[limit]) - particle[limit] = pos_on_limit - particle.position[index] = pos_on_limit - particle.velocity[index] = self.rng.uniform(*self.limits[limit]) - + particle[limit] = particle.position[index] return particle else: particle = particles[0] - return particle # Return 1st input individual w/o changes. + if isinstance(particle, Particle): + return particle # Return 1st input individual w/o changes. + else: + return make_particle(particle) From 0b0c6d765c71c57e423a4f5803aafe2bfb14adf0 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 17 Jul 2023 15:46:36 +0200 Subject: [PATCH 031/139] Added velocity clamping propagator. Adjusted pso_example.py to serve the new propagator. --- ap_pso/propagators/velocity_clamping.py | 39 +++++++++++++++++++++++++ ap_pso/scripts/pso_example.py | 7 ++--- 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 ap_pso/propagators/velocity_clamping.py diff --git a/ap_pso/propagators/velocity_clamping.py b/ap_pso/propagators/velocity_clamping.py new file mode 100644 index 00000000..f38ac615 --- /dev/null +++ b/ap_pso/propagators/velocity_clamping.py @@ -0,0 +1,39 @@ +""" +This file contains a PSO propagator relying on the standard one but additionally performing velocity clamping. +""" +from random import Random + +import numpy as np + +from ap_pso import Particle +from ap_pso.propagators import BasicPSOPropagator + + +class VelocityClampingPropagator(BasicPSOPropagator): + def __init__(self, + w_k: float, + c_cognitive: float, + c_social: float, + rank: int, + limits: dict[str, tuple[float, float]], + rng: Random, + v_limits: float | np.ndarray): + """ + Class constructor. + :param w_k: The particle's inertia factor + :param c_cognitive: constant cognitive factor to scale p_best with + :param c_social: constant social factor to scale g_best with + :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD + :param limits: a dict with str keys and 2-tuples of floats associated to each of them + :param rng: random number generator + :param v_limits: a numpy array containing values that work as relative caps for their corresponding search space dimensions. If this is a float instead, it does its job for all axes. + """ + super().__init__(w_k, c_cognitive, c_social, rank, limits, rng) + self.v_cap = v_limits + + def __call__(self, particles: list[Particle]) -> Particle: + p: Particle = super().__call__(particles) + p.position -= p.velocity + p.velocity = p.velocity.clip(*(self.v_cap * self.laa)) + p.position += p.velocity + return p diff --git a/ap_pso/scripts/pso_example.py b/ap_pso/scripts/pso_example.py index 777f76d2..83611a69 100644 --- a/ap_pso/scripts/pso_example.py +++ b/ap_pso/scripts/pso_example.py @@ -2,12 +2,11 @@ import random import sys -import numpy as np from mpi4py import MPI -from ap_pso.propagators import BasicPSOPropagator, PSOInitUniform +from ap_pso.propagators import PSOInitUniform, VelocityClampingPropagator from propulate import Islands -from propulate.propagators import Compose, InitUniform, Conditional +from propulate.propagators import Compose, Conditional ############ # SETTINGS # @@ -46,7 +45,7 @@ def sphere(params): propagator = Compose( [ - BasicPSOPropagator(0, 0.9, 0.4, MPI.COMM_WORLD.rank, limits, rng), + VelocityClampingPropagator(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.1), PSOInitUniform(limits, parents=1, probability=0.1, rng=rng) ] ) From 17b0683581fbcd3680dc1159f41b67259f5fec4a Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 17 Jul 2023 15:57:01 +0200 Subject: [PATCH 032/139] Cleaned up propagators after crash. --- ap_pso/scripts/pso_example.py | 2 +- isle_0_summary.png | Bin 0 -> 33121 bytes isle_0_v_summary.png | Bin 0 -> 24995 bytes .../.github/workflows/python-publish.yml | 39 ++++++ propulate/CODE_OF_CONDUCT.md | 128 +++++++++++++++++ propulate/README.md | 58 ++++++++ propulate/propagators/__init__.py | 6 +- propulate/propagators/init_propagators.py | 130 ------------------ propulate/propagators/pso_propagator.py | 43 ------ propulate/propulator.py | 1 + propulate/setup.cfg | 115 ++++++++++++++++ 11 files changed, 343 insertions(+), 179 deletions(-) create mode 100644 isle_0_summary.png create mode 100644 isle_0_v_summary.png create mode 100644 propulate/.github/workflows/python-publish.yml create mode 100644 propulate/CODE_OF_CONDUCT.md create mode 100644 propulate/README.md delete mode 100644 propulate/propagators/init_propagators.py delete mode 100644 propulate/propagators/pso_propagator.py create mode 100644 propulate/setup.cfg diff --git a/ap_pso/scripts/pso_example.py b/ap_pso/scripts/pso_example.py index 83611a69..74f5195d 100644 --- a/ap_pso/scripts/pso_example.py +++ b/ap_pso/scripts/pso_example.py @@ -13,7 +13,7 @@ ############ fname = sys.argv[1] # Get function to optimize from command-line. -NUM_GENERATIONS = 22 # Set number of generations. +NUM_GENERATIONS = 100 # Set number of generations. POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. num_migrants = 1 diff --git a/isle_0_summary.png b/isle_0_summary.png new file mode 100644 index 0000000000000000000000000000000000000000..e04297c0c6f09768c492e9ee5ee74b7e15814e00 GIT binary patch literal 33121 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�o9n#1d|BUHWNTglsqb&OT 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

7Fe6j!Y;oqape9Ji-3T1yu^jOzb`JV_U)Vc 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 literal 0 HcmV?d00001 diff --git a/propulate/.github/workflows/python-publish.yml b/propulate/.github/workflows/python-publish.yml new file mode 100644 index 00000000..acd6590a --- /dev/null +++ b/propulate/.github/workflows/python-publish.yml @@ -0,0 +1,39 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries +# see https://python-semantic-release.readthedocs.io/en/latest/automatic-releases/github-actions.html + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + push: + branches: [release] + +permissions: + contents: read + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/propulate/CODE_OF_CONDUCT.md b/propulate/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..874784a7 --- /dev/null +++ b/propulate/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +marie.weiel@kit.edu. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/propulate/README.md b/propulate/README.md new file mode 100644 index 00000000..776bc80b --- /dev/null +++ b/propulate/README.md @@ -0,0 +1,58 @@ +![Propulate Logo](./LOGO.svg) + +# Parallel Propagator of Populations + +[![DOI](https://zenodo.org/badge/495731357.svg)](https://zenodo.org/badge/latestdoi/495731357) +[![fair-software.eu](https://img.shields.io/badge/fair--software.eu-%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8B-yellow)](https://fair-software.eu) +[![License: BSD-3](https://img.shields.io/badge/License-BSD--3-blue)](https://opensource.org/licenses/BSD-3-Clause) +![PyPI](https://img.shields.io/pypi/v/propulate) +![PyPI - Downloads](https://img.shields.io/pypi/dm/propulate) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![](https://img.shields.io/badge/Python-3.6+-blue.svg)](https://www.python.org/downloads/) + +# **Click [here](https://www.scc.kit.edu/en/aboutus/16956.php) to watch our 3 min introduction video!** + +## What `Propulate` can do for you + +`Propulate` is an HPC-tailored software for solving optimization problems in parallel. It is openly accessible and easy to use. Compared to a widely used competitor, `Propulate` is consistently faster - at least an order of magnitude for a set of typical benchmarks - and in some cases even more accurate. + +Inspired by biology, `Propulate` borrows mechanisms from biological evolution, such as selection, recombination, and mutation. Evolution begins with a population of solution candidates, each with randomly initialized genes. It is an iterative "survival of the fittest" process where the population at each iteration can be viewed as a generation. For each generation, the fitness of each candidate in the population is evaluated. The genes of the fittest candidates are incorporated in the next generation. + +Like in nature, `Propulate` does not wait for all compute units to finish the evaluation of the current generation. Instead, the compute units communicate the currently available information and use that to breed the next candidate immediately. This avoids waiting idly for other units and thus a load imbalance. +Each unit is responsible for evaluating a single candidate. The result is a fitness level corresponding with that candidate’s genes, allowing us to compare and rank all candidates. This information is sent to other compute units as soon as it becomes available. +When a unit is finished evaluating a candidate and communicating the resulting fitness, it breeds the candidate for the next generation using the fitness values of all candidates it evaluated and received from other units so far. + +`Propulate` can be used for hyperparameter optimization and neural architecture search. +It was already successfully applied in several accepted scientific publications. Applications include grid load forecasting, remote sensing, and structural molecular biology. + +## In more technical terms + +``Propulate`` is a massively parallel evolutionary hyperparameter optimizer based on the island model with asynchronous propagation of populations and asynchronous migration. +In contrast to classical GAs, ``Propulate`` maintains a continuous population of already evaluated individuals with a softened notion of the typically strictly separated, discrete generations. +Our contributions include: +- A novel parallel genetic algorithm based on a fully asynchronized island model with independently processing workers. +- Massive parallelism by asynchronous propagation of continuous populations and migration via efficient communication using the message passing interface. +- Optimized use efficiency of parallel hardware by minimizing idle times in distributed computing environments. + +To be more efficient, the generations are less well separated than they usually are in evolutionary algorithms. +New individuals are generated from a pool of currently active, already evaluated individuals that may be from any generation. +Individuals may be removed from the breeding population based on different criteria. + +You can find the corresponding publication [here](https://doi.org/10.1007/978-3-031-32041-5_6): +>Taubert, O. *et al.* (2023). Massively Parallel Genetic Optimization Through Asynchronous Propagation of Populations. In: Bhatele, A., Hammond, J., Baboulin, M., Kruse, C. (eds) High Performance Computing. ISC High Performance 2023. Lecture Notes in Computer Science, vol 13948. Springer, Cham. [doi.org/10.1007/978-3-031-32041-5_6](https://doi.org/10.1007/978-3-031-32041-5_6) + +## Documentation + +For usage example, see scripts. We plan to provide a full readthedocs.io documentation soon! + +## Installation + +From PyPI: ``pip install propulate`` +Alternatively, pull and run ``pip install -e .`` or ``python setup.py develop``. +Requires an MPI implementation (currently only tested with OpenMPI) and ``mpi4py``. + +## Acknowledgments +*This work is supported by the Helmholtz AI platform grant.* +

+ +
diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py index f870f784..1a8356a8 100644 --- a/propulate/propagators/__init__.py +++ b/propulate/propagators/__init__.py @@ -4,10 +4,6 @@ __all__ = ["Propagator", "Stochastic", "Conditional", "Compose", "PointMutation", "RandomPointMutation", "IntervalMutationNormal", "MateUniform", "MateMultiple", "MateSigmoid", "SelectMin", "SelectMax", - "SelectUniform", "InitUniform", "PSOPropagator", "PSOInitUniform"] + "SelectUniform", "InitUniform"] from propulate.propagators.propagators import * -from propulate.propagators.pso_propagator import PSOPropagator -from propulate.propagators.init_propagators import InitUniform, PSOInitUniform - - diff --git a/propulate/propagators/init_propagators.py b/propulate/propagators/init_propagators.py deleted file mode 100644 index 334f9a23..00000000 --- a/propulate/propagators/init_propagators.py +++ /dev/null @@ -1,130 +0,0 @@ -from random import Random - -import numpy as np -from particle import Particle - -from propulate.population import Individual -from propulate.propagators import Stochastic - - -# TODO parents should be fixed to one NOTE see utils reason why it is not right now -class InitUniform(Stochastic): - """ - Initialize individuals by uniformly sampling specified limits for each trait. - """ - - def __init__(self, limits, parents=0, probability=1.0, rng=None): - """ - Constructor of InitUniform class. - - In case of parents > 0 and probability < 1., call returns input individual without change. - - Parameters - ---------- - limits : dict - limits of (hyper-)parameters to be optimized - offspring : int - number of offsprings (individuals to be selected) - rng : random.Random() - random number generator - """ - super(InitUniform, self).__init__(parents, 1, probability, rng) - self.limits = limits - - def __call__(self, *inds): - """ - Apply uniform-initialization propagator. - - Parameters - ---------- - inds : list of propulate.population.Individual objects - individuals the propagator is applied to - - Returns - ------- - ind : propulate.population.Individual - list of selected individuals after application of propagator - """ - if (self.rng.random() < self.probability): # Apply only with specified `probability`. - ind = Individual() # Instantiate new individual. - for limit in self.limits: - # Randomly sample from specified limits for each trait. - if (type(self.limits[limit][0]) == int): # If ordinal trait of type integer. - ind[limit] = self.rng.randrange(*self.limits[limit]) - elif (type(self.limits[limit][0]) == float): # If interval trait of type float. - ind[limit] = self.rng.uniform(*self.limits[limit]) - elif (type(self.limits[limit][0]) == str): # If categorical trait of type string. - ind[limit] = self.rng.choice(self.limits[limit]) - else: - raise ValueError( - "Unknown type of limits. Has to be float for interval, int for ordinal, or string for " - "categorical.") - return ind - else: - ind = inds[0] - return ind # Return 1st input individual w/o changes. - - -class PSOInitUniform(Stochastic): - """ - Initialize individuals by uniformly sampling specified limits for each trait. - """ - - def __init__(self, limits: dict[str, tuple[float]], parents=0, probability=1.0, rng: Random = None): - """ - Constructor of PSOInitUniform class. - - In case of parents > 0 and probability < 1., call returns input individual without change. - - Parameters - ---------- - limits : dict - a named list of tuples representing the limits in which the search space resides and where - solutions can be expected to be found. - Limits of (hyper-)parameters to be optimized - parents : int - number of input individuals (-1 for any) - probability : float - the probability with which a completely new individual is created - rng : random.Random() - random number generator - """ - super().__init__(parents, 1, probability, rng) - self.limits = limits - - def __call__(self, *particles: list[Individual]): - """ - Apply uniform-initialization propagator. - - Parameters - ---------- - particles : list of propulate.population.Individual objects - individuals the propagator is applied to - - Returns - ------- - ind : propulate.population.Individual - list of selected individuals after application of propagator - """ - if self.rng.random() < self.probability: # Apply only with specified `probability`. - dim = len(self.limits) - position = np.zeros(shape=dim) - velocity = np.zeros(shape=dim) - - particle = Particle(position, velocity) # Instantiate new particle. - - for index, limit in zip(range(dim), - self.limits): # Since Py 3.7, iterating over dicts is stable, so we can do the following. - if type(self.limits[limit][0]) != float: # Check search space for validity - raise TypeError("PSO only works on continuous search spaces!") - - # Randomly sample from specified limits for each trait. - pos_on_limit = self.rng.uniform(*self.limits[limit]) - particle.position[index] = pos_on_limit - particle[limit] = pos_on_limit - particle.velocity[index] = self.rng.uniform(*self.limits[limit]) - - return particle - else: - particle = particles[0] - return particle # Return 1st input individual w/o changes. diff --git a/propulate/propagators/pso_propagator.py b/propulate/propagators/pso_propagator.py deleted file mode 100644 index 474facb4..00000000 --- a/propulate/propagators/pso_propagator.py +++ /dev/null @@ -1,43 +0,0 @@ -from random import Random - -from propulate.population import Individual - -from propulate.propagators import Propagator - - -class PSOPropagator(Propagator): - - def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, - limits: dict[str, tuple[float, float]], rng: Random): - """ - - :param w_k: The learning rate ... somehow - currently without effect - :param c_cognitive: constant cognitive factor to scale p_best with - :param c_social: constant social factor to scale g_best with - :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD - :param limits: a dict with str keys and 2-tuples of floats associated to each of them - :param rng: random number generator - """ - super().__init__(parents=-1, offspring=1) - self.c_social = c_social - self.c_cognitive = c_cognitive - self.w_k = w_k - self.rank = rank - self.limits = limits - self.rng = rng - - def __call__(self, particles: list[Individual]) -> Individual: - if len(particles) < self.offspring: - raise ValueError("Not enough Particles") - own_p = [x for x in particles if x.rank == self.rank] - old_p = Individual(generation=-1) - for y in own_p: - if y.generation > old_p.generation: - old_p = y - g_best = sorted(particles, key=lambda p: p.loss)[0] - p_best = sorted(own_p, key=lambda p: p.loss)[0] - new_p = Individual(generation=old_p.generation + 1) - for k in self.limits: - new_p[k] = self.c_cognitive * self.rng.uniform(*self.limits[k]) * (p_best[k] - old_p[k]) \ - + self.c_social * self.rng.uniform(*self.limits[k]) * (g_best[k] - old_p[k]) - return new_p diff --git a/propulate/propulator.py b/propulate/propulator.py index 3f1f52f4..05b431e2 100644 --- a/propulate/propulator.py +++ b/propulate/propulator.py @@ -556,3 +556,4 @@ def summarize( res_str += f"({i+1}): {unique_pop[i]}\n" log.info(res_str) return MPI.COMM_WORLD.allgather(best) + diff --git a/propulate/setup.cfg b/propulate/setup.cfg new file mode 100644 index 00000000..6b14343a --- /dev/null +++ b/propulate/setup.cfg @@ -0,0 +1,115 @@ +# This file is used to configure your project. +# Read more about the various options under: +# http://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files + +[metadata] +name = propulate +version = 1.0.1 +description = Massively parallel genetic optimization through asynchronous propagation of populations +author = Oskar Taubert, Marie Weiel, Helmholtz AI +author_email = oskar.taubert@kit.edu, marie.weiel@kit.edu +license = "BSD 3-Clause" +long_description = file: README.md +long_description_content_type = text/markdown; charset=UTF-8 +# Change if running only on Windows, Mac or Linux (comma-separated) +# Add here all kinds of additional classifiers as defined under +# https://pypi.python.org/pypi?%3Aaction=list_classifiers +classifiers = + Development Status :: 4 - Beta + Programming Language :: Python + +[options] +zip_safe = False +packages = find: +include_package_data = True +package_dir = + =. +# DON'T CHANGE THE FOLLOWING LINE! IT WILL BE UPDATED BY PYSCAFFOLD! +#setup_requires = pyscaffold>=3.2a0,<3.3a0 +# Add here dependencies of your project (semicolon/line-separated), e.g. +# install_requires = numpy; scipy +install_requires = + cycler #==0.11.0 + deepdiff #==5.8.0 + kiwisolver #==1.4.2 + matplotlib #==3.2.1 + mpi4py #==3.0.3 + numpy #==1.22.3 + ordered-set #==4.1.0 + pyparsing #==3.0.7 + python-dateutil #==2.8.2 + six #==1.16.0 +# The usage of test_requires is discouraged, see `Dependency Management` docs +# tests_require = pytest; pytest-cov +# Require a specific Python version, e.g. Python 2.7 or >= 3.4 +# python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* + +[options.extras_require] +testing = + pytest + pytest-cov + +[options.entry_points] +# Add here console scripts like: +# console_scripts = +# script_name = propulate.module:function +# For example: +# console_scripts = +# fibonacci = propulate.skeleton:run +# And any other entry points, for example: +# pyscaffold.cli = +# awesome = pyscaffoldext.awesome.extension:AwesomeExtension + +[test] +# py.test options when running `python setup.py test` +# addopts = --verbose +extras = True + +[tool:pytest] +# Options for py.test: +# Specify command line options as you would do when invoking py.test directly. +# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml +# in order to write a coverage file that can be read by Jenkins. +#norecursedirs = +# dist +# build +# .tox +testpaths = tests + +[aliases] +dists = bdist_wheel + +[bdist_wheel] +# Use this option if your package is pure-python +universal = 1 + +# Automatically build docs from doc strings. +[build_sphinx] +source_dir = docs +build_dir = build/sphinx + +[devpi:upload] +# Options for the devpi: PyPI server and packaging tool +# VCS export must be deactivated since we are using setuptools-scm +no-vcs = 1 +formats = bdist_wheel + +[flake8] +# Some sane defaults for the code style checker flake8 +exclude = + .tox + build + dist + .eggs + docs/conf.py +max-line-length = 250 + +[semantic_release] +branch = "release" +upload_to_pypi=true +upload_to_release=true +commit_message= "{version} [skip ci]" + +version_variable = "setup.cfg:version" + +build_command = "python -m build" From 0897ad0e5e1e88092946638339401e458957dc27 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 8 Aug 2023 21:03:43 +0200 Subject: [PATCH 033/139] Cleaned up working tree. --- isle_0_summary.png | Bin 33121 -> 0 bytes isle_0_v_summary.png | Bin 24995 -> 0 bytes .../.github/workflows/python-publish.yml | 39 ------ propulate/CODE_OF_CONDUCT.md | 128 ------------------ propulate/README.md | 58 -------- propulate/setup.cfg | 115 ---------------- 6 files changed, 340 deletions(-) delete mode 100644 isle_0_summary.png delete mode 100644 isle_0_v_summary.png delete mode 100644 propulate/.github/workflows/python-publish.yml delete mode 100644 propulate/CODE_OF_CONDUCT.md delete mode 100644 propulate/README.md delete mode 100644 propulate/setup.cfg diff --git a/isle_0_summary.png b/isle_0_summary.png deleted file mode 100644 index e04297c0c6f09768c492e9ee5ee74b7e15814e00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33121 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�o9n#1d|BUHWNTglsqb&OT zM!AS;RJL2?qE6Kog26Sm`KO2~ zFmx44Z~PQMSSyYVBln_lvc3;c?cEI{E|=kRryx529G=<{V*Jh%i3p4IP4IvbPZC#T zV1CHXh8BY_xV^W&Kkw*}_S_hQ3D0|y;9`b;SqC~R2?{k;R`Q{eYP&55s;}GPyZas9 zIE{6g_rI`{{_PEcDvLz4k#u>=Tbb-=Yba-*>dZ!(esPAdO}Eg90x7}l=UF4h)v(7=m?(=MZ>p?!d13kx9*_#w(ej{qtr$1qHHp?dP2d`k+H1MKM4drP2sr=DtkRtuAS1q)Xta*y{)KYVRL*7lJ3r?oC8u#UUJjJK5hB- z?#=o)JQ!FI76IMoXfn>kq$HRY_agmf<9cSvz8?txAWO=du+gyaKY!sBe~0|`Pu{)` z@U(GuE_cZJIN0)Ah~U3pEUzvT4QK-41O!|!-5*WA_}1m&>om&yJ<|)9_d{%W*E;pt z?FudksLCF5Avaf%QBg91fq{+WaNtDXbiC!aG2fgj`wpUFuj8}sTVdaQnrT=z1gcAL z#KFi?H(Cq~`on^j%X7W>>ag8a&yf@N$=1fTs~punC>v{UOnO6ZE=(lOR+A?mDB9a| z&;_5Gp<+ic7)LG_QqMi{)LM#|mfc}D{4)F zO%~4#At!Loe0lhDt6KD?R6{;-BvMCwK9=s@u%N2aPA?cfbzk}vt+jKUX^-rF)>w4vgOlRn_N?BTWxhVd0?4oY1PCib&Y+FM@)H`(x0(bIKd?R z_U{Crtx*8?01gt}Ysal1IOsf}_I{TERC896_OA4k;Luzh=oSK|%gs@KIc?<|_yc_d zuXGw<0Q+cer11}UL*SXaVbIZ}?L_`9&|eYW+dr1SS6L)#q`sf(c|YXuMfa=agc_2r zP1!B1{&`W0NVwH~>a#P(LCYHn zkm(XsU2igSILTuu1Xf+D2B#43!cHa_e{!o|!32qmkI%Dt@&>pl-4O(Dw!t7t*RCoq z%P-2`4~5eHX&yvL{Khj_l{TZ9K&6)&13nANC`gbzru5#Ki<=va zqaCz4#4rY&lE-o#p5aZPAX&xU4?9x{87F+^r&6iUBm5;p(}!Bz^N?Qt7_d&5uft@F zBMgkCVvKam-p3b^klh6O+qZAY>TGOm@*XAMrMgqxc)F?^ShFT*7<5C3jy`+$t{^5Q zpRb`*V9l8pc?f8urbA0LhMGDl2>dY%3DwOV^V^JMn{~eg51I3Ty3Q^`sWxZyK@1Tg z4U}Q5h28u1)B1FvUo_$U+8(_@7f1D}Ojf;|AjEWwu6s>O>(iI@Osh5`91 zjwGo*u^Pcw0sqD&{RmQdBZCN?s@i&G-^N`s1oOm$Ea(xP<&YrD?~@3Z(Cj~LebX?IiKOnybsgP?Z}2p9@#w~3ZP}X zq(ZLk(l#kbpKcQN#_P+MNDmVC&)vY~rJ%~|)`!!$k-lhMV+x-fZ|+zTS&ke?@9{!B zZ`+g?F(Z9RWOqfsYCjUBzrTLFqlL0#aekp7hgt6QJ>hxraL*rnjp3I=e#CqgZ0+-7 zXiPux5Ju?=101iWJ|&7KBVT!~)l>6EMEc^%MoqBY)(Y2&hX;da;~oIV)uVl}w?l!m zjiMOh-dkgQxBaN}_|Wd}uKn}K^b%(X9+LA;-%XRmpnv$QJqndvE3#o6UBt38wmJFI zFZ%WZt|mge77f0|2U+WRY`vz%s~nodnWvCEzk2y88nmh2Uuxi3CQB!+e9Q0pc{D2e zcKp!#`peY)4Oo7SY-T$(A8$_q*qrUjYB|-;Fv<0#`YJhvB~h;)_G^;NC)N4oP}7We zL)KS)3P7u15XeGCYe7;rLZ7+{H z>x00^K(1)fRy9|Q1dD@JyNPHsi4ED%CX~ZPhNcJewb*WW&BWkO*9*Rf11{gBPt2wqAcVMM-`s=_&Y(8V}oEG zMh}TxFWNt%nwdu}2DzHb!(mq-o0o5M*$U@bbv?d#T{oq*uM4gVa^TSTz_g#99HTK~NH43c&f(;>SY6N2GrHofRSMR9=!IrnW%As>_!k53CV^-BV zvXyzZ0K&w`IY63EzDGBHHUJYt@mZp!RFRR$j?JaZCrEq*4H{$3%_nYnMZ`fVHlc4G zKkbQTr;)&)uyUkuYW0rBbqvm+={4L+}TB*b{1GlWO1G2-CrM%$ug=| zgk?+11)-U3mC95{TP`O6!?^B~BFr=_GV{0kSEMh_+nWcL*B9Q$8xRpD5kIx8&f<3C zmbynF6n>2Pbi@&|H;t3A>up3N$tQ&8; zzuu({OFN&P7bCL%Gd=siQkCal}4)UuXY^LV2^r|X8hmtZF!9BTsEbrPeGuci?nBtn~*VV7W2j5TdR`!DM+j7@YmABk}Gu;Xd1=R zzg4BJ(V8&n1ar!TlL4=oH{*mnpD-L|&PqKRe1)SI0}J9$;JCbgG-#e}iG?HvR8f|q z#OqE(zP{~6V;A;@!NZMQ2hRr}q7|~k@GE03wkG!=Qs||r@72f#{WQ6Mfpy7=lHiT3 zHuSQNP+2Z+MCVQZJG7LY>LBwCC?{-cBM^kJUf&L=T>7SM`R;&_p741IwWs{9KX>*w z4GM&DdutJNC>IEVjAw-!lP?PEf+7TMPGqoPoOehOMTxew+Vs0V%_L?orOPb*Gk|L$ zQR7GDy_TPPCBb_=EB*n)c-xa-rRg2-$^f7}_;7VCGLzsl7Lf6H4j^7m(8Y1XuXhf7 z7QMKmY3|5K2@M&yTd6MMk!RG$rlWg@R5z`a`lY%OK^S*EL$H}2y7Z(qH2+&eYT)epP(IsCc) z+~CUM3T~S815jQIse-r}Sewub)+kgt%?btHT%DwedfY|vU8^st%V0GesJi?$@sH%_ zR^#Y(ejY3er2uzuY3!p%!{#u(tHAuxKy65H08EJMJ2qs6s+imy%F&PiUKUNOnR^@4 zB%b_8_NbotNdO61(Rn|lS_J8)$gkk7vL=qVb(X6?u2!558FDe?MiP}t0!+HSs|~Py z;-`{Fq-|GI52rluFb!u&ypPl4te>4PRKDf$5hIIRcXvWmORFdSgj}R0oJ%WeZb+i^ zY@qhw)rp#wRdP(TH_eRZ*}DGgTh|{ojJ2TaqiGXLG0&pzD^#pCo>qJ=4M?9eT~96; zNc&3Eb+w|b3<$FO!>5``4;?Ktz1uv~7P7nB@UTh(Rrh(gyZ<~i2_G*4Iw=Tq?`{yP zpQWgXgH~I^En8tv+rM4DRVRpmd`nTVl zSql%lrsxv`pM~8+fMss7Zj~c7s%%P8wqe#ebk(2J-zecn$xj#E9TR0MCu8JeYm>}X z$>gWV6+S3#R|Cu5{99M_dc4GFxyWO>ujE!u{>Y<0EVE31Z(PVAk^hHwkoiwIW24cl zuU^h&1=^msC#nY5@9ZAfmJr#}r9!Y2PBgNA>jc+Gc|x^mc|9pZzaw2ELrACL(#eS; z5(~YOIFhGmqlS4sVCk!K`;~W^%JG}dd+&$6>q(MAs1{lUA6y?j1?(}pBe9&D$hP#n z#atFK_xCefi~sx9wKYfXkfZmk*DhG zHFn2S6Qw~k3aYDT3-3Q+etqLuUTIW_Kz|o|`s-szj+tZFM4MO~^kCKV$ zgIjW|XZb0o_5;>|jlLxNG1qO66DI_SMi(RqpAZ+f(|lj1?WjDF-w3a?ck@OD?bOP-!45;#Opa)bNp#N}ysVFP& zZ{D>8jd1BU=-`4lQA=pw2{}cBJ!}4l<{gl`ViXs<%i5uuYZoku_Yw zY6u5{+5N#H+UE=Fybi3^PfG1h^xK91Vu3Uv?0h>0xeaE*b8G!RW;|D_G^!;&ccz;= zlQjK9)X=6IH1+tH4Z0(DN*dYWAgHX9S(6z$M1xlJyC{u}kZ4yH9e4cBd*S5Nyu`N_ zu(t{OQaZE{t=u6uJ{T#7`>WeazKkUh#lC%ULpi;+Q0FCpG z29i=W6Qf6FsHO*4ZJjg}@I`eG>oViWt{;axP1EbXPj$vm0*VLq(!c^>*TQ?Hb;sj6 zDChMLRc*Vrzci)>iKxhx+$x;?F*284ZO`2Qqek`yg^#?uQt=sJcdqGLKcdrq%J0Ok z9p$r-v7M2`hm>=eKEY5HYT{2ds`jKeow563`376}7?wH@L|r;0@TihF0VO8abWg;e z@c+#1mag#?mIB_ntM2+xJ!nZu+xjKKtpRV zTh>=r+eKqC>kU{c)5^}-2m*&pOcV$fT)tz}LSbz{XMvKPIK~2<3jw@}0mp3SeC5LD z38kbB;=@&R8d&0=_3j`MZo#n#cx>xja)botzf(jl-Gc5wW;PXY$_rV_AC01~gP2y$ zM0RcpAXqo!;gh-B1Dm&UA`pK4XEHJ?3@M2ERX2r5D01l{gPtUiH%#Mos?$CT%IY(` zqaw-BwTMaRY8FFR_gf00?8#3#T~AV2RG)321Tm`0e6PO6{I(k?Y&jmtN1U_ zg{){X?~E~46E!IaJ|{fZPd6HPhfjKi{n1*Q?FiYCRObvA9BLBWWUI@P!bD_0)+EW7 z6DW3@YU~ED~Ki} zxLi;v?(+{?IT5`>PK(XI_?)d2Mx8tIzjES;8B9R&2l@Pr#QxD~&i9 zU22QVVqVlJwUR4Fp9YR)g1V+g^zFw&D-l}at`8RL#N^klkKCp!={6=mB!LH7F4fzC1!);ew%53k~71al3ms%00h zB7tHg#pem7#bGJL$u?Yj(X-R5dhU;LGEy{!{0cIwoIT15_Gv^5!kv|5CsEmE0oH=g z1SQ^@Y}Y&mV4d9bwwndVg)#{CwpbT-ml7izzr6^jK4}!D^lMeVq6t>;ld0RSubjX@pt@xyov&U@I8m}j#sAO zu`iVql8Av}*77)E!nbzO(1d>=Vu~ z;~4O+_kEwb@9X+qvok&fM%xa^B<)0<366}Ll#4PSWU4KCmqroWV0!cJ@(^L!EZJMH zi>vSc&mA=$HVia~^>Q!WPPv_Lkp#pl@2phHm!4mY5%f9hYO=`|MbShBp|jCV>Hd zEi!7RjG+>6^GI(dYXkO<)_rME$X~UcL?0ToM^a=tS`5N}4QvWJ^-g(+qma^LVx`9k zX(MACps|XA&X(8dWO|S6xPv{fr*;f$H2JPmGtc;*z|Loo9*QK)=`_jKVV$GPk z3fQZcjR7A{A%FaLVGwBxgdm{fo2)31Y*qr%dmrxx!!$|c)$==%m5_Yn*12|IpZ9G! znubk{_6&FZ;zl*azA{9#RzlP)@iCxZthdB$ zLiZTkAAVw74%HPLFmwzLe}OzRNY~j+CEi=IOT1Qd@LN3Mn%^(>*T*nE1vx$7D3Z*y zP?qi1Z%xra+Ii`o$3XWAcy%v#&P|5D-uAJw$vH`plpM?;iRtmVOg0BTbr)~NI2qsX z00u~P&DYO&Ldz+fJ+Xv&UxJszlY!;Y*WFLpumUypyOIQvj~= z{YZqYqY80wDb$B3REZGB#|gS}pr1FLGW|{;L^kmbr5O~ATd1Y!2>)Ds)KM&*#oUxy zXJu@Kw(q_1WR>)zV|iHr5sxln9PLw2)8HG7%Fn=8UC-7(cMJ#Ogk3*+sDaIMqwlrEEDvq{tmx?aY@ci!0COF`h2Oo~8b|KyXARyqarnfD@|JCG zd0lgLuLBpBw#`};*&Bagg0$Hg4Eo?#Xk2GSb)!S5jN4QgkGgK#`wDHv-(wj*s&yY+ zCs|ybq84~`jGJ~Qa$jGd@p|xxhnb_ij+@l8@wa*mq54Zl^0yc=wVPjo&+v1gFT^)} zNLE@#IjD*nO@Y6ohUkR=hO~2WPJBuQpBkp-cyrrEzHWgn?8fm&0qqME$G` zXeE#w?V>=T#065S786Ykfz&Y#pE`f;$T1B>Cs)%{1E;RFo8Dky=(i!`u_xHa^*w6V z6Ohtq#z_DDvwQ|$4P&wAj>dFoYR6k47H4l9>)@wuP+&Gu<1Hi)#qPlnXdbO%+gbjh zV%m0Z@8@ZB-0x7Jzg?cDm!Sh3j1dfQK#5l*kzL%oE|t?!gCr`h<2GL0YggGv&V;#) zKK(p_#rxoJ|H9hj+2(-q7w2;0#ZzAqlQ)C>dfiNP1VsW%1y>o|B|k_JzRV~IiH!+s zqzRG1svzNg|7^Yg4>Vu@mGV{%kN7s7 zn<&_Df`a9Q6+gs_#~q=rClBypPk{woW~{<#TukfgJbPhzY3It zn0smh2*)LM_9h8R82VCYOey`Bm+5aW!?&j|Zy0A!*V)nq6L3#I41CMRkU>L#y`4)n z#KkYw2wx>BUD!o_A;Zx>$kFzMIDba`vUf&)tRR~{pWs#I<@jEG`0Q4-F6;YG>MdZg zgeCQHFn916-+fhomR*2Y+BkHDc2pBn6>0Xhi=W=5HY0g%oB-!xpYJng>@u4jBpukb zz)(>^*XI?BvW<%e0#BFmT6 z(na?vLu1fxcKUypoe)7F%u*%}Daqk+SgzI_jM@&gl8jDMxAqHo8PCH!>DWL|MM|CI zn-=NPX$RNVz>(!bZS^=|#P*+i!!cqY)jtHVjv*GP7A*O8=DV)1sP~CvF_Twu;H(te z&1)et-%F7S9w#+U7PQU-T6lZyc@WD`uB&KfyC=jNeu5F-Dv}u=B(Y&9V`@jZEuboS zzLb$AaOnH!5;P|7gg=CO`tSE_{iK!&LpAe=8{ZsjASCHlzor*T_~=GU%;`_|PM(K8 z4<&cctpR0-_t69U5)Y*IYOWvrr3-8j>tI#jt;(K0=~^T|n&_V^(|d}?`^p8Yq-pD! zsL)C~r3Bu;-Ub;NT6))e790JVQM)7b$#n`tZfEx27OQnaAMKZ$DH$cytjb@n#dc1n zd_rngdA@L_JXE7j9o!sGy_$d(saW>r#4lYiE#^Ih7|qQtR?d<7yYgqV^Gx)oVm(&n z%ZtfOo8m@F*Wv_5 z-17fG)~sI0HM#HCTbihaV`E_z4!OXXgmy+u(9ED7As00}pmZc>_0R_|kHmsT!!Y;u_QaL!-lb#=sCI~>58!Km%X zJ4sGXTt0cy(te|wlJ&=iW#;opD?9DI6Lgt!tkCp$+Kl7bN{;+j)1#^+P`NjsX~f+N z@BJFPk-J9ASfVawz~3NpmtH%yZzU~7Na1%536}ZeL&A0GiJevWKM6jb6_3(rICbzR z?dsX)G(20Wj#ajFJtv)SqEC|agWHa6OT^G~6Ft_F&$-TnLJ*DP$$BqN+WiyzbcqDR zn67G77)(@Rn~yF1%DW!y<7(J$vE$Bs$u6f*Df3QnH5A@#kTc|GQSfExtd?{3Zw&DQqC^9r>3o=v9_!_JF;WA)k3aiPC3LU7$sPG3Lprzx+OGrU}c=s_RmY z=$pfe`~~F1%+O0&a7kxHBJeSZ;@U>l>HD18G=9h+jaFa6o+U*DqNOjSry_ z;nc)ZS$;}_DjCkA=7B1NmJ|as#w|}do{^ws1Zs%Xj&fA!z*6LqId82Vxp;MNxzJ$e zbDW1m9ghBzs<}kY=|@87VG#>0I=`VqvKk6aH+#>1>NXY{)Ha)0RB&DHJeth>6wupy zR;Wd2$wfJI-y;qxt(nuhJeS2 zrCS(1=P@drJ^?96xYl8->EpW%Am=7uOCKma+H0G3*ngAU2`mPx^@e-OceB1Y&g1(~ z6aBoE?5Nh`kF2$31MLX_SOWua%f7IfdQntcs_y5P@`N5omENV>5vv!>=jcqeo+g@f zUBRf@%urGW2$HDx06oel9F7bgfD00CJ{gerXy%GbwRraRxu!1NGkL}p##!Q?u$wZg zuucIo_}+d;T;cA5*OS3TK-BW{6R)vfhy@@eWC(&i27a>w?)Ws?iF5w%HIt&*(*{0JoeL%un{IP> z$?%6mh?Y*=gFV#6cI#LOJ!WU43RP{G1w3WA)mpKwwB5l$73$$~pN7+~_E#I}cwChQ zMCd9Ru4ar*Nyeq9O-^eH|MX|h5XRZ7zgtOuViQC~h8rhX+~7IsRGwH)v7^dj;B1dT z@9!oc!6=`2e{Jmj%p=P+U+=ISVqoa!(X;4l2D9h;_>8#s5TkzR`-${(YKH<~Zz{ z(AqKlmkp>56e7Fb2Q3%y|Iiw>0y|C-i@zWAB}=()HwRlrIo5`XIpB1Wghubg;c08y zuWFk*V%mA0dgs1PFFh>LP)C{Ojqd!@+tZP?fx*S7s-~wA`CZjF%2-@CaAdhvKFytW zk~YrQQQ|d~*SIJ=QMKWB-ro*QF6gEsj9lqv*GQ(@nLe>l{9`Mg@l8$pK6MU+ z9_4WH+knIMq8%_1^WFK}wjf}s7dyn?a_dkMdYd9*uxq4-S=HFBro!9iCtN!H--=-c z4OffH@@U(10@O-y<&%{}kub9t0u%Pygz9rYzy`*+@GR?(LFCsA%ADsXD8)=diD@6h zTD1;9vm@KQ_G`Fm?3nO}3E{MaxShv?lBSL?Lm1W6QKXQSQ*S%=!f`PFFn@8r^SW(G zG(Jbl-M;i#cCpc`1A;W8O>~!XAR99lSOL?n<7N8E-Rz!ADC#QL}j{19j zj3mWO!m&^9!%hCSe5ER(n3>4|rXY+cyl(Rx@5M;&W}TZcwdO+tpW{QpO zqw*vYZqxRQzwtX_H5W`&zmZ>X{18fsl6~c0I+U``_+8=Wy$hzDw`q~0uhs8$5<%PB zS9vmWd=@(ruFSX#svm_FI`{?X>57snm`f>~C3M2U9NX!=a~I}?n{6o0Nf_T+nW%TTQh36( z(Tv(Bl!6WsBq5L8m6tR5nHz6V-Pw6WRu&eYcb&Cu>D0g2zSKoj8^Jmh^nK7LO55IdA*aMplMob1}w`;E-CdDaRYJGbA>^j(rVd>uSs+d;p>K<>Rjdto2Rp6gS^b6 z{EZa|p&4lt@~w$)IWnBjGp`+AAFzZu!*$u8j^8(+TByEla4}-c25nZ+KRXYUeAo4J zX~cs@mNnjd{@w9Ydt&SE#nBqKsSoV5d&ql9mZm<5^HbCS= z2FAg&b;-rcJ8J5Fhv*lt=K;CMD6@_yuHM7!QTQ32#klrgtOoqO;NTudjtqez%QlNR zgSdcrj``dfTrF}3T1D9AFPAkLrO z4?l7bVdS4DadM$jvPy)9Zw8(`5YjpA%v3qx*j;eJK(D2Je)79y4)+M`pj1Q*O=ZEN zU4D>AMI2uU?nvN=VtA2Fa3Lz!Jrau1Uh<3IU5~pLSJ+EU=h36e9JZa_Sgg`NpfKC@d9HI9QRaXUn)Q-fFxs$1&sG$D-Rrul| zeaeP)v*rt#vXgxtD8}0MQ`#8f=M$2)$`4thv?bpz=x;eP?zR#zL>rDNBfOmdjT{2&krP77eKm&wn~HfcyxDElN+?zy>>z=`baU|4y$q~81d+_w8C!p`f(cSZ&tJOG zjlWz;T#Y%)e`|8%UFitwOOXUMe%4T~O_x-s35S#g2Rq|(>^S^XgDpqIx)3_=nWwmM zxATIM_QnD482cB2l%!EzjOnw@qj6vD9E%+}j*Nrszh)0E&ek%^Z)V1r`Sh~{wz(IL zQWnTYCRn8BxkLFR@GYI>powLKf=D3} z{EKpi{1M;ELOrr?=?*4jXv2X7sK>UVibaSFXY5w`;3i54bT9$$EWyttc#( zNAoQkX1O${(Rhmkt%gC}qgoJ2U2yKHC@6^3w)31YlVkgR|As~Dz6pQ^3^j|$NVTFO zQ+k>r0@JsDp}xFh1%B}Bh1&;0sNE179;_D-4QL)2>*os}B)reU8zVycrTtXA6q!y(Cxfn zyjgX^6WkW4nleuBy7%#RABrKuREPv2D$|33O?;!b^i&k%qxGg*NJl#rC>%yc{nWWS zwE_ov`&v*CUnL=#F|5=wx;SuewFfge((}jBUJj|9t#pwp?Q3RU4NbJi>D7&DZ#b$m zVc>M!4O%UAE-Da@lae^wLi*d4nrg5}gVn`;_A-880|%^{`m9fw%5_B{GgP}#_9Y2= zW4&qMPXvFS&g`s;gGq`(<{}Dog(0WZ{c`tE2(YIFgnJkkT8e*>P5Ej$oQL&lQnURo zCahoYPBRB@ypLW|VRMjF@*7bBto!pRy46~_m6Sq+^h~U9lPvP2-ZY+#_-c>l87cWE z$8D2_Q8NI+4fqu{ml*ETz1^BFVF9Wb{WM`mH^n6x?z5{@@BMg@S>Kkh>to-(PI+I` ztp9p>AMay8LnE02cIe4k&wb=XKhos z%B)(eo%#Elq`U%J=kI2Xh5Wz9j^@5|ki*+T^yiSLb50-=cJ2vhRrdp`=D{44jDIov z-uEIULb#3%6VbPwU{7u|60sFExg37%!5)}Ms!u%pwuA(Z{*|Gawt(v)oXf<|F}v>2 zfc9O%g=%EDxqX)ljbgaP-g?)lk*;9d(h8Rq%;B5!Cab5@{E?l;@+zI%S&s||p~X}U z;uz4f=AP+u+n6(7{fURKKi2$k6#Vpgi>nYEv(k3;#^G57%FgCk&+1)_j{U&f5&-Hw z9bL7(qc6BkPf6`+Vzg%{KY*P!jci^@T^Rp#X`vE6? zl|`5_8MxsNDo*v5W5c8Hgk^Biif&$V#?^Q!we2~X^XRgK=E&N?1aL@4w$$PF2MfYQ zhJzb#gE7?eW+6O+3z3}=Mv5F;_l4Lz;JjdO=ZBRp zwppYrVIhM=?f!jhjXhC%!Yiv4`yZ2`NC=I17^+Rvbj z^AO+Z=-5CXb^AxqI~{&A(r2m`4B+k|+c#6cWZ#+SuW!aJU7$vhTCD)bJ(KAV%4?6c zS`wc=3Ux2dP+5}EY_#z$s#a0wtDbu{mE{Sw^WW;x6MN#xd4}}RX&Cobn2}L1%fLT7 z$|X`VEwsEYTl#c^8mJfjttgU9YaRxEv4iu-~U!OZMI#X+ol< zo55$Ntg9)2s2gg)BDNIymv+qc`;&D?S)f6URb9E|w**@Ow~7&)_PfYB$h*>u3qX*y zV@c^os~l!TxH;88_+cfdz_$gog%;~e$)q7`k4s=ANC`%9TdK3+>!)T`>1j79V^*@y z-58*O%3Nrgwtj;0cjt3XH&yGUSTnAXl0m5$KRu4+g3*LB{kk3$c1H%LOQ8g#v{^p; z^YE!UYLj0j>icig!2~ZrV>R(HpK)H^w_K9a-+D+8QZ05h7-nU6V6DGVNE~fLNl%>Y z-4lsM2#@i{ARtea_H#obkwUQt5$`5_j!M@8#&ZW&E;VuoN$TDjsdSUa?N^ z2A}=iN}*ICb3DJa25MQ)Zm^PH8BHMHd#wxeZY@reFw3k`fNp{(JZT0$2ULTW{9s8s zt>-A9JLPQl^cWRX=#JD5{8xGf{AIxpg<}uqU+Nz^9Qd1!F7I(X@16xTzepmI zD6TQ47fwj>qs9O0LFqhi9G)#YV{1NEup9>n;F5#+<<7s1QU+hjoRy;vspis%1!WYT zgT=!wrCRFtEvBA)wqmaNnp8eUejx-Rd37n;B^%nnl4LIgG;oNbcsJ@>UK>L??dZKd za%XIn>p`V-;Z!YCAxTh7W`@49^CRa`^<5va9k2R1tB_e};fzI}=ko61jzye*OmxCW zt_gg3#~nePPG}ZMN{7oK8x+rC*jdd=M$-&CpKY@Ar_{%A-jqxA#;lbayqPOSpv~zij-R`Xl!cnv027dK-Qn~vC zyPgQ7G}cbqTFQ%x-y+HMgj$ow_R9pL2&ZeUYwYSAG-Tf}D%G!v64-_&>hz5Nrlwk|w}O4(t|RwuFlV!4Q*2~Bi6*UmD^DZo8y!(_Xn8zP zP4O7D9MRb5l##-V#|AX4P$491T}=tcIxik@r0to;vZc|%A4jxkZQ2_aQ@CS>FtycF194y+4Z@#<7(-O z+rn)j8R?U=jE;&2KHQ#AMLR+zeT}_?L?QzCRvgn}mhS}htQQKE|BO{Ds?s#tvCqj! z+&;b*rs8*0?{T`eGt$QQNq#NU8{DuZvRX~^q}WEyb1mHAVsnl9Q!CC97U**%x!Gq! z)mp;c-{c(|QF?c5SfR-mTw>e<4>7W@A`W zY2y$)jwsshLuV6R=xFzE?M1~+p4r(rZ~{jEXLWM2V*He9r|vS(aLWt?oiRehHRx z?#}X)HSolHhhO%x8zTavuD-my7Vefod2Agd*Q&>8=P0s-4_lE3zN);1X1-}%yTYB;DTohTabCeayCgmEq7 zR%>34iBIgCZPjh;(WzUNCpTmBr|5CJ?{O5(`cW8*c`^~@RMTOMW|=bMyV4Wl|%dXs9M;c|a{(c8UFzWxjfn%JFdw{OEjkgS)ld7;W^Bp(%N zXqixlzU$snN+jn$?PKri@*7;a37NSYSKQZ1SN_#yc5B@BXU?AK8Ao>ihRuTD7aOh+ zCMn2XnxzDfOz+pE!S4mHNUGj-wFXrO)WUsr3U?Z3A|)0N@WxVZ{2gadcFrWp8=^{gII_v?KKviYI>~qv})H1^5d18k2WcTK&aN5&o1-wdvympLE+PQL$y`z z4Di@C0}oTx!iwo5jh<&(H#q*Oi9(wlnb=}CkY zY8$UDGuIHSvZp>+f3sid((^Q3BuV$|?V&~iO!m17!@7UPpFHOtx!}cqm0wwcskQ3) z7zV>%Sip1hC;r3CK_sRx;3C+Y%ZL){ZXeP&L@)5Zvx{VtZmUZ|+ZNlHSz{%!^U zjgS@1_wikAHiF-u`t!ONns>+(8s58$)5RIxvEkVP-@n_8q7n17Tgg;VFS=4_ec$eg zP%W)+2=Rj{yoF0WScHT-owS~o-LH-bX!G;9-h5@a8cL%7Pv^W*R*V>&_GbIoQRa+u ztSGE*X}Wn_$l6sWv}T#J#gWbKF3zH;t*znIziS?d zERm|Mu?oFftDTXHLhl^KC^U##2?2=|y)YX^=>kv3$}><)Tf@!dU{hw;io0eXnZUXpB z)8(#+e~?+OJ6gH9xh8$@8T$JAhJ-=zg8wmFZly%B)MDF~^FH=!7bpVt*1ytq10SFL zY3zYe1S!wG%T~I)iiMU}G>StJS`M4_tGL33#h3+S$?KNlftoE`;lGU_%xLxkYdOJ; zPidn$bdC?gd3#Lq$<@lB<`;=&7wldDaSe0s;ZGKDB-jR zVNe_K_3C->d9D2xrg&(^eq3Kt zM5ZvbJRC6I26A;oJy()ak5-jw`C2Uj!@I@z>a7yxDE_aVUT`llY$qUR z<1d_tJK}Kr=2No$`VCq{=n(|2Dt?IqvZC4$sYCgny%bhl_{eoaKHiF%Oa02@(Mqnq zm|`LcFkdL@7!tYCzD$Q*-XRTMK7b%T|548Y=OP$Vs~Mb6Q1q1n<=*1F@b1L^Z4XQ*fycuZbZ?cuZ~znC_$ zDXZ<^#Mdg%t54)8%BuAQY+%jRy~r_W2L7H{C9R@)j|t}(1>3^YN_K<6SFS5b=6WqV z960t7m;^LDYMmNobrOVm6M!5>xpem)41KTfyyJ!Mkqxw|u+OIUmCq_|SkEfJ7&nTn zYpt%UltWb<}{v+D)k=t69FkQw&H_;Y;F$~ra&sNv5d?s&jXS}9R13W{Jpx1$LhXDCPk}aKp9d$)zCAa;26PB?V zf0`-(y%-VSzt|bTM)F)I^-Ow`hC4+MG-v{_D~l9e?8YI|4K;tiwb(hm(Cp6kKHo}sj`-GCX^eocGzq?5)7U^bDS_xb{worv(qeEKz)_^Vyo3>{ zIKbl#`+kwrKukgz?&hoajwT4mt}HHHu}zeZXiyMnkEPR@xcmTs!(%+w{;xW#f07#d z{=2Zvi&F)9K37M)Js@2DN8qXsGim&4@4-?B9RPq5GPzAkZIzn>1L4`rky24k7#gZ&uZaU5}*@a{6HA^Wg#* zYq1$2e5_F03^*}C>{2iR6y!*;*BUECAS_>)n3xziI9-4_?s&NP+_vMYzTZz95c$1B zCZ>kINLOR-Y#@dwAZ#-5@{*L7mxGu^lCy&a}AtwiHcGy`X6b^#;lD#v0fnC#1 z=><8UxBl0sbaV`yl$bTkg8BoJ*YD7^Egd4Pj<3H)lwG?nfLM&OV2 zYgGzUS0VZkF%YGPSz|L57=R2Ui?2vf5eT6M*Bzi6j-?j$AOIJH0JYe)T)qX=7pa{W znh`njag2aTFuTJBqT0l~{x0tPlrA=?Cj!p}fw4rSV!e6$)(YIw)zwu91cWz2APCtN zK-JbyXn`Amz+|`7h!TKKsnhUc_Y+PAle>1?E%m7SlOQ7y{c! zbf5dI$4U$U%7Q2b1ak_I^}7Mmo7Ca^tg}717HV- z0dOBd-$u}qh=_@~t^P3YNnNv8v=#!U2vA-<0Jok7+&#yOLsQYCE@A)}d=QHBAluaH zp7(zZd>FVo4G9Zl;^HCzX2rUll14<(o{X$40~;F-A~MtKbUWm;n738>=chwn6whFP z!6+F3$U23bVdh3K-X=Cqw_G&p!{_R zirSJHpcQH;%xH3fi!O?eib{B#@Dh)LOLBjq)p%yeuYc;GpsY*{(Aspy3$;}#anPo| zsWW+kEVe%IDi@nwKk{yU0&xPaPxD?9QqmtF@OA(MExR1FJx#n=->0+(vHf69C9Qh=^K5^eSiTjfS;^J>#)GxXUk`!64JA zc!PeFYO!5SW444C)9Fb01(0Af}Et=H^K>-r#yba1=Q}UiRKxo*=w3 zO&~PU{5l`3aR~G#2zhMZpKCnY%!ykDp)TzR9+=qrD^;-&Z6i-qupN>Gkr(eXGvk&z z!^}X$AtLlO6ptnn#8QDn95DorAWb1cE{?j%!3q}&I31L;D}hhPu2Qr=73Sz_4e zaL|0d43Z1m08Or+M!@sK`a1D@G4K|vjHFhrdI>GK&In-Up0d5TC0Ft{9#s!AkjC|us03=ROY{KvF3{hE8Q z!lr;J9i*7+8W^wz1qQBy;8ERuYWF$Ul%}n(X}O6SB|7C&(Dz_dP|WKM7Us3Kt~Wi0^(kP(VV7la8TaG#if2FGBOgp8cP*}2?+^=l$3gB zo>Ed!0Fi@0!0nYXHDy4L7QVn*kx1g>>k`N3~X%HYjI%W3#2{Td0U%uUdU0a_Y6C3l2&$X5fn$iGaiiOw-mKsC9)*|}FGQc)6AVYE zUa%B_l{$ap-CP47BFq*6=n`=UEKq}fX&FrfbhnSm=kKumCiwISa2|qdPb2IC&t?X! zKw<>x4UxzR2s7UZ?NB*lE}#`_=wR3P1_m^4@Zk`n62!!&7`ix*8aQio+s<6V&42v&5MFt|+f(hIpT!zy!(TIIHy}j)qEe$@l@ZL1n*;y*PVnN!01AD_n`d9Z z1OVcfiF)Gb@EK6ReGs_0Ubyc;WUzu$1*5n)EvNzrEiW$@+~^=ukO5pg4EWI&`{J3v zhFKDf^`oCWgMzmW2&5Y#@y+XeKL&ux5YWTEU%%Mhe9XvQUy0+1cYP3@6Yap!R$^LKM1<3h_&Ls4fR4$ivO=eaX;Rp YN2gQw4-q8hLBOwp;S!>3n3(w)Hz4yKE`?{~|Iw$(MngS*o85#sZm`aMWnh*pZ1VM01C`jNP z{_(jr@I%;5PS5Ry(@Qr`b60EVnYo*@y_1{$YYQ3=Yu7igog8^N1v#Iv)7ZGVIlmF% z;&S-k4&ZcheZ@74h7t)bg6gcO{|17v&0+uGiY1C(Ly$R+lB|@L*N44%Pe1js&k z<3FE6KZpL5VyWAmf#=|mu_Dx`qp6`VlQRlZq>MtClarEq^!3r@M6N0vAwk$ToDv!u zR>Sk%FoF5kt`|3jcq|UTKGUcFH5nbC#~xgNJKFw>p$@beK4 zE-r4e!xIP({4Bvhpn<*o#{K`l{D04E#J&Cb=(lwW!}7OOw!_oYFd7u)bQ!RGsWihp zHMOnP=Z`SN3a*1d5J|+9u(r?)Wr9xd@9A)_zD47A!m%LI1q$ z`Ja0E0p)!u`=U+P$nU~I=I;@?I*}%D4ojsjB_5r=Se3FqlUR~CV`EMz1%DRx_rym{ z#A7`LD!R@Jr1{*=y3pxYmp10}4E&W-!g@$&T`sRpo$+LQFBcOSO|nakch*%ls&51B zT%NW<`CrAOV#%?w6SsfpIPsAk9X?2hSQv>)X!;xw!M?UP!2B zUy{t#IZl*K=tVi5S9Ud*r7v%<#L

7Fe6j!Y;oqape9Ji-3T1yu^jOzb`JV_U)Vc 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 diff --git a/propulate/.github/workflows/python-publish.yml b/propulate/.github/workflows/python-publish.yml deleted file mode 100644 index acd6590a..00000000 --- a/propulate/.github/workflows/python-publish.yml +++ /dev/null @@ -1,39 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries -# see https://python-semantic-release.readthedocs.io/en/latest/automatic-releases/github-actions.html - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload Python Package - -on: - push: - branches: [release] - -permissions: - contents: read - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/propulate/CODE_OF_CONDUCT.md b/propulate/CODE_OF_CONDUCT.md deleted file mode 100644 index 874784a7..00000000 --- a/propulate/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,128 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -marie.weiel@kit.edu. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. diff --git a/propulate/README.md b/propulate/README.md deleted file mode 100644 index 776bc80b..00000000 --- a/propulate/README.md +++ /dev/null @@ -1,58 +0,0 @@ -![Propulate Logo](./LOGO.svg) - -# Parallel Propagator of Populations - -[![DOI](https://zenodo.org/badge/495731357.svg)](https://zenodo.org/badge/latestdoi/495731357) -[![fair-software.eu](https://img.shields.io/badge/fair--software.eu-%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8F%20%20%E2%97%8B-yellow)](https://fair-software.eu) -[![License: BSD-3](https://img.shields.io/badge/License-BSD--3-blue)](https://opensource.org/licenses/BSD-3-Clause) -![PyPI](https://img.shields.io/pypi/v/propulate) -![PyPI - Downloads](https://img.shields.io/pypi/dm/propulate) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![](https://img.shields.io/badge/Python-3.6+-blue.svg)](https://www.python.org/downloads/) - -# **Click [here](https://www.scc.kit.edu/en/aboutus/16956.php) to watch our 3 min introduction video!** - -## What `Propulate` can do for you - -`Propulate` is an HPC-tailored software for solving optimization problems in parallel. It is openly accessible and easy to use. Compared to a widely used competitor, `Propulate` is consistently faster - at least an order of magnitude for a set of typical benchmarks - and in some cases even more accurate. - -Inspired by biology, `Propulate` borrows mechanisms from biological evolution, such as selection, recombination, and mutation. Evolution begins with a population of solution candidates, each with randomly initialized genes. It is an iterative "survival of the fittest" process where the population at each iteration can be viewed as a generation. For each generation, the fitness of each candidate in the population is evaluated. The genes of the fittest candidates are incorporated in the next generation. - -Like in nature, `Propulate` does not wait for all compute units to finish the evaluation of the current generation. Instead, the compute units communicate the currently available information and use that to breed the next candidate immediately. This avoids waiting idly for other units and thus a load imbalance. -Each unit is responsible for evaluating a single candidate. The result is a fitness level corresponding with that candidate’s genes, allowing us to compare and rank all candidates. This information is sent to other compute units as soon as it becomes available. -When a unit is finished evaluating a candidate and communicating the resulting fitness, it breeds the candidate for the next generation using the fitness values of all candidates it evaluated and received from other units so far. - -`Propulate` can be used for hyperparameter optimization and neural architecture search. -It was already successfully applied in several accepted scientific publications. Applications include grid load forecasting, remote sensing, and structural molecular biology. - -## In more technical terms - -``Propulate`` is a massively parallel evolutionary hyperparameter optimizer based on the island model with asynchronous propagation of populations and asynchronous migration. -In contrast to classical GAs, ``Propulate`` maintains a continuous population of already evaluated individuals with a softened notion of the typically strictly separated, discrete generations. -Our contributions include: -- A novel parallel genetic algorithm based on a fully asynchronized island model with independently processing workers. -- Massive parallelism by asynchronous propagation of continuous populations and migration via efficient communication using the message passing interface. -- Optimized use efficiency of parallel hardware by minimizing idle times in distributed computing environments. - -To be more efficient, the generations are less well separated than they usually are in evolutionary algorithms. -New individuals are generated from a pool of currently active, already evaluated individuals that may be from any generation. -Individuals may be removed from the breeding population based on different criteria. - -You can find the corresponding publication [here](https://doi.org/10.1007/978-3-031-32041-5_6): ->Taubert, O. *et al.* (2023). Massively Parallel Genetic Optimization Through Asynchronous Propagation of Populations. In: Bhatele, A., Hammond, J., Baboulin, M., Kruse, C. (eds) High Performance Computing. ISC High Performance 2023. Lecture Notes in Computer Science, vol 13948. Springer, Cham. [doi.org/10.1007/978-3-031-32041-5_6](https://doi.org/10.1007/978-3-031-32041-5_6) - -## Documentation - -For usage example, see scripts. We plan to provide a full readthedocs.io documentation soon! - -## Installation - -From PyPI: ``pip install propulate`` -Alternatively, pull and run ``pip install -e .`` or ``python setup.py develop``. -Requires an MPI implementation (currently only tested with OpenMPI) and ``mpi4py``. - -## Acknowledgments -*This work is supported by the Helmholtz AI platform grant.* -

- -
diff --git a/propulate/setup.cfg b/propulate/setup.cfg deleted file mode 100644 index 6b14343a..00000000 --- a/propulate/setup.cfg +++ /dev/null @@ -1,115 +0,0 @@ -# This file is used to configure your project. -# Read more about the various options under: -# http://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files - -[metadata] -name = propulate -version = 1.0.1 -description = Massively parallel genetic optimization through asynchronous propagation of populations -author = Oskar Taubert, Marie Weiel, Helmholtz AI -author_email = oskar.taubert@kit.edu, marie.weiel@kit.edu -license = "BSD 3-Clause" -long_description = file: README.md -long_description_content_type = text/markdown; charset=UTF-8 -# Change if running only on Windows, Mac or Linux (comma-separated) -# Add here all kinds of additional classifiers as defined under -# https://pypi.python.org/pypi?%3Aaction=list_classifiers -classifiers = - Development Status :: 4 - Beta - Programming Language :: Python - -[options] -zip_safe = False -packages = find: -include_package_data = True -package_dir = - =. -# DON'T CHANGE THE FOLLOWING LINE! IT WILL BE UPDATED BY PYSCAFFOLD! -#setup_requires = pyscaffold>=3.2a0,<3.3a0 -# Add here dependencies of your project (semicolon/line-separated), e.g. -# install_requires = numpy; scipy -install_requires = - cycler #==0.11.0 - deepdiff #==5.8.0 - kiwisolver #==1.4.2 - matplotlib #==3.2.1 - mpi4py #==3.0.3 - numpy #==1.22.3 - ordered-set #==4.1.0 - pyparsing #==3.0.7 - python-dateutil #==2.8.2 - six #==1.16.0 -# The usage of test_requires is discouraged, see `Dependency Management` docs -# tests_require = pytest; pytest-cov -# Require a specific Python version, e.g. Python 2.7 or >= 3.4 -# python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* - -[options.extras_require] -testing = - pytest - pytest-cov - -[options.entry_points] -# Add here console scripts like: -# console_scripts = -# script_name = propulate.module:function -# For example: -# console_scripts = -# fibonacci = propulate.skeleton:run -# And any other entry points, for example: -# pyscaffold.cli = -# awesome = pyscaffoldext.awesome.extension:AwesomeExtension - -[test] -# py.test options when running `python setup.py test` -# addopts = --verbose -extras = True - -[tool:pytest] -# Options for py.test: -# Specify command line options as you would do when invoking py.test directly. -# e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml -# in order to write a coverage file that can be read by Jenkins. -#norecursedirs = -# dist -# build -# .tox -testpaths = tests - -[aliases] -dists = bdist_wheel - -[bdist_wheel] -# Use this option if your package is pure-python -universal = 1 - -# Automatically build docs from doc strings. -[build_sphinx] -source_dir = docs -build_dir = build/sphinx - -[devpi:upload] -# Options for the devpi: PyPI server and packaging tool -# VCS export must be deactivated since we are using setuptools-scm -no-vcs = 1 -formats = bdist_wheel - -[flake8] -# Some sane defaults for the code style checker flake8 -exclude = - .tox - build - dist - .eggs - docs/conf.py -max-line-length = 250 - -[semantic_release] -branch = "release" -upload_to_pypi=true -upload_to_release=true -commit_message= "{version} [skip ci]" - -version_variable = "setup.cfg:version" - -build_command = "python -m build" From a33d0ca579f7030b8cae630c89c431712df9e355 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 8 Aug 2023 21:46:33 +0200 Subject: [PATCH 034/139] Now, everything is fine. --- in_case_of_update.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 in_case_of_update.md diff --git a/in_case_of_update.md b/in_case_of_update.md new file mode 100644 index 00000000..bd3250ec --- /dev/null +++ b/in_case_of_update.md @@ -0,0 +1,17 @@ +# In Case of Update +As `Propulate`* is under continuous development, from time to time there are updates on the `master` branch in the `Propulate` repo. + +In order to be able to update `Propulate` into this repo, please do the following: + +1. If not present, create a branch `propulate` that points to the `Propulate` repo: + ``` + git remote add propulate git@github.com:Helmholtz-AI-Energy/propulate.git + ``` +2. Then, call + ``` + git pull propulate master + ``` + in order to pull the current state of `Propulate`'s repo's `master` branch into your own `master` branch. +3. You will likely get a merge conflict which you can then resolve by using the built-in tools of PyCharm. + If you are not using PyCharm please refer to the Git manual and look up how to resolve the conflicts. +4. Afterwards, commit everything and push it onto your own GitHub repo so everything is fine. From f62b411a7f8782ac2983565defdbdacdbd87cddb Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 10 Aug 2023 11:27:05 +0200 Subject: [PATCH 035/139] Bug fixing: Updating the Particle dict features correctly. Prior to this, BasicPSOPropagator updated the dict features meant for position with the velocity, while the VelocityClampingPropagator simply omitted it completely. --- ap_pso/propagators/basic_pso.py | 2 +- ap_pso/propagators/velocity_clamping.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ap_pso/propagators/basic_pso.py b/ap_pso/propagators/basic_pso.py index ca2082bd..f35d523c 100644 --- a/ap_pso/propagators/basic_pso.py +++ b/ap_pso/propagators/basic_pso.py @@ -51,5 +51,5 @@ def __call__(self, particles: list[Particle]) -> Particle: new_p = Particle(new_position, new_velocity, old_p.generation + 1, self.rank) for i, k in enumerate(self.limits): - new_p[k] = new_p.velocity[i] + new_p[k] = new_p.position[i] return new_p diff --git a/ap_pso/propagators/velocity_clamping.py b/ap_pso/propagators/velocity_clamping.py index f38ac615..2283e639 100644 --- a/ap_pso/propagators/velocity_clamping.py +++ b/ap_pso/propagators/velocity_clamping.py @@ -36,4 +36,6 @@ def __call__(self, particles: list[Particle]) -> Particle: p.position -= p.velocity p.velocity = p.velocity.clip(*(self.v_cap * self.laa)) p.position += p.velocity + for i, k in enumerate(self.limits): + p[k] = p.position[i] return p From 4a86d88cafa5f3d7060fc5f66e79ae06e0729c13 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 10 Aug 2023 11:35:11 +0200 Subject: [PATCH 036/139] Added some text for clarification. A comment on the field BasicPSOPropagator.laa and more debug message in case of type errors due to unexpected indiviuals. --- ap_pso/propagators/basic_pso.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ap_pso/propagators/basic_pso.py b/ap_pso/propagators/basic_pso.py index f35d523c..835a4120 100644 --- a/ap_pso/propagators/basic_pso.py +++ b/ap_pso/propagators/basic_pso.py @@ -29,7 +29,7 @@ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, self.rank = rank self.limits = limits self.rng = rng - self.laa = np.array(list(limits.values())).T + self.laa = np.array(list(limits.values())).T # laa - "limits as array" def __call__(self, particles: list[Particle]) -> Particle: if len(particles) < self.offspring: @@ -41,7 +41,7 @@ def __call__(self, particles: list[Particle]) -> Particle: old_p = y if not isinstance(old_p, Particle): old_p = make_particle(old_p) - print(f"R{self.rank}, Iteration#{old_p.generation}: Type Error.") + print(f"R{self.rank}, Iteration#{old_p.generation}: Type Error. Converted Individual to Particle. Continuing.") g_best = min(particles, key=lambda p: p.loss) p_best = min(own_p, key=lambda p: p.loss) new_velocity = self.w_k * old_p.velocity \ From 70c424388fe4d8fa2aa34b8f72578785284d792f Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 10 Aug 2023 12:00:21 +0200 Subject: [PATCH 037/139] Added ConstrictionPropagator providing support for constriction PSO. At the same time, changed the way the call function does its work by outsourcing much of the work all Propagators have to do in the same way into new, private methods. --- ap_pso/propagators/basic_pso.py | 33 +++++++++++++++++------- ap_pso/propagators/constriction.py | 41 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 ap_pso/propagators/constriction.py diff --git a/ap_pso/propagators/basic_pso.py b/ap_pso/propagators/basic_pso.py index 835a4120..0c5c4f0e 100644 --- a/ap_pso/propagators/basic_pso.py +++ b/ap_pso/propagators/basic_pso.py @@ -32,24 +32,39 @@ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, self.laa = np.array(list(limits.values())).T # laa - "limits as array" def __call__(self, particles: list[Particle]) -> Particle: + old_p, p_best, g_best = self._prepare_data(particles) + + new_velocity: np.ndarray = self.w_k * old_p.velocity \ + + self.c_cognitive * self.rng.uniform(*self.laa) * (p_best.position - old_p.position) \ + + self.c_social * self.rng.uniform(*self.laa) * (g_best.position - old_p.position) + new_position: np.ndarray = old_p.position + new_velocity + + return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) + + def _prepare_data(self, particles: list[Particle]) -> tuple[Particle, Particle, Particle]: + """ + Returns the following particles in this very order: + 1. old_p: the current particle to be updated now + 2. p_best: the personal best value of this particle + 3. g_best: the global best value currently known + """ if len(particles) < self.offspring: raise ValueError("Not enough Particles") + own_p = [x for x in particles if x.rank == self.rank] - old_p = Particle(iteration=-1) - for y in own_p: - if y.generation > old_p.generation: - old_p = y + old_p = max(own_p, key=lambda p: p.generation) + if not isinstance(old_p, Particle): old_p = make_particle(old_p) print(f"R{self.rank}, Iteration#{old_p.generation}: Type Error. Converted Individual to Particle. Continuing.") + g_best = min(particles, key=lambda p: p.loss) p_best = min(own_p, key=lambda p: p.loss) - new_velocity = self.w_k * old_p.velocity \ - + self.c_cognitive * self.rng.uniform(*self.laa) * (p_best.position - old_p.position) \ - + self.c_social * self.rng.uniform(*self.laa) * (g_best.position - old_p.position) - new_position = old_p.position + new_velocity - new_p = Particle(new_position, new_velocity, old_p.generation + 1, self.rank) + return old_p, p_best, g_best + + def _make_new_particle(self, position: np.ndarray, velocity: np.ndarray, generation: int): + new_p = Particle(position, velocity, generation, self.rank) for i, k in enumerate(self.limits): new_p[k] = new_p.position[i] return new_p diff --git a/ap_pso/propagators/constriction.py b/ap_pso/propagators/constriction.py new file mode 100644 index 00000000..99329a92 --- /dev/null +++ b/ap_pso/propagators/constriction.py @@ -0,0 +1,41 @@ +""" +This file contains a Propagator subclass providing constriction-flavoured pso. +""" +from random import Random + +import numpy as np + +from ap_pso import Particle +from ap_pso.propagators import BasicPSOPropagator + + +class ConstrictionPropagator(BasicPSOPropagator): + def __init__(self, + c_cognitive: float, + c_social: float, + rank: int, + limits: dict[str, tuple[float, float]], + rng: Random): + """ + Class constructor. + Important note: `c_cognitive` and `c_social` have to sum up to something greater than 4! + :param c_cognitive: constant cognitive factor to scale p_best with + :param c_social: constant social factor to scale g_best with + :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD + :param limits: a dict with str keys and 2-tuples of floats associated to each of them + :param rng: random number generator + """ + assert c_cognitive + c_social > 4, "c_cognitive + c_social < 4!" + phi: float = c_cognitive + c_social + chi: float = 2.0 / (phi - 2.0 + np.sqrt(phi * (phi - 4.0))) + super().__init__(chi, c_cognitive, c_social, rank, limits, rng) + + def __call__(self, particles: list[Particle]) -> Particle: + old_p, p_best, g_best = self._prepare_data(particles) + + new_velocity = self.w_k * (old_p.velocity + + self.c_cognitive * self.rng.uniform(*self.laa) * (p_best.position - old_p.position) + + self.c_social * self.rng.uniform(*self.laa) * (g_best.position - old_p.position)) + new_position = old_p.position + new_velocity + + return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) From 71f7dd70fcd42dfc336e74bacc452e3f37b44b19 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 10 Aug 2023 12:59:15 +0200 Subject: [PATCH 038/139] Fitted VelocityClamping update rule to new situation. Also removed an error in the velocity calculation in the basic PSO propagator. --- ap_pso/propagators/basic_pso.py | 8 ++++++-- ap_pso/propagators/velocity_clamping.py | 15 ++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/ap_pso/propagators/basic_pso.py b/ap_pso/propagators/basic_pso.py index 0c5c4f0e..4f6b89c1 100644 --- a/ap_pso/propagators/basic_pso.py +++ b/ap_pso/propagators/basic_pso.py @@ -35,8 +35,8 @@ def __call__(self, particles: list[Particle]) -> Particle: old_p, p_best, g_best = self._prepare_data(particles) new_velocity: np.ndarray = self.w_k * old_p.velocity \ - + self.c_cognitive * self.rng.uniform(*self.laa) * (p_best.position - old_p.position) \ - + self.c_social * self.rng.uniform(*self.laa) * (g_best.position - old_p.position) + + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) \ + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) new_position: np.ndarray = old_p.position + new_velocity return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) @@ -64,6 +64,10 @@ def _prepare_data(self, particles: list[Particle]) -> tuple[Particle, Particle, return old_p, p_best, g_best def _make_new_particle(self, position: np.ndarray, velocity: np.ndarray, generation: int): + """ + Takes the necessary data to create a new Particle with the position dict set to the correct values. + :return: The newly created Particle object + """ new_p = Particle(position, velocity, generation, self.rank) for i, k in enumerate(self.limits): new_p[k] = new_p.position[i] diff --git a/ap_pso/propagators/velocity_clamping.py b/ap_pso/propagators/velocity_clamping.py index 2283e639..6c060998 100644 --- a/ap_pso/propagators/velocity_clamping.py +++ b/ap_pso/propagators/velocity_clamping.py @@ -32,10 +32,11 @@ def __init__(self, self.v_cap = v_limits def __call__(self, particles: list[Particle]) -> Particle: - p: Particle = super().__call__(particles) - p.position -= p.velocity - p.velocity = p.velocity.clip(*(self.v_cap * self.laa)) - p.position += p.velocity - for i, k in enumerate(self.limits): - p[k] = p.position[i] - return p + old_p, p_best, g_best = self._prepare_data(particles) + + new_velocity: np.ndarray = (self.w_k * old_p.velocity + + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position)).clip(*(self.v_cap * self.laa)) + new_position: np.ndarray = old_p.position + new_velocity + + return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) From e6054529039ad1aa384cc89d1450df05ede33c69 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 10 Aug 2023 15:43:56 +0200 Subject: [PATCH 039/139] Integrated new PSO Propagator subclasses into package tree. --- ap_pso/propagators/__init__.py | 4 +++- ap_pso/propagators/constriction.py | 4 ++-- ap_pso/propagators/pso_compose.py | 21 +++++++++++++++++++++ ap_pso/propagators/pso_init_uniform.py | 8 ++++---- 4 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 ap_pso/propagators/pso_compose.py diff --git a/ap_pso/propagators/__init__.py b/ap_pso/propagators/__init__.py index 362b1e0e..71648319 100644 --- a/ap_pso/propagators/__init__.py +++ b/ap_pso/propagators/__init__.py @@ -1,9 +1,11 @@ """ In this package, I collect all PSO-related propagators. """ -__all__ = ["PSOInitUniform", "StatelessPSOPropagator", "BasicPSOPropagator", "VelocityClampingPropagator"] +__all__ = ["PSOInitUniform", "StatelessPSOPropagator", "BasicPSOPropagator", "VelocityClampingPropagator", "ConstrictionPropagator", "PSOCompose"] from ap_pso.propagators.basic_pso import BasicPSOPropagator +from ap_pso.propagators.constriction import ConstrictionPropagator +from ap_pso.propagators.pso_compose import PSOCompose from ap_pso.propagators.pso_init_uniform import PSOInitUniform from ap_pso.propagators.stateless_pso import StatelessPSOPropagator from ap_pso.propagators.velocity_clamping import VelocityClampingPropagator diff --git a/ap_pso/propagators/constriction.py b/ap_pso/propagators/constriction.py index 99329a92..f3b8bda8 100644 --- a/ap_pso/propagators/constriction.py +++ b/ap_pso/propagators/constriction.py @@ -34,8 +34,8 @@ def __call__(self, particles: list[Particle]) -> Particle: old_p, p_best, g_best = self._prepare_data(particles) new_velocity = self.w_k * (old_p.velocity - + self.c_cognitive * self.rng.uniform(*self.laa) * (p_best.position - old_p.position) - + self.c_social * self.rng.uniform(*self.laa) * (g_best.position - old_p.position)) + + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position)) new_position = old_p.position + new_velocity return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) diff --git a/ap_pso/propagators/pso_compose.py b/ap_pso/propagators/pso_compose.py new file mode 100644 index 00000000..4f4607ce --- /dev/null +++ b/ap_pso/propagators/pso_compose.py @@ -0,0 +1,21 @@ +from typing import Iterable + +from ap_pso import Particle +from propulate.population import Individual +from propulate.propagators import Compose + + +class PSOCompose(Compose): + def __call__(self, particles: list[Particle]) -> Particle: + """ + Returns the first element of the list of particles returned by the last Propagator in the list input upon creation of the object. + + This behaviour should change in near future, so that a list of Particles is returned, with hopefully only one member. + """ + for p in self.propagators: + tmp = p(particles) + if isinstance(tmp, Individual): + particles = [tmp] + else: + particles = tmp + return particles[0] diff --git a/ap_pso/propagators/pso_init_uniform.py b/ap_pso/propagators/pso_init_uniform.py index 574df4e8..80b8887a 100644 --- a/ap_pso/propagators/pso_init_uniform.py +++ b/ap_pso/propagators/pso_init_uniform.py @@ -44,7 +44,7 @@ def __init__(self, limits: dict[str, tuple[float, float]], parents=0, probabilit assert v_init_limit.shape[-1] == self.laa.shape[-1] self.v_limits = v_init_limit - def __call__(self, *particles: Individual) -> Particle: + def __call__(self, particles: list[Individual]) -> Particle: """ Apply uniform-initialization propagator. @@ -55,10 +55,10 @@ def __call__(self, *particles: Individual) -> Particle: Returns ------- - ind : propulate.population.Individual - list of selected individuals after application of propagator + ind : propulate.ap_pso.Particle + one particle object """ - if self.rng.random() < self.probability: # Apply only with specified `probability`. + if self.rng.random() < self.probability or len(particles) == 0: # Apply only with specified `probability`. position = self.rng.uniform(*self.laa) velocity = self.rng.uniform(*(self.v_limits * self.laa)) From 98426d5386769da2c9e68c8d4dac020717da1ace Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 10 Aug 2023 15:44:58 +0200 Subject: [PATCH 040/139] Adjusted the propagators.py file to the fact that it has moved. --- propulate/propagators/propagators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/propulate/propagators/propagators.py b/propulate/propagators/propagators.py index 115b3882..6947d8bd 100644 --- a/propulate/propagators/propagators.py +++ b/propulate/propagators/propagators.py @@ -5,7 +5,7 @@ import numpy as np from abc import ABC, abstractmethod -from propulate.population import Individual +from ..population import Individual def _check_compatible(out1: int, in2: int) -> bool: From bcb71fda0a22922516fd7cd8e801aa72f3414c7f Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 10 Aug 2023 15:45:26 +0200 Subject: [PATCH 041/139] Updated the test script. Added a bash file starter to it. --- ap_pso/scripts/pso_example.py | 20 ++++++++++---------- ap_pso/scripts/pso_example.sh | 5 +++++ 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 ap_pso/scripts/pso_example.sh diff --git a/ap_pso/scripts/pso_example.py b/ap_pso/scripts/pso_example.py index 74f5195d..0c05eb1f 100644 --- a/ap_pso/scripts/pso_example.py +++ b/ap_pso/scripts/pso_example.py @@ -4,16 +4,17 @@ from mpi4py import MPI -from ap_pso.propagators import PSOInitUniform, VelocityClampingPropagator +from ap_pso.propagators import PSOInitUniform, VelocityClampingPropagator, ConstrictionPropagator, PSOCompose, \ + BasicPSOPropagator, StatelessPSOPropagator from propulate import Islands -from propulate.propagators import Compose, Conditional +from propulate.propagators import Conditional, InitUniform ############ # SETTINGS # ############ fname = sys.argv[1] # Get function to optimize from command-line. -NUM_GENERATIONS = 100 # Set number of generations. +NUM_GENERATIONS: int = int(sys.argv[2]) # Set number of generations. POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. num_migrants = 1 @@ -30,10 +31,7 @@ def sphere(params): if fname == "sphere": function = sphere - limits = { - "x": (-5.12, 5.12), - "y": (-5.12, 5.12), - } + limits = {"x": (-5.12, 5.12), "y": (-5.12, 5.12), } else: sys.exit("ERROR: Function undefined...exiting") @@ -43,10 +41,12 @@ def sphere(params): rng = random.Random(MPI.COMM_WORLD.rank) - propagator = Compose( + propagator = PSOCompose( [ - VelocityClampingPropagator(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.1), - PSOInitUniform(limits, parents=1, probability=0.1, rng=rng) + # VelocityClampingPropagator(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6) + ConstrictionPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) + # BasicPSOPropagator(0.7298,1.49618,1.49618, MPI.COMM_WORLD.rank, limits, rng) + # StatelessPSOPropagator(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng) # Attention! Does not work with current chart drawing script! ] ) diff --git a/ap_pso/scripts/pso_example.sh b/ap_pso/scripts/pso_example.sh new file mode 100644 index 00000000..15572d11 --- /dev/null +++ b/ap_pso/scripts/pso_example.sh @@ -0,0 +1,5 @@ +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py sphere 100 From 822e4fc903207ed688259378cf50a9733a51e1c3 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 14 Aug 2023 10:57:31 +0200 Subject: [PATCH 042/139] Added 'the canonical pso' propagator. --- ap_pso/propagators/canonical.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 ap_pso/propagators/canonical.py diff --git a/ap_pso/propagators/canonical.py b/ap_pso/propagators/canonical.py new file mode 100644 index 00000000..e8ac09cc --- /dev/null +++ b/ap_pso/propagators/canonical.py @@ -0,0 +1,15 @@ +from ap_pso import Particle +from ap_pso.propagators import ConstrictionPropagator + + +class CanonicalPropagator(ConstrictionPropagator): + def __call__(self, particles: list[Particle]): + # Abuse Constriction's update rule so I don't have to rewrite it. + victim = super().__call__(particles) + + # Set new position and speed. + v = victim.velocity.clip(*self.laa) + p = victim.position - victim.velocity + v + + # create and return new particle. + return self._make_new_particle(p, v, victim.generation) From e3128124c062f70fd0528f03a3a5a57a6fce2c5d Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 14 Aug 2023 15:05:25 +0200 Subject: [PATCH 043/139] Added images directory to gitignore. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3d45a7ed..ce6a126e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,5 @@ scripts/*.png voucher_propulate.txt .gitignore -checkpoints/ \ No newline at end of file +checkpoints/ +images/ From 89fa5ba058f112e5b2534d6917e3d6a74af38787 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 14 Aug 2023 15:05:55 +0200 Subject: [PATCH 044/139] A bunch of debugging of code errors. --- ap_pso/propagators/basic_pso.py | 2 +- ap_pso/propagators/pso_init_uniform.py | 4 ++-- ap_pso/propagators/velocity_clamping.py | 18 ++++++++---------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/ap_pso/propagators/basic_pso.py b/ap_pso/propagators/basic_pso.py index 4f6b89c1..43bd0638 100644 --- a/ap_pso/propagators/basic_pso.py +++ b/ap_pso/propagators/basic_pso.py @@ -29,7 +29,7 @@ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, self.rank = rank self.limits = limits self.rng = rng - self.laa = np.array(list(limits.values())).T # laa - "limits as array" + self.laa: np.ndarray = np.array(list(limits.values())).T # laa - "limits as array" def __call__(self, particles: list[Particle]) -> Particle: old_p, p_best, g_best = self._prepare_data(particles) diff --git a/ap_pso/propagators/pso_init_uniform.py b/ap_pso/propagators/pso_init_uniform.py index 80b8887a..4cf7589e 100644 --- a/ap_pso/propagators/pso_init_uniform.py +++ b/ap_pso/propagators/pso_init_uniform.py @@ -60,8 +60,8 @@ def __call__(self, particles: list[Individual]) -> Particle: """ if self.rng.random() < self.probability or len(particles) == 0: # Apply only with specified `probability`. - position = self.rng.uniform(*self.laa) - velocity = self.rng.uniform(*(self.v_limits * self.laa)) + position = np.array([self.rng.uniform(*self.laa[..., i]) for i in range(self.laa.shape[-1])]) + velocity = np.array([self.rng.uniform(*(self.v_limits * self.laa)[..., i]) for i in range(self.laa.shape[-1])]) particle = Particle(position, velocity) # Instantiate new particle. diff --git a/ap_pso/propagators/velocity_clamping.py b/ap_pso/propagators/velocity_clamping.py index 6c060998..ff4b429c 100644 --- a/ap_pso/propagators/velocity_clamping.py +++ b/ap_pso/propagators/velocity_clamping.py @@ -10,14 +10,8 @@ class VelocityClampingPropagator(BasicPSOPropagator): - def __init__(self, - w_k: float, - c_cognitive: float, - c_social: float, - rank: int, - limits: dict[str, tuple[float, float]], - rng: Random, - v_limits: float | np.ndarray): + def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, + limits: dict[str, tuple[float, float]], rng: Random, v_limits: float | np.ndarray): """ Class constructor. :param w_k: The particle's inertia factor @@ -29,14 +23,18 @@ def __init__(self, :param v_limits: a numpy array containing values that work as relative caps for their corresponding search space dimensions. If this is a float instead, it does its job for all axes. """ super().__init__(w_k, c_cognitive, c_social, rank, limits, rng) - self.v_cap = v_limits + x_min, x_max = self.laa + x_range = np.abs(x_max - x_min) + if v_limits < 0: + v_limits *= -1 + self.v_cap: np.ndarray = np.array([-v_limits * x_range, v_limits * x_range]) def __call__(self, particles: list[Particle]) -> Particle: old_p, p_best, g_best = self._prepare_data(particles) new_velocity: np.ndarray = (self.w_k * old_p.velocity + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) - + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position)).clip(*(self.v_cap * self.laa)) + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position)).clip(*self.v_cap) new_position: np.ndarray = old_p.position + new_velocity return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) From 27cb588a840cb47fb991d0de93cd32182200b664 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 14 Aug 2023 15:13:27 +0200 Subject: [PATCH 045/139] Fixed some issues with the canonical one. --- ap_pso/propagators/__init__.py | 3 ++- ap_pso/propagators/canonical.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ap_pso/propagators/__init__.py b/ap_pso/propagators/__init__.py index 71648319..9e9f9060 100644 --- a/ap_pso/propagators/__init__.py +++ b/ap_pso/propagators/__init__.py @@ -1,10 +1,11 @@ """ In this package, I collect all PSO-related propagators. """ -__all__ = ["PSOInitUniform", "StatelessPSOPropagator", "BasicPSOPropagator", "VelocityClampingPropagator", "ConstrictionPropagator", "PSOCompose"] +__all__ = ["PSOInitUniform", "StatelessPSOPropagator", "BasicPSOPropagator", "VelocityClampingPropagator", "ConstrictionPropagator", "CanonicalPropagator", "PSOCompose"] from ap_pso.propagators.basic_pso import BasicPSOPropagator from ap_pso.propagators.constriction import ConstrictionPropagator +from ap_pso.propagators.canonical import CanonicalPropagator from ap_pso.propagators.pso_compose import PSOCompose from ap_pso.propagators.pso_init_uniform import PSOInitUniform from ap_pso.propagators.stateless_pso import StatelessPSOPropagator diff --git a/ap_pso/propagators/canonical.py b/ap_pso/propagators/canonical.py index e8ac09cc..55526139 100644 --- a/ap_pso/propagators/canonical.py +++ b/ap_pso/propagators/canonical.py @@ -1,14 +1,23 @@ +import numpy as np + from ap_pso import Particle from ap_pso.propagators import ConstrictionPropagator class CanonicalPropagator(ConstrictionPropagator): + + def __init__(self, c_cognitive, c_social, rank, limits, rng): + super().__init__(c_cognitive, c_social, rank, limits, rng) + x_min, x_max = self.laa + x_range = np.abs(x_max - x_min) + self.v_cap: np.ndarray = np.array([-x_range, x_range]) + def __call__(self, particles: list[Particle]): # Abuse Constriction's update rule so I don't have to rewrite it. victim = super().__call__(particles) # Set new position and speed. - v = victim.velocity.clip(*self.laa) + v = victim.velocity.clip(*self.v_cap) p = victim.position - victim.velocity + v # create and return new particle. From 20a1b1c0416b03e044e7ad6ba15b7bcf748a9c8a Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 14 Aug 2023 15:13:45 +0200 Subject: [PATCH 046/139] Updated the Propulate integration helper. --- in_case_of_update.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/in_case_of_update.md b/in_case_of_update.md index bd3250ec..4f85ce98 100644 --- a/in_case_of_update.md +++ b/in_case_of_update.md @@ -9,9 +9,14 @@ In order to be able to update `Propulate` into this repo, please do the followin ``` 2. Then, call ``` - git pull propulate master + git fetch propulate master ``` in order to pull the current state of `Propulate`'s repo's `master` branch into your own `master` branch. 3. You will likely get a merge conflict which you can then resolve by using the built-in tools of PyCharm. If you are not using PyCharm please refer to the Git manual and look up how to resolve the conflicts. + Alternatively, do + ``` + git merge propulate/master + ``` + while being on branch master. 4. Afterwards, commit everything and push it onto your own GitHub repo so everything is fine. From 381ec53c62ffb225170af3557fb6e2d50b01b2c1 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 14 Aug 2023 15:14:03 +0200 Subject: [PATCH 047/139] Updated pso test scripts. --- ap_pso/scripts/pso_example.py | 33 ++++++----------- ap_pso/scripts/pso_example.sh | 67 +++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/ap_pso/scripts/pso_example.py b/ap_pso/scripts/pso_example.py index 0c05eb1f..9a77a47c 100644 --- a/ap_pso/scripts/pso_example.py +++ b/ap_pso/scripts/pso_example.py @@ -5,35 +5,21 @@ from mpi4py import MPI from ap_pso.propagators import PSOInitUniform, VelocityClampingPropagator, ConstrictionPropagator, PSOCompose, \ - BasicPSOPropagator, StatelessPSOPropagator + BasicPSOPropagator, StatelessPSOPropagator, CanonicalPropagator from propulate import Islands from propulate.propagators import Conditional, InitUniform +from scripts.function_benchmark import get_function_search_space ############ # SETTINGS # ############ -fname = sys.argv[1] # Get function to optimize from command-line. +function_name = sys.argv[1] # Get function to optimize from command-line. NUM_GENERATIONS: int = int(sys.argv[2]) # Set number of generations. POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. num_migrants = 1 - -# SPHERE -# continuous, convex, separable, non-differentiable, non-multimodal -# input domain: -5.12 <= x, y <= 5.12 -# global minimum 0 at (x, y) = (0, 0) -def sphere(params): - x = params["x"] - y = params["y"] - return x ** 2 + y ** 2 - - -if fname == "sphere": - function = sphere - limits = {"x": (-5.12, 5.12), "y": (-5.12, 5.12), } -else: - sys.exit("ERROR: Function undefined...exiting") +function, limits = get_function_search_space(function_name) if __name__ == "__main__": # migration_topology = num_migrants*np.ones((4, 4), dtype=int) @@ -44,8 +30,10 @@ def sphere(params): propagator = PSOCompose( [ # VelocityClampingPropagator(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6) - ConstrictionPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) - # BasicPSOPropagator(0.7298,1.49618,1.49618, MPI.COMM_WORLD.rank, limits, rng) + # VelocityClampingPropagator(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng, 0.6) + # ConstrictionPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) + # BasicPSOPropagator(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng) + CanonicalPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) # StatelessPSOPropagator(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng) # Attention! Does not work with current chart drawing script! ] ) @@ -54,5 +42,6 @@ def sphere(params): propagator = Conditional(POP_SIZE, propagator, init) islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path='./checkpoints/', - migration_probability=0) - islands.evolve(top_n=1, logging_interval=1, DEBUG=2) + migration_probability=0, pollination=False) + islands.evolve(top_n=1, logging_interval=1) + islands.propulator.paint_graphs(function_name) diff --git a/ap_pso/scripts/pso_example.sh b/ap_pso/scripts/pso_example.sh index 15572d11..5d47abfa 100644 --- a/ap_pso/scripts/pso_example.sh +++ b/ap_pso/scripts/pso_example.sh @@ -2,4 +2,71 @@ if [ "$1" = "clear" ] then rm -rf ./checkpoints fi +# Options: birastrigin, bisphere, bukin, eggcrate, griewank, himmelblau, keane, leon, quartic, rastrigin, rosenbrock, schwefel, sphere, step + +mpirun -n 4 python "$(dirname "$0")"/pso_example.py birastrigin 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py bisphere 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py bukin 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py eggcrate 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py griewank 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py schwefel 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py himmelblau 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py keane 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py leon 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py quartic 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py rastrigin 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py rosenbrock 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi mpirun -n 4 python "$(dirname "$0")"/pso_example.py sphere 100 +if [ "$1" = "clear" ] +then + rm -rf ./checkpoints +fi +mpirun -n 4 python "$(dirname "$0")"/pso_example.py step 100 From ff762e8348f54c8d3a39c2cae6e3b2c85b18fadd Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 17 Aug 2023 14:51:30 +0200 Subject: [PATCH 048/139] Established compatibility with Python 3.8.6 --- ap_pso/propagators/__init__.py | 3 ++- ap_pso/propagators/basic_pso.py | 15 +++++++++------ ap_pso/propagators/canonical.py | 4 +++- ap_pso/propagators/constriction.py | 5 +++-- ap_pso/propagators/pso_compose.py | 10 ++++++---- ap_pso/propagators/pso_init_uniform.py | 18 ++++++++++++++---- ap_pso/propagators/stateless_pso.py | 11 ++++++----- ap_pso/propagators/velocity_clamping.py | 13 ++++++++----- ap_pso/utils.py | 2 -- 9 files changed, 51 insertions(+), 30 deletions(-) diff --git a/ap_pso/propagators/__init__.py b/ap_pso/propagators/__init__.py index 9e9f9060..783581e1 100644 --- a/ap_pso/propagators/__init__.py +++ b/ap_pso/propagators/__init__.py @@ -1,7 +1,8 @@ """ In this package, I collect all PSO-related propagators. """ -__all__ = ["PSOInitUniform", "StatelessPSOPropagator", "BasicPSOPropagator", "VelocityClampingPropagator", "ConstrictionPropagator", "CanonicalPropagator", "PSOCompose"] +__all__ = ["PSOInitUniform", "StatelessPSOPropagator", "BasicPSOPropagator", "VelocityClampingPropagator", + "ConstrictionPropagator", "CanonicalPropagator", "PSOCompose"] from ap_pso.propagators.basic_pso import BasicPSOPropagator from ap_pso.propagators.constriction import ConstrictionPropagator diff --git a/ap_pso/propagators/basic_pso.py b/ap_pso/propagators/basic_pso.py index 43bd0638..0737d80e 100644 --- a/ap_pso/propagators/basic_pso.py +++ b/ap_pso/propagators/basic_pso.py @@ -2,6 +2,7 @@ This file contains the first stateful PSO propagator for Propulate. """ from random import Random +from typing import Dict, Tuple, List import numpy as np @@ -12,7 +13,7 @@ class BasicPSOPropagator(Propagator): def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, - limits: dict[str, tuple[float, float]], rng: Random): + limits: Dict[str, Tuple[float, float]], rng: Random): """ Class constructor. :param w_k: The learning rate ... somehow @@ -31,17 +32,17 @@ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, self.rng = rng self.laa: np.ndarray = np.array(list(limits.values())).T # laa - "limits as array" - def __call__(self, particles: list[Particle]) -> Particle: + def __call__(self, particles: List[Particle]) -> Particle: old_p, p_best, g_best = self._prepare_data(particles) new_velocity: np.ndarray = self.w_k * old_p.velocity \ - + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) \ - + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) + + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) \ + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) new_position: np.ndarray = old_p.position + new_velocity return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) - def _prepare_data(self, particles: list[Particle]) -> tuple[Particle, Particle, Particle]: + def _prepare_data(self, particles: List[Particle]) -> Tuple[Particle, Particle, Particle]: """ Returns the following particles in this very order: 1. old_p: the current particle to be updated now @@ -56,7 +57,9 @@ def _prepare_data(self, particles: list[Particle]) -> tuple[Particle, Particle, if not isinstance(old_p, Particle): old_p = make_particle(old_p) - print(f"R{self.rank}, Iteration#{old_p.generation}: Type Error. Converted Individual to Particle. Continuing.") + print( + f"R{self.rank}, Iteration#{old_p.generation}: Type Error. " + f"Converted Individual to Particle. Continuing.") g_best = min(particles, key=lambda p: p.loss) p_best = min(own_p, key=lambda p: p.loss) diff --git a/ap_pso/propagators/canonical.py b/ap_pso/propagators/canonical.py index 55526139..296c4acc 100644 --- a/ap_pso/propagators/canonical.py +++ b/ap_pso/propagators/canonical.py @@ -1,3 +1,5 @@ +from typing import List + import numpy as np from ap_pso import Particle @@ -12,7 +14,7 @@ def __init__(self, c_cognitive, c_social, rank, limits, rng): x_range = np.abs(x_max - x_min) self.v_cap: np.ndarray = np.array([-x_range, x_range]) - def __call__(self, particles: list[Particle]): + def __call__(self, particles: List[Particle]): # Abuse Constriction's update rule so I don't have to rewrite it. victim = super().__call__(particles) diff --git a/ap_pso/propagators/constriction.py b/ap_pso/propagators/constriction.py index f3b8bda8..3ca2549a 100644 --- a/ap_pso/propagators/constriction.py +++ b/ap_pso/propagators/constriction.py @@ -2,6 +2,7 @@ This file contains a Propagator subclass providing constriction-flavoured pso. """ from random import Random +from typing import List, Dict, Tuple import numpy as np @@ -14,7 +15,7 @@ def __init__(self, c_cognitive: float, c_social: float, rank: int, - limits: dict[str, tuple[float, float]], + limits: Dict[str, Tuple[float, float]], rng: Random): """ Class constructor. @@ -30,7 +31,7 @@ def __init__(self, chi: float = 2.0 / (phi - 2.0 + np.sqrt(phi * (phi - 4.0))) super().__init__(chi, c_cognitive, c_social, rank, limits, rng) - def __call__(self, particles: list[Particle]) -> Particle: + def __call__(self, particles: List[Particle]) -> Particle: old_p, p_best, g_best = self._prepare_data(particles) new_velocity = self.w_k * (old_p.velocity diff --git a/ap_pso/propagators/pso_compose.py b/ap_pso/propagators/pso_compose.py index 4f4607ce..48338fcf 100644 --- a/ap_pso/propagators/pso_compose.py +++ b/ap_pso/propagators/pso_compose.py @@ -1,4 +1,4 @@ -from typing import Iterable +from typing import List from ap_pso import Particle from propulate.population import Individual @@ -6,11 +6,13 @@ class PSOCompose(Compose): - def __call__(self, particles: list[Particle]) -> Particle: + def __call__(self, particles: List[Particle]) -> Particle: """ - Returns the first element of the list of particles returned by the last Propagator in the list input upon creation of the object. + Returns the first element of the list of particles returned by the last Propagator in the list + input upon creation of the object. - This behaviour should change in near future, so that a list of Particles is returned, with hopefully only one member. + This behaviour should change in near future, so that a list of Particles is returned, + with hopefully only one member. """ for p in self.propagators: tmp = p(particles) diff --git a/ap_pso/propagators/pso_init_uniform.py b/ap_pso/propagators/pso_init_uniform.py index 4cf7589e..7d380a40 100644 --- a/ap_pso/propagators/pso_init_uniform.py +++ b/ap_pso/propagators/pso_init_uniform.py @@ -2,6 +2,7 @@ This file contains propagators, that can be used to initialize a population of either Individuals or Particles. """ from random import Random +from typing import Union, Dict, Tuple, List import numpy as np @@ -15,8 +16,8 @@ class PSOInitUniform(Stochastic): Initialize individuals by uniformly sampling specified limits for each trait. """ - def __init__(self, limits: dict[str, tuple[float, float]], parents=0, probability=1.0, rng: Random = None, *, - v_init_limit: float | np.ndarray = 0.1): + def __init__(self, limits: Dict[str, Tuple[float, float]], parents=0, probability=1.0, rng: Random = None, *, + v_init_limit: Union[float, np.ndarray] = 0.1): """ Constructor of PSOInitUniform class. @@ -44,7 +45,7 @@ def __init__(self, limits: dict[str, tuple[float, float]], parents=0, probabilit assert v_init_limit.shape[-1] == self.laa.shape[-1] self.v_limits = v_init_limit - def __call__(self, particles: list[Individual]) -> Particle: + def __call__(self, particles: List[Individual]) -> Particle: """ Apply uniform-initialization propagator. @@ -61,7 +62,16 @@ def __call__(self, particles: list[Individual]) -> Particle: if self.rng.random() < self.probability or len(particles) == 0: # Apply only with specified `probability`. position = np.array([self.rng.uniform(*self.laa[..., i]) for i in range(self.laa.shape[-1])]) - velocity = np.array([self.rng.uniform(*(self.v_limits * self.laa)[..., i]) for i in range(self.laa.shape[-1])]) + velocity = np.array( + [ + self.rng.uniform( + *( + self.v_limits * self.laa + )[..., i] + ) + for i in range(self.laa.shape[-1]) + ] + ) particle = Particle(position, velocity) # Instantiate new particle. diff --git a/ap_pso/propagators/stateless_pso.py b/ap_pso/propagators/stateless_pso.py index 129c3584..2ee57bbc 100644 --- a/ap_pso/propagators/stateless_pso.py +++ b/ap_pso/propagators/stateless_pso.py @@ -3,16 +3,16 @@ """ from random import Random +from typing import Dict, Tuple, List from propulate.population import Individual - from propulate.propagators import Propagator class StatelessPSOPropagator(Propagator): def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, - limits: dict[str, tuple[float, float]], rng: Random): + limits: Dict[str, Tuple[float, float]], rng: Random): """ :param w_k: The learning rate ... somehow - currently without effect @@ -30,7 +30,7 @@ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, self.limits = limits self.rng = rng - def __call__(self, particles: list[Individual]) -> Individual: + def __call__(self, particles: List[Individual]) -> Individual: if len(particles) < self.offspring: raise ValueError("Not enough Particles") own_p = [x for x in particles if x.rank == self.rank] @@ -42,6 +42,7 @@ def __call__(self, particles: list[Individual]) -> Individual: p_best = sorted(own_p, key=lambda p: p.loss)[0] new_p = Individual(generation=old_p.generation + 1) for k in self.limits: - new_p[k] = self.c_cognitive * self.rng.uniform(*self.limits[k]) * (p_best[k] - old_p[k]) \ - + self.c_social * self.rng.uniform(*self.limits[k]) * (g_best[k] - old_p[k]) + new_p[k] = self.c_cognitive * self.rng.uniform(*self.limits[k]) * ( + p_best[k] - old_p[k]) + self.c_social * self.rng.uniform(*self.limits[k]) * ( + g_best[k] - old_p[k]) return new_p diff --git a/ap_pso/propagators/velocity_clamping.py b/ap_pso/propagators/velocity_clamping.py index ff4b429c..201fe4b5 100644 --- a/ap_pso/propagators/velocity_clamping.py +++ b/ap_pso/propagators/velocity_clamping.py @@ -2,6 +2,7 @@ This file contains a PSO propagator relying on the standard one but additionally performing velocity clamping. """ from random import Random +from typing import Dict, Tuple, Union, List import numpy as np @@ -11,7 +12,7 @@ class VelocityClampingPropagator(BasicPSOPropagator): def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, - limits: dict[str, tuple[float, float]], rng: Random, v_limits: float | np.ndarray): + limits: Dict[str, Tuple[float, float]], rng: Random, v_limits: Union[float, np.ndarray]): """ Class constructor. :param w_k: The particle's inertia factor @@ -19,8 +20,9 @@ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, :param c_social: constant social factor to scale g_best with :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD :param limits: a dict with str keys and 2-tuples of floats associated to each of them - :param rng: random number generator - :param v_limits: a numpy array containing values that work as relative caps for their corresponding search space dimensions. If this is a float instead, it does its job for all axes. + :param rng: random number generator :param v_limits: a numpy array containing values that work as relative caps + for their corresponding search space dimensions. + If this is a float instead, it does its job for all axes. """ super().__init__(w_k, c_cognitive, c_social, rank, limits, rng) x_min, x_max = self.laa @@ -29,12 +31,13 @@ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, v_limits *= -1 self.v_cap: np.ndarray = np.array([-v_limits * x_range, v_limits * x_range]) - def __call__(self, particles: list[Particle]) -> Particle: + def __call__(self, particles: List[Particle]) -> Particle: old_p, p_best, g_best = self._prepare_data(particles) new_velocity: np.ndarray = (self.w_k * old_p.velocity + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) - + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position)).clip(*self.v_cap) + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) + ).clip(*self.v_cap) new_position: np.ndarray = old_p.position + new_velocity return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) diff --git a/ap_pso/utils.py b/ap_pso/utils.py index 6d3a95b1..bc3c284c 100644 --- a/ap_pso/utils.py +++ b/ap_pso/utils.py @@ -1,8 +1,6 @@ """ This file contains all sort of more or less useful stuff. """ -from typing import Iterable - import numpy as np from ap_pso import Particle From c7f2eab98078861055cfddf4e873fafc0ffaff44 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 17 Aug 2023 19:42:42 +0200 Subject: [PATCH 049/139] Fixed implementation to be able to work in multi-island case. --- ap_pso/particle.py | 2 ++ ap_pso/propagators/basic_pso.py | 8 ++++++-- ap_pso/propagators/pso_init_uniform.py | 9 ++++++--- ap_pso/scripts/pso_example.py | 7 ++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/ap_pso/particle.py b/ap_pso/particle.py index aa69ef90..20dee80b 100644 --- a/ap_pso/particle.py +++ b/ap_pso/particle.py @@ -27,3 +27,5 @@ def __init__(self, assert position.shape == velocity.shape self.velocity = velocity self.position = position + self.g_rank = rank # necessary as Propulate splits up the COMM_WORLD communicator which leads to errors with + # rank. diff --git a/ap_pso/propagators/basic_pso.py b/ap_pso/propagators/basic_pso.py index 0737d80e..5ceb3b7f 100644 --- a/ap_pso/propagators/basic_pso.py +++ b/ap_pso/propagators/basic_pso.py @@ -52,8 +52,12 @@ def _prepare_data(self, particles: List[Particle]) -> Tuple[Particle, Particle, if len(particles) < self.offspring: raise ValueError("Not enough Particles") - own_p = [x for x in particles if x.rank == self.rank] - old_p = max(own_p, key=lambda p: p.generation) + own_p = [x for x in particles if (isinstance(x, Particle) and x.g_rank == self.rank) or x.rank == self.rank] + if len(own_p) > 0: + old_p = max(own_p, key=lambda p: p.generation) + else: + victim = max(particles, key=lambda p: p.generation) + old_p = self._make_new_particle(victim.position, victim.velocity, victim.generation) if not isinstance(old_p, Particle): old_p = make_particle(old_p) diff --git a/ap_pso/propagators/pso_init_uniform.py b/ap_pso/propagators/pso_init_uniform.py index 7d380a40..e3b3d5b5 100644 --- a/ap_pso/propagators/pso_init_uniform.py +++ b/ap_pso/propagators/pso_init_uniform.py @@ -17,7 +17,7 @@ class PSOInitUniform(Stochastic): """ def __init__(self, limits: Dict[str, Tuple[float, float]], parents=0, probability=1.0, rng: Random = None, *, - v_init_limit: Union[float, np.ndarray] = 0.1): + v_init_limit: Union[float, np.ndarray] = 0.1, rank: int): """ Constructor of PSOInitUniform class. @@ -33,10 +33,12 @@ def __init__(self, limits: Dict[str, Tuple[float, float]], parents=0, probabilit number of input individuals (-1 for any) probability : float the probability with which a completely new individual is created - rng : random.Random() + rng : random.Random random number generator v_init_limit: float | np.ndarray some multiplicative constant to reduce initial random velocity values. + rank : int + The rank of the worker in MPI.COMM_WORLD """ super().__init__(parents, 1, probability, rng) self.limits = limits @@ -44,6 +46,7 @@ def __init__(self, limits: Dict[str, Tuple[float, float]], parents=0, probabilit if isinstance(v_init_limit, np.ndarray): assert v_init_limit.shape[-1] == self.laa.shape[-1] self.v_limits = v_init_limit + self.rank = rank def __call__(self, particles: List[Individual]) -> Particle: """ @@ -73,7 +76,7 @@ def __call__(self, particles: List[Individual]) -> Particle: ] ) - particle = Particle(position, velocity) # Instantiate new particle. + particle = Particle(position, velocity, rank=self.rank) # Instantiate new particle. for index, limit in enumerate(self.limits): # Since Py 3.7, iterating over dicts is stable, so we can do the following. diff --git a/ap_pso/scripts/pso_example.py b/ap_pso/scripts/pso_example.py index 9a77a47c..b397d558 100644 --- a/ap_pso/scripts/pso_example.py +++ b/ap_pso/scripts/pso_example.py @@ -34,14 +34,15 @@ # ConstrictionPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) # BasicPSOPropagator(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng) CanonicalPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) - # StatelessPSOPropagator(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng) # Attention! Does not work with current chart drawing script! + # StatelessPSOPropagator(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng) # Attention! Does not work + # with current chart drawing script! ] ) - init = PSOInitUniform(limits, rng=rng) + init = PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) propagator = Conditional(POP_SIZE, propagator, init) - islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path='./checkpoints/', + islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, num_islands=4, checkpoint_path='./checkpoints/', migration_probability=0, pollination=False) islands.evolve(top_n=1, logging_interval=1) islands.propulator.paint_graphs(function_name) From c9430c24e39564deb21df2058490dd7291adfc2d Mon Sep 17 00:00:00 2001 From: Morridin Date: Sun, 20 Aug 2023 16:19:43 +0200 Subject: [PATCH 050/139] Wrote some Benchmarking stuff --- ap_pso/bm/bm_hyppopy.sh | 55 ++++++++++++++++++++++++++ ap_pso/bm/bm_init.sh | 70 ++++++++++++++++++++++++++++++++++ ap_pso/bm/bm_starter.sh | 43 +++++++++++++++++++++ ap_pso/bm/hyppopy_benchmark.py | 57 +++++++++++++++++++++++++++ ap_pso/bm/pso_benchmark.py | 45 ++++++++++++++++++++++ 5 files changed, 270 insertions(+) create mode 100644 ap_pso/bm/bm_hyppopy.sh create mode 100644 ap_pso/bm/bm_init.sh create mode 100644 ap_pso/bm/bm_starter.sh create mode 100644 ap_pso/bm/hyppopy_benchmark.py create mode 100644 ap_pso/bm/pso_benchmark.py diff --git a/ap_pso/bm/bm_hyppopy.sh b/ap_pso/bm/bm_hyppopy.sh new file mode 100644 index 00000000..cec868ea --- /dev/null +++ b/ap_pso/bm/bm_hyppopy.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" +BASE_DIR="." +for RACE in {0..4} +do + NODES=$(( 2 ** RACE )) + ITERATIONS=$(( 2000 / NODES )) + QUEUE="single" + if [[ $NODES -gt 1 ]] + then + QUEUE="multiple" + fi +# case "$RACE" in +# 5) +# ITERATIONS=$((ITERATIONS * 10)) +# ;; +# 6) +# ITERATIONS=-1 +# ;; +# *) +# echo "Error: Race $RACE was called." +# exit +# ;; +# esac + SCRIPT="#!/bin/bash +#SBATCH --nodes=${NODES} +#SBATCH --partition=${QUEUE} +#SBATCH --job-name=\"hyppopy_${RACE}\" +#SBATCH --time=15:00 +#SBATCH --mem=10000 +#SBATCH --cpus-per-task=40 +#SBATCH --ntasks-per-node=1 +#SBATCH --mail-type=ALL +#SBATCH --mail-user=pa1164@partner.kit.edu + +cd \$(ws_find propulate_bm_1) +ml purge +ml restore propulate +source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate +" + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + do + DIRNAME="bm_H_${FUNCTION}_${RACE}" + EXECUTION_DIR="${BASE_DIR}/ap_pso/bm/${DIRNAME}" + mkdir "$EXECUTION_DIR" + + SCRIPT+="mpirun python -u ${BASE_DIR}/ap_pso/bm/hyppopy_benchmark.py ${FUNCTION} ${ITERATIONS} ${EXECUTION_DIR} +" + done + SCRIPT+="deactivate +" + FILE="${BASE_DIR}/ap_pso/bm/start_bm_H_${RACE}.sh" + echo "${SCRIPT}" > "${FILE}" + sbatch -p "${QUEUE}" -N "${NODES}" "${FILE}" +done diff --git a/ap_pso/bm/bm_init.sh b/ap_pso/bm/bm_init.sh new file mode 100644 index 00000000..434324fa --- /dev/null +++ b/ap_pso/bm/bm_init.sh @@ -0,0 +1,70 @@ +#!/bin/bash +BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/" +for I in {0..3} +do + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + do + for RACE in {0..4} + do + EXECUTION_DIR="${BASE_DIR}/ap_pso/bm/bm_${I}_${FUNCTION}_${RACE}" + mkdir "$EXECUTION_DIR" + mkdir "${EXECUTION_DIR}/images" + NODES=40 + ITERATIONS=2000 + QUEUE="single" + case "$RACE" in + 0) + ;; + 1) + NODES=$((NODES * 2)) + ITERATIONS=$((ITERATIONS / 2)) + QUEUE="multiple" + ;; + 2) + NODES=$((NODES * 4)) + ITERATIONS=$((ITERATIONS / 4)) + QUEUE="multiple" + ;; + 3) + NODES=$((NODES * 8)) + ITERATIONS=$((ITERATIONS / 8)) + QUEUE="multiple" + ;; + 4) + NODES=$((NODES * 16)) + ITERATIONS=$((ITERATIONS / 16)) + QUEUE="multiple" + ;; + 5) + ITERATIONS=$((ITERATIONS * 10)) + ;; + 6) + ITERATIONS=-1 + ;; + *) + echo "Error: Race $RACE was called." + ;; + esac + SCRIPT="#!/bin/bash +#SBATCH --nodes=${NODES} +#SBATCH --partition=${QUEUE} +#SBATCH --job-name=${FUNCTION}_${I}_${RACE} +#SBATCH --time=15:00 +#SBATCH --mem=10000 +#SBATCH --cpus-per-task=40 +#SBATCH --ntasks-per-node=1 +#SBATCH --mail-type=ALL +#SBATCH --mail-user=pa1164@partner.kit.edu + +cd \$(ws_find propulate_bm_1) +ml purge +ml restore propulate +source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate + +mpirun python -u ${BASE_DIR}/ap_pso/bm/pso_benchmark.py ${FUNCTION} ${ITERATIONS} ${I} ${EXECUTION_DIR} +deactivate +" + echo "${SCRIPT}" > "${EXECUTION_DIR}/bm_start.sh" + done + done +done diff --git a/ap_pso/bm/bm_starter.sh b/ap_pso/bm/bm_starter.sh new file mode 100644 index 00000000..bdd41edb --- /dev/null +++ b/ap_pso/bm/bm_starter.sh @@ -0,0 +1,43 @@ +#!/bin/bash +BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/" +for I in {0..3} +do + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + do + for RACE in {0..4} + do + EXECUTION_DIR="${BASE_DIR}/ap_pso/bm/bm_${I}_${FUNCTION}_${RACE}/bm_start.sh" + NODES=1 + QUEUE="single" + case "$RACE" in + 0) + ;; + 1) + NODES=$((NODES * 2)) + QUEUE="multiple" + ;; + 2) + NODES=$((NODES * 4)) + QUEUE="multiple" + ;; + 3) + NODES=$((NODES * 8)) + QUEUE="multiple" + ;; + 4) + NODES=$((NODES * 16)) + QUEUE="multiple" + ;; + 5) + ;; + 6) + ;; + *) + echo "Error: Race $RACE was called." + exit + ;; + esac + sbatch -p "${QUEUE}" -N "${NODES}" "${EXECUTION_DIR}" + done + done +done diff --git a/ap_pso/bm/hyppopy_benchmark.py b/ap_pso/bm/hyppopy_benchmark.py new file mode 100644 index 00000000..d027d0b0 --- /dev/null +++ b/ap_pso/bm/hyppopy_benchmark.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +import os.path +import pickle +import random +import sys +import warnings +from pathlib import Path + +from hyppopy.HyppopyProject import HyppopyProject +from hyppopy.MPIBlackboxFunction import MPIBlackboxFunction +from hyppopy.SolverPool import SolverPool +from hyppopy.solvers.MPISolverWrapper import MPISolverWrapper +from hyppopy.solvers.OptunitySolver import OptunitySolver +from mpi4py import MPI + +from scripts.function_benchmark import get_function_search_space + +if __name__ == "__main__": + assert len(sys.argv) >= 4 + + ############ + # SETTINGS # + ############ + + function_name = sys.argv[1] # Get function to optimize from command-line. + max_iterations = int(sys.argv[2]) + CHECKPOINT_PLACE = sys.argv[3] + POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. + + function, limits = get_function_search_space(function_name) + rng = random.Random(MPI.COMM_WORLD.rank) + + project = HyppopyProject() + for key in limits: + project.add_hyperparameter(name=key, domain="uniform", data=list(limits[key]), type=float) + project.add_setting(name="max_iterations", value=max_iterations) + project.add_setting(name="solver", value="optunity") + + blackbox = MPIBlackboxFunction(blackbox_func=function, mpi_comm=MPI.COMM_WORLD) + + solver = OptunitySolver(project) + solver = MPISolverWrapper(solver=solver, mpi_comm=MPI.COMM_WORLD) + solver.blackbox = blackbox + + solver.run() + df, best = solver.get_results() + + path = Path(f"{CHECKPOINT_PLACE}/result_{MPI.COMM_WORLD.rank}.pkl") + path.parent.mkdir(parents=True, exist_ok=True) + if os.path.isfile(path): + try: + os.replace(path, path.with_suffix(".bkp")) + warnings.warn("Results file already existing! Possibly overwriting data!") + except OSError as e: + print(e) + with open(path, "wb") as f: + pickle.dump((df, best), f) diff --git a/ap_pso/bm/pso_benchmark.py b/ap_pso/bm/pso_benchmark.py new file mode 100644 index 00000000..2f49f415 --- /dev/null +++ b/ap_pso/bm/pso_benchmark.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import random +import sys + +from mpi4py import MPI + +from ap_pso.propagators import PSOInitUniform, VelocityClampingPropagator, ConstrictionPropagator, PSOCompose, \ + BasicPSOPropagator, StatelessPSOPropagator, CanonicalPropagator +from propulate import Islands +from propulate.propagators import Conditional, InitUniform +from scripts.function_benchmark import get_function_search_space + +############ +# SETTINGS # +############ + +function_name = sys.argv[1] # Get function to optimize from command-line. +NUM_GENERATIONS: int = int(sys.argv[2]) # Set number of generations. +POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. +PSO_TYPE = int(sys.argv[3]) # selects the propagator below +CHECKPOINT_PLACE = sys.argv[4] +num_migrants = 1 + +function, limits = get_function_search_space(function_name) + +if __name__ == "__main__": + # migration_topology = num_migrants*np.ones((4, 4), dtype=int) + # np.fill_diagonal(migration_topology, 0) + + rng = random.Random(MPI.COMM_WORLD.rank) + + propagator = [ + VelocityClampingPropagator(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6), + ConstrictionPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng), + BasicPSOPropagator(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng), + CanonicalPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) + ][PSO_TYPE] + + init = PSOInitUniform(limits, rng=rng) + propagator = Conditional(POP_SIZE, propagator, init) + + islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path=CHECKPOINT_PLACE, + migration_probability=0, pollination=False) + islands.evolve(top_n=1, logging_interval=1) + islands.propulator.paint_graphs(function_name) From 27b8b567ce2973b6736129dad3014813f8374451 Mon Sep 17 00:00:00 2001 From: Morridin Date: Sun, 20 Aug 2023 17:50:11 +0200 Subject: [PATCH 051/139] Corrected a small error in benchmark starter script. --- ap_pso/bm/bm_hyppopy.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ap_pso/bm/bm_hyppopy.sh b/ap_pso/bm/bm_hyppopy.sh index cec868ea..b8a083dc 100644 --- a/ap_pso/bm/bm_hyppopy.sh +++ b/ap_pso/bm/bm_hyppopy.sh @@ -1,6 +1,5 @@ #!/bin/bash -# BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" -BASE_DIR="." +BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" for RACE in {0..4} do NODES=$(( 2 ** RACE )) From 32ed5e852a57287272a77352e02524225b1c4625 Mon Sep 17 00:00:00 2001 From: Morridin Date: Sun, 20 Aug 2023 17:57:57 +0200 Subject: [PATCH 052/139] Created propulate test setup --- ap_pso/bm/results/bm_propulate.sh | 54 +++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 ap_pso/bm/results/bm_propulate.sh diff --git a/ap_pso/bm/results/bm_propulate.sh b/ap_pso/bm/results/bm_propulate.sh new file mode 100644 index 00000000..714525d9 --- /dev/null +++ b/ap_pso/bm/results/bm_propulate.sh @@ -0,0 +1,54 @@ +#!/bin/bash +BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" +for RACE in {0..4} +do + NODES=$(( 2 ** RACE )) + ITERATIONS=$(( 2000 / NODES )) + QUEUE="single" + if [[ $NODES -gt 1 ]] + then + QUEUE="multiple" + fi +# case "$RACE" in +# 5) +# ITERATIONS=$((ITERATIONS * 10)) +# ;; +# 6) +# ITERATIONS=-1 +# ;; +# *) +# echo "Error: Race $RACE was called." +# exit +# ;; +# esac + SCRIPT="#!/bin/bash +#SBATCH --nodes=${NODES} +#SBATCH --partition=${QUEUE} +#SBATCH --job-name=\"propulate_${RACE}\" +#SBATCH --time=15:00 +#SBATCH --mem=10000 +#SBATCH --cpus-per-task=40 +#SBATCH --ntasks-per-node=1 +#SBATCH --mail-type=ALL +#SBATCH --mail-user=pa1164@partner.kit.edu + +cd \$(ws_find propulate_bm_1) +ml purge +ml restore propulate +source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate +" + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + do + DIRNAME="bm_P_${FUNCTION}_${RACE}" + EXECUTION_DIR="${BASE_DIR}/ap_pso/bm/${DIRNAME}" + mkdir "$EXECUTION_DIR" + + SCRIPT+="mpirun python -u ${BASE_DIR}/scripts/islands_example.py -f ${FUNCTION} -g ${ITERATIONS} -ckpt ${EXECUTION_DIR} -i 1 -migp 0 +" + done + SCRIPT+="deactivate +" + FILE="${BASE_DIR}/ap_pso/bm/start_bm_P_${RACE}.sh" + echo "${SCRIPT}" > "${FILE}" + sbatch -p "${QUEUE}" -N "${NODES}" "${FILE}" +done From 87760b62ae50dfba446d99abef71d23b611c144c Mon Sep 17 00:00:00 2001 From: Morridin Date: Sun, 20 Aug 2023 18:03:11 +0200 Subject: [PATCH 053/139] Moved the file where it belongs --- ap_pso/bm/{results => }/bm_propulate.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ap_pso/bm/{results => }/bm_propulate.sh (100%) diff --git a/ap_pso/bm/results/bm_propulate.sh b/ap_pso/bm/bm_propulate.sh similarity index 100% rename from ap_pso/bm/results/bm_propulate.sh rename to ap_pso/bm/bm_propulate.sh From 69920125e4bffacf66526a0f4cf20ded4d6c5f2d Mon Sep 17 00:00:00 2001 From: Morridin Date: Sun, 20 Aug 2023 18:04:57 +0200 Subject: [PATCH 054/139] Small change with large effect on clarity. --- ap_pso/bm/bm_propulate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ap_pso/bm/bm_propulate.sh b/ap_pso/bm/bm_propulate.sh index 714525d9..a26c07d4 100644 --- a/ap_pso/bm/bm_propulate.sh +++ b/ap_pso/bm/bm_propulate.sh @@ -40,7 +40,7 @@ source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" do DIRNAME="bm_P_${FUNCTION}_${RACE}" - EXECUTION_DIR="${BASE_DIR}/ap_pso/bm/${DIRNAME}" + EXECUTION_DIR="${BASE_DIR}/ap_pso/bm/results/${DIRNAME}" mkdir "$EXECUTION_DIR" SCRIPT+="mpirun python -u ${BASE_DIR}/scripts/islands_example.py -f ${FUNCTION} -g ${ITERATIONS} -ckpt ${EXECUTION_DIR} -i 1 -migp 0 From cd2f8f2152501c11c5d90f3589aeb6a1f8bd3b3e Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 23 Aug 2023 16:29:05 +0200 Subject: [PATCH 055/139] Added two scripts to plot me my graphs. --- ap_pso/bm/results/graph_plotter.py | 87 ++++++++++++++++++++++++++++ ap_pso/bm/results/graph_plotter_H.py | 73 +++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 ap_pso/bm/results/graph_plotter.py create mode 100644 ap_pso/bm/results/graph_plotter_H.py diff --git a/ap_pso/bm/results/graph_plotter.py b/ap_pso/bm/results/graph_plotter.py new file mode 100644 index 00000000..2e10da86 --- /dev/null +++ b/ap_pso/bm/results/graph_plotter.py @@ -0,0 +1,87 @@ +import pickle +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.axes import Axes +from matplotlib.figure import Figure + +functions = ("Sphere", "Rosenbrock", "Step", "Quartic", "Griewank", "Rastrigin", "Schwefel", "BiSphere", "BiRastrigin") +function_name = functions[8] + +if __name__ == "__main__": + path = Path(".") + data = [] + pso_names = ("VelocityClamping", "Constriction", "Basic", "Canonical") + marker_list = ("o", "s", "D", "^", "P") # ["o", "v", "^", "<", ">", "s", "p", "P", "*", "h", "X", "D"] + # np.random.shuffle(marker_list) + for i in range(4): + data.append([]) + for p in path.glob(f"bm_{i}_{function_name.lower()}_?"): + if not p.is_dir(): + continue + for file in p.iterdir(): + if not file.suffix == ".pkl": + continue + with open(file, "rb") as f: + data[i].append(pickle.load(f, fix_imports=True)) + data.append([]) + for p in path.glob(f"bm_P_{function_name.lower()}_?"): + if not p.is_dir(): + continue + for file in p.iterdir(): + if not file.suffix == ".pkl": + continue + with open(file, "rb") as f: + data[-1].append(pickle.load(f, fix_imports=True)) + + plt_data = [] + for i in range(5): + plt_data.append([]) + for x in data[i]: + entry = [min(x, key=lambda v: v.loss).loss, max(x, key=lambda v: v.rank).rank + 1] + plt_data[i].append(entry) + plt_data[i] = np.array(sorted(plt_data[i], key=lambda v: v[1])).T + + fig: Figure + ax: Axes + + fig, ax = plt.subplots() + # fig.subplots_adjust(hspace=0) + + ax.set_title(f"PSO@Propulate on {function_name} function") + ax.set_xlabel("Nodes") + + for i in range(4): + ax.plot(plt_data[i][1], plt_data[i][0], label=pso_names[i], marker=marker_list[i], ls="dotted", lw=2) + ax.plot(plt_data[4][1], plt_data[4][0], label="Vanilla Propulate", marker=marker_list[4], ls="dotted", lw=2, ms=8) + ax.set_xscale("log", base=2) + ax.set_xticks([1, 2, 4, 8, 16], [1, 2, 4, 8, 16]) + ax.grid(True) + ax.set_ylabel("Loss") + if function_name == "Rosenbrock": + ax.set_yscale("symlog", linthresh=1e-19) + ax.set_yticks([0, 1e-18, 1e-15, 1e-12, 1e-9, 1e-6, 1e-3, 1, 100]) + ax.set_ylim(-5e-20, 100) + elif function_name == "Step": + ax.set_yscale("symlog") + ax.set_ylim(-1e4, -5) + elif function_name in ("Rastrigin", "Schwefel", "BiSphere", "BiRastrigin"): + ax.set_yscale("linear") + else: + ax.set_yscale("log") + ax.legend() + + fig.show() + + save_path = Path(f"images/pso_{function_name.lower()}.png") + if save_path.parent.exists() and not save_path.parent.is_dir(): + OSError("There is something in the way. We can't store our paintings.") + save_path.parent.mkdir(parents=True, exist_ok=True) + + fig.savefig(save_path) + fig.savefig(save_path.with_suffix(".svg")) + fig.savefig(save_path.with_suffix(".pdf")) + fig.savefig(save_path.with_stem(save_path.stem + "_T"), transparent=True) + fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".svg"), transparent=True) + fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".pdf"), transparent=True) diff --git a/ap_pso/bm/results/graph_plotter_H.py b/ap_pso/bm/results/graph_plotter_H.py new file mode 100644 index 00000000..f4e1e56a --- /dev/null +++ b/ap_pso/bm/results/graph_plotter_H.py @@ -0,0 +1,73 @@ +import pickle +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.axes import Axes +from matplotlib.figure import Figure + +functions = ("Sphere", "Rosenbrock", "Step", "Quartic", "Griewank", "Rastrigin", "Schwefel", "BiSphere", "BiRastrigin") +function_name = functions[8] + +# Nötige Nacharbeiten: +# ? + +if __name__ == "__main__": + path = Path(".") + data = [] + # pso_names = ("VelocityClamping", "Constriction", "Basic", "Canonical") + marker_list = ("o", "s", "D", "^") # ["o", "v", "^", "<", ">", "s", "p", "P", "*", "h", "X", "D"] + for p in path.glob(f"bm_H_{function_name.lower()}_?"): + if not p.is_dir(): + continue + data.append([]) + for file in p.iterdir(): + if not file.suffix == ".pkl": + continue + with open(file, "rb") as f: + data[-1].append(pickle.load(f, fix_imports=True)) + if len(data[-1]) == 0: + del data[-1] + for i, _ in enumerate(data): + data[i] = [dx for dx in data[i] if not any([dxt is None for dxt in dx])][0][0] + del _ + + plt_data = [] + for x in data: + entry = [min(x["losses"]), 2000 / len(x)] + plt_data.append(entry) + plt_data = np.array(sorted(plt_data, key=lambda v: v[1])).T + + fig: Figure + ax: Axes + + fig, ax = plt.subplots() + # fig.subplots_adjust(hspace=0) + + ax.set_title(f"Vanilla Propulate on {function_name} function") + ax.set_xlabel("Nodes") + + ax.plot(plt_data[1], plt_data[0], label="Hyppopy", marker="P", ms=10, ls="dotted", lw=2) + ax.set_xscale("log", base=2) + ax.set_xticks([1, 2, 4, 8, 16], [1, 2, 4, 8, 16]) + ax.grid(True) + ax.set_ylabel("Loss") + if function_name in ("Step", "Griewank", "Rastrigin", "Schwefel", "BiRastrigin"): + ax.set_yscale("linear") + else: + ax.set_yscale("log") + ax.legend() + + fig.show() + + # save_path = Path(f"images/propulate/{function_name.lower()}.png") + # if save_path.parent.exists() and not save_path.parent.is_dir(): + # OSError("There is something in the way. We can't store our paintings.") + # save_path.parent.mkdir(parents=True, exist_ok=True) + # + # fig.savefig(save_path) + # fig.savefig(save_path.with_suffix(".svg")) + # fig.savefig(save_path.with_suffix(".pdf")) + # fig.savefig(save_path.with_stem(save_path.stem + "_T"), transparent=True) + # fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".svg"), transparent=True) + # fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".pdf"), transparent=True) From a8225978def43846b15960f3c7c823b0f55ef8df Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 21 Aug 2023 20:01:54 +0200 Subject: [PATCH 056/139] Redesigned the whole benchmark because I made a severe mistake. --- ap_pso/bm/bm_all.sh | 68 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 ap_pso/bm/bm_all.sh diff --git a/ap_pso/bm/bm_all.sh b/ap_pso/bm/bm_all.sh new file mode 100644 index 00000000..501347e3 --- /dev/null +++ b/ap_pso/bm/bm_all.sh @@ -0,0 +1,68 @@ +#!/bin/bash +BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" +for RACE in {0..4} +do + NODES=$(( 2 ** RACE )) + TASKS=$(( 64 * NODES )) + ITERATIONS=$(( 2000 / NODES )) + QUEUE="dev_multiple_il" + if [[ $RACE -eq 0 ]] + then + NODES=2 + fi + if [[ $RACE -eq 4 ]] + then + QUEUE="multiple_il" + fi + SCRIPT="#!/bin/bash +#SBATCH --nodes=${NODES} +#SBATCH --ntasks=${TASKS} +#SBATCH --partition=${QUEUE} +#SBATCH --job-name=\"all_${RACE}\" +#SBATCH --time=60:00 +#SBATCH --mem=40000 +#SBATCH --cpus-per-task=1 +#SBATCH --mail-type=ALL +#SBATCH --mail-user=pa1164@partner.kit.edu + +cd \$(ws_find propulate_bm_1) +ml purge +ml restore propulate +source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate +" + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + do + DIRNAME="bm_P_${FUNCTION}_${RACE}" + RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results2/${DIRNAME}" + mkdir "$RESULTS_DIR" + + SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/scripts/islands_example.py -f ${FUNCTION} -g ${ITERATIONS} -ckpt ${RESULTS_DIR} -i 1 -migp 0 +" + done + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + do + DIRNAME="bm_H_${FUNCTION}_${RACE}" + RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results2/${DIRNAME}" + mkdir "$RESULTS_DIR" + + SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/hyppopy_benchmark.py -${FUNCTION} ${ITERATIONS} ${RESULTS_DIR} +" + done + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + do + for PSO in {0..3} + do + DIRNAME="bm_${PSO}_${FUNCTION}_${RACE}" + RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results2/${DIRNAME}" + mkdir "$RESULTS_DIR" + + SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/pso_benchmark.py ${FUNCTION} ${ITERATIONS} ${I} ${RESULTS_DIR} +" + done + done + SCRIPT+="deactivate +" + FILE="${BASE_DIR}/ap_pso/bm/start_bm_A_${RACE}.sh" + echo "${SCRIPT}" > "${FILE}" + sbatch -p "${QUEUE}" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 -t 60:00 "${FILE}" +done From 534816b710a9f733141e08f39679034988bd2323 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 21 Aug 2023 20:05:56 +0200 Subject: [PATCH 057/139] Adjusted time settings to the requirements. --- ap_pso/bm/bm_all.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ap_pso/bm/bm_all.sh b/ap_pso/bm/bm_all.sh index 501347e3..5aa1eadc 100644 --- a/ap_pso/bm/bm_all.sh +++ b/ap_pso/bm/bm_all.sh @@ -19,7 +19,7 @@ do #SBATCH --ntasks=${TASKS} #SBATCH --partition=${QUEUE} #SBATCH --job-name=\"all_${RACE}\" -#SBATCH --time=60:00 +#SBATCH --time=30:00 #SBATCH --mem=40000 #SBATCH --cpus-per-task=1 #SBATCH --mail-type=ALL @@ -64,5 +64,5 @@ source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate " FILE="${BASE_DIR}/ap_pso/bm/start_bm_A_${RACE}.sh" echo "${SCRIPT}" > "${FILE}" - sbatch -p "${QUEUE}" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 -t 60:00 "${FILE}" + sbatch -p "${QUEUE}" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 "${FILE}" done From ade8b632285f0bb4156108a0275b4f5cb8da5308 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 23 Aug 2023 09:07:31 +0200 Subject: [PATCH 058/139] Bugfixing: wrong variable given. --- ap_pso/bm/bm_all.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ap_pso/bm/bm_all.sh b/ap_pso/bm/bm_all.sh index 5aa1eadc..033da8eb 100644 --- a/ap_pso/bm/bm_all.sh +++ b/ap_pso/bm/bm_all.sh @@ -3,7 +3,7 @@ BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" for RACE in {0..4} do NODES=$(( 2 ** RACE )) - TASKS=$(( 64 * NODES )) + TASKS=$(( 32 * NODES )) ITERATIONS=$(( 2000 / NODES )) QUEUE="dev_multiple_il" if [[ $RACE -eq 0 ]] @@ -56,7 +56,7 @@ source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results2/${DIRNAME}" mkdir "$RESULTS_DIR" - SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/pso_benchmark.py ${FUNCTION} ${ITERATIONS} ${I} ${RESULTS_DIR} + SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/pso_benchmark.py ${FUNCTION} ${ITERATIONS} ${PSO} ${RESULTS_DIR} " done done From fd03ee31bb749804330976c619a93d375caa4f47 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 23 Aug 2023 09:13:48 +0200 Subject: [PATCH 059/139] Bugfixing: wrong task count given. --- ap_pso/bm/bm_all.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ap_pso/bm/bm_all.sh b/ap_pso/bm/bm_all.sh index 033da8eb..011b326e 100644 --- a/ap_pso/bm/bm_all.sh +++ b/ap_pso/bm/bm_all.sh @@ -3,7 +3,7 @@ BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" for RACE in {0..4} do NODES=$(( 2 ** RACE )) - TASKS=$(( 32 * NODES )) + TASKS=$(( 64 * NODES )) ITERATIONS=$(( 2000 / NODES )) QUEUE="dev_multiple_il" if [[ $RACE -eq 0 ]] From 5c27b2c56acacd5d53f48bd54ca8630fec4635a5 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 23 Aug 2023 10:19:44 +0200 Subject: [PATCH 060/139] Fixed some further issues with the benchmark starting script. --- ap_pso/bm/bm_all.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ap_pso/bm/bm_all.sh b/ap_pso/bm/bm_all.sh index 011b326e..8213978a 100644 --- a/ap_pso/bm/bm_all.sh +++ b/ap_pso/bm/bm_all.sh @@ -5,7 +5,7 @@ do NODES=$(( 2 ** RACE )) TASKS=$(( 64 * NODES )) ITERATIONS=$(( 2000 / NODES )) - QUEUE="dev_multiple_il" + QUEUE="multiple_il" if [[ $RACE -eq 0 ]] then NODES=2 @@ -19,7 +19,7 @@ do #SBATCH --ntasks=${TASKS} #SBATCH --partition=${QUEUE} #SBATCH --job-name=\"all_${RACE}\" -#SBATCH --time=30:00 +#SBATCH --time=4:00:00 #SBATCH --mem=40000 #SBATCH --cpus-per-task=1 #SBATCH --mail-type=ALL @@ -45,7 +45,7 @@ source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results2/${DIRNAME}" mkdir "$RESULTS_DIR" - SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/hyppopy_benchmark.py -${FUNCTION} ${ITERATIONS} ${RESULTS_DIR} + SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/hyppopy_benchmark.py ${FUNCTION} ${ITERATIONS} ${RESULTS_DIR} " done for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" From 76d5d2bb11c07d6e1e081ea69944bb8cfe88eeb3 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 23 Aug 2023 22:39:41 +0200 Subject: [PATCH 061/139] Corrected some infeasible configuration. --- ap_pso/bm/results/graph_plotter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ap_pso/bm/results/graph_plotter.py b/ap_pso/bm/results/graph_plotter.py index 2e10da86..3c9310c6 100644 --- a/ap_pso/bm/results/graph_plotter.py +++ b/ap_pso/bm/results/graph_plotter.py @@ -7,7 +7,7 @@ from matplotlib.figure import Figure functions = ("Sphere", "Rosenbrock", "Step", "Quartic", "Griewank", "Rastrigin", "Schwefel", "BiSphere", "BiRastrigin") -function_name = functions[8] +function_name = functions[7] if __name__ == "__main__": path = Path(".") @@ -54,7 +54,7 @@ for i in range(4): ax.plot(plt_data[i][1], plt_data[i][0], label=pso_names[i], marker=marker_list[i], ls="dotted", lw=2) - ax.plot(plt_data[4][1], plt_data[4][0], label="Vanilla Propulate", marker=marker_list[4], ls="dotted", lw=2, ms=8) + ax.plot(plt_data[4][1], plt_data[4][0], label="Vanilla Propulate", marker=marker_list[4], lw=1, ms=8) ax.set_xscale("log", base=2) ax.set_xticks([1, 2, 4, 8, 16], [1, 2, 4, 8, 16]) ax.grid(True) From d632aa3dedbf46d6130b350e9cad6fff20ef0ee3 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 23 Aug 2023 22:40:32 +0200 Subject: [PATCH 062/139] Adjusted the PSO benchmark script to my needs. --- ap_pso/scripts/pso_example.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ap_pso/scripts/pso_example.py b/ap_pso/scripts/pso_example.py index b397d558..991201ef 100644 --- a/ap_pso/scripts/pso_example.py +++ b/ap_pso/scripts/pso_example.py @@ -17,7 +17,6 @@ function_name = sys.argv[1] # Get function to optimize from command-line. NUM_GENERATIONS: int = int(sys.argv[2]) # Set number of generations. POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. -num_migrants = 1 function, limits = get_function_search_space(function_name) @@ -42,7 +41,7 @@ init = PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) propagator = Conditional(POP_SIZE, propagator, init) - islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, num_islands=4, checkpoint_path='./checkpoints/', + islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path='./checkpoints/', migration_probability=0, pollination=False) - islands.evolve(top_n=1, logging_interval=1) - islands.propulator.paint_graphs(function_name) + islands.evolve(debug=0) + # islands.propulator.paint_graphs(function_name) From 03b8a4299d0d1fd67221e9633ba86a5348b743ba Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 24 Aug 2023 07:59:03 +0200 Subject: [PATCH 063/139] Made another bugfix. Things start to get on my nervs. --- ap_pso/bm/pso_benchmark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ap_pso/bm/pso_benchmark.py b/ap_pso/bm/pso_benchmark.py index 2f49f415..3833b3e5 100644 --- a/ap_pso/bm/pso_benchmark.py +++ b/ap_pso/bm/pso_benchmark.py @@ -36,7 +36,7 @@ CanonicalPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) ][PSO_TYPE] - init = PSOInitUniform(limits, rng=rng) + init = PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) propagator = Conditional(POP_SIZE, propagator, init) islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path=CHECKPOINT_PLACE, From 023a993abeb9deeb87ea9aa3f0cc3a5d34a34807 Mon Sep 17 00:00:00 2001 From: Morridin Date: Fri, 25 Aug 2023 09:18:53 +0200 Subject: [PATCH 064/139] Recovered some important changes to benchmarks --- ap_pso/bm/hyppopy_benchmark.py | 12 ++++++++++++ ap_pso/bm/pso_benchmark.py | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ap_pso/bm/hyppopy_benchmark.py b/ap_pso/bm/hyppopy_benchmark.py index d027d0b0..c409201d 100644 --- a/ap_pso/bm/hyppopy_benchmark.py +++ b/ap_pso/bm/hyppopy_benchmark.py @@ -4,6 +4,7 @@ import random import sys import warnings +import time from pathlib import Path from hyppopy.HyppopyProject import HyppopyProject @@ -30,6 +31,11 @@ function, limits = get_function_search_space(function_name) rng = random.Random(MPI.COMM_WORLD.rank) + if MPI.COMM_WORLD.rank == 0: + print("#-----------------------------------#") + print(f"| Current time: {time.time_ns()} |") + print("#-----------------------------------#") + project = HyppopyProject() for key in limits: project.add_hyperparameter(name=key, domain="uniform", data=list(limits[key]), type=float) @@ -55,3 +61,9 @@ print(e) with open(path, "wb") as f: pickle.dump((df, best), f) + + if MPI.COMM_WORLD.rank == 0: + print("#-----------------------------------#") + print(f"| Current time: {time.time_ns()} |") + print("#-----------------------------------#") + diff --git a/ap_pso/bm/pso_benchmark.py b/ap_pso/bm/pso_benchmark.py index 3833b3e5..64240795 100644 --- a/ap_pso/bm/pso_benchmark.py +++ b/ap_pso/bm/pso_benchmark.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import random import sys +import time from mpi4py import MPI @@ -38,8 +39,18 @@ init = PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) propagator = Conditional(POP_SIZE, propagator, init) + if MPI.COMM_WORLD.rank == 0: + print("#-----------------------------------#") + print(f"| Current time: {time.time_ns()} |") + print("#-----------------------------------#") islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path=CHECKPOINT_PLACE, migration_probability=0, pollination=False) - islands.evolve(top_n=1, logging_interval=1) - islands.propulator.paint_graphs(function_name) + islands.evolve(top_n=1, logging_interval=10, debug=0) + + if MPI.COMM_WORLD.rank == 0: + print("#-----------------------------------#") + print(f"| Current time: {time.time_ns()} |") + print("#-----------------------------------#") + +# islands.propulator.paint_graphs(function_name) From 8f9c03ab697ecb0e51461bc12b721011c991c97e Mon Sep 17 00:00:00 2001 From: Morridin Date: Fri, 25 Aug 2023 18:58:54 +0200 Subject: [PATCH 065/139] Added the NAS benchmark setup. --- ap_pso/bm/torch_benchmark.py | 157 +++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100755 ap_pso/bm/torch_benchmark.py diff --git a/ap_pso/bm/torch_benchmark.py b/ap_pso/bm/torch_benchmark.py new file mode 100755 index 00000000..df21b247 --- /dev/null +++ b/ap_pso/bm/torch_benchmark.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 + +import random +import sys +import time + +import numpy as np +import torch +from torch import nn +from torch.utils.data import DataLoader + +from pytorch_lightning import LightningModule, Trainer +from torchmetrics import Accuracy + +from torchvision.datasets import MNIST +from torchvision.transforms import Compose, ToTensor, Normalize + +from mpi4py import MPI + +from ap_pso.propagators import * +from propulate import Islands +from propulate.propagators import Conditional + +num_generations = 3 +pop_size = 2 * MPI.COMM_WORLD.size +GPUS_PER_NODE = 4 + +limits = { + "convlayers": (2.0, 10.0), + "lr": (0.01, 0.0001), +} + + +class Net(LightningModule): + def __init__(self, convlayers: int, activation, lr: float, loss_fn): + super(Net, self).__init__() + + self.best_accuracy: float = 0.0 + self.lr = lr + self.loss_fn = loss_fn + layers = [] + layers += [ + nn.Sequential(nn.Conv2d(1, + 10, + kernel_size=3, + padding=1), + activation()), + ] + layers += [ + nn.Sequential(nn.Conv2d(10, + 10, + kernel_size=3, + padding=1), + activation()) + for _ in range(convlayers - 1) + ] + + self.fc = nn.Linear(7840, 10) + self.conv_layers = nn.Sequential(*layers) + + self.val_acc = Accuracy('multiclass', num_classes=10) + + def forward(self, x): + b, c, w, h = x.size() + x = self.conv_layers(x) + x = x.view(b, 10 * 28 * 28) + x = self.fc(x) + + return x + + def training_step(self, batch, batch_idx): + x, y = batch + loss = self.loss_fn(self(x), y) + + return loss + + def validation_step(self, batch, batch_idx): + x, y = batch + pred = self(x) + loss = self.loss_fn(pred, y) + val_acc = self.val_acc(torch.nn.functional.softmax(pred, dim=-1), y) + if val_acc > self.best_accuracy: + self.best_accuracy = val_acc + return loss + + def configure_optimizers(self): + optimizer = torch.optim.SGD(self.parameters(), lr=self.lr) + return optimizer + + +def get_data_loaders(batch_size, dl=False): + data_transform = Compose([ToTensor(), Normalize((0.1307,), (0.3081,))]) + train_loader = DataLoader( + MNIST(download=dl, root=".", transform=data_transform, train=True), + batch_size=batch_size, + shuffle=True, + ) + val_loader = DataLoader( + MNIST(download=dl, root=".", transform=data_transform, train=False), + batch_size=1, + shuffle=False, + ) + return train_loader, val_loader + + +def ind_loss(params): + convlayers = int(np.round(params["convlayers"])) + activation = "leakyrelu" + lr = params["lr"] + epochs = 2 + + activations = {"relu": nn.ReLU, "sigmoid": nn.Sigmoid, "tanh": nn.Tanh, "leakyrelu": nn.LeakyReLU} + activation = activations[activation] + loss_fn = torch.nn.CrossEntropyLoss() + + model = Net(convlayers, activation, lr, loss_fn) + if MPI.COMM_WORLD.rank == 0: + train_loader, val_loader = get_data_loaders(8, True) + MPI.COMM_WORLD.barrier() + if MPI.COMM_WORLD.rank != 0: + train_loader, val_loader = get_data_loaders(8) + trainer = Trainer(max_epochs=epochs, + accelerator='gpu', + devices=[ + MPI.COMM_WORLD.Get_rank() % GPUS_PER_NODE + ], + enable_progress_bar=False, + ) + trainer.fit(model, train_loader, val_loader) + + return -model.best_accuracy + + +if __name__ == "__main__": + rng = random.Random(MPI.COMM_WORLD.rank) + pso = [ + VelocityClampingPropagator(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6), + ConstrictionPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng), + BasicPSOPropagator(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng), + CanonicalPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) + ][int(sys.argv[1])] + + # propagator = get_default_propagator(pop_size, limits, 0.7, 0.4, 0.1, rng=rng) + + if MPI.COMM_WORLD.rank == 0: + print("#-----------------------------------#") + print(f"| Current time: {time.time_ns()} |") + print("#-----------------------------------#") + + propagator = Conditional(pop_size, pso, PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank)) + islands = Islands(ind_loss, propagator, rng, generations=num_generations, num_islands=1, migration_probability=0) + islands.evolve(top_n=1, debug=2) + + if MPI.COMM_WORLD.rank == 0: + print("#-----------------------------------#") + print(f"| Current time: {time.time_ns()} |") + print("#-----------------------------------#") From 4012bfbb94cc716d6cb9d81bd1ad2c1dfdc17e99 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 28 Aug 2023 14:52:48 +0200 Subject: [PATCH 066/139] Unified and improved the graph plotters. --- ap_pso/bm/graph_plotter.py | 106 +++++++++++++++++++++++++++ ap_pso/bm/results/graph_plotter.py | 87 ---------------------- ap_pso/bm/results/graph_plotter_H.py | 73 ------------------ 3 files changed, 106 insertions(+), 160 deletions(-) create mode 100644 ap_pso/bm/graph_plotter.py delete mode 100644 ap_pso/bm/results/graph_plotter.py delete mode 100644 ap_pso/bm/results/graph_plotter_H.py diff --git a/ap_pso/bm/graph_plotter.py b/ap_pso/bm/graph_plotter.py new file mode 100644 index 00000000..045c969b --- /dev/null +++ b/ap_pso/bm/graph_plotter.py @@ -0,0 +1,106 @@ +import pickle +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.axes import Axes +from matplotlib.figure import Figure + +functions = ("Sphere", "Rosenbrock", "Step", "Quartic", "Griewank", "Rastrigin", "Schwefel", "BiSphere", "BiRastrigin") +path = Path("./results3/") + + +def insert_data(d_array, idx, pt): + if not p.is_dir() or len([f for f in p.iterdir()]) == 0: + return + for file in pt.iterdir(): + if not file.suffix == ".pkl": + continue + with open(file, "rb") as f: + tmp = pickle.load(f, fix_imports=True) + d_array[idx].append([min(tmp, key=lambda v: v.loss).loss, (max(tmp, key=lambda v: v.rank).rank + 1) / 64]) + + +def refine_value(raw_value) -> int: + for x in range(5): + if raw_value < 2 ** x: + return 2 ** (x - 1) + else: + return 16 + + +if __name__ == "__main__": + for function_name in functions: + data = [] + pso_names = ("VelocityClamping", "Constriction", "Basic", "Canonical") + marker_list = ("o", "s", "D", "^", "P", "X") # ["o", "v", "^", "<", ">", "s", "p", "P", "*", "h", "X", "D"] + # np.random.shuffle(marker_list) + for i in range(5): + data.append([]) + if i == 4: + d = f"bm_P_{function_name.lower()}_?" + else: + d = f"bm_{i}_{function_name.lower()}_?" + for p in path.glob(d): + insert_data(data, i, p) + data[i] = np.array(sorted(data[i], key=lambda v: v[1])).T + data.append([]) + for p in path.glob(f"bm_H_{function_name.lower()}_?"): + if not p.is_dir(): + continue + file = p / Path("result_0.pkl") + with open(file, "rb") as f: + tmp = pickle.load(f, fix_imports=True) + data[-1].append([min(tmp[0]["losses"]), 2000 // len(tmp[0])]) + if data[-1][-1][1] not in (1, 2, 4, 8, 16): + data[-1][-1][1] = refine_value(data[-1][-1][1]) + data[5] = np.array(sorted(data[5], key=lambda v: v[1])).T + + fig: Figure + ax: Axes + + fig, ax = plt.subplots() + # fig.subplots_adjust(hspace=0) + + ax.set_title(f"PSO@Propulate on {function_name} function") + ax.set_xlabel("Nodes") + + for i in range(4): + # if function_name == "Schwefel" and i < len(pso_names) and pso_names[i] == "VelocityClamping": + # continue + ax.plot(data[i][1], data[i][0], label=pso_names[i], marker=marker_list[i], ls="dotted", lw=2) + ax.plot(data[4][1], data[4][0], label="Vanilla Propulate", marker=marker_list[4], lw=1, ms=8) + ax.plot(data[5][1], data[5][0], label="Hyppopy", marker=marker_list[5], lw=1, ms=8) + ax.set_xscale("log", base=2) + ax.set_xticks([1, 2, 4, 8, 16], [1, 2, 4, 8, 16]) + ax.grid(True) + ax.set_ylabel("Loss") + if function_name == "Rosenbrock": + ax.set_yscale("symlog", linthresh=1e-36) + ax.set_yticks([0, 1e-36, 1e-30, 1e-24, 1e-18, 1e-12, 1e-6, 1]) + ax.set_ylim(-5e-36, 1) + elif function_name == "Step": + ax.set_yscale("symlog") + ax.set_ylim(-1e5, -5) + elif function_name == "Schwefel": + ax.set_yscale("symlog") + ax.set_ylim(-50000, 5000) + elif function_name in ("Schwefel", "Rastrigin", "BiSphere", "BiRastrigin"): + ax.set_yscale("linear") + else: + ax.set_yscale("log") + ax.legend() + + fig.show() + + save_path = Path(f"images/pso_{function_name.lower()}.png") + if save_path.parent.exists() and not save_path.parent.is_dir(): + OSError("There is something in the way. We can't store our paintings.") + save_path.parent.mkdir(parents=True, exist_ok=True) + + fig.savefig(save_path) + fig.savefig(save_path.with_suffix(".svg")) + fig.savefig(save_path.with_suffix(".pdf")) + fig.savefig(save_path.with_stem(save_path.stem + "_T"), transparent=True) + fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".svg"), transparent=True) + fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".pdf"), transparent=True) diff --git a/ap_pso/bm/results/graph_plotter.py b/ap_pso/bm/results/graph_plotter.py deleted file mode 100644 index 3c9310c6..00000000 --- a/ap_pso/bm/results/graph_plotter.py +++ /dev/null @@ -1,87 +0,0 @@ -import pickle -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np -from matplotlib.axes import Axes -from matplotlib.figure import Figure - -functions = ("Sphere", "Rosenbrock", "Step", "Quartic", "Griewank", "Rastrigin", "Schwefel", "BiSphere", "BiRastrigin") -function_name = functions[7] - -if __name__ == "__main__": - path = Path(".") - data = [] - pso_names = ("VelocityClamping", "Constriction", "Basic", "Canonical") - marker_list = ("o", "s", "D", "^", "P") # ["o", "v", "^", "<", ">", "s", "p", "P", "*", "h", "X", "D"] - # np.random.shuffle(marker_list) - for i in range(4): - data.append([]) - for p in path.glob(f"bm_{i}_{function_name.lower()}_?"): - if not p.is_dir(): - continue - for file in p.iterdir(): - if not file.suffix == ".pkl": - continue - with open(file, "rb") as f: - data[i].append(pickle.load(f, fix_imports=True)) - data.append([]) - for p in path.glob(f"bm_P_{function_name.lower()}_?"): - if not p.is_dir(): - continue - for file in p.iterdir(): - if not file.suffix == ".pkl": - continue - with open(file, "rb") as f: - data[-1].append(pickle.load(f, fix_imports=True)) - - plt_data = [] - for i in range(5): - plt_data.append([]) - for x in data[i]: - entry = [min(x, key=lambda v: v.loss).loss, max(x, key=lambda v: v.rank).rank + 1] - plt_data[i].append(entry) - plt_data[i] = np.array(sorted(plt_data[i], key=lambda v: v[1])).T - - fig: Figure - ax: Axes - - fig, ax = plt.subplots() - # fig.subplots_adjust(hspace=0) - - ax.set_title(f"PSO@Propulate on {function_name} function") - ax.set_xlabel("Nodes") - - for i in range(4): - ax.plot(plt_data[i][1], plt_data[i][0], label=pso_names[i], marker=marker_list[i], ls="dotted", lw=2) - ax.plot(plt_data[4][1], plt_data[4][0], label="Vanilla Propulate", marker=marker_list[4], lw=1, ms=8) - ax.set_xscale("log", base=2) - ax.set_xticks([1, 2, 4, 8, 16], [1, 2, 4, 8, 16]) - ax.grid(True) - ax.set_ylabel("Loss") - if function_name == "Rosenbrock": - ax.set_yscale("symlog", linthresh=1e-19) - ax.set_yticks([0, 1e-18, 1e-15, 1e-12, 1e-9, 1e-6, 1e-3, 1, 100]) - ax.set_ylim(-5e-20, 100) - elif function_name == "Step": - ax.set_yscale("symlog") - ax.set_ylim(-1e4, -5) - elif function_name in ("Rastrigin", "Schwefel", "BiSphere", "BiRastrigin"): - ax.set_yscale("linear") - else: - ax.set_yscale("log") - ax.legend() - - fig.show() - - save_path = Path(f"images/pso_{function_name.lower()}.png") - if save_path.parent.exists() and not save_path.parent.is_dir(): - OSError("There is something in the way. We can't store our paintings.") - save_path.parent.mkdir(parents=True, exist_ok=True) - - fig.savefig(save_path) - fig.savefig(save_path.with_suffix(".svg")) - fig.savefig(save_path.with_suffix(".pdf")) - fig.savefig(save_path.with_stem(save_path.stem + "_T"), transparent=True) - fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".svg"), transparent=True) - fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".pdf"), transparent=True) diff --git a/ap_pso/bm/results/graph_plotter_H.py b/ap_pso/bm/results/graph_plotter_H.py deleted file mode 100644 index f4e1e56a..00000000 --- a/ap_pso/bm/results/graph_plotter_H.py +++ /dev/null @@ -1,73 +0,0 @@ -import pickle -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np -from matplotlib.axes import Axes -from matplotlib.figure import Figure - -functions = ("Sphere", "Rosenbrock", "Step", "Quartic", "Griewank", "Rastrigin", "Schwefel", "BiSphere", "BiRastrigin") -function_name = functions[8] - -# Nötige Nacharbeiten: -# ? - -if __name__ == "__main__": - path = Path(".") - data = [] - # pso_names = ("VelocityClamping", "Constriction", "Basic", "Canonical") - marker_list = ("o", "s", "D", "^") # ["o", "v", "^", "<", ">", "s", "p", "P", "*", "h", "X", "D"] - for p in path.glob(f"bm_H_{function_name.lower()}_?"): - if not p.is_dir(): - continue - data.append([]) - for file in p.iterdir(): - if not file.suffix == ".pkl": - continue - with open(file, "rb") as f: - data[-1].append(pickle.load(f, fix_imports=True)) - if len(data[-1]) == 0: - del data[-1] - for i, _ in enumerate(data): - data[i] = [dx for dx in data[i] if not any([dxt is None for dxt in dx])][0][0] - del _ - - plt_data = [] - for x in data: - entry = [min(x["losses"]), 2000 / len(x)] - plt_data.append(entry) - plt_data = np.array(sorted(plt_data, key=lambda v: v[1])).T - - fig: Figure - ax: Axes - - fig, ax = plt.subplots() - # fig.subplots_adjust(hspace=0) - - ax.set_title(f"Vanilla Propulate on {function_name} function") - ax.set_xlabel("Nodes") - - ax.plot(plt_data[1], plt_data[0], label="Hyppopy", marker="P", ms=10, ls="dotted", lw=2) - ax.set_xscale("log", base=2) - ax.set_xticks([1, 2, 4, 8, 16], [1, 2, 4, 8, 16]) - ax.grid(True) - ax.set_ylabel("Loss") - if function_name in ("Step", "Griewank", "Rastrigin", "Schwefel", "BiRastrigin"): - ax.set_yscale("linear") - else: - ax.set_yscale("log") - ax.legend() - - fig.show() - - # save_path = Path(f"images/propulate/{function_name.lower()}.png") - # if save_path.parent.exists() and not save_path.parent.is_dir(): - # OSError("There is something in the way. We can't store our paintings.") - # save_path.parent.mkdir(parents=True, exist_ok=True) - # - # fig.savefig(save_path) - # fig.savefig(save_path.with_suffix(".svg")) - # fig.savefig(save_path.with_suffix(".pdf")) - # fig.savefig(save_path.with_stem(save_path.stem + "_T"), transparent=True) - # fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".svg"), transparent=True) - # fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".pdf"), transparent=True) From ce99538d1eda708052f6d7273142bc490de4d183 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 28 Aug 2023 18:13:39 +0200 Subject: [PATCH 067/139] Added first lines of code to integrate time y-axis. --- ap_pso/bm/graph_plotter.py | 43 +++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/ap_pso/bm/graph_plotter.py b/ap_pso/bm/graph_plotter.py index 045c969b..7563a142 100644 --- a/ap_pso/bm/graph_plotter.py +++ b/ap_pso/bm/graph_plotter.py @@ -6,19 +6,22 @@ from matplotlib.axes import Axes from matplotlib.figure import Figure -functions = ("Sphere", "Rosenbrock", "Step", "Quartic", "Griewank", "Rastrigin", "Schwefel", "BiSphere", "BiRastrigin") +functions = ("Sphere", "Rosenbrock", "Step", "Quartic", "Rastrigin", "Griewank", "Schwefel", "BiSphere", "BiRastrigin") +pso_names = ("VelocityClamping", "Constriction", "Basic", "Canonical") + +time_path = Path("./slurm3/") path = Path("./results3/") def insert_data(d_array, idx, pt): if not p.is_dir() or len([f for f in p.iterdir()]) == 0: return - for file in pt.iterdir(): - if not file.suffix == ".pkl": + for fil in pt.iterdir(): + if not fil.suffix == ".pkl": continue - with open(file, "rb") as f: - tmp = pickle.load(f, fix_imports=True) - d_array[idx].append([min(tmp, key=lambda v: v.loss).loss, (max(tmp, key=lambda v: v.rank).rank + 1) / 64]) + with open(fil, "rb") as f: + tm = pickle.load(f, fix_imports=True) + d_array[idx].append([min(tm, key=lambda v: v.loss).loss, (max(tm, key=lambda v: v.rank).rank + 1) / 64]) def refine_value(raw_value) -> int: @@ -29,12 +32,38 @@ def refine_value(raw_value) -> int: return 16 +def calc_time(iterator) -> float: + start = int(next(iterator).strip("\n|: Ceirmnrtu")) + end = int(next(iterator).strip("\n|: Ceirmnrtu")) + return (end - start) / 1e9 + + if __name__ == "__main__": + raw_time_data: list[str] = [] + time_data: dict[str, dict[str, float]] = {} + + for function_name in functions: + time_data[function_name] = {} + + for file in time_path.iterdir(): + with open(file, "r") as f: + raw_time_data.append(f.read()) + + for x in raw_time_data: + scatter = [st for st in x.split("#-----------------------------------#") if "Current time" in st] + itx = iter(scatter) + for program in ("Vanilla Propulate", "Hyppopy"): + for function_name in functions: + time_data[function_name][program] = calc_time(itx) + for function_name in functions: + for program in pso_names: + time_data[function_name][program] = calc_time(itx) + for function_name in functions: data = [] pso_names = ("VelocityClamping", "Constriction", "Basic", "Canonical") marker_list = ("o", "s", "D", "^", "P", "X") # ["o", "v", "^", "<", ">", "s", "p", "P", "*", "h", "X", "D"] - # np.random.shuffle(marker_list) + for i in range(5): data.append([]) if i == 4: From 683c8c84d4a38bd73ab97b135e4793a2727a89e9 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 28 Aug 2023 18:14:01 +0200 Subject: [PATCH 068/139] Created the start script for weak scaling bm experiments. --- ap_pso/bm/bm_all_weak.sh | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 ap_pso/bm/bm_all_weak.sh diff --git a/ap_pso/bm/bm_all_weak.sh b/ap_pso/bm/bm_all_weak.sh new file mode 100644 index 00000000..9dc9026c --- /dev/null +++ b/ap_pso/bm/bm_all_weak.sh @@ -0,0 +1,59 @@ +#!/bin/bash +BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" +for RACE in {1..4} +do + NODES=$(( 2 ** RACE )) + TASKS=$(( 64 * NODES )) + ITERATIONS=2000 + SCRIPT="#!/bin/bash +#SBATCH --nodes=${NODES} +#SBATCH --ntasks=${TASKS} +#SBATCH --partition=multiple_il +#SBATCH --job-name=\"all_${RACE}_weak\" +#SBATCH --time=8:00:00 +#SBATCH --mem=249600mb +#SBATCH --cpus-per-task=1 +#SBATCH --mail-type=ALL +#SBATCH --mail-user=pa1164@partner.kit.edu + +cd \$(ws_find propulate_bm_1) +ml purge +ml restore propulate +source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate +" + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + do + DIRNAME="bm_P_${FUNCTION}_${RACE}" + RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results4/${DIRNAME}" + mkdir "$RESULTS_DIR" + + SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/scripts/islands_example.py -f ${FUNCTION} -g ${ITERATIONS} -ckpt ${RESULTS_DIR} -i 1 -migp 0 -v 0 +" + done + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + do + DIRNAME="bm_H_${FUNCTION}_${RACE}" + RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results4/${DIRNAME}" + mkdir "$RESULTS_DIR" + + SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/hyppopy_benchmark.py ${FUNCTION} ${ITERATIONS} ${RESULTS_DIR} +" + done + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + do + for PSO in {0..3} + do + DIRNAME="bm_${PSO}_${FUNCTION}_${RACE}" + RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results4/${DIRNAME}" + mkdir "$RESULTS_DIR" + + SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/pso_benchmark.py ${FUNCTION} ${ITERATIONS} ${PSO} ${RESULTS_DIR} +" + done + done + SCRIPT+="deactivate +" + FILE="${BASE_DIR}/ap_pso/bm/start_bm_AW_${RACE}.sh" + echo "${SCRIPT}" > "${FILE}" + sbatch -p "multiple_il" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 "${FILE}" +done From 0ef95431de7354a15eb9939928988db99e467432 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 28 Aug 2023 19:20:06 +0200 Subject: [PATCH 069/139] Added some doc strings and integrated the time axis. --- ap_pso/bm/graph_plotter.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/ap_pso/bm/graph_plotter.py b/ap_pso/bm/graph_plotter.py index 7563a142..53084f05 100644 --- a/ap_pso/bm/graph_plotter.py +++ b/ap_pso/bm/graph_plotter.py @@ -13,7 +13,10 @@ path = Path("./results3/") -def insert_data(d_array, idx, pt): +def insert_data(d_array, idx: int, pt: Path): + """ + This function adds the data given via `pt` into the data array given by `d_array` at position `idx`. + """ if not p.is_dir() or len([f for f in p.iterdir()]) == 0: return for fil in pt.iterdir(): @@ -25,6 +28,9 @@ def insert_data(d_array, idx, pt): def refine_value(raw_value) -> int: + """ + This function ensures that values that are larger than they should be, are corrected to the correct number of cores. + """ for x in range(5): if raw_value < 2 ** x: return 2 ** (x - 1) @@ -33,6 +39,9 @@ def refine_value(raw_value) -> int: def calc_time(iterator) -> float: + """ + This function takes an iterator on a certain string array and calculates out of this a time span in seconds. + """ start = int(next(iterator).strip("\n|: Ceirmnrtu")) end = int(next(iterator).strip("\n|: Ceirmnrtu")) return (end - start) / 1e9 @@ -46,7 +55,7 @@ def calc_time(iterator) -> float: time_data[function_name] = {} for file in time_path.iterdir(): - with open(file, "r") as f: + with open(file) as f: raw_time_data.append(f.read()) for x in raw_time_data: @@ -93,17 +102,22 @@ def calc_time(iterator) -> float: ax.set_title(f"PSO@Propulate on {function_name} function") ax.set_xlabel("Nodes") - - for i in range(4): - # if function_name == "Schwefel" and i < len(pso_names) and pso_names[i] == "VelocityClamping": - # continue - ax.plot(data[i][1], data[i][0], label=pso_names[i], marker=marker_list[i], ls="dotted", lw=2) - ax.plot(data[4][1], data[4][0], label="Vanilla Propulate", marker=marker_list[4], lw=1, ms=8) - ax.plot(data[5][1], data[5][0], label="Hyppopy", marker=marker_list[5], lw=1, ms=8) ax.set_xscale("log", base=2) ax.set_xticks([1, 2, 4, 8, 16], [1, 2, 4, 8, 16]) ax.grid(True) ax.set_ylabel("Loss") + + ax_t = ax.twinx() + ax_t.set_ylabel("Time [s]") + + for i in range(4): + ax.plot(data[i][1], data[i][0], label=pso_names[i], marker=marker_list[i], ls="dashed", lw=2) + ax_t.plot(data[i][1], time_data[function_name][pso_names[i]], marker=marker_list[i], ls="dotted") + ax.plot(data[4][1], data[4][0], label="Vanilla Propulate", marker=marker_list[4], lw=1, ms=8) + ax_t.plot(data[4][1], time_data[function_name]["Vanilla Propulate"], marker=marker_list[4], ms=8, ls="dotted") + ax.plot(data[5][1], data[5][0], label="Hyppopy", marker=marker_list[5], lw=1, ms=8) + ax_t.plot(data[4][1], time_data[function_name]["Hyppopy"], marker=marker_list[5], ms=8, ls="dotted") + if function_name == "Rosenbrock": ax.set_yscale("symlog", linthresh=1e-36) ax.set_yticks([0, 1e-36, 1e-30, 1e-24, 1e-18, 1e-12, 1e-6, 1]) From 7b6f48060459579833f4fb17f7c6794b1a6399b4 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 28 Aug 2023 22:05:18 +0200 Subject: [PATCH 070/139] Created two recovery starter scripts. It turned out, that some few runs failed with a _seg fault_. I mean, it's Python. --- ap_pso/bm/recovery/start_rc_1.sh | 16 ++++++++++++++++ ap_pso/bm/recovery/start_rc_3.sh | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 ap_pso/bm/recovery/start_rc_1.sh create mode 100644 ap_pso/bm/recovery/start_rc_3.sh diff --git a/ap_pso/bm/recovery/start_rc_1.sh b/ap_pso/bm/recovery/start_rc_1.sh new file mode 100644 index 00000000..dd30a24e --- /dev/null +++ b/ap_pso/bm/recovery/start_rc_1.sh @@ -0,0 +1,16 @@ +#!/bin/bash +#SBATCH --nodes=2 +#SBATCH --ntasks=128 +#SBATCH --partition=multiple_il +#SBATCH --job-name=rc_1_1 +#SBATCH --time=0:30:00 +#SBATCH --mem=40000 +#SBATCH --cpus-per-task=1 +#SBATCH --mail-type=ALL +#SBATCH --mail-user=pa1164@partner.kit.edu + +cd $(ws_find propulate_bm_1) +ml purge +ml restore propulate +source /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/.venvs/async-parallel-pso/bin/activate +mpirun --bind-to core --map-by core -mca btl ^ofi python -u /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/pso_benchmark.py schwefel 1000 3 /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/results3/bm_3_schwefel_1 \ No newline at end of file diff --git a/ap_pso/bm/recovery/start_rc_3.sh b/ap_pso/bm/recovery/start_rc_3.sh new file mode 100644 index 00000000..b60eded9 --- /dev/null +++ b/ap_pso/bm/recovery/start_rc_3.sh @@ -0,0 +1,17 @@ +#!/bin/bash +#SBATCH --nodes=8 +#SBATCH --ntasks=512 +#SBATCH --partition=multiple_il +#SBATCH --job-name=rc_3_1 +#SBATCH --time=2:00:00 +#SBATCH --mem=40000 +#SBATCH --cpus-per-task=1 +#SBATCH --mail-type=ALL +#SBATCH --mail-user=pa1164@partner.kit.edu + +cd $(ws_find propulate_bm_1) +ml purge +ml restore propulate +source /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/.venvs/async-parallel-pso/bin/activate +mpirun --bind-to core --map-by core -mca btl ^ofi python -u /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/scripts/islands_example.py -f quartic -g 250 -ckpt /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/results3/bm_P_quartic_3 -i 1 -migp 0 -v 0 +mpirun --bind-to core --map-by core -mca btl ^ofi python -u /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/pso_benchmark.py sphere 250 2 /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/results3/bm_2_sphere_3 \ No newline at end of file From 50143bfbb7d5c09210ed8917385b065bd6d2fe2c Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 28 Aug 2023 22:11:58 +0200 Subject: [PATCH 071/139] Error correction. Wrong directory for venv given. --- ap_pso/bm/recovery/start_rc_1.sh | 2 +- ap_pso/bm/recovery/start_rc_3.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ap_pso/bm/recovery/start_rc_1.sh b/ap_pso/bm/recovery/start_rc_1.sh index dd30a24e..7542ffad 100644 --- a/ap_pso/bm/recovery/start_rc_1.sh +++ b/ap_pso/bm/recovery/start_rc_1.sh @@ -12,5 +12,5 @@ cd $(ws_find propulate_bm_1) ml purge ml restore propulate -source /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/.venvs/async-parallel-pso/bin/activate +source /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/.venvs/async-parallel-pso/bin/activate mpirun --bind-to core --map-by core -mca btl ^ofi python -u /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/pso_benchmark.py schwefel 1000 3 /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/results3/bm_3_schwefel_1 \ No newline at end of file diff --git a/ap_pso/bm/recovery/start_rc_3.sh b/ap_pso/bm/recovery/start_rc_3.sh index b60eded9..ebf49dfc 100644 --- a/ap_pso/bm/recovery/start_rc_3.sh +++ b/ap_pso/bm/recovery/start_rc_3.sh @@ -12,6 +12,6 @@ cd $(ws_find propulate_bm_1) ml purge ml restore propulate -source /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/.venvs/async-parallel-pso/bin/activate +source /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/.venvs/async-parallel-pso/bin/activate mpirun --bind-to core --map-by core -mca btl ^ofi python -u /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/scripts/islands_example.py -f quartic -g 250 -ckpt /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/results3/bm_P_quartic_3 -i 1 -migp 0 -v 0 mpirun --bind-to core --map-by core -mca btl ^ofi python -u /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/pso_benchmark.py sphere 250 2 /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/results3/bm_2_sphere_3 \ No newline at end of file From 8d7c5ca2bc4d6fd229db011afb412f84f1b929a9 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 28 Aug 2023 22:41:58 +0200 Subject: [PATCH 072/139] Optimised graph plotter to paint better images. --- ap_pso/bm/graph_plotter.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/ap_pso/bm/graph_plotter.py b/ap_pso/bm/graph_plotter.py index 53084f05..35027e0a 100644 --- a/ap_pso/bm/graph_plotter.py +++ b/ap_pso/bm/graph_plotter.py @@ -8,6 +8,7 @@ functions = ("Sphere", "Rosenbrock", "Step", "Quartic", "Rastrigin", "Griewank", "Schwefel", "BiSphere", "BiRastrigin") pso_names = ("VelocityClamping", "Constriction", "Basic", "Canonical") +other_stuff = ("Vanilla Propulate", "Hyppopy") time_path = Path("./slurm3/") path = Path("./results3/") @@ -42,17 +43,25 @@ def calc_time(iterator) -> float: """ This function takes an iterator on a certain string array and calculates out of this a time span in seconds. """ - start = int(next(iterator).strip("\n|: Ceirmnrtu")) - end = int(next(iterator).strip("\n|: Ceirmnrtu")) + try: + start = int(next(iterator).strip("\n|: Ceirmnrtu")) + except ValueError: + return np.nan + try: + end = int(next(iterator).strip("\n|: Ceirmnrtu")) + except ValueError: + return np.nan return (end - start) / 1e9 if __name__ == "__main__": raw_time_data: list[str] = [] - time_data: dict[str, dict[str, float]] = {} + time_data: dict[str, dict[str, list[float]]] = {} for function_name in functions: time_data[function_name] = {} + for program in other_stuff + pso_names: + time_data[function_name][program] = [] for file in time_path.iterdir(): with open(file) as f: @@ -61,16 +70,15 @@ def calc_time(iterator) -> float: for x in raw_time_data: scatter = [st for st in x.split("#-----------------------------------#") if "Current time" in st] itx = iter(scatter) - for program in ("Vanilla Propulate", "Hyppopy"): + for program in other_stuff: for function_name in functions: - time_data[function_name][program] = calc_time(itx) + time_data[function_name][program].append(calc_time(itx)) for function_name in functions: for program in pso_names: - time_data[function_name][program] = calc_time(itx) + time_data[function_name][program].append(calc_time(itx)) for function_name in functions: data = [] - pso_names = ("VelocityClamping", "Constriction", "Basic", "Canonical") marker_list = ("o", "s", "D", "^", "P", "X") # ["o", "v", "^", "<", ">", "s", "p", "P", "*", "h", "X", "D"] for i in range(5): @@ -109,14 +117,16 @@ def calc_time(iterator) -> float: ax_t = ax.twinx() ax_t.set_ylabel("Time [s]") + ax_t.set_yscale("log") - for i in range(4): - ax.plot(data[i][1], data[i][0], label=pso_names[i], marker=marker_list[i], ls="dashed", lw=2) - ax_t.plot(data[i][1], time_data[function_name][pso_names[i]], marker=marker_list[i], ls="dotted") - ax.plot(data[4][1], data[4][0], label="Vanilla Propulate", marker=marker_list[4], lw=1, ms=8) - ax_t.plot(data[4][1], time_data[function_name]["Vanilla Propulate"], marker=marker_list[4], ms=8, ls="dotted") - ax.plot(data[5][1], data[5][0], label="Hyppopy", marker=marker_list[5], lw=1, ms=8) - ax_t.plot(data[4][1], time_data[function_name]["Hyppopy"], marker=marker_list[5], ms=8, ls="dotted") + everything = pso_names + other_stuff + for i, name in enumerate(everything): + if i < 4: + ms = 6 + else: + ms = 7 + ax.plot(data[i][1], data[i][0], label=name, marker=marker_list[i], ls="dashed", lw=2, ms=ms) + ax_t.plot(data[i][1], time_data[function_name][name], marker=marker_list[i], ls="dotted", ms=ms) if function_name == "Rosenbrock": ax.set_yscale("symlog", linthresh=1e-36) From 1ef8dfb71e4648dd5985c56d0b19fd97c7b478dd Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 31 Aug 2023 08:10:19 +0200 Subject: [PATCH 073/139] Small helper message for better orientation if jobs fail. --- ap_pso/bm/pso_benchmark.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ap_pso/bm/pso_benchmark.py b/ap_pso/bm/pso_benchmark.py index 64240795..3c50cec0 100644 --- a/ap_pso/bm/pso_benchmark.py +++ b/ap_pso/bm/pso_benchmark.py @@ -2,6 +2,7 @@ import random import sys import time +from pathlib import Path from mpi4py import MPI @@ -43,6 +44,7 @@ print("#-----------------------------------#") print(f"| Current time: {time.time_ns()} |") print("#-----------------------------------#") + print(f"\nSaving files to: {Path(CHECKPOINT_PLACE).name}") islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path=CHECKPOINT_PLACE, migration_probability=0, pollination=False) From 5eb58a1a2e2dcce3d3198ea01613036784184dcc Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 31 Aug 2023 08:10:31 +0200 Subject: [PATCH 074/139] Benchmark should work now. --- ap_pso/bm/torch_benchmark.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ap_pso/bm/torch_benchmark.py b/ap_pso/bm/torch_benchmark.py index df21b247..b2a33069 100755 --- a/ap_pso/bm/torch_benchmark.py +++ b/ap_pso/bm/torch_benchmark.py @@ -21,9 +21,10 @@ from propulate import Islands from propulate.propagators import Conditional -num_generations = 3 +num_generations = 50 pop_size = 2 * MPI.COMM_WORLD.size -GPUS_PER_NODE = 4 +GPUS_PER_NODE = 1 # 4 +CHECKPOINT_PATH = "checkpoints" limits = { "convlayers": (2.0, 10.0), @@ -35,7 +36,7 @@ class Net(LightningModule): def __init__(self, convlayers: int, activation, lr: float, loss_fn): super(Net, self).__init__() - self.best_accuracy: float = 0.0 + self.best_loss: float = np.inf self.lr = lr self.loss_fn = loss_fn layers = [] @@ -79,8 +80,8 @@ def validation_step(self, batch, batch_idx): pred = self(x) loss = self.loss_fn(pred, y) val_acc = self.val_acc(torch.nn.functional.softmax(pred, dim=-1), y) - if val_acc > self.best_accuracy: - self.best_accuracy = val_acc + if loss < self.best_loss: + self.best_loss = loss return loss def configure_optimizers(self): @@ -105,6 +106,8 @@ def get_data_loaders(batch_size, dl=False): def ind_loss(params): convlayers = int(np.round(params["convlayers"])) + if convlayers < 0: + return float(-convlayers) activation = "leakyrelu" lr = params["lr"] epochs = 2 @@ -116,19 +119,21 @@ def ind_loss(params): model = Net(convlayers, activation, lr, loss_fn) if MPI.COMM_WORLD.rank == 0: train_loader, val_loader = get_data_loaders(8, True) - MPI.COMM_WORLD.barrier() - if MPI.COMM_WORLD.rank != 0: + MPI.COMM_WORLD.barrier() + else: + MPI.COMM_WORLD.barrier() train_loader, val_loader = get_data_loaders(8) trainer = Trainer(max_epochs=epochs, accelerator='gpu', devices=[ MPI.COMM_WORLD.Get_rank() % GPUS_PER_NODE ], + limit_train_batches=50, enable_progress_bar=False, ) trainer.fit(model, train_loader, val_loader) - return -model.best_accuracy + return model.best_loss if __name__ == "__main__": @@ -148,7 +153,8 @@ def ind_loss(params): print("#-----------------------------------#") propagator = Conditional(pop_size, pso, PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank)) - islands = Islands(ind_loss, propagator, rng, generations=num_generations, num_islands=1, migration_probability=0) + islands = Islands(ind_loss, propagator, rng, generations=num_generations, pollination=False, + migration_probability=0, checkpoint_path=CHECKPOINT_PATH) islands.evolve(top_n=1, debug=2) if MPI.COMM_WORLD.rank == 0: From f58413f7b563554ccb8d9908394dfeb7bc6663f2 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 31 Aug 2023 08:41:50 +0200 Subject: [PATCH 075/139] Adjusted torch_benchmark.py to new contents of torch_example.py --- ap_pso/bm/torch_benchmark.py | 251 ++++++++++++++++++++++++++--------- 1 file changed, 189 insertions(+), 62 deletions(-) diff --git a/ap_pso/bm/torch_benchmark.py b/ap_pso/bm/torch_benchmark.py index b2a33069..6b9b83ed 100755 --- a/ap_pso/bm/torch_benchmark.py +++ b/ap_pso/bm/torch_benchmark.py @@ -3,9 +3,11 @@ import random import sys import time +from typing import Tuple, Dict, Union import numpy as np import torch +from lightning.pytorch import loggers from torch import nn from torch.utils.data import DataLoader @@ -21,10 +23,10 @@ from propulate import Islands from propulate.propagators import Conditional -num_generations = 50 +num_generations = 10 pop_size = 2 * MPI.COMM_WORLD.size GPUS_PER_NODE = 1 # 4 -CHECKPOINT_PATH = "checkpoints" +log_path = "torch_ckpts" limits = { "convlayers": (2.0, 10.0), @@ -36,9 +38,9 @@ class Net(LightningModule): def __init__(self, convlayers: int, activation, lr: float, loss_fn): super(Net, self).__init__() - self.best_loss: float = np.inf self.lr = lr self.loss_fn = loss_fn + self.best_accuracy = 0.0 layers = [] layers += [ nn.Sequential(nn.Conv2d(1, @@ -60,80 +62,205 @@ def __init__(self, convlayers: int, activation, lr: float, loss_fn): self.conv_layers = nn.Sequential(*layers) self.val_acc = Accuracy('multiclass', num_classes=10) + self.train_acc = Accuracy("multiclass", num_classes=10) def forward(self, x): + """ + Forward pass. + + Parameters + ---------- + x: torch.Tensor + data sample + + Returns + ------- + torch.Tensor + The model's predictions for input data sample + """ b, c, w, h = x.size() x = self.conv_layers(x) x = x.view(b, 10 * 28 * 28) x = self.fc(x) - return x - def training_step(self, batch, batch_idx): + def training_step( + self, batch: Tuple[torch.Tensor, torch.Tensor], batch_idx: int + ) -> torch.Tensor: + """ + Calculate loss for training step in Lightning train loop. + + Parameters + ---------- + batch: Tuple[torch.Tensor, torch.Tensor] + input batch + batch_idx: int + batch index + + Returns + ------- + torch.Tensor + training loss for input batch + """ x, y = batch - loss = self.loss_fn(self(x), y) - - return loss - - def validation_step(self, batch, batch_idx): + pred = self(x) + loss_val = self.loss_fn(pred, y) + self.log("train loss", loss_val) + train_acc_val = self.train_acc(torch.nn.functional.softmax(pred, dim=-1), y) + self.log("train_ acc", train_acc_val) + return loss_val + + def validation_step( + self, batch: Tuple[torch.Tensor, torch.Tensor], batch_idx: int + ) -> torch.Tensor: + """ + Calculate loss for validation step in Lightning validation loop during training. + + Parameters + ---------- + batch: Tuple[torch.Tensor, torch.Tensor] + current batch + batch_idx: int + batch index + + Returns + ------- + torch.Tensor + validation loss for input batch + """ x, y = batch pred = self(x) - loss = self.loss_fn(pred, y) - val_acc = self.val_acc(torch.nn.functional.softmax(pred, dim=-1), y) - if loss < self.best_loss: - self.best_loss = loss - return loss - - def configure_optimizers(self): - optimizer = torch.optim.SGD(self.parameters(), lr=self.lr) - return optimizer - - -def get_data_loaders(batch_size, dl=False): + loss_val = self.loss_fn(pred, y) + val_acc_val = self.val_acc(torch.nn.functional.softmax(pred, dim=-1), y) + self.log("val_loss", loss_val) + self.log("val_acc", val_acc_val) + return loss_val + + def configure_optimizers(self) -> torch.optim.SGD: + """ + Configure optimizer. + + Returns + ------- + torch.optim.sgd.SGD + stochastic gradient descent optimizer + """ + return torch.optim.SGD(self.parameters(), lr=self.lr) + + def on_validation_epoch_end(self): + """ + Calculate and store the model's validation accuracy after each epoch. + """ + val_acc_val = self.val_acc.compute() + self.log("val_acc_val", val_acc_val) + self.val_acc.reset() + if val_acc_val > self.best_accuracy: + self.best_accuracy = val_acc_val + +def get_data_loaders(batch_size: int) -> Tuple[DataLoader, DataLoader]: + """ + Get MNIST train and validation dataloaders. + + Parameters + ---------- + batch_size: int + batch size + + Returns + ------- + DataLoader + training dataloader + DataLoader + validation dataloader + """ data_transform = Compose([ToTensor(), Normalize((0.1307,), (0.3081,))]) - train_loader = DataLoader( - MNIST(download=dl, root=".", transform=data_transform, train=True), - batch_size=batch_size, - shuffle=True, - ) + + if MPI.COMM_WORLD.Get_rank() == 0: # Only root downloads data. + train_loader = DataLoader( + dataset=MNIST( + download=True, root=".", transform=data_transform, + ), # Use MNIST training dataset. + batch_size=batch_size, # Batch size + shuffle=True, # Shuffle data. + ) + MPI.COMM_WORLD.Barrier() + else: + MPI.COMM_WORLD.Barrier() + train_loader = DataLoader( + dataset=MNIST( + root=".", transform=data_transform + ), # Use MNIST training dataset. + batch_size=batch_size, # Batch size + shuffle=True, # Shuffle data. + ) val_loader = DataLoader( - MNIST(download=dl, root=".", transform=data_transform, train=False), - batch_size=1, - shuffle=False, + dataset=MNIST( + root=".", transform=data_transform, train=False + ), # Use MNIST testing dataset. + shuffle=False, # Do not shuffle data. ) return train_loader, val_loader - -def ind_loss(params): - convlayers = int(np.round(params["convlayers"])) - if convlayers < 0: - return float(-convlayers) - activation = "leakyrelu" - lr = params["lr"] - epochs = 2 - - activations = {"relu": nn.ReLU, "sigmoid": nn.Sigmoid, "tanh": nn.Tanh, "leakyrelu": nn.LeakyReLU} - activation = activations[activation] - loss_fn = torch.nn.CrossEntropyLoss() - - model = Net(convlayers, activation, lr, loss_fn) - if MPI.COMM_WORLD.rank == 0: - train_loader, val_loader = get_data_loaders(8, True) - MPI.COMM_WORLD.barrier() - else: - MPI.COMM_WORLD.barrier() - train_loader, val_loader = get_data_loaders(8) - trainer = Trainer(max_epochs=epochs, - accelerator='gpu', - devices=[ - MPI.COMM_WORLD.Get_rank() % GPUS_PER_NODE - ], - limit_train_batches=50, - enable_progress_bar=False, - ) - trainer.fit(model, train_loader, val_loader) - - return model.best_loss +def ind_loss(params: Dict[str, Union[int, float, str]]) -> float: + """ + Loss function for evolutionary optimization with Propulate. Minimize the model's negative validation accuracy. + + Parameters + ---------- + params: dict[str, int | float | str]] + + Returns + ------- + float + The trained model's negative validation accuracy + """ + # Extract hyperparameter combination to test from input dictionary. + conv_layers = int(np.round(params["conv_layers"])) # Number of convolutional layers + if conv_layers < 1: + return float(10 - 10 * conv_layers) + activation = params["activation"] # Activation function + lr = params["lr"] # Learning rate + + epochs = 2 # Number of epochs to train + + activations = { + "relu": nn.ReLU, + "sigmoid": nn.Sigmoid, + "tanh": nn.Tanh, + } # Define activation function mapping. + activation = activations[activation] # Get activation function. + loss_fn = ( + torch.nn.CrossEntropyLoss() + ) # Use cross-entropy loss for multi-class classification. + + model = Net( + conv_layers, activation, lr, loss_fn + ) # Set up neural network with specified hyperparameters. + model.best_accuracy = 0.0 # Initialize the model's best validation accuracy. + + train_loader, val_loader = get_data_loaders( + batch_size=8 + ) # Get training and validation data loaders. + + tb_logger = loggers.TensorBoardLogger( + save_dir=log_path + "/lightning_logs" + ) # Get tensor board logger. + + # Under the hood, the Lightning Trainer handles the training loop details. + trainer = Trainer( + max_epochs=epochs, # Stop training once this number of epochs is reached. + accelerator="gpu", # Pass accelerator type. + devices=[MPI.COMM_WORLD.Get_rank() % GPUS_PER_NODE], # Devices to train on + enable_progress_bar=True, # Disable progress bar. + logger=tb_logger, # Logger + ) + trainer.fit( # Run full model training optimization routine. + model=model, # Model to train + train_dataloaders=train_loader, # Dataloader for training samples + val_dataloaders=val_loader, # Dataloader for validation samples + ) + # Return negative best validation accuracy as an individual's loss. + return -model.best_accuracy if __name__ == "__main__": @@ -154,7 +281,7 @@ def ind_loss(params): propagator = Conditional(pop_size, pso, PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank)) islands = Islands(ind_loss, propagator, rng, generations=num_generations, pollination=False, - migration_probability=0, checkpoint_path=CHECKPOINT_PATH) + migration_probability=0, checkpoint_path=log_path) islands.evolve(top_n=1, debug=2) if MPI.COMM_WORLD.rank == 0: From 28c368172ffbfa7bfb375e3e48e2d0f5ff15d853 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 31 Aug 2023 08:45:10 +0200 Subject: [PATCH 076/139] Again, some bugfixing. --- ap_pso/bm/torch_benchmark.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ap_pso/bm/torch_benchmark.py b/ap_pso/bm/torch_benchmark.py index 6b9b83ed..f9f0ae0b 100755 --- a/ap_pso/bm/torch_benchmark.py +++ b/ap_pso/bm/torch_benchmark.py @@ -29,7 +29,7 @@ log_path = "torch_ckpts" limits = { - "convlayers": (2.0, 10.0), + "conv_layers": (2.0, 10.0), "lr": (0.01, 0.0001), } @@ -218,7 +218,7 @@ def ind_loss(params: Dict[str, Union[int, float, str]]) -> float: conv_layers = int(np.round(params["conv_layers"])) # Number of convolutional layers if conv_layers < 1: return float(10 - 10 * conv_layers) - activation = params["activation"] # Activation function + # activation = params["activation"] # Activation function lr = params["lr"] # Learning rate epochs = 2 # Number of epochs to train @@ -227,8 +227,9 @@ def ind_loss(params: Dict[str, Union[int, float, str]]) -> float: "relu": nn.ReLU, "sigmoid": nn.Sigmoid, "tanh": nn.Tanh, + "leaky_relu": nn.LeakyReLU } # Define activation function mapping. - activation = activations[activation] # Get activation function. + activation = activations["leaky_relu"] # Get activation function. loss_fn = ( torch.nn.CrossEntropyLoss() ) # Use cross-entropy loss for multi-class classification. From 618ff2341618fae8fe02e2896b6ff15b81db2cac Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 10:11:19 +0200 Subject: [PATCH 077/139] Scaled down the weak scaling benchmark experiment. The original version could not be successfully executed due to run time and memory problems. --- ap_pso/bm/bm_all_weak.sh | 51 +++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/ap_pso/bm/bm_all_weak.sh b/ap_pso/bm/bm_all_weak.sh index 9dc9026c..719485c5 100644 --- a/ap_pso/bm/bm_all_weak.sh +++ b/ap_pso/bm/bm_all_weak.sh @@ -1,15 +1,25 @@ #!/bin/bash BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" -for RACE in {1..4} +mkdir "${BASE_DIR}/ap_pso/bm/result5" +for RACE in {0..4} do NODES=$(( 2 ** RACE )) TASKS=$(( 64 * NODES )) - ITERATIONS=2000 + if [[ $RACE -eq 4 ]] + then + NODES=2 + TASKS=32 + fi + if [[ $RACE -eq 0 ]] + then + NODES=2 + fi + ITERATIONS=512 SCRIPT="#!/bin/bash #SBATCH --nodes=${NODES} #SBATCH --ntasks=${TASKS} #SBATCH --partition=multiple_il -#SBATCH --job-name=\"all_${RACE}_weak\" +#SBATCH --job-name=\"all_${RACE}_weak_v2\" #SBATCH --time=8:00:00 #SBATCH --mem=249600mb #SBATCH --cpus-per-task=1 @@ -21,39 +31,48 @@ ml purge ml restore propulate source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate " + SCRIPT_T=$SCRIPT for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" do DIRNAME="bm_P_${FUNCTION}_${RACE}" - RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results4/${DIRNAME}" + RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results5/${DIRNAME}" mkdir "$RESULTS_DIR" - SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/scripts/islands_example.py -f ${FUNCTION} -g ${ITERATIONS} -ckpt ${RESULTS_DIR} -i 1 -migp 0 -v 0 + SCRIPT_T+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/scripts/islands_example.py -f ${FUNCTION} -g ${ITERATIONS} -ckpt ${RESULTS_DIR} -i 1 -migp 0 -v 0 " done + FILE="${BASE_DIR}/ap_pso/bm/start_bm_AW_${RACE}_P.sh" + echo "${SCRIPT_T}" > "${FILE}" + sbatch -p "multiple_il" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 "${FILE}" + + SCRIPT_T=$SCRIPT for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" do DIRNAME="bm_H_${FUNCTION}_${RACE}" - RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results4/${DIRNAME}" + RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results5/${DIRNAME}" mkdir "$RESULTS_DIR" - SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/hyppopy_benchmark.py ${FUNCTION} ${ITERATIONS} ${RESULTS_DIR} + SCRIPT_T+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/hyppopy_benchmark.py ${FUNCTION} ${ITERATIONS} ${RESULTS_DIR} " done - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" + FILE="${BASE_DIR}/ap_pso/bm/start_bm_AW_${RACE}_H.sh" + echo "${SCRIPT_T}" > "${FILE}" + sbatch -p "multiple_il" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 "${FILE}" + + for PSO in {0..3} do - for PSO in {0..3} + SCRIPT_T=$SCRIPT + for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" do DIRNAME="bm_${PSO}_${FUNCTION}_${RACE}" - RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results4/${DIRNAME}" + RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results5/${DIRNAME}" mkdir "$RESULTS_DIR" - SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/pso_benchmark.py ${FUNCTION} ${ITERATIONS} ${PSO} ${RESULTS_DIR} + SCRIPT_T+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/pso_benchmark.py ${FUNCTION} ${ITERATIONS} ${PSO} ${RESULTS_DIR} " done + FILE="${BASE_DIR}/ap_pso/bm/start_bm_AW_${RACE}_${PSO}.sh" + echo "${SCRIPT_T}" > "${FILE}" + sbatch -p "multiple_il" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 "${FILE}" done - SCRIPT+="deactivate -" - FILE="${BASE_DIR}/ap_pso/bm/start_bm_AW_${RACE}.sh" - echo "${SCRIPT}" > "${FILE}" - sbatch -p "multiple_il" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 "${FILE}" done From 6041fa3e422ae5261de042267ef7cfcdee608c9d Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 13:59:39 +0200 Subject: [PATCH 078/139] Some error corrections --- ap_pso/bm/bm_all_weak.sh | 2 +- ap_pso/bm/torch_benchmark.py | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ap_pso/bm/bm_all_weak.sh b/ap_pso/bm/bm_all_weak.sh index 719485c5..27808015 100644 --- a/ap_pso/bm/bm_all_weak.sh +++ b/ap_pso/bm/bm_all_weak.sh @@ -1,6 +1,6 @@ #!/bin/bash BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" -mkdir "${BASE_DIR}/ap_pso/bm/result5" +mkdir "${BASE_DIR}/ap_pso/bm/results5" for RACE in {0..4} do NODES=$(( 2 ** RACE )) diff --git a/ap_pso/bm/torch_benchmark.py b/ap_pso/bm/torch_benchmark.py index f9f0ae0b..62d3a4da 100755 --- a/ap_pso/bm/torch_benchmark.py +++ b/ap_pso/bm/torch_benchmark.py @@ -31,6 +31,7 @@ limits = { "conv_layers": (2.0, 10.0), "lr": (0.01, 0.0001), + "epochs": (2.0, 400.0) } @@ -151,7 +152,7 @@ def on_validation_epoch_end(self): """ Calculate and store the model's validation accuracy after each epoch. """ - val_acc_val = self.val_acc.compute() + val_acc_val = self.val_acc.compute().item() self.log("val_acc_val", val_acc_val) self.val_acc.reset() if val_acc_val > self.best_accuracy: @@ -183,9 +184,9 @@ def get_data_loaders(batch_size: int) -> Tuple[DataLoader, DataLoader]: batch_size=batch_size, # Batch size shuffle=True, # Shuffle data. ) - MPI.COMM_WORLD.Barrier() + MPI.COMM_WORLD.barrier() else: - MPI.COMM_WORLD.Barrier() + MPI.COMM_WORLD.barrier() train_loader = DataLoader( dataset=MNIST( root=".", transform=data_transform @@ -214,14 +215,18 @@ def ind_loss(params: Dict[str, Union[int, float, str]]) -> float: float The trained model's negative validation accuracy """ + loss = 0.0 # Extract hyperparameter combination to test from input dictionary. conv_layers = int(np.round(params["conv_layers"])) # Number of convolutional layers - if conv_layers < 1: - return float(10 - 10 * conv_layers) + if conv_layers < 2: + loss += 10 - 5 * conv_layers + conv_layers = 2 # activation = params["activation"] # Activation function lr = params["lr"] # Learning rate - - epochs = 2 # Number of epochs to train + epochs = params["epochs"] + if epochs < 2: + loss += 10 - 5 * epochs + epochs = 2 # Number of epochs to train activations = { "relu": nn.ReLU, From e2804513fcfc05fd088911d20453b588b301ec02 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 14:04:49 +0200 Subject: [PATCH 079/139] Moved and finalised pso example script. --- {ap_pso/scripts => tutorials}/pso_example.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) rename {ap_pso/scripts => tutorials}/pso_example.py (76%) diff --git a/ap_pso/scripts/pso_example.py b/tutorials/pso_example.py similarity index 76% rename from ap_pso/scripts/pso_example.py rename to tutorials/pso_example.py index 991201ef..538ad864 100644 --- a/ap_pso/scripts/pso_example.py +++ b/tutorials/pso_example.py @@ -7,14 +7,14 @@ from ap_pso.propagators import PSOInitUniform, VelocityClampingPropagator, ConstrictionPropagator, PSOCompose, \ BasicPSOPropagator, StatelessPSOPropagator, CanonicalPropagator from propulate import Islands -from propulate.propagators import Conditional, InitUniform -from scripts.function_benchmark import get_function_search_space +from propulate.propagators import Conditional +from function_benchmark import get_function_search_space ############ # SETTINGS # ############ -function_name = sys.argv[1] # Get function to optimize from command-line. +function_name = sys.argv[1] # Get function to optimize from command-line. Possible Options: See function_benchmark.py NUM_GENERATIONS: int = int(sys.argv[2]) # Set number of generations. POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. @@ -26,22 +26,19 @@ rng = random.Random(MPI.COMM_WORLD.rank) - propagator = PSOCompose( + pso_propagator = PSOCompose( [ # VelocityClampingPropagator(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6) - # VelocityClampingPropagator(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng, 0.6) # ConstrictionPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) # BasicPSOPropagator(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng) CanonicalPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) - # StatelessPSOPropagator(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng) # Attention! Does not work - # with current chart drawing script! + # StatelessPSOPropagator(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng) ] ) init = PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) - propagator = Conditional(POP_SIZE, propagator, init) + propagator = Conditional(POP_SIZE, pso_propagator, init) islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path='./checkpoints/', migration_probability=0, pollination=False) islands.evolve(debug=0) - # islands.propulator.paint_graphs(function_name) From 6e16d5b64680abe5f68e5c6463b46a72a941f05c Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 14:06:51 +0200 Subject: [PATCH 080/139] Moved and updated the particle and a transformator function --- ap_pso/__init__.py | 2 +- ap_pso/utils.py | 14 -------- {ap_pso => propulate}/particle.py | 0 propulate/utils.py | 57 +++++++++++++++++++++---------- 4 files changed, 40 insertions(+), 33 deletions(-) rename {ap_pso => propulate}/particle.py (100%) diff --git a/ap_pso/__init__.py b/ap_pso/__init__.py index 358d00ff..e1421198 100644 --- a/ap_pso/__init__.py +++ b/ap_pso/__init__.py @@ -4,5 +4,5 @@ """ __all__ = ["Particle", "propagators", "make_particle"] -from ap_pso.particle import Particle +from propulate.particle import Particle from ap_pso.utils import make_particle diff --git a/ap_pso/utils.py b/ap_pso/utils.py index bc3c284c..6c4a4871 100644 --- a/ap_pso/utils.py +++ b/ap_pso/utils.py @@ -7,18 +7,4 @@ from propulate.population import Individual -def make_particle(individual: Individual) -> Particle: - """ - Makes particles out of individuals. - Parameters - ---------- - individual : An Individual that needs to be a particle - """ - p = Particle(iteration=individual.generation) - p.position = np.zeros(len(individual)) - p.velocity = np.zeros(len(individual)) - for i, k in enumerate(individual): - p.position[i] = individual[k] - p[k] = individual[k] - return p diff --git a/ap_pso/particle.py b/propulate/particle.py similarity index 100% rename from ap_pso/particle.py rename to propulate/particle.py diff --git a/propulate/utils.py b/propulate/utils.py index 9687cdb4..9c440f41 100644 --- a/propulate/utils.py +++ b/propulate/utils.py @@ -5,9 +5,13 @@ import colorlog import random import sys + +import numpy as np from mpi4py import MPI from typing import Dict, Union, Tuple +from .particle import Particle +from .population import Individual from .propagators import ( Compose, Conditional, @@ -22,17 +26,17 @@ def get_default_propagator( - pop_size: int, - limits: Union[ - Dict[str, Tuple[float, float]], - Dict[str, Tuple[int, int]], - Dict[str, Tuple[str, ...]], - ], - mate_prob: float, - mut_prob: float, - random_prob: float, - sigma_factor: float = 0.05, - rng: random.Random = None, + pop_size: int, + limits: Union[ + Dict[str, Tuple[float, float]], + Dict[str, Tuple[int, int]], + Dict[str, Tuple[str, ...]], + ], + mate_prob: float, + mut_prob: float, + random_prob: float, + sigma_factor: float = 0.05, + rng: random.Random = None, ) -> Propagator: """ Get Propulate's default evolutionary optimization propagator. @@ -60,7 +64,7 @@ def get_default_propagator( A basic evolutionary optimization propagator. """ if any( - isinstance(limits[x][0], float) for x in limits + isinstance(limits[x][0], float) for x in limits ): # Check for existence of at least one continuous trait. propagator = Compose( [ # Compose propagator out of basic evolutionary operators with Compose(...). @@ -94,11 +98,11 @@ def get_default_propagator( def set_logger_config( - level: int = logging.INFO, - log_file: Union[str, Path] = None, - log_to_stdout: bool = True, - log_rank: bool = False, - colors: bool = True, + level: int = logging.INFO, + log_file: Union[str, Path] = None, + log_to_stdout: bool = True, + log_rank: bool = False, + colors: bool = True, ) -> None: """ Set up the logger. Should only need to be done once. @@ -130,7 +134,7 @@ def set_logger_config( if colors: formatter = colorlog.ColoredFormatter( fmt=f"{rank}[%(cyan)s%(asctime)s%(reset)s][%(blue)s%(name)s%(reset)s]" - f"[%(log_color)s%(levelname)s%(reset)s] - %(message)s", + f"[%(log_color)s%(levelname)s%(reset)s] - %(message)s", datefmt=None, reset=True, log_colors={ @@ -156,3 +160,20 @@ def set_logger_config( base_logger.addHandler(file_handler) base_logger.setLevel(level) return + + +def make_particle(individual: Individual) -> Particle: + """ + Makes particles out of individuals. + + Parameters + ---------- + individual : An Individual that needs to be a particle + """ + p = Particle(iteration=individual.generation) + p.position = np.zeros(len(individual)) + p.velocity = np.zeros(len(individual)) + for i, k in enumerate(individual): + p.position[i] = individual[k] + p[k] = individual[k] + return p From 122d1e093188f1511e67e3adde5fccbeb1ec4236 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 14:26:30 +0200 Subject: [PATCH 081/139] Merge into Propulate completed. --- ap_pso/__init__.py | 8 - ap_pso/bm/bm_all.sh | 68 ---- ap_pso/bm/bm_all_weak.sh | 78 ----- ap_pso/bm/bm_hyppopy.sh | 54 ---- ap_pso/bm/bm_init.sh | 70 ----- ap_pso/bm/bm_propulate.sh | 54 ---- ap_pso/bm/bm_starter.sh | 43 --- ap_pso/bm/graph_plotter.py | 159 ---------- ap_pso/bm/hyppopy_benchmark.py | 69 ---- ap_pso/bm/pso_benchmark.py | 58 ---- ap_pso/bm/recovery/start_rc_1.sh | 16 - ap_pso/bm/recovery/start_rc_3.sh | 17 - ap_pso/bm/torch_benchmark.py | 296 ------------------ ap_pso/propagators/__init__.py | 13 - ap_pso/scripts/pso_example.sh | 72 ----- ap_pso/utils.py | 10 - propulate/propagators/pso/__init__.py | 0 .../propagators/pso}/basic_pso.py | 0 .../propagators/pso}/canonical.py | 0 .../propagators/pso}/constriction.py | 0 .../propagators/pso}/pso_compose.py | 0 .../propagators/pso}/pso_init_uniform.py | 0 .../propagators/pso}/stateless_pso.py | 0 .../propagators/pso}/velocity_clamping.py | 0 24 files changed, 1085 deletions(-) delete mode 100644 ap_pso/__init__.py delete mode 100644 ap_pso/bm/bm_all.sh delete mode 100644 ap_pso/bm/bm_all_weak.sh delete mode 100644 ap_pso/bm/bm_hyppopy.sh delete mode 100644 ap_pso/bm/bm_init.sh delete mode 100644 ap_pso/bm/bm_propulate.sh delete mode 100644 ap_pso/bm/bm_starter.sh delete mode 100644 ap_pso/bm/graph_plotter.py delete mode 100644 ap_pso/bm/hyppopy_benchmark.py delete mode 100644 ap_pso/bm/pso_benchmark.py delete mode 100644 ap_pso/bm/recovery/start_rc_1.sh delete mode 100644 ap_pso/bm/recovery/start_rc_3.sh delete mode 100755 ap_pso/bm/torch_benchmark.py delete mode 100644 ap_pso/propagators/__init__.py delete mode 100644 ap_pso/scripts/pso_example.sh delete mode 100644 ap_pso/utils.py create mode 100644 propulate/propagators/pso/__init__.py rename {ap_pso/propagators => propulate/propagators/pso}/basic_pso.py (100%) rename {ap_pso/propagators => propulate/propagators/pso}/canonical.py (100%) rename {ap_pso/propagators => propulate/propagators/pso}/constriction.py (100%) rename {ap_pso/propagators => propulate/propagators/pso}/pso_compose.py (100%) rename {ap_pso/propagators => propulate/propagators/pso}/pso_init_uniform.py (100%) rename {ap_pso/propagators => propulate/propagators/pso}/stateless_pso.py (100%) rename {ap_pso/propagators => propulate/propagators/pso}/velocity_clamping.py (100%) diff --git a/ap_pso/__init__.py b/ap_pso/__init__.py deleted file mode 100644 index e1421198..00000000 --- a/ap_pso/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -This package contains - except for the example and the init propagator everything I added to propulate to be able to -run PSO on it. -""" -__all__ = ["Particle", "propagators", "make_particle"] - -from propulate.particle import Particle -from ap_pso.utils import make_particle diff --git a/ap_pso/bm/bm_all.sh b/ap_pso/bm/bm_all.sh deleted file mode 100644 index 8213978a..00000000 --- a/ap_pso/bm/bm_all.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash -BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" -for RACE in {0..4} -do - NODES=$(( 2 ** RACE )) - TASKS=$(( 64 * NODES )) - ITERATIONS=$(( 2000 / NODES )) - QUEUE="multiple_il" - if [[ $RACE -eq 0 ]] - then - NODES=2 - fi - if [[ $RACE -eq 4 ]] - then - QUEUE="multiple_il" - fi - SCRIPT="#!/bin/bash -#SBATCH --nodes=${NODES} -#SBATCH --ntasks=${TASKS} -#SBATCH --partition=${QUEUE} -#SBATCH --job-name=\"all_${RACE}\" -#SBATCH --time=4:00:00 -#SBATCH --mem=40000 -#SBATCH --cpus-per-task=1 -#SBATCH --mail-type=ALL -#SBATCH --mail-user=pa1164@partner.kit.edu - -cd \$(ws_find propulate_bm_1) -ml purge -ml restore propulate -source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate -" - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" - do - DIRNAME="bm_P_${FUNCTION}_${RACE}" - RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results2/${DIRNAME}" - mkdir "$RESULTS_DIR" - - SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/scripts/islands_example.py -f ${FUNCTION} -g ${ITERATIONS} -ckpt ${RESULTS_DIR} -i 1 -migp 0 -" - done - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" - do - DIRNAME="bm_H_${FUNCTION}_${RACE}" - RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results2/${DIRNAME}" - mkdir "$RESULTS_DIR" - - SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/hyppopy_benchmark.py ${FUNCTION} ${ITERATIONS} ${RESULTS_DIR} -" - done - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" - do - for PSO in {0..3} - do - DIRNAME="bm_${PSO}_${FUNCTION}_${RACE}" - RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results2/${DIRNAME}" - mkdir "$RESULTS_DIR" - - SCRIPT+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/pso_benchmark.py ${FUNCTION} ${ITERATIONS} ${PSO} ${RESULTS_DIR} -" - done - done - SCRIPT+="deactivate -" - FILE="${BASE_DIR}/ap_pso/bm/start_bm_A_${RACE}.sh" - echo "${SCRIPT}" > "${FILE}" - sbatch -p "${QUEUE}" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 "${FILE}" -done diff --git a/ap_pso/bm/bm_all_weak.sh b/ap_pso/bm/bm_all_weak.sh deleted file mode 100644 index 27808015..00000000 --- a/ap_pso/bm/bm_all_weak.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash -BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" -mkdir "${BASE_DIR}/ap_pso/bm/results5" -for RACE in {0..4} -do - NODES=$(( 2 ** RACE )) - TASKS=$(( 64 * NODES )) - if [[ $RACE -eq 4 ]] - then - NODES=2 - TASKS=32 - fi - if [[ $RACE -eq 0 ]] - then - NODES=2 - fi - ITERATIONS=512 - SCRIPT="#!/bin/bash -#SBATCH --nodes=${NODES} -#SBATCH --ntasks=${TASKS} -#SBATCH --partition=multiple_il -#SBATCH --job-name=\"all_${RACE}_weak_v2\" -#SBATCH --time=8:00:00 -#SBATCH --mem=249600mb -#SBATCH --cpus-per-task=1 -#SBATCH --mail-type=ALL -#SBATCH --mail-user=pa1164@partner.kit.edu - -cd \$(ws_find propulate_bm_1) -ml purge -ml restore propulate -source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate -" - SCRIPT_T=$SCRIPT - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" - do - DIRNAME="bm_P_${FUNCTION}_${RACE}" - RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results5/${DIRNAME}" - mkdir "$RESULTS_DIR" - - SCRIPT_T+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/scripts/islands_example.py -f ${FUNCTION} -g ${ITERATIONS} -ckpt ${RESULTS_DIR} -i 1 -migp 0 -v 0 -" - done - FILE="${BASE_DIR}/ap_pso/bm/start_bm_AW_${RACE}_P.sh" - echo "${SCRIPT_T}" > "${FILE}" - sbatch -p "multiple_il" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 "${FILE}" - - SCRIPT_T=$SCRIPT - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" - do - DIRNAME="bm_H_${FUNCTION}_${RACE}" - RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results5/${DIRNAME}" - mkdir "$RESULTS_DIR" - - SCRIPT_T+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/hyppopy_benchmark.py ${FUNCTION} ${ITERATIONS} ${RESULTS_DIR} -" - done - FILE="${BASE_DIR}/ap_pso/bm/start_bm_AW_${RACE}_H.sh" - echo "${SCRIPT_T}" > "${FILE}" - sbatch -p "multiple_il" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 "${FILE}" - - for PSO in {0..3} - do - SCRIPT_T=$SCRIPT - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" - do - DIRNAME="bm_${PSO}_${FUNCTION}_${RACE}" - RESULTS_DIR="${BASE_DIR}/ap_pso/bm/results5/${DIRNAME}" - mkdir "$RESULTS_DIR" - - SCRIPT_T+="mpirun --bind-to core --map-by core python -u ${BASE_DIR}/ap_pso/bm/pso_benchmark.py ${FUNCTION} ${ITERATIONS} ${PSO} ${RESULTS_DIR} -" - done - FILE="${BASE_DIR}/ap_pso/bm/start_bm_AW_${RACE}_${PSO}.sh" - echo "${SCRIPT_T}" > "${FILE}" - sbatch -p "multiple_il" -N "${NODES}" -n "${TASKS}" --cpus-per-task 1 "${FILE}" - done -done diff --git a/ap_pso/bm/bm_hyppopy.sh b/ap_pso/bm/bm_hyppopy.sh deleted file mode 100644 index b8a083dc..00000000 --- a/ap_pso/bm/bm_hyppopy.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" -for RACE in {0..4} -do - NODES=$(( 2 ** RACE )) - ITERATIONS=$(( 2000 / NODES )) - QUEUE="single" - if [[ $NODES -gt 1 ]] - then - QUEUE="multiple" - fi -# case "$RACE" in -# 5) -# ITERATIONS=$((ITERATIONS * 10)) -# ;; -# 6) -# ITERATIONS=-1 -# ;; -# *) -# echo "Error: Race $RACE was called." -# exit -# ;; -# esac - SCRIPT="#!/bin/bash -#SBATCH --nodes=${NODES} -#SBATCH --partition=${QUEUE} -#SBATCH --job-name=\"hyppopy_${RACE}\" -#SBATCH --time=15:00 -#SBATCH --mem=10000 -#SBATCH --cpus-per-task=40 -#SBATCH --ntasks-per-node=1 -#SBATCH --mail-type=ALL -#SBATCH --mail-user=pa1164@partner.kit.edu - -cd \$(ws_find propulate_bm_1) -ml purge -ml restore propulate -source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate -" - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" - do - DIRNAME="bm_H_${FUNCTION}_${RACE}" - EXECUTION_DIR="${BASE_DIR}/ap_pso/bm/${DIRNAME}" - mkdir "$EXECUTION_DIR" - - SCRIPT+="mpirun python -u ${BASE_DIR}/ap_pso/bm/hyppopy_benchmark.py ${FUNCTION} ${ITERATIONS} ${EXECUTION_DIR} -" - done - SCRIPT+="deactivate -" - FILE="${BASE_DIR}/ap_pso/bm/start_bm_H_${RACE}.sh" - echo "${SCRIPT}" > "${FILE}" - sbatch -p "${QUEUE}" -N "${NODES}" "${FILE}" -done diff --git a/ap_pso/bm/bm_init.sh b/ap_pso/bm/bm_init.sh deleted file mode 100644 index 434324fa..00000000 --- a/ap_pso/bm/bm_init.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash -BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/" -for I in {0..3} -do - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" - do - for RACE in {0..4} - do - EXECUTION_DIR="${BASE_DIR}/ap_pso/bm/bm_${I}_${FUNCTION}_${RACE}" - mkdir "$EXECUTION_DIR" - mkdir "${EXECUTION_DIR}/images" - NODES=40 - ITERATIONS=2000 - QUEUE="single" - case "$RACE" in - 0) - ;; - 1) - NODES=$((NODES * 2)) - ITERATIONS=$((ITERATIONS / 2)) - QUEUE="multiple" - ;; - 2) - NODES=$((NODES * 4)) - ITERATIONS=$((ITERATIONS / 4)) - QUEUE="multiple" - ;; - 3) - NODES=$((NODES * 8)) - ITERATIONS=$((ITERATIONS / 8)) - QUEUE="multiple" - ;; - 4) - NODES=$((NODES * 16)) - ITERATIONS=$((ITERATIONS / 16)) - QUEUE="multiple" - ;; - 5) - ITERATIONS=$((ITERATIONS * 10)) - ;; - 6) - ITERATIONS=-1 - ;; - *) - echo "Error: Race $RACE was called." - ;; - esac - SCRIPT="#!/bin/bash -#SBATCH --nodes=${NODES} -#SBATCH --partition=${QUEUE} -#SBATCH --job-name=${FUNCTION}_${I}_${RACE} -#SBATCH --time=15:00 -#SBATCH --mem=10000 -#SBATCH --cpus-per-task=40 -#SBATCH --ntasks-per-node=1 -#SBATCH --mail-type=ALL -#SBATCH --mail-user=pa1164@partner.kit.edu - -cd \$(ws_find propulate_bm_1) -ml purge -ml restore propulate -source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate - -mpirun python -u ${BASE_DIR}/ap_pso/bm/pso_benchmark.py ${FUNCTION} ${ITERATIONS} ${I} ${EXECUTION_DIR} -deactivate -" - echo "${SCRIPT}" > "${EXECUTION_DIR}/bm_start.sh" - done - done -done diff --git a/ap_pso/bm/bm_propulate.sh b/ap_pso/bm/bm_propulate.sh deleted file mode 100644 index a26c07d4..00000000 --- a/ap_pso/bm/bm_propulate.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso" -for RACE in {0..4} -do - NODES=$(( 2 ** RACE )) - ITERATIONS=$(( 2000 / NODES )) - QUEUE="single" - if [[ $NODES -gt 1 ]] - then - QUEUE="multiple" - fi -# case "$RACE" in -# 5) -# ITERATIONS=$((ITERATIONS * 10)) -# ;; -# 6) -# ITERATIONS=-1 -# ;; -# *) -# echo "Error: Race $RACE was called." -# exit -# ;; -# esac - SCRIPT="#!/bin/bash -#SBATCH --nodes=${NODES} -#SBATCH --partition=${QUEUE} -#SBATCH --job-name=\"propulate_${RACE}\" -#SBATCH --time=15:00 -#SBATCH --mem=10000 -#SBATCH --cpus-per-task=40 -#SBATCH --ntasks-per-node=1 -#SBATCH --mail-type=ALL -#SBATCH --mail-user=pa1164@partner.kit.edu - -cd \$(ws_find propulate_bm_1) -ml purge -ml restore propulate -source ${BASE_DIR}/../.venvs/async-parallel-pso/bin/activate -" - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" - do - DIRNAME="bm_P_${FUNCTION}_${RACE}" - EXECUTION_DIR="${BASE_DIR}/ap_pso/bm/results/${DIRNAME}" - mkdir "$EXECUTION_DIR" - - SCRIPT+="mpirun python -u ${BASE_DIR}/scripts/islands_example.py -f ${FUNCTION} -g ${ITERATIONS} -ckpt ${EXECUTION_DIR} -i 1 -migp 0 -" - done - SCRIPT+="deactivate -" - FILE="${BASE_DIR}/ap_pso/bm/start_bm_P_${RACE}.sh" - echo "${SCRIPT}" > "${FILE}" - sbatch -p "${QUEUE}" -N "${NODES}" "${FILE}" -done diff --git a/ap_pso/bm/bm_starter.sh b/ap_pso/bm/bm_starter.sh deleted file mode 100644 index bdd41edb..00000000 --- a/ap_pso/bm/bm_starter.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -BASE_DIR="/pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/" -for I in {0..3} -do - for FUNCTION in "sphere" "rosenbrock" "step" "quartic" "rastrigin" "griewank" "schwefel" "bisphere" "birastrigin" - do - for RACE in {0..4} - do - EXECUTION_DIR="${BASE_DIR}/ap_pso/bm/bm_${I}_${FUNCTION}_${RACE}/bm_start.sh" - NODES=1 - QUEUE="single" - case "$RACE" in - 0) - ;; - 1) - NODES=$((NODES * 2)) - QUEUE="multiple" - ;; - 2) - NODES=$((NODES * 4)) - QUEUE="multiple" - ;; - 3) - NODES=$((NODES * 8)) - QUEUE="multiple" - ;; - 4) - NODES=$((NODES * 16)) - QUEUE="multiple" - ;; - 5) - ;; - 6) - ;; - *) - echo "Error: Race $RACE was called." - exit - ;; - esac - sbatch -p "${QUEUE}" -N "${NODES}" "${EXECUTION_DIR}" - done - done -done diff --git a/ap_pso/bm/graph_plotter.py b/ap_pso/bm/graph_plotter.py deleted file mode 100644 index 35027e0a..00000000 --- a/ap_pso/bm/graph_plotter.py +++ /dev/null @@ -1,159 +0,0 @@ -import pickle -from pathlib import Path - -import matplotlib.pyplot as plt -import numpy as np -from matplotlib.axes import Axes -from matplotlib.figure import Figure - -functions = ("Sphere", "Rosenbrock", "Step", "Quartic", "Rastrigin", "Griewank", "Schwefel", "BiSphere", "BiRastrigin") -pso_names = ("VelocityClamping", "Constriction", "Basic", "Canonical") -other_stuff = ("Vanilla Propulate", "Hyppopy") - -time_path = Path("./slurm3/") -path = Path("./results3/") - - -def insert_data(d_array, idx: int, pt: Path): - """ - This function adds the data given via `pt` into the data array given by `d_array` at position `idx`. - """ - if not p.is_dir() or len([f for f in p.iterdir()]) == 0: - return - for fil in pt.iterdir(): - if not fil.suffix == ".pkl": - continue - with open(fil, "rb") as f: - tm = pickle.load(f, fix_imports=True) - d_array[idx].append([min(tm, key=lambda v: v.loss).loss, (max(tm, key=lambda v: v.rank).rank + 1) / 64]) - - -def refine_value(raw_value) -> int: - """ - This function ensures that values that are larger than they should be, are corrected to the correct number of cores. - """ - for x in range(5): - if raw_value < 2 ** x: - return 2 ** (x - 1) - else: - return 16 - - -def calc_time(iterator) -> float: - """ - This function takes an iterator on a certain string array and calculates out of this a time span in seconds. - """ - try: - start = int(next(iterator).strip("\n|: Ceirmnrtu")) - except ValueError: - return np.nan - try: - end = int(next(iterator).strip("\n|: Ceirmnrtu")) - except ValueError: - return np.nan - return (end - start) / 1e9 - - -if __name__ == "__main__": - raw_time_data: list[str] = [] - time_data: dict[str, dict[str, list[float]]] = {} - - for function_name in functions: - time_data[function_name] = {} - for program in other_stuff + pso_names: - time_data[function_name][program] = [] - - for file in time_path.iterdir(): - with open(file) as f: - raw_time_data.append(f.read()) - - for x in raw_time_data: - scatter = [st for st in x.split("#-----------------------------------#") if "Current time" in st] - itx = iter(scatter) - for program in other_stuff: - for function_name in functions: - time_data[function_name][program].append(calc_time(itx)) - for function_name in functions: - for program in pso_names: - time_data[function_name][program].append(calc_time(itx)) - - for function_name in functions: - data = [] - marker_list = ("o", "s", "D", "^", "P", "X") # ["o", "v", "^", "<", ">", "s", "p", "P", "*", "h", "X", "D"] - - for i in range(5): - data.append([]) - if i == 4: - d = f"bm_P_{function_name.lower()}_?" - else: - d = f"bm_{i}_{function_name.lower()}_?" - for p in path.glob(d): - insert_data(data, i, p) - data[i] = np.array(sorted(data[i], key=lambda v: v[1])).T - data.append([]) - for p in path.glob(f"bm_H_{function_name.lower()}_?"): - if not p.is_dir(): - continue - file = p / Path("result_0.pkl") - with open(file, "rb") as f: - tmp = pickle.load(f, fix_imports=True) - data[-1].append([min(tmp[0]["losses"]), 2000 // len(tmp[0])]) - if data[-1][-1][1] not in (1, 2, 4, 8, 16): - data[-1][-1][1] = refine_value(data[-1][-1][1]) - data[5] = np.array(sorted(data[5], key=lambda v: v[1])).T - - fig: Figure - ax: Axes - - fig, ax = plt.subplots() - # fig.subplots_adjust(hspace=0) - - ax.set_title(f"PSO@Propulate on {function_name} function") - ax.set_xlabel("Nodes") - ax.set_xscale("log", base=2) - ax.set_xticks([1, 2, 4, 8, 16], [1, 2, 4, 8, 16]) - ax.grid(True) - ax.set_ylabel("Loss") - - ax_t = ax.twinx() - ax_t.set_ylabel("Time [s]") - ax_t.set_yscale("log") - - everything = pso_names + other_stuff - for i, name in enumerate(everything): - if i < 4: - ms = 6 - else: - ms = 7 - ax.plot(data[i][1], data[i][0], label=name, marker=marker_list[i], ls="dashed", lw=2, ms=ms) - ax_t.plot(data[i][1], time_data[function_name][name], marker=marker_list[i], ls="dotted", ms=ms) - - if function_name == "Rosenbrock": - ax.set_yscale("symlog", linthresh=1e-36) - ax.set_yticks([0, 1e-36, 1e-30, 1e-24, 1e-18, 1e-12, 1e-6, 1]) - ax.set_ylim(-5e-36, 1) - elif function_name == "Step": - ax.set_yscale("symlog") - ax.set_ylim(-1e5, -5) - elif function_name == "Schwefel": - ax.set_yscale("symlog") - ax.set_ylim(-50000, 5000) - elif function_name in ("Schwefel", "Rastrigin", "BiSphere", "BiRastrigin"): - ax.set_yscale("linear") - else: - ax.set_yscale("log") - ax.legend() - - fig.show() - - save_path = Path(f"images/pso_{function_name.lower()}.png") - if save_path.parent.exists() and not save_path.parent.is_dir(): - OSError("There is something in the way. We can't store our paintings.") - save_path.parent.mkdir(parents=True, exist_ok=True) - - fig.savefig(save_path) - fig.savefig(save_path.with_suffix(".svg")) - fig.savefig(save_path.with_suffix(".pdf")) - fig.savefig(save_path.with_stem(save_path.stem + "_T"), transparent=True) - fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".svg"), transparent=True) - fig.savefig(save_path.with_stem(save_path.stem + "_T").with_suffix(".pdf"), transparent=True) diff --git a/ap_pso/bm/hyppopy_benchmark.py b/ap_pso/bm/hyppopy_benchmark.py deleted file mode 100644 index c409201d..00000000 --- a/ap_pso/bm/hyppopy_benchmark.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -import os.path -import pickle -import random -import sys -import warnings -import time -from pathlib import Path - -from hyppopy.HyppopyProject import HyppopyProject -from hyppopy.MPIBlackboxFunction import MPIBlackboxFunction -from hyppopy.SolverPool import SolverPool -from hyppopy.solvers.MPISolverWrapper import MPISolverWrapper -from hyppopy.solvers.OptunitySolver import OptunitySolver -from mpi4py import MPI - -from scripts.function_benchmark import get_function_search_space - -if __name__ == "__main__": - assert len(sys.argv) >= 4 - - ############ - # SETTINGS # - ############ - - function_name = sys.argv[1] # Get function to optimize from command-line. - max_iterations = int(sys.argv[2]) - CHECKPOINT_PLACE = sys.argv[3] - POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. - - function, limits = get_function_search_space(function_name) - rng = random.Random(MPI.COMM_WORLD.rank) - - if MPI.COMM_WORLD.rank == 0: - print("#-----------------------------------#") - print(f"| Current time: {time.time_ns()} |") - print("#-----------------------------------#") - - project = HyppopyProject() - for key in limits: - project.add_hyperparameter(name=key, domain="uniform", data=list(limits[key]), type=float) - project.add_setting(name="max_iterations", value=max_iterations) - project.add_setting(name="solver", value="optunity") - - blackbox = MPIBlackboxFunction(blackbox_func=function, mpi_comm=MPI.COMM_WORLD) - - solver = OptunitySolver(project) - solver = MPISolverWrapper(solver=solver, mpi_comm=MPI.COMM_WORLD) - solver.blackbox = blackbox - - solver.run() - df, best = solver.get_results() - - path = Path(f"{CHECKPOINT_PLACE}/result_{MPI.COMM_WORLD.rank}.pkl") - path.parent.mkdir(parents=True, exist_ok=True) - if os.path.isfile(path): - try: - os.replace(path, path.with_suffix(".bkp")) - warnings.warn("Results file already existing! Possibly overwriting data!") - except OSError as e: - print(e) - with open(path, "wb") as f: - pickle.dump((df, best), f) - - if MPI.COMM_WORLD.rank == 0: - print("#-----------------------------------#") - print(f"| Current time: {time.time_ns()} |") - print("#-----------------------------------#") - diff --git a/ap_pso/bm/pso_benchmark.py b/ap_pso/bm/pso_benchmark.py deleted file mode 100644 index 3c50cec0..00000000 --- a/ap_pso/bm/pso_benchmark.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -import random -import sys -import time -from pathlib import Path - -from mpi4py import MPI - -from ap_pso.propagators import PSOInitUniform, VelocityClampingPropagator, ConstrictionPropagator, PSOCompose, \ - BasicPSOPropagator, StatelessPSOPropagator, CanonicalPropagator -from propulate import Islands -from propulate.propagators import Conditional, InitUniform -from scripts.function_benchmark import get_function_search_space - -############ -# SETTINGS # -############ - -function_name = sys.argv[1] # Get function to optimize from command-line. -NUM_GENERATIONS: int = int(sys.argv[2]) # Set number of generations. -POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. -PSO_TYPE = int(sys.argv[3]) # selects the propagator below -CHECKPOINT_PLACE = sys.argv[4] -num_migrants = 1 - -function, limits = get_function_search_space(function_name) - -if __name__ == "__main__": - # migration_topology = num_migrants*np.ones((4, 4), dtype=int) - # np.fill_diagonal(migration_topology, 0) - - rng = random.Random(MPI.COMM_WORLD.rank) - - propagator = [ - VelocityClampingPropagator(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6), - ConstrictionPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng), - BasicPSOPropagator(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng), - CanonicalPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) - ][PSO_TYPE] - - init = PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) - propagator = Conditional(POP_SIZE, propagator, init) - if MPI.COMM_WORLD.rank == 0: - print("#-----------------------------------#") - print(f"| Current time: {time.time_ns()} |") - print("#-----------------------------------#") - print(f"\nSaving files to: {Path(CHECKPOINT_PLACE).name}") - - islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path=CHECKPOINT_PLACE, - migration_probability=0, pollination=False) - islands.evolve(top_n=1, logging_interval=10, debug=0) - - if MPI.COMM_WORLD.rank == 0: - print("#-----------------------------------#") - print(f"| Current time: {time.time_ns()} |") - print("#-----------------------------------#") - -# islands.propulator.paint_graphs(function_name) diff --git a/ap_pso/bm/recovery/start_rc_1.sh b/ap_pso/bm/recovery/start_rc_1.sh deleted file mode 100644 index 7542ffad..00000000 --- a/ap_pso/bm/recovery/start_rc_1.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -#SBATCH --nodes=2 -#SBATCH --ntasks=128 -#SBATCH --partition=multiple_il -#SBATCH --job-name=rc_1_1 -#SBATCH --time=0:30:00 -#SBATCH --mem=40000 -#SBATCH --cpus-per-task=1 -#SBATCH --mail-type=ALL -#SBATCH --mail-user=pa1164@partner.kit.edu - -cd $(ws_find propulate_bm_1) -ml purge -ml restore propulate -source /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/.venvs/async-parallel-pso/bin/activate -mpirun --bind-to core --map-by core -mca btl ^ofi python -u /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/pso_benchmark.py schwefel 1000 3 /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/results3/bm_3_schwefel_1 \ No newline at end of file diff --git a/ap_pso/bm/recovery/start_rc_3.sh b/ap_pso/bm/recovery/start_rc_3.sh deleted file mode 100644 index ebf49dfc..00000000 --- a/ap_pso/bm/recovery/start_rc_3.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -#SBATCH --nodes=8 -#SBATCH --ntasks=512 -#SBATCH --partition=multiple_il -#SBATCH --job-name=rc_3_1 -#SBATCH --time=2:00:00 -#SBATCH --mem=40000 -#SBATCH --cpus-per-task=1 -#SBATCH --mail-type=ALL -#SBATCH --mail-user=pa1164@partner.kit.edu - -cd $(ws_find propulate_bm_1) -ml purge -ml restore propulate -source /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/.venvs/async-parallel-pso/bin/activate -mpirun --bind-to core --map-by core -mca btl ^ofi python -u /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/scripts/islands_example.py -f quartic -g 250 -ckpt /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/results3/bm_P_quartic_3 -i 1 -migp 0 -v 0 -mpirun --bind-to core --map-by core -mca btl ^ofi python -u /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/pso_benchmark.py sphere 250 2 /pfs/work7/workspace/scratch/pa1164-propulate_bm_1/async-parallel-pso/ap_pso/bm/results3/bm_2_sphere_3 \ No newline at end of file diff --git a/ap_pso/bm/torch_benchmark.py b/ap_pso/bm/torch_benchmark.py deleted file mode 100755 index 62d3a4da..00000000 --- a/ap_pso/bm/torch_benchmark.py +++ /dev/null @@ -1,296 +0,0 @@ -#!/usr/bin/env python3 - -import random -import sys -import time -from typing import Tuple, Dict, Union - -import numpy as np -import torch -from lightning.pytorch import loggers -from torch import nn -from torch.utils.data import DataLoader - -from pytorch_lightning import LightningModule, Trainer -from torchmetrics import Accuracy - -from torchvision.datasets import MNIST -from torchvision.transforms import Compose, ToTensor, Normalize - -from mpi4py import MPI - -from ap_pso.propagators import * -from propulate import Islands -from propulate.propagators import Conditional - -num_generations = 10 -pop_size = 2 * MPI.COMM_WORLD.size -GPUS_PER_NODE = 1 # 4 -log_path = "torch_ckpts" - -limits = { - "conv_layers": (2.0, 10.0), - "lr": (0.01, 0.0001), - "epochs": (2.0, 400.0) -} - - -class Net(LightningModule): - def __init__(self, convlayers: int, activation, lr: float, loss_fn): - super(Net, self).__init__() - - self.lr = lr - self.loss_fn = loss_fn - self.best_accuracy = 0.0 - layers = [] - layers += [ - nn.Sequential(nn.Conv2d(1, - 10, - kernel_size=3, - padding=1), - activation()), - ] - layers += [ - nn.Sequential(nn.Conv2d(10, - 10, - kernel_size=3, - padding=1), - activation()) - for _ in range(convlayers - 1) - ] - - self.fc = nn.Linear(7840, 10) - self.conv_layers = nn.Sequential(*layers) - - self.val_acc = Accuracy('multiclass', num_classes=10) - self.train_acc = Accuracy("multiclass", num_classes=10) - - def forward(self, x): - """ - Forward pass. - - Parameters - ---------- - x: torch.Tensor - data sample - - Returns - ------- - torch.Tensor - The model's predictions for input data sample - """ - b, c, w, h = x.size() - x = self.conv_layers(x) - x = x.view(b, 10 * 28 * 28) - x = self.fc(x) - return x - - def training_step( - self, batch: Tuple[torch.Tensor, torch.Tensor], batch_idx: int - ) -> torch.Tensor: - """ - Calculate loss for training step in Lightning train loop. - - Parameters - ---------- - batch: Tuple[torch.Tensor, torch.Tensor] - input batch - batch_idx: int - batch index - - Returns - ------- - torch.Tensor - training loss for input batch - """ - x, y = batch - pred = self(x) - loss_val = self.loss_fn(pred, y) - self.log("train loss", loss_val) - train_acc_val = self.train_acc(torch.nn.functional.softmax(pred, dim=-1), y) - self.log("train_ acc", train_acc_val) - return loss_val - - def validation_step( - self, batch: Tuple[torch.Tensor, torch.Tensor], batch_idx: int - ) -> torch.Tensor: - """ - Calculate loss for validation step in Lightning validation loop during training. - - Parameters - ---------- - batch: Tuple[torch.Tensor, torch.Tensor] - current batch - batch_idx: int - batch index - - Returns - ------- - torch.Tensor - validation loss for input batch - """ - x, y = batch - pred = self(x) - loss_val = self.loss_fn(pred, y) - val_acc_val = self.val_acc(torch.nn.functional.softmax(pred, dim=-1), y) - self.log("val_loss", loss_val) - self.log("val_acc", val_acc_val) - return loss_val - - def configure_optimizers(self) -> torch.optim.SGD: - """ - Configure optimizer. - - Returns - ------- - torch.optim.sgd.SGD - stochastic gradient descent optimizer - """ - return torch.optim.SGD(self.parameters(), lr=self.lr) - - def on_validation_epoch_end(self): - """ - Calculate and store the model's validation accuracy after each epoch. - """ - val_acc_val = self.val_acc.compute().item() - self.log("val_acc_val", val_acc_val) - self.val_acc.reset() - if val_acc_val > self.best_accuracy: - self.best_accuracy = val_acc_val - -def get_data_loaders(batch_size: int) -> Tuple[DataLoader, DataLoader]: - """ - Get MNIST train and validation dataloaders. - - Parameters - ---------- - batch_size: int - batch size - - Returns - ------- - DataLoader - training dataloader - DataLoader - validation dataloader - """ - data_transform = Compose([ToTensor(), Normalize((0.1307,), (0.3081,))]) - - if MPI.COMM_WORLD.Get_rank() == 0: # Only root downloads data. - train_loader = DataLoader( - dataset=MNIST( - download=True, root=".", transform=data_transform, - ), # Use MNIST training dataset. - batch_size=batch_size, # Batch size - shuffle=True, # Shuffle data. - ) - MPI.COMM_WORLD.barrier() - else: - MPI.COMM_WORLD.barrier() - train_loader = DataLoader( - dataset=MNIST( - root=".", transform=data_transform - ), # Use MNIST training dataset. - batch_size=batch_size, # Batch size - shuffle=True, # Shuffle data. - ) - val_loader = DataLoader( - dataset=MNIST( - root=".", transform=data_transform, train=False - ), # Use MNIST testing dataset. - shuffle=False, # Do not shuffle data. - ) - return train_loader, val_loader - -def ind_loss(params: Dict[str, Union[int, float, str]]) -> float: - """ - Loss function for evolutionary optimization with Propulate. Minimize the model's negative validation accuracy. - - Parameters - ---------- - params: dict[str, int | float | str]] - - Returns - ------- - float - The trained model's negative validation accuracy - """ - loss = 0.0 - # Extract hyperparameter combination to test from input dictionary. - conv_layers = int(np.round(params["conv_layers"])) # Number of convolutional layers - if conv_layers < 2: - loss += 10 - 5 * conv_layers - conv_layers = 2 - # activation = params["activation"] # Activation function - lr = params["lr"] # Learning rate - epochs = params["epochs"] - if epochs < 2: - loss += 10 - 5 * epochs - epochs = 2 # Number of epochs to train - - activations = { - "relu": nn.ReLU, - "sigmoid": nn.Sigmoid, - "tanh": nn.Tanh, - "leaky_relu": nn.LeakyReLU - } # Define activation function mapping. - activation = activations["leaky_relu"] # Get activation function. - loss_fn = ( - torch.nn.CrossEntropyLoss() - ) # Use cross-entropy loss for multi-class classification. - - model = Net( - conv_layers, activation, lr, loss_fn - ) # Set up neural network with specified hyperparameters. - model.best_accuracy = 0.0 # Initialize the model's best validation accuracy. - - train_loader, val_loader = get_data_loaders( - batch_size=8 - ) # Get training and validation data loaders. - - tb_logger = loggers.TensorBoardLogger( - save_dir=log_path + "/lightning_logs" - ) # Get tensor board logger. - - # Under the hood, the Lightning Trainer handles the training loop details. - trainer = Trainer( - max_epochs=epochs, # Stop training once this number of epochs is reached. - accelerator="gpu", # Pass accelerator type. - devices=[MPI.COMM_WORLD.Get_rank() % GPUS_PER_NODE], # Devices to train on - enable_progress_bar=True, # Disable progress bar. - logger=tb_logger, # Logger - ) - trainer.fit( # Run full model training optimization routine. - model=model, # Model to train - train_dataloaders=train_loader, # Dataloader for training samples - val_dataloaders=val_loader, # Dataloader for validation samples - ) - # Return negative best validation accuracy as an individual's loss. - return -model.best_accuracy - - -if __name__ == "__main__": - rng = random.Random(MPI.COMM_WORLD.rank) - pso = [ - VelocityClampingPropagator(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6), - ConstrictionPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng), - BasicPSOPropagator(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng), - CanonicalPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) - ][int(sys.argv[1])] - - # propagator = get_default_propagator(pop_size, limits, 0.7, 0.4, 0.1, rng=rng) - - if MPI.COMM_WORLD.rank == 0: - print("#-----------------------------------#") - print(f"| Current time: {time.time_ns()} |") - print("#-----------------------------------#") - - propagator = Conditional(pop_size, pso, PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank)) - islands = Islands(ind_loss, propagator, rng, generations=num_generations, pollination=False, - migration_probability=0, checkpoint_path=log_path) - islands.evolve(top_n=1, debug=2) - - if MPI.COMM_WORLD.rank == 0: - print("#-----------------------------------#") - print(f"| Current time: {time.time_ns()} |") - print("#-----------------------------------#") diff --git a/ap_pso/propagators/__init__.py b/ap_pso/propagators/__init__.py deleted file mode 100644 index 783581e1..00000000 --- a/ap_pso/propagators/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -In this package, I collect all PSO-related propagators. -""" -__all__ = ["PSOInitUniform", "StatelessPSOPropagator", "BasicPSOPropagator", "VelocityClampingPropagator", - "ConstrictionPropagator", "CanonicalPropagator", "PSOCompose"] - -from ap_pso.propagators.basic_pso import BasicPSOPropagator -from ap_pso.propagators.constriction import ConstrictionPropagator -from ap_pso.propagators.canonical import CanonicalPropagator -from ap_pso.propagators.pso_compose import PSOCompose -from ap_pso.propagators.pso_init_uniform import PSOInitUniform -from ap_pso.propagators.stateless_pso import StatelessPSOPropagator -from ap_pso.propagators.velocity_clamping import VelocityClampingPropagator diff --git a/ap_pso/scripts/pso_example.sh b/ap_pso/scripts/pso_example.sh deleted file mode 100644 index 5d47abfa..00000000 --- a/ap_pso/scripts/pso_example.sh +++ /dev/null @@ -1,72 +0,0 @@ -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -# Options: birastrigin, bisphere, bukin, eggcrate, griewank, himmelblau, keane, leon, quartic, rastrigin, rosenbrock, schwefel, sphere, step - -mpirun -n 4 python "$(dirname "$0")"/pso_example.py birastrigin 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py bisphere 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py bukin 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py eggcrate 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py griewank 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py schwefel 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py himmelblau 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py keane 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py leon 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py quartic 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py rastrigin 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py rosenbrock 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py sphere 100 -if [ "$1" = "clear" ] -then - rm -rf ./checkpoints -fi -mpirun -n 4 python "$(dirname "$0")"/pso_example.py step 100 diff --git a/ap_pso/utils.py b/ap_pso/utils.py deleted file mode 100644 index 6c4a4871..00000000 --- a/ap_pso/utils.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -This file contains all sort of more or less useful stuff. -""" -import numpy as np - -from ap_pso import Particle -from propulate.population import Individual - - - diff --git a/propulate/propagators/pso/__init__.py b/propulate/propagators/pso/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ap_pso/propagators/basic_pso.py b/propulate/propagators/pso/basic_pso.py similarity index 100% rename from ap_pso/propagators/basic_pso.py rename to propulate/propagators/pso/basic_pso.py diff --git a/ap_pso/propagators/canonical.py b/propulate/propagators/pso/canonical.py similarity index 100% rename from ap_pso/propagators/canonical.py rename to propulate/propagators/pso/canonical.py diff --git a/ap_pso/propagators/constriction.py b/propulate/propagators/pso/constriction.py similarity index 100% rename from ap_pso/propagators/constriction.py rename to propulate/propagators/pso/constriction.py diff --git a/ap_pso/propagators/pso_compose.py b/propulate/propagators/pso/pso_compose.py similarity index 100% rename from ap_pso/propagators/pso_compose.py rename to propulate/propagators/pso/pso_compose.py diff --git a/ap_pso/propagators/pso_init_uniform.py b/propulate/propagators/pso/pso_init_uniform.py similarity index 100% rename from ap_pso/propagators/pso_init_uniform.py rename to propulate/propagators/pso/pso_init_uniform.py diff --git a/ap_pso/propagators/stateless_pso.py b/propulate/propagators/pso/stateless_pso.py similarity index 100% rename from ap_pso/propagators/stateless_pso.py rename to propulate/propagators/pso/stateless_pso.py diff --git a/ap_pso/propagators/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py similarity index 100% rename from ap_pso/propagators/velocity_clamping.py rename to propulate/propagators/pso/velocity_clamping.py From 637686b3286cea9bdac4efa0cf0d6db6018f8c6b Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 14:36:21 +0200 Subject: [PATCH 082/139] Removed gitmodules file. --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 6e9be902..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "propulate"] - path = propulate - url = https://github.com/Helmholtz-AI-Energy/propulate/ From 94eec81b130ae74d72d248c89b93c8cedf9720d2 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 14:36:51 +0200 Subject: [PATCH 083/139] Added the file that originally belonged to the merge. --- propulate/propagators/__init__.py | 4 +++- propulate/propagators/pso/__init__.py | 8 +++++++ propulate/propagators/pso/basic_pso.py | 5 +++-- propulate/propagators/pso/canonical.py | 6 ++--- propulate/propagators/pso/constriction.py | 6 ++--- propulate/propagators/pso/pso_init_uniform.py | 3 ++- propulate/propagators/pso/stateless_pso.py | 2 +- .../propagators/pso/velocity_clamping.py | 6 ++--- tutorials/pso_example.py | 22 +++++++++---------- 9 files changed, 36 insertions(+), 26 deletions(-) diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py index 1a8356a8..e3f0926c 100644 --- a/propulate/propagators/__init__.py +++ b/propulate/propagators/__init__.py @@ -4,6 +4,8 @@ __all__ = ["Propagator", "Stochastic", "Conditional", "Compose", "PointMutation", "RandomPointMutation", "IntervalMutationNormal", "MateUniform", "MateMultiple", "MateSigmoid", "SelectMin", "SelectMax", - "SelectUniform", "InitUniform"] + "SelectUniform", "InitUniform", "pso", "StatelessPSO"] from propulate.propagators.propagators import * +import pso +from pso.stateless_pso import StatelessPSO diff --git a/propulate/propagators/pso/__init__.py b/propulate/propagators/pso/__init__.py index e69de29b..e6f05f75 100644 --- a/propulate/propagators/pso/__init__.py +++ b/propulate/propagators/pso/__init__.py @@ -0,0 +1,8 @@ +__all__ = ["PSOInitUniform", "BasicPSO", "VelocityClamping", "Constriction", "Canonical"] + +from pso_init_uniform import PSOInitUniform +from basic_pso import BasicPSO +from velocity_clamping import VelocityClamping +from constriction import Constriction +from canonical import Canonical + diff --git a/propulate/propagators/pso/basic_pso.py b/propulate/propagators/pso/basic_pso.py index 5ceb3b7f..d98001e1 100644 --- a/propulate/propagators/pso/basic_pso.py +++ b/propulate/propagators/pso/basic_pso.py @@ -6,11 +6,12 @@ import numpy as np -from ap_pso import Particle, make_particle +from propulate.particle import Particle from propulate.propagators import Propagator +from propulate.utils import make_particle -class BasicPSOPropagator(Propagator): +class BasicPSO(Propagator): def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, limits: Dict[str, Tuple[float, float]], rng: Random): diff --git a/propulate/propagators/pso/canonical.py b/propulate/propagators/pso/canonical.py index 296c4acc..72a84d63 100644 --- a/propulate/propagators/pso/canonical.py +++ b/propulate/propagators/pso/canonical.py @@ -2,11 +2,11 @@ import numpy as np -from ap_pso import Particle -from ap_pso.propagators import ConstrictionPropagator +from propulate.particle import Particle +from constriction import Constriction -class CanonicalPropagator(ConstrictionPropagator): +class Canonical(Constriction): def __init__(self, c_cognitive, c_social, rank, limits, rng): super().__init__(c_cognitive, c_social, rank, limits, rng) diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py index 3ca2549a..2a4c8996 100644 --- a/propulate/propagators/pso/constriction.py +++ b/propulate/propagators/pso/constriction.py @@ -6,11 +6,11 @@ import numpy as np -from ap_pso import Particle -from ap_pso.propagators import BasicPSOPropagator +from basic_pso import BasicPSO +from propulate.particle import Particle -class ConstrictionPropagator(BasicPSOPropagator): +class Constriction(BasicPSO): def __init__(self, c_cognitive: float, c_social: float, diff --git a/propulate/propagators/pso/pso_init_uniform.py b/propulate/propagators/pso/pso_init_uniform.py index e3b3d5b5..41ace6a3 100644 --- a/propulate/propagators/pso/pso_init_uniform.py +++ b/propulate/propagators/pso/pso_init_uniform.py @@ -6,9 +6,10 @@ import numpy as np -from ap_pso import Particle, make_particle +from propulate.particle import Particle from propulate.population import Individual from propulate.propagators import Stochastic +from propulate.utils import make_particle class PSOInitUniform(Stochastic): diff --git a/propulate/propagators/pso/stateless_pso.py b/propulate/propagators/pso/stateless_pso.py index 2ee57bbc..8324ca97 100644 --- a/propulate/propagators/pso/stateless_pso.py +++ b/propulate/propagators/pso/stateless_pso.py @@ -9,7 +9,7 @@ from propulate.propagators import Propagator -class StatelessPSOPropagator(Propagator): +class StatelessPSO(Propagator): def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, limits: Dict[str, Tuple[float, float]], rng: Random): diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py index 201fe4b5..a94bcb23 100644 --- a/propulate/propagators/pso/velocity_clamping.py +++ b/propulate/propagators/pso/velocity_clamping.py @@ -6,11 +6,11 @@ import numpy as np -from ap_pso import Particle -from ap_pso.propagators import BasicPSOPropagator +from propulate.particle import Particle +from basic_pso import BasicPSO -class VelocityClampingPropagator(BasicPSOPropagator): +class VelocityClamping(BasicPSO): def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, limits: Dict[str, Tuple[float, float]], rng: Random, v_limits: Union[float, np.ndarray]): """ diff --git a/tutorials/pso_example.py b/tutorials/pso_example.py index 538ad864..54772562 100644 --- a/tutorials/pso_example.py +++ b/tutorials/pso_example.py @@ -4,11 +4,10 @@ from mpi4py import MPI -from ap_pso.propagators import PSOInitUniform, VelocityClampingPropagator, ConstrictionPropagator, PSOCompose, \ - BasicPSOPropagator, StatelessPSOPropagator, CanonicalPropagator from propulate import Islands -from propulate.propagators import Conditional +from propulate.propagators import Conditional, StatelessPSO from function_benchmark import get_function_search_space +from propulate.propagators.pso import BasicPSO, VelocityClamping, Constriction, Canonical, PSOInitUniform ############ # SETTINGS # @@ -26,15 +25,14 @@ rng = random.Random(MPI.COMM_WORLD.rank) - pso_propagator = PSOCompose( - [ - # VelocityClampingPropagator(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6) - # ConstrictionPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) - # BasicPSOPropagator(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng) - CanonicalPropagator(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) - # StatelessPSOPropagator(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng) - ] - ) + pso_propagator = [ + StatelessPSO(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng), + + BasicPSO(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng), + VelocityClamping(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6), + Constriction(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng), + Canonical(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) + ][1] # Please choose with this index, which Propagator to use in the optimisation process. init = PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) propagator = Conditional(POP_SIZE, pso_propagator, init) From 7427e13d06c82f8e966f75e5a38c0585d325b07d Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 14:38:15 +0200 Subject: [PATCH 084/139] Removed a helper file only for myself in my repo. --- in_case_of_update.md | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 in_case_of_update.md diff --git a/in_case_of_update.md b/in_case_of_update.md deleted file mode 100644 index 4f85ce98..00000000 --- a/in_case_of_update.md +++ /dev/null @@ -1,22 +0,0 @@ -# In Case of Update -As `Propulate`* is under continuous development, from time to time there are updates on the `master` branch in the `Propulate` repo. - -In order to be able to update `Propulate` into this repo, please do the following: - -1. If not present, create a branch `propulate` that points to the `Propulate` repo: - ``` - git remote add propulate git@github.com:Helmholtz-AI-Energy/propulate.git - ``` -2. Then, call - ``` - git fetch propulate master - ``` - in order to pull the current state of `Propulate`'s repo's `master` branch into your own `master` branch. -3. You will likely get a merge conflict which you can then resolve by using the built-in tools of PyCharm. - If you are not using PyCharm please refer to the Git manual and look up how to resolve the conflicts. - Alternatively, do - ``` - git merge propulate/master - ``` - while being on branch master. -4. Afterwards, commit everything and push it onto your own GitHub repo so everything is fine. From c4349688d6f738bf52ddc1ff3e21fce6dc4036ac Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 14:40:48 +0200 Subject: [PATCH 085/139] Removed paint_graphs routine as it is obsolete. --- propulate/propulator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/propulate/propulator.py b/propulate/propulator.py index 05b431e2..3f1f52f4 100644 --- a/propulate/propulator.py +++ b/propulate/propulator.py @@ -556,4 +556,3 @@ def summarize( res_str += f"({i+1}): {unique_pop[i]}\n" log.info(res_str) return MPI.COMM_WORLD.allgather(best) - From fe1ed94fe26bb93ae033ce14adfc506d77c16ae0 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 14:53:22 +0200 Subject: [PATCH 086/139] Error correction. --- propulate/propagators/pso/pso_compose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/propulate/propagators/pso/pso_compose.py b/propulate/propagators/pso/pso_compose.py index 48338fcf..c917a08e 100644 --- a/propulate/propagators/pso/pso_compose.py +++ b/propulate/propagators/pso/pso_compose.py @@ -1,6 +1,6 @@ from typing import List -from ap_pso import Particle +from propulate.particle import Particle from propulate.population import Individual from propulate.propagators import Compose From 420266d2c5935cb3287a3d92d754cbd35a427335 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 15:13:36 +0200 Subject: [PATCH 087/139] Added a whole bunch of doc strings. --- propulate/propagators/pso/basic_pso.py | 20 +++++++++++++++++++ propulate/propagators/pso/canonical.py | 8 ++++++++ propulate/propagators/pso/constriction.py | 8 ++++++++ propulate/propagators/pso/pso_compose.py | 4 ++++ propulate/propagators/pso/pso_init_uniform.py | 3 +++ propulate/propagators/pso/stateless_pso.py | 7 +++++++ .../propagators/pso/velocity_clamping.py | 8 ++++++++ 7 files changed, 58 insertions(+) diff --git a/propulate/propagators/pso/basic_pso.py b/propulate/propagators/pso/basic_pso.py index d98001e1..ba23ea3c 100644 --- a/propulate/propagators/pso/basic_pso.py +++ b/propulate/propagators/pso/basic_pso.py @@ -12,6 +12,26 @@ class BasicPSO(Propagator): + """ + This propagator implements the most basic PSO variant one possibly could think of. + + It features an inertia factor w_k applied to the old velocity in the velocity update, + a social and a cognitive factor, as well as some measures to implement some none-linearity. + + This is done with the help of some randomness. + + As this propagator is very basic in all manners, it can only feature float-typed search domains. + Please keep this in mind when feeding the propagator with limits. + Else, it might happen that warnings occur. + + This propagator also serves as the foundation of all other pso propagators and supplies + them with protected methods that help in the update process. + + If you want to implement further pso propagators, please do your best to + derive them from this propagator or from one that is derived from this. + + This propagator works on Particle-class objects. + """ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, limits: Dict[str, Tuple[float, float]], rng: Random): diff --git a/propulate/propagators/pso/canonical.py b/propulate/propagators/pso/canonical.py index 72a84d63..6e4a5b7b 100644 --- a/propulate/propagators/pso/canonical.py +++ b/propulate/propagators/pso/canonical.py @@ -7,6 +7,14 @@ class Canonical(Constriction): + """ + This propagator subclass features a combination of Constriction PSO and VelocityClamping PSO. + + The velocity clamping is made with a clamping factor of 1, + the constriction is done as on the parental Constriction propagator. + + For information on the method parameters, please refer to the Constriction propagator. + """ def __init__(self, c_cognitive, c_social, rank, limits, rng): super().__init__(c_cognitive, c_social, rank, limits, rng) diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py index 2a4c8996..c630d102 100644 --- a/propulate/propagators/pso/constriction.py +++ b/propulate/propagators/pso/constriction.py @@ -11,6 +11,14 @@ class Constriction(BasicPSO): + """ + This propagator subclass features Constriction PSO as proposed by Clerc and Kennedy in 2002. + + Instead of an inertia factor that affects the old velocity value within the velocity update, + there is a constriction factor, that is applied on the new velocity `after' the update. + + This propagator runs on Particle-class objects. + """ def __init__(self, c_cognitive: float, c_social: float, diff --git a/propulate/propagators/pso/pso_compose.py b/propulate/propagators/pso/pso_compose.py index c917a08e..55a90faa 100644 --- a/propulate/propagators/pso/pso_compose.py +++ b/propulate/propagators/pso/pso_compose.py @@ -6,6 +6,10 @@ class PSOCompose(Compose): + """ + This class is the Particle-using counterpart to the Compose propagator. + It does basically exact the same things. For further reference, please refer to the standard Compose propagator. + """ def __call__(self, particles: List[Particle]) -> Particle: """ Returns the first element of the list of particles returned by the last Propagator in the list diff --git a/propulate/propagators/pso/pso_init_uniform.py b/propulate/propagators/pso/pso_init_uniform.py index 41ace6a3..3202aa86 100644 --- a/propulate/propagators/pso/pso_init_uniform.py +++ b/propulate/propagators/pso/pso_init_uniform.py @@ -15,6 +15,9 @@ class PSOInitUniform(Stochastic): """ Initialize individuals by uniformly sampling specified limits for each trait. + + This propagator is the Particle-using counterpart to the InitUniform propagator. + For more information, please have a look there. """ def __init__(self, limits: Dict[str, Tuple[float, float]], parents=0, probability=1.0, rng: Random = None, *, diff --git a/propulate/propagators/pso/stateless_pso.py b/propulate/propagators/pso/stateless_pso.py index 8324ca97..2233380f 100644 --- a/propulate/propagators/pso/stateless_pso.py +++ b/propulate/propagators/pso/stateless_pso.py @@ -10,6 +10,13 @@ class StatelessPSO(Propagator): + """ + The first draft of a pso propagator. It uses the infrastructure brought to you by vanilla Propulate and nothing more. + + Thus, it won't deliver that interesting results. + + This propagator works on Propulate's Individual-class objects. + """ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, limits: Dict[str, Tuple[float, float]], rng: Random): diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py index a94bcb23..ea60d2b4 100644 --- a/propulate/propagators/pso/velocity_clamping.py +++ b/propulate/propagators/pso/velocity_clamping.py @@ -11,6 +11,14 @@ class VelocityClamping(BasicPSO): + """ + This propagator implements the Velocity Clamping pso variant. + + In addition to the parameters known from the basic PSO propagator, it features a parameter, + via which relative values (best between 0 and 1) are passed to the propagator. + + Based on these values, the velocities of the particles are cut down to a reasonable value. + """ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, limits: Dict[str, Tuple[float, float]], rng: Random, v_limits: Union[float, np.ndarray]): """ From 86ec92e1e64be389c2ed73bb0c526ebcbd653a52 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 16:15:32 +0200 Subject: [PATCH 088/139] Re-added the workflow files because github can't offer to accept partial PRs. --- .github/workflows/fair-software.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/fair-software.yml diff --git a/.github/workflows/fair-software.yml b/.github/workflows/fair-software.yml new file mode 100644 index 00000000..80969ded --- /dev/null +++ b/.github/workflows/fair-software.yml @@ -0,0 +1,17 @@ +name: fair-software + +on: + push: + branches: [master] + +jobs: + verify: + name: "fair-software" + runs-on: ubuntu-latest + steps: + - uses: fair-software/howfairis-github-action@0.2.1 + name: Measure compliance with fair-software.eu recommendations + env: + PYCHARM_HOSTED: "Trick colorama into displaying colored output" + with: + MY_REPO_URL: "https://github.com/${{ github.repository }}" From ead45c7f9f076f3b7653f8371bb74227cfcb8463 Mon Sep 17 00:00:00 2001 From: Morridin Date: Mon, 4 Sep 2023 16:25:48 +0200 Subject: [PATCH 089/139] Resolved the image-painting in a nicer way. --- propulate/islands.py | 72 +++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/propulate/islands.py b/propulate/islands.py index 039febb3..ad0473f2 100644 --- a/propulate/islands.py +++ b/propulate/islands.py @@ -1,16 +1,16 @@ +import logging +import platform import random from pathlib import Path from typing import Callable, Union, List, Type -import logging -from mpi4py import MPI import numpy as np +from mpi4py import MPI -from .propagators import Propagator, SelectMin, SelectMax from .migrator import Migrator from .pollinator import Pollinator from .population import Individual - +from .propagators import Propagator, SelectMin, SelectMax log = logging.getLogger(__name__) # Get logger instance. @@ -27,19 +27,19 @@ class Islands: """ def __init__( - self, - loss_fn: Callable, - propagator: Propagator, - rng: random.Random, - generations: int = 0, - num_islands: int = 1, - island_sizes: np.ndarray = None, - migration_topology: np.ndarray = None, - migration_probability: float = 0.0, - emigration_propagator: Type[Propagator] = SelectMin, - immigration_propagator: Type[Propagator] = SelectMax, - pollination: bool = True, - checkpoint_path: Union[str, Path] = Path("./"), + self, + loss_fn: Callable, + propagator: Propagator, + rng: random.Random, + generations: int = 0, + num_islands: int = 1, + island_sizes: np.ndarray = None, + migration_topology: np.ndarray = None, + migration_probability: float = 0.0, + emigration_propagator: Type[Propagator] = SelectMin, + immigration_propagator: Type[Propagator] = SelectMax, + pollination: bool = True, + checkpoint_path: Union[str, Path] = Path("./"), ) -> None: """ Initialize island model with given parameters. @@ -97,22 +97,24 @@ def __init__( print( "#################################################\n" "# PROPULATE: Parallel Propagator of Populations #\n" - "#################################################\n\n" - " ⠀⠀⠀⠈⠉⠛⢷⣦⡀⠀⣀⣠⣤⠤⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" - "⠀ ⠀⠀⠀⠀⠀⣀⣻⣿⣿⣿⣋⣀⡀⠀⠀⢀⣠⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" - "⠀ ⠀⠀⣠⠾⠛⠛⢻⣿⣿⣿⠟⠛⠛⠓⠢⠀⠀⠉⢿⣿⣆⣀⣠⣤⣀⣀⠀⠀⠀\n" - "⠀ ⠀⠘⠁⠀⠀⣰⡿⠛⠿⠿⣧⡀⠀⠀⢀⣤⣤⣤⣼⣿⣿⣿⡿⠟⠋⠉⠉⠀⠀\n" - "⠀ ⠀⠀⠀⠀⠠⠋⠀⠀⠀⠀⠘⣷⡀⠀⠀⠀⠀⠹⣿⣿⣿⠟⠻⢶⣄⠀⠀⠀⠀\n" - "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣧⠀⠀⠀⠀⢠⡿⠁⠀⠀⠀⠀⠈⠀⠀⠀⠀\n" - "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⡄⠀⠀⢠⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" - "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣾⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" - "⠀ ⣤⣤⣤⣤⣤⣤⡤⠄⠀⠀⣀⡀⢸⡇⢠⣤⣁⣀⠀⠀⠠⢤⣤⣤⣤⣤⣤⣤⠀\n" - "⠀⠀⠀⠀⠀ ⠀⣀⣤⣶⣾⣿⣿⣷⣤⣤⣾⣿⣿⣿⣿⣷⣶⣤⣀⠀⠀⠀⠀⠀⠀\n" - " ⠀⠀⠀⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣄⠀⠀⠀\n" - "⠀ ⠀⠼⠿⣿⣿⠿⠛⠉⠉⠉⠙⠛⠿⣿⣿⠿⠛⠛⠛⠛⠿⢿⣿⣿⠿⠿⠇⠀⠀\n" - "⠀ ⢶⣤⣀⣀⣠⣴⠶⠛⠋⠙⠻⣦⣄⣀⣀⣠⣤⣴⠶⠶⣦⣄⣀⣀⣠⣤⣤⡶⠀\n" - " ⠀⠀⠈⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀\n" + "#################################################\n" ) + if "Windows" not in platform.system(): + print(" ⠀⠀⠀⠈⠉⠛⢷⣦⡀⠀⣀⣠⣤⠤⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀ ⠀⠀⠀⠀⠀⣀⣻⣿⣿⣿⣋⣀⡀⠀⠀⢀⣠⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀ ⠀⠀⣠⠾⠛⠛⢻⣿⣿⣿⠟⠛⠛⠓⠢⠀⠀⠉⢿⣿⣆⣀⣠⣤⣀⣀⠀⠀⠀\n" + "⠀ ⠀⠘⠁⠀⠀⣰⡿⠛⠿⠿⣧⡀⠀⠀⢀⣤⣤⣤⣼⣿⣿⣿⡿⠟⠋⠉⠉⠀⠀\n" + "⠀ ⠀⠀⠀⠀⠠⠋⠀⠀⠀⠀⠘⣷⡀⠀⠀⠀⠀⠹⣿⣿⣿⠟⠻⢶⣄⠀⠀⠀⠀\n" + "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣧⠀⠀⠀⠀⢠⡿⠁⠀⠀⠀⠀⠈⠀⠀⠀⠀\n" + "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⡄⠀⠀⢠⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣾⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀ ⣤⣤⣤⣤⣤⣤⡤⠄⠀⠀⣀⡀⢸⡇⢠⣤⣁⣀⠀⠀⠠⢤⣤⣤⣤⣤⣤⣤⠀\n" + "⠀⠀⠀⠀⠀ ⠀⣀⣤⣶⣾⣿⣿⣷⣤⣤⣾⣿⣿⣿⣿⣷⣶⣤⣀⠀⠀⠀⠀⠀⠀\n" + " ⠀⠀⠀⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣄⠀⠀⠀\n" + "⠀ ⠀⠼⠿⣿⣿⠿⠛⠉⠉⠉⠙⠛⠿⣿⣿⠿⠛⠛⠛⠛⠿⢿⣿⣿⠿⠿⠇⠀⠀\n" + "⠀ ⢶⣤⣀⣀⣠⣴⠶⠛⠋⠙⠻⣦⣄⣀⣀⣠⣤⣴⠶⠶⣦⣄⣀⣀⣠⣤⣤⡶⠀\n" + " ⠀⠀⠈⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀\n" + ) # Homogeneous case with equal island sizes (differences of +-1 possible due to load balancing). if island_sizes is None: @@ -122,11 +124,11 @@ def __init__( ) base_size = size // num_islands # Base number of workers of each island remainder = ( - size % num_islands + size % num_islands ) # Number of remaining workers to be distributed island_sizes = base_size * np.ones(num_islands, dtype=int) island_sizes[ - :remainder + :remainder ] += 1 # Distribute remaining workers equally for balanced load. # Heterogeneous case with user-defined island sizes. @@ -254,7 +256,7 @@ def _run( return self.propulator.summarize(top_n, debug) def evolve( - self, top_n: int = 3, logging_interval: int = 10, debug: int = 1 + self, top_n: int = 3, logging_interval: int = 10, debug: int = 1 ) -> List[Union[List[Individual], Individual]]: """ Run Propulate optimization. From c2139072528afa0126f1149e00add24d7c39a38b Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Tue, 5 Sep 2023 10:23:09 +0200 Subject: [PATCH 090/139] added black code styling --- propulate/islands.py | 63 ++++++++++--------- propulate/particle.py | 13 ++-- propulate/propagators/__init__.py | 21 ++++++- propulate/propagators/pso/__init__.py | 9 ++- propulate/propagators/pso/basic_pso.py | 45 +++++++++---- propulate/propagators/pso/constriction.py | 23 ++++--- propulate/propagators/pso/pso_compose.py | 1 + propulate/propagators/pso/pso_init_uniform.py | 35 +++++++---- propulate/propagators/pso/stateless_pso.py | 17 +++-- .../propagators/pso/velocity_clamping.py | 22 +++++-- propulate/utils.py | 36 +++++------ tutorials/pso_example.py | 34 +++++++--- 12 files changed, 209 insertions(+), 110 deletions(-) diff --git a/propulate/islands.py b/propulate/islands.py index ad0473f2..67b8e4aa 100644 --- a/propulate/islands.py +++ b/propulate/islands.py @@ -27,19 +27,19 @@ class Islands: """ def __init__( - self, - loss_fn: Callable, - propagator: Propagator, - rng: random.Random, - generations: int = 0, - num_islands: int = 1, - island_sizes: np.ndarray = None, - migration_topology: np.ndarray = None, - migration_probability: float = 0.0, - emigration_propagator: Type[Propagator] = SelectMin, - immigration_propagator: Type[Propagator] = SelectMax, - pollination: bool = True, - checkpoint_path: Union[str, Path] = Path("./"), + self, + loss_fn: Callable, + propagator: Propagator, + rng: random.Random, + generations: int = 0, + num_islands: int = 1, + island_sizes: np.ndarray = None, + migration_topology: np.ndarray = None, + migration_probability: float = 0.0, + emigration_propagator: Type[Propagator] = SelectMin, + immigration_propagator: Type[Propagator] = SelectMax, + pollination: bool = True, + checkpoint_path: Union[str, Path] = Path("./"), ) -> None: """ Initialize island model with given parameters. @@ -100,21 +100,22 @@ def __init__( "#################################################\n" ) if "Windows" not in platform.system(): - print(" ⠀⠀⠀⠈⠉⠛⢷⣦⡀⠀⣀⣠⣤⠤⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" - "⠀ ⠀⠀⠀⠀⠀⣀⣻⣿⣿⣿⣋⣀⡀⠀⠀⢀⣠⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" - "⠀ ⠀⠀⣠⠾⠛⠛⢻⣿⣿⣿⠟⠛⠛⠓⠢⠀⠀⠉⢿⣿⣆⣀⣠⣤⣀⣀⠀⠀⠀\n" - "⠀ ⠀⠘⠁⠀⠀⣰⡿⠛⠿⠿⣧⡀⠀⠀⢀⣤⣤⣤⣼⣿⣿⣿⡿⠟⠋⠉⠉⠀⠀\n" - "⠀ ⠀⠀⠀⠀⠠⠋⠀⠀⠀⠀⠘⣷⡀⠀⠀⠀⠀⠹⣿⣿⣿⠟⠻⢶⣄⠀⠀⠀⠀\n" - "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣧⠀⠀⠀⠀⢠⡿⠁⠀⠀⠀⠀⠈⠀⠀⠀⠀\n" - "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⡄⠀⠀⢠⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" - "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣾⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" - "⠀ ⣤⣤⣤⣤⣤⣤⡤⠄⠀⠀⣀⡀⢸⡇⢠⣤⣁⣀⠀⠀⠠⢤⣤⣤⣤⣤⣤⣤⠀\n" - "⠀⠀⠀⠀⠀ ⠀⣀⣤⣶⣾⣿⣿⣷⣤⣤⣾⣿⣿⣿⣿⣷⣶⣤⣀⠀⠀⠀⠀⠀⠀\n" - " ⠀⠀⠀⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣄⠀⠀⠀\n" - "⠀ ⠀⠼⠿⣿⣿⠿⠛⠉⠉⠉⠙⠛⠿⣿⣿⠿⠛⠛⠛⠛⠿⢿⣿⣿⠿⠿⠇⠀⠀\n" - "⠀ ⢶⣤⣀⣀⣠⣴⠶⠛⠋⠙⠻⣦⣄⣀⣀⣠⣤⣴⠶⠶⣦⣄⣀⣀⣠⣤⣤⡶⠀\n" - " ⠀⠀⠈⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀\n" - ) + print( + " ⠀⠀⠀⠈⠉⠛⢷⣦⡀⠀⣀⣠⣤⠤⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀ ⠀⠀⠀⠀⠀⣀⣻⣿⣿⣿⣋⣀⡀⠀⠀⢀⣠⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀ ⠀⠀⣠⠾⠛⠛⢻⣿⣿⣿⠟⠛⠛⠓⠢⠀⠀⠉⢿⣿⣆⣀⣠⣤⣀⣀⠀⠀⠀\n" + "⠀ ⠀⠘⠁⠀⠀⣰⡿⠛⠿⠿⣧⡀⠀⠀⢀⣤⣤⣤⣼⣿⣿⣿⡿⠟⠋⠉⠉⠀⠀\n" + "⠀ ⠀⠀⠀⠀⠠⠋⠀⠀⠀⠀⠘⣷⡀⠀⠀⠀⠀⠹⣿⣿⣿⠟⠻⢶⣄⠀⠀⠀⠀\n" + "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣧⠀⠀⠀⠀⢠⡿⠁⠀⠀⠀⠀⠈⠀⠀⠀⠀\n" + "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⡄⠀⠀⢠⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡇⠀⠀⣾⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\n" + "⠀ ⣤⣤⣤⣤⣤⣤⡤⠄⠀⠀⣀⡀⢸⡇⢠⣤⣁⣀⠀⠀⠠⢤⣤⣤⣤⣤⣤⣤⠀\n" + "⠀⠀⠀⠀⠀ ⠀⣀⣤⣶⣾⣿⣿⣷⣤⣤⣾⣿⣿⣿⣿⣷⣶⣤⣀⠀⠀⠀⠀⠀⠀\n" + " ⠀⠀⠀⣠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣄⠀⠀⠀\n" + "⠀ ⠀⠼⠿⣿⣿⠿⠛⠉⠉⠉⠙⠛⠿⣿⣿⠿⠛⠛⠛⠛⠿⢿⣿⣿⠿⠿⠇⠀⠀\n" + "⠀ ⢶⣤⣀⣀⣠⣴⠶⠛⠋⠙⠻⣦⣄⣀⣀⣠⣤⣴⠶⠶⣦⣄⣀⣀⣠⣤⣤⡶⠀\n" + " ⠀⠀⠈⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀\n" + ) # Homogeneous case with equal island sizes (differences of +-1 possible due to load balancing). if island_sizes is None: @@ -124,11 +125,11 @@ def __init__( ) base_size = size // num_islands # Base number of workers of each island remainder = ( - size % num_islands + size % num_islands ) # Number of remaining workers to be distributed island_sizes = base_size * np.ones(num_islands, dtype=int) island_sizes[ - :remainder + :remainder ] += 1 # Distribute remaining workers equally for balanced load. # Heterogeneous case with user-defined island sizes. @@ -256,7 +257,7 @@ def _run( return self.propulator.summarize(top_n, debug) def evolve( - self, top_n: int = 3, logging_interval: int = 10, debug: int = 1 + self, top_n: int = 3, logging_interval: int = 10, debug: int = 1 ) -> List[Union[List[Individual], Individual]]: """ Run Propulate optimization. diff --git a/propulate/particle.py b/propulate/particle.py index 20dee80b..48ac86d1 100644 --- a/propulate/particle.py +++ b/propulate/particle.py @@ -16,12 +16,13 @@ class Particle(Individual): matches their dict contents and vice versa. """ - def __init__(self, - position: np.ndarray = None, - velocity: np.ndarray = None, - iteration: int = 0, - rank: int = None - ): + def __init__( + self, + position: np.ndarray = None, + velocity: np.ndarray = None, + iteration: int = 0, + rank: int = None, + ): super().__init__(generation=iteration, rank=rank) if position is not None and velocity is not None: assert position.shape == velocity.shape diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py index e3f0926c..6360ccde 100644 --- a/propulate/propagators/__init__.py +++ b/propulate/propagators/__init__.py @@ -2,9 +2,24 @@ This package holds all Propagator subclasses including the Propagator itself. """ -__all__ = ["Propagator", "Stochastic", "Conditional", "Compose", "PointMutation", "RandomPointMutation", - "IntervalMutationNormal", "MateUniform", "MateMultiple", "MateSigmoid", "SelectMin", "SelectMax", - "SelectUniform", "InitUniform", "pso", "StatelessPSO"] +__all__ = [ + "Propagator", + "Stochastic", + "Conditional", + "Compose", + "PointMutation", + "RandomPointMutation", + "IntervalMutationNormal", + "MateUniform", + "MateMultiple", + "MateSigmoid", + "SelectMin", + "SelectMax", + "SelectUniform", + "InitUniform", + "pso", + "StatelessPSO", +] from propulate.propagators.propagators import * import pso diff --git a/propulate/propagators/pso/__init__.py b/propulate/propagators/pso/__init__.py index e6f05f75..d4400351 100644 --- a/propulate/propagators/pso/__init__.py +++ b/propulate/propagators/pso/__init__.py @@ -1,8 +1,13 @@ -__all__ = ["PSOInitUniform", "BasicPSO", "VelocityClamping", "Constriction", "Canonical"] +__all__ = [ + "PSOInitUniform", + "BasicPSO", + "VelocityClamping", + "Constriction", + "Canonical", +] from pso_init_uniform import PSOInitUniform from basic_pso import BasicPSO from velocity_clamping import VelocityClamping from constriction import Constriction from canonical import Canonical - diff --git a/propulate/propagators/pso/basic_pso.py b/propulate/propagators/pso/basic_pso.py index ba23ea3c..6170e4e1 100644 --- a/propulate/propagators/pso/basic_pso.py +++ b/propulate/propagators/pso/basic_pso.py @@ -33,8 +33,15 @@ class BasicPSO(Propagator): This propagator works on Particle-class objects. """ - def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, - limits: Dict[str, Tuple[float, float]], rng: Random): + def __init__( + self, + w_k: float, + c_cognitive: float, + c_social: float, + rank: int, + limits: Dict[str, Tuple[float, float]], + rng: Random, + ): """ Class constructor. :param w_k: The learning rate ... somehow @@ -51,19 +58,25 @@ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, self.rank = rank self.limits = limits self.rng = rng - self.laa: np.ndarray = np.array(list(limits.values())).T # laa - "limits as array" + self.laa: np.ndarray = np.array( + list(limits.values()) + ).T # laa - "limits as array" def __call__(self, particles: List[Particle]) -> Particle: old_p, p_best, g_best = self._prepare_data(particles) - new_velocity: np.ndarray = self.w_k * old_p.velocity \ - + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) \ - + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) + new_velocity: np.ndarray = ( + self.w_k * old_p.velocity + + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) + ) new_position: np.ndarray = old_p.position + new_velocity return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) - def _prepare_data(self, particles: List[Particle]) -> Tuple[Particle, Particle, Particle]: + def _prepare_data( + self, particles: List[Particle] + ) -> Tuple[Particle, Particle, Particle]: """ Returns the following particles in this very order: 1. old_p: the current particle to be updated now @@ -73,25 +86,35 @@ def _prepare_data(self, particles: List[Particle]) -> Tuple[Particle, Particle, if len(particles) < self.offspring: raise ValueError("Not enough Particles") - own_p = [x for x in particles if (isinstance(x, Particle) and x.g_rank == self.rank) or x.rank == self.rank] + own_p = [ + x + for x in particles + if (isinstance(x, Particle) and x.g_rank == self.rank) + or x.rank == self.rank + ] if len(own_p) > 0: old_p = max(own_p, key=lambda p: p.generation) else: victim = max(particles, key=lambda p: p.generation) - old_p = self._make_new_particle(victim.position, victim.velocity, victim.generation) + old_p = self._make_new_particle( + victim.position, victim.velocity, victim.generation + ) if not isinstance(old_p, Particle): old_p = make_particle(old_p) print( f"R{self.rank}, Iteration#{old_p.generation}: Type Error. " - f"Converted Individual to Particle. Continuing.") + f"Converted Individual to Particle. Continuing." + ) g_best = min(particles, key=lambda p: p.loss) p_best = min(own_p, key=lambda p: p.loss) return old_p, p_best, g_best - def _make_new_particle(self, position: np.ndarray, velocity: np.ndarray, generation: int): + def _make_new_particle( + self, position: np.ndarray, velocity: np.ndarray, generation: int + ): """ Takes the necessary data to create a new Particle with the position dict set to the correct values. :return: The newly created Particle object diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py index c630d102..0d69ba73 100644 --- a/propulate/propagators/pso/constriction.py +++ b/propulate/propagators/pso/constriction.py @@ -19,12 +19,15 @@ class Constriction(BasicPSO): This propagator runs on Particle-class objects. """ - def __init__(self, - c_cognitive: float, - c_social: float, - rank: int, - limits: Dict[str, Tuple[float, float]], - rng: Random): + + def __init__( + self, + c_cognitive: float, + c_social: float, + rank: int, + limits: Dict[str, Tuple[float, float]], + rng: Random, + ): """ Class constructor. Important note: `c_cognitive` and `c_social` have to sum up to something greater than 4! @@ -42,9 +45,11 @@ def __init__(self, def __call__(self, particles: List[Particle]) -> Particle: old_p, p_best, g_best = self._prepare_data(particles) - new_velocity = self.w_k * (old_p.velocity - + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) - + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position)) + new_velocity = self.w_k * ( + old_p.velocity + + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) + ) new_position = old_p.position + new_velocity return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) diff --git a/propulate/propagators/pso/pso_compose.py b/propulate/propagators/pso/pso_compose.py index 55a90faa..1fa14020 100644 --- a/propulate/propagators/pso/pso_compose.py +++ b/propulate/propagators/pso/pso_compose.py @@ -10,6 +10,7 @@ class PSOCompose(Compose): This class is the Particle-using counterpart to the Compose propagator. It does basically exact the same things. For further reference, please refer to the standard Compose propagator. """ + def __call__(self, particles: List[Particle]) -> Particle: """ Returns the first element of the list of particles returned by the last Propagator in the list diff --git a/propulate/propagators/pso/pso_init_uniform.py b/propulate/propagators/pso/pso_init_uniform.py index 3202aa86..16d8a202 100644 --- a/propulate/propagators/pso/pso_init_uniform.py +++ b/propulate/propagators/pso/pso_init_uniform.py @@ -20,8 +20,16 @@ class PSOInitUniform(Stochastic): For more information, please have a look there. """ - def __init__(self, limits: Dict[str, Tuple[float, float]], parents=0, probability=1.0, rng: Random = None, *, - v_init_limit: Union[float, np.ndarray] = 0.1, rank: int): + def __init__( + self, + limits: Dict[str, Tuple[float, float]], + parents=0, + probability=1.0, + rng: Random = None, + *, + v_init_limit: Union[float, np.ndarray] = 0.1, + rank: int + ): """ Constructor of PSOInitUniform class. @@ -66,26 +74,29 @@ def __call__(self, particles: List[Individual]) -> Particle: ind : propulate.ap_pso.Particle one particle object """ - if self.rng.random() < self.probability or len(particles) == 0: # Apply only with specified `probability`. - - position = np.array([self.rng.uniform(*self.laa[..., i]) for i in range(self.laa.shape[-1])]) + if ( + self.rng.random() < self.probability or len(particles) == 0 + ): # Apply only with specified `probability`. + position = np.array( + [self.rng.uniform(*self.laa[..., i]) for i in range(self.laa.shape[-1])] + ) velocity = np.array( [ - self.rng.uniform( - *( - self.v_limits * self.laa - )[..., i] - ) + self.rng.uniform(*(self.v_limits * self.laa)[..., i]) for i in range(self.laa.shape[-1]) ] ) - particle = Particle(position, velocity, rank=self.rank) # Instantiate new particle. + particle = Particle( + position, velocity, rank=self.rank + ) # Instantiate new particle. for index, limit in enumerate(self.limits): # Since Py 3.7, iterating over dicts is stable, so we can do the following. - if type(self.limits[limit][0]) != float: # Check search space for validity + if ( + type(self.limits[limit][0]) != float + ): # Check search space for validity raise TypeError("PSO only works on continuous search spaces!") # Randomly sample from specified limits for each trait. diff --git a/propulate/propagators/pso/stateless_pso.py b/propulate/propagators/pso/stateless_pso.py index 2233380f..3bc4c3fd 100644 --- a/propulate/propagators/pso/stateless_pso.py +++ b/propulate/propagators/pso/stateless_pso.py @@ -18,8 +18,15 @@ class StatelessPSO(Propagator): This propagator works on Propulate's Individual-class objects. """ - def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, - limits: Dict[str, Tuple[float, float]], rng: Random): + def __init__( + self, + w_k: float, + c_cognitive: float, + c_social: float, + rank: int, + limits: Dict[str, Tuple[float, float]], + rng: Random, + ): """ :param w_k: The learning rate ... somehow - currently without effect @@ -50,6 +57,8 @@ def __call__(self, particles: List[Individual]) -> Individual: new_p = Individual(generation=old_p.generation + 1) for k in self.limits: new_p[k] = self.c_cognitive * self.rng.uniform(*self.limits[k]) * ( - p_best[k] - old_p[k]) + self.c_social * self.rng.uniform(*self.limits[k]) * ( - g_best[k] - old_p[k]) + p_best[k] - old_p[k] + ) + self.c_social * self.rng.uniform(*self.limits[k]) * ( + g_best[k] - old_p[k] + ) return new_p diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py index ea60d2b4..84dce5c5 100644 --- a/propulate/propagators/pso/velocity_clamping.py +++ b/propulate/propagators/pso/velocity_clamping.py @@ -19,8 +19,17 @@ class VelocityClamping(BasicPSO): Based on these values, the velocities of the particles are cut down to a reasonable value. """ - def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, - limits: Dict[str, Tuple[float, float]], rng: Random, v_limits: Union[float, np.ndarray]): + + def __init__( + self, + w_k: float, + c_cognitive: float, + c_social: float, + rank: int, + limits: Dict[str, Tuple[float, float]], + rng: Random, + v_limits: Union[float, np.ndarray], + ): """ Class constructor. :param w_k: The particle's inertia factor @@ -42,10 +51,11 @@ def __init__(self, w_k: float, c_cognitive: float, c_social: float, rank: int, def __call__(self, particles: List[Particle]) -> Particle: old_p, p_best, g_best = self._prepare_data(particles) - new_velocity: np.ndarray = (self.w_k * old_p.velocity - + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) - + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) - ).clip(*self.v_cap) + new_velocity: np.ndarray = ( + self.w_k * old_p.velocity + + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) + ).clip(*self.v_cap) new_position: np.ndarray = old_p.position + new_velocity return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) diff --git a/propulate/utils.py b/propulate/utils.py index 9c440f41..6a9ae41b 100644 --- a/propulate/utils.py +++ b/propulate/utils.py @@ -26,17 +26,17 @@ def get_default_propagator( - pop_size: int, - limits: Union[ - Dict[str, Tuple[float, float]], - Dict[str, Tuple[int, int]], - Dict[str, Tuple[str, ...]], - ], - mate_prob: float, - mut_prob: float, - random_prob: float, - sigma_factor: float = 0.05, - rng: random.Random = None, + pop_size: int, + limits: Union[ + Dict[str, Tuple[float, float]], + Dict[str, Tuple[int, int]], + Dict[str, Tuple[str, ...]], + ], + mate_prob: float, + mut_prob: float, + random_prob: float, + sigma_factor: float = 0.05, + rng: random.Random = None, ) -> Propagator: """ Get Propulate's default evolutionary optimization propagator. @@ -64,7 +64,7 @@ def get_default_propagator( A basic evolutionary optimization propagator. """ if any( - isinstance(limits[x][0], float) for x in limits + isinstance(limits[x][0], float) for x in limits ): # Check for existence of at least one continuous trait. propagator = Compose( [ # Compose propagator out of basic evolutionary operators with Compose(...). @@ -98,11 +98,11 @@ def get_default_propagator( def set_logger_config( - level: int = logging.INFO, - log_file: Union[str, Path] = None, - log_to_stdout: bool = True, - log_rank: bool = False, - colors: bool = True, + level: int = logging.INFO, + log_file: Union[str, Path] = None, + log_to_stdout: bool = True, + log_rank: bool = False, + colors: bool = True, ) -> None: """ Set up the logger. Should only need to be done once. @@ -134,7 +134,7 @@ def set_logger_config( if colors: formatter = colorlog.ColoredFormatter( fmt=f"{rank}[%(cyan)s%(asctime)s%(reset)s][%(blue)s%(name)s%(reset)s]" - f"[%(log_color)s%(levelname)s%(reset)s] - %(message)s", + f"[%(log_color)s%(levelname)s%(reset)s] - %(message)s", datefmt=None, reset=True, log_colors={ diff --git a/tutorials/pso_example.py b/tutorials/pso_example.py index 54772562..a6ea9d54 100644 --- a/tutorials/pso_example.py +++ b/tutorials/pso_example.py @@ -7,13 +7,21 @@ from propulate import Islands from propulate.propagators import Conditional, StatelessPSO from function_benchmark import get_function_search_space -from propulate.propagators.pso import BasicPSO, VelocityClamping, Constriction, Canonical, PSOInitUniform +from propulate.propagators.pso import ( + BasicPSO, + VelocityClamping, + Constriction, + Canonical, + PSOInitUniform, +) ############ # SETTINGS # ############ -function_name = sys.argv[1] # Get function to optimize from command-line. Possible Options: See function_benchmark.py +function_name = sys.argv[ + 1 +] # Get function to optimize from command-line. Possible Options: See function_benchmark.py NUM_GENERATIONS: int = int(sys.argv[2]) # Set number of generations. POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. @@ -27,16 +35,26 @@ pso_propagator = [ StatelessPSO(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng), - BasicPSO(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng), - VelocityClamping(0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6), + VelocityClamping( + 0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6 + ), Constriction(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng), - Canonical(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng) - ][1] # Please choose with this index, which Propagator to use in the optimisation process. + Canonical(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng), + ][ + 1 + ] # Please choose with this index, which Propagator to use in the optimisation process. init = PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) propagator = Conditional(POP_SIZE, pso_propagator, init) - islands = Islands(function, propagator, rng, generations=NUM_GENERATIONS, checkpoint_path='./checkpoints/', - migration_probability=0, pollination=False) + islands = Islands( + function, + propagator, + rng, + generations=NUM_GENERATIONS, + checkpoint_path="./checkpoints/", + migration_probability=0, + pollination=False, + ) islands.evolve(debug=0) From bb7354d7a039d74320566324adc84ce445ec3d8f Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 5 Sep 2023 22:53:25 +0200 Subject: [PATCH 091/139] Update propulate/particle.py Co-authored-by: Marie Weiel <48559085+mcw92@users.noreply.github.com> --- propulate/particle.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/propulate/particle.py b/propulate/particle.py index 48ac86d1..8cbb99a9 100644 --- a/propulate/particle.py +++ b/propulate/particle.py @@ -8,12 +8,9 @@ class Particle(Individual): """ - Extension of Individual class with additional properties necessary for full PSO. - It also comes along with a numpy array to store positional information in. - As Propulate rather relies on Individuals being dicts and using this property to work with, it is just for future use. - - Please keep in mind, that users of this class are responsible to ensure, that a Particle's position always - matches their dict contents and vice versa. + Child class of ``Individual`` with additional properties required for PSO, i.e., an array-type velocity field and a (redundant) array-type position field. + Note that Propulate relies on ``Individual``s being ``dict``s. + When defining new propagators, users of the ``Particle`` class thus need to ensure that a ``Particle``'s position always matches its dict contents and vice versa. """ def __init__( From 4035ef24a30ea0d6618b4f0356391f0ee6f98c40 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 5 Sep 2023 23:21:00 +0200 Subject: [PATCH 092/139] Update propulate/propagators/pso/pso_init_uniform.py Co-authored-by: Marie Weiel <48559085+mcw92@users.noreply.github.com> --- propulate/propagators/pso/pso_init_uniform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/propulate/propagators/pso/pso_init_uniform.py b/propulate/propagators/pso/pso_init_uniform.py index 16d8a202..0d72d154 100644 --- a/propulate/propagators/pso/pso_init_uniform.py +++ b/propulate/propagators/pso/pso_init_uniform.py @@ -75,7 +75,7 @@ def __call__(self, particles: List[Individual]) -> Particle: one particle object """ if ( - self.rng.random() < self.probability or len(particles) == 0 + len(particles) == 0 or self.rng.random() < self.probability ): # Apply only with specified `probability`. position = np.array( [self.rng.uniform(*self.laa[..., i]) for i in range(self.laa.shape[-1])] From 22ca603bdfea356bb818145f7ccad30df3cba2c2 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 5 Sep 2023 23:21:28 +0200 Subject: [PATCH 093/139] Rewrite the imports. There were some ugly stars. And the import wasn't relative. --- propulate/propagators/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py index 6360ccde..12b23d23 100644 --- a/propulate/propagators/__init__.py +++ b/propulate/propagators/__init__.py @@ -21,6 +21,7 @@ "StatelessPSO", ] -from propulate.propagators.propagators import * +from propagators import Propagator, Stochastic, Conditional, Compose, PointMutation, RandomPointMutation, \ + IntervalMutationNormal, MateUniform, MateMultiple, MateSigmoid, SelectMin, SelectMax, SelectUniform, InitUniform import pso from pso.stateless_pso import StatelessPSO From f2813cd55318b1c6f1abe6c1292118654822e607 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 5 Sep 2023 23:26:38 +0200 Subject: [PATCH 094/139] Shortened and clarified DocString on Basic PSO propagator. Co-authored-by: Marie Weiel <48559085+mcw92@users.noreply.github.com> --- propulate/propagators/pso/basic_pso.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/propulate/propagators/pso/basic_pso.py b/propulate/propagators/pso/basic_pso.py index 6170e4e1..30e8e6e1 100644 --- a/propulate/propagators/pso/basic_pso.py +++ b/propulate/propagators/pso/basic_pso.py @@ -20,17 +20,9 @@ class BasicPSO(Propagator): This is done with the help of some randomness. - As this propagator is very basic in all manners, it can only feature float-typed search domains. - Please keep this in mind when feeding the propagator with limits. - Else, it might happen that warnings occur. - - This propagator also serves as the foundation of all other pso propagators and supplies - them with protected methods that help in the update process. - - If you want to implement further pso propagators, please do your best to - derive them from this propagator or from one that is derived from this. - - This propagator works on Particle-class objects. + This basic PSO propagator can only explore real-valued search spaces, i.e., continuous parameters. + It works on ``Particle`` objects and serves as the foundation of all other PSO propagators. + Further PSO propagators should be derived from this propagator or from one that is derived from this. """ def __init__( From fc8b8f9e7a79e8674c11e55f08e8aaada445dd80 Mon Sep 17 00:00:00 2001 From: Morridin Date: Tue, 5 Sep 2023 23:36:39 +0200 Subject: [PATCH 095/139] Update class docstring of canonical pso propgator. Co-authored-by: Marie Weiel <48559085+mcw92@users.noreply.github.com> --- propulate/propagators/pso/canonical.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/propulate/propagators/pso/canonical.py b/propulate/propagators/pso/canonical.py index 6e4a5b7b..f65c9929 100644 --- a/propulate/propagators/pso/canonical.py +++ b/propulate/propagators/pso/canonical.py @@ -8,12 +8,10 @@ class Canonical(Constriction): """ - This propagator subclass features a combination of Constriction PSO and VelocityClamping PSO. + This propagator subclass features a combination of constriction and velocity clamping. - The velocity clamping is made with a clamping factor of 1, - the constriction is done as on the parental Constriction propagator. - - For information on the method parameters, please refer to the Constriction propagator. + The velocity clamping uses a clamping factor of 1, + the constriction is done as in the parental ``Constriction`` propagator. """ def __init__(self, c_cognitive, c_social, rank, limits, rng): From fcc48539fd583a5dc6703cc855886b940a21b7c7 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 11:49:16 +0200 Subject: [PATCH 096/139] Renamed the pso propagators to a consistent scheme. --- propulate/propagators/__init__.py | 4 +--- propulate/propagators/pso/__init__.py | 8 ++++---- propulate/propagators/pso/{basic_pso.py => basic.py} | 2 +- .../propagators/pso/{pso_compose.py => compose.py} | 4 ++-- propulate/propagators/pso/constriction.py | 4 ++-- .../pso/{pso_init_uniform.py => init_uniform.py} | 8 ++++---- .../pso/{stateless_pso.py => stateless.py} | 2 +- propulate/propagators/pso/velocity_clamping.py | 4 ++-- tutorials/pso_example.py | 12 ++++++------ 9 files changed, 23 insertions(+), 25 deletions(-) rename propulate/propagators/pso/{basic_pso.py => basic.py} (99%) rename propulate/propagators/pso/{pso_compose.py => compose.py} (92%) rename propulate/propagators/pso/{pso_init_uniform.py => init_uniform.py} (95%) rename propulate/propagators/pso/{stateless_pso.py => stateless.py} (98%) diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py index 12b23d23..201137b4 100644 --- a/propulate/propagators/__init__.py +++ b/propulate/propagators/__init__.py @@ -18,10 +18,8 @@ "SelectUniform", "InitUniform", "pso", - "StatelessPSO", ] from propagators import Propagator, Stochastic, Conditional, Compose, PointMutation, RandomPointMutation, \ IntervalMutationNormal, MateUniform, MateMultiple, MateSigmoid, SelectMin, SelectMax, SelectUniform, InitUniform -import pso -from pso.stateless_pso import StatelessPSO +import pso \ No newline at end of file diff --git a/propulate/propagators/pso/__init__.py b/propulate/propagators/pso/__init__.py index d4400351..80359ac5 100644 --- a/propulate/propagators/pso/__init__.py +++ b/propulate/propagators/pso/__init__.py @@ -1,13 +1,13 @@ __all__ = [ - "PSOInitUniform", - "BasicPSO", + "InitUniform", + "Basic", "VelocityClamping", "Constriction", "Canonical", ] -from pso_init_uniform import PSOInitUniform -from basic_pso import BasicPSO +from init_uniform import InitUniform +from basic import Basic from velocity_clamping import VelocityClamping from constriction import Constriction from canonical import Canonical diff --git a/propulate/propagators/pso/basic_pso.py b/propulate/propagators/pso/basic.py similarity index 99% rename from propulate/propagators/pso/basic_pso.py rename to propulate/propagators/pso/basic.py index 30e8e6e1..0b25bce6 100644 --- a/propulate/propagators/pso/basic_pso.py +++ b/propulate/propagators/pso/basic.py @@ -11,7 +11,7 @@ from propulate.utils import make_particle -class BasicPSO(Propagator): +class Basic(Propagator): """ This propagator implements the most basic PSO variant one possibly could think of. diff --git a/propulate/propagators/pso/pso_compose.py b/propulate/propagators/pso/compose.py similarity index 92% rename from propulate/propagators/pso/pso_compose.py rename to propulate/propagators/pso/compose.py index 1fa14020..0e55ceef 100644 --- a/propulate/propagators/pso/pso_compose.py +++ b/propulate/propagators/pso/compose.py @@ -2,10 +2,10 @@ from propulate.particle import Particle from propulate.population import Individual -from propulate.propagators import Compose +from propulate import propagators -class PSOCompose(Compose): +class Compose(propagators.Compose): """ This class is the Particle-using counterpart to the Compose propagator. It does basically exact the same things. For further reference, please refer to the standard Compose propagator. diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py index 0d69ba73..4dde31ce 100644 --- a/propulate/propagators/pso/constriction.py +++ b/propulate/propagators/pso/constriction.py @@ -6,11 +6,11 @@ import numpy as np -from basic_pso import BasicPSO +from basic import Basic from propulate.particle import Particle -class Constriction(BasicPSO): +class Constriction(Basic): """ This propagator subclass features Constriction PSO as proposed by Clerc and Kennedy in 2002. diff --git a/propulate/propagators/pso/pso_init_uniform.py b/propulate/propagators/pso/init_uniform.py similarity index 95% rename from propulate/propagators/pso/pso_init_uniform.py rename to propulate/propagators/pso/init_uniform.py index 0d72d154..32f75082 100644 --- a/propulate/propagators/pso/pso_init_uniform.py +++ b/propulate/propagators/pso/init_uniform.py @@ -12,7 +12,7 @@ from propulate.utils import make_particle -class PSOInitUniform(Stochastic): +class InitUniform(Stochastic): """ Initialize individuals by uniformly sampling specified limits for each trait. @@ -31,7 +31,7 @@ def __init__( rank: int ): """ - Constructor of PSOInitUniform class. + Constructor of InitUniform class. In case of parents > 0 and probability < 1., call returns input individual without change. @@ -94,8 +94,8 @@ def __call__(self, particles: List[Individual]) -> Particle: for index, limit in enumerate(self.limits): # Since Py 3.7, iterating over dicts is stable, so we can do the following. - if ( - type(self.limits[limit][0]) != float + if not isinstance( + self.limits[limit][0], float ): # Check search space for validity raise TypeError("PSO only works on continuous search spaces!") diff --git a/propulate/propagators/pso/stateless_pso.py b/propulate/propagators/pso/stateless.py similarity index 98% rename from propulate/propagators/pso/stateless_pso.py rename to propulate/propagators/pso/stateless.py index 3bc4c3fd..024cc496 100644 --- a/propulate/propagators/pso/stateless_pso.py +++ b/propulate/propagators/pso/stateless.py @@ -9,7 +9,7 @@ from propulate.propagators import Propagator -class StatelessPSO(Propagator): +class Stateless(Propagator): """ The first draft of a pso propagator. It uses the infrastructure brought to you by vanilla Propulate and nothing more. diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py index 84dce5c5..a3474b82 100644 --- a/propulate/propagators/pso/velocity_clamping.py +++ b/propulate/propagators/pso/velocity_clamping.py @@ -7,10 +7,10 @@ import numpy as np from propulate.particle import Particle -from basic_pso import BasicPSO +from basic import Basic -class VelocityClamping(BasicPSO): +class VelocityClamping(Basic): """ This propagator implements the Velocity Clamping pso variant. diff --git a/tutorials/pso_example.py b/tutorials/pso_example.py index a6ea9d54..8a547dae 100644 --- a/tutorials/pso_example.py +++ b/tutorials/pso_example.py @@ -5,14 +5,14 @@ from mpi4py import MPI from propulate import Islands -from propulate.propagators import Conditional, StatelessPSO +from propulate.propagators import Conditional, Stateless from function_benchmark import get_function_search_space from propulate.propagators.pso import ( - BasicPSO, + Basic, VelocityClamping, Constriction, Canonical, - PSOInitUniform, + InitUniform, ) ############ @@ -34,8 +34,8 @@ rng = random.Random(MPI.COMM_WORLD.rank) pso_propagator = [ - StatelessPSO(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng), - BasicPSO(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng), + Stateless(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng), + Basic(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng), VelocityClamping( 0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6 ), @@ -45,7 +45,7 @@ 1 ] # Please choose with this index, which Propagator to use in the optimisation process. - init = PSOInitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) + init = InitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) propagator = Conditional(POP_SIZE, pso_propagator, init) islands = Islands( From b9f4b0d4d2bb6f732484ddb75f2afa2914dad895 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 11:52:45 +0200 Subject: [PATCH 097/139] Moved the population member classes to an own package. --- propulate/population/__init__.py | 6 ++++++ propulate/{population.py => population/individual.py} | 0 propulate/{ => population}/particle.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 propulate/population/__init__.py rename propulate/{population.py => population/individual.py} (100%) rename propulate/{ => population}/particle.py (96%) diff --git a/propulate/population/__init__.py b/propulate/population/__init__.py new file mode 100644 index 00000000..06ff064d --- /dev/null +++ b/propulate/population/__init__.py @@ -0,0 +1,6 @@ +""" +This package bundles all classes that are used as individuals of Propulate's optimization population. +""" +__all__ = ["Individual", "Particle"] +from individual import Individual +from particle import Particle diff --git a/propulate/population.py b/propulate/population/individual.py similarity index 100% rename from propulate/population.py rename to propulate/population/individual.py diff --git a/propulate/particle.py b/propulate/population/particle.py similarity index 96% rename from propulate/particle.py rename to propulate/population/particle.py index 8cbb99a9..ae78a47b 100644 --- a/propulate/particle.py +++ b/propulate/population/particle.py @@ -3,7 +3,7 @@ """ import numpy as np -from propulate.population import Individual +from propulate.individual import Individual class Particle(Individual): From 142951d4a6273104bc58af032ef52c39a475aae1 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 11:53:13 +0200 Subject: [PATCH 098/139] Automated code reformatting to black style. --- propulate/islands.py | 2 +- propulate/pollinator.py | 9 ++-- propulate/propagators/propagators.py | 52 +++++++++++------------ propulate/propagators/pso/compose.py | 3 +- propulate/propagators/pso/init_uniform.py | 4 +- propulate/propagators/pso/stateless.py | 3 +- propulate/propulator.py | 17 ++++---- propulate/utils.py | 9 ++-- 8 files changed, 49 insertions(+), 50 deletions(-) diff --git a/propulate/islands.py b/propulate/islands.py index 67b8e4aa..dab60bc4 100644 --- a/propulate/islands.py +++ b/propulate/islands.py @@ -7,9 +7,9 @@ import numpy as np from mpi4py import MPI +from .individual import Individual from .migrator import Migrator from .pollinator import Pollinator -from .population import Individual from .propagators import Propagator, SelectMin, SelectMax log = logging.getLogger(__name__) # Get logger instance. diff --git a/propulate/pollinator.py b/propulate/pollinator.py index cce8df88..8a224822 100644 --- a/propulate/pollinator.py +++ b/propulate/pollinator.py @@ -2,18 +2,17 @@ import logging import random import time -from typing import Callable, Union, Tuple, List, Type from pathlib import Path +from typing import Callable, Union, Tuple, List, Type import numpy as np from mpi4py import MPI from ._globals import MIGRATION_TAG, SYNCHRONIZATION_TAG +from .individual import Individual from .propagators import Propagator, SelectMin, SelectMax -from .population import Individual from .propulator import Propulator - log = logging.getLogger(__name__) @@ -327,9 +326,9 @@ def _check_for_duplicates( Returns ------- - list[list[propulate.population.Individual | int]] + list[list[propulate.individual.Individual | int]] individuals and their occurrences - list[propulate.population.Individual] + list[propulate.individual.Individual] unique individuals in population """ if active: diff --git a/propulate/propagators/propagators.py b/propulate/propagators/propagators.py index 6947d8bd..faa39090 100644 --- a/propulate/propagators/propagators.py +++ b/propulate/propagators/propagators.py @@ -5,7 +5,7 @@ import numpy as np from abc import ABC, abstractmethod -from ..population import Individual +from ..individual import Individual def _check_compatible(out1: int, in2: int) -> bool: @@ -66,7 +66,7 @@ def __call__(self, inds: List[Individual]) -> Union[List[Individual], Individual Parameters ---------- - inds: list[propulate.population.Individual] + inds: list[propulate.individual.Individual] input individuals the propagator is applied to Returns @@ -166,12 +166,12 @@ def __call__(self, inds: List[Individual]) -> List[Individual]: Parameters ---------- - inds: list[propulate.population.Individual] + inds: list[propulate.individual.Individual] input individuals the propagator is applied to Returns ------- - list[propulate.population.Individual] + list[propulate.individual.Individual] output individuals returned by the conditional propagator """ if ( @@ -224,12 +224,12 @@ def __call__(self, inds: List[Individual]) -> List[Individual]: Parameters ---------- - inds: list[propulate.population.Individual] + inds: list[propulate.individual.Individual] input individuals the propagator is applied to Returns ------- - list[propulate.population.Individual] + list[propulate.individual.Individual] output individuals after application of propagator """ for p in self.propagators: @@ -286,12 +286,12 @@ def __call__(self, ind: Individual) -> Individual: Parameters ---------- - ind: propulate.population.Individual + ind: propulate.individual.Individual individual the propagator is applied to Returns ------- - propulate.population.Individual + propulate.individual.Individual possibly point-mutated individual after application of propagator """ if ( @@ -384,12 +384,12 @@ def __call__(self, ind: Individual) -> Individual: Parameters ---------- - ind: propulate.population.Individual + ind: propulate.individual.Individual individual the propagator is applied to Returns ------- - propulate.population.Individual + propulate.individual.Individual possibly point-mutated individual after application of propagator """ if ( @@ -471,12 +471,12 @@ def __call__(self, ind: Individual) -> Individual: Parameters ---------- - ind: propulate.population.Individual + ind: propulate.individual.Individual input individual the propagator is applied to Returns ------- - propulate.population.Individual + propulate.individual.Individual possibly interval-mutated output individual after application of propagator """ if ( @@ -549,12 +549,12 @@ def __call__(self, inds: List[Individual]) -> Individual: Parameters ---------- - inds: List[propulate.population.Individual] + inds: List[propulate.individual.Individual] individuals the propagator is applied to Returns ------- - propulate.population.Individual + propulate.individual.Individual possibly cross-bred individual after application of propagator """ ind = copy.deepcopy(inds[0]) # Consider 1st parent. @@ -599,12 +599,12 @@ def __call__(self, inds: List[Individual]) -> Individual: Parameters ---------- - inds: list[propulate.population.Individual] + inds: list[propulate.individual.Individual] individuals the propagator is applied to Returns ------- - propulate.population.Individual + propulate.individual.Individual possibly cross-bred individual after application of propagator """ ind = copy.deepcopy(inds[0]) # Consider 1st parent. @@ -655,12 +655,12 @@ def __call__(self, inds: List[Individual]) -> Individual: Parameters ---------- - inds: list[propulate.population.Individual] + inds: list[propulate.individual.Individual] individuals the propagator is applied to Returns ------- - propulate.population.Individual + propulate.individual.Individual possibly cross-bred individual after application of propagator """ ind = copy.deepcopy(inds[0]) # Consider 1st parent. @@ -708,12 +708,12 @@ def __call__(self, inds: List[Individual]) -> List[Individual]: Parameters ---------- - inds: list[propulate.population.Individual] + inds: list[propulate.individual.Individual] input individuals the propagator is applied to Returns ------- - list[propulate.population.Individual] + list[propulate.individual.Individual] selected output individuals after application of the propagator Raises @@ -757,12 +757,12 @@ def __call__(self, inds: List[Individual]) -> List[Individual]: Parameters ---------- - inds: list[propulate.population.Individual] + inds: list[propulate.individual.Individual] individuals the propagator is applied to Returns ------- - list[propulate.population.Individual] + list[propulate.individual.Individual] selected individuals after application of the propagator Raises @@ -803,12 +803,12 @@ def __call__(self, inds: List[Individual]) -> List[Individual]: Parameters ---------- - inds: list[propulate.population.Individual] + inds: list[propulate.individual.Individual] individuals the propagator is applied to Returns ------- - list[propulate.population.Individual] + list[propulate.individual.Individual] selected individuals after application of propagator Raises @@ -864,12 +864,12 @@ def __call__(self, *inds: Individual) -> Individual: Parameters ---------- - inds: propulate.population.Individual + inds: propulate.individual.Individual individuals the propagator is applied to Returns ------- - propulate.population.Individual + propulate.individual.Individual output individual after application of propagator Raises diff --git a/propulate/propagators/pso/compose.py b/propulate/propagators/pso/compose.py index 0e55ceef..01b17008 100644 --- a/propulate/propagators/pso/compose.py +++ b/propulate/propagators/pso/compose.py @@ -1,7 +1,8 @@ from typing import List +from propulate.individual import Individual from propulate.particle import Particle -from propulate.population import Individual + from propulate import propagators diff --git a/propulate/propagators/pso/init_uniform.py b/propulate/propagators/pso/init_uniform.py index 32f75082..fce250af 100644 --- a/propulate/propagators/pso/init_uniform.py +++ b/propulate/propagators/pso/init_uniform.py @@ -5,9 +5,9 @@ from typing import Union, Dict, Tuple, List import numpy as np - +from propulate.individual import Individual from propulate.particle import Particle -from propulate.population import Individual + from propulate.propagators import Stochastic from propulate.utils import make_particle diff --git a/propulate/propagators/pso/stateless.py b/propulate/propagators/pso/stateless.py index 024cc496..ad99c8fe 100644 --- a/propulate/propagators/pso/stateless.py +++ b/propulate/propagators/pso/stateless.py @@ -5,7 +5,8 @@ from random import Random from typing import Dict, Tuple, List -from propulate.population import Individual +from propulate.individual import Individual + from propulate.propagators import Propagator diff --git a/propulate/propulator.py b/propulate/propulator.py index 3f1f52f4..7005b4e5 100644 --- a/propulate/propulator.py +++ b/propulate/propulator.py @@ -12,10 +12,9 @@ import numpy as np from mpi4py import MPI -from .propagators import Propagator, SelectMin -from .population import Individual from ._globals import DUMP_TAG, INDIVIDUAL_TAG - +from .individual import Individual +from .propagators import Propagator, SelectMin log = logging.getLogger(__name__) # Get logger instance. @@ -159,7 +158,7 @@ def _get_active_individuals(self) -> Tuple[List[Individual], int]: Returns ------- - list[propulate.population.Individual] + list[propulate.individual.Individual] currently active individuals in population int number of currently active individuals @@ -174,7 +173,7 @@ def _breed(self) -> Individual: Returns ------- - propulate.population.Individual + propulate.individual.Individual newly bred individual """ active_pop, _ = self._get_active_individuals() @@ -278,7 +277,7 @@ def _get_unique_individuals(self) -> List[Individual]: Returns ------- - list[propulate.population.Individual] + list[propulate.individual.Individual] unique individuals """ unique_inds = [] @@ -302,7 +301,7 @@ def _check_intra_island_synchronization( Parameters ---------- - populations: list[list[propulate.population.Individual]] + populations: list[list[propulate.individual.Individual]] list of islands' sorted population lists Returns @@ -459,9 +458,9 @@ def _check_for_duplicates( Returns ------- - list[list[propulate.population.Individual | int]] + list[list[propulate.individual.Individual | int]] individuals and their occurrences - list[propulate.population.Individual] + list[propulate.individual.Individual] unique individuals in population """ if active: diff --git a/propulate/utils.py b/propulate/utils.py index 6a9ae41b..a9ae28d3 100644 --- a/propulate/utils.py +++ b/propulate/utils.py @@ -1,17 +1,16 @@ # -*- coding: utf-8 -*- import logging -from pathlib import Path - -import colorlog import random import sys +from pathlib import Path +from typing import Dict, Union, Tuple +import colorlog import numpy as np from mpi4py import MPI -from typing import Dict, Union, Tuple +from .individual import Individual from .particle import Particle -from .population import Individual from .propagators import ( Compose, Conditional, From ea0c6174528392bd1a663d22e03882e6421f29d1 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 12:18:16 +0200 Subject: [PATCH 099/139] Unified interface of Particle and Individual. --- propulate/population/particle.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/propulate/population/particle.py b/propulate/population/particle.py index ae78a47b..527dafd1 100644 --- a/propulate/population/particle.py +++ b/propulate/population/particle.py @@ -3,7 +3,7 @@ """ import numpy as np -from propulate.individual import Individual +from .individual import Individual class Particle(Individual): @@ -17,13 +17,15 @@ def __init__( self, position: np.ndarray = None, velocity: np.ndarray = None, - iteration: int = 0, - rank: int = None, + generation: int = -1, + rank: int = -1, ): - super().__init__(generation=iteration, rank=rank) + super().__init__(generation=generation, rank=rank) if position is not None and velocity is not None: assert position.shape == velocity.shape self.velocity = velocity self.position = position - self.g_rank = rank # necessary as Propulate splits up the COMM_WORLD communicator which leads to errors with - # rank. + self.g_rank = ( + rank # necessary as Propulate splits up the COMM_WORLD communicator + ) + # which leads to errors with normal rank in multi-island case. From 98c454099bc1fe550da0e4e635119038c1b12cf8 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 12:19:02 +0200 Subject: [PATCH 100/139] Adjusted imports. --- propulate/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/propulate/utils.py b/propulate/utils.py index a9ae28d3..6f12c459 100644 --- a/propulate/utils.py +++ b/propulate/utils.py @@ -9,8 +9,7 @@ import numpy as np from mpi4py import MPI -from .individual import Individual -from .particle import Particle +from .population import Individual, Particle from .propagators import ( Compose, Conditional, @@ -169,7 +168,7 @@ def make_particle(individual: Individual) -> Particle: ---------- individual : An Individual that needs to be a particle """ - p = Particle(iteration=individual.generation) + p = Particle(generation=individual.generation) p.position = np.zeros(len(individual)) p.velocity = np.zeros(len(individual)) for i, k in enumerate(individual): From 41a4e32732afd82228689b47a5663fdccb649c5c Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 12:19:20 +0200 Subject: [PATCH 101/139] Deleted propagator as it does not work. --- propulate/propagators/pso/compose.py | 29 ---------------------------- 1 file changed, 29 deletions(-) delete mode 100644 propulate/propagators/pso/compose.py diff --git a/propulate/propagators/pso/compose.py b/propulate/propagators/pso/compose.py deleted file mode 100644 index 01b17008..00000000 --- a/propulate/propagators/pso/compose.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import List - -from propulate.individual import Individual -from propulate.particle import Particle - -from propulate import propagators - - -class Compose(propagators.Compose): - """ - This class is the Particle-using counterpart to the Compose propagator. - It does basically exact the same things. For further reference, please refer to the standard Compose propagator. - """ - - def __call__(self, particles: List[Particle]) -> Particle: - """ - Returns the first element of the list of particles returned by the last Propagator in the list - input upon creation of the object. - - This behaviour should change in near future, so that a list of Particles is returned, - with hopefully only one member. - """ - for p in self.propagators: - tmp = p(particles) - if isinstance(tmp, Individual): - particles = [tmp] - else: - particles = tmp - return particles[0] From b50b9d5114ad3f621f364c9251f072432fbd20e2 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 12:20:45 +0200 Subject: [PATCH 102/139] Adjusted imports, input types, --- propulate/propagators/propagators.py | 6 +++- propulate/propagators/pso/basic.py | 36 +++++++++++-------- propulate/propagators/pso/canonical.py | 8 ++--- propulate/propagators/pso/constriction.py | 6 ++-- propulate/propagators/pso/init_uniform.py | 15 ++++---- propulate/propagators/pso/stateless.py | 7 ++-- .../propagators/pso/velocity_clamping.py | 6 ++-- 7 files changed, 46 insertions(+), 38 deletions(-) diff --git a/propulate/propagators/propagators.py b/propulate/propagators/propagators.py index faa39090..774c5a4e 100644 --- a/propulate/propagators/propagators.py +++ b/propulate/propagators/propagators.py @@ -5,7 +5,7 @@ import numpy as np from abc import ABC, abstractmethod -from ..individual import Individual +from ..population import Individual def _check_compatible(out1: int, in2: int) -> bool: @@ -201,6 +201,10 @@ def __init__(self, propagators: List[Propagator]) -> None: ValueError If propagators to stack are incompatible in terms of number of input and output individuals. """ + if len(propagators) < 1: + raise ValueError( + f"Not enough Propagators given ({len(propagators)}). At least 1 is required." + ) super(Compose, self).__init__(propagators[0].parents, propagators[-1].offspring) for i in range(len(propagators) - 1): # Check compatibility of consecutive propagators in terms of number of parents + offsprings. diff --git a/propulate/propagators/pso/basic.py b/propulate/propagators/pso/basic.py index 0b25bce6..077bfcc5 100644 --- a/propulate/propagators/pso/basic.py +++ b/propulate/propagators/pso/basic.py @@ -6,9 +6,9 @@ import numpy as np -from propulate.particle import Particle -from propulate.propagators import Propagator -from propulate.utils import make_particle +from ..propagators import Propagator +from ...population import Particle, Individual +from ...utils import make_particle class Basic(Propagator): @@ -54,8 +54,8 @@ def __init__( list(limits.values()) ).T # laa - "limits as array" - def __call__(self, particles: List[Particle]) -> Particle: - old_p, p_best, g_best = self._prepare_data(particles) + def __call__(self, individuals: List[Individual]) -> Particle: + old_p, p_best, g_best = self._prepare_data(individuals) new_velocity: np.ndarray = ( self.w_k * old_p.velocity @@ -67,7 +67,7 @@ def __call__(self, particles: List[Particle]) -> Particle: return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) def _prepare_data( - self, particles: List[Particle] + self, individuals: List[Individual] ) -> Tuple[Particle, Particle, Particle]: """ Returns the following particles in this very order: @@ -75,9 +75,16 @@ def _prepare_data( 2. p_best: the personal best value of this particle 3. g_best: the global best value currently known """ - if len(particles) < self.offspring: + if len(individuals) < self.offspring: raise ValueError("Not enough Particles") + particles = [] + for individual in individuals: + if isinstance(individual, Particle): + particles.append(individual) + else: + particles.append(make_particle(individual)) + own_p = [ x for x in particles @@ -85,20 +92,19 @@ def _prepare_data( or x.rank == self.rank ] if len(own_p) > 0: - old_p = max(own_p, key=lambda p: p.generation) + old_p: Individual = max(own_p, key=lambda p: p.generation) + if not isinstance(old_p, Particle): + old_p = make_particle(old_p) + print( + f"R{self.rank}, Iteration#{old_p.generation}: Type Error. " + f"Converted Individual to Particle. Continuing." + ) else: victim = max(particles, key=lambda p: p.generation) old_p = self._make_new_particle( victim.position, victim.velocity, victim.generation ) - if not isinstance(old_p, Particle): - old_p = make_particle(old_p) - print( - f"R{self.rank}, Iteration#{old_p.generation}: Type Error. " - f"Converted Individual to Particle. Continuing." - ) - g_best = min(particles, key=lambda p: p.loss) p_best = min(own_p, key=lambda p: p.loss) diff --git a/propulate/propagators/pso/canonical.py b/propulate/propagators/pso/canonical.py index f65c9929..8104bf1e 100644 --- a/propulate/propagators/pso/canonical.py +++ b/propulate/propagators/pso/canonical.py @@ -2,8 +2,8 @@ import numpy as np -from propulate.particle import Particle from constriction import Constriction +from ...population import Individual, Particle class Canonical(Constriction): @@ -20,9 +20,9 @@ def __init__(self, c_cognitive, c_social, rank, limits, rng): x_range = np.abs(x_max - x_min) self.v_cap: np.ndarray = np.array([-x_range, x_range]) - def __call__(self, particles: List[Particle]): - # Abuse Constriction's update rule so I don't have to rewrite it. - victim = super().__call__(particles) + def __call__(self, individuals: List[Individual]) -> Particle: + # Abuse Constriction's update rule, so I don't have to rewrite it. + victim = super().__call__(individuals) # Set new position and speed. v = victim.velocity.clip(*self.v_cap) diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py index 4dde31ce..a67ba6fe 100644 --- a/propulate/propagators/pso/constriction.py +++ b/propulate/propagators/pso/constriction.py @@ -7,7 +7,7 @@ import numpy as np from basic import Basic -from propulate.particle import Particle +from ...population import Individual, Particle class Constriction(Basic): @@ -42,8 +42,8 @@ def __init__( chi: float = 2.0 / (phi - 2.0 + np.sqrt(phi * (phi - 4.0))) super().__init__(chi, c_cognitive, c_social, rank, limits, rng) - def __call__(self, particles: List[Particle]) -> Particle: - old_p, p_best, g_best = self._prepare_data(particles) + def __call__(self, individuals: List[Individual]) -> Particle: + old_p, p_best, g_best = self._prepare_data(individuals) new_velocity = self.w_k * ( old_p.velocity diff --git a/propulate/propagators/pso/init_uniform.py b/propulate/propagators/pso/init_uniform.py index fce250af..a46bb5e6 100644 --- a/propulate/propagators/pso/init_uniform.py +++ b/propulate/propagators/pso/init_uniform.py @@ -5,11 +5,10 @@ from typing import Union, Dict, Tuple, List import numpy as np -from propulate.individual import Individual -from propulate.particle import Particle -from propulate.propagators import Stochastic -from propulate.utils import make_particle +from ..propagators import Stochastic +from ...population import Individual, Particle +from ...utils import make_particle class InitUniform(Stochastic): @@ -60,13 +59,13 @@ def __init__( self.v_limits = v_init_limit self.rank = rank - def __call__(self, particles: List[Individual]) -> Particle: + def __call__(self, individuals: List[Individual]) -> Particle: """ Apply uniform-initialization propagator. Parameters ---------- - particles : list of propulate.population.Individual objects + individuals : list of propulate.population.Individual objects individuals the propagator is applied to Returns @@ -75,7 +74,7 @@ def __call__(self, particles: List[Individual]) -> Particle: one particle object """ if ( - len(particles) == 0 or self.rng.random() < self.probability + len(individuals) == 0 or self.rng.random() < self.probability ): # Apply only with specified `probability`. position = np.array( [self.rng.uniform(*self.laa[..., i]) for i in range(self.laa.shape[-1])] @@ -103,7 +102,7 @@ def __call__(self, particles: List[Individual]) -> Particle: particle[limit] = particle.position[index] return particle else: - particle = particles[0] + particle = individuals[0] if isinstance(particle, Particle): return particle # Return 1st input individual w/o changes. else: diff --git a/propulate/propagators/pso/stateless.py b/propulate/propagators/pso/stateless.py index ad99c8fe..60c668ab 100644 --- a/propulate/propagators/pso/stateless.py +++ b/propulate/propagators/pso/stateless.py @@ -5,9 +5,8 @@ from random import Random from typing import Dict, Tuple, List -from propulate.individual import Individual - -from propulate.propagators import Propagator +from ..propagators import Propagator +from ...population import Individual class Stateless(Propagator): @@ -49,7 +48,7 @@ def __call__(self, particles: List[Individual]) -> Individual: if len(particles) < self.offspring: raise ValueError("Not enough Particles") own_p = [x for x in particles if x.rank == self.rank] - old_p = Individual(generation=-1) + old_p = Individual() for y in own_p: if y.generation > old_p.generation: old_p = y diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py index a3474b82..ad23fc4c 100644 --- a/propulate/propagators/pso/velocity_clamping.py +++ b/propulate/propagators/pso/velocity_clamping.py @@ -6,8 +6,8 @@ import numpy as np -from propulate.particle import Particle from basic import Basic +from ...population import Individual, Particle class VelocityClamping(Basic): @@ -48,8 +48,8 @@ def __init__( v_limits *= -1 self.v_cap: np.ndarray = np.array([-v_limits * x_range, v_limits * x_range]) - def __call__(self, particles: List[Particle]) -> Particle: - old_p, p_best, g_best = self._prepare_data(particles) + def __call__(self, individuals: List[Individual]) -> Particle: + old_p, p_best, g_best = self._prepare_data(individuals) new_velocity: np.ndarray = ( self.w_k * old_p.velocity From 43121bacfae3410779764ed173e54c38fa3aff84 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 13:48:58 +0200 Subject: [PATCH 103/139] Corrected update rule. --- propulate/propagators/pso/stateless.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/propulate/propagators/pso/stateless.py b/propulate/propagators/pso/stateless.py index 60c668ab..92c8f6d1 100644 --- a/propulate/propagators/pso/stateless.py +++ b/propulate/propagators/pso/stateless.py @@ -56,9 +56,9 @@ def __call__(self, particles: List[Individual]) -> Individual: p_best = sorted(own_p, key=lambda p: p.loss)[0] new_p = Individual(generation=old_p.generation + 1) for k in self.limits: - new_p[k] = self.c_cognitive * self.rng.uniform(*self.limits[k]) * ( - p_best[k] - old_p[k] - ) + self.c_social * self.rng.uniform(*self.limits[k]) * ( - g_best[k] - old_p[k] + new_p[k] = ( + old_p[k] + + self.rng.uniform(0, self.c_cognitive) * (p_best[k] - old_p[k]) + + self.rng.uniform(0, self.c_social) * (g_best[k] - old_p[k]) ) return new_p From 31aa6272010148f559ac6373649e58e87d122efc Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 13:49:24 +0200 Subject: [PATCH 104/139] Adjusted class doc string slightly. --- propulate/propagators/pso/init_uniform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/propulate/propagators/pso/init_uniform.py b/propulate/propagators/pso/init_uniform.py index a46bb5e6..5eeb4315 100644 --- a/propulate/propagators/pso/init_uniform.py +++ b/propulate/propagators/pso/init_uniform.py @@ -30,7 +30,7 @@ def __init__( rank: int ): """ - Constructor of InitUniform class. + Constructor of InitUniform pso propagator class. In case of parents > 0 and probability < 1., call returns input individual without change. From 1ab1dba218bf372cbee6d16f6a09053af53daa28 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 13:49:41 +0200 Subject: [PATCH 105/139] Adjusted class doc string to recommendation. --- propulate/propagators/pso/velocity_clamping.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py index ad23fc4c..1fa7a243 100644 --- a/propulate/propagators/pso/velocity_clamping.py +++ b/propulate/propagators/pso/velocity_clamping.py @@ -12,12 +12,14 @@ class VelocityClamping(Basic): """ - This propagator implements the Velocity Clamping pso variant. + This propagator implements velocity clamping PSO. - In addition to the parameters known from the basic PSO propagator, it features a parameter, - via which relative values (best between 0 and 1) are passed to the propagator. + In addition to the parameters known from the basic PSO + propagator, it features a clamping factor within [0, 1] used to determine each parameter's maximum velocity value + relative to its search-space limits. - Based on these values, the velocities of the particles are cut down to a reasonable value. + Based on these values, the velocities of the particles are cut down to a + reasonable value. """ def __init__( From 96f30fe0a2baa1f9f4b4622183bef28fcf1586a0 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 13:52:09 +0200 Subject: [PATCH 106/139] Accelerated stateless PSO update. --- propulate/propagators/pso/init_uniform.py | 2 +- propulate/propagators/pso/stateless.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/propulate/propagators/pso/init_uniform.py b/propulate/propagators/pso/init_uniform.py index 5eeb4315..19311dd3 100644 --- a/propulate/propagators/pso/init_uniform.py +++ b/propulate/propagators/pso/init_uniform.py @@ -43,7 +43,7 @@ def __init__( parents : int number of input individuals (-1 for any) probability : float - the probability with which a completely new individual is created + the probability with which a completely new individual is created rng : random.Random random number generator v_init_limit: float | np.ndarray diff --git a/propulate/propagators/pso/stateless.py b/propulate/propagators/pso/stateless.py index 92c8f6d1..99fee15f 100644 --- a/propulate/propagators/pso/stateless.py +++ b/propulate/propagators/pso/stateless.py @@ -52,8 +52,8 @@ def __call__(self, particles: List[Individual]) -> Individual: for y in own_p: if y.generation > old_p.generation: old_p = y - g_best = sorted(particles, key=lambda p: p.loss)[0] - p_best = sorted(own_p, key=lambda p: p.loss)[0] + g_best = min(particles, key=lambda p: p.loss) + p_best = min(own_p, key=lambda p: p.loss) new_p = Individual(generation=old_p.generation + 1) for k in self.limits: new_p[k] = ( From 861d4695bc60c2594d96be19e60040f14c7f60b5 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 13:59:27 +0200 Subject: [PATCH 107/139] Adjusted update base particle search to the way of _prepare_data of Basic pso propagator. --- propulate/propagators/pso/stateless.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/propulate/propagators/pso/stateless.py b/propulate/propagators/pso/stateless.py index 99fee15f..8560fa0e 100644 --- a/propulate/propagators/pso/stateless.py +++ b/propulate/propagators/pso/stateless.py @@ -48,10 +48,13 @@ def __call__(self, particles: List[Individual]) -> Individual: if len(particles) < self.offspring: raise ValueError("Not enough Particles") own_p = [x for x in particles if x.rank == self.rank] - old_p = Individual() - for y in own_p: - if y.generation > old_p.generation: - old_p = y + if len(own_p) > 0: + old_p = max(own_p, key=lambda p: p.generation) + else: # No own particle found in given parameters, thus creating new one. + old_p = Individual(0, self.rank) + for k in self.limits: + old_p[k] = self.rng.uniform(*self.limits[k]) + return old_p g_best = min(particles, key=lambda p: p.loss) p_best = min(own_p, key=lambda p: p.loss) new_p = Individual(generation=old_p.generation + 1) From 2fe01fe6fa0dd060e4a1973645bd5fa7625ede80 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 14:01:54 +0200 Subject: [PATCH 108/139] Adjusted imports in propagators/__init__.py to use relative imports --- propulate/propagators/__init__.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py index 201137b4..81e57cd4 100644 --- a/propulate/propagators/__init__.py +++ b/propulate/propagators/__init__.py @@ -20,6 +20,20 @@ "pso", ] -from propagators import Propagator, Stochastic, Conditional, Compose, PointMutation, RandomPointMutation, \ - IntervalMutationNormal, MateUniform, MateMultiple, MateSigmoid, SelectMin, SelectMax, SelectUniform, InitUniform -import pso \ No newline at end of file +from . import pso +from .propagators import ( + Propagator, + Stochastic, + Conditional, + Compose, + PointMutation, + RandomPointMutation, + IntervalMutationNormal, + MateUniform, + MateMultiple, + MateSigmoid, + SelectMin, + SelectMax, + SelectUniform, + InitUniform, +) From 2ea7f2aa45a63892aba6acf517303693a6db6772 Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 14:03:53 +0200 Subject: [PATCH 109/139] Adjusted imports in propagators/pso/__init__.py to use relative imports --- propulate/propagators/pso/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/propulate/propagators/pso/__init__.py b/propulate/propagators/pso/__init__.py index 80359ac5..874593ae 100644 --- a/propulate/propagators/pso/__init__.py +++ b/propulate/propagators/pso/__init__.py @@ -6,8 +6,8 @@ "Canonical", ] -from init_uniform import InitUniform -from basic import Basic -from velocity_clamping import VelocityClamping -from constriction import Constriction -from canonical import Canonical +from .basic import Basic +from .canonical import Canonical +from .constriction import Constriction +from .init_uniform import InitUniform +from .velocity_clamping import VelocityClamping From d245807d5d62ea0b9fed82d32cc7beb302fa86fe Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 14:06:00 +0200 Subject: [PATCH 110/139] Added a paper ref code for the basic PSO update rule. --- propulate/propagators/pso/basic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/propulate/propagators/pso/basic.py b/propulate/propagators/pso/basic.py index 077bfcc5..0bd2b917 100644 --- a/propulate/propagators/pso/basic.py +++ b/propulate/propagators/pso/basic.py @@ -1,5 +1,5 @@ """ -This file contains the first stateful PSO propagator for Propulate. +This file contains the original (stateful) PSO propagator for Propulate. """ from random import Random from typing import Dict, Tuple, List @@ -23,6 +23,9 @@ class Basic(Propagator): This basic PSO propagator can only explore real-valued search spaces, i.e., continuous parameters. It works on ``Particle`` objects and serves as the foundation of all other PSO propagators. Further PSO propagators should be derived from this propagator or from one that is derived from this. + + TODO: Decode refcode. + Reference: Shi1998 """ def __init__( From c4e5394153bc93a4b7306b68e43793fd880689cf Mon Sep 17 00:00:00 2001 From: Morridin Date: Wed, 20 Sep 2023 14:06:51 +0200 Subject: [PATCH 111/139] Adjusted doc string of __init__ method. --- propulate/propagators/pso/basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/propulate/propagators/pso/basic.py b/propulate/propagators/pso/basic.py index 0bd2b917..c16a0504 100644 --- a/propulate/propagators/pso/basic.py +++ b/propulate/propagators/pso/basic.py @@ -39,7 +39,7 @@ def __init__( ): """ Class constructor. - :param w_k: The learning rate ... somehow + :param w_k: The inertia weight. :param c_cognitive: constant cognitive factor to scale p_best with :param c_social: constant social factor to scale g_best with :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD From 7d24d2dff686b3ab3e815364298dd032026ad8ea Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 09:45:31 +0200 Subject: [PATCH 112/139] Revamped PSO example script to Argparser. --- tutorials/pso_example.py | 184 +++++++++++++++++++++++++++++++-------- 1 file changed, 147 insertions(+), 37 deletions(-) diff --git a/tutorials/pso_example.py b/tutorials/pso_example.py index 8a547dae..454bba93 100644 --- a/tutorials/pso_example.py +++ b/tutorials/pso_example.py @@ -1,12 +1,17 @@ -#!/usr/bin/env python3 +""" +This files contains an example use case for the PSO propagators. Here, you can choose between benchmark functions and +optimize them. + +The example shows, how to set up Propulate in order to use it with PSO. +""" +import argparse import random -import sys +from typing import Dict from mpi4py import MPI -from propulate import Islands -from propulate.propagators import Conditional, Stateless -from function_benchmark import get_function_search_space +from propulate import set_logger_config, Propulator +from propulate.propagators import Conditional from propulate.propagators.pso import ( Basic, VelocityClamping, @@ -14,47 +19,152 @@ Canonical, InitUniform, ) +from tutorials.function_benchmark import get_function_search_space -############ -# SETTINGS # -############ +if __name__ == "__main__": + comm = MPI.COMM_WORLD -function_name = sys.argv[ - 1 -] # Get function to optimize from command-line. Possible Options: See function_benchmark.py -NUM_GENERATIONS: int = int(sys.argv[2]) # Set number of generations. -POP_SIZE = 2 * MPI.COMM_WORLD.size # Set size of breeding population. + if comm.rank == 0: + print( + "#################################################\n" + "# PROPULATE: Parallel Propagator of Populations #\n" + "#################################################\n" + ) -function, limits = get_function_search_space(function_name) + parser = argparse.ArgumentParser( + prog="Simple PSO example", + description="Set up and run a basic particle swarm optimization of mathematical functions.", + ) + parser.add_argument( # Function to optimize + "-f", + "--function", + type=str, + choices=[ + "bukin", + "eggcrate", + "himmelblau", + "keane", + "leon", + "rastrigin", + "schwefel", + "sphere", + "step", + "rosenbrock", + "quartic", + "bisphere", + "birastrigin", + "griewank", + ], + default="sphere", + ) + parser.add_argument( + "-g", "--generations", type=int, default=1000 + ) # Number of generations + parser.add_argument( + "-s", "--seed", type=int, default=0 + ) # Seed for Propulate random number generator + parser.add_argument( + "-v", "--verbosity", type=int, default=1, choices=range(6) + ) # Verbosity level + parser.add_argument( + "-ckpt", "--checkpoint", type=str, default="./" + ) # Path for loading and writing checkpoints. + parser.add_argument( + "-p", "--pop_size", type=int, default=2 * comm.size + ) # Breeding pool size + parser.add_argument( + "-var", + "--variant", + type=str, + choices=["Basic", "VelocityClamping", "Constriction", "Canonical"], + default="Basic", + ) # PSO variant to run -if __name__ == "__main__": - # migration_topology = num_migrants*np.ones((4, 4), dtype=int) - # np.fill_diagonal(migration_topology, 0) + hp_set: Dict[str, bool] = { + "inertia": False, + "cognitive": False, + "social": False, + } - rng = random.Random(MPI.COMM_WORLD.rank) + class ParamSettingCatcher(argparse.Action): + """ + This class extends argparse's Action class in order to allow for an action, that logs, if one of the PSO HP + was actually set. + """ - pso_propagator = [ - Stateless(0, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng), - Basic(0.7298, 0.5, 0.5, MPI.COMM_WORLD.rank, limits, rng), - VelocityClamping( - 0.7298, 1.49618, 1.49618, MPI.COMM_WORLD.rank, limits, rng, 0.6 - ), - Constriction(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng), - Canonical(2.49618, 2.49618, MPI.COMM_WORLD.rank, limits, rng), - ][ - 1 - ] # Please choose with this index, which Propagator to use in the optimisation process. + def __call__(self, parser, namespace, values, option_string=None): + hp_set[self.dest] = True + super().__call__(parser, namespace, values, option_string) + + parser.add_argument( + "--inertia", type=float, default=0.729, action=ParamSettingCatcher + ) # Inertia weight + parser.add_argument( + "--cognitive", type=float, default=1.49445, action=ParamSettingCatcher + ) # Cognitive factor + parser.add_argument( + "--social", type=float, default=1.49445, action=ParamSettingCatcher + ) # Social factor + parser.add_argument( + "--clamping_factor", type=float, default=0.6 + ) # Clamping factor for velocity clamping + parser.add_argument("-t", "--top_n", type=int, default=1) + parser.add_argument("-l", "--logging_int", type=int, default=10) + config = parser.parse_args() + # Set up separate logger for Propulate optimization. + set_logger_config( + level=10 * config.verbosity, # logging level + log_file=f"{config.checkpoint}/propulator.log", # logging path + ) + + rng = random.Random( + config.seed + comm.rank + ) # Separate random number generator for optimization. + function, limits = get_function_search_space( + config.function + ) # Get callable function + search-space limits. + + if config.variant in ("Constriction", "Canonical"): + if not hp_set["cognitive"]: + config.cognitive = 2.05 + if not hp_set["social"]: + config.social = 2.05 + pso_propagator = { + "Basic": Basic( + config.inertia, + config.cognitive, + config.social, + MPI.COMM_WORLD.rank, + limits, + rng, + ), + "VelocityClamping": VelocityClamping( + config.inertia, + config.cognitive, + config.social, + MPI.COMM_WORLD.rank, + limits, + rng, + config.clamping_factor, + ), + "Constriction": Constriction( + config.cognitive, config.social, MPI.COMM_WORLD.rank, limits, rng + ), + "Canonical": Canonical( + config.cognitive, config.social, MPI.COMM_WORLD.rank, limits, rng + ), + }[config.variant] init = InitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) - propagator = Conditional(POP_SIZE, pso_propagator, init) + propagator = Conditional(config.pop_size, pso_propagator, init) - islands = Islands( + propulator = Propulator( function, propagator, - rng, - generations=NUM_GENERATIONS, - checkpoint_path="./checkpoints/", - migration_probability=0, - pollination=False, + comm=comm, + generations=config.generations, + checkpoint_path=config.checkpoint, + rng=rng, ) - islands.evolve(debug=0) + propulator.propulate(config.logging_int, config.verbosity) + propulator.summarize(top_n=config.top_n, debug=config.verbosity) From 0374b58c4a7eb1ec2e7a62b3ecb0e7aaf005f9a2 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 10:14:43 +0200 Subject: [PATCH 113/139] Repaired some broken imports. --- propulate/islands.py | 2 +- propulate/pollinator.py | 2 +- propulate/population/__init__.py | 4 +-- propulate/propagators/__init__.py | 4 +-- propulate/propagators/pso/canonical.py | 2 +- propulate/propagators/pso/constriction.py | 2 +- .../propagators/pso/velocity_clamping.py | 2 +- propulate/propulator.py | 2 +- tutorials/pso_example.py | 28 +++++++++++-------- 9 files changed, 27 insertions(+), 21 deletions(-) diff --git a/propulate/islands.py b/propulate/islands.py index dab60bc4..67b8e4aa 100644 --- a/propulate/islands.py +++ b/propulate/islands.py @@ -7,9 +7,9 @@ import numpy as np from mpi4py import MPI -from .individual import Individual from .migrator import Migrator from .pollinator import Pollinator +from .population import Individual from .propagators import Propagator, SelectMin, SelectMax log = logging.getLogger(__name__) # Get logger instance. diff --git a/propulate/pollinator.py b/propulate/pollinator.py index 8a224822..059e03d4 100644 --- a/propulate/pollinator.py +++ b/propulate/pollinator.py @@ -9,7 +9,7 @@ from mpi4py import MPI from ._globals import MIGRATION_TAG, SYNCHRONIZATION_TAG -from .individual import Individual +from .population import Individual from .propagators import Propagator, SelectMin, SelectMax from .propulator import Propulator diff --git a/propulate/population/__init__.py b/propulate/population/__init__.py index 06ff064d..ac90ae40 100644 --- a/propulate/population/__init__.py +++ b/propulate/population/__init__.py @@ -2,5 +2,5 @@ This package bundles all classes that are used as individuals of Propulate's optimization population. """ __all__ = ["Individual", "Particle"] -from individual import Individual -from particle import Particle +from .individual import Individual +from .particle import Particle diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py index 81e57cd4..95aec6e8 100644 --- a/propulate/propagators/__init__.py +++ b/propulate/propagators/__init__.py @@ -20,8 +20,7 @@ "pso", ] -from . import pso -from .propagators import ( +from propulate.propagators.propagators import ( Propagator, Stochastic, Conditional, @@ -37,3 +36,4 @@ SelectUniform, InitUniform, ) +from . import pso diff --git a/propulate/propagators/pso/canonical.py b/propulate/propagators/pso/canonical.py index 8104bf1e..23a2f82b 100644 --- a/propulate/propagators/pso/canonical.py +++ b/propulate/propagators/pso/canonical.py @@ -2,7 +2,7 @@ import numpy as np -from constriction import Constriction +from .constriction import Constriction from ...population import Individual, Particle diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py index a67ba6fe..a1abcf10 100644 --- a/propulate/propagators/pso/constriction.py +++ b/propulate/propagators/pso/constriction.py @@ -6,7 +6,7 @@ import numpy as np -from basic import Basic +from .basic import Basic from ...population import Individual, Particle diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py index 1fa7a243..48a81795 100644 --- a/propulate/propagators/pso/velocity_clamping.py +++ b/propulate/propagators/pso/velocity_clamping.py @@ -6,7 +6,7 @@ import numpy as np -from basic import Basic +from .basic import Basic from ...population import Individual, Particle diff --git a/propulate/propulator.py b/propulate/propulator.py index 7005b4e5..c6d0aa0d 100644 --- a/propulate/propulator.py +++ b/propulate/propulator.py @@ -13,7 +13,7 @@ from mpi4py import MPI from ._globals import DUMP_TAG, INDIVIDUAL_TAG -from .individual import Individual +from .population import Individual from .propagators import Propagator, SelectMin log = logging.getLogger(__name__) # Get logger instance. diff --git a/tutorials/pso_example.py b/tutorials/pso_example.py index 454bba93..ffb8ec75 100644 --- a/tutorials/pso_example.py +++ b/tutorials/pso_example.py @@ -11,7 +11,7 @@ from mpi4py import MPI from propulate import set_logger_config, Propulator -from propulate.propagators import Conditional +from propulate.propagators import Conditional, Propagator from propulate.propagators.pso import ( Basic, VelocityClamping, @@ -130,16 +130,19 @@ def __call__(self, parser, namespace, values, option_string=None): config.cognitive = 2.05 if not hp_set["social"]: config.social = 2.05 - pso_propagator = { - "Basic": Basic( + + pso_propagator: Propagator + if config.variant == "Basic": + pso_propagator = Basic( config.inertia, config.cognitive, config.social, MPI.COMM_WORLD.rank, limits, rng, - ), - "VelocityClamping": VelocityClamping( + ) + elif config.variant == "VelocityClamping": + pso_propagator = VelocityClamping( config.inertia, config.cognitive, config.social, @@ -147,14 +150,17 @@ def __call__(self, parser, namespace, values, option_string=None): limits, rng, config.clamping_factor, - ), - "Constriction": Constriction( + ) + elif config.variant == "Constriction": + pso_propagator = Constriction( config.cognitive, config.social, MPI.COMM_WORLD.rank, limits, rng - ), - "Canonical": Canonical( + ) + elif config.variant == "Canonical": + pso_propagator = Canonical( config.cognitive, config.social, MPI.COMM_WORLD.rank, limits, rng - ), - }[config.variant] + ) + else: + raise ValueError("Invalid PSO propagator name given.") init = InitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) propagator = Conditional(config.pop_size, pso_propagator, init) From f3b9b75a9ebd49c593bbfc7feb1378e3fbb023da Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 11:24:55 +0200 Subject: [PATCH 114/139] Various doc string adjustments. --- propulate/population/particle.py | 18 ++- propulate/propagators/propagators.py | 4 +- propulate/propagators/pso/basic.py | 118 +++++++++++++----- propulate/propagators/pso/canonical.py | 49 +++++++- propulate/propagators/pso/constriction.py | 2 +- .../propagators/pso/velocity_clamping.py | 10 +- 6 files changed, 156 insertions(+), 45 deletions(-) diff --git a/propulate/population/particle.py b/propulate/population/particle.py index 527dafd1..476088b4 100644 --- a/propulate/population/particle.py +++ b/propulate/population/particle.py @@ -8,9 +8,18 @@ class Particle(Individual): """ - Child class of ``Individual`` with additional properties required for PSO, i.e., an array-type velocity field and a (redundant) array-type position field. + Child class of ``Individual`` with additional properties required for PSO, i.e., an array-type velocity field and + a (redundant) array-type position field. + Note that Propulate relies on ``Individual``s being ``dict``s. - When defining new propagators, users of the ``Particle`` class thus need to ensure that a ``Particle``'s position always matches its dict contents and vice versa. + + When defining new propagators, users of the ``Particle`` class thus need to ensure that a ``Particle``'s position + always matches its dict contents and vice versa. + + This class also contains an attribute field called ``global_rank``. It contains the global rank of the propagator + that + created it. + This is for purposes of better (or at all) retrieval in multi swarm case. """ def __init__( @@ -25,7 +34,4 @@ def __init__( assert position.shape == velocity.shape self.velocity = velocity self.position = position - self.g_rank = ( - rank # necessary as Propulate splits up the COMM_WORLD communicator - ) - # which leads to errors with normal rank in multi-island case. + self.global_rank = rank # The global rank of the creating propagator for later retrieval upon update. diff --git a/propulate/propagators/propagators.py b/propulate/propagators/propagators.py index 774c5a4e..e7b954c9 100644 --- a/propulate/propagators/propagators.py +++ b/propulate/propagators/propagators.py @@ -388,12 +388,12 @@ def __call__(self, ind: Individual) -> Individual: Parameters ---------- - ind: propulate.individual.Individual + ind: propulate.population.Individual individual the propagator is applied to Returns ------- - propulate.individual.Individual + propulate.population.Individual possibly point-mutated individual after application of propagator """ if ( diff --git a/propulate/propagators/pso/basic.py b/propulate/propagators/pso/basic.py index c16a0504..f90023f0 100644 --- a/propulate/propagators/pso/basic.py +++ b/propulate/propagators/pso/basic.py @@ -1,6 +1,7 @@ """ This file contains the original (stateful) PSO propagator for Propulate. """ +import logging from random import Random from typing import Dict, Tuple, List @@ -15,22 +16,23 @@ class Basic(Propagator): """ This propagator implements the most basic PSO variant one possibly could think of. - It features an inertia factor w_k applied to the old velocity in the velocity update, - a social and a cognitive factor, as well as some measures to implement some none-linearity. + It features an inertia factor applied to the old velocity in the velocity update, + a social and a cognitive factor. - This is done with the help of some randomness. + With the help of the random number generator required as creation parameter, non-linearity is added to the particle + update in order to not collapse to linear regression. This basic PSO propagator can only explore real-valued search spaces, i.e., continuous parameters. It works on ``Particle`` objects and serves as the foundation of all other PSO propagators. Further PSO propagators should be derived from this propagator or from one that is derived from this. - TODO: Decode refcode. - Reference: Shi1998 + This variant was first proposed in Y. Shi and R. Eberhart. “A modified particle swarm optimizer”, 1998, + https://doi.org/10.1109/ICEC.1998.699146 """ def __init__( self, - w_k: float, + inertia: float, c_cognitive: float, c_social: float, rank: int, @@ -38,30 +40,62 @@ def __init__( rng: Random, ): """ - Class constructor. - :param w_k: The inertia weight. - :param c_cognitive: constant cognitive factor to scale p_best with - :param c_social: constant social factor to scale g_best with - :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD - :param limits: a dict with str keys and 2-tuples of floats associated to each of them - :param rng: random number generator + The class constructor. + + In theory, it should be of no problem to hand over numpy arrays instead of the float hyperparameters inertia, + cognitive and social factor. + Please note that in this case, you are on your own to ensure that the dimension of the passed arrays fits to the + search domain. + + Parameters + ---------- + inertia : float + The inertia weight. + c_cognitive : float + Constant cognitive factor to scale the distance to the particle's personal best value with + c_social : float + Constant social factor to scale the distance to the swarm's global best value with + rank : int + The global rank of the worker the propagator is living on + limits : Dict[str, Tuple[float, float]] + A dict with str keys and 2-tuples of floats associated to each of them. It describes the borders of + the search domain. + rng : random.Random + Random number generator for said non-linearity """ super().__init__(parents=-1, offspring=1) self.c_social = c_social self.c_cognitive = c_cognitive - self.w_k = w_k + self.inertia = inertia self.rank = rank self.limits = limits self.rng = rng - self.laa: np.ndarray = np.array( - list(limits.values()) - ).T # laa - "limits as array" + self.limits_as_array: np.ndarray = np.array(list(limits.values())).T def __call__(self, individuals: List[Individual]) -> Particle: + """ + Applies the standard PSO update rule with inertia. + + Returns a Particle object that contains the updated values + of the youngest passed Particle or Individual that belongs to the worker the propagator is living on. + + Parameters + ---------- + individuals: List[Individual] + A list of individuals that must at least contain one individual that belongs to the propagator. + This list is used to calculate personal and global best of the particle and the swarm and to + then update the particle based on the retrieved results. + Individuals that cannot be used as Particle class objects are copied to particles before going on. + + Returns + ------- + propulate.population.Particle + An updated Particle. + """ old_p, p_best, g_best = self._prepare_data(individuals) new_velocity: np.ndarray = ( - self.w_k * old_p.velocity + self.inertia * old_p.velocity + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) ) @@ -73,10 +107,22 @@ def _prepare_data( self, individuals: List[Individual] ) -> Tuple[Particle, Particle, Particle]: """ - Returns the following particles in this very order: - 1. old_p: the current particle to be updated now - 2. p_best: the personal best value of this particle - 3. g_best: the global best value currently known + This method prepares the passed list of Individuals, that hopefully are Particles. + If they are not, they are copied over to Particle objects to avoid handling issues. + + Parameters + ---------- + individuals : List[Individual] + A list of Individual objects that shall be used as data basis for a PSO update step + + Returns + ------- + Tuple[propulate.population.Particle, propulate.population.Particle, propulate.population.Particle] + The following particles in this very order: + + 1. old_p: the current particle to be updated now + 2. p_best: the personal best value of this particle + 3. g_best: the global best value currently known """ if len(individuals) < self.offspring: raise ValueError("Not enough Particles") @@ -87,21 +133,22 @@ def _prepare_data( particles.append(individual) else: particles.append(make_particle(individual)) + logging.warning( + f"Got Individual instead of Particle. If this is on purpose, you can ignore this warning. " + f"Converted the Individual to Particle. Continuing." + ) own_p = [ x for x in particles - if (isinstance(x, Particle) and x.g_rank == self.rank) + if (isinstance(x, Particle) and x.global_rank == self.rank) or x.rank == self.rank ] if len(own_p) > 0: old_p: Individual = max(own_p, key=lambda p: p.generation) if not isinstance(old_p, Particle): old_p = make_particle(old_p) - print( - f"R{self.rank}, Iteration#{old_p.generation}: Type Error. " - f"Converted Individual to Particle. Continuing." - ) + else: victim = max(particles, key=lambda p: p.generation) old_p = self._make_new_particle( @@ -115,10 +162,23 @@ def _prepare_data( def _make_new_particle( self, position: np.ndarray, velocity: np.ndarray, generation: int - ): + ) -> Particle: """ Takes the necessary data to create a new Particle with the position dict set to the correct values. - :return: The newly created Particle object + + Parameters + ---------- + position : np.ndarray + An array containing the position of the particle to be created + velocity : np.ndarray + An array containing the velocity of the particle to be created + generation : int + The generation of the new particle + + Returns + ------- + propulate.population.Particle + The newly created Particle object that results from the PSO update. """ new_p = Particle(position, velocity, generation, self.rank) for i, k in enumerate(self.limits): diff --git a/propulate/propagators/pso/canonical.py b/propulate/propagators/pso/canonical.py index 23a2f82b..b32f23c7 100644 --- a/propulate/propagators/pso/canonical.py +++ b/propulate/propagators/pso/canonical.py @@ -11,16 +11,61 @@ class Canonical(Constriction): This propagator subclass features a combination of constriction and velocity clamping. The velocity clamping uses a clamping factor of 1, - the constriction is done as in the parental ``Constriction`` propagator. + the constriction is done as in the parental constriction propagator. + + This variant of PSO is to be found here: + Riccardo Poli, James Kennedy, and Tim Blackwell: “Particle swarm optimization”, 2007, + https://doi.org/10.1007/s11721-007-0002-0 """ def __init__(self, c_cognitive, c_social, rank, limits, rng): + """ + The class constructor. + + In theory, it should be of no problem to hand over numpy arrays instead of the float hyperparameters cognitive + and social factor. + Please note that in this case, you are on your own to ensure that the dimension of the passed arrays fits to the + search domain. + + Parameters + ---------- + c_cognitive : float + Constant cognitive factor to scale the distance to the particle's personal best value with + c_social : float + Constant social factor to scale the distance to the swarm's global best value with + rank : int + The global rank of the worker the propagator is living on + limits : Dict[str, Tuple[float, float]] + A dict with str keys and 2-tuples of floats associated to each of them. It describes the borders of + the search domain. + rng : random.Random + Random number generator for said non-linearity + """ super().__init__(c_cognitive, c_social, rank, limits, rng) - x_min, x_max = self.laa + x_min, x_max = self.limits_as_array x_range = np.abs(x_max - x_min) self.v_cap: np.ndarray = np.array([-x_range, x_range]) def __call__(self, individuals: List[Individual]) -> Particle: + """ + Applies the canonical PSO variant update rule. + + Returns a Particle object that contains the updated values of the youngest passed Particle or Individual that + belongs to the worker the propagator is living on. + + Parameters + ---------- + individuals: List[Individual] + A list of individuals that must at least contain one individual that belongs to the propagator. + This list is used to calculate personal and global best of the particle and the swarm and to + then update the particle based on the retrieved results. + Individuals that cannot be used as Particle class objects are copied to Particles before going on. + + Returns + ------- + propulate.population.Particle + An updated Particle. + """ # Abuse Constriction's update rule, so I don't have to rewrite it. victim = super().__call__(individuals) diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py index a1abcf10..48c447c0 100644 --- a/propulate/propagators/pso/constriction.py +++ b/propulate/propagators/pso/constriction.py @@ -45,7 +45,7 @@ def __init__( def __call__(self, individuals: List[Individual]) -> Particle: old_p, p_best, g_best = self._prepare_data(individuals) - new_velocity = self.w_k * ( + new_velocity = self.inertia * ( old_p.velocity + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py index 48a81795..81b3b5fa 100644 --- a/propulate/propagators/pso/velocity_clamping.py +++ b/propulate/propagators/pso/velocity_clamping.py @@ -24,7 +24,7 @@ class VelocityClamping(Basic): def __init__( self, - w_k: float, + inertia: float, c_cognitive: float, c_social: float, rank: int, @@ -34,7 +34,7 @@ def __init__( ): """ Class constructor. - :param w_k: The particle's inertia factor + :param inertia: The particle's inertia factor :param c_cognitive: constant cognitive factor to scale p_best with :param c_social: constant social factor to scale g_best with :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD @@ -43,8 +43,8 @@ def __init__( for their corresponding search space dimensions. If this is a float instead, it does its job for all axes. """ - super().__init__(w_k, c_cognitive, c_social, rank, limits, rng) - x_min, x_max = self.laa + super().__init__(inertia, c_cognitive, c_social, rank, limits, rng) + x_min, x_max = self.limits_as_array x_range = np.abs(x_max - x_min) if v_limits < 0: v_limits *= -1 @@ -54,7 +54,7 @@ def __call__(self, individuals: List[Individual]) -> Particle: old_p, p_best, g_best = self._prepare_data(individuals) new_velocity: np.ndarray = ( - self.w_k * old_p.velocity + self.inertia * old_p.velocity + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) ).clip(*self.v_cap) From 9105b94c93cd905fb32f5b7630765e03b13c7cb9 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 11:26:01 +0200 Subject: [PATCH 115/139] Adjust make_particle function's docstring. Co-authored-by: Marie Weiel <48559085+mcw92@users.noreply.github.com> --- propulate/utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/propulate/utils.py b/propulate/utils.py index 6f12c459..2714e8fd 100644 --- a/propulate/utils.py +++ b/propulate/utils.py @@ -162,11 +162,17 @@ def set_logger_config( def make_particle(individual: Individual) -> Particle: """ - Makes particles out of individuals. + Convert individuals to particles. Parameters ---------- - individual : An Individual that needs to be a particle + individual: Individual + Individual to be converted to a particle + + Returns + -------- + Particle + Converted individual """ p = Particle(generation=individual.generation) p.position = np.zeros(len(individual)) From 7e9c43a7b7c2e42fed776fc8aae054a4129f6314 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 11:31:33 +0200 Subject: [PATCH 116/139] Adjust file doc string. --- propulate/propagators/pso/init_uniform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/propulate/propagators/pso/init_uniform.py b/propulate/propagators/pso/init_uniform.py index 19311dd3..1f0e1a00 100644 --- a/propulate/propagators/pso/init_uniform.py +++ b/propulate/propagators/pso/init_uniform.py @@ -1,5 +1,5 @@ """ -This file contains propagators, that can be used to initialize a population of either Individuals or Particles. +This file contains a propagator to initialize a population of either ``Individuals`` or ``Particles``. """ from random import Random from typing import Union, Dict, Tuple, List From eb7fe4f09673649c113fdd40d7426d343b9d776e Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 11:34:01 +0200 Subject: [PATCH 117/139] Some further doc string adjustments. --- propulate/propagators/pso/init_uniform.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/propulate/propagators/pso/init_uniform.py b/propulate/propagators/pso/init_uniform.py index 1f0e1a00..6673462c 100644 --- a/propulate/propagators/pso/init_uniform.py +++ b/propulate/propagators/pso/init_uniform.py @@ -13,10 +13,7 @@ class InitUniform(Stochastic): """ - Initialize individuals by uniformly sampling specified limits for each trait. - - This propagator is the Particle-using counterpart to the InitUniform propagator. - For more information, please have a look there. + Initialize ``Particles`` by uniformly sampling specified limits for each trait. """ def __init__( @@ -65,13 +62,13 @@ def __call__(self, individuals: List[Individual]) -> Particle: Parameters ---------- - individuals : list of propulate.population.Individual objects + individuals : List[Individual] individuals the propagator is applied to Returns ------- - ind : propulate.ap_pso.Particle - one particle object + propulate.population.Particle + One particle object """ if ( len(individuals) == 0 or self.rng.random() < self.probability From 534b7992d823a2fb529bc9d5fa7bb288818a06ae Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 11:45:39 +0200 Subject: [PATCH 118/139] Stateless PSO propagator now has good doc strings. --- propulate/propagators/pso/stateless.py | 52 ++++++++++++++++++-------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/propulate/propagators/pso/stateless.py b/propulate/propagators/pso/stateless.py index 8560fa0e..4a7e9d20 100644 --- a/propulate/propagators/pso/stateless.py +++ b/propulate/propagators/pso/stateless.py @@ -1,5 +1,5 @@ """ -This file contains the first prototype of a propagator that runs PSO on Propulate. +This file contains a prototype proof-of-concept propagator to run PSO in Propulate. """ from random import Random @@ -11,16 +11,16 @@ class Stateless(Propagator): """ - The first draft of a pso propagator. It uses the infrastructure brought to you by vanilla Propulate and nothing more. + This propagator performs PSO without the need of Particles, but as a consequence, also without velocity. + Thus, it is called stateless. - Thus, it won't deliver that interesting results. + As this propagator works without velocity, there is also no inertia weight used. - This propagator works on Propulate's Individual-class objects. + It uses only classes provided by vanilla Propulate. """ def __init__( self, - w_k: float, c_cognitive: float, c_social: float, rank: int, @@ -28,26 +28,46 @@ def __init__( rng: Random, ): """ + The class constructor. - :param w_k: The learning rate ... somehow - currently without effect - :param c_cognitive: constant cognitive factor to scale p_best with - :param c_social: constant social factor to scale g_best with - :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD - :param limits: a dict with str keys and 2-tuples of floats associated to each of them - :param rng: random number generator + Parameters + ---------- + c_cognitive : float + Constant cognitive factor to scale individual's personal best value with + c_social : float + Constant social factor to scale swarm's global best value with + rank : int + The global rank of the worker the propagator is living on + limits : Dict[str, Tuple[float, float] + A dict with str keys and 2-tuples of floats associated to each of them describing the borders of + the search domain. + rng : random.Random + The random number generator required for non-linearity of update. """ super().__init__(parents=-1, offspring=1) self.c_social = c_social self.c_cognitive = c_cognitive - self.w_k = w_k self.rank = rank self.limits = limits self.rng = rng - def __call__(self, particles: List[Individual]) -> Individual: - if len(particles) < self.offspring: + def __call__(self, individuals: List[Individual]) -> Individual: + """ + Apply standard PSO update without inertia and old velocity. + + Parameters + ---------- + individuals : List[Individual] + The individual that are used as data basis for the PSO update + + Returns + ------- + propulate.population.Individual + An updated Individual + """ + if len(individuals) < self.offspring: raise ValueError("Not enough Particles") - own_p = [x for x in particles if x.rank == self.rank] + own_p = [x for x in individuals if x.rank == self.rank] if len(own_p) > 0: old_p = max(own_p, key=lambda p: p.generation) else: # No own particle found in given parameters, thus creating new one. @@ -55,7 +75,7 @@ def __call__(self, particles: List[Individual]) -> Individual: for k in self.limits: old_p[k] = self.rng.uniform(*self.limits[k]) return old_p - g_best = min(particles, key=lambda p: p.loss) + g_best = min(individuals, key=lambda p: p.loss) p_best = min(own_p, key=lambda p: p.loss) new_p = Individual(generation=old_p.generation + 1) for k in self.limits: From 54f81425e2160017456e6adccd4eab839a6ae289 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 11:48:01 +0200 Subject: [PATCH 119/139] Added an error raising section. --- propulate/propagators/pso/stateless.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/propulate/propagators/pso/stateless.py b/propulate/propagators/pso/stateless.py index 4a7e9d20..9759f547 100644 --- a/propulate/propagators/pso/stateless.py +++ b/propulate/propagators/pso/stateless.py @@ -64,6 +64,11 @@ def __call__(self, individuals: List[Individual]) -> Individual: ------- propulate.population.Individual An updated Individual + + Raises + ------ + ValueError + If the individuals list passed is empty and the propagator thus has no data to work on. """ if len(individuals) < self.offspring: raise ValueError("Not enough Particles") From d175f37dc03fd8f17cb15e998be8b2d064f05b80 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 11:57:32 +0200 Subject: [PATCH 120/139] Reworked the doc strings. --- .../propagators/pso/velocity_clamping.py | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py index 81b3b5fa..01d5c86c 100644 --- a/propulate/propagators/pso/velocity_clamping.py +++ b/propulate/propagators/pso/velocity_clamping.py @@ -33,15 +33,28 @@ def __init__( v_limits: Union[float, np.ndarray], ): """ - Class constructor. - :param inertia: The particle's inertia factor - :param c_cognitive: constant cognitive factor to scale p_best with - :param c_social: constant social factor to scale g_best with - :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD - :param limits: a dict with str keys and 2-tuples of floats associated to each of them - :param rng: random number generator :param v_limits: a numpy array containing values that work as relative caps - for their corresponding search space dimensions. - If this is a float instead, it does its job for all axes. + The class constructor. + + Parameters + ---------- + inertia : float + The particle's inertia factor + c_cognitive : float + Constant cognitive factor to scale the distance to the particle's personal best value with + c_social : float + Constant social factor to scale the distance to the swarm's global best value with + rank : int + The global rank of the worker the propagator is living on + limits : Dict[str, Tuple[float, float]] + A dict with str keys and 2-tuples of floats associated to each of them. It describes the borders of + the search domain. + rng : random.Random + Random number generator for said non-linearity + v_limits : Union[float, np.ndarray] + This parameter is multiplied with the clamping limit in order to reduce it further in most cases + (this is, when the value is in (0; 1)). + If this parameter has float type, it is applied to all dimensions of the search domain, else, + each of its elements are applied to their corresponding dimension of the search domain. """ super().__init__(inertia, c_cognitive, c_social, rank, limits, rng) x_min, x_max = self.limits_as_array @@ -51,6 +64,25 @@ def __init__( self.v_cap: np.ndarray = np.array([-v_limits * x_range, v_limits * x_range]) def __call__(self, individuals: List[Individual]) -> Particle: + """ + Applies the standard PSO update rule with inertia, extended by cutting off too high velocities. + + Returns a Particle object that contains the updated values of the youngest passed Particle or Individual that + belongs to the worker the propagator is living on. + + Parameters + ---------- + individuals: List[Individual] + A list of individuals that must at least contain one individual that belongs to the propagator. + This list is used to calculate personal and global best of the particle and the swarm and to + then update the particle based on the retrieved results. + Individuals that cannot be used as Particle class objects are copied to particles before going on. + + Returns + ------- + propulate.population.Particle + An updated Particle. + """ old_p, p_best, g_best = self._prepare_data(individuals) new_velocity: np.ndarray = ( From 62a1e5aa538af81f0d64792add24d07d522413a4 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 12:05:32 +0200 Subject: [PATCH 121/139] Resolved a problem with ambiguous truth values. --- propulate/propagators/pso/velocity_clamping.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py index 01d5c86c..13e00c33 100644 --- a/propulate/propagators/pso/velocity_clamping.py +++ b/propulate/propagators/pso/velocity_clamping.py @@ -58,9 +58,8 @@ def __init__( """ super().__init__(inertia, c_cognitive, c_social, rank, limits, rng) x_min, x_max = self.limits_as_array - x_range = np.abs(x_max - x_min) - if v_limits < 0: - v_limits *= -1 + x_range = abs(x_max - x_min) + v_limits = abs(v_limits) self.v_cap: np.ndarray = np.array([-v_limits * x_range, v_limits * x_range]) def __call__(self, individuals: List[Individual]) -> Particle: From e5a897fe0555bd5a1eb8f6e2579710760448f6f8 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 12:08:18 +0200 Subject: [PATCH 122/139] Added docstring for __call__ method. --- propulate/propagators/pso/constriction.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py index 48c447c0..49b37654 100644 --- a/propulate/propagators/pso/constriction.py +++ b/propulate/propagators/pso/constriction.py @@ -43,6 +43,25 @@ def __init__( super().__init__(chi, c_cognitive, c_social, rank, limits, rng) def __call__(self, individuals: List[Individual]) -> Particle: + """ + Applies the constriction PSO update rule. + + Returns a Particle object that contains the updated values of the youngest passed Particle or Individual that + belongs to the worker the propagator is living on. + + Parameters + ---------- + individuals: List[Individual] + A list of individuals that must at least contain one individual that belongs to the propagator. + This list is used to calculate personal and global best of the particle and the swarm and to + then update the particle based on the retrieved results. + Individuals that cannot be used as Particle class objects are copied to particles before going on. + + Returns + ------- + propulate.population.Particle + An updated Particle. + """ old_p, p_best, g_best = self._prepare_data(individuals) new_velocity = self.inertia * ( From 865fb1bcc2a3932ca01b6f7206a7f464c528fa74 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 12:07:54 +0200 Subject: [PATCH 123/139] Update class docstring. Co-authored-by: Marie Weiel <48559085+mcw92@users.noreply.github.com> --- propulate/propagators/pso/constriction.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py index 49b37654..fc8ad29f 100644 --- a/propulate/propagators/pso/constriction.py +++ b/propulate/propagators/pso/constriction.py @@ -12,7 +12,9 @@ class Constriction(Basic): """ - This propagator subclass features Constriction PSO as proposed by Clerc and Kennedy in 2002. + This propagator subclass features constriction PSO as proposed by Clerc and Kennedy in 2002. + + Original publication: Poli, R., Kennedy, J. & Blackwell, T. Particle swarm optimization. Swarm Intell 1, 33–57 (2007). https://doi.org/10.1007/s11721-007-0002-0 Instead of an inertia factor that affects the old velocity value within the velocity update, there is a constriction factor, that is applied on the new velocity `after' the update. From c0241bf645880e0e7970ac5954027ddcbcaf3b73 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 12:13:51 +0200 Subject: [PATCH 124/139] Optimized docstring. --- propulate/propagators/pso/constriction.py | 38 ++++++++++++++++------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py index fc8ad29f..96746200 100644 --- a/propulate/propagators/pso/constriction.py +++ b/propulate/propagators/pso/constriction.py @@ -13,13 +13,16 @@ class Constriction(Basic): """ This propagator subclass features constriction PSO as proposed by Clerc and Kennedy in 2002. - - Original publication: Poli, R., Kennedy, J. & Blackwell, T. Particle swarm optimization. Swarm Intell 1, 33–57 (2007). https://doi.org/10.1007/s11721-007-0002-0 - Instead of an inertia factor that affects the old velocity value within the velocity update, - there is a constriction factor, that is applied on the new velocity `after' the update. + Reference publication: Poli, R., Kennedy, J. & Blackwell, T. Particle swarm optimization. Swarm Intell 1, + 33–57 (2007). https://doi.org/10.1007/s11721-007-0002-0 - This propagator runs on Particle-class objects. + Instead of an inertia factor that affects the old velocity value within the velocity update, a constriction factor + is applied to the new velocity *after* the update. + + The constriction factor is calculated from cognitive and social factors and thus no hyperparameter. + + This propagator runs on ``Particle`` objects. """ def __init__( @@ -31,13 +34,24 @@ def __init__( rng: Random, ): """ - Class constructor. - Important note: `c_cognitive` and `c_social` have to sum up to something greater than 4! - :param c_cognitive: constant cognitive factor to scale p_best with - :param c_social: constant social factor to scale g_best with - :param rank: the rank of the worker the propagator is living on in MPI.COMM_WORLD - :param limits: a dict with str keys and 2-tuples of floats associated to each of them - :param rng: random number generator + The class constructor. + *Important note:* `c_cognitive` and `c_social` have to sum up to something greater than 4! + + Parameters + ---------- + c_cognitive : float + Constant cognitive factor to scale the distance to the particle's personal best value with. + *Has to sum up with `c_social` to more than 4!* + c_social : float + Constant social factor to scale the distance to the swarm's global best value with. + *Has to sum up with `c_cognitive` to more than 4!* + rank : int + The global rank of the worker the propagator is living on + limits : Dict[str, Tuple[float, float]] + A dict with str keys and 2-tuples of floats associated to each of them. It describes the borders of + the search domain. + rng : random.Random + Random number generator for said non-linearity """ assert c_cognitive + c_social > 4, "c_cognitive + c_social < 4!" phi: float = c_cognitive + c_social From 596bed980006de96ebb67a442ba51d58232be912 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 12:24:31 +0200 Subject: [PATCH 125/139] Optimized docstring. --- propulate/propagators/pso/canonical.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/propulate/propagators/pso/canonical.py b/propulate/propagators/pso/canonical.py index b32f23c7..4cc7b1f1 100644 --- a/propulate/propagators/pso/canonical.py +++ b/propulate/propagators/pso/canonical.py @@ -8,14 +8,15 @@ class Canonical(Constriction): """ - This propagator subclass features a combination of constriction and velocity clamping. + This propagator subclass features a combination of constriction PSO and velocity clamping. - The velocity clamping uses a clamping factor of 1, - the constriction is done as in the parental constriction propagator. + The velocity clamping uses with a clamping factor of 1, the constriction is done as in the parental ``Constriction`` + propagator. - This variant of PSO is to be found here: - Riccardo Poli, James Kennedy, and Tim Blackwell: “Particle swarm optimization”, 2007, - https://doi.org/10.1007/s11721-007-0002-0 + For information on the method parameters, please refer to the ``Constriction`` propagator. + + Original publications: Poli, R., Kennedy, J. & Blackwell, T. Particle swarm optimization. Swarm Intell 1, 33–57 (2007). https://doi.org/10.1007/s11721-007-0002-0 + R. C. Eberhart and Y. Shi, "Comparing inertia weights and constriction factors in particle swarm optimization," Proceedings of the 2000 Congress on Evolutionary Computation. CEC00 (Cat. No.00TH8512), La Jolla, CA, USA, 2000, pp. 84-88 vol.1, doi: 10.1109/CEC.2000.870279. """ def __init__(self, c_cognitive, c_social, rank, limits, rng): From f50b0646726675688c21fa57f48cd1d5ea04aa67 Mon Sep 17 00:00:00 2001 From: Morridin Date: Thu, 21 Sep 2023 12:26:43 +0200 Subject: [PATCH 126/139] Reordered parameters of creation routine. --- propulate/propagators/pso/init_uniform.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/propulate/propagators/pso/init_uniform.py b/propulate/propagators/pso/init_uniform.py index 6673462c..2fe87ecf 100644 --- a/propulate/propagators/pso/init_uniform.py +++ b/propulate/propagators/pso/init_uniform.py @@ -19,12 +19,11 @@ class InitUniform(Stochastic): def __init__( self, limits: Dict[str, Tuple[float, float]], + rank: int, parents=0, probability=1.0, rng: Random = None, - *, v_init_limit: Union[float, np.ndarray] = 0.1, - rank: int ): """ Constructor of InitUniform pso propagator class. @@ -37,6 +36,8 @@ def __init__( a named list of tuples representing the limits in which the search space resides and where solutions can be expected to be found. Limits of (hyper-)parameters to be optimized + rank : int + The rank of the worker in MPI.COMM_WORLD parents : int number of input individuals (-1 for any) probability : float @@ -45,8 +46,6 @@ def __init__( random number generator v_init_limit: float | np.ndarray some multiplicative constant to reduce initial random velocity values. - rank : int - The rank of the worker in MPI.COMM_WORLD """ super().__init__(parents, 1, probability, rng) self.limits = limits From ebd2327a7dab2fee09c88e463e95005a3d930795 Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 11:29:40 +0200 Subject: [PATCH 127/139] remove PSO folder and create module file for PSO propagators --- propulate/propagators/pso.py | 599 ++++++++++++++++++ propulate/propagators/pso/__init__.py | 13 - propulate/propagators/pso/basic.py | 186 ------ propulate/propagators/pso/canonical.py | 78 --- propulate/propagators/pso/constriction.py | 90 --- propulate/propagators/pso/init_uniform.py | 105 --- propulate/propagators/pso/stateless.py | 92 --- .../propagators/pso/velocity_clamping.py | 94 --- 8 files changed, 599 insertions(+), 658 deletions(-) create mode 100644 propulate/propagators/pso.py delete mode 100644 propulate/propagators/pso/__init__.py delete mode 100644 propulate/propagators/pso/basic.py delete mode 100644 propulate/propagators/pso/canonical.py delete mode 100644 propulate/propagators/pso/constriction.py delete mode 100644 propulate/propagators/pso/init_uniform.py delete mode 100644 propulate/propagators/pso/stateless.py delete mode 100644 propulate/propagators/pso/velocity_clamping.py diff --git a/propulate/propagators/pso.py b/propulate/propagators/pso.py new file mode 100644 index 00000000..876dfbc5 --- /dev/null +++ b/propulate/propagators/pso.py @@ -0,0 +1,599 @@ +import logging +from random import Random +from typing import Dict, Tuple, Union, List + +import numpy as np + +from ..propagators import Propagator, Stochastic +from ..population import Particle, Individual +from ..utils import make_particle + + +class Basic(Propagator): + """ + This propagator implements the most basic PSO variant one possibly could think of. + + It features an inertia factor applied to the old velocity in the velocity update, a social and a cognitive factor. + + With the help of the random number generator required as creation parameter, non-linearity is added to the particle + update in order to not collapse to linear regression. + + This basic PSO propagator can only explore real-valued search spaces, i.e., continuous parameters. + It works on ``Particle`` objects and serves as the foundation of all other PSO propagators. + Further PSO propagators should be derived from this propagator or from one that is derived from this. + + This variant was first proposed in 1998 by Y. Shi and R. Eberhart, "A modified particle swarm optimizer" + https://doi.org/10.1109/ICEC.1998.699146. + """ + + def __init__( + self, + inertia: float, + c_cognitive: float, + c_social: float, + rank: int, + limits: Dict[str, Tuple[float, float]], + rng: Random, + ): + """ + Instantiate a basic PSO propagator. + + In theory, it should be no problem to hand over numpy arrays instead of the float-type hyperparameters inertia, + cognitive factor, and social factor. In this case, please ensure that the dimension of the passed arrays fits + the search domain. + + Parameters + ---------- + inertia : float + inertia weight. + c_cognitive : float + constant cognitive factor for scaling the distance to the particle's personal best value + c_social : float + constant social factor for scaling the distance to the swarm's global best value + rank : int + global rank of the worker the propagator is living on + limits : Dict[str, Tuple[float, float]] + borders of the continuous search domain + rng : random.Random + random number generator for introducing non-linearity + """ + super().__init__(parents=-1, offspring=1) + self.c_social = c_social + self.c_cognitive = c_cognitive + self.inertia = inertia + self.rank = rank + self.limits = limits + self.rng = rng + self.limits_as_array: np.ndarray = np.array(list(limits.values())).T + + def __call__(self, individuals: List[Individual]) -> Particle: + """ + Apply the standard PSO update rule with inertia. + + Return a ``Particle`` object containing the updated values of the youngest passed ``Particle`` or ``Individual`` + that belongs to the worker the propagator is living on. + + Parameters + ---------- + individuals: List[Individual] + list of individuals that must at least contain one individual that belongs to the propagator + This list is used to calculate personal and global best of the particle and the swarm, + respectively, and then to update the particle based on the retrieved results. Individuals that + cannot be used as ``Particle`` objects are converted to particles first. + + Returns + ------- + propulate.population.Particle + updated particle + """ + old_p, p_best, g_best = self._prepare_data(individuals) + + new_velocity: np.ndarray = ( + self.inertia * old_p.velocity + + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) + ) + new_position: np.ndarray = old_p.position + new_velocity + + return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) + + def _prepare_data( + self, individuals: List[Individual] + ) -> Tuple[Particle, Particle, Particle]: + """ + Given a list of ``Individual`` or ``Particle`` objects, determine the particle to be updated on this rank, its + current personal best, and the currently known global best of the swarm to perform a particle update step. + + Parameters + ---------- + individuals : List[Individual] + ``Individual`` objects that shall be used as data basis for a PSO update step + + Returns + ------- + Tuple[propulate.population.Particle, propulate.population.Particle, propulate.population.Particle] + The following particles in this very order: + 1. old_p: the current particle to be updated now + 2. p_best: the personal best value of this particle + 3. g_best: the global best value currently known + """ + if len(individuals) < self.offspring: + raise ValueError("Not enough Particles") + + particles = [] + for individual in individuals: + if isinstance(individual, Particle): + particles.append(individual) + else: + particles.append(make_particle(individual)) + logging.warning( + f"Got Individual instead of Particle. If this is on purpose, you can ignore this warning. " + f"Converted the Individual to Particle. Continuing." + ) + + own_p = [ + x + for x in particles + if (isinstance(x, Particle) and x.global_rank == self.rank) + or x.rank == self.rank + ] + if len(own_p) > 0: + old_p: Individual = max(own_p, key=lambda p: p.generation) + if not isinstance(old_p, Particle): + old_p = make_particle(old_p) + + else: + victim = max(particles, key=lambda p: p.generation) + old_p = self._make_new_particle( + victim.position, victim.velocity, victim.generation + ) + + g_best = min(particles, key=lambda p: p.loss) + p_best = min(own_p, key=lambda p: p.loss) + + return old_p, p_best, g_best + + def _make_new_particle( + self, position: np.ndarray, velocity: np.ndarray, generation: int + ) -> Particle: + """ + Create a new ``Particle`` with the position dictionary set to the values provided by the numpy array. + + Parameters + ---------- + position : np.ndarray + position of the particle to be created + velocity : np.ndarray + velocity of the particle to be created + generation : int + generation of the new particle + + Returns + ------- + propulate.population.Particle + new ``Particle`` object resulting from the PSO update step + """ + new_p = Particle(position, velocity, generation, self.rank) + for i, k in enumerate(self.limits): + new_p[k] = new_p.position[i] + return new_p + + +class VelocityClamping(Basic): + """ + This propagator implements velocity clamping PSO. + + In addition to the parameters known from the basic PSO propagator, it features a clamping factor within [0, 1] used + to determine each parameter's maximum velocity value relative to its search-space limits. + + Based on these values, the velocities of the particles are cut down to a reasonable value. + """ + + def __init__( + self, + inertia: float, + c_cognitive: float, + c_social: float, + rank: int, + limits: Dict[str, Tuple[float, float]], + rng: Random, + v_limits: Union[float, np.ndarray], + ): + """ + Instantiate a velocity clamping PSO propagator. + + Parameters + ---------- + inertia : float + inertia factor + c_cognitive : float + constant cognitive factor for scaling the distance to the particle's personal best value + c_social : float + Constant social factor for scaling the distance to the swarm's global best value + rank : int + global rank of the worker the propagator is living on + limits : Dict[str, Tuple[float, float]] + borders of the continuous search domain + rng : random.Random + random number generator for introducing non-linearity + v_limits : Union[float, np.ndarray] + clamping factor to be multiplied with the clamping limit in order to reduce it further + Should be in (0, 1). If this parameter has float type, it is applied to all dimensions of the search + domain; else, each of its elements is applied to the corresponding dimension of the search domain. + """ + super().__init__(inertia, c_cognitive, c_social, rank, limits, rng) + x_min, x_max = self.limits_as_array + x_range = abs(x_max - x_min) + v_limits = abs(v_limits) + self.v_cap: np.ndarray = np.array([-v_limits * x_range, v_limits * x_range]) + + def __call__(self, individuals: List[Individual]) -> Particle: + """ + Apply the standard PSO update rule with inertia, extended by cutting off too high velocities. + + Return a ``Particle`` object containing the updated values of the youngest passed ``Particle`` or ``Individual`` + that belongs to the worker the propagator is living on. + + Parameters + ---------- + individuals: List[Individual] + list of individuals that must at least contain one individual that belongs to the propagator + This list is used to calculate personal and global best of the particle and the swarm, + respectively, and then to update the particle based on the retrieved results. Individuals that + cannot be used as ``Particle`` objects are converted to particles first. + + Returns + ------- + propulate.population.Particle + updated particle + """ + old_p, p_best, g_best = self._prepare_data(individuals) + + new_velocity: np.ndarray = ( + self.inertia * old_p.velocity + + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) + ).clip(*self.v_cap) + new_position: np.ndarray = old_p.position + new_velocity + + return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) + + +class Constriction(Basic): + """ + This propagator subclass features constriction PSO as proposed by Clerc and Kennedy in 2002. + + Reference publication: R. Poli, J. Kennedy, and T. Blackwell. Particle swarm optimization. Swarm Intell 1, 33–57 + (2007). https://doi.org/10.1007/s11721-007-0002-0 + + Instead of an inertia factor that affects the old velocity value within the velocity update, a constriction factor + is applied to the new velocity *after* the update. + + The constriction factor is calculated from cognitive and social factors and thus no separate hyperparameter. + + This propagator runs on ``Particle`` objects. + """ + + def __init__( + self, + c_cognitive: float, + c_social: float, + rank: int, + limits: Dict[str, Tuple[float, float]], + rng: Random, + ): + """ + Instantiate a constriction PSO propagator. + + *Important note:* ``c_cognitive`` and ``c_social`` have to sum up to a number greater than 4! + + Parameters + ---------- + c_cognitive : float + constant cognitive factor for scaling the distance to the particle's personal best value + *Has to sum up with ``c_social`` to a number greater than 4!* + c_social : float + Constant social factor for scaling the distance to the swarm's global best value + *Has to sum up with ``c_cognitive`` to a number greater than 4!* + rank : int + The global rank of the worker the propagator is living on + limits : Dict[str, Tuple[float, float]] + borders of the continuous search domain + rng : random.Random + random number generator for introducing non-linearity + + Raises + ------ + ValueError + If ``c_social`` and ``c_cognitive`` do not sum up to a number greater than 4. + """ + if c_cognitive + c_social <= 4: + raise ValueError( + "c_cognitive + c_social < 4 but should sum up to a number > 4!" + ) + phi: float = c_cognitive + c_social + chi: float = 2.0 / (phi - 2.0 + np.sqrt(phi * (phi - 4.0))) + super().__init__(chi, c_cognitive, c_social, rank, limits, rng) + + def __call__(self, individuals: List[Individual]) -> Particle: + """ + Apply the constriction PSO update rule. + + Return a ``Particle`` object containing the updated values of the youngest passed ``Particle`` or ``Individual`` + that belongs to the worker the propagator is living on. + + Parameters + ---------- + individuals: List[Individual] + list of individuals that must at least contain one individual that belongs to the propagator + This list is used to calculate personal and global best of the particle and the swarm, + respectively, and then to update the particle based on the retrieved results. Individuals that + cannot be used as ``Particle`` objects are converted to particles first. + + Returns + ------- + propulate.population.Particle + updated particle + """ + old_p, p_best, g_best = self._prepare_data(individuals) + + new_velocity = self.inertia * ( + old_p.velocity + + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) + + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) + ) + new_position = old_p.position + new_velocity + + return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) + + +class Canonical(Constriction): + """ + This propagator subclass features a combination of constriction PSO and velocity clamping. + + The velocity clamping uses a clamping factor of 1, the constriction is done as in the parental ``Constriction`` + propagator. + + For information on the method parameters, please refer to the ``Constriction`` propagator. + + Original publications: + R. Poli, J. Kennedy, and T. Blackwell. Particle swarm optimization. Swarm Intell 1, 33–57 (2007). + https://doi.org/10.1007/s11721-007-0002-0 + R. C. Eberhart and Y. Shi. Comparing inertia weights and constriction factors in particle swarm optimization. + Proceedings of the 2000 Congress on Evolutionary Computation. CEC00 (Cat. No.00TH8512), La Jolla, CA, USA, 2000, + pp. 84-88 vol.1, https://10.1109/CEC.2000.870279. + + See Also + -------- + :class:`ParentClassName` : Parent class + """ + + def __init__(self, c_cognitive, c_social, rank, limits, rng): + """ + Initialize a canonical PSO propagator. + + In theory, it should be no problem to hand over numpy arrays instead of the float-type hyperparameters inertia, + cognitive factor, and social factor. In this case, please ensure that the dimension of the passed arrays fits + the search domain. + + Parameters + ---------- + c_cognitive : float + constant cognitive factor for scaling the distance to the particle's personal best value + c_social : float + constant social factor to scaling the distance to the swarm's global best value + rank : int + global rank of the worker the propagator is living on + limits : Dict[str, Tuple[float, float]] + Borders of the continuous search domain + rng : random.Random + random number generator for introducing non-linearity + """ + super().__init__(c_cognitive, c_social, rank, limits, rng) + x_min, x_max = self.limits_as_array + x_range = np.abs(x_max - x_min) + self.v_cap: np.ndarray = np.array([-x_range, x_range]) + + def __call__(self, individuals: List[Individual]) -> Particle: + """ + Apply the canonical PSO variant update rule. + + Return a ``Particle`` object containing the updated values of the youngest passed ``Particle`` or ``Individual`` + that belongs to the worker the propagator is living on. + + Parameters + ---------- + individuals: List[Individual] + list of individuals that must at least contain one individual that belongs to the propagator + This list is used to calculate personal and global best of the particle and the swarm, + respectively, and then to update the particle based on the retrieved results. Individuals that + cannot be used as ``Particle`` objects are converted to particles first. + + Returns + ------- + propulate.population.Particle + update particle + """ + # Abuse Constriction's update rule, so I don't have to rewrite it. + victim = super().__call__(individuals) + + # Set new position and speed. + v = victim.velocity.clip(*self.v_cap) + p = victim.position - victim.velocity + v + + # Create and return new particle. + return self._make_new_particle(p, v, victim.generation) + + +class InitUniform(Stochastic): + """ + Initialize ``Particle`` by uniformly sampling specified limits for each trait. + """ + + def __init__( + self, + limits: Dict[str, Tuple[float, float]], + rank: int, + parents=0, + probability=1.0, + rng: Random = None, + v_init_limit: Union[float, np.ndarray] = 0.1, + ): + """ + Instantiate a uniform-initialization PSO propagator. + + In case of parents > 0 and probability < 1., call returns input individual without change. + + Parameters + ---------- + limits : dict[str, tuple[float, float]] + limits of the search space, i.e., limits of (hyper-)parameters to be optimized + rank : int + rank of the worker in MPI.COMM_WORLD + parents : int + number of input individuals (-1 for any) + probability : float + probability of creating a completely new individual + rng : random.Random + random number generator + v_init_limit: float | np.ndarray + multiplicative constant to reduce initial random velocity values + """ + super().__init__(parents, 1, probability, rng) + self.limits = limits + self.laa = np.array(list(limits.values())).T + if isinstance(v_init_limit, np.ndarray): + assert v_init_limit.shape[-1] == self.laa.shape[-1] + self.v_limits = v_init_limit + self.rank = rank + + def __call__(self, individuals: List[Individual]) -> Particle: + """ + Apply uniform-initialization propagator. + + Parameters + ---------- + individuals : List[Individual] + individuals the propagator is applied to + + Returns + ------- + propulate.population.Particle + one particle object + """ + if ( + len(individuals) == 0 or self.rng.random() < self.probability + ): # Apply only with specified `probability`. + position = np.array( + [self.rng.uniform(*self.laa[..., i]) for i in range(self.laa.shape[-1])] + ) + velocity = np.array( + [ + self.rng.uniform(*(self.v_limits * self.laa)[..., i]) + for i in range(self.laa.shape[-1]) + ] + ) + + particle = Particle( + position, velocity, rank=self.rank + ) # Instantiate new particle. + + for index, limit in enumerate(self.limits): + # Since Py 3.7, iterating over dicts is stable, so we can do the following. + + if not isinstance( + self.limits[limit][0], float + ): # Check search space for validity + raise TypeError("PSO only works on continuous search spaces!") + + # Randomly sample from specified limits for each trait. + particle[limit] = particle.position[index] + return particle + else: + particle = individuals[0] + if isinstance(particle, Particle): + return particle # Return 1st input individual w/o changes. + else: + return make_particle(particle) + + +class Stateless(Propagator): + """ + This propagator performs PSO without the need of Particles, but as a consequence, also without velocity. + Thus, it is called stateless. + + As this propagator works without velocity, there is also no inertia weight used. + + It uses only classes provided by vanilla Propulate. + """ + + def __init__( + self, + c_cognitive: float, + c_social: float, + rank: int, + limits: Dict[str, Tuple[float, float]], + rng: Random, + ): + """ + Instantiate a stateless PSO propagator. + + Parameters + ---------- + c_cognitive : float + constant cognitive factor for scaling individual's personal best value + c_social : float + constant social factor for scaling swarm's global best value + rank : int + global rank of the worker the propagator is living on + limits : Dict[str, Tuple[float, float] + Borders of the continuous search domain + rng : random.Random + random number generator required for non-linearity of update + """ + super().__init__(parents=-1, offspring=1) + self.c_social = c_social + self.c_cognitive = c_cognitive + self.rank = rank + self.limits = limits + self.rng = rng + + def __call__(self, individuals: List[Individual]) -> Individual: + """ + Apply standard PSO update without inertia and old velocity. + + Parameters + ---------- + individuals : List[Individual] + individuals are used as data basis for the PSO update + + Returns + ------- + propulate.population.Individual + An updated Individual + + Raises + ------ + ValueError + If the individuals list passed is empty and the propagator thus has no data to work on. + """ + if len(individuals) < self.offspring: + raise ValueError("Not enough particles.") + own_p = [x for x in individuals if x.rank == self.rank] + if len(own_p) > 0: + old_p = max(own_p, key=lambda p: p.generation) + else: # No own particle found in given parameters, thus creating new one. + old_p = Individual(0, self.rank) + for k in self.limits: + old_p[k] = self.rng.uniform(*self.limits[k]) + return old_p + g_best = min(individuals, key=lambda p: p.loss) + p_best = min(own_p, key=lambda p: p.loss) + new_p = Individual(generation=old_p.generation + 1) + for k in self.limits: + new_p[k] = ( + old_p[k] + + self.rng.uniform(0, self.c_cognitive) * (p_best[k] - old_p[k]) + + self.rng.uniform(0, self.c_social) * (g_best[k] - old_p[k]) + ) + return new_p diff --git a/propulate/propagators/pso/__init__.py b/propulate/propagators/pso/__init__.py deleted file mode 100644 index 874593ae..00000000 --- a/propulate/propagators/pso/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -__all__ = [ - "InitUniform", - "Basic", - "VelocityClamping", - "Constriction", - "Canonical", -] - -from .basic import Basic -from .canonical import Canonical -from .constriction import Constriction -from .init_uniform import InitUniform -from .velocity_clamping import VelocityClamping diff --git a/propulate/propagators/pso/basic.py b/propulate/propagators/pso/basic.py deleted file mode 100644 index f90023f0..00000000 --- a/propulate/propagators/pso/basic.py +++ /dev/null @@ -1,186 +0,0 @@ -""" -This file contains the original (stateful) PSO propagator for Propulate. -""" -import logging -from random import Random -from typing import Dict, Tuple, List - -import numpy as np - -from ..propagators import Propagator -from ...population import Particle, Individual -from ...utils import make_particle - - -class Basic(Propagator): - """ - This propagator implements the most basic PSO variant one possibly could think of. - - It features an inertia factor applied to the old velocity in the velocity update, - a social and a cognitive factor. - - With the help of the random number generator required as creation parameter, non-linearity is added to the particle - update in order to not collapse to linear regression. - - This basic PSO propagator can only explore real-valued search spaces, i.e., continuous parameters. - It works on ``Particle`` objects and serves as the foundation of all other PSO propagators. - Further PSO propagators should be derived from this propagator or from one that is derived from this. - - This variant was first proposed in Y. Shi and R. Eberhart. “A modified particle swarm optimizer”, 1998, - https://doi.org/10.1109/ICEC.1998.699146 - """ - - def __init__( - self, - inertia: float, - c_cognitive: float, - c_social: float, - rank: int, - limits: Dict[str, Tuple[float, float]], - rng: Random, - ): - """ - The class constructor. - - In theory, it should be of no problem to hand over numpy arrays instead of the float hyperparameters inertia, - cognitive and social factor. - Please note that in this case, you are on your own to ensure that the dimension of the passed arrays fits to the - search domain. - - Parameters - ---------- - inertia : float - The inertia weight. - c_cognitive : float - Constant cognitive factor to scale the distance to the particle's personal best value with - c_social : float - Constant social factor to scale the distance to the swarm's global best value with - rank : int - The global rank of the worker the propagator is living on - limits : Dict[str, Tuple[float, float]] - A dict with str keys and 2-tuples of floats associated to each of them. It describes the borders of - the search domain. - rng : random.Random - Random number generator for said non-linearity - """ - super().__init__(parents=-1, offspring=1) - self.c_social = c_social - self.c_cognitive = c_cognitive - self.inertia = inertia - self.rank = rank - self.limits = limits - self.rng = rng - self.limits_as_array: np.ndarray = np.array(list(limits.values())).T - - def __call__(self, individuals: List[Individual]) -> Particle: - """ - Applies the standard PSO update rule with inertia. - - Returns a Particle object that contains the updated values - of the youngest passed Particle or Individual that belongs to the worker the propagator is living on. - - Parameters - ---------- - individuals: List[Individual] - A list of individuals that must at least contain one individual that belongs to the propagator. - This list is used to calculate personal and global best of the particle and the swarm and to - then update the particle based on the retrieved results. - Individuals that cannot be used as Particle class objects are copied to particles before going on. - - Returns - ------- - propulate.population.Particle - An updated Particle. - """ - old_p, p_best, g_best = self._prepare_data(individuals) - - new_velocity: np.ndarray = ( - self.inertia * old_p.velocity - + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) - + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) - ) - new_position: np.ndarray = old_p.position + new_velocity - - return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) - - def _prepare_data( - self, individuals: List[Individual] - ) -> Tuple[Particle, Particle, Particle]: - """ - This method prepares the passed list of Individuals, that hopefully are Particles. - If they are not, they are copied over to Particle objects to avoid handling issues. - - Parameters - ---------- - individuals : List[Individual] - A list of Individual objects that shall be used as data basis for a PSO update step - - Returns - ------- - Tuple[propulate.population.Particle, propulate.population.Particle, propulate.population.Particle] - The following particles in this very order: - - 1. old_p: the current particle to be updated now - 2. p_best: the personal best value of this particle - 3. g_best: the global best value currently known - """ - if len(individuals) < self.offspring: - raise ValueError("Not enough Particles") - - particles = [] - for individual in individuals: - if isinstance(individual, Particle): - particles.append(individual) - else: - particles.append(make_particle(individual)) - logging.warning( - f"Got Individual instead of Particle. If this is on purpose, you can ignore this warning. " - f"Converted the Individual to Particle. Continuing." - ) - - own_p = [ - x - for x in particles - if (isinstance(x, Particle) and x.global_rank == self.rank) - or x.rank == self.rank - ] - if len(own_p) > 0: - old_p: Individual = max(own_p, key=lambda p: p.generation) - if not isinstance(old_p, Particle): - old_p = make_particle(old_p) - - else: - victim = max(particles, key=lambda p: p.generation) - old_p = self._make_new_particle( - victim.position, victim.velocity, victim.generation - ) - - g_best = min(particles, key=lambda p: p.loss) - p_best = min(own_p, key=lambda p: p.loss) - - return old_p, p_best, g_best - - def _make_new_particle( - self, position: np.ndarray, velocity: np.ndarray, generation: int - ) -> Particle: - """ - Takes the necessary data to create a new Particle with the position dict set to the correct values. - - Parameters - ---------- - position : np.ndarray - An array containing the position of the particle to be created - velocity : np.ndarray - An array containing the velocity of the particle to be created - generation : int - The generation of the new particle - - Returns - ------- - propulate.population.Particle - The newly created Particle object that results from the PSO update. - """ - new_p = Particle(position, velocity, generation, self.rank) - for i, k in enumerate(self.limits): - new_p[k] = new_p.position[i] - return new_p diff --git a/propulate/propagators/pso/canonical.py b/propulate/propagators/pso/canonical.py deleted file mode 100644 index 4cc7b1f1..00000000 --- a/propulate/propagators/pso/canonical.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import List - -import numpy as np - -from .constriction import Constriction -from ...population import Individual, Particle - - -class Canonical(Constriction): - """ - This propagator subclass features a combination of constriction PSO and velocity clamping. - - The velocity clamping uses with a clamping factor of 1, the constriction is done as in the parental ``Constriction`` - propagator. - - For information on the method parameters, please refer to the ``Constriction`` propagator. - - Original publications: Poli, R., Kennedy, J. & Blackwell, T. Particle swarm optimization. Swarm Intell 1, 33–57 (2007). https://doi.org/10.1007/s11721-007-0002-0 - R. C. Eberhart and Y. Shi, "Comparing inertia weights and constriction factors in particle swarm optimization," Proceedings of the 2000 Congress on Evolutionary Computation. CEC00 (Cat. No.00TH8512), La Jolla, CA, USA, 2000, pp. 84-88 vol.1, doi: 10.1109/CEC.2000.870279. - """ - - def __init__(self, c_cognitive, c_social, rank, limits, rng): - """ - The class constructor. - - In theory, it should be of no problem to hand over numpy arrays instead of the float hyperparameters cognitive - and social factor. - Please note that in this case, you are on your own to ensure that the dimension of the passed arrays fits to the - search domain. - - Parameters - ---------- - c_cognitive : float - Constant cognitive factor to scale the distance to the particle's personal best value with - c_social : float - Constant social factor to scale the distance to the swarm's global best value with - rank : int - The global rank of the worker the propagator is living on - limits : Dict[str, Tuple[float, float]] - A dict with str keys and 2-tuples of floats associated to each of them. It describes the borders of - the search domain. - rng : random.Random - Random number generator for said non-linearity - """ - super().__init__(c_cognitive, c_social, rank, limits, rng) - x_min, x_max = self.limits_as_array - x_range = np.abs(x_max - x_min) - self.v_cap: np.ndarray = np.array([-x_range, x_range]) - - def __call__(self, individuals: List[Individual]) -> Particle: - """ - Applies the canonical PSO variant update rule. - - Returns a Particle object that contains the updated values of the youngest passed Particle or Individual that - belongs to the worker the propagator is living on. - - Parameters - ---------- - individuals: List[Individual] - A list of individuals that must at least contain one individual that belongs to the propagator. - This list is used to calculate personal and global best of the particle and the swarm and to - then update the particle based on the retrieved results. - Individuals that cannot be used as Particle class objects are copied to Particles before going on. - - Returns - ------- - propulate.population.Particle - An updated Particle. - """ - # Abuse Constriction's update rule, so I don't have to rewrite it. - victim = super().__call__(individuals) - - # Set new position and speed. - v = victim.velocity.clip(*self.v_cap) - p = victim.position - victim.velocity + v - - # create and return new particle. - return self._make_new_particle(p, v, victim.generation) diff --git a/propulate/propagators/pso/constriction.py b/propulate/propagators/pso/constriction.py deleted file mode 100644 index 96746200..00000000 --- a/propulate/propagators/pso/constriction.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -This file contains a Propagator subclass providing constriction-flavoured pso. -""" -from random import Random -from typing import List, Dict, Tuple - -import numpy as np - -from .basic import Basic -from ...population import Individual, Particle - - -class Constriction(Basic): - """ - This propagator subclass features constriction PSO as proposed by Clerc and Kennedy in 2002. - - Reference publication: Poli, R., Kennedy, J. & Blackwell, T. Particle swarm optimization. Swarm Intell 1, - 33–57 (2007). https://doi.org/10.1007/s11721-007-0002-0 - - Instead of an inertia factor that affects the old velocity value within the velocity update, a constriction factor - is applied to the new velocity *after* the update. - - The constriction factor is calculated from cognitive and social factors and thus no hyperparameter. - - This propagator runs on ``Particle`` objects. - """ - - def __init__( - self, - c_cognitive: float, - c_social: float, - rank: int, - limits: Dict[str, Tuple[float, float]], - rng: Random, - ): - """ - The class constructor. - *Important note:* `c_cognitive` and `c_social` have to sum up to something greater than 4! - - Parameters - ---------- - c_cognitive : float - Constant cognitive factor to scale the distance to the particle's personal best value with. - *Has to sum up with `c_social` to more than 4!* - c_social : float - Constant social factor to scale the distance to the swarm's global best value with. - *Has to sum up with `c_cognitive` to more than 4!* - rank : int - The global rank of the worker the propagator is living on - limits : Dict[str, Tuple[float, float]] - A dict with str keys and 2-tuples of floats associated to each of them. It describes the borders of - the search domain. - rng : random.Random - Random number generator for said non-linearity - """ - assert c_cognitive + c_social > 4, "c_cognitive + c_social < 4!" - phi: float = c_cognitive + c_social - chi: float = 2.0 / (phi - 2.0 + np.sqrt(phi * (phi - 4.0))) - super().__init__(chi, c_cognitive, c_social, rank, limits, rng) - - def __call__(self, individuals: List[Individual]) -> Particle: - """ - Applies the constriction PSO update rule. - - Returns a Particle object that contains the updated values of the youngest passed Particle or Individual that - belongs to the worker the propagator is living on. - - Parameters - ---------- - individuals: List[Individual] - A list of individuals that must at least contain one individual that belongs to the propagator. - This list is used to calculate personal and global best of the particle and the swarm and to - then update the particle based on the retrieved results. - Individuals that cannot be used as Particle class objects are copied to particles before going on. - - Returns - ------- - propulate.population.Particle - An updated Particle. - """ - old_p, p_best, g_best = self._prepare_data(individuals) - - new_velocity = self.inertia * ( - old_p.velocity - + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) - + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) - ) - new_position = old_p.position + new_velocity - - return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) diff --git a/propulate/propagators/pso/init_uniform.py b/propulate/propagators/pso/init_uniform.py deleted file mode 100644 index 2fe87ecf..00000000 --- a/propulate/propagators/pso/init_uniform.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -This file contains a propagator to initialize a population of either ``Individuals`` or ``Particles``. -""" -from random import Random -from typing import Union, Dict, Tuple, List - -import numpy as np - -from ..propagators import Stochastic -from ...population import Individual, Particle -from ...utils import make_particle - - -class InitUniform(Stochastic): - """ - Initialize ``Particles`` by uniformly sampling specified limits for each trait. - """ - - def __init__( - self, - limits: Dict[str, Tuple[float, float]], - rank: int, - parents=0, - probability=1.0, - rng: Random = None, - v_init_limit: Union[float, np.ndarray] = 0.1, - ): - """ - Constructor of InitUniform pso propagator class. - - In case of parents > 0 and probability < 1., call returns input individual without change. - - Parameters - ---------- - limits : dict - a named list of tuples representing the limits in which the search space resides and where - solutions can be expected to be found. - Limits of (hyper-)parameters to be optimized - rank : int - The rank of the worker in MPI.COMM_WORLD - parents : int - number of input individuals (-1 for any) - probability : float - the probability with which a completely new individual is created - rng : random.Random - random number generator - v_init_limit: float | np.ndarray - some multiplicative constant to reduce initial random velocity values. - """ - super().__init__(parents, 1, probability, rng) - self.limits = limits - self.laa = np.array(list(limits.values())).T - if isinstance(v_init_limit, np.ndarray): - assert v_init_limit.shape[-1] == self.laa.shape[-1] - self.v_limits = v_init_limit - self.rank = rank - - def __call__(self, individuals: List[Individual]) -> Particle: - """ - Apply uniform-initialization propagator. - - Parameters - ---------- - individuals : List[Individual] - individuals the propagator is applied to - - Returns - ------- - propulate.population.Particle - One particle object - """ - if ( - len(individuals) == 0 or self.rng.random() < self.probability - ): # Apply only with specified `probability`. - position = np.array( - [self.rng.uniform(*self.laa[..., i]) for i in range(self.laa.shape[-1])] - ) - velocity = np.array( - [ - self.rng.uniform(*(self.v_limits * self.laa)[..., i]) - for i in range(self.laa.shape[-1]) - ] - ) - - particle = Particle( - position, velocity, rank=self.rank - ) # Instantiate new particle. - - for index, limit in enumerate(self.limits): - # Since Py 3.7, iterating over dicts is stable, so we can do the following. - - if not isinstance( - self.limits[limit][0], float - ): # Check search space for validity - raise TypeError("PSO only works on continuous search spaces!") - - # Randomly sample from specified limits for each trait. - particle[limit] = particle.position[index] - return particle - else: - particle = individuals[0] - if isinstance(particle, Particle): - return particle # Return 1st input individual w/o changes. - else: - return make_particle(particle) diff --git a/propulate/propagators/pso/stateless.py b/propulate/propagators/pso/stateless.py deleted file mode 100644 index 9759f547..00000000 --- a/propulate/propagators/pso/stateless.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -This file contains a prototype proof-of-concept propagator to run PSO in Propulate. -""" - -from random import Random -from typing import Dict, Tuple, List - -from ..propagators import Propagator -from ...population import Individual - - -class Stateless(Propagator): - """ - This propagator performs PSO without the need of Particles, but as a consequence, also without velocity. - Thus, it is called stateless. - - As this propagator works without velocity, there is also no inertia weight used. - - It uses only classes provided by vanilla Propulate. - """ - - def __init__( - self, - c_cognitive: float, - c_social: float, - rank: int, - limits: Dict[str, Tuple[float, float]], - rng: Random, - ): - """ - The class constructor. - - Parameters - ---------- - c_cognitive : float - Constant cognitive factor to scale individual's personal best value with - c_social : float - Constant social factor to scale swarm's global best value with - rank : int - The global rank of the worker the propagator is living on - limits : Dict[str, Tuple[float, float] - A dict with str keys and 2-tuples of floats associated to each of them describing the borders of - the search domain. - rng : random.Random - The random number generator required for non-linearity of update. - """ - super().__init__(parents=-1, offspring=1) - self.c_social = c_social - self.c_cognitive = c_cognitive - self.rank = rank - self.limits = limits - self.rng = rng - - def __call__(self, individuals: List[Individual]) -> Individual: - """ - Apply standard PSO update without inertia and old velocity. - - Parameters - ---------- - individuals : List[Individual] - The individual that are used as data basis for the PSO update - - Returns - ------- - propulate.population.Individual - An updated Individual - - Raises - ------ - ValueError - If the individuals list passed is empty and the propagator thus has no data to work on. - """ - if len(individuals) < self.offspring: - raise ValueError("Not enough Particles") - own_p = [x for x in individuals if x.rank == self.rank] - if len(own_p) > 0: - old_p = max(own_p, key=lambda p: p.generation) - else: # No own particle found in given parameters, thus creating new one. - old_p = Individual(0, self.rank) - for k in self.limits: - old_p[k] = self.rng.uniform(*self.limits[k]) - return old_p - g_best = min(individuals, key=lambda p: p.loss) - p_best = min(own_p, key=lambda p: p.loss) - new_p = Individual(generation=old_p.generation + 1) - for k in self.limits: - new_p[k] = ( - old_p[k] - + self.rng.uniform(0, self.c_cognitive) * (p_best[k] - old_p[k]) - + self.rng.uniform(0, self.c_social) * (g_best[k] - old_p[k]) - ) - return new_p diff --git a/propulate/propagators/pso/velocity_clamping.py b/propulate/propagators/pso/velocity_clamping.py deleted file mode 100644 index 13e00c33..00000000 --- a/propulate/propagators/pso/velocity_clamping.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -This file contains a PSO propagator relying on the standard one but additionally performing velocity clamping. -""" -from random import Random -from typing import Dict, Tuple, Union, List - -import numpy as np - -from .basic import Basic -from ...population import Individual, Particle - - -class VelocityClamping(Basic): - """ - This propagator implements velocity clamping PSO. - - In addition to the parameters known from the basic PSO - propagator, it features a clamping factor within [0, 1] used to determine each parameter's maximum velocity value - relative to its search-space limits. - - Based on these values, the velocities of the particles are cut down to a - reasonable value. - """ - - def __init__( - self, - inertia: float, - c_cognitive: float, - c_social: float, - rank: int, - limits: Dict[str, Tuple[float, float]], - rng: Random, - v_limits: Union[float, np.ndarray], - ): - """ - The class constructor. - - Parameters - ---------- - inertia : float - The particle's inertia factor - c_cognitive : float - Constant cognitive factor to scale the distance to the particle's personal best value with - c_social : float - Constant social factor to scale the distance to the swarm's global best value with - rank : int - The global rank of the worker the propagator is living on - limits : Dict[str, Tuple[float, float]] - A dict with str keys and 2-tuples of floats associated to each of them. It describes the borders of - the search domain. - rng : random.Random - Random number generator for said non-linearity - v_limits : Union[float, np.ndarray] - This parameter is multiplied with the clamping limit in order to reduce it further in most cases - (this is, when the value is in (0; 1)). - If this parameter has float type, it is applied to all dimensions of the search domain, else, - each of its elements are applied to their corresponding dimension of the search domain. - """ - super().__init__(inertia, c_cognitive, c_social, rank, limits, rng) - x_min, x_max = self.limits_as_array - x_range = abs(x_max - x_min) - v_limits = abs(v_limits) - self.v_cap: np.ndarray = np.array([-v_limits * x_range, v_limits * x_range]) - - def __call__(self, individuals: List[Individual]) -> Particle: - """ - Applies the standard PSO update rule with inertia, extended by cutting off too high velocities. - - Returns a Particle object that contains the updated values of the youngest passed Particle or Individual that - belongs to the worker the propagator is living on. - - Parameters - ---------- - individuals: List[Individual] - A list of individuals that must at least contain one individual that belongs to the propagator. - This list is used to calculate personal and global best of the particle and the swarm and to - then update the particle based on the retrieved results. - Individuals that cannot be used as Particle class objects are copied to particles before going on. - - Returns - ------- - propulate.population.Particle - An updated Particle. - """ - old_p, p_best, g_best = self._prepare_data(individuals) - - new_velocity: np.ndarray = ( - self.inertia * old_p.velocity - + self.rng.uniform(0, self.c_cognitive) * (p_best.position - old_p.position) - + self.rng.uniform(0, self.c_social) * (g_best.position - old_p.position) - ).clip(*self.v_cap) - new_position: np.ndarray = old_p.position + new_velocity - - return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) From b721b1034b93af6194a79e4b69e85610b879977d Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 11:49:25 +0200 Subject: [PATCH 128/139] refactored base propagator classes and functions into separate file --- propulate/propagators/base.py | 467 ++++++++ propulate/propagators/propagators.py | 1635 -------------------------- 2 files changed, 467 insertions(+), 1635 deletions(-) create mode 100644 propulate/propagators/base.py delete mode 100644 propulate/propagators/propagators.py diff --git a/propulate/propagators/base.py b/propulate/propagators/base.py new file mode 100644 index 00000000..c69d9922 --- /dev/null +++ b/propulate/propagators/base.py @@ -0,0 +1,467 @@ +import copy +import random +from typing import List, Dict, Union, Tuple + +import numpy as np +from abc import ABC, abstractmethod + +from ..population import Individual + + +def _check_compatible(out1: int, in2: int) -> bool: + """ + Check compatibility of two propagators for stacking them together sequentially with ``Compose``. + + Parameters + ---------- + out1: int + number of output individuals returned by first propagator + in2: int + number of input individuals taken by second propagator + + Returns + ------- + bool + True if propagators can be stacked, False if not. + """ + return out1 == in2 or in2 == -1 + + +class Propagator: + """ + Abstract base class for all propagators, i.e., evolutionary operators. + + A propagator takes a collection of individuals and uses them to breed a new collection of individuals. + """ + + def __init__( + self, parents: int = 0, offspring: int = 0, rng: random.Random = None + ) -> None: + """ + Initialize a propagator with given parameters. + + Parameters + ---------- + parents: int + number of input individuals (-1 for any) + offspring: int + number of output individuals + rng: random.Random + random number generator + + Raises + ------ + ValueError + If the number of offspring to breed is zero. + """ + if offspring == 0: + raise ValueError("Propagator has to sire more than 0 offspring.") + self.offspring = offspring # Number of offspring individuals to breed + self.parents = parents # Number of parent individuals + self.rng = rng # Random number generator + + def __call__(self, inds: List[Individual]) -> Union[List[Individual], Individual]: + """ + Apply the propagator (not implemented for abstract base class). + + Parameters + ---------- + inds: list[propulate.individual.Individual] + input individuals the propagator is applied to + + Returns + ------- + list[Individual] | Individual + individual(s) bred by applying the propagator + While this abstract base class method actually returns ``None``, each concrete child class of ``Propagator`` + should return an ``Individual`` instance or a list of them. + + Raises + ------ + NotImplementedError + Whenever called (abstract base class method). + """ + raise NotImplementedError() + + +class Stochastic(Propagator): + """ + Apply a propagator with a given probability. + + If the propagator is not applied, the output still has to adhere to the defined number of offspring. + """ + + def __init__( + self, + parents: int = 0, + offspring: int = 0, + probability: float = 1.0, + rng: random.Random = None, + ) -> None: + """ + Initialize a stochastic propagator that is only applied with a specified probability. + + Parameters + ---------- + parents: int + number of input individuals (-1 for any) + offspring: int + number of output individuals + probability: float + probability of application + rng: random.Random + random number generator + + Raises + ------ + ValueError + If the number of offspring to breed is zero. + """ + super(Stochastic, self).__init__(parents, offspring, rng) + self.probability = probability + if offspring == 0: + raise ValueError("Propagator has to sire more than 0 offspring.") + + +class Conditional(Propagator): + """ + Apply different propagators depending on whether the breeding population is already large enough or not. + + If the population consists of the specified number of individuals required for breeding (or more), + a different propagator is applied than if not. + """ + + def __init__( + self, + pop_size: int, + true_prop: Propagator, + false_prop: Propagator, + parents: int = -1, + offspring: int = -1, + ) -> None: + """ + Initialize the conditional propagator. + + Parameters + ---------- + pop_size: int + breeding population size + true_prop: propulate.propagators.Propagator + propagator applied if size of current population >= pop_size. + false_prop: propulate.propagators.Propagator + propagator applied if size of current population < pop_size. + parents: int + number of input individuals (-1 for any) + offspring: int + number of output individuals + """ + super(Conditional, self).__init__(parents, offspring) + self.pop_size = pop_size + self.true_prop = true_prop + self.false_prop = false_prop + + def __call__(self, inds: List[Individual]) -> List[Individual]: + """ + Apply conditional propagator. + + Parameters + ---------- + inds: list[propulate.individual.Individual] + input individuals the propagator is applied to + + Returns + ------- + list[propulate.individual.Individual] + output individuals returned by the conditional propagator + """ + if ( + len(inds) >= self.pop_size + ): # If number of evaluated individuals >= pop_size apply true_prop. + return self.true_prop(inds) + else: # Else apply false_prop. + return self.false_prop(inds) + + +class Compose(Propagator): + """ + Stack propagators together sequentially for successive application. + """ + + def __init__(self, propagators: List[Propagator]) -> None: + """ + Initialize composed propagator. + + Parameters + ---------- + propagators: list[propulate.propagators.Propagator] + propagators to be stacked together sequentially + + Raises + ------ + ValueError + If propagators to stack are incompatible in terms of number of input and output individuals. + """ + if len(propagators) < 1: + raise ValueError( + f"Not enough propagators given ({len(propagators)}). At least 1 is required." + ) + super(Compose, self).__init__(propagators[0].parents, propagators[-1].offspring) + for i in range(len(propagators) - 1): + # Check compatibility of consecutive propagators in terms of number of parents + offsprings. + if not _check_compatible( + propagators[i].offspring, propagators[i + 1].parents + ): + outp = propagators[i] + inp = propagators[i + 1] + outd = outp.offspring + ind = inp.parents + + raise ValueError( + f"Incompatible combination of {outd} output individuals " + f"of {outp} and {ind} input individuals of {inp}." + ) + self.propagators = propagators + + def __call__(self, inds: List[Individual]) -> List[Individual]: + """ + Apply composed propagator. + + Parameters + ---------- + inds: list[propulate.individual.Individual] + input individuals the propagator is applied to + + Returns + ------- + list[propulate.individual.Individual] + output individuals after application of propagator + """ + for p in self.propagators: + inds = p(inds) + return inds + + +class SelectMin(Propagator): + """ + Select specified number of best performing individuals as evaluated by their losses. + i.e., those individuals with minimum losses. + """ + + def __init__( + self, + offspring: int, + ) -> None: + """ + Initialize elitist selection propagator. + + Parameters + ---------- + offspring: int + number of offsprings (individuals to be selected) + """ + super(SelectMin, self).__init__(-1, offspring) + + def __call__(self, inds: List[Individual]) -> List[Individual]: + """ + Apply elitist-selection propagator. + + Parameters + ---------- + inds: list[propulate.individual.Individual] + input individuals the propagator is applied to + + Returns + ------- + list[propulate.individual.Individual] + selected output individuals after application of the propagator + + Raises + ------ + ValueError + If more individuals than put in shall be selected. + """ + if len(inds) < self.offspring: + raise ValueError( + f"Has to have at least {self.offspring} individuals to select the {self.offspring} best ones." + ) + # Sort elements of given iterable in specific order + return as list. + return sorted(inds, key=lambda ind: ind.loss)[ + : self.offspring + ] # Return `self.offspring` best individuals in terms of loss. + + +class SelectMax(Propagator): + """ + Select specified number of worst performing individuals as evaluated by their losses, + i.e., those individuals with maximum losses. + """ + + def __init__( + self, + offspring: int, + ) -> None: + """ + Initialize anti-elitist propagator. + + Parameters + ---------- + offspring: int + number of offspring (individuals to be selected) + """ + super(SelectMax, self).__init__(-1, offspring) + + def __call__(self, inds: List[Individual]) -> List[Individual]: + """ + Apply anti-elitist-selection propagator. + + Parameters + ---------- + inds: list[propulate.individual.Individual] + individuals the propagator is applied to + + Returns + ------- + list[propulate.individual.Individual] + selected individuals after application of the propagator + + Raises + ------ + ValueError + If more individuals than put in shall be selected. + """ + if len(inds) < self.offspring: + raise ValueError( + f"Has to have at least {self.offspring} individuals to select the {self.offspring} worst ones." + ) + # Sort elements of given iterable in specific order + return as list. + return sorted(inds, key=lambda ind: -ind.loss)[ + : self.offspring + ] # Return the `self.offspring` worst individuals in terms of loss. + + +class SelectUniform(Propagator): + """ + Select specified number of individuals randomly. + """ + + def __init__(self, offspring: int, rng: random.Random = None) -> None: + """ + Initialize random-selection propagator. + + Parameters + ---------- + offspring: int + number of offspring (individuals to be selected) + rng: random.Random + random number generator + """ + super(SelectUniform, self).__init__(-1, offspring, rng) + + def __call__(self, inds: List[Individual]) -> List[Individual]: + """ + Apply uniform-selection propagator. + + Parameters + ---------- + inds: list[propulate.individual.Individual] + individuals the propagator is applied to + + Returns + ------- + list[propulate.individual.Individual] + selected individuals after application of propagator + + Raises + ------ + ValueError + If more individuals than put in shall be selected. + """ + if len(inds) < self.offspring: + raise ValueError( + f"Has to have at least {self.offspring} individuals to select {self.offspring} from them." + ) + # Return a `self.offspring` length list of unique elements chosen from `particles`. + # Used for random sampling without replacement. + return self.rng.sample(inds, self.offspring) + + +# TODO parents should be fixed to one NOTE see utils reason why it is not right now +class InitUniform(Stochastic): + """ + Initialize individual by uniformly sampling specified limits for each trait. + """ + + def __init__( + self, + limits: Union[ + Dict[str, Tuple[float, float]], + Dict[str, Tuple[int, int]], + Dict[str, Tuple[str, ...]], + ], + parents: int = 0, + probability: float = 1.0, + rng: random.Random = None, + ) -> None: + """ + Initialize random-initialization propagator. + + Parameters + ---------- + limits: dict[str, tuple[float, float]] | dict[str, tuple[int, int]] | dict[str, tuple[str, ...]] + search space, i.e., limits of (hyper-)parameters to be optimized + parents: int + number of parents + probability: float + probability of application + rng: random.Random + random number generator + """ + super(InitUniform, self).__init__(parents, 1, probability, rng) + self.limits = limits + + def __call__(self, *inds: Individual) -> Individual: + """ + Apply uniform-initialization propagator. + + Parameters + ---------- + inds: propulate.individual.Individual + individuals the propagator is applied to + + Returns + ------- + propulate.individual.Individual + output individual after application of propagator + + Raises + ------ + ValueError + If a parameter's type is invalid, i.e., not float (continuous), int (ordinal), or str (categorical). + """ + if ( + self.rng.random() < self.probability + ): # Apply only with specified probability. + ind = Individual() # Instantiate new individual. + for ( + limit + ) in self.limits: # Randomly sample from specified limits for each trait. + if isinstance( + self.limits[limit][0], int + ): # If ordinal trait of type integer. + ind[limit] = self.rng.randint(*self.limits[limit]) + elif isinstance( + self.limits[limit][0], float + ): # If interval trait of type float. + ind[limit] = self.rng.uniform(*self.limits[limit]) + elif isinstance( + self.limits[limit][0], str + ): # If categorical trait of type string. + ind[limit] = self.rng.choice(self.limits[limit]) + else: + raise ValueError( + "Unknown type of limits. Has to be float for interval, " + "int for ordinal, or string for categorical." + ) + else: # Return first input individual w/o changes otherwise. + ind = inds[0] + return ind diff --git a/propulate/propagators/propagators.py b/propulate/propagators/propagators.py deleted file mode 100644 index e7b954c9..00000000 --- a/propulate/propagators/propagators.py +++ /dev/null @@ -1,1635 +0,0 @@ -import copy -import random -from typing import List, Dict, Union, Tuple - -import numpy as np -from abc import ABC, abstractmethod - -from ..population import Individual - - -def _check_compatible(out1: int, in2: int) -> bool: - """ - Check compatibility of two propagators for stacking them together sequentially with `Compose`. - - Parameters - ---------- - out1: int - number of output individuals returned by first propagator - in2: int - number of input individuals taken by second propagator - - Returns - ------- - bool - True if propagators can be stacked, False if not. - """ - return out1 == in2 or in2 == -1 - - -class Propagator: - """ - Abstract base class for all propagators, i.e., evolutionary operators. - - A propagator takes a collection of individuals and uses them to breed a new collection of individuals. - """ - - def __init__( - self, parents: int = 0, offspring: int = 0, rng: random.Random = None - ) -> None: - """ - Initialize a propagator with given parameters. - - Parameters - ---------- - parents: int - number of input individuals (-1 for any) - offspring: int - number of output individuals - rng: random.Random - random number generator - - Raises - ------ - ValueError - If the number of offspring to breed is zero. - """ - if offspring == 0: - raise ValueError("Propagator has to sire more than 0 offspring.") - self.offspring = offspring # Number of offspring individuals to breed - self.parents = parents # Number of parent individuals - self.rng = rng # Random number generator - - def __call__(self, inds: List[Individual]) -> Union[List[Individual], Individual]: - """ - Apply the propagator (not implemented for abstract base class). - - Parameters - ---------- - inds: list[propulate.individual.Individual] - input individuals the propagator is applied to - - Returns - ------- - list[Individual] | Individual - individual(s) bred by applying the propagator - While this abstract base class method actually returns ``None``, each concrete child class - of ``Propagator`` should return an individual or a list of individuals. - - Raises - ------ - NotImplementedError - Whenever called (abstract base class method). - """ - raise NotImplementedError() - - -class Stochastic(Propagator): - """ - Apply a propagator with a given probability. - - If the propagator is not applied, the output still has to adhere to the defined number of offspring. - """ - - def __init__( - self, - parents: int = 0, - offspring: int = 0, - probability: float = 1.0, - rng: random.Random = None, - ) -> None: - """ - Initialize a stochastic propagator that is only applied with a specified probability. - - Parameters - ---------- - parents: int - number of input individuals (-1 for any) - offspring: int - number of output individuals - probability: float - probability of application - rng: random.Random - random number generator - - Raises - ------ - ValueError - If the number of offspring to breed is zero. - """ - super(Stochastic, self).__init__(parents, offspring, rng) - self.probability = probability - if offspring == 0: - raise ValueError("Propagator has to sire more than 0 offspring.") - - -class Conditional(Propagator): - """ - Apply different propagators depending on whether the breeding population is already large enough or not. - - If the population consists of the specified number of individuals required for breeding (or more), - a different propagator is applied than if not. - """ - - def __init__( - self, - pop_size: int, - true_prop: Propagator, - false_prop: Propagator, - parents: int = -1, - offspring: int = -1, - ) -> None: - """ - Initialize the conditional propagator. - - Parameters - ---------- - pop_size: int - breeding population size - true_prop: propulate.propagators.Propagator - propagator applied if size of current population >= pop_size. - false_prop: propulate.propagators.Propagator - propagator applied if size of current population < pop_size. - parents: int - number of input individuals (-1 for any) - offspring: int - number of output individuals - """ - super(Conditional, self).__init__(parents, offspring) - self.pop_size = pop_size - self.true_prop = true_prop - self.false_prop = false_prop - - def __call__(self, inds: List[Individual]) -> List[Individual]: - """ - Apply conditional propagator. - - Parameters - ---------- - inds: list[propulate.individual.Individual] - input individuals the propagator is applied to - - Returns - ------- - list[propulate.individual.Individual] - output individuals returned by the conditional propagator - """ - if ( - len(inds) >= self.pop_size - ): # If number of evaluated individuals >= pop_size apply true_prop. - return self.true_prop(inds) - else: # Else apply false_prop. - return self.false_prop(inds) - - -class Compose(Propagator): - """ - Stack propagators together sequentially for successive application. - """ - - def __init__(self, propagators: List[Propagator]) -> None: - """ - Initialize composed propagator. - - Parameters - ---------- - propagators: list[propulate.propagators.Propagator] - propagators to be stacked together sequentially - - Raises - ------ - ValueError - If propagators to stack are incompatible in terms of number of input and output individuals. - """ - if len(propagators) < 1: - raise ValueError( - f"Not enough Propagators given ({len(propagators)}). At least 1 is required." - ) - super(Compose, self).__init__(propagators[0].parents, propagators[-1].offspring) - for i in range(len(propagators) - 1): - # Check compatibility of consecutive propagators in terms of number of parents + offsprings. - if not _check_compatible( - propagators[i].offspring, propagators[i + 1].parents - ): - outp = propagators[i] - inp = propagators[i + 1] - outd = outp.offspring - ind = inp.parents - - raise ValueError( - f"Incompatible combination of {outd} output individuals " - f"of {outp} and {ind} input individuals of {inp}." - ) - self.propagators = propagators - - def __call__(self, inds: List[Individual]) -> List[Individual]: - """ - Apply composed propagator. - - Parameters - ---------- - inds: list[propulate.individual.Individual] - input individuals the propagator is applied to - - Returns - ------- - list[propulate.individual.Individual] - output individuals after application of propagator - """ - for p in self.propagators: - inds = p(inds) - return inds - - -class PointMutation(Stochastic): - """ - Point-mutate given number of traits with given probability. - """ - - def __init__( - self, - limits: Union[ - Dict[str, Tuple[float, float]], - Dict[str, Tuple[int, int]], - Dict[str, Tuple[str, ...]], - ], - points: int = 1, - probability: float = 1.0, - rng: random.Random = None, - ) -> None: - """ - Initialize point-mutation propagator. - - Parameters - ---------- - limits: dict[str, tuple[float, float]] | dict[str, tuple[int, int]] | dict[str, tuple[str, ...]] - limits of (hyper-)parameters to be optimized - points: int - number of points to mutate - probability: float - probability of application - rng: random.Random - random number generator - - Raises - ------ - ValueError - If the requested number of points to mutate is greater than the number of traits. - """ - super(PointMutation, self).__init__(1, 1, probability, rng) - self.points = points - self.limits = limits - if len(limits) < points: - raise ValueError( - f"Too many points to mutate for individual with {len(limits)} traits." - ) - - def __call__(self, ind: Individual) -> Individual: - """ - Apply point-mutation propagator. - - Parameters - ---------- - ind: propulate.individual.Individual - individual the propagator is applied to - - Returns - ------- - propulate.individual.Individual - possibly point-mutated individual after application of propagator - """ - if ( - self.rng.random() < self.probability - ): # Apply propagator only with specified probability - ind = copy.deepcopy(ind) - ind.loss = None # Initialize individual's loss attribute. - # Determine traits to mutate via random sampling. - # Return `self.points` length list of unique elements chosen from `ind.keys()`. - # Used for random sampling without replacement. - to_mutate = self.rng.sample(sorted(ind.keys()), self.points) - # Point-mutate `self.points` randomly chosen traits of individual `ind`. - for i in to_mutate: - if isinstance(ind[i], int): - # Return randomly selected element from int range(start, stop, step). - ind[i] = self.rng.randint(*self.limits[i]) - elif isinstance(ind[i], float): - # Return random floating point number within limits. - ind[i] = self.rng.uniform(*self.limits[i]) - elif isinstance(ind[i], str): - # Return random element from non-empty sequence. - ind[i] = self.rng.choice(self.limits[i]) - - return ind # Return point-mutated individual. - - -class RandomPointMutation(Stochastic): - """ - Point-mutate random number of traits with given probability. - """ - - def __init__( - self, - limits: Union[ - Dict[str, Tuple[float, float]], - Dict[str, Tuple[int, int]], - Dict[str, Tuple[str, ...]], - ], - min_points: int = 1, - max_points: int = 1, - probability: float = 1.0, - rng: random.Random = None, - ) -> None: - """ - Initialize random point-mutation propagator. - - Parameters - ---------- - limits: dict[str, tuple[float, float]] | dict[str, tuple[int, int]] | dict[str, tuple[str, ...]] - limits of parameters to optimize, i.e., search space - min_points: int - minimum number of points to mutate - max_points: int - maximum number of points to mutate - probability: float - probability of application - rng: random.Random - random number generator - - Raises - ------ - ValueError - If no or a negative number of points shall be mutated. - ValueError - If there are fewer traits than requested number of points to mutate. - ValueError - If the requested minimum number of points to mutate is greater than the requested maximum number. - """ - super(RandomPointMutation, self).__init__(1, 1, probability, rng) - if min_points <= 0: - raise ValueError( - f"Minimum number of points to mutate must be > 0 but was {min_points}." - ) - if len(limits) < max_points: - raise ValueError( - f"Too many points to mutate for individual with {len(limits)} traits." - ) - if min_points > max_points: - raise ValueError( - f"Minimum number of traits to mutate must be <= respective maximum number " - f"but min_points = {min_points} > {max_points} = max_points." - ) - self.min_points = min_points - self.max_points = max_points - self.limits = limits - - def __call__(self, ind: Individual) -> Individual: - """ - Apply random-point-mutation propagator. - - Parameters - ---------- - ind: propulate.population.Individual - individual the propagator is applied to - - Returns - ------- - propulate.population.Individual - possibly point-mutated individual after application of propagator - """ - if ( - self.rng.random() < self.probability - ): # Apply propagator only with specified probability. - ind = copy.deepcopy(ind) - ind.loss = None # Initialize individual's loss attribute. - # Determine traits to mutate via random sampling. - # Return `self.points` length list of unique elements chosen from `ind.keys()`. - # Used for random sampling without replacement. - points = self.rng.randint(self.min_points, self.max_points) - to_mutate = self.rng.sample(sorted(ind.keys()), points) - # Point-mutate `points` randomly chosen traits of individual `ind`. - for i in to_mutate: - if isinstance(ind[i], int): - # Return randomly selected element from int range(start, stop, step). - ind[i] = self.rng.randint(*self.limits[i]) - elif isinstance(ind[i], float): - # Return random floating point number N within limits. - ind[i] = self.rng.uniform(*self.limits[i]) - elif isinstance(ind[i], str): - # Return random element from non-empty sequence. - ind[i] = self.rng.choice(self.limits[i]) - - return ind # Return point-mutated individual. - - -class IntervalMutationNormal(Stochastic): - """ - Mutate given number of traits according to Gaussian distribution around current value with given probability. - """ - - def __init__( - self, - limits: Union[ - Dict[str, Tuple[float, float]], - Dict[str, Tuple[int, int]], - Dict[str, Tuple[str, ...]], - ], - sigma_factor: float = 0.1, - points: int = 1, - probability: float = 1.0, - rng: random.Random = None, - ) -> None: - """ - Initialize interval-mutation propagator. - - Parameters - ---------- - limits: dict[str, tuple[float, float]] | dict[str, tuple[int, int]] | dict[str, tuple[str, ...]] - limits of (hyper-)parameters to be optimized, i.e., search space - sigma_factor: float - scaling factor for interval width to obtain standard deviation - points: int - number of points to mutate - probability: float - probability of application - rng: random.Random - random number generator - - Raises - ------ - ValueError - If the individuals has fewer continuous traits than the requested number of points to mutate. - """ - super(IntervalMutationNormal, self).__init__(1, 1, probability, rng) - self.points = points # number of traits to point-mutate - self.limits = limits - self.sigma_factor = sigma_factor - n_interval_traits = len([x for x in limits if isinstance(limits[x][0], float)]) - if n_interval_traits < points: - raise ValueError( - f"Too many points to mutate ({points}) for individual with {n_interval_traits} continuous traits." - ) - - def __call__(self, ind: Individual) -> Individual: - """ - Apply interval-mutation propagator. - - Parameters - ---------- - ind: propulate.individual.Individual - input individual the propagator is applied to - - Returns - ------- - propulate.individual.Individual - possibly interval-mutated output individual after application of propagator - """ - if ( - self.rng.random() < self.probability - ): # Apply propagator only with specified probability. - ind = copy.deepcopy(ind) - ind.loss = None # Initialize individual's loss attribute. - # Determine traits of type float. - interval_keys = [x for x in ind.keys() if isinstance(ind[x], float)] - # Determine ´self.points` traits to mutate. - to_mutate = self.rng.sample(interval_keys, self.points) - # Mutate traits by sampling from Gaussian distribution centered around current value - # with `sigma_factor` scaled interval width as standard distribution. - for i in to_mutate: - min_val, max_val = self.limits[i] # Determine interval boundaries. - sigma = ( - max_val - min_val - ) * self.sigma_factor # Determine std from interval boundaries and sigma factor. - ind[i] = self.rng.gauss( - ind[i], sigma - ) # Sample new value from Gaussian centered around current value. - ind[i] = min( - max_val, ind[i] - ) # Make sure new value is within specified limits. - ind[i] = max(min_val, ind[i]) - - return ind # Return point-mutated individual. - - -class MateUniform(Stochastic): # uniform crossover - """ - Generate new individual by uniform crossover of two parents with specified relative parent contribution. - """ - - def __init__( - self, - rel_parent_contrib: float = 0.5, - probability: float = 1.0, - rng: random.Random = None, - ) -> None: - """ - Initialize uniform crossover propagator. - - Parameters - ---------- - rel_parent_contrib: float - relative parent contribution w.r.t. first parent - probability: float - probability of application - rng: random.Random - random number generator - - Raises - ------ - ValueError - If the relative parent contribution is not within [0, 1]. - """ - super(MateUniform, self).__init__( - 2, 1, probability, rng - ) # Breed 1 offspring from 2 parents. - if rel_parent_contrib <= 0 or rel_parent_contrib >= 1: - raise ValueError( - f"Relative parent contribution must be within (0, 1) but was {rel_parent_contrib}." - ) - self.rel_parent_contrib = rel_parent_contrib - - def __call__(self, inds: List[Individual]) -> Individual: - """ - Apply uniform-crossover propagator. - - Parameters - ---------- - inds: List[propulate.individual.Individual] - individuals the propagator is applied to - - Returns - ------- - propulate.individual.Individual - possibly cross-bred individual after application of propagator - """ - ind = copy.deepcopy(inds[0]) # Consider 1st parent. - ind.loss = None # Initialize individual's loss attribute. - if ( - self.rng.random() < self.probability - ): # Apply propagator only with specified `probability`. - # Replace traits in first parent with values of second parent with specified relative parent contribution. - for k in ind.keys(): - if self.rng.random() > self.rel_parent_contrib: - ind[k] = inds[1][k] - return ind # Return offspring. - - -class MateMultiple(Stochastic): # uniform crossover - """ - Breed new individual by uniform crossover of multiple parents. - """ - - def __init__( - self, parents: int = -1, probability: float = 1.0, rng: random.Random = None - ) -> None: - """ - Initialize multiple-crossover propagator. - - Parameters - ---------- - probability: float - probability of application - parents: int - number of parents - rng: random.Random - random number generator - """ - super(MateMultiple, self).__init__( - parents, 1, probability, rng - ) # Breed 1 offspring from specified number of parents. - - def __call__(self, inds: List[Individual]) -> Individual: - """ - Apply multiple-crossover propagator. - - Parameters - ---------- - inds: list[propulate.individual.Individual] - individuals the propagator is applied to - - Returns - ------- - propulate.individual.Individual - possibly cross-bred individual after application of propagator - """ - ind = copy.deepcopy(inds[0]) # Consider 1st parent. - ind.loss = None # Initialize individual's loss attribute. - if ( - self.rng.random() < self.probability - ): # Apply propagator only with specified `probability`. - # Choose traits from all parents with uniform probability. - for k in ind.keys(): - ind[k] = self.rng.choice([parent[k] for parent in inds]) - return ind # Return offspring. - - -class MateSigmoid(Stochastic): - """ - Generate new individual by crossover of two parents according to Boltzmann sigmoid probability. - - Consider two parent individuals with fitness values f1 and f2. Let f1 <= f2. For each trait, - the better parent's value is accepted with the probability sigmoid(- (f1-f2) / temperature). - """ - - def __init__( - self, - temperature: float = 1.0, - probability: float = 1.0, - rng: random.Random = None, - ) -> None: - """ - Initialize sigmoid-crossover propagator. - - Parameters - ---------- - temperature: float - temperature for Boltzmann factor in sigmoid probability - probability: float - probability of application - rng: random.Random - random number generator - """ - super(MateSigmoid, self).__init__( - 2, 1, probability, rng - ) # Breed 1 offspring from 2 parents. - self.temperature = temperature - - def __call__(self, inds: List[Individual]) -> Individual: - """ - Apply sigmoid-crossover propagator. - - Parameters - ---------- - inds: list[propulate.individual.Individual] - individuals the propagator is applied to - - Returns - ------- - propulate.individual.Individual - possibly cross-bred individual after application of propagator - """ - ind = copy.deepcopy(inds[0]) # Consider 1st parent. - ind.loss = None # Initialize individual's loss attribute. - if inds[0].loss <= inds[1].loss: - delta = inds[0].loss - inds[1].loss - fraction = 1 / (1 + np.exp(-delta / self.temperature)) - else: - delta = inds[1].loss - inds[0].loss - fraction = 1 - 1 / (1 + np.exp(-delta / self.temperature)) - - if ( - self.rng.random() < self.probability - ): # Apply propagator only with specified `probability`. - # Replace traits in 1st parent with values of 2nd parent with Boltzmann probability. - for k in inds[1].keys(): - if self.rng.random() > fraction: - ind[k] = inds[1][k] - return ind # Return offspring. - - -class SelectMin(Propagator): - """ - Select specified number of best performing individuals as evaluated by their losses. - i.e., those individuals with minimum losses. - """ - - def __init__( - self, - offspring: int, - ) -> None: - """ - Initialize elitist selection propagator. - - Parameters - ---------- - offspring: int - number of offsprings (individuals to be selected) - """ - super(SelectMin, self).__init__(-1, offspring) - - def __call__(self, inds: List[Individual]) -> List[Individual]: - """ - Apply elitist-selection propagator. - - Parameters - ---------- - inds: list[propulate.individual.Individual] - input individuals the propagator is applied to - - Returns - ------- - list[propulate.individual.Individual] - selected output individuals after application of the propagator - - Raises - ------ - ValueError - If more individuals than put in shall be selected. - """ - if len(inds) < self.offspring: - raise ValueError( - f"Has to have at least {self.offspring} individuals to select the {self.offspring} best ones." - ) - # Sort elements of given iterable in specific order + return as list. - return sorted(inds, key=lambda ind: ind.loss)[ - : self.offspring - ] # Return `self.offspring` best individuals in terms of loss. - - -class SelectMax(Propagator): - """ - Select specified number of worst performing individuals as evaluated by their losses, - i.e., those individuals with maximum losses. - """ - - def __init__( - self, - offspring: int, - ) -> None: - """ - Initialize anti-elitist propagator. - - Parameters - ---------- - offspring: int - number of offspring (individuals to be selected) - """ - super(SelectMax, self).__init__(-1, offspring) - - def __call__(self, inds: List[Individual]) -> List[Individual]: - """ - Apply anti-elitist-selection propagator. - - Parameters - ---------- - inds: list[propulate.individual.Individual] - individuals the propagator is applied to - - Returns - ------- - list[propulate.individual.Individual] - selected individuals after application of the propagator - - Raises - ------ - ValueError: If more individuals than put in shall be selected. - """ - if len(inds) < self.offspring: - raise ValueError( - f"Has to have at least {self.offspring} individuals to select the {self.offspring} worst ones." - ) - # Sort elements of given iterable in specific order + return as list. - return sorted(inds, key=lambda ind: -ind.loss)[ - : self.offspring - ] # Return the `self.offspring` worst individuals in terms of loss. - - -class SelectUniform(Propagator): - """ - Select specified number of individuals randomly. - """ - - def __init__(self, offspring: int, rng: random.Random = None) -> None: - """ - Initialize random-selection propagator. - - Parameters - ---------- - offspring: int - number of offspring (individuals to be selected) - rng: random.Random - random number generator - """ - super(SelectUniform, self).__init__(-1, offspring, rng) - - def __call__(self, inds: List[Individual]) -> List[Individual]: - """ - Apply uniform-selection propagator. - - Parameters - ---------- - inds: list[propulate.individual.Individual] - individuals the propagator is applied to - - Returns - ------- - list[propulate.individual.Individual] - selected individuals after application of propagator - - Raises - ------ - ValueError: If more individuals than put in shall be selected. - """ - if len(inds) < self.offspring: - raise ValueError( - f"Has to have at least {self.offspring} individuals to select {self.offspring} from them." - ) - # Return a `self.offspring` length list of unique elements chosen from `particles`. - # Used for random sampling without replacement. - return self.rng.sample(inds, self.offspring) - - -# TODO parents should be fixed to one NOTE see utils reason why it is not right now -class InitUniform(Stochastic): - """ - Initialize individual by uniformly sampling specified limits for each trait. - """ - - def __init__( - self, - limits: Union[ - Dict[str, Tuple[float, float]], - Dict[str, Tuple[int, int]], - Dict[str, Tuple[str, ...]], - ], - parents: int = 0, - probability: float = 1.0, - rng: random.Random = None, - ) -> None: - """ - Initialize random-initialization propagator. - - Parameters - ---------- - limits: dict[str, tuple[float, float]] | dict[str, tuple[int, int]] | dict[str, tuple[str, ...]] - search space, i.e., limits of (hyper-)parameters to be optimized - parents: int - number of parents - probability: float - probability of application - rng: random.Random - random number generator - """ - super(InitUniform, self).__init__(parents, 1, probability, rng) - self.limits = limits - - def __call__(self, *inds: Individual) -> Individual: - """ - Apply uniform-initialization propagator. - - Parameters - ---------- - inds: propulate.individual.Individual - individuals the propagator is applied to - - Returns - ------- - propulate.individual.Individual - output individual after application of propagator - - Raises - ------ - ValueError - If a parameter's type is invalid, i.e., not float (continuous), int (ordinal), or str (categorical). - """ - if ( - self.rng.random() < self.probability - ): # Apply only with specified probability. - ind = Individual() # Instantiate new individual. - for ( - limit - ) in self.limits: # Randomly sample from specified limits for each trait. - if isinstance( - self.limits[limit][0], int - ): # If ordinal trait of type integer. - ind[limit] = self.rng.randint(*self.limits[limit]) - elif isinstance( - self.limits[limit][0], float - ): # If interval trait of type float. - ind[limit] = self.rng.uniform(*self.limits[limit]) - elif isinstance( - self.limits[limit][0], str - ): # If categorical trait of type string. - ind[limit] = self.rng.choice(self.limits[limit]) - else: - raise ValueError( - "Unknown type of limits. Has to be float for interval, " - "int for ordinal, or string for categorical." - ) - else: # Return first input individual w/o changes otherwise. - ind = inds[0] - return ind - - -class CMAParameter: - """ - Handles and stores all Basic/Active CMA related constants/variables and strategy parameters. - """ - - def __init__( - self, - lamb: int, - mu: int, - problem_dimension: int, - weights: np.ndarray, - mu_eff: float, - c_c: float, - c_1: float, - c_mu: float, - limits: Dict, - exploration: bool, - ) -> None: - """ - Initializes a CMAParameter object. - Parameters - ---------- - lamb : the number of individuals considered for each generation - mu : number of positive recombination weights - problem_dimension: the number of dimensions in the search space - weights : recombination weights - mu_eff : variance effective selection mass - c_c : decay rate for evolution path for the rank-one update of the covariance matrix - c_1 : learning rate for the rank-one update of the covariance matrix update - c_mu : learning rate for the rank-mu update of the covariance matrix update - limits : limits of search space - exploration : if true decompose covariance matrix for each generation (worse runtime, less exploitation, more decompose_in_each_generation)), else decompose covariance matrix only after a certain number of individuals evaluated (better runtime, more exploitation, less decompose_in_each_generation) - """ - self.problem_dimension = problem_dimension - self.limits = limits - self.lamb = lamb - self.mu = mu - self.weights = weights - # self.c_m = c_m - self.mu_eff = mu_eff - self.c_c = c_c - self.c_1 = c_1 - self.c_mu = c_mu - - # Step-size control params - self.c_sigma = (mu_eff + 2) / (problem_dimension + mu_eff + 5) - self.d_sigma = ( - 1 - + 2 * max(0, np.sqrt((mu_eff - 1) / (problem_dimension + 1)) - 1) - + self.c_sigma - ) - - # Initialize dynamic strategy variables - self.p_sigma = np.zeros((problem_dimension, 1)) - self.p_c = np.zeros((problem_dimension, 1)) - - # prevent equal eigenvals, hack from https://github.com/CMA-ES/pycma/blob/development/cma/sampler.py - self.co_matrix = np.diag( - np.ones(problem_dimension) - * np.exp( - (1e-4 / self.problem_dimension) * np.arange(self.problem_dimension) - ) - ) - self.b_matrix = np.eye(self.problem_dimension) - # assuming here self.co_matrix is initialized to be diagonal - self.d_matrix = np.diag(self.co_matrix) ** 0.5 - # sort eigenvalues in ascending order - indices_eig = self.d_matrix.argsort() - self.d_matrix = self.d_matrix[indices_eig] - self.b_matrix = self.b_matrix[:, indices_eig] - # the square root of the inverse of the covariance matrix: C^-1/2 = B*D^(-1)*B^T - self.co_inv_sqrt = ( - self.b_matrix @ np.diag(self.d_matrix ** (-1)) @ self.b_matrix.T - ) - # the maximum allowed condition of the covariance matrix to ensure numerical stability - self.condition_limit = 1e5 - 1 - # whether to keep the trace (sum of diagonal elements) of self.co_matrix constant - self.constant_trace = False - - # use this initial mean when using multiple islands? - self.mean = np.array( - [[np.random.uniform(*limits[limit]) for limit in limits]] - ).reshape((problem_dimension, 1)) - # 0.3 instead of 0.2 is also often used for greater initial step size - self.sigma = 0.2 * ( - (max(max(limits[i]) for i in limits)) - min(min(limits[i]) for i in limits) - ) - - # the mean of the last generation - self.old_mean = None - self.exploration = exploration - - # the number of individuals evaluated when the covariance matrix was last decomposed into B and D - self.eigen_eval = 0 - # the number of individuals evaluated - self.count_eval = 0 - - # expectation value of ||N(0,I)|| - self.chiN = problem_dimension**0.5 * ( - 1 - 1.0 / (4 * problem_dimension) + 1.0 / (21 * problem_dimension**2) - ) - - def set_mean(self, new_mean: np.ndarray) -> None: - """ - Setter for mean property. Updates the old mean as well. - Parameters - ---------- - new_mean : the new mean - """ - self.old_mean = self.mean - self.mean = new_mean - - def set_p_sigma(self, new_p_sigma: np.ndarray) -> None: - """ - Setter for evolution path of step-size adatpiton - Parameters - ---------- - new_p_sigma : the new evolution path - """ - self.p_sigma = new_p_sigma - - def set_p_c(self, new_p_c: np.ndarray) -> None: - """ - Setter for evolution path of covariance matrix adaption - Parameters - ---------- - new_p_c : the new evolution path - """ - self.p_c = new_p_c - - def set_sigma(self, new_sigma: float) -> None: - """ - Setter for step-size - Parameters - ---------- - new_sigma : the new step-size - """ - self.sigma = new_sigma - - # version without condition handling - """def set_co_matrix_depr(self, new_co_matrix: np.ndarray) -> None: - Setter for the covariance matrix. Computes new values for b_matrix, d_matrix and co_inv_sqrt as well - Parameters - ---------- - new_co_matrix : the new covariance matrix - Update b and d matrix and co_inv_sqrt only after certain number of evaluations to ensure 0(n^2) - Also trade-Off decompose_in_each_generation or not - if self.decompose_in_each_generation or ( - self.count_eval - self.eigen_eval - > self.lamb / (self.c_1 + self.c_mu) / self.problem_dimension / 10 - ): - self.eigen_eval = self.count_eval - c = np.triu(new_co_matrix) + np.triu(new_co_matrix, 1).T # Enforce symmetry - d, self.b_matrix = np.linalg.eigh(c) # Eigen decomposition - self.d_matrix = np.sqrt(d) # Replace eigenvalues with standard deviations - self.co_matrix = c - self.co_inv_sqrt = ( - self.b_matrix @ np.diag(self.d_matrix ** (-1)) @ self.b_matrix.T - ) - self.co_inv_sqrt = (self.co_inv_sqrt + self.co_inv_sqrt.T) / 2 # ensure symmetry - self._sort_b_d_matrix()""" - - def set_co_matrix(self, new_co_matrix: np.ndarray) -> None: - """ - Setter for the covariance matrix. Computes new values for b_matrix, d_matrix and co_inv_sqrt as well - Decomposition of co_matrix in O(n^3), hence why the possibility of lazy updating b_matrix and d_matrix. - Parameters - ---------- - new_co_matrix : the new covariance matrix - """ - # Update b and d matrix and co_inv_sqrt only after certain number of evaluations to ensure 0(n^2) - # Also trade-Off decompose_in_each_generation or not - if self.exploration or ( - self.count_eval - self.eigen_eval - > self.lamb / (self.c_1 + self.c_mu) / self.problem_dimension / 10 - ): - self.eigen_eval = self.count_eval - self._decompose_co_matrix(new_co_matrix) - self.co_inv_sqrt = ( - self.b_matrix @ np.diag(self.d_matrix ** (-1)) @ self.b_matrix.T - ) - # ensure symmetry - self.co_inv_sqrt = (self.co_inv_sqrt + self.co_inv_sqrt.T) / 2 - - def _decompose_co_matrix(self, new_co_matrix: np.ndarray) -> None: - """ - Eigendecomposition of the covariance matrix into eigenvalues (d_matrix) and eigenvectors (columns of b_matrix) - Parameters - ---------- - new_co_matrix: the new covariance matrix that should be decomposed - """ - # Enforce symmetry - self.co_matrix = np.triu(new_co_matrix) + np.triu(new_co_matrix, 1).T - d_matrix_old = self.d_matrix - try: - self.d_matrix, self.b_matrix = np.linalg.eigh(self.co_matrix) - if any(self.d_matrix <= 0): - # covariance matrix eigen decomposition failed, consider reformulating objective function - raise ValueError("covariance matrix was not positive definite") - except Exception as _: - # add min(eigenvalues(self.co_matrix_old)) to diag(self.co_matrix) and try again - min_eig_old = min(d_matrix_old) ** 2 - for i in range(self.problem_dimension): - self.co_matrix[i, i] += min_eig_old - # Replace eigenvalues with standard deviations - self.d_matrix = (d_matrix_old**2 + min_eig_old) ** 0.5 - self._decompose_co_matrix(self.co_matrix) - else: - assert all(np.isfinite(self.d_matrix)) - self._sort_b_d_matrix() - if self.condition_limit is not None: - self._limit_condition(self.condition_limit) - if self.constant_trace: - s = 1 / np.mean( - self.d_matrix - ) # normalize co_matrix to control overall magnitude - self.co_matrix *= s - self.d_matrix *= s - self.d_matrix **= 0.5 - - def _limit_condition(self, limit) -> None: - """ - Limit the condition (square of ratio largest to smallest eigenvalue) of the covariance matrix if it exceeds a limit. - Credits on how to limit the condition: https://github.com/CMA-ES/pycma/blob/development/cma/sampler.py - Parameters - ---------- - limit: the treshold for the condition of the matrix - """ - # check if condition number of matrix is to big - if (self.d_matrix[-1] / self.d_matrix[0]) ** 2 > limit: - eps = (self.d_matrix[-1] ** 2 - limit * self.d_matrix[0] ** 2) / (limit - 1) - for i in range(self.problem_dimension): - # decrease ratio of largest to smallest eigenvalue, absolute difference remains - self.co_matrix[i, i] += eps - # eigenvalues are definitely positive now - self.d_matrix **= 2 - self.d_matrix += eps - self.d_matrix **= 0.5 - - def _sort_b_d_matrix(self) -> None: - """ - Sort columns of b_matrix and d_matrix according to the eigenvalues in d_matrix - """ - indices_eig = np.argsort(self.d_matrix) - self.d_matrix = self.d_matrix[indices_eig] - self.b_matrix = self.b_matrix[:, indices_eig] - assert (min(self.d_matrix), max(self.d_matrix)) == ( - self.d_matrix[0], - self.d_matrix[-1], - ) - - def mahalanobis_norm(self, dx: np.ndarray) -> np.ndarray: - """ - Computes the mahalanobis distance by using C^(-1/2) and the difference vector of a point to the mean of a distribution. - Parameters - ---------- - dx : the difference vector - - Returns - ------- - the resulting mahalanobis distance - """ - return np.linalg.norm(np.dot(self.co_inv_sqrt, dx)) - - -class CMAAdapter(ABC): - """ - Abstract base class for the adaption of strategy parameters of CMA-ES. Strategy class from the viewpoint of the strategy desing pattern. - """ - - @abstractmethod - def update_mean(self, par: CMAParameter, arx: np.ndarray) -> None: - """ - Abstract method for updating of mean in CMA-ES variants. - Parameters - ---------- - par : the parameter object of the CMA-ES propagation - arx : the individuals of the distribution - """ - pass - - def update_step_size(self, par: CMAParameter) -> None: - """ - Method for updating step-size in CMA-ES variants. Calculates the current evolution path for the step-size adaption. - Parameters - ---------- - par : the parameter object of the CMA-ES propagation - """ - par.set_p_sigma( - (1 - par.c_sigma) * par.p_sigma - + np.sqrt(par.c_sigma * (2 - par.c_sigma) * par.mu_eff) - * par.co_inv_sqrt - @ (par.mean - par.old_mean) - / par.sigma - ) - par.set_sigma( - par.sigma - * np.exp( - (par.c_sigma / par.d_sigma) - * (np.linalg.norm(par.p_sigma, ord=2) / par.chiN - 1) - ) - ) - - @abstractmethod - def update_covariance_matrix(self, par: CMAParameter, arx: np.ndarray) -> None: - """ - Abstract method for the adaption of the covariance matrix of CMA-ES variants. - Parameters - ---------- - par : the parameter object of the CMA-ES propagation - arx : the individuals of the distribution - """ - pass - - @abstractmethod - def compute_weights( - self, mu: int, lamb: int, problem_dimension: int - ) -> Tuple[np.ndarray, float, float, float, float]: - """ - Abstract method for computing the recombination weights of a CMA-ES variant. - Parameters - ---------- - mu : the number of positive recombination weights - lamb : the number of individuals considered for each generation - problem_dimension : the number of dimensions in the search space - - Returns - ------- - A Tuple of the weights, mu_eff, c_1, c_c and c_mu - """ - pass - - @staticmethod - def compute_learning_rates( - mu_eff: float, problem_dimension: int - ) -> Tuple[float, float, float]: - """ - Computes the learning rates for the CMA-variants. - Parameters - ---------- - mu_eff : the variance effective selection mass - problem_dimension : the number of dimensions in the search space - - Returns - ------- - A Tuple of c_c, c_1, c_mu - """ - c_c = (4 + mu_eff / problem_dimension) / ( - problem_dimension + 4 + 2 * mu_eff / problem_dimension - ) - c_1 = 2 / ((problem_dimension + 1.3) ** 2 + mu_eff) - c_mu = min( - 1 - c_1, - 2 * (mu_eff - 2 + (1 / mu_eff)) / ((problem_dimension + 2) ** 2 + mu_eff), - ) - return c_c, c_1, c_mu - - -class BasicCMA(CMAAdapter): - """ - Adaption of strategy parameters of CMA-ES according to the original CMA-ES algorithm. Concrete strategy class from the viewpoint of the strategy design pattern. - """ - - def compute_weights( - self, mu: int, lamb: int, problem_dimension: int - ) -> Tuple[np.ndarray, float, float, float, float]: - """ - Computes the recombination weights for Basic CMA-ES - Parameters - ---------- - mu : the number of positive recombination weights - lamb : the number of individuals considered for each generation - problem_dimension : the number of dimensions in the search space - - Returns - ------- - A Tuple of the weights, mu_eff, c_1, c_c and c_mu. - """ - weights = np.log(mu + 0.5) - np.log(np.arange(1, mu + 1)) - weights /= np.sum(weights) - mu_eff = np.sum(weights) ** 2 / np.sum(weights**2) - c_c, c_1, c_mu = BasicCMA.compute_learning_rates(mu_eff, problem_dimension) - return weights, mu_eff, c_c, c_1, c_mu - - def update_mean(self, par: CMAParameter, arx: np.ndarray) -> None: - """ - Updates the mean in Basic CMA-ES. - Parameters - ---------- - par : the parameter object of the CMA-ES propagation - arx : the individuals of the distribution - - """ - # matrix vector multiplication (reshape weights to column vector) - par.set_mean(arx @ par.weights.reshape(-1, 1)) - - def update_covariance_matrix(self, par: CMAParameter, arx: np.ndarray) -> None: - """ - Adapts the covariance matrix of Basic CMA-ES. - Parameters - ---------- - par : the parameter object of the CMA-ES propagation - arx : the individuals of the distribution - """ - # turn off rank-one accumulation when sigma increases quickly - h_sig = np.sum(par.p_sigma**2) / ( - 1 - (1 - par.c_sigma) ** (2 * (par.count_eval / par.lamb)) - ) / par.problem_dimension < 2 + 4.0 / (par.problem_dimension + 1) - # update evolution path - par.set_p_c( - (1 - par.c_c) * par.p_c - + h_sig - * np.sqrt(par.c_c * (2 - par.c_c) * par.mu_eff) - * (par.mean - par.old_mean) - / par.sigma - ) - # use h_sig to the power of two (unlike in paper) for the variance loss from h_sig - ar_tmp = (1 / par.sigma) * ( - arx[:, : par.mu] - np.tile(par.old_mean, (1, par.mu)) - ) - new_co_matrix = ( - (1 - par.c_1 - par.c_mu) * par.co_matrix - + par.c_1 - * ( - par.p_c @ par.p_c.T - + (1 - h_sig) * par.c_c * (2 - par.c_c) * par.co_matrix - ) - + par.c_mu * ar_tmp @ (par.weights * ar_tmp).T - ) - par.set_co_matrix(new_co_matrix) - - -class ActiveCMA(CMAAdapter): - """ - Adaption of strategy parameters of CMA-ES according to the Active CMA-ES algorithm. Different to the original CMA-ES algorithm Active CMA-ES uses negative recombination weights (only for the covariance matrix adaption) for individuals with relatively low fitness. Concrete strategy class from the viewpoint of the strategy design pattern. - """ - - def compute_weights( - self, mu: int, lamb: int, problem_dimension: int - ) -> Tuple[np.ndarray, float, float, float, float]: - """ - Computes the recombination weights for Active CMA-ES - Parameters - ---------- - mu : the number of positive recombination weights - lamb : the number of individuals considered for each generation - problem_dimension : the number of dimensions in the search space - - Returns - ------- - A Tuple of the weights, mu_eff, c_1, c_c and c_mu. - """ - weights_preliminary = np.log(lamb / 2 + 0.5) - np.log(np.arange(1, lamb + 1)) - mu_eff = np.sum(weights_preliminary[:mu]) ** 2 / np.sum( - weights_preliminary[:mu] ** 2 - ) - c_c, c_1, c_mu = ActiveCMA.compute_learning_rates(mu_eff, problem_dimension) - # now compute final weights - mu_eff_minus = np.sum(weights_preliminary[mu:]) ** 2 / np.sum( - weights_preliminary[mu:] ** 2 - ) - alpha_mu_minus = 1 + c_1 / c_mu - alpha_mu_eff_minus = 1 + 2 * mu_eff_minus / (mu_eff + 2) - alpha_pos_def_minus = (1 - c_1 - c_mu) / problem_dimension * c_mu - weights = weights_preliminary - weights[:mu] /= np.sum(weights_preliminary[:mu]) - weights[mu:] *= ( - min(alpha_mu_minus, alpha_mu_eff_minus, alpha_pos_def_minus) - / np.sum(weights_preliminary[mu:]) - * -1 - ) - return weights, mu_eff, c_c, c_1, c_mu - - def update_mean(self, par: CMAParameter, arx: np.ndarray) -> None: - """ - Updates the mean in Active CMA-ES. - Parameters - ---------- - par : the parameter object of the CMA-ES propagation - arx : the individuals of the distribution - """ - # matrix vector multiplication (reshape weights to column vector) - # Only consider positive weights - par.set_mean(arx @ par.weights[: par.mu].reshape(-1, 1)) - - def update_covariance_matrix(self, par: CMAParameter, arx: np.ndarray) -> None: - """ - Adapts the covariance matrix of Basic CMA-ES. - Parameters - ---------- - par : the parameter object of the CMA-ES propagation - arx : the individuals of the distribution - """ - # turn off rank-one accumulation when sigma increases quickly - h_sig = np.sum(par.p_sigma**2) / ( - 1 - (1 - par.c_sigma) ** (2 * (par.count_eval / par.lamb)) - ) / par.problem_dimension < 2 + 4.0 / (par.problem_dimension + 1) - # update evolution path - par.set_p_c( - (1 - par.c_c) * par.p_c - + h_sig - * np.sqrt(par.c_c * (2 - par.c_c) * par.mu_eff) - * (par.mean - par.old_mean) - / par.sigma - ) - weights_circle = np.zeros((par.lamb,)) - for i, w_i in enumerate(par.weights): - # guaranty positive definiteness - weights_circle[i] = w_i - if w_i < 0: - weights_circle[i] *= ( - par.problem_dimension - * ( - par.sigma - / par.mahalanobis_norm(arx[:, i] - par.old_mean.ravel()) - ) - ** 2 - ) - # use h_sig to the power of two (unlike in paper) for the variance loss from h_sig? - ar_tmp = (1 / par.sigma) * (arx - np.tile(par.old_mean, (1, par.lamb))) - new_co_matrix = ( - (1 - par.c_1 - par.c_mu) * par.co_matrix - + par.c_1 - * ( - par.p_c @ par.p_c.T - + (1 - h_sig) * par.c_c * (2 - par.c_c) * par.co_matrix - ) - + par.c_mu * ar_tmp @ (weights_circle * ar_tmp).T - ) - par.set_co_matrix(new_co_matrix) - - -class CMAPropagator(Propagator): - """ - Propagator of CMA-ES. Uses CMAAdapter to adapt strategy parameters like mean, step-size and covariance matrix and stores them in a CMAParameter object. - The context class from the viewpoint of the strategy design pattern. - """ - - def __init__( - self, - adapter: CMAAdapter, - limits: Dict, - rng, - decompose_in_each_generation=False, - select_worst_all_time=False, - pop_size=None, - pool_size=3, - ) -> None: - """ - Constructor of CMAPropagator. - Parameters - ---------- - adapter : the adaption strategy of CMA-ES - limits : the limits of the search space - decompose_in_each_generation : if true decompose covariance matrix for each generation (worse runtime, less exploitation, more exploration)), else decompose covariance matrix only after a certain number of individuals evaluated (better runtime, more exploitation, less exploration) - select_worst_all_time : if true use the worst individuals for negative recombination weights in active CMA-ES, else use the worst (lambda - mu) individuals of the best lambda individuals. If BasicCMA is used the given value is irrelevant with regards to functionality. - pop_size: the number of individuals to be considered in each generation - pool_size: the size of the pool of individuals preselected before selecting the best of this pool - """ - self.adapter = adapter - problem_dimension = len(limits) - # The number of individuals considered for each generation - lamb = ( - pop_size if pop_size else 4 + int(np.floor(3 * np.log(problem_dimension))) - ) - super(CMAPropagator, self).__init__(lamb, 1) - - # Number of positive recombination weights - mu = lamb // 2 - self.select_worst = SelectMax(lamb - mu) - self.select_worst_all_time = select_worst_all_time - - # CMA-ES variant specific weights and learning rates - weights, mu_eff, c_c, c_1, c_mu = adapter.compute_weights( - mu, lamb, problem_dimension - ) - - self.par = CMAParameter( - lamb, - mu, - problem_dimension, - weights, - mu_eff, - c_c, - c_1, - c_mu, - limits, - decompose_in_each_generation, - ) - self.pool_size = int(pool_size) if int(pool_size) >= 1 else 3 - self.select_pool = SelectMin(self.pool_size * lamb) - self.select_from_pool = SelectUniform(mu - 1, rng=rng) - self.select_best_1 = SelectMin(1) - - def __call__(self, inds: List[Individual]) -> Individual: - """ - The skeleton of the CMA-ES algorithm using the template method design pattern. Sampling individuals and adapting the strategy parameters. - Template methods are "update_mean()", "update_covariance_matrix()" and "update_step_size()". - Parameters - ---------- - inds: list of individuals available - - Returns - ------- - new_ind : the new sampled individual - """ - num_inds = len(inds) - # add individuals from different workers to eval_count - self.par.count_eval += num_inds - self.par.count_eval - # sample new individual - new_ind = self._sample_cma() - # check if len(inds) >= or < pool_size * lambda and make sample or sample + update - if num_inds >= self.pool_size * self.par.lamb: - inds_pooled = self.select_pool(inds) - best = self.select_best_1(inds_pooled) - if not self.select_worst_all_time: - worst = self.select_worst(inds_pooled) - else: - worst = self.select_worst(inds) - - inds_filtered = [ - ind for ind in inds_pooled if ind not in best and ind not in worst - ] - arx = self._transform_individuals_to_matrix( - best + self.select_from_pool(inds_filtered) + worst - ) - - # Update mean - self.adapter.update_mean(self.par, arx[:, : self.par.mu]) - # Update Covariance Matrix - self.adapter.update_covariance_matrix(self.par, arx) - # Update step_size - self.adapter.update_step_size(self.par) - return new_ind - - def _transform_individuals_to_matrix(self, inds: List[Individual]) -> np.ndarray: - """ - Takes a list of individuals and transform it to numpy matrix for easier subsequent computation - Parameters - ---------- - inds : list of individuals - - Returns - ------- - arx : a numpy array of shape (problem_dimension, len(inds)) - """ - arx = np.zeros((self.par.problem_dimension, len(inds))) - for k, ind in enumerate(inds): - for i, (dim, _) in enumerate(self.par.limits.items()): - arx[i, k] = ind[dim] - return arx - - def _sample_cma(self) -> Individual: - """ - Samples new individuals according to CMA-ES. - Returns - ------- - new_ind : the new sampled individual - """ - new_x = None - # Generate new offspring - random_vector = np.random.randn(self.par.problem_dimension, 1) - try: - new_x = self.par.mean + self.par.sigma * self.par.b_matrix @ ( - self.par.d_matrix * random_vector - ) - except (RuntimeWarning, Exception) as _: - raise ValueError( - "Failed to generate new offsprings, probably due to not well defined target function." - ) - self.par.count_eval += 1 - # Remove problem_dim - new_ind = Individual() - - for i, (dim, _) in enumerate(self.par.limits.items()): - new_ind[dim] = new_x[i, 0] - return new_ind - - def get_mean(self) -> np.ndarray: - """ - Getter for mean attribute. - Returns - ------- - mean : the current cma-es mean of the best mu individuals - """ - return self.par.mean - - def get_sigma(self) -> float: - """ - Getter for step size. - Returns - ------- - sigma : the current step-size - """ - return self.par.sigma - - def get_co_matrix(self) -> np.ndarray: - """ - Getter for covariance matrix. - Returns - ------- - co_matrix : current covariance matrix - """ - return self.par.co_matrix - - def get_evolution_path_sigma(self) -> np.ndarray: - """ - Getter for evolution path of step-size adaption. - Returns - ------- - p_sigma : evolution path for step-size adaption - """ - return self.par.p_sigma - - def get_evolution_path_co_matrix(self) -> np.ndarray: - """ - Getter for evolution path of covariance matrix adpation. - Returns - ------- - p_c : evolution path for covariance matrix adaption - """ - return self.par.p_c - From 219fce5bfb57983602e02b81a3e32d864ed8b0a7 Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 11:52:21 +0200 Subject: [PATCH 129/139] refactor GA propagator classes into separate file --- propulate/propagators/ga.py | 454 ++++++++++++++++++++++++++++++++++++ 1 file changed, 454 insertions(+) create mode 100644 propulate/propagators/ga.py diff --git a/propulate/propagators/ga.py b/propulate/propagators/ga.py new file mode 100644 index 00000000..20d46086 --- /dev/null +++ b/propulate/propagators/ga.py @@ -0,0 +1,454 @@ +import copy +import random +from typing import List, Dict, Union, Tuple + +import numpy as np +from abc import ABC, abstractmethod + +from .propagators import Stochastic +from ..population import Individual + + +class PointMutation(Stochastic): + """ + Point-mutate given number of traits with given probability. + """ + + def __init__( + self, + limits: Union[ + Dict[str, Tuple[float, float]], + Dict[str, Tuple[int, int]], + Dict[str, Tuple[str, ...]], + ], + points: int = 1, + probability: float = 1.0, + rng: random.Random = None, + ) -> None: + """ + Initialize point-mutation propagator. + + Parameters + ---------- + limits: dict[str, tuple[float, float]] | dict[str, tuple[int, int]] | dict[str, tuple[str, ...]] + limits of (hyper-)parameters to be optimized + points: int + number of points to mutate + probability: float + probability of application + rng: random.Random + random number generator + + Raises + ------ + ValueError + If the requested number of points to mutate is greater than the number of traits. + """ + super(PointMutation, self).__init__(1, 1, probability, rng) + self.points = points + self.limits = limits + if len(limits) < points: + raise ValueError( + f"Too many points to mutate for individual with {len(limits)} traits." + ) + + def __call__(self, ind: Individual) -> Individual: + """ + Apply point-mutation propagator. + + Parameters + ---------- + ind: propulate.individual.Individual + individual the propagator is applied to + + Returns + ------- + propulate.individual.Individual + possibly point-mutated individual after application of propagator + """ + if ( + self.rng.random() < self.probability + ): # Apply propagator only with specified probability + ind = copy.deepcopy(ind) + ind.loss = None # Initialize individual's loss attribute. + # Determine traits to mutate via random sampling. + # Return `self.points` length list of unique elements chosen from `ind.keys()`. + # Used for random sampling without replacement. + to_mutate = self.rng.sample(sorted(ind.keys()), self.points) + # Point-mutate `self.points` randomly chosen traits of individual `ind`. + for i in to_mutate: + if isinstance(ind[i], int): + # Return randomly selected element from int range(start, stop, step). + ind[i] = self.rng.randint(*self.limits[i]) + elif isinstance(ind[i], float): + # Return random floating point number within limits. + ind[i] = self.rng.uniform(*self.limits[i]) + elif isinstance(ind[i], str): + # Return random element from non-empty sequence. + ind[i] = self.rng.choice(self.limits[i]) + + return ind # Return point-mutated individual. + + +class RandomPointMutation(Stochastic): + """ + Point-mutate random number of traits with given probability. + """ + + def __init__( + self, + limits: Union[ + Dict[str, Tuple[float, float]], + Dict[str, Tuple[int, int]], + Dict[str, Tuple[str, ...]], + ], + min_points: int = 1, + max_points: int = 1, + probability: float = 1.0, + rng: random.Random = None, + ) -> None: + """ + Initialize random point-mutation propagator. + + Parameters + ---------- + limits: dict[str, tuple[float, float]] | dict[str, tuple[int, int]] | dict[str, tuple[str, ...]] + limits of parameters to optimize, i.e., search space + min_points: int + minimum number of points to mutate + max_points: int + maximum number of points to mutate + probability: float + probability of application + rng: random.Random + random number generator + + Raises + ------ + ValueError + If no or a negative number of points shall be mutated. + ValueError + If there are fewer traits than requested number of points to mutate. + ValueError + If the requested minimum number of points to mutate is greater than the requested maximum number. + """ + super(RandomPointMutation, self).__init__(1, 1, probability, rng) + if min_points <= 0: + raise ValueError( + f"Minimum number of points to mutate must be > 0 but was {min_points}." + ) + if len(limits) < max_points: + raise ValueError( + f"Too many points to mutate for individual with {len(limits)} traits." + ) + if min_points > max_points: + raise ValueError( + f"Minimum number of traits to mutate must be <= respective maximum number " + f"but min_points = {min_points} > {max_points} = max_points." + ) + self.min_points = min_points + self.max_points = max_points + self.limits = limits + + def __call__(self, ind: Individual) -> Individual: + """ + Apply random-point-mutation propagator. + + Parameters + ---------- + ind: propulate.population.Individual + individual the propagator is applied to + + Returns + ------- + propulate.population.Individual + possibly point-mutated individual after application of propagator + """ + if ( + self.rng.random() < self.probability + ): # Apply propagator only with specified probability. + ind = copy.deepcopy(ind) + ind.loss = None # Initialize individual's loss attribute. + # Determine traits to mutate via random sampling. + # Return `self.points` length list of unique elements chosen from `ind.keys()`. + # Used for random sampling without replacement. + points = self.rng.randint(self.min_points, self.max_points) + to_mutate = self.rng.sample(sorted(ind.keys()), points) + # Point-mutate `points` randomly chosen traits of individual `ind`. + for i in to_mutate: + if isinstance(ind[i], int): + # Return randomly selected element from int range(start, stop, step). + ind[i] = self.rng.randint(*self.limits[i]) + elif isinstance(ind[i], float): + # Return random floating point number N within limits. + ind[i] = self.rng.uniform(*self.limits[i]) + elif isinstance(ind[i], str): + # Return random element from non-empty sequence. + ind[i] = self.rng.choice(self.limits[i]) + + return ind # Return point-mutated individual. + + +class IntervalMutationNormal(Stochastic): + """ + Mutate given number of traits according to Gaussian distribution around current value with given probability. + """ + + def __init__( + self, + limits: Union[ + Dict[str, Tuple[float, float]], + Dict[str, Tuple[int, int]], + Dict[str, Tuple[str, ...]], + ], + sigma_factor: float = 0.1, + points: int = 1, + probability: float = 1.0, + rng: random.Random = None, + ) -> None: + """ + Initialize interval-mutation propagator. + + Parameters + ---------- + limits: dict[str, tuple[float, float]] | dict[str, tuple[int, int]] | dict[str, tuple[str, ...]] + limits of (hyper-)parameters to be optimized, i.e., search space + sigma_factor: float + scaling factor for interval width to obtain standard deviation + points: int + number of points to mutate + probability: float + probability of application + rng: random.Random + random number generator + + Raises + ------ + ValueError + If the individuals has fewer continuous traits than the requested number of points to mutate. + """ + super(IntervalMutationNormal, self).__init__(1, 1, probability, rng) + self.points = points # number of traits to point-mutate + self.limits = limits + self.sigma_factor = sigma_factor + n_interval_traits = len([x for x in limits if isinstance(limits[x][0], float)]) + if n_interval_traits < points: + raise ValueError( + f"Too many points to mutate ({points}) for individual with {n_interval_traits} continuous traits." + ) + + def __call__(self, ind: Individual) -> Individual: + """ + Apply interval-mutation propagator. + + Parameters + ---------- + ind: propulate.individual.Individual + input individual the propagator is applied to + + Returns + ------- + propulate.individual.Individual + possibly interval-mutated output individual after application of propagator + """ + if ( + self.rng.random() < self.probability + ): # Apply propagator only with specified probability. + ind = copy.deepcopy(ind) + ind.loss = None # Initialize individual's loss attribute. + # Determine traits of type float. + interval_keys = [x for x in ind.keys() if isinstance(ind[x], float)] + # Determine ´self.points` traits to mutate. + to_mutate = self.rng.sample(interval_keys, self.points) + # Mutate traits by sampling from Gaussian distribution centered around current value + # with `sigma_factor` scaled interval width as standard distribution. + for i in to_mutate: + min_val, max_val = self.limits[i] # Determine interval boundaries. + sigma = ( + max_val - min_val + ) * self.sigma_factor # Determine std from interval boundaries and sigma factor. + ind[i] = self.rng.gauss( + ind[i], sigma + ) # Sample new value from Gaussian centered around current value. + ind[i] = min( + max_val, ind[i] + ) # Make sure new value is within specified limits. + ind[i] = max(min_val, ind[i]) + + return ind # Return point-mutated individual. + + +class MateUniform(Stochastic): # uniform crossover + """ + Generate new individual by uniform crossover of two parents with specified relative parent contribution. + """ + + def __init__( + self, + rel_parent_contrib: float = 0.5, + probability: float = 1.0, + rng: random.Random = None, + ) -> None: + """ + Initialize uniform crossover propagator. + + Parameters + ---------- + rel_parent_contrib: float + relative parent contribution w.r.t. first parent + probability: float + probability of application + rng: random.Random + random number generator + + Raises + ------ + ValueError + If the relative parent contribution is not within [0, 1]. + """ + super(MateUniform, self).__init__( + 2, 1, probability, rng + ) # Breed 1 offspring from 2 parents. + if rel_parent_contrib <= 0 or rel_parent_contrib >= 1: + raise ValueError( + f"Relative parent contribution must be within (0, 1) but was {rel_parent_contrib}." + ) + self.rel_parent_contrib = rel_parent_contrib + + def __call__(self, inds: List[Individual]) -> Individual: + """ + Apply uniform-crossover propagator. + + Parameters + ---------- + inds: List[propulate.individual.Individual] + individuals the propagator is applied to + + Returns + ------- + propulate.individual.Individual + possibly cross-bred individual after application of propagator + """ + ind = copy.deepcopy(inds[0]) # Consider 1st parent. + ind.loss = None # Initialize individual's loss attribute. + if ( + self.rng.random() < self.probability + ): # Apply propagator only with specified `probability`. + # Replace traits in first parent with values of second parent with specified relative parent contribution. + for k in ind.keys(): + if self.rng.random() > self.rel_parent_contrib: + ind[k] = inds[1][k] + return ind # Return offspring. + + +class MateMultiple(Stochastic): # uniform crossover + """ + Breed new individual by uniform crossover of multiple parents. + """ + + def __init__( + self, parents: int = -1, probability: float = 1.0, rng: random.Random = None + ) -> None: + """ + Initialize multiple-crossover propagator. + + Parameters + ---------- + probability: float + probability of application + parents: int + number of parents + rng: random.Random + random number generator + """ + super(MateMultiple, self).__init__( + parents, 1, probability, rng + ) # Breed 1 offspring from specified number of parents. + + def __call__(self, inds: List[Individual]) -> Individual: + """ + Apply multiple-crossover propagator. + + Parameters + ---------- + inds: list[propulate.individual.Individual] + individuals the propagator is applied to + + Returns + ------- + propulate.individual.Individual + possibly cross-bred individual after application of propagator + """ + ind = copy.deepcopy(inds[0]) # Consider 1st parent. + ind.loss = None # Initialize individual's loss attribute. + if ( + self.rng.random() < self.probability + ): # Apply propagator only with specified `probability`. + # Choose traits from all parents with uniform probability. + for k in ind.keys(): + ind[k] = self.rng.choice([parent[k] for parent in inds]) + return ind # Return offspring. + + +class MateSigmoid(Stochastic): + """ + Generate new individual by crossover of two parents according to Boltzmann sigmoid probability. + + Consider two parent individuals with fitness values f1 and f2. Let f1 <= f2. For each trait, + the better parent's value is accepted with the probability sigmoid(- (f1-f2) / temperature). + """ + + def __init__( + self, + temperature: float = 1.0, + probability: float = 1.0, + rng: random.Random = None, + ) -> None: + """ + Initialize sigmoid-crossover propagator. + + Parameters + ---------- + temperature: float + temperature for Boltzmann factor in sigmoid probability + probability: float + probability of application + rng: random.Random + random number generator + """ + super(MateSigmoid, self).__init__( + 2, 1, probability, rng + ) # Breed 1 offspring from 2 parents. + self.temperature = temperature + + def __call__(self, inds: List[Individual]) -> Individual: + """ + Apply sigmoid-crossover propagator. + + Parameters + ---------- + inds: list[propulate.individual.Individual] + individuals the propagator is applied to + + Returns + ------- + propulate.individual.Individual + possibly cross-bred individual after application of propagator + """ + ind = copy.deepcopy(inds[0]) # Consider 1st parent. + ind.loss = None # Initialize individual's loss attribute. + if inds[0].loss <= inds[1].loss: + delta = inds[0].loss - inds[1].loss + fraction = 1 / (1 + np.exp(-delta / self.temperature)) + else: + delta = inds[1].loss - inds[0].loss + fraction = 1 - 1 / (1 + np.exp(-delta / self.temperature)) + + if ( + self.rng.random() < self.probability + ): # Apply propagator only with specified `probability`. + # Replace traits in 1st parent with values of 2nd parent with Boltzmann probability. + for k in inds[1].keys(): + if self.rng.random() > fraction: + ind[k] = inds[1][k] + return ind # Return offspring. From cbfe41d2973e6f4dd8e2a3e32e66b6a8f97c468a Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 11:54:19 +0200 Subject: [PATCH 130/139] remove unused imports --- propulate/propagators/ga.py | 1 - 1 file changed, 1 deletion(-) diff --git a/propulate/propagators/ga.py b/propulate/propagators/ga.py index 20d46086..dd398bf7 100644 --- a/propulate/propagators/ga.py +++ b/propulate/propagators/ga.py @@ -3,7 +3,6 @@ from typing import List, Dict, Union, Tuple import numpy as np -from abc import ABC, abstractmethod from .propagators import Stochastic from ..population import Individual From f103865dbe2871c69773ea55e3b63b014e491ccd Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 11:55:20 +0200 Subject: [PATCH 131/139] remove unused imports --- propulate/propagators/base.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/propulate/propagators/base.py b/propulate/propagators/base.py index c69d9922..7b98397d 100644 --- a/propulate/propagators/base.py +++ b/propulate/propagators/base.py @@ -1,10 +1,6 @@ -import copy import random from typing import List, Dict, Union, Tuple -import numpy as np -from abc import ABC, abstractmethod - from ..population import Individual From 4baadf65c83c95e5c61cc3d5c7af26802f4da36c Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 14:45:17 +0200 Subject: [PATCH 132/139] restructure population classes --- propulate/__init__.py | 6 +++ .../individual.py => population.py} | 31 ++++++++++++++++ propulate/population/__init__.py | 6 --- propulate/population/particle.py | 37 ------------------- 4 files changed, 37 insertions(+), 43 deletions(-) rename propulate/{population/individual.py => population.py} (79%) delete mode 100644 propulate/population/__init__.py delete mode 100644 propulate/population/particle.py diff --git a/propulate/__init__.py b/propulate/__init__.py index 1de1a856..36bbc829 100644 --- a/propulate/__init__.py +++ b/propulate/__init__.py @@ -11,16 +11,22 @@ del get_distribution, DistributionNotFound from .islands import Islands +from .population import Individual, Particle from .propulator import Propulator from .migrator import Migrator from .pollinator import Pollinator from .utils import get_default_propagator, set_logger_config +from . import propagators + __all__ = [ "Islands", + "Individual", + "Particle", "Propulator", "Migrator", "Pollinator", "get_default_propagator", "set_logger_config", + "propagators", ] diff --git a/propulate/population/individual.py b/propulate/population.py similarity index 79% rename from propulate/population/individual.py rename to propulate/population.py index f961c056..96f2f6fa 100644 --- a/propulate/population/individual.py +++ b/propulate/population.py @@ -133,3 +133,34 @@ def equals(self, other) -> bool: compare_traits = False break return compare_traits and self.loss == other.loss + + +class Particle(Individual): + """ + Child class of ``Individual`` with additional properties required for PSO, i.e., an array-type velocity field and + a (redundant) array-type position field. + + Note that Propulate relies on ``Individual``s being ``dict``s. + + When defining new propagators, users of the ``Particle`` class thus need to ensure that a ``Particle``'s position + always matches its dict contents and vice versa. + + This class also contains an attribute field called ``global_rank``. It contains the global rank of the propagator + that + created it. + This is for purposes of better (or at all) retrieval in multi swarm case. + """ + + def __init__( + self, + position: np.ndarray = None, + velocity: np.ndarray = None, + generation: int = -1, + rank: int = -1, + ): + super().__init__(generation=generation, rank=rank) + if position is not None and velocity is not None: + assert position.shape == velocity.shape + self.velocity = velocity + self.position = position + self.global_rank = rank # The global rank of the creating propagator for later retrieval upon update. diff --git a/propulate/population/__init__.py b/propulate/population/__init__.py deleted file mode 100644 index ac90ae40..00000000 --- a/propulate/population/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -This package bundles all classes that are used as individuals of Propulate's optimization population. -""" -__all__ = ["Individual", "Particle"] -from .individual import Individual -from .particle import Particle diff --git a/propulate/population/particle.py b/propulate/population/particle.py deleted file mode 100644 index 476088b4..00000000 --- a/propulate/population/particle.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -This file contains the Particle class, an extension of Propulate's Individual class. -""" -import numpy as np - -from .individual import Individual - - -class Particle(Individual): - """ - Child class of ``Individual`` with additional properties required for PSO, i.e., an array-type velocity field and - a (redundant) array-type position field. - - Note that Propulate relies on ``Individual``s being ``dict``s. - - When defining new propagators, users of the ``Particle`` class thus need to ensure that a ``Particle``'s position - always matches its dict contents and vice versa. - - This class also contains an attribute field called ``global_rank``. It contains the global rank of the propagator - that - created it. - This is for purposes of better (or at all) retrieval in multi swarm case. - """ - - def __init__( - self, - position: np.ndarray = None, - velocity: np.ndarray = None, - generation: int = -1, - rank: int = -1, - ): - super().__init__(generation=generation, rank=rank) - if position is not None and velocity is not None: - assert position.shape == velocity.shape - self.velocity = velocity - self.position = position - self.global_rank = rank # The global rank of the creating propagator for later retrieval upon update. From 020531900a6d6023f2a285295944976ea304bf6f Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 14:45:57 +0200 Subject: [PATCH 133/139] refactor CMA-ES functionality into separate file --- propulate/propagators/cmaes.py | 832 +++++++++++++++++++++++++++++++++ 1 file changed, 832 insertions(+) create mode 100644 propulate/propagators/cmaes.py diff --git a/propulate/propagators/cmaes.py b/propulate/propagators/cmaes.py new file mode 100644 index 00000000..079a9379 --- /dev/null +++ b/propulate/propagators/cmaes.py @@ -0,0 +1,832 @@ +import copy +import random +from typing import List, Dict, Union, Tuple + +import numpy as np + +from .base import Propagator, SelectMax, SelectMin, SelectUniform +from ..population import Individual + + +class CMAParameter: + """ + Handle and store all basic/active CMA-related constants/variables and strategy parameters. + """ + + def __init__( + self, + lamb: int, + mu: int, + problem_dimension: int, + weights: np.ndarray, + mu_eff: float, + c_c: float, + c_1: float, + c_mu: float, + limits: Dict, + exploration: bool, + ) -> None: + """ + Instantiate a ``CMAParameter`` object. + + Parameters + ---------- + lamb : int + number of individuals considered for each generation + mu : int + number of positive recombination weights + problem_dimension : int + number of dimensions in the search space + weights : numpy.ndarray + recombination weights + mu_eff : float + variance effective selection mass + c_c : float + decay rate for evolution path for the rank-one update of the covariance matrix + c_1 : float + learning rate for the rank-one update of the covariance matrix update + c_mu : float + learning rate for the rank-mu update of the covariance matrix update + limits : dict + limits of search space + exploration : bool + If True decompose covariance matrix for each generation (worse runtime, less exploitation, more + ``decompose_in_each_generation``); else decompose covariance matrix only after a certain number of + individuals evaluated (better runtime, more exploitation, less ``decompose_in_each_generation``). + """ + self.problem_dimension = problem_dimension + self.limits = limits + self.lamb = lamb + self.mu = mu + self.weights = weights + self.mu_eff = mu_eff + self.c_c = c_c + self.c_1 = c_1 + self.c_mu = c_mu + + # Step-size control parameters + self.c_sigma = (mu_eff + 2) / (problem_dimension + mu_eff + 5) + self.d_sigma = ( + 1 + + 2 * max(0, np.sqrt((mu_eff - 1) / (problem_dimension + 1)) - 1) + + self.c_sigma + ) + + # Initialize dynamic strategy variables. + self.p_sigma = np.zeros((problem_dimension, 1)) + self.p_c = np.zeros((problem_dimension, 1)) + + # Prevent equal eigenvalues, hack from https://github.com/CMA-ES/pycma/blob/development/cma/sampler.py + self.co_matrix = np.diag( + np.ones(problem_dimension) + * np.exp( + (1e-4 / self.problem_dimension) * np.arange(self.problem_dimension) + ) + ) + self.b_matrix = np.eye(self.problem_dimension) + # Assume ``self.co_matrix`` to be initialized as a diagonal matrix. + self.d_matrix = np.diag(self.co_matrix) ** 0.5 + # Sort eigenvalues in ascending order + indices_eig = self.d_matrix.argsort() + self.d_matrix = self.d_matrix[indices_eig] + self.b_matrix = self.b_matrix[:, indices_eig] + # Square root of the inverse of the covariance matrix: C^-1/2 = B*D^(-1)*B^T + self.co_inv_sqrt = ( + self.b_matrix @ np.diag(self.d_matrix ** (-1)) @ self.b_matrix.T + ) + # Maximum allowed condition of the covariance matrix to ensure numerical stability + self.condition_limit = 1e5 - 1 + # Whether to keep the trace (sum of diagonal elements) of ``self.co_matrix`` constant. + self.constant_trace = False + + # Use this initial mean when using multiple islands. + self.mean = np.array( + [[np.random.uniform(*limits[limit]) for limit in limits]] + ).reshape((problem_dimension, 1)) + # 0.3 instead of 0.2 is also often used for greater initial step size + self.sigma = 0.2 * ( + (max(max(limits[i]) for i in limits)) - min(min(limits[i]) for i in limits) + ) + + # Mean of the last generation + self.old_mean = None + self.exploration = exploration + + # Number of individuals evaluated when the covariance matrix was last decomposed into B and D + self.eigen_eval = 0 + # Number of individuals evaluated + self.count_eval = 0 + + # Expectation value of ||N(0,I)|| + self.chiN = problem_dimension**0.5 * ( + 1 - 1.0 / (4 * problem_dimension) + 1.0 / (21 * problem_dimension**2) + ) + + def set_mean(self, new_mean: np.ndarray) -> None: + """ + Setter for mean property. Updates the old mean as well. + + Parameters + ---------- + new_mean : numpy.ndarray + new mean + """ + self.old_mean = self.mean + self.mean = new_mean + + def set_p_sigma(self, new_p_sigma: np.ndarray) -> None: + """ + Setter for evolution path of step-size adaptation. + + Parameters + ---------- + new_p_sigma : numpy.ndarray + new evolution path + """ + self.p_sigma = new_p_sigma + + def set_p_c(self, new_p_c: np.ndarray) -> None: + """ + Setter for evolution path of covariance matrix adaptation. + + Parameters + ---------- + new_p_c : numpy.ndarray + evolution path + """ + self.p_c = new_p_c + + def set_sigma(self, new_sigma: float) -> None: + """ + Setter for step-size. + + Parameters + ---------- + new_sigma : float + step-size + """ + self.sigma = new_sigma + + def set_co_matrix(self, new_co_matrix: np.ndarray) -> None: + """ + Setter for the covariance matrix. + + Computes new values for ``b_matrix``, ``d_matrix``, and ``co_inv_sqrt``. Decomposition of ``co_matrix`` is + O(n^3), hence the possibility of lazy updating ``b_matrix`` and ``d_matrix``. + + Parameters + ---------- + new_co_matrix : numpy.ndarray + new covariance matrix + """ + # Update b and d matrix and co_inv_sqrt only after certain number of evaluations to ensure 0(n^2). + # Also trade-off decompose_in_each_generation or not. + if self.exploration or ( + self.count_eval - self.eigen_eval + > self.lamb / (self.c_1 + self.c_mu) / self.problem_dimension / 10 + ): + self.eigen_eval = self.count_eval + self._decompose_co_matrix(new_co_matrix) + self.co_inv_sqrt = ( + self.b_matrix @ np.diag(self.d_matrix ** (-1)) @ self.b_matrix.T + ) + # Ensure symmetry. + self.co_inv_sqrt = (self.co_inv_sqrt + self.co_inv_sqrt.T) / 2 + + def _decompose_co_matrix(self, new_co_matrix: np.ndarray) -> None: + """ + Eigendecomposition of the covariance matrix into eigenvalues (d_matrix) and eigenvectors (columns of b_matrix) + Parameters + ---------- + new_co_matrix: the new covariance matrix that should be decomposed + """ + # Enforce symmetry. + self.co_matrix = np.triu(new_co_matrix) + np.triu(new_co_matrix, 1).T + d_matrix_old = self.d_matrix + try: + self.d_matrix, self.b_matrix = np.linalg.eigh(self.co_matrix) + if any(self.d_matrix <= 0): + # Covariance matrix eigen decomposition failed, consider reformulating objective function. + raise ValueError("Covariance matrix not positive definite.") + except Exception as _: + # Add min(eigenvalues(self.co_matrix_old)) to diag(self.co_matrix) and try again + min_eig_old = min(d_matrix_old) ** 2 + for i in range(self.problem_dimension): + self.co_matrix[i, i] += min_eig_old + # Replace eigenvalues with standard deviations + self.d_matrix = (d_matrix_old**2 + min_eig_old) ** 0.5 + self._decompose_co_matrix(self.co_matrix) + else: + assert all(np.isfinite(self.d_matrix)) + self._sort_b_d_matrix() + if self.condition_limit is not None: + self._limit_condition(self.condition_limit) + if self.constant_trace: + s = 1 / np.mean( + self.d_matrix + ) # normalize co_matrix to control overall magnitude + self.co_matrix *= s + self.d_matrix *= s + self.d_matrix **= 0.5 + + def _limit_condition(self, limit: float) -> None: + """ + Limit the condition (square of ratio largest to smallest eigenvalue) of the covariance matrix if it exceeds a + threshold. + + Credits on how to limit the condition: https://github.com/CMA-ES/pycma/blob/development/cma/sampler.py + + Parameters + ---------- + limit : float + threshold for the condition of the matrix + """ + # Check if condition number of matrix is too big. + if (self.d_matrix[-1] / self.d_matrix[0]) ** 2 > limit: + eps = (self.d_matrix[-1] ** 2 - limit * self.d_matrix[0] ** 2) / (limit - 1) + for i in range(self.problem_dimension): + # Decrease ratio of largest to smallest eigenvalue, absolute difference remains. + self.co_matrix[i, i] += eps + # Eigenvalues are definitely positive now. + self.d_matrix **= 2 + self.d_matrix += eps + self.d_matrix **= 0.5 + + def _sort_b_d_matrix(self) -> None: + """ + Sort columns of ``b_matrix`` and ``d_matrix`` according to the eigenvalues in ``d_matrix``. + """ + indices_eig = np.argsort(self.d_matrix) + self.d_matrix = self.d_matrix[indices_eig] + self.b_matrix = self.b_matrix[:, indices_eig] + assert (min(self.d_matrix), max(self.d_matrix)) == ( + self.d_matrix[0], + self.d_matrix[-1], + ) + + def mahalanobis_norm(self, dx: np.ndarray) -> np.ndarray: + """ + Compute the Mahalanobis distance by using C^(-1/2) and the difference vector of a point to the mean of a + distribution. + + Parameters + ---------- + dx : numpy.ndarray + difference vector + + Returns + ------- + numpy.ndarray + resulting Mahalanobis distance + """ + return np.linalg.norm(np.dot(self.co_inv_sqrt, dx)) + + +class CMAAdapter: + """ + Abstract base class for the adaption of strategy parameters of CMA-ES. + Strategy class from the viewpoint of the strategy design pattern. + """ + + def update_mean(self, par: CMAParameter, arx: np.ndarray) -> None: + """ + Abstract method for updating of mean in CMA-ES variants. + Parameters + ---------- + par : CMAParameter + parameter object of the CMA-ES propagation + arx : numpy.ndarray + individuals of the distribution + + Raises + ------ + NotImplementedError + Whenever called (abstract base class method). + """ + raise NotImplementedError + + @staticmethod + def update_step_size(par: CMAParameter) -> None: + """ + Update step-size in CMA-ES variants. Calculate the current evolution path for the step-size adaption. + + Parameters + ---------- + par : CMAParameter + parameter object of the CMA-ES propagation + """ + par.set_p_sigma( + (1 - par.c_sigma) * par.p_sigma + + np.sqrt(par.c_sigma * (2 - par.c_sigma) * par.mu_eff) + * par.co_inv_sqrt + @ (par.mean - par.old_mean) + / par.sigma + ) + par.set_sigma( + par.sigma + * np.exp( + (par.c_sigma / par.d_sigma) + * (np.linalg.norm(par.p_sigma, ord=2) / par.chiN - 1) + ) + ) + + def update_covariance_matrix(self, par: CMAParameter, arx: np.ndarray) -> None: + """ + Abstract method for the adaption of the covariance matrix of CMA-ES variants. + + Parameters + ---------- + par : CMAParameter + parameter object of the CMA-ES propagation + arx : numpy.ndarray + individuals of the distribution + + Raises + ------ + NotImplementedError + Whenever called (abstract base class method). + """ + raise NotImplementedError + + def compute_weights( + self, mu: int, lamb: int, problem_dimension: int + ) -> Tuple[np.ndarray, float, float, float, float]: + """ + Abstract method for computing the recombination weights of a CMA-ES variant. + + Parameters + ---------- + mu : int + number of positive recombination weights + lamb : int + number of individuals considered for each generation + problem_dimension : int + number of dimensions in the search space + + Returns + ------- + tuple[np.ndarray, float, float, float, float] + tuple of the weights, mu_eff, c_1, c_c and c_mu + + Raises + ------ + NotImplementedError + Whenever called (abstract base class method). + """ + raise NotImplementedError + + @staticmethod + def compute_learning_rates( + mu_eff: float, problem_dimension: int + ) -> Tuple[float, float, float]: + """ + Compute the learning rates for the CMA-variants. + + Parameters + ---------- + mu_eff : float + variance effective selection mass + problem_dimension : int + number of dimensions in the search space + + Returns + ------- + tuple[float, float, float] + tuple of c_c, c_1, c_mu + """ + c_c = (4 + mu_eff / problem_dimension) / ( + problem_dimension + 4 + 2 * mu_eff / problem_dimension + ) + c_1 = 2 / ((problem_dimension + 1.3) ** 2 + mu_eff) + c_mu = min( + 1 - c_1, + 2 * (mu_eff - 2 + (1 / mu_eff)) / ((problem_dimension + 2) ** 2 + mu_eff), + ) + return c_c, c_1, c_mu + + +class BasicCMA(CMAAdapter): + """ + Adaption of strategy parameters of CMA-ES according to the original CMA-ES algorithm. Concrete strategy class from + the viewpoint of the strategy design pattern. + """ + + def compute_weights( + self, mu: int, lamb: int, problem_dimension: int + ) -> Tuple[np.ndarray, float, float, float, float]: + """ + Compute the recombination weights for basic CMA-ES. + + Parameters + ---------- + mu : int + number of positive recombination weights + lamb : int + number of individuals considered for each generation + problem_dimension : int + number of dimensions in the search space + + Returns + ------- + tuple[np.ndarray, float, float, float, float] + tuple of the weights, mu_eff, c_1, c_c and c_mu. + """ + weights = np.log(mu + 0.5) - np.log(np.arange(1, mu + 1)) + weights /= np.sum(weights) + mu_eff = np.sum(weights) ** 2 / np.sum(weights**2) + c_c, c_1, c_mu = BasicCMA.compute_learning_rates(mu_eff, problem_dimension) + return weights, mu_eff, c_c, c_1, c_mu + + def update_mean(self, par: CMAParameter, arx: np.ndarray) -> None: + """ + Update the mean in basic CMA-ES. + + Parameters + ---------- + par : CMAParameter + parameter object of the CMA-ES propagation + arx : numpy.ndarray + individuals of the distribution + """ + # Matrix vector multiplication (reshape weights to column vector) + par.set_mean(arx @ par.weights.reshape(-1, 1)) + + def update_covariance_matrix(self, par: CMAParameter, arx: np.ndarray) -> None: + """ + Adapt the covariance matrix of basic CMA-ES. + + Parameters + ---------- + par : CMAParameter + parameter object of the CMA-ES propagation + arx : numpy.ndarray + individuals of the distribution + """ + # Turn off rank-one accumulation when sigma increases quickly. + h_sig = np.sum(par.p_sigma**2) / ( + 1 - (1 - par.c_sigma) ** (2 * (par.count_eval / par.lamb)) + ) / par.problem_dimension < 2 + 4.0 / (par.problem_dimension + 1) + # Update evolution path. + par.set_p_c( + (1 - par.c_c) * par.p_c + + h_sig + * np.sqrt(par.c_c * (2 - par.c_c) * par.mu_eff) + * (par.mean - par.old_mean) + / par.sigma + ) + # Use ``h_sig`` to the power of two (unlike in paper) for the variance loss from ``h_sig``. + ar_tmp = (1 / par.sigma) * ( + arx[:, : par.mu] - np.tile(par.old_mean, (1, par.mu)) + ) + new_co_matrix = ( + (1 - par.c_1 - par.c_mu) * par.co_matrix + + par.c_1 + * ( + par.p_c @ par.p_c.T + + (1 - h_sig) * par.c_c * (2 - par.c_c) * par.co_matrix + ) + + par.c_mu * ar_tmp @ (par.weights * ar_tmp).T + ) + par.set_co_matrix(new_co_matrix) + + +class ActiveCMA(CMAAdapter): + """ + Adaption of strategy parameters of CMA-ES according to the active CMA-ES algorithm. + + Differently from the original CMA-ES algorithm, active CMA-ES uses negative recombination weights (only for the + covariance matrix adaptation) for individuals with relatively low fitness. + Concrete strategy class from the viewpoint of the strategy design pattern. + """ + + def compute_weights( + self, mu: int, lamb: int, problem_dimension: int + ) -> Tuple[np.ndarray, float, float, float, float]: + """ + Compute the recombination weights for active CMA-ES. + + Parameters + ---------- + mu : int + number of positive recombination weights + lamb : int + number of individuals considered for each generation + problem_dimension : int + number of dimensions in the search space + + Returns + ------- + tuple[np.ndarray, float, float, float, float] + tuple of the weights, mu_eff, c_1, c_c and c_mu + """ + weights_preliminary = np.log(lamb / 2 + 0.5) - np.log(np.arange(1, lamb + 1)) + mu_eff = np.sum(weights_preliminary[:mu]) ** 2 / np.sum( + weights_preliminary[:mu] ** 2 + ) + c_c, c_1, c_mu = ActiveCMA.compute_learning_rates(mu_eff, problem_dimension) + # Now compute final weights. + mu_eff_minus = np.sum(weights_preliminary[mu:]) ** 2 / np.sum( + weights_preliminary[mu:] ** 2 + ) + alpha_mu_minus = 1 + c_1 / c_mu + alpha_mu_eff_minus = 1 + 2 * mu_eff_minus / (mu_eff + 2) + alpha_pos_def_minus = (1 - c_1 - c_mu) / problem_dimension * c_mu + weights = weights_preliminary + weights[:mu] /= np.sum(weights_preliminary[:mu]) + weights[mu:] *= ( + min(alpha_mu_minus, alpha_mu_eff_minus, alpha_pos_def_minus) + / np.sum(weights_preliminary[mu:]) + * -1 + ) + return weights, mu_eff, c_c, c_1, c_mu + + def update_mean(self, par: CMAParameter, arx: np.ndarray) -> None: + """ + Update the mean in active CMA-ES. + + Parameters + ---------- + par : CMAParameter + parameter object of the CMA-ES propagation + arx : numpy.ndarray + individuals of the distribution + """ + # Matrix vector multiplication (reshape weights to column vector) + # Only consider positive weights. + par.set_mean(arx @ par.weights[: par.mu].reshape(-1, 1)) + + def update_covariance_matrix(self, par: CMAParameter, arx: np.ndarray) -> None: + """ + Adapt the covariance matrix of active CMA-ES. + + Parameters + ---------- + par : CMAParameter + parameter object of the CMA-ES propagation + arx : numpy.ndarray + individuals of the distribution + """ + # Turn off rank-one accumulation when sigma increases quickly. + h_sig = np.sum(par.p_sigma**2) / ( + 1 - (1 - par.c_sigma) ** (2 * (par.count_eval / par.lamb)) + ) / par.problem_dimension < 2 + 4.0 / (par.problem_dimension + 1) + # Update evolution path. + par.set_p_c( + (1 - par.c_c) * par.p_c + + h_sig + * np.sqrt(par.c_c * (2 - par.c_c) * par.mu_eff) + * (par.mean - par.old_mean) + / par.sigma + ) + weights_circle = np.zeros((par.lamb,)) + for i, w_i in enumerate(par.weights): + # Guarantee positive definiteness. + weights_circle[i] = w_i + if w_i < 0: + weights_circle[i] *= ( + par.problem_dimension + * ( + par.sigma + / par.mahalanobis_norm(arx[:, i] - par.old_mean.ravel()) + ) + ** 2 + ) + # Use ``h_sig`` to the power of two (unlike in paper) for the variance loss from ``h_sig``. + ar_tmp = (1 / par.sigma) * (arx - np.tile(par.old_mean, (1, par.lamb))) + new_co_matrix = ( + (1 - par.c_1 - par.c_mu) * par.co_matrix + + par.c_1 + * ( + par.p_c @ par.p_c.T + + (1 - h_sig) * par.c_c * (2 - par.c_c) * par.co_matrix + ) + + par.c_mu * ar_tmp @ (weights_circle * ar_tmp).T + ) + par.set_co_matrix(new_co_matrix) + + +class CMAPropagator(Propagator): + """ + CMA-ES propagator. + + Uses ``CMAAdapter`` to adapt strategy parameters like mean, step-size, and covariance matrix and stores them in a + ``CMAParameter`` object. The context class from the viewpoint of the strategy design pattern. + """ + + def __init__( + self, + adapter: CMAAdapter, + limits: Dict, + rng: random.Random, + decompose_in_each_generation: bool = False, + select_worst_all_time: bool = False, + pop_size: int = None, + pool_size: int = 3, + ) -> None: + """ + Instantiate a CMA-ES propagator. + + Parameters + ---------- + adapter : CMAAdapter + adaption strategy of CMA-ES + limits : dict + limits of the search space + rng: random.Random + random number generator + decompose_in_each_generation : bool + If True, decompose covariance matrix for each generation (worse runtime, less exploitation, more + exploration); else decompose covariance matrix only after a certain number of individuals evaluated + (better runtime, more exploitation, less exploration) + select_worst_all_time : bool + If True, use the worst individuals for negative recombination weights in active CMA-ES, else use the worst + (lambda - mu) individuals of the best lambda individuals. If BasicCMA is used, the given value is irrelevant + regarding functionality. + pop_size : int + number of individuals to be considered in each generation + pool_size : iny + size of the pool of individuals preselected before selecting the best of this pool + """ + self.adapter = adapter + problem_dimension = len(limits) + # Number of individuals considered for each generation + lamb = ( + pop_size if pop_size else 4 + int(np.floor(3 * np.log(problem_dimension))) + ) + super(CMAPropagator, self).__init__(lamb, 1) + + # Number of positive recombination weights + mu = lamb // 2 + self.select_worst = SelectMax(lamb - mu) + self.select_worst_all_time = select_worst_all_time + + # CMA-ES variant specific weights and learning rates + weights, mu_eff, c_c, c_1, c_mu = adapter.compute_weights( + mu, lamb, problem_dimension + ) + + self.par = CMAParameter( + lamb, + mu, + problem_dimension, + weights, + mu_eff, + c_c, + c_1, + c_mu, + limits, + decompose_in_each_generation, + ) + self.pool_size = int(pool_size) if int(pool_size) >= 1 else 3 + self.select_pool = SelectMin(self.pool_size * lamb) + self.select_from_pool = SelectUniform(mu - 1, rng=rng) + self.select_best_1 = SelectMin(1) + + def __call__(self, inds: List[Individual]) -> Individual: + """ + The skeleton of the CMA-ES algorithm using the template method design pattern. + + Sampling individuals and adapting the strategy parameters. Template methods are ``update_mean``, + ``update_covariance_matrix``, and ``update_step_size``. + + Parameters + ---------- + inds: list[Individual] + individuals available + + Returns + ------- + new_ind : Individual + newly sampled individual + """ + num_inds = len(inds) + # Add individuals from different workers to ``eval_count``. + self.par.count_eval += num_inds - self.par.count_eval + # Sample new individual. + new_ind = self._sample_cma() + # Check if ``len(inds)`` >= or < ``pool_size * lambda`` and make sample or sample + update. + if num_inds >= self.pool_size * self.par.lamb: + inds_pooled = self.select_pool(inds) + best = self.select_best_1(inds_pooled) + if not self.select_worst_all_time: + worst = self.select_worst(inds_pooled) + else: + worst = self.select_worst(inds) + + inds_filtered = [ + ind for ind in inds_pooled if ind not in best and ind not in worst + ] + arx = self._transform_individuals_to_matrix( + best + self.select_from_pool(inds_filtered) + worst + ) + + # Update mean. + self.adapter.update_mean(self.par, arx[:, : self.par.mu]) + # Update covariance matrix. + self.adapter.update_covariance_matrix(self.par, arx) + # Update step size. + self.adapter.update_step_size(self.par) + return new_ind + + def _transform_individuals_to_matrix(self, inds: List[Individual]) -> np.ndarray: + """ + Take a list of individuals and transform it to numpy matrix for easier subsequent computation. + + Parameters + ---------- + inds : list[Individual] + list of individuals + + Returns + ------- + arx : numpy.ndarray + Array of shape (problem_dimension, len(inds)) + """ + arx = np.zeros((self.par.problem_dimension, len(inds))) + for k, ind in enumerate(inds): + for i, (dim, _) in enumerate(self.par.limits.items()): + arx[i, k] = ind[dim] + return arx + + def _sample_cma(self) -> Individual: + """ + Sample new individuals according to CMA-ES. + + Returns + ------- + new_ind : Individual + the newly sampled individual + """ + new_x = None + # Generate new offspring + random_vector = np.random.randn(self.par.problem_dimension, 1) + try: + new_x = self.par.mean + self.par.sigma * self.par.b_matrix @ ( + self.par.d_matrix * random_vector + ) + except (RuntimeWarning, Exception) as _: + raise ValueError( + "Failed to generate new offsprings, probably due to not well defined target function." + ) + self.par.count_eval += 1 + # Remove problem_dim. + new_ind = Individual() + + for i, (dim, _) in enumerate(self.par.limits.items()): + new_ind[dim] = new_x[i, 0] + return new_ind + + def get_mean(self) -> np.ndarray: + """ + Getter for mean attribute. + + Returns + ------- + mean : numpy.ndarray + current CMA-ES mean of the best mu individuals + """ + return self.par.mean + + def get_sigma(self) -> float: + """ + Getter for step size. + + Returns + ------- + sigma : float + current step-size + """ + return self.par.sigma + + def get_co_matrix(self) -> np.ndarray: + """ + Getter for covariance matrix. + + Returns + ------- + co_matrix : numpy.ndarray + current covariance matrix + """ + return self.par.co_matrix + + def get_evolution_path_sigma(self) -> np.ndarray: + """ + Getter for evolution path of step-size adaption. + + Returns + ------- + p_sigma : numpy.ndarray + evolution path for step-size adaption + """ + return self.par.p_sigma + + def get_evolution_path_co_matrix(self) -> np.ndarray: + """ + Getter for evolution path of covariance matrix adaption. + + Returns + ------- + p_c : numpy.ndarray + evolution path for covariance matrix adaption + """ + return self.par.p_c From 5ab0ad05a6230573aae73dc036ecca1ce2ac92c5 Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 14:50:23 +0200 Subject: [PATCH 134/139] update checkpoint file suffix to pickle --- propulate/propulator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/propulate/propulator.py b/propulate/propulator.py index c6d0aa0d..63f0714c 100644 --- a/propulate/propulator.py +++ b/propulate/propulator.py @@ -103,7 +103,7 @@ def __init__( self.rng = rng # Load initial population of evaluated individuals from checkpoint if exists. - load_ckpt_file = self.checkpoint_path / f"island_{self.island_idx}_ckpt.pkl" + load_ckpt_file = self.checkpoint_path / f"island_{self.island_idx}_ckpt.pickle" if not os.path.isfile(load_ckpt_file): # If not exists, check for backup file. load_ckpt_file = load_ckpt_file.with_suffix(".bkp") @@ -400,7 +400,7 @@ def _dump_checkpoint(self): f"Island {self.island_idx} Worker {self.comm.rank} Generation {self.generation}: " f"Dumping checkpoint..." ) - save_ckpt_file = self.checkpoint_path / f"island_{self.island_idx}_ckpt.pkl" + save_ckpt_file = self.checkpoint_path / f"island_{self.island_idx}_ckpt.pickle" if os.path.isfile(save_ckpt_file): try: os.replace(save_ckpt_file, save_ckpt_file.with_suffix(".bkp")) @@ -431,7 +431,7 @@ def _dump_final_checkpoint(self): """ Dump final checkpoint. """ - save_ckpt_file = self.checkpoint_path / f"island_{self.island_idx}_ckpt.pkl" + save_ckpt_file = self.checkpoint_path / f"island_{self.island_idx}_ckpt.pickle" if os.path.isfile(save_ckpt_file): try: os.replace(save_ckpt_file, save_ckpt_file.with_suffix(".bkp")) From 3efc2bf8cc65e05dd97905754d0f99b18639b8d9 Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 14:52:50 +0200 Subject: [PATCH 135/139] update gitignore --- .gitignore | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index ce6a126e..9226285b 100644 --- a/.gitignore +++ b/.gitignore @@ -49,15 +49,6 @@ MANIFEST # Per-project virtualenvs .venv*/ -*.pkl - +*.pickle MNIST -*cpt.p -*cpt.p.bkp -scripts/*.png - -voucher_propulate.txt - -.gitignore -checkpoints/ -images/ +*.bkp From 28d23c93916f312fe2e74e7f71287bad8187a5c8 Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 14:53:14 +0200 Subject: [PATCH 136/139] add missing numpy import --- propulate/population.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/propulate/population.py b/propulate/population.py index 96f2f6fa..8aa5315c 100644 --- a/propulate/population.py +++ b/propulate/population.py @@ -1,5 +1,7 @@ from decimal import Decimal +import numpy as np + class Individual(dict): """ From b2ca829f51d5e3927b85c7c077bda10fd686dd49 Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 14:55:33 +0200 Subject: [PATCH 137/139] fix imports after refactoring propagators and population classes --- propulate/propagators/__init__.py | 75 +++++++++++++++++++++---------- propulate/propagators/ga.py | 2 +- propulate/propagators/pso.py | 12 ++--- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/propulate/propagators/__init__.py b/propulate/propagators/__init__.py index 95aec6e8..d852105d 100644 --- a/propulate/propagators/__init__.py +++ b/propulate/propagators/__init__.py @@ -1,39 +1,66 @@ """ -This package holds all Propagator subclasses including the Propagator itself. +This package bundles all classes that are used as propagators in Propulate's optimization routine. """ +from .base import ( + Propagator, + Stochastic, + Conditional, + Compose, + SelectMin, + SelectMax, + SelectUniform, + InitUniform, +) + +from .ga import ( + PointMutation, + RandomPointMutation, + IntervalMutationNormal, + MateUniform, + MateMultiple, + MateSigmoid, +) + +from .pso import ( + BasicPSO, + VelocityClampingPSO, + ConstrictionPSO, + CanonicalPSO, + StatelessPSO, +) + +from .cmaes import ( + CMAAdapter, + CMAParameter, + CMAPropagator, + BasicCMA, + ActiveCMA, +) + __all__ = [ "Propagator", "Stochastic", "Conditional", "Compose", + "SelectMin", + "SelectMax", + "SelectUniform", + "InitUniform", "PointMutation", "RandomPointMutation", "IntervalMutationNormal", "MateUniform", "MateMultiple", "MateSigmoid", - "SelectMin", - "SelectMax", - "SelectUniform", - "InitUniform", - "pso", + "BasicPSO", + "VelocityClampingPSO", + "ConstrictionPSO", + "CanonicalPSO", + "StatelessPSO", + "CMAAdapter", + "CMAParameter", + "CMAPropagator", + "BasicCMA", + "ActiveCMA", ] - -from propulate.propagators.propagators import ( - Propagator, - Stochastic, - Conditional, - Compose, - PointMutation, - RandomPointMutation, - IntervalMutationNormal, - MateUniform, - MateMultiple, - MateSigmoid, - SelectMin, - SelectMax, - SelectUniform, - InitUniform, -) -from . import pso diff --git a/propulate/propagators/ga.py b/propulate/propagators/ga.py index dd398bf7..3d7bbc70 100644 --- a/propulate/propagators/ga.py +++ b/propulate/propagators/ga.py @@ -4,7 +4,7 @@ import numpy as np -from .propagators import Stochastic +from .base import Stochastic from ..population import Individual diff --git a/propulate/propagators/pso.py b/propulate/propagators/pso.py index 876dfbc5..d9673b21 100644 --- a/propulate/propagators/pso.py +++ b/propulate/propagators/pso.py @@ -9,7 +9,7 @@ from ..utils import make_particle -class Basic(Propagator): +class BasicPSO(Propagator): """ This propagator implements the most basic PSO variant one possibly could think of. @@ -179,7 +179,7 @@ def _make_new_particle( return new_p -class VelocityClamping(Basic): +class VelocityClampingPSO(BasicPSO): """ This propagator implements velocity clamping PSO. @@ -259,7 +259,7 @@ def __call__(self, individuals: List[Individual]) -> Particle: return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) -class Constriction(Basic): +class ConstrictionPSO(BasicPSO): """ This propagator subclass features constriction PSO as proposed by Clerc and Kennedy in 2002. @@ -347,7 +347,7 @@ def __call__(self, individuals: List[Individual]) -> Particle: return self._make_new_particle(new_position, new_velocity, old_p.generation + 1) -class Canonical(Constriction): +class CanonicalPSO(ConstrictionPSO): """ This propagator subclass features a combination of constriction PSO and velocity clamping. @@ -425,7 +425,7 @@ def __call__(self, individuals: List[Individual]) -> Particle: return self._make_new_particle(p, v, victim.generation) -class InitUniform(Stochastic): +class InitUniformPSO(Stochastic): """ Initialize ``Particle`` by uniformly sampling specified limits for each trait. """ @@ -517,7 +517,7 @@ def __call__(self, individuals: List[Individual]) -> Particle: return make_particle(particle) -class Stateless(Propagator): +class StatelessPSO(Propagator): """ This propagator performs PSO without the need of Particles, but as a consequence, also without velocity. Thus, it is called stateless. From b92aec48d27d51d558e3c54861eac6bbdae753a3 Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 14:56:32 +0200 Subject: [PATCH 138/139] fix typos and default param values --- tutorials/cmaes_example.py | 2 +- tutorials/pso_example.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tutorials/cmaes_example.py b/tutorials/cmaes_example.py index 69958545..d67d93ca 100644 --- a/tutorials/cmaes_example.py +++ b/tutorials/cmaes_example.py @@ -84,7 +84,7 @@ elif config.adapter == "active": adapter = ActiveCMA() else: - raise ValueError("Adapter can be either 'basic' or 'active'") + raise ValueError("Adapter can be either 'basic' or 'active'.") propagator = CMAPropagator(adapter, limits, rng=rng) diff --git a/tutorials/pso_example.py b/tutorials/pso_example.py index ffb8ec75..5f0249a9 100644 --- a/tutorials/pso_example.py +++ b/tutorials/pso_example.py @@ -13,11 +13,11 @@ from propulate import set_logger_config, Propulator from propulate.propagators import Conditional, Propagator from propulate.propagators.pso import ( - Basic, - VelocityClamping, - Constriction, - Canonical, - InitUniform, + BasicPSO, + VelocityClampingPSO, + ConstrictionPSO, + CanonicalPSO, + InitUniformPSO, ) from tutorials.function_benchmark import get_function_search_space @@ -109,12 +109,12 @@ def __call__(self, parser, namespace, values, option_string=None): "--clamping_factor", type=float, default=0.6 ) # Clamping factor for velocity clamping parser.add_argument("-t", "--top_n", type=int, default=1) - parser.add_argument("-l", "--logging_int", type=int, default=10) + parser.add_argument("-l", "--logging_int", type=int, default=20) config = parser.parse_args() # Set up separate logger for Propulate optimization. set_logger_config( - level=10 * config.verbosity, # logging level + level=config.logging_int, # logging level log_file=f"{config.checkpoint}/propulator.log", # logging path ) @@ -133,7 +133,7 @@ def __call__(self, parser, namespace, values, option_string=None): pso_propagator: Propagator if config.variant == "Basic": - pso_propagator = Basic( + pso_propagator = BasicPSO( config.inertia, config.cognitive, config.social, @@ -142,7 +142,7 @@ def __call__(self, parser, namespace, values, option_string=None): rng, ) elif config.variant == "VelocityClamping": - pso_propagator = VelocityClamping( + pso_propagator = VelocityClampingPSO( config.inertia, config.cognitive, config.social, @@ -152,16 +152,16 @@ def __call__(self, parser, namespace, values, option_string=None): config.clamping_factor, ) elif config.variant == "Constriction": - pso_propagator = Constriction( + pso_propagator = ConstrictionPSO( config.cognitive, config.social, MPI.COMM_WORLD.rank, limits, rng ) elif config.variant == "Canonical": - pso_propagator = Canonical( + pso_propagator = CanonicalPSO( config.cognitive, config.social, MPI.COMM_WORLD.rank, limits, rng ) else: raise ValueError("Invalid PSO propagator name given.") - init = InitUniform(limits, rng=rng, rank=MPI.COMM_WORLD.rank) + init = InitUniformPSO(limits, rng=rng, rank=MPI.COMM_WORLD.rank) propagator = Conditional(config.pop_size, pso_propagator, init) propulator = Propulator( From 82f96fe11ab048629863c840a920b3c2dd110462 Mon Sep 17 00:00:00 2001 From: Marie Weiel Date: Wed, 11 Oct 2023 15:02:02 +0200 Subject: [PATCH 139/139] black formatting --- propulate/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/propulate/utils.py b/propulate/utils.py index 2714e8fd..0815f8a8 100644 --- a/propulate/utils.py +++ b/propulate/utils.py @@ -168,7 +168,7 @@ def make_particle(individual: Individual) -> Particle: ---------- individual: Individual Individual to be converted to a particle - + Returns -------- Particle