package LBJ2.infer; import java.util.*; import com.dashoptimization.*; import LBJ2.util.DVector; import LBJ2.util.DVector2D; import LBJ2.util.IVector; import LBJ2.util.IVector2D; /** * This interface to Xpress-MP from * <a href="http://www.dashoptimization.com">Dash Optimization</a> is * designed to work with Xpress-Optimizer 15.25.02. Make sure the file * <code>xprs.jar</code> is on your <code>CLASSPATH</code> and that the * Xpress-MP libraries are installed appropriately on your system before * attempting to compile and use this class. * * <p> * <i><b>Note to users:</b></i> When invoking a classifier in your * application that calls this algorithm to do inference, files whose names * start with <code>xmp</code> and end with <code>.sol</code> or * <code>.glb</code> may be created in the directory from which you invoked * your application. These files are created by the Xpress-MP library, and I * haven't found a way to avoid it. * * <p> * Xpress-MP represents an ILP problem as a coefficient matrix in which rows * represent constraints and columns represent variables. In the * documentation below, the terms "row" and "constraint" are used * interchangeably, as are the terms "column" and "variable". * * @author Nick Rizzolo **/ public class XpressMPHook implements ILPSolver { /** * The elements of this array are suitable for use as elements of the * <code>qrtype</code> array parameter of the <code>XPRSloadglobal</code> * method. **/ private static final byte[] typeCodes = { 'L', 'E', 'G' }; /** Index into the {@link #typeCodes} array representing "less than". */ private static final int ROWLESSTHAN = 0; /** Index into the {@link #typeCodes} array representing "equal". */ private static final int ROWEQUAL = 1; /** Index into the {@link #typeCodes} array representing "greater than". */ private static final int ROWGREATERTHAN = 2; /** Keeps track of the next ID number for an instance of this class. */ private static int nextID = 0; /** The ID of an instance is used in the name of the solution file. */ private int problemID; /** * Verbosity level. {@link ILPInference#VERBOSITY_NONE} produces no * incidental output. If set to {@link ILPInference#VERBOSITY_LOW}, only * variable and constraint counts are reported on <code>STDOUT</code>. If * set to {@link ILPInference#VERBOSITY_HIGH}, a textual representation of * the entire optimization problem is also generated on * <code>STDOUT</code>. **/ protected int verbosity; /** * Xpress-MP's ILP problem representation is stored here for easy access to * the solution values of variables. **/ private XPRSprob problem; /** The values of the primal variables after solving the problem. */ private double[] x; /** * Remembers whether the objective function should be maximized or * minimzed. **/ private boolean maximize; /** Stores the objective function's coefficients. */ private DVector objective; /** Stores the types of each constraint. */ private IVector rowTypes; /** Stores the constant bounds on each constraint. */ private DVector rhs; /** * Contains one vector for each added variable representing a list of row * indexes at which the associated variable has a non-zero coefficient. **/ private XMPIVector2D rowIndexes; /** * Contains one vector for each added variable representing a list of * non-zero coefficients associated with the variable in the various * constraints in the problem. These coefficients correspond do the row * indexes at the same locations in the 2D {@link #rowIndexes} array. **/ private XMPDVector2D coefficients; /** * Contains one vector for each SOS1 in the ILP problem representing the * list of columns involved in the set. **/ private XMPIVector2D setColumns; /** Default constructor. */ public XpressMPHook() { this(ILPInference.VERBOSITY_NONE); } /** * Use this constructor to control printing of the problem's representation * before its solution is carried out. * * @param v Setting for the {@link #verbosity} level. **/ public XpressMPHook(int v) { verbosity = v; if (nextID == 0) { try { XPRS.init(); } catch (Exception e) { System.err.println("Couldn't initialize Xpress-MP: " + e); System.exit(1); } Runtime.getRuntime().addShutdownHook( new Thread() { public void run() { XPRS.free(); } }); } problemID = nextID++; reset(); } /** * This method clears the all constraints and variables out of the ILP * solver's problem representation, bringing the <code>ILPSolver</code> * instance back to the state it was in when first constructed. **/ public void reset() { if (problem != null) { problemID = nextID++; problem = null; } x = null; objective = new DVector(); rowTypes = new IVector(); rhs = new DVector(); rowIndexes = new XMPIVector2D(); coefficients = new XMPDVector2D(); setColumns = new XMPIVector2D(); } /** * Sets the direction of the objective function. * * @param d <code>true</code> if the objective function is to be * maximized. **/ public void setMaximize(boolean d) { maximize = d; } /** * Adds a new Boolean variable (an integer variable constrained to take * either the value 0 or the value 1) with the specified coefficient in the * objective function to the problem. * * @param c The objective function coefficient for the new Boolean * variable. * @return The index of the created variable. **/ public int addBooleanVariable(double c) { int result = objective.size(); objective.add(c); return result; } /** * Adds a general, multi-valued discrete variable, which is implemented as * a set of Boolean variables, one per value of the discrete variable, with * exactly one of those variables set <code>true</code> at any given time. * * @param c The objective function coefficients for the new Boolean * variables. * @return The indexes of the newly created variables. **/ public int[] addDiscreteVariable(double[] c) { int sosIndex = setColumns.size(); int[] result = new int[c.length]; int variables = objective.size(), sosSize = setColumns.size(sosIndex); for (int i = 0; i < c.length; ++i) { result[i] = variables + i; setColumns.set(sosIndex, sosSize + i, variables + i); objective.add(c[i]); } double[] a = new double[c.length]; Arrays.fill(a, 1); addGreaterThanConstraint(result, a, 1); return result; } /** * Adds a general, multi-valued discrete variable, which is implemented as * a set of Boolean variables, one per value of the discrete variable, with * exactly one of those variables set <code>true</code> at any given time. * * @param c An array of {@link LBJ2.classify.Score}s containing the * objective function coefficients for the new Boolean variables. * @return The indexes of the newly created variables. **/ public int[] addDiscreteVariable(LBJ2.classify.Score[] c) { int sosIndex = setColumns.size(); int[] result = new int[c.length]; int variables = objective.size(), sosSize = setColumns.size(sosIndex); for (int i = 0; i < c.length; ++i) { result[i] = variables + i; setColumns.set(sosIndex, sosSize + i, variables + i); objective.add(c[i].score); } double[] a = new double[c.length]; Arrays.fill(a, 1); addGreaterThanConstraint(result, a, 1); return result; } /** * Adds a new constraint to the problem with the specified type. This * method is called by all the other <code>add*Constraint()</code> methods. * * @param i The indexes of the variables with non-zero coefficients. * @param a The coefficients of the variables with the given indexes. * @param b The new constraint will enforce (in)equality with this * constant. * @param t The type of linear inequality constraint to add. **/ public void addConstraint(int[] i, double[] a, double b, int t) { int newRow = rowTypes.size(); rowTypes.add(t); rhs.add(b); for (int j = 0; j < i.length; ++j) { int s = rowIndexes.size(i[j]); rowIndexes.set(i[j], s, newRow); coefficients.set(i[j], s, a[j]); } } /** * Adds a new fixed constraint to the problem. The two array arguments * must be the same length, as their elements correspond to each other. * Variables whose coefficients are zero need not be mentioned. Variables * that are mentioned must have previously been added via * {@link #addBooleanVariable(double)} or * {@link #addDiscreteVariable(double[])}. The resulting constraint has * the form: * <blockquote> <code>x<sub>i</sub> * a = b</code> </blockquote> * where <code>x<sub>i</sub></code> represents the inference variables * whose indexes are contained in the array <code>i</code> and * <code>*</code> represents dot product. * * @param i The indexes of the variables with non-zero coefficients. * @param a The coefficients of the variables with the given indexes. * @param b The new constraint will enforce equality with this constant. **/ public void addEqualityConstraint(int[] i, double[] a, double b) { addConstraint(i, a, b, ROWEQUAL); } /** * Adds a new lower bounded constraint to the problem. The two array * arguments must be the same length, as their elements correspond to each * other. Variables whose coefficients are zero need not be mentioned. * Variables that are mentioned must have previously been added via * {@link #addBooleanVariable(double)} or * {@link #addDiscreteVariable(double[])}. The resulting constraint has * the form: * <blockquote> <code>x<sub>i</sub> * a >= b</code> </blockquote> * where <code>x<sub>i</sub></code> represents the inference variables * whose indexes are contained in the array <code>i</code> and * <code>*</code> represents dot product. * * @param i The indexes of the variables with non-zero coefficients. * @param a The coefficients of the variables with the given indexes. * @param b The lower bound for the new constraint. **/ public void addGreaterThanConstraint(int[] i, double[] a, double b) { addConstraint(i, a, b, ROWGREATERTHAN); } /** * Adds a new upper bounded constraint to the problem. The two array * arguments must be the same length, as their elements correspond to each * other. Variables whose coefficients are zero need not be mentioned. * Variables that are mentioned must have previously been added via * {@link #addBooleanVariable(double)} or * {@link #addDiscreteVariable(double[])}. The resulting constraint has * the form: * <blockquote> <code>x<sub>i</sub> * a <= b</code> </blockquote> * where <code>x<sub>i</sub></code> represents the inference variables * whose indexes are contained in the array <code>i</code> and * <code>*</code> represents dot product. * * @param i The indexes of the variables with non-zero coefficients. * @param a The coefficients of the variables with the given indexes. * @param b The upper bound for the new constraint. **/ public void addLessThanConstraint(int[] i, double[] a, double b) { addConstraint(i, a, b, ROWLESSTHAN); } /** * Solves the ILP problem, saving the solution internally. * * @return <code>true</code> iff a solution was found successfully. **/ public boolean solve() throws Exception { int ncol = objective.size(); int nrow = rowTypes.size(); byte[] qrtype = new byte[nrow]; for (int i = 0; i < nrow; ++i) qrtype[i] = typeCodes[rowTypes.get(i)]; double[] b = rhs.toArray(); double[] obj = objective.toArray(); int[] mstart = rowIndexes.indexMap(); int[] mrwind = rowIndexes.flatten(); double[] dmatval = coefficients.flatten(); double[] dlb = new double[ncol]; double[] dub = new double[ncol]; Arrays.fill(dub, 1); int nsets = setColumns.size(); byte[] qgtype = new byte[ncol]; int[] mgcols = new int[ncol]; Arrays.fill(qgtype, (byte) 'B'); for (int i = 0; i < ncol; ++i) mgcols[i] = i; byte[] qstype = new byte[nsets]; Arrays.fill(qstype, (byte) '1'); int[] msstart = setColumns.indexMap(); int[] mscols = setColumns.flatten(); double[] dref = new double[mscols.length]; for (int i = 0; i < dref.length; ++i) dref[i] = i + 1; if (verbosity > ILPInference.VERBOSITY_NONE) { System.out.println(" variables: " + obj.length); System.out.println(" constraints: " + b.length); } if (verbosity == ILPInference.VERBOSITY_HIGH) { StringBuffer buffer = new StringBuffer(); write(buffer); System.out.print(buffer); /* System.out.println( "\nloadGlobal(\n" + " \"\",\n" + " " + ncol + ",\n" + " " + nrow + ",\n" + " " + arrayToString(qrtype) + ",\n" + " " + arrayToString(b) + ",\n" + " null,\n" + " " + arrayToString(obj) + ",\n" + " " + arrayToString(mstart) + ",\n" + " null,\n" + " " + arrayToString(mrwind) + ",\n" + " " + arrayToString(dmatval) + ",\n" + " " + arrayToString(dlb) + ",\n" + " " + arrayToString(dub) + ",\n" + " " + ncol + ",\n" + " " + nsets + ",\n" + " " + arrayToString(qgtype) + ",\n" + " " + arrayToString(mgcols) + ",\n" + " null,\n" + " " + arrayToString(qstype) + ",\n" + " " + arrayToString(msstart) + ",\n" + " " + arrayToString(mscols) + ",\n" + " " + arrayToString(dref) + ")"); */ } problem = new XPRSprob(); try { problem.setIntControl(XPRS.BAROUTPUT, 0); problem.setIntControl(XPRS.MIPLOG, 0); problem.setIntControl(XPRS.OUTPUTLOG, 0); } catch (Exception e) { System.err.println("Can't set Xpress-MP output control: " + e); System.err.println("Xpress-MP will probably be very verbose."); } String id = "" + problemID; while (id.length() < 5) id = "0" + id; id = "xmp" + id; problem.loadGlobal(id, ncol, nrow, qrtype, b, null, obj, mstart, null, mrwind, dmatval, dlb, dub, ncol, nsets, qgtype, mgcols, null, qstype, msstart, mscols, dref); if (maximize) problem.maxim("g"); else problem.minim("g"); int status = problem.getIntAttrib(XPRS.MIPSTATUS); return status == XPRS.MIP_OPTIMAL || status == XPRS.MIP_SOLUTION; } /** * Generates a string representation of the given array. * * @param a The array to represent as a string. * @return A string representation of <code>a</code>. **/ private static String arrayToString(byte[] a) { String result = "["; for (int i = 0; i < a.length; ++i) { result += " " + (char) a[i]; if (i < a.length - 1) result += ","; } result += " ]"; return result; } /** * Generates a string representation of the given array. * * @param a The array to represent as a string. * @return A string representation of <code>a</code>. **/ private static String arrayToString(int[] a) { String result = "["; for (int i = 0; i < a.length; ++i) { result += " " + a[i]; if (i < a.length - 1) result += ","; } result += " ]"; return result; } /** * Generates a string representation of the given array. * * @param a The array to represent as a string. * @return A string representation of <code>a</code>. **/ private static String arrayToString(double[] a) { String result = "["; for (int i = 0; i < a.length; ++i) { result += " " + a[i]; if (i < a.length - 1) result += ","; } result += " ]"; return result; } /** * Tests whether the problem represented by this <code>ILPSolver</code> * instance has been solved already. **/ public boolean isSolved() { return problem != null; } /** * When the problem has been solved, use this method to retrieve the value * of any Boolean inference variable. The result of this method is * undefined when the problem has not yet been solved. * * @param index The index of the variable whose value is requested. * @return The value of the variable. **/ public boolean getBooleanValue(int index) { if (x == null) { x = new double[objective.size()]; try { problem.getSol(x, null, null, null); } catch (Exception e) { System.err.println("Couldn't get Xpress-MP problem solution: " + e); System.exit(1); } } return x[index] > 0.5; } /** * When the problem has been solved, use this method to retrieve the value * of the objective function at the solution. The result of this method is * undefined when the problem has not yet been solved. If the problem had * no feasible solutions, negative (positive, respectively) infinity will * be returned if maximizing (minimizing). * * @return The value of the objective function at the solution. **/ public double objectiveValue() { int status = problem.getIntAttrib(XPRS.MIPSTATUS); if (status == XPRS.MIP_INFEAS) return maximize ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY; if (status == XPRS.MIP_OPTIMAL || status == XPRS.MIP_SOLUTION) return problem.getDblAttrib(XPRS.MIPOBJVAL); return 0; } /** * Creates a textual representation of the ILP problem in an algebraic * notation. * * @param buffer The created textual representation will be appended here. **/ public void write(StringBuffer buffer) { if (maximize) buffer.append("max"); else buffer.append("min"); int variables = objective.size(); for (int i = 0; i < variables; ++i) { double c = objective.get(i); buffer.append(" "); if (c >= 0) buffer.append("+"); buffer.append(c); buffer.append(" x_"); buffer.append(i); } buffer.append("\n"); int sosSets = setColumns.size(); for (int i = 0; i < sosSets; ++i) { buffer.append(" atmost 1 of (x in {"); int sosSize = setColumns.size(i); for (int j = 0; j < sosSize; ++j) { buffer.append("x_"); buffer.append(setColumns.get(i, j)); if (j + 1 < sosSize) buffer.append(", "); } buffer.append("}) (x > 0)\n"); } int constraints = rowTypes.size(); StringBuffer[] rowBuffers = new StringBuffer[constraints]; for (int i = 0; i < rowBuffers.length; ++i) { rowBuffers[i] = new StringBuffer(); rowBuffers[i].append(" "); } for (int i = 0; i < variables; ++i) { int rows = rowIndexes.size(i); for (int j = 0; j < rows; ++j) { int r = rowIndexes.get(i, j); double c = coefficients.get(i, j); rowBuffers[r].append(" "); if (c >= 0) rowBuffers[r].append("+"); rowBuffers[r].append(c); rowBuffers[r].append(" x_"); rowBuffers[r].append(i); } } for (int i = 0; i < constraints; ++i) { int t = rowTypes.get(i); if (typeCodes[t] == 'L') rowBuffers[i].append(" <= "); else if (typeCodes[t] == 'E') rowBuffers[i].append(" = "); else if (typeCodes[t] == 'G') rowBuffers[i].append(" >= "); rowBuffers[i].append(rhs.get(i)); } for (int i = 0; i < rowBuffers.length; ++i) { buffer.append(rowBuffers[i]); buffer.append("\n"); } } /** * Gives <code>DVector2D</code>s the ability to {@link #flatten() flatten} * and to produce {@link #indexMap() index maps} into the flattened * representation. * * @author Nick Rizzolo **/ private static class XMPDVector2D extends DVector2D { /** Creates the vector. */ public XMPDVector2D() { } /** * The array returned by this method contains indexes into the array * returned by {@link #flatten()} indicating where each subvector began * and ended before they were flattened. **/ public int[] indexMap() { int size = sizes.size(); int[] result = new int[size + 1]; for (int i = 0; i < size; ++i) result[i + 1] = result[i] + sizes.get(i); return result; } /** * Returns all elements in all vectors in a single, one dimensional array * in the appropriate order. **/ public double[] flatten() { int size = sizes.size(); int totalElements = 0; for (int i = 0; i < size; ++i) totalElements += sizes.get(i); double[] result = new double[totalElements]; totalElements = 0; for (int i = 0; i < size; ++i) { int s = sizes.get(i); System.arraycopy(vector[i], 0, result, totalElements, s); totalElements += s; } return result; } } /** * Gives <code>IVector2D</code>s the ability to {@link #flatten() flatten} * and to produce {@link #indexMap() index maps} into the flattened * representation. * * @author Nick Rizzolo **/ private static class XMPIVector2D extends IVector2D { /** Creates the vector. */ public XMPIVector2D() { } /** * The array returned by this method contains indexes into the array * returned by {@link #flatten()} indicating where each subvector began * and ended before they were flattened. **/ public int[] indexMap() { int size = sizes.size(); int[] result = new int[size + 1]; for (int i = 0; i < size; ++i) result[i + 1] = result[i] + sizes.get(i); return result; } /** * Returns all elements in all vectors in a single, one dimensional array * in the appropriate order. **/ public int[] flatten() { int size = sizes.size(); int totalElements = 0; for (int i = 0; i < size; ++i) totalElements += sizes.get(i); int[] result = new int[totalElements]; totalElements = 0; for (int i = 0; i < size; ++i) { int s = sizes.get(i); System.arraycopy(vector[i], 0, result, totalElements, s); totalElements += s; } return result; } } }