package LBJ2.infer; import java.util.Arrays; import gurobi.GRB; import gurobi.GRBConstr; import gurobi.GRBEnv; import gurobi.GRBException; import gurobi.GRBLinExpr; import gurobi.GRBModel; import gurobi.GRBSOS; import gurobi.GRBVar; import LBJ2.classify.Score; import LBJ2.util.DVector; import LBJ2.util.OVector; /** * This is an interface to the <a href="http://www.gurobi.com">Gurobi * Optimizer</a>. Make sure the jar file is on your <code>CLASSPATH</code> * and that the Gurobi libraries are installed appropriately on your system * before attempting to compile and use this class. * * @author Nick Rizzolo **/ public class GurobiHook implements ILPSolver { /** Prints an error message and exits the JVM. */ protected static void handleException(GRBException e) { System.out.println( "Gurobi error " + e.getErrorCode() + ": " + e.getMessage()); e.printStackTrace(); System.exit(-1); } /** The model to be optimized will be associated with this environment. */ protected GRBEnv environment; /** The model to be optimized. */ protected GRBModel model; /** The model's decision variables. */ protected OVector variables; /** Contains all of the Gurobi SOS objects created for the model. */ protected OVector SOSes; /** * Whether or not the <code>GRBModel.update()</code> method needs to be * called before adding more constraints. **/ protected boolean needsUpdate; /** Whether or not the model has been solved. */ protected boolean isSolved; /** * 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; /** * The coefficients of the variables in the objective function. This is * redundant memory, and it's only being stored in the event that someone * wants to call {@link #write(StringBuffer)}. Once we get Gurobi 4.0, we * can discard of it. **/ protected DVector objectiveCoefficients; /** Create a new Gurobi hook with the default environment parameters. */ public GurobiHook() { this(ILPInference.VERBOSITY_NONE); } /** * Create a new Gurobi hook with the default environment parameters. * * @param v Setting for the {@link #verbosity} level. **/ public GurobiHook(int v) { try { environment = new GRBEnv(); environment.set(GRB.IntParam.OutputFlag, 0); } catch (GRBException e) { handleException(e); } verbosity = v; reset(); } /** * Create a new Gurobi hook with the specified environment. * * @param env An environment containing user-specified parameters. **/ public GurobiHook(GRBEnv env) { this(env, ILPInference.VERBOSITY_NONE); } /** * Create a new Gurobi hook with the specified environment. * * @param env An environment containing user-specified parameters. * @param v Setting for the {@link #verbosity} level. **/ public GurobiHook(GRBEnv env, int v) { environment = env; verbosity = v; 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() { try { model = new GRBModel(environment); } catch (GRBException e) { handleException(e); } variables = new OVector(); SOSes = new OVector(); objectiveCoefficients = new DVector(); needsUpdate = isSolved = false; } /** * 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) { try { model.set(GRB.IntAttr.ModelSense, d ? -1 : 1); } catch (GRBException e) { handleException(e); } } /** * 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 id = variables.size(); try { variables.add(model.addVar(0, 1, c, GRB.BINARY, "x_" + id)); } catch (GRBException e) { handleException(e); } // TODO: delete the line below once we get Gurobi 4.0 objectiveCoefficients.add(c); needsUpdate = true; return id; } /** * 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[] result = new int[c.length]; for (int i = 0; i < c.length; ++i) result[i] = addBooleanVariable(c[i]); double[] w = new double[c.length]; Arrays.fill(w, 1); addGreaterThanConstraint(result, w, 1); try { SOSes.add(model.addSOS(idsToVariables(result), w, GRB.SOS_TYPE1)); } catch (GRBException e) { handleException(e); } 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(Score[] c) { double[] scores = new double[c.length]; for (int i = 0; i < c.length; ++i) scores[i] = c[i].score; return addDiscreteVariable(scores); } /** * 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. **/ protected void addConstraint(int[] i, double[] a, double b, char t) { if (needsUpdate) { try { model.update(); } catch (GRBException e) { handleException(e); } needsUpdate = false; } try { int constraints = model.get(GRB.IntAttr.NumConstrs); model.addConstr(makeLinearExpression(i, a), t, b, "c_" + constraints); } catch (GRBException e) { handleException(e); } } /** * 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, GRB.EQUAL); } /** * 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, GRB.GREATER_EQUAL); } /** * 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, GRB.LESS_EQUAL); } /** * Solves the ILP problem, saving the solution internally. * * @return <code>true</code> iff a solution was found successfully. **/ public boolean solve() throws Exception { if (verbosity > ILPInference.VERBOSITY_NONE) { System.out.println(" variables: " + model.get(GRB.IntAttr.NumVars)); System.out.println(" constraints: " + model.get(GRB.IntAttr.NumConstrs)); } if (verbosity == ILPInference.VERBOSITY_HIGH) { StringBuffer buffer = new StringBuffer(); write(buffer); System.out.print(buffer); } model.optimize(); int status = model.get(GRB.IntAttr.Status); isSolved = status == GRB.OPTIMAL || status == GRB.SUBOPTIMAL; return isSolved; } /** * Tests whether the problem represented by this <code>ILPSolver</code> * instance has been solved already. **/ public boolean isSolved() { return isSolved; } /** * 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 (!isSolved) return false; try { double x = ((GRBVar) variables.get(index)).get(GRB.DoubleAttr.X); return x > 0.5; } catch (GRBException e) { handleException(e); } return false; } /** * 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() { try { if (isSolved) return model.get(GRB.DoubleAttr.ObjVal); int status = model.get(GRB.IntAttr.Status); if (status == GRB.INFEASIBLE || status == GRB.INF_OR_UNBD || status == GRB.UNBOUNDED) return model.get(GRB.IntAttr.ModelSense) == -1 ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY; } catch (GRBException e) { handleException(e); } return 0; } /** * Given an array of variable indexes, this method returns the * corresponding Gurobi variable objects in an array. * * @param ids The array of variable indexes. * @return The corresponding Gurobi variable objects. **/ protected GRBVar[] idsToVariables(int[] ids) { GRBVar[] result = new GRBVar[ids.length]; for (int i = 0; i < ids.length; i++) result[i] = (GRBVar) variables.get(ids[i]); return result; } /** * Creates a Gurobi linear expression object representing the dot product * of the variables with the specified indexes and the specified * coefficients. * * @param ids The indexes of the variables. * @param c The corresponding coefficients. * @return A Gurobi linear expression representing the dot product. **/ protected GRBLinExpr makeLinearExpression(int[] ids, double[] c) { try { GRBLinExpr expr = new GRBLinExpr(); expr.addTerms(c, idsToVariables(ids)); return expr; } catch (GRBException e) { handleException(e); } return null; } /** * 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) { try { model.update(); if (model.get(GRB.IntAttr.ModelSense) == -1) buffer.append("max"); else buffer.append("min"); // Using this bit of code until we get Gurobi 4.0 or higher. for (int i = 0; i < objectiveCoefficients.size(); ++i) { double c = objectiveCoefficients.get(i); buffer.append(" "); if (c >= 0) buffer.append("+"); buffer.append(c); buffer.append(" x_"); buffer.append(i); } /* This code should work once we have Gurobi 4.0 or higher. Then we * don't have to redundantly store objectiveCoefficients. GRBLinExpr objective = (GRBLinExpr) model.getObjective(); int objectiveSize = objective.size(); for (int i = 0; i < objectiveSize; ++i) { double c = objective.getCoeff(i); buffer.append(" "); if (c >= 0) buffer.append("+"); buffer.append(c); buffer.append(" "); buffer.append(objective.getVar(i).get(GRB.StringAttr.VarName)); } */ buffer.append("\n"); int SOSesSize = SOSes.size(); for (int i = 0; i < SOSesSize; ++i) { GRBSOS sos = (GRBSOS) SOSes.get(i); int[] type = new int[1]; int size = model.getSOS(sos, null, null, type); GRBVar[] sosVariables = new GRBVar[size]; model.getSOS(sos, sosVariables, new double[size], type); buffer.append(" atmost 1 of (x in {"); for (int j = 0; j < size; ++j) { buffer.append(sosVariables[j].get(GRB.StringAttr.VarName)); if (j + 1 < size) buffer.append(", "); } buffer.append("}) (x > 0)\n"); } GRBConstr[] constraints = model.getConstrs(); for (int i = 0; i < constraints.length; ++i) { GRBLinExpr row = model.getRow(constraints[i]); int rowSize = row.size(); buffer.append(" "); for (int j = 0; j < rowSize; ++j) { double c = row.getCoeff(j); buffer.append(" "); if (c >= 0) buffer.append("+"); buffer.append(c); buffer.append(" "); buffer.append(row.getVar(j).get(GRB.StringAttr.VarName)); } char type = constraints[i].get(GRB.CharAttr.Sense); if (type == GRB.LESS_EQUAL) buffer.append(" <= "); else if (type == GRB.GREATER_EQUAL) buffer.append(" >= "); else buffer.append(" = "); buffer.append(constraints[i].get(GRB.DoubleAttr.RHS)); buffer.append("\n"); } } catch (GRBException e) { handleException(e); } } }