From b0d83a88be5c4de6479ebb228703c5297eace52f Mon Sep 17 00:00:00 2001 From: Donato Rimenti Date: Thu, 22 Feb 2018 21:01:23 +0100 Subject: [PATCH] BAEL-1352 Multiswarm optimization algorithm. --- .../algorithms/multiswarm/Constants.java | 47 ++++ .../multiswarm/FitnessFunction.java | 21 ++ .../algorithms/multiswarm/Multiswarm.java | 227 ++++++++++++++++++ .../algorithms/multiswarm/Particle.java | 204 ++++++++++++++++ .../baeldung/algorithms/multiswarm/Swarm.java | 156 ++++++++++++ .../multiswarm/MultiswarmUnitTest.java | 78 ++++++ .../algorithms/support/MayFailRule.java | 38 +++ 7 files changed, 771 insertions(+) create mode 100644 algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Constants.java create mode 100644 algorithms/src/main/java/com/baeldung/algorithms/multiswarm/FitnessFunction.java create mode 100644 algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Multiswarm.java create mode 100644 algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Particle.java create mode 100644 algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Swarm.java create mode 100644 algorithms/src/test/java/com/baeldung/algorithms/multiswarm/MultiswarmUnitTest.java create mode 100644 algorithms/src/test/java/com/baeldung/algorithms/support/MayFailRule.java diff --git a/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Constants.java b/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Constants.java new file mode 100644 index 0000000000..b646c686b2 --- /dev/null +++ b/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Constants.java @@ -0,0 +1,47 @@ +package com.baeldung.algorithms.multiswarm; + +/** + * Constants used by the Multi-swarm optimization algorithms. + * + * @author Donato Rimenti + * + */ +public class Constants { + + /** + * The inertia factor encourages a particle to continue moving in its + * current direction. + */ + public static final double INERTIA_FACTOR = 0.729; + + /** + * The cognitive weight encourages a particle to move toward its historical + * best-known position. + */ + public static final double COGNITIVE_WEIGHT = 1.49445; + + /** + * The social weight encourages a particle to move toward the best-known + * position found by any of the particle’s swarm-mates. + */ + public static final double SOCIAL_WEIGHT = 1.49445; + + /** + * The global weight encourages a particle to move toward the best-known + * position found by any particle in any swarm. + */ + public static final double GLOBAL_WEIGHT = 0.3645; + + /** + * Upper bound for the random generation. We use it to reduce the + * computation time since we can rawly estimate it. + */ + public static final int PARTICLE_UPPER_BOUND = 10000000; + + /** + * Private constructor for utility class. + */ + private Constants() { + } + +} diff --git a/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/FitnessFunction.java b/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/FitnessFunction.java new file mode 100644 index 0000000000..2d86ec8d94 --- /dev/null +++ b/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/FitnessFunction.java @@ -0,0 +1,21 @@ +package com.baeldung.algorithms.multiswarm; + +/** + * Interface for a fitness function, used to decouple the main algorithm logic + * from the specific problem solution. + * + * @author Donato Rimenti + * + */ +public interface FitnessFunction { + + /** + * Returns the fitness of a particle given its position. + * + * @param particlePosition + * the position of the particle + * @return the fitness of the particle + */ + public double getFitness(long[] particlePosition); + +} diff --git a/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Multiswarm.java b/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Multiswarm.java new file mode 100644 index 0000000000..ef60726278 --- /dev/null +++ b/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Multiswarm.java @@ -0,0 +1,227 @@ +package com.baeldung.algorithms.multiswarm; + +import java.util.Arrays; +import java.util.Random; + +/** + * Represents a collection of {@link Swarm}. + * + * @author Donato Rimenti + * + */ +public class Multiswarm { + + /** + * The swarms managed by this multiswarm. + */ + private Swarm[] swarms; + + /** + * The best position found within all the {@link #swarms}. + */ + private long[] bestPosition; + + /** + * The best fitness score found within all the {@link #swarms}. + */ + private double bestFitness = Double.NEGATIVE_INFINITY; + + /** + * A random generator. + */ + private Random random = new Random(); + + /** + * The fitness function used to determine how good is a particle. + */ + private FitnessFunction fitnessFunction; + + /** + * Instantiates a new Multiswarm. + * + * @param numSwarms + * the number of {@link #swarms} + * @param particlesPerSwarm + * the number of particle for each {@link #swarms} + * @param fitnessFunction + * the {@link #fitnessFunction} + */ + public Multiswarm(int numSwarms, int particlesPerSwarm, FitnessFunction fitnessFunction) { + this.fitnessFunction = fitnessFunction; + this.swarms = new Swarm[numSwarms]; + for (int i = 0; i < numSwarms; i++) { + swarms[i] = new Swarm(particlesPerSwarm); + } + } + + /** + * Main loop of the algorithm. Iterates all particles of all + * {@link #swarms}. For each particle, computes the new fitness and checks + * if a new best position has been found among itself, the swarm and all the + * swarms and finally updates the particle position and speed. + */ + public void mainLoop() { + for (Swarm swarm : swarms) { + for (Particle particle : swarm.getParticles()) { + + long[] particleOldPosition = particle.getPosition().clone(); + + // Calculate the particle fitness. + particle.setFitness(fitnessFunction.getFitness(particleOldPosition)); + + // Check if a new best position has been found for the particle + // itself, within the swarm and the multiswarm. + if (particle.getFitness() > particle.getBestFitness()) { + particle.setBestFitness(particle.getFitness()); + particle.setBestPosition(particleOldPosition); + + if (particle.getFitness() > swarm.getBestFitness()) { + swarm.setBestFitness(particle.getFitness()); + swarm.setBestPosition(particleOldPosition); + + if (swarm.getBestFitness() > bestFitness) { + bestFitness = swarm.getBestFitness(); + bestPosition = swarm.getBestPosition().clone(); + } + + } + } + + // Updates the particle position by adding the speed to the + // actual position. + long[] position = particle.getPosition(); + long[] speed = particle.getSpeed(); + + position[0] += speed[0]; + position[1] += speed[1]; + + // Updates the particle speed. + speed[0] = getNewParticleSpeedForIndex(particle, swarm, 0); + speed[1] = getNewParticleSpeedForIndex(particle, swarm, 1); + } + } + } + + /** + * Computes a new speed for a given particle of a given swarm on a given + * axis. The new speed is computed using the formula: + * + *
+	 * ({@link Constants#INERTIA_FACTOR} * {@link Particle#getSpeed()}) + 
+	 * (({@link Constants#COGNITIVE_WEIGHT} * random(0,1)) * ({@link Particle#getBestPosition()} - {@link Particle#getPosition()})) +
+	 * (({@link Constants#SOCIAL_WEIGHT} * random(0,1)) * ({@link Swarm#getBestPosition()} - {@link Particle#getPosition()})) + 
+	 * (({@link Constants#GLOBAL_WEIGHT} * random(0,1)) * ({@link #bestPosition} - {@link Particle#getPosition()}))
+	 * 
+ * + * @param particle + * the particle whose new speed needs to be computed + * @param swarm + * the swarm which contains the particle + * @param index + * the index of the particle axis whose speeds needs to be + * computed + * @return the new speed of the particle passed on the given axis + */ + private int getNewParticleSpeedForIndex(Particle particle, Swarm swarm, int index) { + return (int) ((Constants.INERTIA_FACTOR * particle.getSpeed()[index]) + + (randomizePercentage(Constants.COGNITIVE_WEIGHT) + * (particle.getBestPosition()[index] - particle.getPosition()[index])) + + (randomizePercentage(Constants.SOCIAL_WEIGHT) + * (swarm.getBestPosition()[index] - particle.getPosition()[index])) + + (randomizePercentage(Constants.GLOBAL_WEIGHT) + * (bestPosition[index] - particle.getPosition()[index]))); + } + + /** + * Returns a random number between 0 and the value passed as argument. + * + * @param value + * the value to randomize + * @return a random value between 0 and the one passed as argument + */ + private double randomizePercentage(double value) { + return random.nextDouble() * value; + } + + /** + * Gets the {@link #bestPosition}. + * + * @return the {@link #bestPosition} + */ + public long[] getBestPosition() { + return bestPosition; + } + + /** + * Gets the {@link #bestFitness}. + * + * @return the {@link #bestFitness} + */ + public double getBestFitness() { + return bestFitness; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(bestFitness); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + Arrays.hashCode(bestPosition); + result = prime * result + ((fitnessFunction == null) ? 0 : fitnessFunction.hashCode()); + result = prime * result + ((random == null) ? 0 : random.hashCode()); + result = prime * result + Arrays.hashCode(swarms); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Multiswarm other = (Multiswarm) obj; + if (Double.doubleToLongBits(bestFitness) != Double.doubleToLongBits(other.bestFitness)) + return false; + if (!Arrays.equals(bestPosition, other.bestPosition)) + return false; + if (fitnessFunction == null) { + if (other.fitnessFunction != null) + return false; + } else if (!fitnessFunction.equals(other.fitnessFunction)) + return false; + if (random == null) { + if (other.random != null) + return false; + } else if (!random.equals(other.random)) + return false; + if (!Arrays.equals(swarms, other.swarms)) + return false; + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Multiswarm [swarms=" + Arrays.toString(swarms) + ", bestPosition=" + Arrays.toString(bestPosition) + + ", bestFitness=" + bestFitness + ", random=" + random + ", fitnessFunction=" + fitnessFunction + "]"; + } + +} diff --git a/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Particle.java b/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Particle.java new file mode 100644 index 0000000000..5930a94267 --- /dev/null +++ b/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Particle.java @@ -0,0 +1,204 @@ +package com.baeldung.algorithms.multiswarm; + +import java.util.Arrays; + +/** + * Represents a particle, the basic component of a {@link Swarm}. + * + * @author Donato Rimenti + * + */ +public class Particle { + + /** + * The current position of this particle. + */ + private long[] position; + + /** + * The speed of this particle. + */ + private long[] speed; + + /** + * The fitness of this particle for the current position. + */ + private double fitness; + + /** + * The best position found by this particle. + */ + private long[] bestPosition; + + /** + * The best fitness found by this particle. + */ + private double bestFitness = Double.NEGATIVE_INFINITY; + + /** + * Instantiates a new Particle. + * + * @param initialPosition + * the initial {@link #position} + * @param initialSpeed + * the initial {@link #speed} + */ + public Particle(long[] initialPosition, long[] initialSpeed) { + this.position = initialPosition; + this.speed = initialSpeed; + } + + /** + * Gets the {@link #position}. + * + * @return the {@link #position} + */ + public long[] getPosition() { + return position; + } + + /** + * Gets the {@link #speed}. + * + * @return the {@link #speed} + */ + public long[] getSpeed() { + return speed; + } + + /** + * Gets the {@link #fitness}. + * + * @return the {@link #fitness} + */ + public double getFitness() { + return fitness; + } + + /** + * Gets the {@link #bestPosition}. + * + * @return the {@link #bestPosition} + */ + public long[] getBestPosition() { + return bestPosition; + } + + /** + * Gets the {@link #bestFitness}. + * + * @return the {@link #bestFitness} + */ + public double getBestFitness() { + return bestFitness; + } + + /** + * Sets the {@link #position}. + * + * @param position + * the new {@link #position} + */ + public void setPosition(long[] position) { + this.position = position; + } + + /** + * Sets the {@link #speed}. + * + * @param speed + * the new {@link #speed} + */ + public void setSpeed(long[] speed) { + this.speed = speed; + } + + /** + * Sets the {@link #fitness}. + * + * @param fitness + * the new {@link #fitness} + */ + public void setFitness(double fitness) { + this.fitness = fitness; + } + + /** + * Sets the {@link #bestPosition}. + * + * @param bestPosition + * the new {@link #bestPosition} + */ + public void setBestPosition(long[] bestPosition) { + this.bestPosition = bestPosition; + } + + /** + * Sets the {@link #bestFitness}. + * + * @param bestFitness + * the new {@link #bestFitness} + */ + public void setBestFitness(double bestFitness) { + this.bestFitness = bestFitness; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(bestFitness); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + Arrays.hashCode(bestPosition); + temp = Double.doubleToLongBits(fitness); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + Arrays.hashCode(position); + result = prime * result + Arrays.hashCode(speed); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Particle other = (Particle) obj; + if (Double.doubleToLongBits(bestFitness) != Double.doubleToLongBits(other.bestFitness)) + return false; + if (!Arrays.equals(bestPosition, other.bestPosition)) + return false; + if (Double.doubleToLongBits(fitness) != Double.doubleToLongBits(other.fitness)) + return false; + if (!Arrays.equals(position, other.position)) + return false; + if (!Arrays.equals(speed, other.speed)) + return false; + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Particle [position=" + Arrays.toString(position) + ", speed=" + Arrays.toString(speed) + ", fitness=" + + fitness + ", bestPosition=" + Arrays.toString(bestPosition) + ", bestFitness=" + bestFitness + "]"; + } + +} diff --git a/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Swarm.java b/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Swarm.java new file mode 100644 index 0000000000..56ab712a1d --- /dev/null +++ b/algorithms/src/main/java/com/baeldung/algorithms/multiswarm/Swarm.java @@ -0,0 +1,156 @@ +package com.baeldung.algorithms.multiswarm; + +import java.util.Arrays; +import java.util.Random; + +// TODO: Auto-generated Javadoc +/** + * Represents a collection of {@link Particle}. + * + * @author Donato Rimenti + * + */ +public class Swarm { + + /** + * The particles of this swarm. + */ + private Particle[] particles; + + /** + * The best position found within the particles of this swarm. + */ + private long[] bestPosition; + + /** + * The best fitness score found within the particles of this swarm. + */ + private double bestFitness = Double.NEGATIVE_INFINITY; + + /** + * A random generator. + */ + private Random random = new Random(); + + /** + * Instantiates a new Swarm. + * + * @param numParticles + * the number of particles of the swarm + */ + public Swarm(int numParticles) { + particles = new Particle[numParticles]; + for (int i = 0; i < numParticles; i++) { + long[] initialParticlePosition = { random.nextInt(Constants.PARTICLE_UPPER_BOUND), + random.nextInt(Constants.PARTICLE_UPPER_BOUND) }; + long[] initialParticleSpeed = { random.nextInt(Constants.PARTICLE_UPPER_BOUND), + random.nextInt(Constants.PARTICLE_UPPER_BOUND) }; + particles[i] = new Particle(initialParticlePosition, initialParticleSpeed); + } + } + + /** + * Gets the {@link #particles}. + * + * @return the {@link #particles} + */ + public Particle[] getParticles() { + return particles; + } + + /** + * Gets the {@link #bestPosition}. + * + * @return the {@link #bestPosition} + */ + public long[] getBestPosition() { + return bestPosition; + } + + /** + * Gets the {@link #bestFitness}. + * + * @return the {@link #bestFitness} + */ + public double getBestFitness() { + return bestFitness; + } + + /** + * Sets the {@link #bestPosition}. + * + * @param bestPosition + * the new {@link #bestPosition} + */ + public void setBestPosition(long[] bestPosition) { + this.bestPosition = bestPosition; + } + + /** + * Sets the {@link #bestFitness}. + * + * @param bestFitness + * the new {@link #bestFitness} + */ + public void setBestFitness(double bestFitness) { + this.bestFitness = bestFitness; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(bestFitness); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + Arrays.hashCode(bestPosition); + result = prime * result + Arrays.hashCode(particles); + result = prime * result + ((random == null) ? 0 : random.hashCode()); + return result; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Swarm other = (Swarm) obj; + if (Double.doubleToLongBits(bestFitness) != Double.doubleToLongBits(other.bestFitness)) + return false; + if (!Arrays.equals(bestPosition, other.bestPosition)) + return false; + if (!Arrays.equals(particles, other.particles)) + return false; + if (random == null) { + if (other.random != null) + return false; + } else if (!random.equals(other.random)) + return false; + return true; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "Swarm [particles=" + Arrays.toString(particles) + ", bestPosition=" + Arrays.toString(bestPosition) + + ", bestFitness=" + bestFitness + ", random=" + random + "]"; + } + +} diff --git a/algorithms/src/test/java/com/baeldung/algorithms/multiswarm/MultiswarmUnitTest.java b/algorithms/src/test/java/com/baeldung/algorithms/multiswarm/MultiswarmUnitTest.java new file mode 100644 index 0000000000..f1c1609a9e --- /dev/null +++ b/algorithms/src/test/java/com/baeldung/algorithms/multiswarm/MultiswarmUnitTest.java @@ -0,0 +1,78 @@ +package com.baeldung.algorithms.multiswarm; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +import com.baeldung.algorithms.support.MayFailRule; + +/** + * Test for {@link Multiswarm}. + * + * @author Donato Rimenti + * + */ +public class MultiswarmUnitTest { + + /** + * Rule for handling expected failures. We use this since this test may + * actually fail due to bad luck in the random generation. + */ + @Rule + public MayFailRule mayFailRule = new MayFailRule(); + + /** + * Tests the multiswarm algorithm with a generic problem. + * + * The problem is the following: + * + * In League of Legends, a player's Effective Health when defending against + * physical damage is given by E=H(100+A)/100, where H is health and A is + * armor. + * + * Health costs 2.5 gold per unit, and Armor costs 18 gold per unit. You + * have 3600 gold, and you need to optimize the effectiveness E of your + * health and armor to survive as long as possible against the enemy team's + * attacks. How much of each should you buy? + * + * The solution is H = 1080, A = 50 for a total fitness of 1620. + * + * Tested with 50 swarms each with 1000 particles. + */ + @Test + public void givenMultiswarm_whenThousandIteration_thenSolutionFound() { + Multiswarm multiswarm = new Multiswarm(50, 1000, values -> { + + // No negatives values accepted. + if (values[0] < 0 && values[1] < 0) { + return -(values[0] * values[1]); + } else if (values[0] < 0) { + return values[0]; + } else if (values[1] < 0) { + return values[1]; + } + + // Checks if the solution is actually feasible provided our gold. + double cost = (values[0] * 2.5) + (values[1] * 18); + if (cost > 3600) { + return 3600 - cost; + } else { + // Check how good is the solution. + long fitness = (values[0] * (100 + values[1])) / 100; + return fitness; + } + }); + + // Iterates 1000 times through the main loop and prints the result. + for (int i = 0; i < 1000; i++) { + multiswarm.mainLoop(); + } + + System.out.println("Best fitness found: " + multiswarm.getBestFitness() + "[" + multiswarm.getBestPosition()[0] + + "," + multiswarm.getBestPosition()[1] + "]"); + Assert.assertEquals(1080, multiswarm.getBestPosition()[0]); + Assert.assertEquals(50, multiswarm.getBestPosition()[1]); + Assert.assertEquals(1620, (int) multiswarm.getBestFitness()); + } + +} diff --git a/algorithms/src/test/java/com/baeldung/algorithms/support/MayFailRule.java b/algorithms/src/test/java/com/baeldung/algorithms/support/MayFailRule.java new file mode 100644 index 0000000000..91df78ce4a --- /dev/null +++ b/algorithms/src/test/java/com/baeldung/algorithms/support/MayFailRule.java @@ -0,0 +1,38 @@ +package com.baeldung.algorithms.support; + +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * JUnit custom rule for managing tests that may fail due to heuristics or + * randomness. In order to use this, just instantiate this object as a public + * field inside the test class and annotate it with {@link Rule}. + * + * @author Donato Rimenti + * + */ +public class MayFailRule implements TestRule { + + /* + * (non-Javadoc) + * + * @see org.junit.rules.TestRule#apply(org.junit.runners.model.Statement, + * org.junit.runner.Description) + */ + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + base.evaluate(); + } catch (Throwable e) { + // Ignore the exception since we expect this. + } + } + }; + } + +}