/* 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.problem;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import org.moeaframework.core.Problem;
import org.moeaframework.core.Solution;
import org.moeaframework.core.Variable;
import org.moeaframework.core.variable.BinaryVariable;
import org.moeaframework.core.variable.Permutation;
import org.moeaframework.core.variable.RealVariable;
import org.moeaframework.util.io.RedirectStream;
/**
* Evaluate solutions using an externally-defined problem. Two modes of
* operation are supported: standard I/O and sockets.
*
* <h4>Standard I/O Mode</h4>
* Standard I/O is the easiest mode to setup and run. First, an executable
* program on the computer is launched by invoking the constructor with the
* program name (and any optional arguments):
* <pre>
* new ExternalProblem("./problem.exe", "arg1", "arg2") { ... }
* </pre>
* Then, solutions are sent to the process on its standard input (stdin) stream,
* and the objectives and constraints are read from its standard output (stdout)
* stream.
* <p>
* The program can not use the standard I/O for any other purpose. Programs
* which read from or write to the standard I/O streams should instead use
* sockets, as discussed below.
*
* <h4>Socket Mode</h4>
* Socket mode is more complicated to setup, but is more flexible and robust.
* It has the ability to not only evaluate the problem on the host computer,
* but can be spread across a computer network. To use sockets, use either the
* {@link #ExternalProblem(String, int)} or
* {@link #ExternalProblem(InetAddress, int)} constructor.
*
* <h4>C/C++ Interface</h4>
* A C/C++ interface is provided for implementing problems. This interface
* supports both modes of communication, depending on which initialization
* routine is invoked. See the {@code moeaframework.c} and
* {@code moeaframework.h} files in the {@code examples/} folder for details.
* This interface conforms to the communication protocol described below.
* <p>
* The communication protocol consists of sending decision variables to the
* external process, and the process responding with the objectives and
* constraints. The decision variables line consists of one or more variables
* separated by whitespace and terminated by a newline. The process evaluates
* the problem for the given variables and outputs the objectives separated by
* whitespace and terminated by a newline. If the problem also has constraints,
* each constraint is returned after the objectives on the same line. The
* process must only terminate when the end of stream is reached. In addition,
* the process should flush the output stream to ensure the output is processed
* immediately.
* <p>
* Whitespace is one or more spaces, tabs or any combination thereof. The
* newline is either the line feed ('\n'), carriage return ('\r') or a carriage
* return followed immediately by a line feed ("\r\n").
* <p>
* <b>It is critical that the {@link #close()} method be invoked to ensure the
* external process is shutdown cleanly.</b>
*/
public abstract class ExternalProblem implements Problem {
/**
* The default port used by the MOEA Framework to connect to remote
* evaluation processes via sockets.
*/
public static final int DEFAULT_PORT = 16801;
/**
* Reader connected to the process' standard output.
*/
private final BufferedReader reader;
/**
* Writer connected to the process' standard input.
*/
private final BufferedWriter writer;
/**
* Constructs an external problem using {@code new
* ProcessBuilder(command).start()}. If the command contains arguments,
* the arguments should be passed in as separate strings, such as
* <pre>
* new ExternalProblem("command", "arg1", "arg2");
* </pre>
*
* @param command a specified system command
* @throws IOException if an I/O error occured
*/
public ExternalProblem(String... command) throws IOException {
this(new ProcessBuilder(command).start());
}
/**
* Constructs an external problem that connects to a remote process via
* sockets. The remote process should be instantiated and already
* listening to the designated port number prior to invoking this
* constructor.
*
* @param host the host name of the remote system; or {@code null} to use
* the local host
* @param port the port number
* @throws UnknownHostException if the IP address of the specified host
* could not be determined
* @throws IOException if an I/O error occurred
*/
public ExternalProblem(String host, int port) throws IOException,
UnknownHostException {
this(new Socket(host, port));
}
/**
* Constructs an external problem that connects to a remote process via
* sockets. The remote process should be instantiated and already
* listening to the designated port number prior to invoking this
* constructor.
*
* @param address the IP address of the remote system
* @param port the port number
* @throws IOException if an I/O error occurred
*/
public ExternalProblem(InetAddress address, int port) throws IOException {
this(new Socket(address, port));
}
/**
* Constructs an external problem using the specified socket.
*
* @param socket the socket used to send solutions to be evaluated
* @throws IOException if an I/O error occurred
*/
ExternalProblem(Socket socket) throws IOException {
this(socket.getInputStream(), socket.getOutputStream());
}
/**
* Constructs an external problem using the specified process.
*
* @param process the process used to evaluate solutions
*/
ExternalProblem(Process process) {
this(process.getInputStream(), process.getOutputStream());
RedirectStream.redirect(process.getErrorStream(), System.err);
}
/**
* Constructs an external problem using the specified input and output
* streams.
*
* @param input the input stream
* @param output the output stream
*/
ExternalProblem(InputStream input, OutputStream output) {
super();
reader = new BufferedReader(new InputStreamReader(input));
writer = new BufferedWriter(new OutputStreamWriter(output));
}
/**
* Closes the connection to the process. No further invocations of
* {@code evaluate} are permitted.
*/
@Override
public synchronized void close() {
try {
writer.close();
} catch (IOException e) {
throw new ProblemException(this, e);
} finally {
try {
reader.close();
} catch (IOException e) {
throw new ProblemException(this, e);
}
}
}
/**
* Evaluates the specified solution using the process defined by this class'
* constructor.
*
* @param solution the solution to evaluate
*/
@Override
public synchronized void evaluate(Solution solution)
throws ProblemException {
// send variables to external process
try {
writer.write(encode(solution.getVariable(0)));
for (int i = 1; i < solution.getNumberOfVariables(); i++) {
writer.write(" ");
writer.write(encode(solution.getVariable(i)));
}
writer.newLine();
writer.flush();
} catch (IOException e) {
throw new ProblemException(this, "error sending variables to external process", e);
}
// receive objectives from external process
try {
String line = reader.readLine();
if (line == null) {
throw new ProblemException(this, "end of stream reached when response expected");
}
String[] tokens = line.split("\\s+");
if (tokens.length != (solution.getNumberOfObjectives() +
solution.getNumberOfConstraints())) {
throw new ProblemException(this, "response contained fewer tokens than expected");
}
int index = 0;
for (int i = 0; i < solution.getNumberOfObjectives(); i++) {
solution.setObjective(i, Double.parseDouble(tokens[index]));
index++;
}
for (int i = 0; i < solution.getNumberOfConstraints(); i++) {
solution.setConstraint(i, Double.parseDouble(tokens[index]));
index++;
}
} catch (IOException e) {
throw new ProblemException(this, "error receiving variables from external process", e);
} catch (NumberFormatException e) {
throw new ProblemException(this, "error receiving variables from external process", e);
}
}
/**
* Serializes a variable to a string form.
*
* @param variable the variable whose value is serialized
* @return the serialized version of the variable
* @throws IOException if an error occurs during serialization
*/
private String encode(Variable variable) throws IOException {
StringBuilder sb = new StringBuilder();
if (variable instanceof RealVariable) {
RealVariable rv = (RealVariable)variable;
sb.append(rv.getValue());
} else if (variable instanceof BinaryVariable) {
BinaryVariable bv = (BinaryVariable)variable;
for (int i=0; i<bv.getNumberOfBits(); i++) {
sb.append(bv.get(i) ? "1" : "0");
}
} else if (variable instanceof Permutation) {
Permutation p = (Permutation)variable;
for (int i=0; i<p.size(); i++) {
if (i > 0) {
sb.append(',');
}
sb.append(p.get(i));
}
} else {
throw new IOException("unable to serialize variable");
}
return sb.toString();
}
}