/* 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.algorithm.pisa; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import org.apache.commons.lang3.ArrayUtils; import org.moeaframework.algorithm.AbstractAlgorithm; import org.moeaframework.algorithm.AlgorithmException; import org.moeaframework.core.Initialization; import org.moeaframework.core.NondominatedPopulation; import org.moeaframework.core.PRNG; import org.moeaframework.core.Problem; import org.moeaframework.core.Settings; import org.moeaframework.core.Solution; import org.moeaframework.core.Variation; import org.moeaframework.core.operator.RandomInitialization; import org.moeaframework.util.TypedProperties; import org.moeaframework.util.io.FileUtils; import org.moeaframework.util.io.RedirectStream; /** * Algorithm for interfacing with an external PISA selector. The PISA * framework is a platform and programming language independent interface for * search algorithms. PISA separates search algorithms into <em>selector</em>s, * describing the optimization algorithm, and <em>variator</em>s, describing * the optimization problem. PISA uses a file-based communication channel * between selectors and variators, which may result in excessive * communication costs, file system bottlenecks and file name collisions. * See the PISA homepage for detailed instructions. * <p> * Note that some PISA selectors parse the command line arguments using sscanf * <pre> * sscanf(argv[2], "%s", filenamebase); * </pre> * On some operating systems, this will not work if the files used by PISA * contain whitespace in the filename. It may be necessary to set the JVM * property {@code java.io.tmpdir} to a folder with no whitespace in the * filename. * * @see <a href="http://www.tik.ee.ethz.ch/pisa/">PISA Homepage</a> */ public class PISAAlgorithm extends AbstractAlgorithm { /** * The file prefix used when creating the PISA communication files. */ private final String filePrefix; /** * The {@link ProcessBuilder} to start the selector process. */ private final ProcessBuilder selector; /** * The shared state. */ private final State state; /** * The population size. */ private final int alpha; /** * The number of parents. */ private final int mu; /** * The number of offspring. */ private final int lambda; /** * Mapping from identifiers to solutions. */ private final Map<Integer, Solution> solutions; /** * The variation operator. */ private final Variation variation; /** * Constructs an adapter for a PISA selector. * * @param name the name of the PISA selector * @param problem the problem being solved * @param variation the variation operator * @param properties additional properties for the PISA selector * configuration file * @throws IOException if an I/O error occurred */ public PISAAlgorithm(String name, Problem problem, Variation variation, Properties properties) throws IOException { super(problem); this.variation = variation; TypedProperties typedProperties = new TypedProperties(properties); String command = Settings.getPISACommand(name); String configuration = Settings.getPISAConfiguration(name); int pollRate = Settings.getPISAPollRate(); if (command == null) { throw new IllegalArgumentException("missing command"); } //This is slightly unsafe since the actual files used by //PISA add the arc, cfg, ini, sel and sta extensions. This //dependency on files for communication is part of PISA's //design. filePrefix = File.createTempFile("pisa", "").getCanonicalPath(); //ensure the seed property is set if (!properties.containsKey("seed")) { properties.setProperty("seed", Integer.toString(PRNG.nextInt())); } //write the configuration file if one is not specified if (configuration == null) { PrintWriter writer = null; configuration = new File(filePrefix + "par").getCanonicalPath(); try { writer = new PrintWriter(new BufferedWriter(new FileWriter( configuration))); for (String parameter : Settings.getPISAParameters(name)) { writer.print(parameter); writer.print(' '); writer.println(typedProperties.getString(parameter, Settings.getPISAParameterDefaultValue(name, parameter))); } } finally { if (writer != null) { writer.close(); } } } //construct the command line call to start the PISA selector selector = new ProcessBuilder(ArrayUtils.addAll( Settings.parseCommand(command), configuration, filePrefix, Double.toString(pollRate/(double)1000))); //ensure population size is a multiple of the # of parents int populationSize = (int)typedProperties.getDouble("populationSize", 100); while (populationSize % variation.getArity() != 0) { populationSize++; } //configure the remaining options alpha = populationSize; mu = (int)typedProperties.getDouble("mu", alpha); lambda = (int)typedProperties.getDouble("lambda", alpha); state = new State(new File(filePrefix + "sta")); solutions = new HashMap<Integer, Solution>(); } @Override public void initialize() { super.initialize(); try { configure(); state.set(0); state0(); state.set(1); Process process = selector.start(); RedirectStream.redirect(process.getInputStream(), System.out); RedirectStream.redirect(process.getErrorStream(), System.err); } catch (Exception e) { throw new AlgorithmException(this, e); } } @Override public void terminate() { super.terminate(); //guard against attempting to access the non-existent state file if //this algorithm is not yet initialized if (!isInitialized()) { return; } try { int currentState = state.get(); while (true) { if (currentState == 2) { state.set(4); state4(); state.set(5); break; } else if ((currentState == 4) || (currentState == 7)) { break; } else if (currentState < 8) { currentState = state.waitWhile(currentState); } else { throw new AlgorithmException(this, "restart not supported"); } } } catch (Exception e) { throw new AlgorithmException(this, e); } } @Override public void iterate() { try { int currentState = state.get(); while (true) { if (currentState == 2) { state2(); state.set(3); break; } else if ((currentState == 4) || (currentState == 7)) { terminate(); break; } else if (currentState < 8) { currentState = state.waitWhile(currentState); } else { throw new AlgorithmException(this, "restart not supported"); } } } catch (Exception e) { throw new AlgorithmException(this, e); } } @Override public NondominatedPopulation getResult() { NondominatedPopulation result = new NondominatedPopulation(); result.addAll(solutions.values()); return result; } /** * Clears the specified file. Some selector implementations may block until * the {@code sel} and {@code arc} files are cleared. * * @param file the file to clear * @throws IOException if an I/O error occurred */ private void clearFile(File file) throws IOException { PrintWriter writer = null; try { writer = new PrintWriter(new BufferedWriter(new FileWriter(file))); writer.println('0'); } finally { if (writer != null) { writer.close(); } } } /** * Updates the population, retaining only those solutions with the * specified identifiers. * * @param ids the identifiers to retain */ private void updatePopulation(int[] ids) { List<Integer> archivedIds = new ArrayList<Integer>(); for (int id : ids) { archivedIds.add(id); } solutions.keySet().retainAll(archivedIds); } /** * Adds the specified solution to the population, returning its assigned * identifier. * * @param solution the solution * @return the assigned identifier for the solution */ private int addToPopulation(Solution solution) { int id = nextFreeId(); solutions.put(id, solution); return id; } /** * Returns the next available identifier. * * @return the next available identifier */ private int nextFreeId() { int id = 0; Set<Integer> ids = solutions.keySet(); while (ids.contains(id)) { id++; } return id; } /** * The commands to execute when in state 0. * * @throws IOException if an I/O error occurred */ private void state0() throws IOException { Initialization initialization = new RandomInitialization(problem, alpha); Solution[] initialPopulation = initialization.initialize(); int[] initialIds = new int[alpha]; evaluateAll(initialPopulation); for (int i = 0; i < alpha; i++) { initialIds[i] = addToPopulation(initialPopulation[i]); } writePopulation(new File(filePrefix + "ini"), initialIds); } /** * The commands to execute when in state 4. * * @throws IOException if an I/O error occurred */ private void state4() throws IOException { int[] archivedIds = readList(new File(filePrefix + "arc")); // if (archivedIds.length != alpha) { // throw new IOException("invalid archive length"); // } updatePopulation(archivedIds); } /** * The commands to execute when in state 2. * * @throws IOException if an I/O error occurred */ private void state2() throws IOException { int[] selectionIds = readList(new File(filePrefix + "sel")); int[] archivedIds = readList(new File(filePrefix + "arc")); int[] variationIds = new int[lambda]; if (selectionIds.length != mu) { throw new IOException("invalid selection length"); } //if (archivedIds.length != alpha) { // System.out.println(archivedIds.length + " " + alpha); // throw new IOException("invalid archive length"); //} updatePopulation(archivedIds); clearFile(new File(filePrefix + "sel")); clearFile(new File(filePrefix + "arc")); List<Solution> offspring = new ArrayList<Solution>(); for (int i = 0; i < mu; i += variation.getArity()) { Solution[] parents = new Solution[variation.getArity()]; for (int j = 0; j < variation.getArity(); j++) { parents[j] = solutions.get(selectionIds[i+j]); } offspring.addAll(Arrays.asList(variation.evolve(parents))); } if (offspring.size() != lambda) { throw new IOException("invalid variation length"); } evaluateAll(offspring); for (int i = 0; i < lambda; i++) { variationIds[i] = addToPopulation(offspring.get(i)); } writePopulation(new File(filePrefix + "var"), variationIds); } /** * Reads either the {@code sel} or {@code arc} files, returning the list * of identifiers contained in the file. * * @param file the {@code sel} or {@code arc} file * @return the list of identifiers contained in the file * @throws IOException if an I/O error occurred */ private int[] readList(File file) throws IOException { BufferedReader reader = null; String line = null; try { reader = new BufferedReader(new FileReader(file)); line = reader.readLine(); if (line == null) { throw new IOException("unexpected end of file"); } int size = Integer.parseInt(line); int[] result = new int[size]; for (int i = 0; i < size; i++) { line = reader.readLine(); if (line == null) { throw new IOException("unexpected end of file"); } result[i] = Integer.parseInt(line); } // sanity check if (!"END".equals(reader.readLine())) { throw new IOException("expected END on last line"); } return result; } finally { if (reader != null) { reader.close(); } } } /** * Writes either the {@code ini} or {@code var} file with the specified * identifiers. * * @param file the {@code ini} or {@code var} file * @param ids the identifiers of solutions written to the file * @throws IOException if an I/O error occurred */ private void writePopulation(File file, int[] ids) throws IOException { PrintWriter writer = null; try { writer = new PrintWriter(new BufferedWriter(new FileWriter(file))); writer.println(ids.length * (problem.getNumberOfObjectives() + 1)); for (int i = 0; i < ids.length; i++) { writer.print(ids[i]); for (int j = 0; j < problem.getNumberOfObjectives(); j++) { writer.print(' '); writer.print(solutions.get(ids[i]).getObjective(j)); } writer.println(); } writer.println("END"); } finally { if (writer != null) { writer.close(); } } } /** * Removes any existing PISA communication files, creating a new {@code cfg} * file with the appropriate settings. * * @throws IOException if an I/O error occurred */ private void configure() throws IOException { FileUtils.delete(new File(filePrefix + "arc")); FileUtils.delete(new File(filePrefix + "cfg")); FileUtils.delete(new File(filePrefix + "ini")); FileUtils.delete(new File(filePrefix + "sel")); FileUtils.delete(new File(filePrefix + "sta")); PrintWriter writer = null; try { writer = new PrintWriter(new BufferedWriter(new FileWriter( new File(filePrefix + "cfg")))); writer.print("alpha "); writer.println(alpha); writer.print("mu "); writer.println(mu); writer.print("lambda "); writer.println(lambda); writer.print("dim "); writer.print(problem.getNumberOfObjectives()); } finally { if (writer != null) { writer.close(); } } } }