/* Copyright 2009-2015 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.analysis.tools;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import org.moeaframework.algorithm.PeriodicAction;
import org.moeaframework.analysis.sensitivity.ResultEntry;
import org.moeaframework.analysis.sensitivity.ResultFileWriter;
import org.moeaframework.core.Algorithm;
import org.moeaframework.core.FrameworkException;
import org.moeaframework.core.NondominatedPopulation;
import org.moeaframework.core.PRNG;
import org.moeaframework.core.Problem;
import org.moeaframework.core.Solution;
import org.moeaframework.core.Variable;
import org.moeaframework.core.operator.RandomInitialization;
import org.moeaframework.core.spi.AlgorithmFactory;
import org.moeaframework.core.spi.ProblemFactory;
import org.moeaframework.core.variable.EncodingUtils;
import org.moeaframework.problem.ExternalProblem;
import org.moeaframework.util.CommandLineUtility;
import org.moeaframework.util.io.FileUtils;
/**
* Command line utility for solving an optimization problem using any of the
* supported optimization algorithms. This utility supports solving problems
* defined within the MOEA Framework as well as compatible external problems.
* See {@link ExternalProblem} for details on developing an external problem.
*/
public class Solve extends CommandLineUtility {
/**
* Constructs the command line utility for solving an optimization problem.
*/
public Solve() {
super();
}
@SuppressWarnings("static-access")
@Override
public Options getOptions() {
Options options = super.getOptions();
options.addOption(OptionBuilder
.withLongOpt("output")
.hasArg()
.withArgName("file")
.isRequired()
.create('f'));
options.addOption(OptionBuilder
.withLongOpt("problem")
.hasArg()
.withArgName("name")
.create('b'));
options.addOption(OptionBuilder
.withLongOpt("algorithm")
.hasArg()
.withArgName("name")
.isRequired()
.create('a'));
options.addOption(OptionBuilder
.withLongOpt("properties")
.hasArgs()
.withArgName("p1=v1;p2=v2;...")
.withValueSeparator(';')
.create('x'));
options.addOption(OptionBuilder
.withLongOpt("seed")
.hasArg()
.withArgName("value")
.create('s'));
options.addOption(OptionBuilder
.withLongOpt("epsilon")
.hasArg()
.withArgName("e1,e2,...")
.create('e'));
options.addOption(OptionBuilder
.withLongOpt("numberOfEvaluations")
.hasArg()
.withArgName("value")
.isRequired()
.create('n'));
options.addOption(OptionBuilder
.withLongOpt("runtimeFrequency")
.hasArg()
.withArgName("value")
.create('F'));
options.addOption(OptionBuilder
.withLongOpt("variables")
.hasArg()
.withArgName("v1,v2,...")
.create('v'));
options.addOption(OptionBuilder
.withLongOpt("objectives")
.hasArg()
.withArgName("value")
.create('o'));
options.addOption(OptionBuilder
.withLongOpt("constraints")
.hasArg()
.withArgName("value")
.create('c'));
options.addOption(OptionBuilder
.withLongOpt("lowerBounds")
.hasArg()
.withArgName("v1,v2,...")
.create("l"));
options.addOption(OptionBuilder
.withLongOpt("upperBounds")
.hasArg()
.withArgName("v1,v2,...")
.create('u'));
options.addOption(OptionBuilder
.withLongOpt("useSocket")
.create('S'));
options.addOption(OptionBuilder
.withLongOpt("hostname")
.hasArg()
.withArgName("value")
.create('H'));
options.addOption(OptionBuilder
.withLongOpt("port")
.hasArg()
.withArgName("value")
.create('P'));
options.addOption(OptionBuilder
.withLongOpt("startupDelay")
.hasArg()
.withArgName("value")
.create('D'));
options.addOption(OptionBuilder
.withLongOpt("test")
.hasOptionalArg()
.withArgName("trials")
.create('t'));
return options;
}
/**
* Parses a single variable specification from the command line. This
* method is case sensitive.
*
* @param token the variable specification from the command line
* @return the generated variable object
* @throws ParseException if an error occurred while parsing the variable
* specification
*/
private Variable parseVariableSpecification(String token)
throws ParseException {
if (!token.endsWith(")")) {
throw new ParseException("invalid variable specification '" +
token + "', not properly formatted");
}
if (token.startsWith("R(")) {
// real-valued decision variable
String content = token.substring(2, token.length()-1);
int index = content.indexOf(';');
if (index >= 0) {
double lowerBound = Double.parseDouble(
content.substring(0, index));
double upperBound = Double.parseDouble(
content.substring(index+1, content.length()));
return EncodingUtils.newReal(lowerBound, upperBound);
} else {
throw new ParseException("invalid real specification '" +
token + "', expected R(<lb>,<ub>)");
}
} else if (token.startsWith("B(")) {
String content = token.substring(2, token.length()-1);
try {
int length = Integer.parseInt(content.trim());
return EncodingUtils.newBinary(length);
} catch (NumberFormatException e) {
throw new ParseException("invalid binary specification '" +
token + "', expected B(<length>)");
}
} else if (token.startsWith("P(")) {
String content = token.substring(2, token.length()-1);
try {
int length = Integer.parseInt(content.trim());
return EncodingUtils.newPermutation(length);
} catch (NumberFormatException e) {
throw new ParseException("invalid permutation specification '"
+ token + "', expected P(<length>)");
}
} else {
throw new ParseException("invalid variable specification '"
+ token + "', unknown type");
}
}
/**
* Parses the decision variable specification either from the
* {@code --lowerBounds} and {@code --upperBounds} options or the
* {@code --variables} option.
*
* @param commandLine the command line arguments
* @return the parsed variable specifications
* @throws ParseException if an error occurred while parsing the variable
* specifications
*/
private List<Variable> parseVariables(CommandLine commandLine)
throws ParseException {
List<Variable> variables = new ArrayList<Variable>();
if (commandLine.hasOption("lowerBounds") &&
commandLine.hasOption("upperBounds")) {
String[] lowerBoundTokens =
commandLine.getOptionValue("lowerBounds").split(",");
String[] upperBoundTokens =
commandLine.getOptionValue("upperBounds").split(",");
if (lowerBoundTokens.length != upperBoundTokens.length) {
throw new ParseException("lower bound and upper bounds not " +
"the same length");
}
for (int i = 0; i < lowerBoundTokens.length; i++) {
double lowerBound = Double.parseDouble(lowerBoundTokens[i]);
double upperBound = Double.parseDouble(upperBoundTokens[i]);
variables.add(EncodingUtils.newReal(lowerBound, upperBound));
}
} else if (commandLine.hasOption("variables")) {
String[] tokens =
commandLine.getOptionValue("variables").split(",");
for (String token : tokens) {
variables.add(parseVariableSpecification(
token.trim().toUpperCase()));
}
} else {
throw new ParseException("must specify either the problem, the " +
"variables, or the lower and upper bounds arguments");
}
return variables;
}
/**
* Creates an external problem using the information provided on the
* command line.
*
* @param commandLine the command line arguments
* @return the external problem
* @throws ParseException if an error occurred parsing any of the command
* line options
* @throws IOException if an error occurred starting the external program
*/
private Problem createExternalProblem(final CommandLine commandLine)
throws ParseException, IOException {
final int numberOfObjectives = Integer.parseInt(
commandLine.getOptionValue("objectives"));
final int numberOfConstraints = commandLine.hasOption("constraints") ?
Integer.parseInt(commandLine.getOptionValue("constraints")) :
0;
final List<Variable> variables = parseVariables(commandLine);
if (commandLine.hasOption("useSocket")) {
String hostname = null; // default to localhost
int port = 16801;
int delay = 1;
if (commandLine.hasOption("hostname")) {
hostname = commandLine.getOptionValue("hostname");
}
if (commandLine.hasOption("port")) {
port = Integer.parseInt(commandLine.getOptionValue("port"));
}
if (commandLine.hasOption("startupDelay")) {
delay = Integer.parseInt(
commandLine.getOptionValue("startupDelay"));
}
if (commandLine.getArgs().length > 0) {
// the command to run is specified on the command line
System.out.print("Running ");
System.out.println(StringUtils.join(commandLine.getArgs()));
new ProcessBuilder(commandLine.getArgs()).start();
}
try {
System.out.print("Sleeping for ");
System.out.print(delay);
System.out.println(" seconds");
Thread.sleep(delay*1000);
} catch (InterruptedException e) {
// do nothing
}
System.out.println("Starting optimization");
return new ExternalProblem(hostname, port) {
@Override
public String getName() {
return StringUtils.join(commandLine.getArgs());
}
@Override
public int getNumberOfVariables() {
return variables.size();
}
@Override
public int getNumberOfObjectives() {
return numberOfObjectives;
}
@Override
public int getNumberOfConstraints() {
return numberOfConstraints;
}
@Override
public Solution newSolution() {
Solution solution = new Solution(variables.size(),
numberOfObjectives, numberOfConstraints);
for (int i = 0; i < variables.size(); i++) {
solution.setVariable(i, variables.get(i).copy());
}
return solution;
}
};
} else {
return new ExternalProblem(commandLine.getArgs()) {
@Override
public String getName() {
return StringUtils.join(commandLine.getArgs());
}
@Override
public int getNumberOfVariables() {
return variables.size();
}
@Override
public int getNumberOfObjectives() {
return numberOfObjectives;
}
@Override
public int getNumberOfConstraints() {
return numberOfConstraints;
}
@Override
public Solution newSolution() {
Solution solution = new Solution(variables.size(),
numberOfObjectives, numberOfConstraints);
for (int i = 0; i < variables.size(); i++) {
solution.setVariable(i, variables.get(i).copy());
}
return solution;
}
};
}
}
/**
* Runs a number of trials as a way to quickly test if the connection
* between this solver and the problem is functional.
*
* @param problem the problem
* @param commandLine the command line arguments
*/
private void runTests(Problem problem, CommandLine commandLine) {
int trials = 5;
if (commandLine.getOptionValue("test") != null) {
trials = Integer.parseInt(commandLine.getOptionValue("test"));
}
try {
int count = 0;
RandomInitialization initialization = new RandomInitialization(
problem, trials);
Solution[] solutions = initialization.initialize();
for (Solution solution : solutions) {
System.out.println("Running test " + (++count) + ":");
for (int j = 0; j < solution.getNumberOfVariables(); j++) {
System.out.print(" Variable ");
System.out.print(j+1);
System.out.print(" = ");
System.out.println(solution.getVariable(j));
}
System.out.println(" * Evaluating solution *");
problem.evaluate(solution);
System.out.println(" * Evaluation complete *");
for (int j = 0; j < solution.getNumberOfObjectives(); j++) {
System.out.print(" Objective ");
System.out.print(j+1);
System.out.print(" = ");
System.out.println(solution.getObjective(j));
}
for (int j = 0; j < solution.getNumberOfConstraints(); j++) {
System.out.print(" Constraint ");
System.out.print(j+1);
System.out.print(" = ");
System.out.println(solution.getConstraint(j));
}
if ((solution.getNumberOfConstraints() > 0) &&
solution.violatesConstraints()) {
System.out.println(" Solution is infeasible (non-zero " +
"constraint value)!");
}
}
System.out.println("Test succeeded!");
} catch (Exception e) {
e.printStackTrace();
System.out.println("Test failed! Please see the error message " +
"above for details.");
}
}
@Override
public void run(CommandLine commandLine) throws IOException {
// parse the algorithm parameters
Properties properties = new Properties();
if (commandLine.hasOption("properties")) {
for (String property : commandLine.getOptionValues("properties")) {
String[] tokens = property.split("=");
if (tokens.length == 2) {
properties.setProperty(tokens[0], tokens[1]);
} else {
throw new FrameworkException("malformed property argument");
}
}
}
if (commandLine.hasOption("epsilon")) {
properties.setProperty("epsilon",
commandLine.getOptionValue("epsilon"));
}
int maxEvaluations = Integer.parseInt(
commandLine.getOptionValue("numberOfEvaluations"));
// seed the pseudo-random number generator
if (commandLine.hasOption("seed")) {
PRNG.setSeed(Long.parseLong(commandLine.getOptionValue("seed")));
}
// parse the runtime frequency
int runtimeFrequency = 100;
if (commandLine.hasOption("runtimeFrequency")) {
runtimeFrequency = Integer.parseInt(
commandLine.getOptionValue("runtimeFrequency"));
}
// open the resources and begin processing
Problem problem = null;
Algorithm algorithm = null;
ResultFileWriter writer = null;
File file = new File(commandLine.getOptionValue("output"));
try {
if (commandLine.hasOption("problem")) {
problem = ProblemFactory.getInstance().getProblem(
commandLine.getOptionValue("problem"));
} else {
problem = createExternalProblem(commandLine);
}
if (commandLine.hasOption("test")) {
runTests(problem, commandLine);
return;
}
try {
algorithm = AlgorithmFactory.getInstance().getAlgorithm(
commandLine.getOptionValue("algorithm"),
properties,
problem);
// if the output file exists, delete first to avoid appending
FileUtils.delete(file);
try {
writer = new ResultFileWriter(problem, file);
algorithm = new RuntimeCollector(algorithm,
runtimeFrequency, writer);
while (!algorithm.isTerminated() &&
(algorithm.getNumberOfEvaluations() < maxEvaluations)) {
algorithm.step();
}
} finally {
if (writer != null) {
writer.close();
}
}
} finally {
if (algorithm != null) {
algorithm.terminate();
}
}
} catch (ParseException e) {
throw new IOException(e);
} finally {
if (problem != null) {
problem.close();
}
}
}
/**
* Wraps an algorithm to write the approximation set and periodic intervals.
*/
private static class RuntimeCollector extends PeriodicAction {
/**
* The result file writer where the runtime information is stored.
*/
private final ResultFileWriter writer;
/**
* The time, in nanoseconds, this collector was created. This roughly
* corresponds to the time the algorithm starts, assuming that the
* algorithm is run immediately following its setup.
*/
private final long startTime;
/**
* Constructs a new wrapper to collect runtime dynamics.
*
* @param algorithm the wrapped algorithm
* @param frequency the frequency at which the runtime snapshots are
* recorded
* @param writer the result file writer where the runtime information
* is stored
*/
public RuntimeCollector(Algorithm algorithm, int frequency,
ResultFileWriter writer) {
super(algorithm, frequency, FrequencyType.EVALUATIONS);
this.writer = writer;
startTime = System.nanoTime();
}
@Override
public void doAction() {
double elapsedTime = (System.nanoTime() - startTime) * 1e-9;
NondominatedPopulation result = algorithm.getResult();
Properties properties = new Properties();
properties.setProperty("NFE",
Integer.toString(algorithm.getNumberOfEvaluations()));
properties.setProperty("ElapsedTime",
Double.toString(elapsedTime));
try {
writer.append(new ResultEntry(result, properties));
} catch (IOException e) {
throw new FrameworkException(e);
}
}
}
/**
* Starts the command line utility for solving an optimization problem.
*
* @param args the command line arguments
* @throws Exception if an error occurred
*/
public static void main(String[] args) throws Exception {
new Solve().start(args);
}
}