/*
* This file is part of JGrasstools (http://www.jgrasstools.org)
* (C) HydroloGIS - www.hydrologis.com
*
* JGrasstools is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jgrasstools.gears.utils.optimizers.particleswarm;
import java.util.Arrays;
import java.util.Random;
import org.jgrasstools.gears.libs.exceptions.ModelsIllegalargumentException;
import org.jgrasstools.gears.utils.math.NumericsUtilities;
/**
* Particle swarm main engine.
*
* <p>http://www.borgelt.net/psopt.html
* <p>Biblio: http://ncra.ucd.ie/COMP30290/crc2006/Olapeju_Ayoola_03304281.pdf ?</p>
*
* @author Andrea Antonello (www.hydrologis.com)
*/
public class PSEngine {
private double accelerationFactorLocal;
private double accelerationFactorGlobal;
private double initDecelerationFactor;
private int maxIterations;
private double decayFactor;
private int particlesNum;
private Particle[] swarm;
private double globalBest;
private double[] globalBestLocations;
private IPSFunction function;
private int iterationStep;
private Random rand;
private double[][] ranges;
private String prefix;
/**
* Constructor.
*
* @param particlesNum number of particles involved.
* @param maxIterations maximum iterations.
* @param accelerationFactorLocal local acceleration factor for particles.
* @param accelerationFactorGlobal global acceleration factor for particles.
* @param initDecelerationFactor initial deceleration factor.
* @param decayFactor decay factor.
* @param function the fitting {@link IPSFunction function} to use.
* @param prefix TODO
*/
public PSEngine( int particlesNum, int maxIterations, double accelerationFactorLocal, double accelerationFactorGlobal,
double initDecelerationFactor, double decayFactor, IPSFunction function, String prefix ) {
this.particlesNum = particlesNum;
this.accelerationFactorLocal = accelerationFactorLocal;
this.accelerationFactorGlobal = accelerationFactorGlobal;
this.initDecelerationFactor = initDecelerationFactor;
this.decayFactor = decayFactor;
this.maxIterations = maxIterations;
this.function = function;
this.prefix = prefix;
}
/**
* Set ranges for the parameter space.
*
* <p>The order of the ranges needs to be the same
* that will be used be the particles and fitting function.
*
* @param ranges the [min, max] ranges to use.
*/
public void initializeRanges( double[]... ranges ) {
this.ranges = ranges;
}
/**
* Run the particle swarm engine.
* @throws Exception
*/
public void run() throws Exception {
if (ranges == null) {
throw new ModelsIllegalargumentException("No ranges have been defined for the parameter space.", this);
}
createSwarm();
double[] previous = null;
while( iterationStep <= maxIterations ) {
updateSwarm();
if (printStep()) {
System.out.println(prefix + " - ITER: " + iterationStep + " global best: " + globalBest + " - for positions: "
+ Arrays.toString(globalBestLocations));
}
if (function.hasConverged(globalBest, globalBestLocations, previous)) {
break;
}
previous = globalBestLocations.clone();
}
}
private boolean printStep() {
if (maxIterations > 10000) {
if (iterationStep % 1000 == 0) {
return true;
}
} else if (maxIterations > 1000) {
if (iterationStep % 100 == 0) {
return true;
}
} else {
return true;
}
return false;
}
/**
* Getter for the found solution.
*
* @return the solution.
*/
public double[] getSolution() {
return globalBestLocations.clone();
}
public double getSolutionFittingValue() {
return globalBest;
}
private void createSwarm() throws Exception {
rand = new Random();
iterationStep = 0;
globalBest = function.getInitialGlobalBest();
swarm = new Particle[particlesNum];
for( int j = 0; j < swarm.length; j++ ) {
swarm[j] = new Particle(ranges);
double[] currentLocations = swarm[j].getInitialLocations();
double evaluated = function.evaluate(iterationStep, j, currentLocations, ranges);
swarm[j].setParticleBestFunction(evaluated);
/* find globally best function value */
if (function.isBetter(evaluated, globalBest)) {
globalBest = evaluated;
if (globalBestLocations == null) {
globalBestLocations = new double[currentLocations.length];
}
for( int k = 0; k < currentLocations.length; k++ ) {
globalBestLocations[k] = currentLocations[k];
}
} else if (globalBestLocations == null) {
throw new RuntimeException("No evaluated value found better than the initial global best: " + evaluated + " vs. "
+ globalBest);
}
}
}
private void updateSwarm() throws Exception {
// System.out.println("UPDATE SWARM");
iterationStep++;
/*
* velocity decay factor:
*
* - it decreases when the iteration number increases
* - it decreases when the decay factor increases
*/
double w = initDecelerationFactor * Math.pow(iterationStep, -decayFactor);
// System.out.println("W = " + w);
/* traverse the particles */
for( int i = 0; i < swarm.length; i++ ) {
Particle particle = this.swarm[i];
double[] currentLocations = particle.update(w, accelerationFactorLocal, rand.nextDouble(), accelerationFactorGlobal,
rand.nextDouble(), globalBestLocations);
double evaluated;
if (currentLocations != null) {
evaluated = function.evaluate(iterationStep, i, currentLocations, ranges);
} else {
// parameters were outside, ignore and try next round with new position
continue;
}
/* update best local function value */
if (function.isBetter(evaluated, particle.getParticleBestFunction())) {
particle.setParticleBestFunction(evaluated);
particle.setParticleLocalBeststoCurrent();
}
/* update best global function value */
if (function.isBetter(evaluated, globalBest)) {
globalBest = evaluated;
for( int j = 0; j < currentLocations.length; j++ ) {
globalBestLocations[j] = currentLocations[j];
}
}
}
}
/**
* Checks if the parameters are in the ranges.
*
* @param parameters the params.
* @param ranges the ranges.
* @return <code>true</code>, if they are inside the given ranges.
*/
public static boolean parametersInRange( double[] parameters, double[]... ranges ) {
for( int i = 0; i < ranges.length; i++ ) {
if (!NumericsUtilities.isBetween(parameters[i], ranges[i])) {
return false;
}
}
return true;
}
}