/* Copyright 2006 by Sean Luke Licensed under the Academic Free License version 3.0 See the file "LICENSE" for more information */ package ec.multiobjective; import java.io.*; import ec.util.DecodeReturn; import ec.util.Parameter; import ec.util.Code; import ec.Fitness; import ec.EvolutionState; import java.util.*; import ec.*; /* * MultiObjectiveFitness.java * * Created: Tue Aug 10 20:27:38 1999 * By: Sean Luke */ /** * MultiObjectiveFitness is a subclass of Fitness which implements basic * multi-objective mechanisms suitable for being used with a variety of * multi-objective selection mechanisms, including ones using pareto-optimality. * *

* The object contains two items: an array of floating point values representing * the various multiple fitnesses, and a flag (maximize) indicating whether * higher is considered better. By default, isIdealFitness() always returns * false; you might want to override that, though it'd be unusual -- what is the * ideal fitness from the perspective of a pareto front? * *

* The object also contains maximum and minimum fitness values suggested for the * problem, on a per-objective basis. By default the maximum values are all 1.0 * and the minimum values are all 0.0, but you can change these. Note that * maximum does not mean "best" unless maximize is true. * *

The class also contains utility methods or computing pareto dominance, * Pareto Fronts and Pareto Front Ranks, and distance in multiobjective space. * The default comparison operators use Pareto Dominance, though this is often * overridden by subclasses. * *

The fitness() method returns the maximum of the fitness values, which is * clearly nonsensical: you should not be using this method. * *

Subclasses of this class may add certain auxiliary fitness measures which * are printed out by MultiObjectiveStatistics along with the multiple objectives. * To have these values printed out, override the getAuxiliaryFitnessNames() * and getAuxiliaryFitnessValues() methods. * *

* Parameters
* * * * * * * * *
base.num-objectives
* (else)multi.num-objectives
* int >= 1
(the number of fitnesses in the objectives array)
base.maximize
* bool = true (default) or false
(are higher values considered "better"?) *
* * * base.max
* float (1.0 default) * (maximum fitness value for all objectives) * * * base.max.i
* float (1.0 default) * (maximum fitness value for objective i. Overrides the * all-objective maximum fitness.) * * * base.min
* float (0.0 (default) * (minimum fitness value for all objectives) * * * base.min.i
* float = 0.0 (default) * (minimum fitness value for objective i. Overrides the * all-objective minimum fitness.) * *

* Default Base
* multi.fitness * * @author Sean Luke * @version 1.1 */ public class MultiObjectiveFitness extends Fitness { public static final String MULTI_FITNESS_POSTAMBLE = "["; public static final String FITNESS_POSTAMBLE = "]"; /** parameter for size of objectives */ public static final String P_NUMOBJECTIVES = "num-objectives"; /** parameter for max fitness values */ public static final String P_MAXOBJECTIVES = "max"; /** parameter for min fitness values */ public static final String P_MINOBJECTIVES = "min"; /** Is higher better? */ public static final String P_MAXIMIZE = "maximize"; /** Desired maximum fitness values. By default these are 1.0. Shared. */ public float[] maxObjective; /** Desired minimum fitness values. By default these are 0.0. Shared. */ public float[] minObjective; /** The various fitnesses. */ protected float[] objectives; // values range from 0 (worst) to 1 INCLUSIVE protected boolean maximize = true; /** Returns auxilliary fitness value names to be printed by the statistics object. By default, an empty array is returned, but various algorithms may override this to provide additional columns. */ public String[] getAuxilliaryFitnessNames() { return new String[] { }; } /** Returns auxilliary fitness values to be printed by the statistics object. By default, an empty array is returned, but various algorithms may override this to provide additional columns. */ public double[] getAuxilliaryFitnessValues() { return new double[] { }; } public boolean isMaximizing() { return maximize; } public int getNumObjectives() { return objectives.length; } /** * Returns the objectives as an array. Note that this is the *actual array*. * Though you could set values in this array, you should NOT do this -- * rather, set them using setObjectives(). */ public float[] getObjectives() { return objectives; } public float getObjective(int i) { return objectives[i]; } public void setObjectives(final EvolutionState state, float[] newObjectives) { if (newObjectives == null) { state.output.fatal("Null objective array provided to MultiObjectiveFitness."); } if (newObjectives.length != objectives.length) { state.output.fatal("New objective array length does not match current length."); } for (int i = 0; i < newObjectives.length; i++) { float _f = newObjectives[i]; if (_f == Float.POSITIVE_INFINITY || _f == Float.NEGATIVE_INFINITY || Float.isNaN(_f)) { state.output.warning("Bad objective #" + i + ": " + _f + ", setting to worst value for that objective."); if (maximize) newObjectives[i] = minObjective[i]; else newObjectives[i] = maxObjective[i]; } } objectives = newObjectives; } public Parameter defaultBase() { return MultiObjectiveDefaults.base().push(P_FITNESS); } public Object clone() { MultiObjectiveFitness f = (MultiObjectiveFitness) (super.clone()); f.objectives = (float[]) (objectives.clone()); // cloning an array // note that we do NOT clone max and min fitness -- they're shared return f; } /** * Returns the Max() of objectives, which adheres to Fitness.java's protocol * for this method. Though you should not rely on a selection or statistics * method which requires this. */ public float fitness() { float fit = objectives[0]; for (int x = 1; x < objectives.length; x++) if (fit < objectives[x]) fit = objectives[x]; return fit; } /** * Sets up. This must be called at least once in the prototype before * instantiating any fitnesses that will actually be used in evolution. */ public void setup(EvolutionState state, Parameter base) { super.setup(state, base); // unnecessary really Parameter def = defaultBase(); int numFitnesses; numFitnesses = state.parameters.getInt(base.push(P_NUMOBJECTIVES), def.push(P_NUMOBJECTIVES), 0); if (numFitnesses <= 0) state.output.fatal("The number of objectives must be an integer >= 1.", base.push(P_NUMOBJECTIVES), def.push(P_NUMOBJECTIVES)); maximize = state.parameters.getBoolean(base.push(P_MAXIMIZE), def.push(P_MAXIMIZE), true); objectives = new float[numFitnesses]; maxObjective = new float[numFitnesses]; minObjective = new float[numFitnesses]; for (int i = 0; i < numFitnesses; i++) { // load default globals minObjective[i] = state.parameters.getFloatWithDefault(base.push(P_MINOBJECTIVES), def.push(P_MINOBJECTIVES), 0.0f); maxObjective[i] = state.parameters.getFloatWithDefault(base.push(P_MAXOBJECTIVES), def.push(P_MAXOBJECTIVES), 1.0f); // load specifics if any minObjective[i] = state.parameters.getFloatWithDefault(base.push(P_MINOBJECTIVES).push("" + i), def.push(P_MINOBJECTIVES).push("" + i), minObjective[i]); maxObjective[i] = state.parameters.getFloatWithDefault(base.push(P_MAXOBJECTIVES).push("" + i), def.push(P_MAXOBJECTIVES).push("" + i), maxObjective[i]); // test for validity if (minObjective[i] >= maxObjective[i]) state.output.error("For objective " + i + "the min fitness must be strictly less than the max fitness."); } state.output.exitIfErrors(); } /** * Returns true if this fitness is the "ideal" fitness. Default always * returns false. You may want to override this. */ public boolean isIdealFitness() { return false; } /** * Returns true if I'm equivalent in fitness (neither better nor worse) to * _fitness. The rule I'm using is this: If one of us is better in one or * more criteria, and we are equal in the others, then equivalentTo is * false. If each of us is better in one or more criteria each, or we are * equal in all criteria, then equivalentTo is true. Multiobjective optimization algorithms may * choose to override this to do something else. */ public boolean equivalentTo(Fitness _fitness) { MultiObjectiveFitness other = (MultiObjectiveFitness) _fitness; boolean abeatsb = false; boolean bbeatsa = false; if (maximize != other.maximize) throw new RuntimeException( "Attempt made to compare two multiobjective fitnesses; but one expects higher values to be better and the other expectes lower values to be better."); if (objectives.length != other.objectives.length) throw new RuntimeException("Attempt made to compare two multiobjective fitnesses; but they have different numbers of objectives."); if (maximize) { for (int x = 0; x < objectives.length; x++) { if (objectives[x] > other.objectives[x]) abeatsb = true; if (objectives[x] < other.objectives[x]) bbeatsa = true; if (abeatsb && bbeatsa) return true; } } else // lower is better { for (int x = 0; x < objectives.length; x++) { if (objectives[x] < other.objectives[x]) abeatsb = true; if (objectives[x] > other.objectives[x]) bbeatsa = true; if (abeatsb && bbeatsa) return true; } } if (abeatsb || bbeatsa) return false; return true; } /** * Returns true if I'm better than _fitness. The DEFAULT rule I'm using is this: if * I am better in one or more criteria, and we are equal in the others, then * betterThan is true, else it is false. Multiobjective optimization algorithms may * choose to override this to do something else. */ public boolean betterThan(Fitness fitness) { return paretoDominates((MultiObjectiveFitness)fitness); } /** * Returns true if I'm better than _fitness. The rule I'm using is this: if * I am better in one or more criteria, and we are equal in the others, then * betterThan is true, else it is false. */ public boolean paretoDominates(MultiObjectiveFitness other) { boolean abeatsb = false; if (maximize != other.maximize) throw new RuntimeException( "Attempt made to compare two multiobjective fitnesses; but one expects higher values to be better and the other expectes lower values to be better."); if (objectives.length != other.objectives.length) throw new RuntimeException("Attempt made to compare two multiobjective fitnesses; but they have different numbers of objectives."); if (maximize) { for (int x = 0; x < objectives.length; x++) { if (objectives[x] > other.objectives[x]) abeatsb = true; else if (objectives[x] < other.objectives[x]) return false; } } else { for (int x = 0; x < objectives.length; x++) { if (objectives[x] < other.objectives[x]) abeatsb = true; else if (objectives[x] > other.objectives[x]) return false; } } return abeatsb; } // Remove an individual from the ArrayList, shifting the topmost // individual in his place static void yank(int val, ArrayList list) { int size = list.size(); list.set(val, list.get(size - 1)); list.remove(size - 1); } /** * Divides an array of Individuals into the Pareto front and the "nonFront" (everyone else). * The Pareto front is returned. You may provide ArrayLists for the front and a nonFront. * If you provide null for the front, an ArrayList will be created for you. If you provide * null for the nonFront, non-front individuals will not be added to it. This algorithm is * O(n^2). */ public static ArrayList partitionIntoParetoFront(Individual[] inds, ArrayList front, ArrayList nonFront) { if (front == null) front = new ArrayList(); // put the first guy in the front front.add(inds[0]); // iterate over all the remaining individuals for (int i = 1; i < inds.length; i++) { Individual ind = (Individual) (inds[i]); boolean noOneWasBetter = true; int frontSize = front.size(); // iterate over the entire front for (int j = 0; j < frontSize; j++) { Individual frontmember = (Individual) (front.get(j)); // if the front member is better than the individual, dump the individual and go to the next one if (((MultiObjectiveFitness) (frontmember.fitness)).paretoDominates((MultiObjectiveFitness) (ind.fitness))) { if (nonFront != null) nonFront.add(ind); noOneWasBetter = false; break; // failed. He's not in the front } // if the individual was better than the front member, dump the front member. But look over the // other front members (don't break) because others might be dominated by the individual as well. else if (((MultiObjectiveFitness) (ind.fitness)).paretoDominates((MultiObjectiveFitness) (frontmember.fitness))) { yank(j, front); // a front member is dominated by the new individual. Replace him frontSize--; // member got removed j--; // because there's another guy we now need to consider in his place if (nonFront != null) nonFront.add(frontmember); } } if (noOneWasBetter) front.add(ind); } return front; } /** Divides inds into pareto front ranks (each an ArrayList), and returns them, in order, stored in an ArrayList. */ public static ArrayList partitionIntoRanks(Individual[] inds) { Individual[] dummy = new Individual[0]; ArrayList frontsByRank = new ArrayList(); while(inds.length > 0) { ArrayList front = new ArrayList(); ArrayList nonFront = new ArrayList(); MultiObjectiveFitness.partitionIntoParetoFront(inds, front, nonFront); // build inds out of remainder inds = (Individual[]) nonFront.toArray(dummy); frontsByRank.add(front); } return frontsByRank; } /** * Returns the sum of the squared difference between two Fitnesses in Objective space. */ public double sumSquaredObjectiveDistance(MultiObjectiveFitness other) { double s = 0; for (int i = 0; i < objectives.length; i++) { double a = (objectives[i] - other.objectives[i]); s += a * a; } return s; } /** * Returns the Manhattan difference between two Fitnesses in Objective space. */ public double manhattanObjectiveDistance(MultiObjectiveFitness other) { double s = 0; for (int i = 0; i < objectives.length; i++) { s += Math.abs(objectives[i] - other.objectives[i]); } return s; } public String fitnessToString() { String s = FITNESS_PREAMBLE + MULTI_FITNESS_POSTAMBLE; for (int x = 0; x < objectives.length; x++) { if (x > 0) s = s + " "; s = s + Code.encode(objectives[x]); } s = s + " "; s = s + Code.encode(maximize); return s + FITNESS_POSTAMBLE; } public String fitnessToStringForHumans() { String s = FITNESS_PREAMBLE + MULTI_FITNESS_POSTAMBLE; for (int x = 0; x < objectives.length; x++) { if (x > 0) s = s + " "; s = s + objectives[x]; } s = s + " "; s = s + (maximize ? "max" : "min"); return s + FITNESS_POSTAMBLE; } public void readFitness(final EvolutionState state, final LineNumberReader reader) throws IOException { DecodeReturn d = Code.checkPreamble(FITNESS_PREAMBLE + MULTI_FITNESS_POSTAMBLE, state, reader); for (int x = 0; x < objectives.length; x++) { Code.decode(d); if (d.type != DecodeReturn.T_FLOAT) state.output.fatal("Reading Line " + d.lineNumber + ": " + "Bad Fitness (objectives value #" + x + ")."); objectives[x] = (float) d.d; } Code.decode(d); if (d.type != DecodeReturn.T_BOOLEAN) state.output.fatal("Reading Line " + d.lineNumber + ": " + "Information missing about whether higher is better"); maximize = (boolean) (d.l != 0); } public void writeFitness(final EvolutionState state, final DataOutput dataOutput) throws IOException { dataOutput.writeInt(objectives.length); for (int x = 0; x < objectives.length; x++) dataOutput.writeFloat(objectives[x]); dataOutput.writeBoolean(maximize); } public void readFitness(final EvolutionState state, final DataInput dataInput) throws IOException { int len = dataInput.readInt(); if (objectives == null || objectives.length != len) objectives = new float[len]; for (int x = 0; x < objectives.length; x++) objectives[x] = dataInput.readFloat(); maximize = dataInput.readBoolean(); } }