/* Copyright 2009-2016 David Hadka
*
* This file is part of the MOEA Framework.
*
* The MOEA Framework is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* The MOEA Framework 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 Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the MOEA Framework. If not, see <http://www.gnu.org/licenses/>.
*/
package org.moeaframework.algorithm;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.moeaframework.core.FrameworkException;
import org.moeaframework.core.Population;
import org.moeaframework.core.Solution;
import org.moeaframework.util.Vector;
import org.moeaframework.util.weights.NormalBoundaryIntersectionGenerator;
/**
* A reference vector guided population, for use with RVEA, that truncates
* the population using the method outlined in [1].
* <p>
* References:
* <ol>
* <li>R. Cheng, Y. Jin, M. Olhofer, and B. Sendhoff. "A Reference Vector
* Guided Evolutionary Algorithm for Many-objective Optimization."
* IEEE Transactions on Evolutionary Computation, Issue 99, 2016.
* </ol>
*/
public class ReferenceVectorGuidedPopulation extends Population {
/**
* The name of the attribute for storing the normalized objectives.
*/
private static final String NORMALIZED_OBJECTIVES = "Normalized Objectives";
/**
* The number of objectives.
*/
private final int numberOfObjectives;
/**
* The number of outer divisions.
*/
private final int divisionsOuter;
/**
* The number of inner divisions, or {@code 0} if no inner divisions should
* be used.
*/
private final int divisionsInner;
/**
* The ideal point.
*/
double[] idealPoint;
/**
* The original, unnormalized reference vectors.
*/
private List<double[]> originalWeights;
/**
* The normalized reference vectors.
*/
List<double[]> weights;
/**
* The minimum angle between reference vectors.
*/
private double[] minAngles;
/**
* Scaling factor used in the angle-penalized distance function. This
* should be set to {@code currentGeneration / maxGenerations}.
*/
private double scalingFactor = 0.0;
/**
* Controls the rate of change in the angle-penalized distance function.
*/
private final double alpha;
/**
* Constructs a new population for RVEA.
*
* @param numberOfObjectives the number of objectives
* @param divisions the number of divisions
* @param alpha controls the rate of change in the angle-penalized distance
* function
*/
public ReferenceVectorGuidedPopulation(int numberOfObjectives,
int divisions, double alpha) {
super();
this.numberOfObjectives = numberOfObjectives;
this.divisionsOuter = divisions;
this.divisionsInner = 0;
this.alpha = alpha;
initialize();
}
/**
* Constructs a new population for RVEA.
*
* @param numberOfObjectives the number of objectives
* @param divisions the number of divisions
* @param alpha controls the rate of change in the angle-penalized distance
* function
* @param iterable the solutions used to initialize this population
*/
public ReferenceVectorGuidedPopulation(
int numberOfObjectives, int divisions, double alpha,
Iterable<? extends Solution> iterable) {
super(iterable);
this.numberOfObjectives = numberOfObjectives;
this.divisionsOuter = divisions;
this.divisionsInner = 0;
this.alpha = alpha;
initialize();
}
/**
* Constructs a new population for RVEA.
*
* @param numberOfObjectives the number of objectives
* @param divisionsOuter the number of outer divisions
* @param divisionsInner the number of inner divisions
* @param alpha controls the rate of change in the angle-penalized distance
* function
*/
public ReferenceVectorGuidedPopulation(int numberOfObjectives,
int divisionsOuter, int divisionsInner, double alpha) {
super();
this.numberOfObjectives = numberOfObjectives;
this.divisionsOuter = divisionsOuter;
this.divisionsInner = divisionsInner;
this.alpha = alpha;
initialize();
}
/**
* Constructs a new population for RVEA.
*
* @param numberOfObjectives the number of objectives
* @param divisionsOuter the number of outer divisions
* @param divisionsInner the number of inner divisions
* @param alpha controls the rate of change in the angle-penalized distance
* function
* @param iterable the solutions used to initialize this population
*/
public ReferenceVectorGuidedPopulation(
int numberOfObjectives, int divisionsOuter, int divisionsInner,
double alpha, Iterable<? extends Solution> iterable) {
super(iterable);
this.numberOfObjectives = numberOfObjectives;
this.divisionsOuter = divisionsOuter;
this.divisionsInner = divisionsInner;
this.alpha = alpha;
initialize();
}
/**
* Scaling factor used in the angle-penalized distance function. This
* should be set to {@code currentGeneration / maxGenerations}. Smaller
* values favor convergence while larger values favor diversity.
*
* @param scalingFactor the scaling factor, between 0 and 1
*/
public void setScalingFactor(double scalingFactor) {
if (scalingFactor < 0.0) {
scalingFactor = 0.0;
} else if (scalingFactor > 1.0) {
scalingFactor = 1.0;
}
this.scalingFactor = scalingFactor;
}
/**
* Normalize the reference vectors.
*/
public void adapt() {
// compute the minimum and maximum of the objectives
double[] zmin = new double[numberOfObjectives];
double[] zmax = new double[numberOfObjectives];
Arrays.fill(zmin, Double.POSITIVE_INFINITY);
Arrays.fill(zmax, Double.NEGATIVE_INFINITY);
for (Solution solution : this) {
for (int i = 0; i < numberOfObjectives; i++) {
zmin[i] = Math.min(zmin[i], solution.getObjective(i));
zmax[i] = Math.max(zmax[i], solution.getObjective(i));
}
}
// create the new normalized reference vectors
weights.clear();
for (double[] weight : originalWeights) {
double[] newWeight = weight.clone();
for (int i = 0; i < numberOfObjectives; i++) {
newWeight[i] *= Math.max(0.01, zmax[i] - zmin[i]);
}
weights.add(Vector.normalize(newWeight));
}
// compute the minimum angles between reference vectors
minAngles = new double[weights.size()];
for (int i = 0; i < weights.size(); i++) {
minAngles[i] = smallestAngleBetweenWeights(i);
}
}
/**
* Initializes the reference vectors and compute the minimum angles between
* the vectors
*/
private void initialize() {
// validate arguments
if (numberOfObjectives < 2) {
throw new FrameworkException("requires at least two objectives");
}
// create the reference vectors
originalWeights = new NormalBoundaryIntersectionGenerator(
numberOfObjectives, divisionsOuter, divisionsInner).generate();
for (int i = 0; i < originalWeights.size(); i++) {
originalWeights.set(i, Vector.normalize(originalWeights.get(i)));
}
// create a copy of the reference vectors (so the original reference
// vectors remain unchanged when we adapt)
weights = new ArrayList<double[]>();
for (double[] weight : originalWeights) {
weights.add(weight.clone());
}
// compute the minimum angles between reference vectors
minAngles = new double[weights.size()];
for (int i = 0; i < weights.size(); i++) {
minAngles[i] = smallestAngleBetweenWeights(i);
}
}
/**
* Compute the ideal point.
*/
protected void calculateIdealPoint() {
idealPoint = new double[numberOfObjectives];
Arrays.fill(idealPoint, Double.POSITIVE_INFINITY);
for (Solution solution : this) {
if (solution.getNumberOfObjectives() != numberOfObjectives) {
throw new FrameworkException("incorrect number of objectives");
}
for (int i = 0; i < numberOfObjectives; i++) {
idealPoint[i] = Math.min(idealPoint[i], solution.getObjective(i));
}
}
}
/**
* Offsets the solutions in this population by the ideal point. This
* method does not modify the objective values, it creates a new attribute
* with the name {@value NORMALIZED_OBJECTIVES}.
*/
protected void translateByIdealPoint() {
for (Solution solution : this) {
double[] objectives = solution.getObjectives();
for (int i = 0; i < numberOfObjectives; i++) {
objectives[i] -= idealPoint[i];
}
solution.setAttribute(NORMALIZED_OBJECTIVES, objectives);
}
}
/**
* Returns the cosine between the objective vector and a reference vector.
* This method assumes the line is a normalized weight vector; the point
* does not need to be normalized.
*
* @param line the line originating from the origin
* @param point the point
* @return the cosine
*/
protected static double cosine(double[] line, double[] point) {
return Vector.dot(point, line) / Vector.magnitude(point);
}
/**
* Returns the angle between the objective vector and a reference vector.
* This method assumes the line is a normalized weight vector; the point
* does not need to be normalized.
*
* @param line the line originating from the origin
* @param point the point
* @return the angle (acosine)
*/
protected static double acosine(double[] line, double[] point) {
return Math.acos(cosine(line, point));
}
/**
* Associates each solution to the nearest reference vector, returning a
* list-of-lists. The outer list maps to each reference vector using their
* index. The inner list is an unordered collection of the solutions
* associated with the reference point.
*
* @param population the population of solutions
* @return the association of solutions to reference points
*/
protected List<List<Solution>> associateToReferencePoint(Population population) {
List<List<Solution>> result = new ArrayList<List<Solution>>();
for (int i = 0; i < weights.size(); i++) {
result.add(new ArrayList<Solution>());
}
for (Solution solution : population) {
double[] objectives = (double[])solution.getAttribute(NORMALIZED_OBJECTIVES);
double maxDistance = Double.NEGATIVE_INFINITY;
int maxIndex = -1;
for (int i = 0; i < weights.size(); i++) {
double distance = cosine(weights.get(i), objectives);
if (distance > maxDistance) {
maxDistance = distance;
maxIndex = i;
}
}
// if there is only a single solution, then the normalized
// objectives will be 0 (since the ideal point == the solution);
// in this case, the solution could be associated with any
// reference vector
if (maxIndex < 0) {
maxIndex = 0;
}
result.get(maxIndex).add(solution);
}
return result;
}
/**
* Computes the smallest angle between the given reference vector and all
* remaining vectors.
*
* @param index the index of the reference vector
* @return the smallest angle between the given reference vector and all
* remaining vectors
*/
protected double smallestAngleBetweenWeights(int index) {
double smallestAngle = Double.POSITIVE_INFINITY;
for (int i = 0; i < weights.size(); i++) {
if (i != index) {
smallestAngle = Math.min(smallestAngle,
acosine(weights.get(index), weights.get(i)));
}
}
return smallestAngle;
}
/**
* Select the solution with the smallest penalized distance.
*
* @param solutions the solutions
* @param index the index of the reference vector
* @return the solution with the smallest penalized distance
*/
protected Solution select(List<Solution> solutions, int index) {
double[] weight = weights.get(index);
double minDistance = Double.POSITIVE_INFINITY;
Solution minSolution = null;
for (Solution solution : solutions) {
if (!solution.violatesConstraints()) {
double[] objectives = (double[])solution.getAttribute(
NORMALIZED_OBJECTIVES);
double penalty = numberOfObjectives *
Math.pow(scalingFactor, alpha) *
acosine(weight, objectives) / minAngles[index];
double tempDistance = Vector.magnitude(objectives) * (1.0 + penalty);
if (tempDistance < minDistance) {
minDistance = tempDistance;
minSolution = solution;
}
}
}
if (minSolution == null) {
// all solutions were infeasible, find one with smallest constraint
// violation
for (Solution solution : solutions) {
double tempDistance = 0.0;
for (int i = 0; i < solution.getNumberOfConstraints(); i++) {
tempDistance += Math.abs(solution.getConstraint(i));
}
if (tempDistance < minDistance) {
minDistance= tempDistance;
minSolution = solution;
}
}
}
return minSolution;
}
@Override
public void truncate(int size, Comparator<? super Solution> comparator) {
throw new UnsupportedOperationException("call truncate() instead");
}
/**
* Truncates the population so that only one solution is associated with
* each reference vector.
*/
public void truncate() {
// update the ideal point
calculateIdealPoint();
// translate objectives so the ideal point is at the origin
translateByIdealPoint();
// associate each solution to a reference vector
List<List<Solution>> members = associateToReferencePoint(this);
// elitist selection
clear();
for (int i = 0; i < members.size(); i++) {
List<Solution> associations = members.get(i);
if (associations.size() > 0) {
add(select(associations, i));
}
}
}
/**
* Returns the serializable state of this population.
*
* @return the serializable state of this population
*/
protected ReferenceVectorGuidedPopulationState getState() {
double[] idealPointClone = idealPoint == null ? null : idealPoint.clone();
List<double[]> originalWeightsClone = new ArrayList<double[]>();
List<double[]> weightsClone = new ArrayList<double[]>();
double[] minAnglesClone = minAngles == null ? null : minAngles.clone();
List<Solution> populationList = new ArrayList<Solution>();
for (double[] weight : originalWeights) {
originalWeightsClone.add(weight.clone());
}
for (double[] weight : weights) {
weightsClone.add(weight.clone());
}
for (Solution solution : this) {
populationList.add(solution);
}
return new ReferenceVectorGuidedPopulationState(populationList,
idealPointClone, originalWeightsClone, weightsClone,
minAnglesClone, scalingFactor);
}
/**
* Configures this population given the serializable state.
*
* @param state the serializable state
*/
protected void setState(ReferenceVectorGuidedPopulationState state) {
idealPoint = state.getIdealPoint() == null ? null : state.getIdealPoint().clone();
minAngles = state.getMinAngles() == null ? null : state.getMinAngles().clone();
scalingFactor = state.getScalingFactor();
originalWeights.clear();
weights.clear();
clear();
for (double[] weight : state.getOriginalWeights()) {
originalWeights.add(weight.clone());
}
for (double[] weight : state.getWeights()) {
weights.add(weight.clone());
}
addAll(state.getPopulation());
}
/**
* Proxy for serializing the state of this population. All data fields
* provided to or read from this proxy should be cloned.
*/
protected static class ReferenceVectorGuidedPopulationState implements Serializable {
private static final long serialVersionUID = 2967034665150122964L;
/**
* The population stored in a serializable list.
*/
private final List<Solution> population;
/**
* The ideal point.
*/
private final double[] idealPoint;
/**
* The original weight vectors.
*/
private final List<double[]> originalWeights;
/**
* The weight vectors.
*/
private final List<double[]> weights;
/**
* The minimum angles between weight vectors.
*/
private final double[] minAngles;
/**
* The scaling factor.
*/
private final double scalingFactor;
/**
* Creates a new proxy for serializing the state of this population.
*
* @param population the population stored in a serializable list
* @param idealPoint the ideal point
* @param originalWeights the original weight vectors
* @param weights the weight vectors
* @param minAngles the minimum angles between weight vectors
* @param scalingFactor the scaling factor
*/
public ReferenceVectorGuidedPopulationState(List<Solution> population,
double[] idealPoint, List<double[]> originalWeights,
List<double[]> weights, double[] minAngles,
double scalingFactor) {
super();
this.population = population;
this.idealPoint = idealPoint;
this.originalWeights = originalWeights;
this.weights = weights;
this.minAngles = minAngles;
this.scalingFactor = scalingFactor;
}
/**
* Returns the population stored in a serializable list.
*
* @return the population stored in a serializable list
*/
public List<Solution> getPopulation() {
return population;
}
/**
* Returns the ideal point.
*
* @return the ideal point
*/
public double[] getIdealPoint() {
return idealPoint;
}
/**
* Returns the original weight vectors.
*
* @return the original weight vectors
*/
public List<double[]> getOriginalWeights() {
return originalWeights;
}
/**
* Returns the weight vectors.
*
* @return the weight vectors
*/
public List<double[]> getWeights() {
return weights;
}
/**
* Returns the minimum angles between weight vectors.
*
* @return the minimum angles between weight vectors
*/
public double[] getMinAngles() {
return minAngles;
}
/**
* Returns the scaling factor.
*
* @return the scaling factor
*/
public double getScalingFactor() {
return scalingFactor;
}
}
}